Remove FTUE onboarding as it is incompatible with SSO/OIDC (#28943)

* Remove FTUE onboarding as it is incompatible with SSO/OIDC

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove stale screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-01-14 13:35:20 +00:00
committed by GitHub
parent 2559cba482
commit 60f70b93e0
49 changed files with 16 additions and 2711 deletions

View File

@@ -35,7 +35,6 @@ import { UIComponent } from "../../settings/UIFeature";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import PosthogTrackers from "../../PosthogTrackers";
import PageType from "../../PageTypes";
import { UserOnboardingButton } from "../views/user-onboarding/UserOnboardingButton";
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
interface IProps {
@@ -398,10 +397,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{shouldShowComponent(UIComponent.FilterContainer) && this.renderSearchDialExplore()}
{this.renderBreadcrumbs()}
{!this.props.isMinimized && <RoomListHeader onVisibilityChange={this.refreshStickyHeaders} />}
<UserOnboardingButton
selected={this.props.pageType === PageType.HomePage}
minimized={this.props.isMinimized}
/>
<nav className="mx_LeftPanel_roomListWrapper" aria-label={_t("common|rooms")}>
<div
className={roomListClasses}

View File

@@ -61,7 +61,7 @@ import { TimelineRenderingType } from "../../contexts/RoomContext";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
import HomePage from "./HomePage";
import { PipContainer } from "./PipContainer";
import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushRules";
import { ConfigOptions } from "../../SdkConfig";
@@ -678,7 +678,7 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
case PageTypes.HomePage:
pageElement = <UserOnboardingPage justRegistered={this.props.justRegistered} />;
pageElement = <HomePage justRegistered={this.props.justRegistered} />;
break;
case PageTypes.UserView:

View File

@@ -55,7 +55,6 @@ import { FontWatcher } from "../../settings/watchers/FontWatcher";
import { storeRoomAliasInCache } from "../../RoomAliasCache";
import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager";
import { UseCase } from "../../settings/enums/UseCase";
import type LoggedInViewType from "./LoggedInView";
import LoggedInView from "./LoggedInView";
import { Action } from "../../dispatcher/actions";
@@ -114,7 +113,6 @@ import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { TimelineRenderingType } from "../../contexts/RoomContext";
import { UseCaseSelection } from "../views/elements/UseCaseSelection";
import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
@@ -866,8 +864,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER &&
this.state.view !== Views.COMPLETE_SECURITY &&
this.state.view !== Views.E2E_SETUP &&
this.state.view !== Views.USE_CASE_SELECTION
this.state.view !== Views.E2E_SETUP
) {
this.onLoggedIn();
}
@@ -1359,12 +1356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
await this.onShowPostLoginScreen();
}
private async onShowPostLoginScreen(useCase?: UseCase): Promise<void> {
if (useCase) {
PosthogAnalytics.instance.setProperty("ftueUseCaseSelection", useCase);
SettingsStore.setValue("FTUE.useCaseSelection", null, SettingLevel.ACCOUNT, useCase);
}
private async onShowPostLoginScreen(): Promise<void> {
this.setStateForNewView({ view: Views.LOGGED_IN });
// If a specific screen is set to be shown after login, show that above
// all else, as it probably means the user clicked on something already.
@@ -2010,33 +2002,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// complete security / e2e setup has finished
private onCompleteSecurityE2eSetupFinished = (): void => {
if (MatrixClientPeg.currentUserIsJustRegistered() && SettingsStore.getValue("FTUE.useCaseSelection") === null) {
this.setStateForNewView({ view: Views.USE_CASE_SELECTION });
// Listen to changes in settings and hide the use case screen if appropriate - this is necessary because
// account settings can still be changing at this point in app init (due to the initial sync being cached,
// then subsequent syncs being received from the server)
//
// This seems unlikely for something that should happen directly after registration, but if a user does
// their initial login on another device/browser than they registered on, we want to avoid asking this
// question twice
//
// initPosthogAnalyticsToast pioneered this technique, were just reusing it here.
SettingsStore.watchSetting(
"FTUE.useCaseSelection",
null,
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
if (newValue !== null && this.state.view === Views.USE_CASE_SELECTION) {
this.onShowPostLoginScreen();
}
},
);
} else {
// This is async but we makign this function async to wait for it isn't useful
this.onShowPostLoginScreen().catch((e) => {
logger.error("Exception showing post-login screen", e);
});
}
// This is async but we making this function async to wait for it isn't useful
this.onShowPostLoginScreen().catch((e) => {
logger.error("Exception showing post-login screen", e);
});
};
private getFragmentAfterLogin(): string {
@@ -2156,8 +2125,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
fragmentAfterLogin={fragmentAfterLogin}
/>
);
} else if (this.state.view === Views.USE_CASE_SELECTION) {
view = <UseCaseSelection onFinished={(useCase): Promise<void> => this.onShowPostLoginScreen(useCase)} />;
} else if (this.state.view === Views.LOCK_STOLEN) {
view = <SessionLockStolenView />;
} else {

View File

@@ -17,7 +17,7 @@ import RightPanel from "./RightPanel";
import Spinner from "../views/elements/Spinner";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
import HomePage from "./HomePage.tsx";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
@@ -93,7 +93,7 @@ export default class UserView extends React.Component<IProps, IState> {
defaultSize={420}
analyticsRoomType="user_profile"
>
<UserOnboardingPage />
<HomePage />
</MainSplit>
);
} else {

View File

@@ -1,136 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { FC } from "react";
import { Icon as FDroidBadge } from "../../../../res/img/badges/f-droid.svg";
import { Icon as GooglePlayBadge } from "../../../../res/img/badges/google-play.svg";
import { Icon as IOSBadge } from "../../../../res/img/badges/ios.svg";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import AccessibleButton from "../elements/AccessibleButton";
import QRCode from "../elements/QRCode";
import Heading from "../typography/Heading";
import BaseDialog from "./BaseDialog";
interface Props {
onFinished(): void;
}
export const showAppDownloadDialogPrompt = (): boolean => {
const desktopBuilds = SdkConfig.getObject("desktop_builds");
const mobileBuilds = SdkConfig.getObject("mobile_builds");
return (
!!desktopBuilds?.get("available") ||
!!mobileBuilds?.get("ios") ||
!!mobileBuilds?.get("android") ||
!!mobileBuilds?.get("fdroid")
);
};
export const AppDownloadDialog: FC<Props> = ({ onFinished }) => {
const brand = SdkConfig.get("brand");
const desktopBuilds = SdkConfig.getObject("desktop_builds");
const mobileBuilds = SdkConfig.getObject("mobile_builds");
const urlAppStore = mobileBuilds?.get("ios");
const urlGooglePlay = mobileBuilds?.get("android");
const urlFDroid = mobileBuilds?.get("fdroid");
const urlAndroid = urlGooglePlay ?? urlFDroid;
return (
<BaseDialog
title={_t("onboarding|download_brand", { brand })}
className="mx_AppDownloadDialog"
fixedWidth
onFinished={onFinished}
>
{desktopBuilds?.get("available") && (
<div className="mx_AppDownloadDialog_desktop">
<Heading size="3">{_t("onboarding|download_brand_desktop", { brand })}</Heading>
<AccessibleButton
kind="primary"
element="a"
href={desktopBuilds?.get("url")}
target="_blank"
onClick={() => {}}
>
{_t("onboarding|download_brand_desktop", { brand })}
</AccessibleButton>
</div>
)}
<div className="mx_AppDownloadDialog_mobile">
{urlAppStore && (
<div className="mx_AppDownloadDialog_app">
<Heading size="3">{_t("common|ios")}</Heading>
<QRCode data={urlAppStore} margin={0} width={172} />
<div className="mx_AppDownloadDialog_info">
{_t("onboarding|qr_or_app_links", {
appLinks: "",
qrCode: "",
})}
</div>
<div className="mx_AppDownloadDialog_links">
<AccessibleButton
element="a"
href={urlAppStore}
target="_blank"
aria-label={_t("onboarding|download_app_store")}
onClick={() => {}}
>
<IOSBadge />
</AccessibleButton>
</div>
</div>
)}
{urlAndroid && (
<div className="mx_AppDownloadDialog_app">
<Heading size="3">{_t("common|android")}</Heading>
<QRCode data={urlAndroid} margin={0} width={172} />
<div className="mx_AppDownloadDialog_info">
{_t("onboarding|qr_or_app_links", {
appLinks: "",
qrCode: "",
})}
</div>
<div className="mx_AppDownloadDialog_links">
{urlGooglePlay && (
<AccessibleButton
element="a"
href={urlGooglePlay}
target="_blank"
aria-label={_t("onboarding|download_google_play")}
onClick={() => {}}
>
<GooglePlayBadge />
</AccessibleButton>
)}
{urlFDroid && (
<AccessibleButton
element="a"
href={urlFDroid}
target="_blank"
aria-label={_t("onboarding|download_f_droid")}
onClick={() => {}}
>
<FDroidBadge />
</AccessibleButton>
)}
</div>
</div>
)}
</div>
<div className="mx_AppDownloadDialog_legal">
<p>{_t("onboarding|apple_trademarks")}</p>
<p>{_t("onboarding|google_trademarks")}</p>
</div>
</BaseDialog>
);
};

View File

@@ -1,78 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import { _t } from "../../../languageHandler";
import { UseCase } from "../../../settings/enums/UseCase";
import SplashPage from "../../structures/SplashPage";
import AccessibleButton from "../elements/AccessibleButton";
import { UseCaseSelectionButton } from "./UseCaseSelectionButton";
interface Props {
onFinished: (useCase: UseCase) => void;
}
const TIMEOUT = 1500;
export function UseCaseSelection({ onFinished }: Props): JSX.Element {
const [selection, setSelected] = useState<UseCase | null>(null);
// Call onFinished 1.5s after `selection` becomes truthy, to give time for the animation to run
useEffect(() => {
if (selection) {
let handler: number | null = window.setTimeout(() => {
handler = null;
onFinished(selection);
}, TIMEOUT);
return () => {
if (handler !== null) clearTimeout(handler);
handler = null;
};
}
}, [selection, onFinished]);
return (
<SplashPage
className={classNames("mx_UseCaseSelection", {
mx_UseCaseSelection_selected: selection !== null,
})}
>
<div className="mx_UseCaseSelection_title mx_UseCaseSelection_slideIn">
<h1>{_t("onboarding|use_case_heading1")}</h1>
</div>
<div className="mx_UseCaseSelection_info mx_UseCaseSelection_slideInDelayed">
<h2>{_t("onboarding|use_case_heading2")}</h2>
<h3>{_t("onboarding|use_case_heading3")}</h3>
</div>
<div className="mx_UseCaseSelection_options mx_UseCaseSelection_slideInDelayed">
<UseCaseSelectionButton
useCase={UseCase.PersonalMessaging}
selected={selection === UseCase.PersonalMessaging}
onClick={setSelected}
/>
<UseCaseSelectionButton
useCase={UseCase.WorkMessaging}
selected={selection === UseCase.WorkMessaging}
onClick={setSelected}
/>
<UseCaseSelectionButton
useCase={UseCase.CommunityMessaging}
selected={selection === UseCase.CommunityMessaging}
onClick={setSelected}
/>
</div>
<div className="mx_UseCaseSelection_skip mx_UseCaseSelection_slideInDelayed">
<AccessibleButton kind="link" onClick={async () => setSelected(UseCase.Skip)}>
{_t("action|skip")}
</AccessibleButton>
</div>
</SplashPage>
);
}

View File

@@ -1,54 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React from "react";
import { _t } from "../../../languageHandler";
import { UseCase } from "../../../settings/enums/UseCase";
import AccessibleButton from "./AccessibleButton";
interface Props {
useCase: UseCase;
selected: boolean;
onClick: (useCase: UseCase) => void;
}
export function UseCaseSelectionButton({ useCase, onClick, selected }: Props): JSX.Element {
let label: string | undefined;
switch (useCase) {
case UseCase.PersonalMessaging:
label = _t("onboarding|use_case_personal_messaging");
break;
case UseCase.WorkMessaging:
label = _t("onboarding|use_case_work_messaging");
break;
case UseCase.CommunityMessaging:
label = _t("onboarding|use_case_community_messaging");
break;
}
return (
<AccessibleButton
className={classNames("mx_UseCaseSelectionButton", {
mx_UseCaseSelectionButton_selected: selected,
})}
onClick={async () => onClick(useCase)}
>
<div
className={classNames("mx_UseCaseSelectionButton_icon", {
mx_UseCaseSelectionButton_messaging: useCase === UseCase.PersonalMessaging,
mx_UseCaseSelectionButton_work: useCase === UseCase.WorkMessaging,
mx_UseCaseSelectionButton_community: useCase === UseCase.CommunityMessaging,
})}
/>
<span>{label}</span>
<div className="mx_UseCaseSelectionButton_selectedIcon" />
</AccessibleButton>
);
}

View File

@@ -22,7 +22,6 @@ import { UserTab } from "../../../dialogs/UserTab";
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../../../dispatcher/actions";
import SdkConfig from "../../../../../SdkConfig";
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
import { SettingsSubsection } from "../../shared/SettingsSubsection";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
@@ -117,7 +116,7 @@ const SpellCheckSection: React.FC = () => {
};
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "FTUE.userOnboardingButton"];
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs"];
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
@@ -237,10 +236,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
public render(): React.ReactNode {
const useCase = SettingsStore.getValue("FTUE.useCaseSelection");
const roomListSettings = PreferencesUserSettingsTab.ROOM_LIST_SETTINGS
// Only show the user onboarding setting if the user should see the user onboarding page
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
const roomListSettings = PreferencesUserSettingsTab.ROOM_LIST_SETTINGS;
const browserTimezoneLabel: string = _t("settings|preferences|default_timezone", {
timezone: TimezoneHandler.shortBrowserTimezone(),

View File

@@ -1,80 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { useCallback } from "react";
import { Action } from "../../../dispatcher/actions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { useSettingValue } from "../../../hooks/useSettings";
import { _t } from "../../../languageHandler";
import PosthogTrackers from "../../../PosthogTrackers";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import Heading from "../../views/typography/Heading";
import { showUserOnboardingPage } from "./UserOnboardingPage";
interface Props {
selected: boolean;
minimized: boolean;
}
export function UserOnboardingButton({ selected, minimized }: Props): JSX.Element {
const useCase = useSettingValue("FTUE.useCaseSelection");
const visible = useSettingValue("FTUE.userOnboardingButton");
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
return <></>;
}
return <UserOnboardingButtonInternal selected={selected} minimized={minimized} />;
}
function UserOnboardingButtonInternal({ selected, minimized }: Props): JSX.Element {
const onDismiss = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingIgnoreButton", ev);
SettingsStore.setValue("FTUE.userOnboardingButton", null, SettingLevel.ACCOUNT, false);
}, []);
const onClick = useCallback((ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingButton", ev);
defaultDispatcher.fire(Action.ViewHomePage);
}, []);
return (
<AccessibleButton
className={classNames("mx_UserOnboardingButton", {
mx_UserOnboardingButton_selected: selected,
mx_UserOnboardingButton_minimized: minimized,
})}
onClick={onClick}
>
{!minimized && (
<>
<div className="mx_UserOnboardingButton_content">
<Heading size="4" className="mx_Heading_h4">
{_t("common|welcome")}
</Heading>
<AccessibleButton
className="mx_UserOnboardingButton_close"
onClick={onDismiss}
aria-label={_t("action|dismiss")}
/>
</div>
</>
)}
</AccessibleButton>
);
}

View File

@@ -1,82 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import PosthogTrackers from "../../../PosthogTrackers";
import SdkConfig from "../../../SdkConfig";
import { UseCase } from "../../../settings/enums/UseCase";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import Heading from "../../views/typography/Heading";
const onClickSendDm = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebUserOnboardingHeaderSendDm", ev);
defaultDispatcher.dispatch({ action: "view_create_chat" });
};
interface Props {
useCase: UseCase | null;
}
export function UserOnboardingHeader({ useCase }: Props): JSX.Element {
let title: string;
let description = _t("onboarding|free_e2ee_messaging_unlimited_voip", {
brand: SdkConfig.get("brand"),
});
let image: string;
let actionLabel: string;
switch (useCase) {
/* eslint-disable @typescript-eslint/no-require-imports */
case UseCase.PersonalMessaging:
title = _t("onboarding|personal_messaging_title");
image = require("../../../../res/img/user-onboarding/PersonalMessaging.png");
actionLabel = _t("onboarding|personal_messaging_action");
break;
case UseCase.WorkMessaging:
title = _t("onboarding|work_messaging_title");
description = _t("onboarding|free_e2ee_messaging_unlimited_voip", {
brand: SdkConfig.get("brand"),
});
image = require("../../../../res/img/user-onboarding/WorkMessaging.png");
actionLabel = _t("onboarding|work_messaging_action");
break;
case UseCase.CommunityMessaging:
title = _t("onboarding|community_messaging_title");
description = _t("onboarding|community_messaging_description");
image = require("../../../../res/img/user-onboarding/CommunityMessaging.png");
actionLabel = _t("onboarding|community_messaging_action");
break;
default:
title = _t("onboarding|welcome_to_brand", {
brand: SdkConfig.get("brand"),
});
image = require("../../../../res/img/user-onboarding/PersonalMessaging.png");
actionLabel = _t("onboarding|personal_messaging_action");
break;
/* eslint-enable @typescript-eslint/no-require-imports */
}
return (
<div className="mx_UserOnboardingHeader">
<div className="mx_UserOnboardingHeader_content">
<Heading size="1">
{title}
<span className="mx_UserOnboardingHeader_dot">.</span>
</Heading>
<p>{description}</p>
<AccessibleButton onClick={onClickSendDm} kind="primary">
{actionLabel}
</AccessibleButton>
</div>
<img className="mx_UserOnboardingHeader_image" src={image} alt="" />
</div>
);
}

View File

@@ -1,68 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { UserOnboardingTaskWithResolvedCompletion } from "../../../hooks/useUserOnboardingTasks";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import ProgressBar from "../../views/elements/ProgressBar";
import Heading from "../../views/typography/Heading";
import { UserOnboardingTask } from "./UserOnboardingTask";
export const getUserOnboardingCounters = (
tasks: UserOnboardingTaskWithResolvedCompletion[],
): {
completed: number;
waiting: number;
total: number;
} => {
const completed = tasks.filter((task) => task.completed === true).length;
const waiting = tasks.filter((task) => task.completed === false).length;
return {
completed: completed,
waiting: waiting,
total: completed + waiting,
};
};
interface Props {
tasks: UserOnboardingTaskWithResolvedCompletion[];
}
export function UserOnboardingList({ tasks }: Props): JSX.Element {
const { completed, waiting, total } = getUserOnboardingCounters(tasks);
return (
<div className="mx_UserOnboardingList" data-testid="user-onboarding-list">
<div className="mx_UserOnboardingList_header">
<Heading size="3" className="mx_UserOnboardingList_title">
{waiting > 0
? _t("onboarding|only_n_steps_to_go", {
count: waiting,
})
: _t("onboarding|you_did_it")}
</Heading>
<div className="mx_UserOnboardingList_hint">
{_t("onboarding|complete_these", {
brand: SdkConfig.get("brand"),
})}
</div>
</div>
<div className="mx_UserOnboardingList_progress">
<ProgressBar value={completed} max={total} animated />
</div>
<ol className="mx_UserOnboardingList_list">
{tasks.map((task) => (
<UserOnboardingTask key={task.id} completed={task.completed} task={task} />
))}
</ol>
</div>
);
}

View File

@@ -1,78 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020-2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { useEffect, useState } from "react";
import * as React from "react";
import { useInitialSyncComplete } from "../../../hooks/useIsInitialSyncComplete";
import { useSettingValue } from "../../../hooks/useSettings";
import { useUserOnboardingContext } from "../../../hooks/useUserOnboardingContext";
import { useUserOnboardingTasks } from "../../../hooks/useUserOnboardingTasks";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from "../../../SdkConfig";
import { UseCase } from "../../../settings/enums/UseCase";
import { getHomePageUrl } from "../../../utils/pages";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import EmbeddedPage from "../../structures/EmbeddedPage";
import HomePage from "../../structures/HomePage";
import { UserOnboardingHeader } from "./UserOnboardingHeader";
import { UserOnboardingList } from "./UserOnboardingList";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
interface Props {
justRegistered?: boolean;
}
// We decided to only show the new user onboarding page to new users
// For now, that means we set the cutoff at 2022-07-01 00:00 UTC
const USER_ONBOARDING_CUTOFF_DATE = new Date(1_656_633_600);
export function showUserOnboardingPage(useCase: UseCase | null): boolean {
return useCase !== null || MatrixClientPeg.userRegisteredAfter(USER_ONBOARDING_CUTOFF_DATE);
}
const ANIMATION_DURATION = 2800;
export function UserOnboardingPage({ justRegistered = false }: Props): JSX.Element {
const cli = useMatrixClientContext();
const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config, cli);
const useCase = useSettingValue("FTUE.useCaseSelection");
const context = useUserOnboardingContext();
const tasks = useUserOnboardingTasks(context);
const initialSyncComplete = useInitialSyncComplete();
const [showList, setShowList] = useState<boolean>(false);
useEffect(() => {
if (initialSyncComplete) {
const handler = window.setTimeout(() => {
setShowList(true);
}, ANIMATION_DURATION);
return () => {
clearTimeout(handler);
};
} else {
setShowList(false);
}
}, [initialSyncComplete, setShowList]);
// Only show new onboarding list to users who registered after a given date or have chosen a use case
if (!showUserOnboardingPage(useCase)) {
return <HomePage justRegistered={justRegistered} />;
}
if (pageUrl) {
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
}
return (
<AutoHideScrollbar className="mx_UserOnboardingPage">
<UserOnboardingHeader useCase={useCase} />
{showList && <UserOnboardingList tasks={tasks} />}
</AutoHideScrollbar>
);
}

View File

@@ -1,59 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import * as React from "react";
import { UserOnboardingTaskWithResolvedCompletion } from "../../../hooks/useUserOnboardingTasks";
import AccessibleButton from "../../views/elements/AccessibleButton";
import Heading from "../../views/typography/Heading";
interface Props {
task: UserOnboardingTaskWithResolvedCompletion;
completed?: boolean;
}
export function UserOnboardingTask({ task, completed = false }: Props): JSX.Element {
const title = typeof task.title === "function" ? task.title() : task.title;
const description = typeof task.description === "function" ? task.description() : task.description;
return (
<li
data-testid="user-onboarding-task"
className={classNames("mx_UserOnboardingTask", {
mx_UserOnboardingTask_completed: completed,
})}
>
<div
className="mx_UserOnboardingTask_number"
role="checkbox"
aria-disabled="true"
aria-checked={completed}
aria-labelledby={`mx_UserOnboardingTask_${task.id}`}
/>
<div id={`mx_UserOnboardingTask_${task.id}`} className="mx_UserOnboardingTask_content">
<Heading size="4" className="mx_UserOnboardingTask_title">
{title}
</Heading>
<div className="mx_UserOnboardingTask_description">{description}</div>
</div>
{task.action && (!task.action.hideOnComplete || !completed) && (
<AccessibleButton
element="a"
className="mx_UserOnboardingTask_action"
kind="primary_outline"
href={task.action.href}
target="_blank"
onClick={task.action.onClick ?? null}
>
{task.action.label}
</AccessibleButton>
)}
</li>
);
}