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

Reduce amount of requests done by the onboarding task list #9194

Merged
merged 12 commits into from
Aug 22, 2022
13 changes: 12 additions & 1 deletion cypress/e2e/user-onboarding/user-onboarding-new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,18 @@ 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.get(".mx_SendMessageComposer").type("Hi!{enter}");
const message = "Hi!";
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
cy.visit("/#/home");
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");
});
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
});
});
Expand Down
45 changes: 16 additions & 29 deletions src/components/views/user-onboarding/UserOnboardingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,62 +20,55 @@ 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;
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");

let progress = 1;
if (context && waiting) {
progress = completed / total;
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
return null;
}

return (
<UserOnboardingButtonInternal selected={selected} minimized={minimized} />
);
}

function UserOnboardingButtonInternal({ selected, minimized }: Props) {
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);
}, []);

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 && (
Expand All @@ -84,17 +77,11 @@ export function UserOnboardingButton({ selected, minimized }: Props) {
<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>
Expand Down
111 changes: 78 additions & 33 deletions src/hooks/useUserOnboardingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,99 @@ limitations under the License.
*/

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 { ClientEvent } from "matrix-js-sdk/src/matrix";
import { useCallback, useEffect, useRef, useState } from "react";

import { MatrixClientPeg } from "../MatrixClientPeg";
import { Notifier } from "../Notifier";
import DMRoomMap from "../utils/DMRoomMap";
import { useEventEmitter } from "./useEventEmitter";

export interface UserOnboardingContext {
avatar: string | null;
myDevice: string;
devices: IMyDevice[];
dmRooms: {[userId: string]: Room};
hasAvatar: boolean;
hasDevices: boolean;
hasDmRooms: boolean;
hasNotificationsEnabled: boolean;
}

const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;

function useRefOf<T extends [], R>(value: (...values: T) => R): (...values: T) => R {
const ref = useRef(value);
ref.current = value;
return useCallback(
(...values: T) => ref.current(...values),
[],
);
justjanne marked this conversation as resolved.
Show resolved Hide resolved
}

export function useUserOnboardingContext(): UserOnboardingContext | null {
const [context, setContext] = useState<UserOnboardingContext | null>(null);

const cli = MatrixClientPeg.get();
const handler = useCallback(async () => {
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);
const handler = useRefOf(
useCallback(async () => {
justjanne marked this conversation as resolved.
Show resolved Hide resolved
try {
let hasAvatar = context?.hasAvatar;
if (!hasAvatar) {
const profile = await cli.getProfileInfo(cli.getUserId());
hasAvatar = Boolean(profile?.avatar_url);
}

let hasDevices = context?.hasDevices;
if (!hasDevices) {
const myDevice = cli.getDeviceId();
const devices = await cli.getDevices();
hasDevices = Boolean(devices.devices.find(device => device.device_id !== myDevice));
}
justjanne marked this conversation as resolved.
Show resolved Hide resolved

let hasDmRooms = context?.hasDmRooms;
if (!hasDmRooms) {
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
hasDmRooms = Boolean(Object.keys(dmRooms).length);
}

let hasNotificationsEnabled = context?.hasNotificationsEnabled;
if (!hasNotificationsEnabled) {
hasNotificationsEnabled = Notifier.isPossible();
}

if (hasAvatar !== context?.hasAvatar
|| hasDevices !== context?.hasDevices
|| hasDmRooms !== context?.hasDmRooms
|| hasNotificationsEnabled !== context?.hasNotificationsEnabled) {
setContext({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled });
}
} catch (e) {
logger.warn("Could not load context for user onboarding task list: ", e);
setContext(null);
}
}, [context, cli]),
);

useEffect(() => {
const handle = setInterval(handler, 2000);
handler();
let handle: number | null;
justjanne marked this conversation as resolved.
Show resolved Hide resolved
let enabled = true;
const repeater = async () => {
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
await handler();
if (enabled) {
handle = setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
}
};
repeater().catch(err => logger.warn("could not update user onboarding context", err));
cli.on(ClientEvent.AccountData, repeater);
return () => {
if (handle) {
clearInterval(handle);
enabled = false;
cli.off(ClientEvent.AccountData, repeater);
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
};
}, [handler]);
}, [cli, handler]);

return context;
}
16 changes: 6 additions & 10 deletions src/hooks/useUserOnboardingTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ interface InternalUserOnboardingTask extends UserOnboardingTask {
completed: (ctx: UserOnboardingContext) => boolean;
}

const hasOpenDMs = (ctx: UserOnboardingContext) => Boolean(Object.entries(ctx.dmRooms).length);

const onClickStartDm = (ev: ButtonEvent) => {
PosthogTrackers.trackInteraction("WebUserOnboardingTaskSendDm", ev);
defaultDispatcher.dispatch({ action: 'view_create_chat' });
Expand All @@ -64,7 +62,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "find-friends",
title: _t("Find and invite your friends"),
description: _t("It’s what you’re here for, so lets get to it"),
completed: hasOpenDMs,
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.PersonalMessaging, UseCase.Skip],
action: {
label: _t("Find friends"),
Expand All @@ -75,7 +73,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "find-coworkers",
title: _t("Find and invite your co-workers"),
description: _t("Get stuff done by finding your teammates"),
completed: hasOpenDMs,
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.WorkMessaging],
action: {
label: _t("Find people"),
Expand All @@ -86,7 +84,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "find-community-members",
title: _t("Find and invite your community members"),
description: _t("Get stuff done by finding your teammates"),
completed: hasOpenDMs,
completed: (ctx: UserOnboardingContext) => ctx.hasDmRooms,
relevant: [UseCase.CommunityMessaging],
action: {
label: _t("Find people"),
Expand All @@ -97,9 +95,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "download-apps",
title: _t("Download Element"),
description: _t("Don’t miss a thing by taking Element with you"),
completed: (ctx: UserOnboardingContext) => {
return Boolean(ctx.devices.filter(it => it.device_id !== ctx.myDevice).length);
},
completed: (ctx: UserOnboardingContext) => ctx.hasDevices,
action: {
label: _t("Download apps"),
onClick: (ev: ButtonEvent) => {
Expand All @@ -112,7 +108,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "setup-profile",
title: _t("Set up your profile"),
description: _t("Make sure people know it’s really you"),
completed: (info: UserOnboardingContext) => Boolean(info.avatar),
completed: (ctx: UserOnboardingContext) => ctx.hasAvatar,
action: {
label: _t("Your profile"),
onClick: (ev: ButtonEvent) => {
Expand All @@ -128,7 +124,7 @@ const tasks: InternalUserOnboardingTask[] = [
id: "permission-notifications",
title: _t("Turn on notifications"),
description: _t("Don’t miss a reply or important message"),
completed: () => Notifier.isPossible(),
completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled,
action: {
label: _t("Enable notifications"),
onClick: (ev: ButtonEvent) => {
Expand Down