The Welcome Home Screen: Return Button (#9089)

* Implement button to return to user onboarding screen
* Add analytics events
* Increase stability of lazy loading test
pull/28788/head^2
Janne Mareike Koschinski 2022-08-12 12:55:31 +02:00 committed by GitHub
parent 8db7766a40
commit 9eaf48b176
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 241 additions and 28 deletions

View File

@ -40,6 +40,13 @@ describe("User Onboarding (new user)", () => {
bot1 = _bot1;
});
cy.get('.mx_UserOnboardingPage').should('exist');
cy.get('.mx_UserOnboardingButton').should('exist');
cy.get('.mx_UserOnboardingList')
.should('exist')
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
});
});
});
@ -47,20 +54,14 @@ describe("User Onboarding (new user)", () => {
cy.stopSynapse(synapse);
});
it("page is shown", () => {
cy.get('.mx_UserOnboardingPage').should('exist');
cy.get('.mx_UserOnboardingList')
.should('exist')
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
});
it("page is shown and preference exists", () => {
cy.get('.mx_UserOnboardingPage')
.percySnapshotElement("User onboarding page");
cy.openUserSettings("Preferences");
cy.contains("Show shortcut to welcome checklist above the room list").should("exist");
});
it("app download dialog", () => {
cy.get('.mx_UserOnboardingPage').should('exist');
cy.contains(".mx_UserOnboardingTask_action", "Download apps").click();
cy.get('[role=dialog]')
.contains("#mx_BaseDialog_title", "Download Element")
@ -79,8 +80,7 @@ describe("User Onboarding (new user)", () => {
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
cy.visit("/#/home");
cy.get(".mx_SendMessageComposer").type("Hi!{enter}");
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
});
});

View File

@ -40,7 +40,10 @@ describe("User Onboarding (old user)", () => {
cy.stopSynapse(synapse);
});
it("page is hidden", () => {
it("page and preference are hidden", () => {
cy.get('.mx_UserOnboardingPage').should('not.exist');
cy.get('.mx_UserOnboardingButton').should('not.exist');
cy.openUserSettings("Preferences");
cy.contains("Show shortcut to welcome page above the room list").should("not.exist");
});
});

View File

@ -337,6 +337,7 @@
@import "./views/user-onboarding/_UserOnboardingList.pcss";
@import "./views/user-onboarding/_UserOnboardingPage.pcss";
@import "./views/user-onboarding/_UserOnboardingTask.pcss";
@import "./views/user-onboarding/_UserOnboardingButton.pcss";
@import "./views/verification/_VerificationShowSas.pcss";
@import "./views/voip/CallView/_CallViewButtons.pcss";
@import "./views/voip/_CallPreview.pcss";

View File

@ -0,0 +1,84 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.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-size: $font-14px;
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: contain;
width: 7px;
height: 7px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
mask-image: url("$(res)/img/element-icons/cancel-rounded.svg");
}
}
}
.mx_ProgressBar {
width: auto;
margin-top: $spacing-8;
background: $background;
}
&.mx_UserOnboardingButton_completed .mx_ProgressBar {
display: none;
}
}

View File

@ -45,9 +45,12 @@ import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { UIComponent } from "../../settings/UIFeature";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import PosthogTrackers from "../../PosthogTrackers";
import PageType from "../../PageTypes";
import { UserOnboardingButton } from "../views/user-onboarding/UserOnboardingButton";
interface IProps {
isMinimized: boolean;
pageType: PageType;
resizeNotifier: ResizeNotifier;
}
@ -390,6 +393,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onVisibilityChange={this.refreshStickyHeaders}
/>
) }
<UserOnboardingButton
selected={this.props.pageType === PageType.HomePage}
minimized={this.props.isMinimized}
/>
<div className="mx_LeftPanel_roomListWrapper">
<div
className={roomListClasses}

View File

@ -689,6 +689,7 @@ class LoggedInView extends React.Component<IProps, IState> {
data-collapsed={this.props.collapseLhs ? true : undefined}
>
<LeftPanel
pageType={this.props.page_type as PageTypes}
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>

View File

@ -46,6 +46,7 @@ interface IState {
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS = [
'breadcrumbs',
"FTUE.userOnboardingButton",
];
private static SPACES_SETTINGS = [

View File

@ -0,0 +1,102 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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 { useUserOnboardingContext } from "../../../hooks/useUserOnboardingContext";
import { useUserOnboardingTasks } from "../../../hooks/useUserOnboardingTasks";
import { _t } from "../../../languageHandler";
import PosthogTrackers from "../../../PosthogTrackers";
import { UseCase } from "../../../settings/enums/UseCase";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import ProgressBar from "../../views/elements/ProgressBar";
import Heading from "../../views/typography/Heading";
import { showUserOnboardingPage } from "./UserOnboardingPage";
function toPercentage(progress: number): string {
return (progress * 100).toFixed(0);
}
interface Props {
selected: boolean;
minimized: boolean;
}
export function UserOnboardingButton({ selected, minimized }: Props) {
const context = useUserOnboardingContext();
const [completedTasks, waitingTasks] = useUserOnboardingTasks(context);
const completed = completedTasks.length;
const waiting = waitingTasks.length;
const total = completed + waiting;
let progress = 1;
if (context && waiting) {
progress = completed / total;
}
const onDismiss = useCallback((ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingIgnoreButton", ev);
SettingsStore.setValue("FTUE.userOnboardingButton", null, SettingLevel.ACCOUNT, false);
}, []);
const onClick = useCallback((ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingButton", ev);
defaultDispatcher.fire(Action.ViewHomePage);
}, []);
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
return null;
}
return (
<AccessibleButton
className={classNames("mx_UserOnboardingButton", {
"mx_UserOnboardingButton_selected": selected,
"mx_UserOnboardingButton_minimized": minimized,
"mx_UserOnboardingButton_completed": !waiting || !context,
})}
onClick={onClick}>
{ !minimized && (
<>
<div className="mx_UserOnboardingButton_content">
<Heading size="h4" className="mx_Heading_h4">
{ _t("Welcome") }
</Heading>
{ context && !completed && (
<div className="mx_UserOnboardingButton_percentage">
{ toPercentage(progress) }%
</div>
) }
<AccessibleButton
className="mx_UserOnboardingButton_close"
onClick={onDismiss}
/>
</div>
<ProgressBar value={completed} max={total} animated />
</>
) }
</AccessibleButton>
);
}

View File

@ -19,6 +19,7 @@ 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";
@ -47,7 +48,8 @@ export function UserOnboardingPage({ justRegistered = false }: Props) {
const pageUrl = getHomePageUrl(config);
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
const [completedTasks, waitingTasks] = useUserOnboardingTasks();
const context = useUserOnboardingContext();
const [completedTasks, waitingTasks] = useUserOnboardingTasks(context);
const initialSyncComplete = useInitialSyncComplete();
const [showList, setShowList] = useState<boolean>(false);

View File

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, IMyDevice, Room } from "matrix-js-sdk/src/matrix";
import { useCallback, useEffect, useState } from "react";
import { MatrixClientPeg } from "../MatrixClientPeg";
import DMRoomMap from "../utils/DMRoomMap";
@ -33,18 +34,23 @@ export function useUserOnboardingContext(): UserOnboardingContext | null {
const cli = MatrixClientPeg.get();
const handler = useCallback(async () => {
const profile = await cli.getProfileInfo(cli.getUserId());
try {
const profile = await cli.getProfileInfo(cli.getUserId());
const myDevice = cli.getDeviceId();
const devices = await cli.getDevices();
const myDevice = cli.getDeviceId();
const devices = await cli.getDevices();
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
setContext({
avatar: profile?.avatar_url ?? null,
myDevice,
devices: devices.devices,
dmRooms: dmRooms,
});
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
setContext({
avatar: profile?.avatar_url ?? null,
myDevice,
devices: devices.devices,
dmRooms: dmRooms,
});
} catch (e) {
logger.warn("Could not load context for user onboarding task list: ", e);
setContext(null);
}
}, [cli]);
useEventEmitter(cli, ClientEvent.AccountData, handler);

View File

@ -27,7 +27,7 @@ import { Notifier } from "../Notifier";
import PosthogTrackers from "../PosthogTrackers";
import { UseCase } from "../settings/enums/UseCase";
import { useSettingValue } from "./useSettings";
import { UserOnboardingContext, useUserOnboardingContext } from "./useUserOnboardingContext";
import { UserOnboardingContext } from "./useUserOnboardingContext";
export interface UserOnboardingTask {
id: string;
@ -140,13 +140,12 @@ const tasks: InternalUserOnboardingTask[] = [
},
];
export function useUserOnboardingTasks(): [UserOnboardingTask[], UserOnboardingTask[]] {
export function useUserOnboardingTasks(context: UserOnboardingContext): [UserOnboardingTask[], UserOnboardingTask[]] {
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection") ?? UseCase.Skip;
const relevantTasks = useMemo(
() => tasks.filter(it => !it.relevant || it.relevant.includes(useCase)),
[useCase],
);
const onboardingInfo = useUserOnboardingContext();
const completedTasks = relevantTasks.filter(it => onboardingInfo && it.completed(onboardingInfo));
const completedTasks = relevantTasks.filter(it => context && it.completed(context));
return [completedTasks, relevantTasks.filter(it => !completedTasks.includes(it))];
}

View File

@ -951,6 +951,7 @@
"Order rooms by name": "Order rooms by name",
"Show rooms with unread notifications first": "Show rooms with unread notifications first",
"Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list",
"Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list",
"Show hidden events in timeline": "Show hidden events in timeline",
"Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
@ -1147,6 +1148,7 @@
"Anchor": "Anchor",
"Headphones": "Headphones",
"Folder": "Folder",
"Welcome": "Welcome",
"How are you finding Element so far?": "How are you finding Element so far?",
"Wed appreciate any feedback on how youre finding Element.": "Wed appreciate any feedback on how youre finding Element.",
"Feedback": "Feedback",

View File

@ -804,6 +804,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: true,
controller: new IncompatibleController("feature_breadcrumbs_v2", true),
},
"FTUE.userOnboardingButton": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Show shortcut to welcome checklist above the room list"),
default: true,
},
"showHiddenEventsInTimeline": {
displayName: _td("Show hidden events in timeline"),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,