Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 94d292a

Browse files
authored
Reduce amount of requests done by the onboarding task list (#9194)
* Significantly reduce work of useUserOnboardingContext * Wrap UserOnboardingButton to avoid unnecessary work when it's not shown * Remove progress from user onboarding button
1 parent e8eefeb commit 94d292a

File tree

4 files changed

+109
-73
lines changed

4 files changed

+109
-73
lines changed

cypress/e2e/user-onboarding/user-onboarding-new.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,18 @@ describe("User Onboarding (new user)", () => {
8080
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
8181
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
8282
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
83-
cy.get(".mx_SendMessageComposer").type("Hi!{enter}");
83+
const message = "Hi!";
84+
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
85+
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
86+
cy.visit("/#/home");
87+
cy.get('.mx_UserOnboardingPage').should('exist');
88+
cy.get('.mx_UserOnboardingButton').should('exist');
89+
cy.get('.mx_UserOnboardingList')
90+
.should('exist')
91+
.should(($list) => {
92+
const list = $list.get(0);
93+
expect(getComputedStyle(list).opacity).to.be.eq("1");
94+
});
8495
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
8596
});
8697
});

src/components/views/user-onboarding/UserOnboardingButton.tsx

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,62 +20,55 @@ import React, { useCallback } from "react";
2020
import { Action } from "../../../dispatcher/actions";
2121
import defaultDispatcher from "../../../dispatcher/dispatcher";
2222
import { useSettingValue } from "../../../hooks/useSettings";
23-
import { useUserOnboardingContext } from "../../../hooks/useUserOnboardingContext";
24-
import { useUserOnboardingTasks } from "../../../hooks/useUserOnboardingTasks";
2523
import { _t } from "../../../languageHandler";
2624
import PosthogTrackers from "../../../PosthogTrackers";
2725
import { UseCase } from "../../../settings/enums/UseCase";
2826
import { SettingLevel } from "../../../settings/SettingLevel";
2927
import SettingsStore from "../../../settings/SettingsStore";
3028
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
31-
import ProgressBar from "../../views/elements/ProgressBar";
3229
import Heading from "../../views/typography/Heading";
3330
import { showUserOnboardingPage } from "./UserOnboardingPage";
3431

35-
function toPercentage(progress: number): string {
36-
return (progress * 100).toFixed(0);
37-
}
38-
3932
interface Props {
4033
selected: boolean;
4134
minimized: boolean;
4235
}
4336

4437
export function UserOnboardingButton({ selected, minimized }: Props) {
45-
const context = useUserOnboardingContext();
46-
const [completedTasks, waitingTasks] = useUserOnboardingTasks(context);
47-
48-
const completed = completedTasks.length;
49-
const waiting = waitingTasks.length;
50-
const total = completed + waiting;
38+
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
39+
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");
5140

52-
let progress = 1;
53-
if (context && waiting) {
54-
progress = completed / total;
41+
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
42+
return null;
5543
}
5644

45+
return (
46+
<UserOnboardingButtonInternal selected={selected} minimized={minimized} />
47+
);
48+
}
49+
50+
function UserOnboardingButtonInternal({ selected, minimized }: Props) {
5751
const onDismiss = useCallback((ev: ButtonEvent) => {
52+
ev.preventDefault();
53+
ev.stopPropagation();
54+
5855
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingIgnoreButton", ev);
5956
SettingsStore.setValue("FTUE.userOnboardingButton", null, SettingLevel.ACCOUNT, false);
6057
}, []);
6158

6259
const onClick = useCallback((ev: ButtonEvent) => {
60+
ev.preventDefault();
61+
ev.stopPropagation();
62+
6363
PosthogTrackers.trackInteraction("WebRoomListUserOnboardingButton", ev);
6464
defaultDispatcher.fire(Action.ViewHomePage);
6565
}, []);
6666

67-
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
68-
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");
69-
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
70-
return null;
71-
}
72-
7367
return (
7468
<AccessibleButton
7569
className={classNames("mx_UserOnboardingButton", {
7670
"mx_UserOnboardingButton_selected": selected,
7771
"mx_UserOnboardingButton_minimized": minimized,
78-
"mx_UserOnboardingButton_completed": !waiting || !context,
7972
})}
8073
onClick={onClick}>
8174
{ !minimized && (
@@ -84,17 +77,11 @@ export function UserOnboardingButton({ selected, minimized }: Props) {
8477
<Heading size="h4" className="mx_Heading_h4">
8578
{ _t("Welcome") }
8679
</Heading>
87-
{ context && !completed && (
88-
<div className="mx_UserOnboardingButton_percentage">
89-
{ toPercentage(progress) }%
90-
</div>
91-
) }
9280
<AccessibleButton
9381
className="mx_UserOnboardingButton_close"
9482
onClick={onDismiss}
9583
/>
9684
</div>
97-
<ProgressBar value={completed} max={total} animated />
9885
</>
9986
) }
10087
</AccessibleButton>

src/hooks/useUserOnboardingContext.ts

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,54 +15,96 @@ limitations under the License.
1515
*/
1616

1717
import { logger } from "matrix-js-sdk/src/logger";
18-
import { ClientEvent, IMyDevice, Room } from "matrix-js-sdk/src/matrix";
19-
import { useCallback, useEffect, useState } from "react";
18+
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
19+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2020

2121
import { MatrixClientPeg } from "../MatrixClientPeg";
22+
import { Notifier } from "../Notifier";
2223
import DMRoomMap from "../utils/DMRoomMap";
23-
import { useEventEmitter } from "./useEventEmitter";
2424

2525
export interface UserOnboardingContext {
26-
avatar: string | null;
27-
myDevice: string;
28-
devices: IMyDevice[];
29-
dmRooms: {[userId: string]: Room};
26+
hasAvatar: boolean;
27+
hasDevices: boolean;
28+
hasDmRooms: boolean;
29+
hasNotificationsEnabled: boolean;
3030
}
3131

32-
export function useUserOnboardingContext(): UserOnboardingContext | null {
33-
const [context, setContext] = useState<UserOnboardingContext | null>(null);
32+
const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;
33+
34+
/**
35+
* Returns a persistent, non-changing reference to a function
36+
* This function proxies all its calls to the current value of the given input callback
37+
*
38+
* This allows you to use the current value of e.g., a state in a callback that’s used by e.g., a useEventEmitter or
39+
* similar hook without re-registering the hook when the state changes
40+
* @param value changing callback
41+
*/
42+
function useRefOf<T extends any[], R>(value: (...values: T) => R): (...values: T) => R {
43+
const ref = useRef(value);
44+
ref.current = value;
45+
return useCallback(
46+
(...values: T) => ref.current(...values),
47+
[],
48+
);
49+
}
3450

51+
function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: MatrixClient) => Promise<T>): T {
52+
const [value, setValue] = useState<T>(defaultValue);
3553
const cli = MatrixClientPeg.get();
36-
const handler = useCallback(async () => {
37-
try {
38-
const profile = await cli.getProfileInfo(cli.getUserId());
3954

40-
const myDevice = cli.getDeviceId();
41-
const devices = await cli.getDevices();
55+
const handler = useRefOf(callback);
4256

43-
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
44-
setContext({
45-
avatar: profile?.avatar_url ?? null,
46-
myDevice,
47-
devices: devices.devices,
48-
dmRooms: dmRooms,
49-
});
50-
} catch (e) {
51-
logger.warn("Could not load context for user onboarding task list: ", e);
52-
setContext(null);
57+
useEffect(() => {
58+
if (value) {
59+
return;
5360
}
54-
}, [cli]);
5561

56-
useEventEmitter(cli, ClientEvent.AccountData, handler);
57-
useEffect(() => {
58-
const handle = setInterval(handler, 2000);
59-
handler();
62+
let handle: number | null = null;
63+
let enabled = true;
64+
const repeater = async () => {
65+
if (handle !== null) {
66+
clearTimeout(handle);
67+
handle = null;
68+
}
69+
setValue(await handler(cli));
70+
if (enabled) {
71+
handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
72+
}
73+
};
74+
repeater().catch(err => logger.warn("could not update user onboarding context", err));
75+
cli.on(ClientEvent.AccountData, repeater);
6076
return () => {
61-
if (handle) {
62-
clearInterval(handle);
77+
enabled = false;
78+
cli.off(ClientEvent.AccountData, repeater);
79+
if (handle !== null) {
80+
clearTimeout(handle);
81+
handle = null;
6382
}
6483
};
65-
}, [handler]);
84+
}, [cli, handler, value]);
85+
return value;
86+
}
87+
88+
export function useUserOnboardingContext(): UserOnboardingContext | null {
89+
const hasAvatar = useUserOnboardingContextValue(false, async (cli) => {
90+
const profile = await cli.getProfileInfo(cli.getUserId());
91+
return Boolean(profile?.avatar_url);
92+
});
93+
const hasDevices = useUserOnboardingContextValue(false, async (cli) => {
94+
const myDevice = cli.getDeviceId();
95+
const devices = await cli.getDevices();
96+
return Boolean(devices.devices.find(device => device.device_id !== myDevice));
97+
});
98+
const hasDmRooms = useUserOnboardingContextValue(false, async () => {
99+
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
100+
return Boolean(Object.keys(dmRooms).length);
101+
});
102+
const hasNotificationsEnabled = useUserOnboardingContextValue(false, async () => {
103+
return Notifier.isPossible();
104+
});
66105

67-
return context;
106+
return useMemo(
107+
() => ({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled }),
108+
[hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled],
109+
);
68110
}

src/hooks/useUserOnboardingTasks.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ interface InternalUserOnboardingTask extends UserOnboardingTask {
4747
completed: (ctx: UserOnboardingContext) => boolean;
4848
}
4949

50-
const hasOpenDMs = (ctx: UserOnboardingContext) => Boolean(Object.entries(ctx.dmRooms).length);
51-
5250
const onClickStartDm = (ev: ButtonEvent) => {
5351
PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev);
5452
defaultDispatcher.dispatch({ action: 'view_create_chat' });
@@ -65,7 +63,7 @@ const tasks: InternalUserOnboardingTask[] = [
6563
id: "find-friends",
6664
title: _t("Find and invite your friends"),
6765
description: _t("It’s what you’re here for, so lets get to it"),
68-
completed: hasOpenDMs,
66+
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
6967
relevant: [UseCase.PersonalMessaging, UseCase.Skip],
7068
action: {
7169
label: _t("Find friends"),
@@ -76,7 +74,7 @@ const tasks: InternalUserOnboardingTask[] = [
7674
id: "find-coworkers",
7775
title: _t("Find and invite your co-workers"),
7876
description: _t("Get stuff done by finding your teammates"),
79-
completed: hasOpenDMs,
77+
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
8078
relevant: [UseCase.WorkMessaging],
8179
action: {
8280
label: _t("Find people"),
@@ -87,7 +85,7 @@ const tasks: InternalUserOnboardingTask[] = [
8785
id: "find-community-members",
8886
title: _t("Find and invite your community members"),
8987
description: _t("Get stuff done by finding your teammates"),
90-
completed: hasOpenDMs,
88+
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
9189
relevant: [UseCase.CommunityMessaging],
9290
action: {
9391
label: _t("Find people"),
@@ -102,9 +100,7 @@ const tasks: InternalUserOnboardingTask[] = [
102100
description: () => _t("Don’t miss a thing by taking %(brand)s with you", {
103101
brand: SdkConfig.get("brand"),
104102
}),
105-
completed: (ctx: UserOnboardingContext) => {
106-
return Boolean(ctx.devices.filter(it => it.device_id !== ctx.myDevice).length);
107-
},
103+
completed: (ctx: UserOnboardingContext) => ctx.hasDevices,
108104
action: {
109105
label: _t("Download apps"),
110106
onClick: (ev: ButtonEvent) => {
@@ -117,7 +113,7 @@ const tasks: InternalUserOnboardingTask[] = [
117113
id: "setup-profile",
118114
title: _t("Set up your profile"),
119115
description: _t("Make sure people know it’s really you"),
120-
completed: (info: UserOnboardingContext) => Boolean(info.avatar),
116+
completed: (ctx: UserOnboardingContext) => ctx.hasAvatar,
121117
action: {
122118
label: _t("Your profile"),
123119
onClick: (ev: ButtonEvent) => {
@@ -133,7 +129,7 @@ const tasks: InternalUserOnboardingTask[] = [
133129
id: "permission-notifications",
134130
title: _t("Turn on notifications"),
135131
description: _t("Don’t miss a reply or important message"),
136-
completed: () => Notifier.isPossible(),
132+
completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled,
137133
action: {
138134
label: _t("Enable notifications"),
139135
onClick: (ev: ButtonEvent) => {

0 commit comments

Comments
 (0)