The Welcome Home Screen: Return Button (#9089)
* Implement button to return to user onboarding screen * Add analytics events * Increase stability of lazy loading testpull/28788/head^2
parent
8db7766a40
commit
9eaf48b176
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))];
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue