Remove FTUE onboarding as it is incompatible with SSO/OIDC

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
t3chguy/remove-ftue-onboarding
Michael Telatynski 2025-01-08 15:02:01 +00:00
parent 7685e547de
commit 964c27557b
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
44 changed files with 14 additions and 2703 deletions

View File

@ -1,79 +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 { test, expect } from "../../element-web-test";
test.describe("User Onboarding (new user)", () => {
test.use({
displayName: "Jane Doe",
});
// This first beforeEach happens before the `user` fixture runs
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("mx_registration_time", "1656633601");
});
});
test.beforeEach(async ({ page, user }) => {
await expect(page.locator(".mx_UserOnboardingPage")).toBeVisible();
await expect(page.getByRole("button", { name: "Welcome" })).toBeVisible();
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
});
test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => {
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
);
await app.settings.openUserSettings("Preferences");
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
});
test("app download dialog", { tag: "@screenshot" }, async ({ page }) => {
await page.getByRole("button", { name: "Download apps" }).click();
await expect(
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),
).toBeVisible();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot(
"User-Onboarding-new-user-app-download-dialog-1.png",
{
// Set a constant bg behind the modal to ensure screenshot stability
css: `
.mx_AppDownloadDialog_wrapper {
background: black;
}
`,
},
);
});
test("using find friends action should increase progress", async ({ page, homeserver }) => {
const bot = await homeserver.registerUser("botbob", "password", "BotBob");
const oldProgress = parseFloat(await page.getByRole("progressbar").getAttribute("value"));
await page.getByRole("button", { name: "Find friends" }).click();
await page.locator(".mx_InviteDialog_editor").getByRole("textbox").fill(bot.userId);
await page.getByRole("button", { name: "Go" }).click();
await expect(page.locator(".mx_InviteDialog_buttonAndSpinner")).not.toBeVisible();
const message = "Hi!";
const composer = page.getByRole("textbox", { name: "Send a message…" });
await composer.fill(`${message}`);
await composer.press("Enter");
await expect(page.locator(".mx_MTextBody.mx_EventTile_content", { hasText: message })).toBeVisible();
await page.goto("/#/home");
await expect(page.locator(".mx_UserOnboardingPage")).toBeVisible();
await expect(page.getByRole("button", { name: "Welcome" })).toBeVisible();
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
await page.waitForTimeout(500); // await progress bar animation
const progress = parseFloat(await page.getByRole("progressbar").getAttribute("value"));
expect(progress).toBeGreaterThan(oldProgress);
});
});

View File

@ -1,28 +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 { test, expect } from "../../element-web-test";
test.describe("User Onboarding (old user)", () => {
test.use({
displayName: "Jane Doe",
});
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("mx_registration_time", "2");
});
});
test("page and preference are hidden", async ({ page, user, app }) => {
await expect(page.locator(".mx_UserOnboardingPage")).not.toBeVisible();
await expect(page.locator(".mx_UserOnboardingButton")).not.toBeVisible();
await app.settings.openUserSettings("Preferences");
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).not.toBeVisible();
});
});

View File

@ -126,7 +126,6 @@
@import "./views/context_menus/_RoomNotificationContextMenu.pcss";
@import "./views/dialogs/_AddExistingToSpaceDialog.pcss";
@import "./views/dialogs/_AnalyticsLearnMoreDialog.pcss";
@import "./views/dialogs/_AppDownloadDialog.pcss";
@import "./views/dialogs/_BugReportDialog.pcss";
@import "./views/dialogs/_BulkRedactDialog.pcss";
@import "./views/dialogs/_ChangelogDialog.pcss";
@ -217,8 +216,6 @@
@import "./views/elements/_TagComposer.pcss";
@import "./views/elements/_TextWithTooltip.pcss";
@import "./views/elements/_ToggleSwitch.pcss";
@import "./views/elements/_UseCaseSelection.pcss";
@import "./views/elements/_UseCaseSelectionButton.pcss";
@import "./views/elements/_Validation.pcss";
@import "./views/emojipicker/_EmojiPicker.pcss";
@import "./views/location/_LocationPicker.pcss";
@ -375,11 +372,6 @@
@import "./views/toasts/_IncomingLegacyCallToast.pcss";
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
@import "./views/typography/_Heading.pcss";
@import "./views/user-onboarding/_UserOnboardingButton.pcss";
@import "./views/user-onboarding/_UserOnboardingHeader.pcss";
@import "./views/user-onboarding/_UserOnboardingList.pcss";
@import "./views/user-onboarding/_UserOnboardingPage.pcss";
@import "./views/user-onboarding/_UserOnboardingTask.pcss";
@import "./views/verification/_VerificationShowSas.pcss";
@import "./views/voip/LegacyCallView/_LegacyCallViewButtons.pcss";
@import "./views/voip/_CallDuration.pcss";

View File

@ -1,77 +0,0 @@
.mx_AppDownloadDialog {
display: flex;
flex-direction: column;
gap: $spacing-32;
color: $primary-content;
&.mx_Dialog_fixedWidth {
width: 640px;
}
.mx_AppDownloadDialog_desktop {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-16;
}
.mx_AppDownloadDialog_mobile {
display: flex;
flex-direction: row;
gap: $spacing-24;
.mx_AppDownloadDialog_app {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-basis: 50%;
align-items: center;
gap: $spacing-16;
.mx_QRCode {
/* intentionally hardcoded color to ensure the QR code is readable in any situation */
background: #ffffff;
padding: $spacing-24;
border: 1px solid $quinary-content;
border-radius: 4px;
align-self: stretch;
display: flex;
align-items: center;
flex-direction: column;
.mx_VerificationQRCode {
height: 144px;
width: 144px;
image-rendering: pixelated;
border-radius: 0;
}
}
.mx_AppDownloadDialog_info {
font-size: $font-12px;
color: $tertiary-content;
}
.mx_AppDownloadDialog_links {
display: flex;
flex-direction: row;
gap: $spacing-8;
.mx_AccessibleButton {
svg {
height: 40px;
}
}
}
}
}
.mx_AppDownloadDialog_legal {
p {
margin: 0;
font-size: $font-12px;
color: $tertiary-content;
}
}
}

View File

@ -1,122 +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.
*/
.mx_UseCaseSelection {
display: grid;
grid-template-rows: 1fr 1fr max-content 2fr;
height: 100%;
grid-gap: $spacing-40;
.mx_UseCaseSelection_title {
display: flex;
flex-direction: column;
justify-content: flex-end;
h1 {
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-32px;
text-align: center;
}
}
.mx_UseCaseSelection_info {
display: flex;
flex-direction: column;
gap: $spacing-8;
align-self: flex-end;
h2 {
margin: 0;
font-weight: 500;
font-size: $font-24px;
text-align: center;
}
h3 {
margin: 0;
font-weight: 400;
font-size: $font-16px;
color: $secondary-content;
text-align: center;
}
}
.mx_UseCaseSelection_options {
display: grid;
grid-template-columns: repeat(auto-fit, 232px);
gap: $spacing-32;
align-self: stretch;
justify-content: center;
}
.mx_UseCaseSelection_skip {
display: flex;
flex-direction: column;
align-self: flex-start;
}
}
.mx_UseCaseSelection_slideIn {
animation-delay: 800ms;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
animation-name: mx_UseCaseSelection_slideInLong;
animation-fill-mode: backwards;
will-change: opacity;
}
.mx_UseCaseSelection_slideInDelayed {
animation-delay: 1500ms;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
animation-name: mx_UseCaseSelection_slideInShort;
animation-fill-mode: backwards;
will-change: transform, opacity;
}
.mx_UseCaseSelection_selected {
.mx_UseCaseSelection_slideIn,
.mx_UseCaseSelection_slideInDelayed {
animation-delay: 800ms;
animation-duration: 300ms;
animation-fill-mode: forwards;
animation-name: mx_UseCaseSelection_fadeOut;
will-change: opacity;
}
}
@keyframes mx_UseCaseSelection_slideInLong {
0% {
transform: translate(0, 20px);
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 1;
}
}
@keyframes mx_UseCaseSelection_slideInShort {
0% {
transform: translate(0, 8px);
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 1;
}
}
@keyframes mx_UseCaseSelection_fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -1,98 +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.
*/
.mx_UseCaseSelectionButton {
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-24 $spacing-16;
background: $background;
border: 1px solid $quinary-content;
border-radius: 8px;
text-align: center;
position: relative;
transition-property: box-shadow, transform;
transition-duration: 300ms;
.mx_UseCaseSelectionButton_icon {
/* workaround: design expects a layering of two colors */
background: linear-gradient(0deg, rgba(172, 59, 168, 0.15), rgba(172, 59, 168, 0.15)), #ffffff;
border-radius: 14px;
padding: $spacing-8;
margin-bottom: $spacing-16;
&::before {
content: "";
display: block;
/* this has to remain the same color across all themes,
as its background has a fixed color as well */
background: #1e1e1e;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
width: 22px;
height: 22px;
}
&.mx_UseCaseSelectionButton_messaging::before {
mask-image: url("$(res)/img/element-icons/chat-bubble.svg");
}
&.mx_UseCaseSelectionButton_work::before {
mask-image: url("$(res)/img/element-icons/view-community.svg");
}
&.mx_UseCaseSelectionButton_community::before {
mask-image: url("@vector-im/compound-design-tokens/icons/public.svg");
mask-size: 24px;
}
}
&:hover,
&:focus {
box-shadow: 0 $spacing-4 $spacing-8 rgba(0, 0, 0, 0.08);
transform: translate(0, -$spacing-8);
}
.mx_UseCaseSelectionButton_selectedIcon {
right: -12px;
top: -12px;
position: absolute;
border-radius: 24px;
background: $accent;
padding: 6px;
transition-property: opacity, transform;
transition-duration: 150ms;
opacity: 0;
transform: scale(0.6);
&::before {
content: "";
display: block;
background: $background;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
width: 12px;
height: 12px;
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
}
}
&.mx_UseCaseSelectionButton_selected {
border: 2px solid $accent;
padding: calc($spacing-24 - 1px) calc($spacing-16 - 1px);
box-shadow: 0 $spacing-4 $spacing-8 rgba(0, 0, 0, 0.08);
.mx_UseCaseSelectionButton_selectedIcon {
opacity: 1;
transform: scale(1);
}
}
}

View File

@ -1,75 +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.
*/
.mx_UserOnboardingButton {
display: flex;
flex-direction: column;
align-content: stretch;
align-items: stretch;
border-radius: 8px;
margin: $spacing-8 $spacing-8 0;
padding: $spacing-12;
&.mx_UserOnboardingButton_selected,
&:hover,
&:focus-within {
background-color: $panel-actions;
}
.mx_UserOnboardingButton_content {
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
.mx_Heading_h4 {
margin-right: auto;
font: var(--cpd-font-body-md-regular);
color: $primary-content;
}
.mx_UserOnboardingButton_percentage {
font-size: $font-12px;
color: $secondary-content;
}
.mx_UserOnboardingButton_close {
position: relative;
box-sizing: border-box;
width: 14px;
height: 14px;
border-radius: 7px;
border: 1px solid $secondary-content;
flex-shrink: 0;
&::before {
background-color: $secondary-content;
content: "";
mask-repeat: no-repeat;
mask-position: center;
mask-size: 12px;
width: inherit;
height: inherit;
position: absolute;
left: -1px;
top: -1px;
mask-image: url("@vector-im/compound-design-tokens/icons/close.svg");
}
}
}
.mx_ProgressBar {
width: auto;
margin-top: $spacing-8;
background: $background;
}
&.mx_UserOnboardingButton_completed .mx_ProgressBar {
display: none;
}
}

View File

@ -1,93 +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.
*/
.mx_UserOnboardingHeader {
display: flex;
flex-direction: row;
padding: $spacing-32;
border-radius: 16px;
background: $system;
gap: $spacing-64;
animation-delay: 1500ms;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
animation-name: mx_UserOnboardingHeader_slideIn;
animation-fill-mode: backwards;
will-change: opacity, transform;
@media (max-width: 1280px) {
margin: $spacing-32;
}
.mx_UserOnboardingHeader_dot {
color: $accent;
}
.mx_UserOnboardingHeader_content {
display: flex;
flex-direction: column;
flex-basis: 50%;
flex-shrink: 1;
flex-grow: 1;
min-width: 0;
gap: $spacing-24;
margin-right: auto;
p {
margin: 0;
}
.mx_AccessibleButton {
margin-top: auto;
align-self: flex-start;
padding: $spacing-12 $spacing-24;
}
}
.mx_UserOnboardingHeader_image {
flex-basis: 30%;
flex-shrink: 1;
flex-grow: 1;
align-self: center;
height: calc(100% + $spacing-64 + $spacing-64);
aspect-ratio: 4 / 3;
object-fit: contain;
min-width: 0;
min-height: 0;
margin-top: -$spacing-64;
margin-bottom: -$spacing-64;
animation-delay: 1500ms;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
animation-name: mx_UserOnboardingHeader_slideInLong;
animation-fill-mode: backwards;
will-change: opacity, transform;
}
}
@keyframes mx_UserOnboardingHeader_slideIn {
0% {
transform: translate(0, 8px);
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 1;
}
}
@keyframes mx_UserOnboardingHeader_slideInLong {
0% {
transform: translate(0, 32px);
}
100% {
transform: translate(0, 0);
}
}

View File

@ -1,67 +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.
*/
.mx_UserOnboardingList {
display: flex;
flex-direction: column;
margin: 0 $spacing-32;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0, 0, 0.58, 1);
animation-name: mx_UserOnboardingList_slideIn;
animation-fill-mode: backwards;
will-change: opacity;
.mx_UserOnboardingList_header {
display: flex;
flex-direction: row;
gap: 12px;
align-items: center;
.mx_UserOnboardingList_hint {
color: $secondary-content;
}
}
.mx_UserOnboardingList_progress {
display: flex;
flex-direction: column;
counter-reset: user-onboarding;
.mx_ProgressBar {
width: auto;
margin-top: $spacing-16;
height: 16px;
@mixin ProgressBarBorderRadius 16px;
}
}
.mx_UserOnboardingList_list {
display: grid;
grid-template-columns: max-content 1fr max-content;
appearance: none;
list-style: none;
margin: $spacing-32 0 0;
padding: 0;
grid-gap: $spacing-24;
}
}
@keyframes mx_UserOnboardingList_slideIn {
0% {
transform: translate(0, 8px);
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 1;
}
}

View File

@ -1,27 +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.
*/
.mx_UserOnboardingPage {
width: 100%;
height: 100%;
align-self: stretch;
max-width: 1200px;
margin: 0 auto auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
gap: $spacing-64;
padding: $spacing-64 100px;
@media (max-width: 1280px) {
padding: $spacing-48 $spacing-32;
}
}

View File

@ -1,112 +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.
*/
.mx_UserOnboardingTask {
display: contents;
.mx_UserOnboardingTask_number {
counter-increment: user-onboarding;
grid-column: 1;
color: $secondary-content;
width: 32px;
height: 32px;
text-align: center;
border: 2px solid $quinary-content;
border-radius: 32px;
line-height: 32px;
align-self: center;
position: relative;
&::before {
content: counter(user-onboarding);
}
}
.mx_UserOnboardingTask_content {
grid-column: 2;
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
transition: all 500ms;
.mx_UserOnboardingTask_title {
font: var(--cpd-font-body-md-medium);
}
.mx_UserOnboardingTask_description {
font-size: $font-12px;
}
}
.mx_UserOnboardingTask_action.mx_AccessibleButton {
grid-column: 3;
min-width: 180px;
@media (max-width: 800px) {
grid-column: 2;
margin-top: -16px;
}
}
&.mx_UserOnboardingTask_completed {
.mx_UserOnboardingTask_number {
&::before {
content: "";
position: absolute;
inset: -2px;
background: var(--cpd-color-icon-accent-tertiary);
border-radius: 32px;
animation-duration: 300ms;
animation-fill-mode: both;
animation-name: mx_UserOnboardingTask_spring;
will-change: opacity, transform;
}
&::after {
background-color: var(--cpd-color-icon-on-solid-primary);
content: "";
mask-repeat: no-repeat;
mask-position: center;
mask-size: 24px;
width: inherit;
height: inherit;
position: absolute;
left: 0;
top: 0;
mask-image: url("@vector-im/compound-design-tokens/icons/check.svg");
animation-duration: 300ms;
animation-fill-mode: both;
animation-name: mx_UserOnboardingTask_spring;
will-change: opacity, transform;
}
}
.mx_UserOnboardingTask_content {
opacity: 0.6;
}
}
}
@keyframes mx_UserOnboardingTask_spring {
0% {
opacity: 0;
transform: scale(0.6);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 KiB

View File

@ -24,7 +24,6 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
[Views.WELCOME]: "Welcome",
[Views.LOGIN]: "Login",
[Views.REGISTER]: "Register",
[Views.USE_CASE_SELECTION]: "UseCaseSelection",
[Views.FORGOT_PASSWORD]: "ForgotPassword",
[Views.COMPLETE_SECURITY]: "CompleteSecurity",
[Views.E2E_SETUP]: "E2ESetup",

View File

@ -33,9 +33,6 @@ enum Views {
// flow to setup SSSS / cross-signing on this account
E2E_SETUP,
// screen that allows users to select which use case theyll use matrix for
USE_CASE_SELECTION,
// we are logged in with an active matrix client. The logged_in state also
// includes guests users as they too are logged in at the client level.
LOGGED_IN,

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>
);
}

View File

@ -1,17 +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 { ClientEvent } from "matrix-js-sdk/src/matrix";
import { useEventEmitterState } from "./useEventEmitter";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
export function useInitialSyncComplete(): boolean {
const cli = useMatrixClientContext();
return useEventEmitterState(cli, ClientEvent.Sync, () => cli.isInitialSyncComplete());
}

View File

@ -1,124 +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 { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Notifier, NotifierEvent } from "../Notifier";
import DMRoomMap from "../utils/DMRoomMap";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import { useSettingValue } from "./useSettings";
import { useEventEmitter, useTypedEventEmitter } from "./useEventEmitter";
export interface UserOnboardingContext {
hasAvatar: boolean;
hasDevices: boolean;
hasDmRooms: boolean;
showNotificationsPrompt: boolean;
}
const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;
/**
* Returns a persistent, non-changing reference to a function
* This function proxies all its calls to the current value of the given input callback
*
* This allows you to use the current value of e.g., a state in a callback thats used by e.g., a useEventEmitter or
* similar hook without re-registering the hook when the state changes
* @param value changing callback
*/
function useRefOf<T extends any[], R>(value: (...values: T) => R): (...values: T) => R {
const ref = useRef(value);
ref.current = value;
return useCallback((...values: T) => ref.current(...values), []);
}
function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: MatrixClient) => Promise<T>): T {
const [value, setValue] = useState<T>(defaultValue);
const cli = useMatrixClientContext();
const handler = useRefOf(callback);
useEffect(() => {
if (value) {
return;
}
let handle: number | null = null;
let enabled = true;
const repeater = async (): Promise<void> => {
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
setValue(await handler(cli));
if (enabled) {
handle = window.setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
}
};
repeater().catch((err) => logger.warn("could not update user onboarding context", err));
cli.on(ClientEvent.AccountData, repeater);
return () => {
enabled = false;
cli.off(ClientEvent.AccountData, repeater);
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
};
}, [cli, handler, value]);
return value;
}
function useShowNotificationsPrompt(): boolean {
const client = useMatrixClientContext();
const [value, setValue] = useState<boolean>(client.pushRules ? Notifier.shouldShowPrompt() : true);
const updateValue = useCallback(() => {
setValue(client.pushRules ? Notifier.shouldShowPrompt() : true);
}, [client]);
useEventEmitter(Notifier, NotifierEvent.NotificationHiddenChange, () => {
updateValue();
});
const setting = useSettingValue("notificationsEnabled");
useEffect(() => {
updateValue();
}, [setting, updateValue]);
// shouldShowPrompt is dependent on the client having push rules. There isn't an event for the client
// fetching its push rules, but we'll know it has them by the time it sync, so we update this on sync.
useTypedEventEmitter(client, ClientEvent.Sync, updateValue);
return value;
}
export function useUserOnboardingContext(): UserOnboardingContext {
const hasAvatar = useUserOnboardingContextValue(false, async (cli) => {
const profile = await cli.getProfileInfo(cli.getUserId()!);
return Boolean(profile?.avatar_url);
});
const hasDevices = useUserOnboardingContextValue(false, async (cli) => {
const myDevice = cli.getDeviceId();
const devices = await cli.getDevices();
return Boolean(devices.devices.find((device) => device.device_id !== myDevice));
});
const hasDmRooms = useUserOnboardingContextValue(false, async () => {
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
return Boolean(Object.keys(dmRooms).length);
});
const showNotificationsPrompt = useShowNotificationsPrompt();
return useMemo(
() => ({ hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt }),
[hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt],
);
}

View File

@ -1,161 +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 { useMemo } from "react";
import { AppDownloadDialog, showAppDownloadDialogPrompt } from "../components/views/dialogs/AppDownloadDialog";
import { UserTab } from "../components/views/dialogs/UserTab";
import { ButtonEvent } from "../components/views/elements/AccessibleButton";
import { Action } from "../dispatcher/actions";
import defaultDispatcher from "../dispatcher/dispatcher";
import { _t } from "../languageHandler";
import Modal from "../Modal";
import { Notifier } from "../Notifier";
import PosthogTrackers from "../PosthogTrackers";
import SdkConfig from "../SdkConfig";
import { UseCase } from "../settings/enums/UseCase";
import { useSettingValue } from "./useSettings";
import { UserOnboardingContext } from "./useUserOnboardingContext";
interface UserOnboardingTask {
id: string;
title: string | (() => string);
description: string | (() => string);
relevant?: UseCase[];
action?: {
label: string;
onClick?: (ev: ButtonEvent) => void;
href?: string;
hideOnComplete?: boolean;
};
completed: (ctx: UserOnboardingContext) => boolean;
disabled?(): boolean;
}
export interface UserOnboardingTaskWithResolvedCompletion extends Omit<UserOnboardingTask, "completed"> {
completed: boolean;
}
const onClickStartDm = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev);
defaultDispatcher.dispatch({ action: "view_create_chat" });
};
const tasks: UserOnboardingTask[] = [
{
id: "create-account",
title: _t("auth|create_account_title"),
description: _t("onboarding|you_made_it"),
completed: () => true,
},
{
id: "find-friends",
title: _t("onboarding|find_friends"),
description: _t("onboarding|find_friends_description"),
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.PersonalMessaging, UseCase.Skip],
action: {
label: _t("onboarding|find_friends_action"),
onClick: onClickStartDm,
},
},
{
id: "find-coworkers",
title: _t("onboarding|find_coworkers"),
description: _t("onboarding|get_stuff_done"),
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.WorkMessaging],
action: {
label: _t("onboarding|find_people"),
onClick: onClickStartDm,
},
},
{
id: "find-community-members",
title: _t("onboarding|find_community_members"),
description: _t("onboarding|get_stuff_done"),
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.CommunityMessaging],
action: {
label: _t("onboarding|find_people"),
onClick: onClickStartDm,
},
},
{
id: "download-apps",
title: () =>
_t("onboarding|download_app", {
brand: SdkConfig.get("brand"),
}),
description: () =>
_t("onboarding|download_app_description", {
brand: SdkConfig.get("brand"),
}),
completed: (ctx: UserOnboardingContext) => ctx.hasDevices,
action: {
label: _t("onboarding|download_app_action"),
onClick: (ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebUserOnboardingTaskDownloadApps", ev);
Modal.createDialog(AppDownloadDialog, {}, "mx_AppDownloadDialog_wrapper", false, true);
},
},
disabled(): boolean {
return !showAppDownloadDialogPrompt();
},
},
{
id: "setup-profile",
title: _t("onboarding|set_up_profile"),
description: _t("onboarding|set_up_profile_description"),
completed: (ctx: UserOnboardingContext) => ctx.hasAvatar,
action: {
label: _t("onboarding|set_up_profile_action"),
onClick: (ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebUserOnboardingTaskSetupProfile", ev);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Account,
});
},
},
},
{
id: "permission-notifications",
title: _t("onboarding|enable_notifications"),
description: _t("onboarding|enable_notifications_description"),
completed: (ctx: UserOnboardingContext) => !ctx.showNotificationsPrompt,
action: {
label: _t("onboarding|enable_notifications_action"),
onClick: (ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Notifications,
});
Notifier.setPromptHidden(true);
},
hideOnComplete: !Notifier.isPossible(),
},
},
];
export function useUserOnboardingTasks(context: UserOnboardingContext): UserOnboardingTaskWithResolvedCompletion[] {
const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip;
return useMemo<UserOnboardingTaskWithResolvedCompletion[]>(() => {
return tasks
.filter((task) => {
if (task.disabled?.()) return false;
return !task.relevant || task.relevant.includes(useCase);
})
.map((task) => ({
...task,
completed: task.completed(context),
}));
}, [context, useCase]);
}

View File

@ -451,7 +451,6 @@
"one": "and one other...",
"other": "and %(count)s others..."
},
"android": "Android",
"appearance": "Appearance",
"application": "Application",
"are_you_sure": "Are you sure?",
@ -491,7 +490,6 @@
"identity_server": "Identity server",
"image": "Image",
"integration_manager": "Integration manager",
"ios": "iOS",
"joined": "Joined",
"labs": "Labs",
"legal": "Legal",
@ -585,8 +583,7 @@
"video": "Video",
"video_room": "Video room",
"view_message": "View message",
"warning": "Warning",
"welcome": "Welcome"
"warning": "Warning"
},
"composer": {
"autocomplete": {
@ -1628,61 +1625,15 @@
"m.key.verification.request": "%(name)s is requesting verification"
},
"onboarding": {
"apple_trademarks": "App Store® and the Apple logo® are trademarks of Apple Inc.",
"community_messaging_action": "Find your people",
"community_messaging_description": "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.",
"community_messaging_title": "Community ownership",
"complete_these": "Complete these to get the most out of %(brand)s",
"create_room": "Create a Group Chat",
"download_app": "Download %(brand)s",
"download_app_action": "Download apps",
"download_app_description": "Dont miss a thing by taking %(brand)s with you",
"download_app_store": "Download on the App Store",
"download_brand": "Download %(brand)s",
"download_brand_desktop": "Download %(brand)s Desktop",
"download_f_droid": "Get it on F-Droid",
"download_google_play": "Get it on Google Play",
"enable_notifications": "Turn on desktop notifications",
"enable_notifications_action": "Open settings",
"enable_notifications_description": "Dont miss a reply or important message",
"explore_rooms": "Explore Public Rooms",
"find_community_members": "Find and invite your community members",
"find_coworkers": "Find and invite your co-workers",
"find_friends": "Find and invite your friends",
"find_friends_action": "Find friends",
"find_friends_description": "Its what youre here for, so lets get to it",
"find_people": "Find people",
"free_e2ee_messaging_unlimited_voip": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.",
"get_stuff_done": "Get stuff done by finding your teammates",
"google_trademarks": "Google Play and the Google Play logo are trademarks of Google LLC.",
"has_avatar_label": "Great, that'll help people know it's you",
"intro_byline": "Own your conversations.",
"intro_welcome": "Welcome to %(appName)s",
"no_avatar_label": "Add a photo so people know it's you.",
"only_n_steps_to_go": {
"one": "Only %(count)s step to go",
"other": "Only %(count)s steps to go"
},
"personal_messaging_action": "Start your first chat",
"personal_messaging_title": "Secure messaging for friends and family",
"qr_or_app_links": "%(qrCode)s or %(appLinks)s",
"send_dm": "Send a Direct Message",
"set_up_profile": "Set up your profile",
"set_up_profile_action": "Your profile",
"set_up_profile_description": "Make sure people know its really you",
"use_case_community_messaging": "Online community members",
"use_case_heading1": "You're in",
"use_case_heading2": "Who will you chat to the most?",
"use_case_heading3": "We'll help you get connected.",
"use_case_personal_messaging": "Friends and family",
"use_case_work_messaging": "Coworkers and teams",
"welcome_detail": "Now, let's help you get started",
"welcome_to_brand": "Welcome to %(brand)s",
"welcome_user": "Welcome %(name)s",
"work_messaging_action": "Find your co-workers",
"work_messaging_title": "Secure messaging for work",
"you_did_it": "You did it!",
"you_made_it": "You made it!"
"welcome_user": "Welcome %(name)s"
},
"pill": {
"permalink_other_room": "Message in %(room)s",
@ -2686,7 +2637,6 @@
"room_directory_heading": "Room directory",
"room_list_heading": "Room list",
"show_avatars_pills": "Show avatars in user, room and event mentions",
"show_checklist_shortcuts": "Show shortcut to welcome checklist above the room list",
"show_polls_button": "Show polls button",
"surround_text": "Surround selected text when typing special characters",
"time_heading": "Displaying time",

View File

@ -38,7 +38,6 @@ import { WatchManager } from "./WatchManager";
import { CustomTheme } from "../theme";
import AnalyticsController from "./controllers/AnalyticsController";
import FallbackIceServerController from "./controllers/FallbackIceServerController";
import { UseCase } from "./enums/UseCase.tsx";
import { IRightPanelForRoomStored } from "../stores/right-panel/RightPanelStoreIPanelState.ts";
import { ILayoutSettings } from "../stores/widgets/WidgetLayoutStore.ts";
import { ReleaseAnnouncementData } from "../stores/ReleaseAnnouncementStore.ts";
@ -280,7 +279,6 @@ export interface Settings {
"analyticsOptIn": IBaseSetting<boolean>;
"pseudonymousAnalyticsOptIn": IBaseSetting<boolean | null>;
"deviceClientInformationOptIn": IBaseSetting<boolean>;
"FTUE.useCaseSelection": IBaseSetting<UseCase | null>;
"Registration.mobileRegistrationHelper": IBaseSetting<boolean>;
"autocompleteDelay": IBaseSetting<number>;
"readMarkerInViewThresholdMs": IBaseSetting<number>;
@ -308,7 +306,6 @@ export interface Settings {
deny?: string[];
}>;
"breadcrumbs": IBaseSetting<boolean>;
"FTUE.userOnboardingButton": IBaseSetting<boolean>;
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
"lowBandwidth": IBaseSetting<boolean>;
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
@ -992,10 +989,6 @@ export const SETTINGS: Settings = {
displayName: _td("settings|security|record_session_details"),
default: false,
},
"FTUE.useCaseSelection": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null,
},
"Registration.mobileRegistrationHelper": {
supportedLevels: [SettingLevel.CONFIG],
default: false,
@ -1086,11 +1079,6 @@ export const SETTINGS: Settings = {
displayName: _td("settings|show_breadcrumbs"),
default: true,
},
"FTUE.userOnboardingButton": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("settings|preferences|show_checklist_shortcuts"),
default: true,
},
"showHiddenEventsInTimeline": {
displayName: _td("devtools|show_hidden_events"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,

View File

@ -1,14 +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.
*/
export enum UseCase {
PersonalMessaging = "PersonalMessaging",
WorkMessaging = "WorkMessaging",
CommunityMessaging = "CommunityMessaging",
Skip = "Skip",
}

View File

@ -1114,19 +1114,6 @@ describe("<MatrixChat />", () => {
// set up keys screen is rendered
expect(screen.getByText("Setting up keys")).toBeInTheDocument();
});
it("should go to use case selection if user just registered", async () => {
loginClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
MatrixClientPeg.setJustRegisteredUserId(userId);
await getComponentAndLogin();
bootstrapDeferred.resolve();
await expect(
screen.findByRole("heading", { name: "You're in", level: 1 }),
).resolves.toBeInTheDocument();
});
});
});

View File

@ -1,72 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 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 from "react";
import { render, screen } from "jest-matrix-react";
import { AppDownloadDialog } from "../../../../../src/components/views/dialogs/AppDownloadDialog";
import SdkConfig, { ConfigOptions } from "../../../../../src/SdkConfig";
describe("AppDownloadDialog", () => {
afterEach(() => {
SdkConfig.reset();
});
it("should render with desktop, ios, android, fdroid buttons by default", () => {
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling fdroid build", () => {
SdkConfig.add({
mobile_builds: {
fdroid: null,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling desktop build", () => {
SdkConfig.add({
desktop_builds: {
available: false,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should allow disabling mobile builds", () => {
SdkConfig.add({
mobile_builds: {
ios: null,
android: null,
fdroid: null,
},
} as ConfigOptions);
const { asFragment } = render(<AppDownloadDialog onFinished={jest.fn()} />);
expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Download on the App Store" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on Google Play" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -1,540 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppDownloadDialog should allow disabling desktop build 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
<a
aria-label="Get it on F-Droid"
class="mx_AccessibleButton"
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should allow disabling fdroid build 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should allow disabling mobile builds 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
/>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`AppDownloadDialog should render with desktop, ios, android, fdroid buttons by default 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_AppDownloadDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Download Element
</h1>
</div>
<div
class="mx_AppDownloadDialog_desktop"
>
<h3
class="mx_Heading_h3"
>
Download Element Desktop
</h3>
<a
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
href="https://element.io/download"
role="button"
tabindex="0"
target="_blank"
>
Download Element Desktop
</a>
</div>
<div
class="mx_AppDownloadDialog_mobile"
>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
iOS
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Download on the App Store"
class="mx_AccessibleButton"
href="https://apps.apple.com/app/vector/id1083446067"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
<div
class="mx_AppDownloadDialog_app"
>
<h3
class="mx_Heading_h3"
>
Android
</h3>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<div
class="mx_AppDownloadDialog_info"
>
or
</div>
<div
class="mx_AppDownloadDialog_links"
>
<a
aria-label="Get it on Google Play"
class="mx_AccessibleButton"
href="https://play.google.com/store/apps/details?id=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
<a
aria-label="Get it on F-Droid"
class="mx_AccessibleButton"
href="https://f-droid.org/repository/browse/?fdid=im.vector.app"
role="button"
tabindex="0"
target="_blank"
>
<div />
</a>
</div>
</div>
</div>
<div
class="mx_AppDownloadDialog_legal"
>
<p>
App Store® and the Apple logo® are trademarks of Apple Inc.
</p>
<p>
Google Play and the Google Play logo are trademarks of Google LLC.
</p>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@ -1,48 +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 { getUserOnboardingCounters } from "../../../../../src/components/views/user-onboarding/UserOnboardingList";
const tasks = [
{
id: "1",
title: "Lorem ipsum",
description: "Lorem ipsum dolor amet.",
completed: true,
},
{
id: "2",
title: "Lorem ipsum",
description: "Lorem ipsum dolor amet.",
completed: false,
},
];
describe("getUserOnboardingCounters()", () => {
it.each([
{
tasks: [],
expectation: {
completed: 0,
waiting: 0,
total: 0,
},
},
{
tasks: tasks,
expectation: {
completed: 1,
waiting: 1,
total: 2,
},
},
])("should calculate counters correctly", ({ tasks, expectation }) => {
const result = getUserOnboardingCounters(tasks);
expect(result).toStrictEqual(expectation);
});
});

View File

@ -1,86 +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 from "react";
import { act, render, RenderResult } from "jest-matrix-react";
import { filterConsole, withClientContextRenderOptions, stubClient } from "../../../../test-utils";
import { UserOnboardingPage } from "../../../../../src/components/views/user-onboarding/UserOnboardingPage";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SdkConfig from "../../../../../src/SdkConfig";
jest.mock("../../../../../src/components/structures/EmbeddedPage", () => ({
__esModule: true,
default: ({ url }: { url: string }) => <div>{url}</div>,
}));
jest.mock("../../../../../src/components/structures/HomePage", () => ({
__esModule: true,
default: () => <div>home page</div>,
}));
describe("UserOnboardingPage", () => {
const renderComponent = async (): Promise<RenderResult> => {
const renderResult = render(<UserOnboardingPage />, withClientContextRenderOptions(MatrixClientPeg.safeGet()));
await act(async () => {
jest.runAllTimers();
});
return renderResult;
};
filterConsole(
// unrelated for this test
"could not update user onboarding context",
);
beforeEach(() => {
stubClient();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});
describe("when the user registered before the cutoff date", () => {
beforeEach(() => {
jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(false);
});
it("should render the home page", async () => {
expect((await renderComponent()).queryByText("home page")).toBeInTheDocument();
});
});
describe("when the user registered after the cutoff date", () => {
beforeEach(() => {
jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(true);
});
describe("and there is an explicit home page configured", () => {
beforeEach(() => {
jest.spyOn(SdkConfig, "get").mockReturnValue({
embedded_pages: {
home_url: "https://example.com/home",
},
});
});
it("should render the configured page", async () => {
expect((await renderComponent()).queryByText("https://example.com/home")).toBeInTheDocument();
});
});
describe("and there is no home page configured", () => {
it("should render the onboarding", async () => {
expect((await renderComponent()).queryByTestId("user-onboarding-list")).toBeInTheDocument();
});
});
});
});

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 React from "react";
import { renderHook, waitFor } from "jest-matrix-react";
import { useUserOnboardingTasks } from "../../../src/hooks/useUserOnboardingTasks";
import { useUserOnboardingContext } from "../../../src/hooks/useUserOnboardingContext";
import { stubClient } from "../../test-utils";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import PlatformPeg from "../../../src/PlatformPeg";
describe("useUserOnboardingTasks", () => {
it.each([
{
context: {
hasAvatar: false,
hasDevices: false,
hasDmRooms: false,
showNotificationsPrompt: false,
},
},
{
context: {
hasAvatar: true,
hasDevices: false,
hasDmRooms: false,
showNotificationsPrompt: true,
},
},
])("sequence should stay static", async ({ context }) => {
const { result } = renderHook(() => useUserOnboardingTasks(context));
expect(result.current).toHaveLength(5);
expect(result.current[0].id).toBe("create-account");
expect(result.current[1].id).toBe("find-friends");
expect(result.current[2].id).toBe("download-apps");
expect(result.current[3].id).toBe("setup-profile");
expect(result.current[4].id).toBe("permission-notifications");
});
it("should mark desktop notifications task completed on click", async () => {
jest.spyOn(PlatformPeg, "get").mockReturnValue({
supportsNotifications: jest.fn().mockReturnValue(true),
maySendNotifications: jest.fn().mockReturnValue(false),
} as any);
const cli = stubClient();
cli.pushRules = {
global: {
override: [
{
rule_id: ".m.rule.master",
enabled: false,
actions: [],
default: true,
},
],
},
};
DMRoomMap.makeShared(cli);
const context = renderHook(() => useUserOnboardingContext(), {
wrapper: (props) => {
return <MatrixClientContext.Provider value={cli}>{props.children}</MatrixClientContext.Provider>;
},
});
const { result, rerender } = renderHook(() => useUserOnboardingTasks(context.result.current));
expect(result.current[4].id).toBe("permission-notifications");
expect(result.current[4].completed).toBe(false);
result.current[4].action!.onClick!({ type: "click" } as any);
await waitFor(() => {
rerender();
expect(result.current[4].completed).toBe(true);
});
});
});