From 9eaf48b176a7ffbee7d942a5b0cafe5470b72614 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 12 Aug 2022 12:55:31 +0200 Subject: [PATCH] The Welcome Home Screen: Return Button (#9089) * Implement button to return to user onboarding screen * Add analytics events * Increase stability of lazy loading test --- .../user-onboarding/user-onboarding-new.ts | 22 ++-- .../user-onboarding/user-onboarding-old.ts | 5 +- res/css/_components.pcss | 1 + .../_UserOnboardingButton.pcss | 84 +++++++++++++++ src/components/structures/LeftPanel.tsx | 7 ++ src/components/structures/LoggedInView.tsx | 1 + .../tabs/user/PreferencesUserSettingsTab.tsx | 1 + .../user-onboarding/UserOnboardingButton.tsx | 102 ++++++++++++++++++ .../user-onboarding/UserOnboardingPage.tsx | 4 +- src/hooks/useUserOnboardingContext.ts | 32 +++--- src/hooks/useUserOnboardingTasks.ts | 7 +- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 5 + 13 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 res/css/views/user-onboarding/_UserOnboardingButton.pcss create mode 100644 src/components/views/user-onboarding/UserOnboardingButton.tsx diff --git a/cypress/e2e/user-onboarding/user-onboarding-new.ts b/cypress/e2e/user-onboarding/user-onboarding-new.ts index 44787ee61e8..cef27803edf 100644 --- a/cypress/e2e/user-onboarding/user-onboarding-new.ts +++ b/cypress/e2e/user-onboarding/user-onboarding-new.ts @@ -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); }); }); diff --git a/cypress/e2e/user-onboarding/user-onboarding-old.ts b/cypress/e2e/user-onboarding/user-onboarding-old.ts index 2be066e0a1c..f079ed9a4c3 100644 --- a/cypress/e2e/user-onboarding/user-onboarding-old.ts +++ b/cypress/e2e/user-onboarding/user-onboarding-old.ts @@ -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"); }); }); diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 5e43b2810db..0e5c5fd3a59 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -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"; diff --git a/res/css/views/user-onboarding/_UserOnboardingButton.pcss b/res/css/views/user-onboarding/_UserOnboardingButton.pcss new file mode 100644 index 00000000000..3eba86045ac --- /dev/null +++ b/res/css/views/user-onboarding/_UserOnboardingButton.pcss @@ -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; + } +} diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 94f1b9e0eee..bd120529a90 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -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 { onVisibilityChange={this.refreshStickyHeaders} /> ) } +
{ data-collapsed={this.props.collapseLhs ? true : undefined} > diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 5a7aeba565d..2322d705102 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -46,6 +46,7 @@ interface IState { export default class PreferencesUserSettingsTab extends React.Component { private static ROOM_LIST_SETTINGS = [ 'breadcrumbs', + "FTUE.userOnboardingButton", ]; private static SPACES_SETTINGS = [ diff --git a/src/components/views/user-onboarding/UserOnboardingButton.tsx b/src/components/views/user-onboarding/UserOnboardingButton.tsx new file mode 100644 index 00000000000..26904829e85 --- /dev/null +++ b/src/components/views/user-onboarding/UserOnboardingButton.tsx @@ -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("FTUE.useCaseSelection"); + const visible = useSettingValue("FTUE.userOnboardingButton"); + if (!visible || minimized || !showUserOnboardingPage(useCase)) { + return null; + } + + return ( + + { !minimized && ( + <> +
+ + { _t("Welcome") } + + { context && !completed && ( +
+ { toPercentage(progress) }% +
+ ) } + +
+ + + ) } +
+ ); +} diff --git a/src/components/views/user-onboarding/UserOnboardingPage.tsx b/src/components/views/user-onboarding/UserOnboardingPage.tsx index 7ca13232986..cc90a3d09d5 100644 --- a/src/components/views/user-onboarding/UserOnboardingPage.tsx +++ b/src/components/views/user-onboarding/UserOnboardingPage.tsx @@ -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("FTUE.useCaseSelection"); - const [completedTasks, waitingTasks] = useUserOnboardingTasks(); + const context = useUserOnboardingContext(); + const [completedTasks, waitingTasks] = useUserOnboardingTasks(context); const initialSyncComplete = useInitialSyncComplete(); const [showList, setShowList] = useState(false); diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index 8b1d6bcfb4f..d1f45217f81 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -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()); - - 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, - }); + try { + const profile = await cli.getProfileInfo(cli.getUserId()); + + 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, + }); + } catch (e) { + logger.warn("Could not load context for user onboarding task list: ", e); + setContext(null); + } }, [cli]); useEventEmitter(cli, ClientEvent.AccountData, handler); diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index daef154de02..9ac0b5d98b5 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -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("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))]; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5b53f989786..7c3483e09e7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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?", "We’d appreciate any feedback on how you’re finding Element.": "We’d appreciate any feedback on how you’re finding Element.", "Feedback": "Feedback", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1a3f361c1a8..004dfe9d488 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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,