element-web/src/hooks/useUserOnboardingContext.ts
David Baker 67cb8b7590
Force verification even for refreshed clients (#44)
* Force verification even for refreshed cients

Set a flag on login to remember that the device needs to be verified
so that we don't forget if the user refreshes the page, but still allow
user with an existing unverified session to stay logged in.

* Hopefully make matrixchat tests pass?

Much, much tweaking to make the matrixchat tests pass again. Should
hopefully make them a bit more solid in general with judicious use of
waitFor rather than flushPromises(). Also lots of fun to stop the state
bleeding between tests.

* Manual yarn.lock manipulation

to hopefully resolve infinite package sadness

* Make final test pass(?)

Mock out the createClient method to return the same client, because
we've mocked the peg to always return that client, so if we let the
code make another one having still overridden the peg, everything becomes
cursed.

Also mock out the autodiscovery stuff rather than relying on fetch-mock.

* another waitFor

* death to flushPromises

* Put the logged in dispatch back

Actually it breaks all sorts of other things too, having fixed all the
MatrixChat tests (although this is useful anyway).

* Try displaying the screen in onClientStarted instead

* Put post login screen back in logged in

but move ready transition to avoid flash of main UI

* Rejig more in the hope it does the right thing

* Make hook work before push rules are fetched

* Add test for unskippable verification

* Add test for use case selection

* Fix test

* Add playwright test for unskippable verification

* Remove console log

* Add log message to log line

* Add tsdoc

* Use useTypedEventEmitter

* Remove commented code

* Use catch instead of empty then on unawaited promises

or in one case just await it because the caller was async anyway

* Add new mock
2024-10-03 08:55:06 +00:00

125 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Notifier, NotifierEvent } from "../Notifier";
import DMRoomMap from "../utils/DMRoomMap";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import { useSettingValue } from "./useSettings";
import { useEventEmitter, useTypedEventEmitter } from "./useEventEmitter";
export interface UserOnboardingContext {
hasAvatar: boolean;
hasDevices: boolean;
hasDmRooms: boolean;
showNotificationsPrompt: boolean;
}
const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;
/**
* Returns a persistent, non-changing reference to a function
* This function proxies all its calls to the current value of the given input callback
*
* This allows you to use the current value of e.g., a state in a callback thats used by e.g., a useEventEmitter or
* similar hook without re-registering the hook when the state changes
* @param value changing callback
*/
function useRefOf<T extends any[], R>(value: (...values: T) => R): (...values: T) => R {
const ref = useRef(value);
ref.current = value;
return useCallback((...values: T) => ref.current(...values), []);
}
function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: MatrixClient) => Promise<T>): T {
const [value, setValue] = useState<T>(defaultValue);
const cli = useMatrixClientContext();
const handler = useRefOf(callback);
useEffect(() => {
if (value) {
return;
}
let handle: number | null = null;
let enabled = true;
const repeater = async (): Promise<void> => {
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
setValue(await handler(cli));
if (enabled) {
handle = window.setTimeout(repeater, USER_ONBOARDING_CONTEXT_INTERVAL);
}
};
repeater().catch((err) => logger.warn("could not update user onboarding context", err));
cli.on(ClientEvent.AccountData, repeater);
return () => {
enabled = false;
cli.off(ClientEvent.AccountData, repeater);
if (handle !== null) {
clearTimeout(handle);
handle = null;
}
};
}, [cli, handler, value]);
return value;
}
function useShowNotificationsPrompt(): boolean {
const client = useMatrixClientContext();
const [value, setValue] = useState<boolean>(client.pushRules ? Notifier.shouldShowPrompt() : true);
const updateValue = useCallback(() => {
setValue(client.pushRules ? Notifier.shouldShowPrompt() : true);
}, [client]);
useEventEmitter(Notifier, NotifierEvent.NotificationHiddenChange, () => {
updateValue();
});
const setting = useSettingValue("notificationsEnabled");
useEffect(() => {
updateValue();
}, [setting, updateValue]);
// shouldShowPrompt is dependent on the client having push rules. There isn't an event for the client
// fetching its push rules, but we'll know it has them by the time it sync, so we update this on sync.
useTypedEventEmitter(client, ClientEvent.Sync, updateValue);
return value;
}
export function useUserOnboardingContext(): UserOnboardingContext {
const hasAvatar = useUserOnboardingContextValue(false, async (cli) => {
const profile = await cli.getProfileInfo(cli.getUserId()!);
return Boolean(profile?.avatar_url);
});
const hasDevices = useUserOnboardingContextValue(false, async (cli) => {
const myDevice = cli.getDeviceId();
const devices = await cli.getDevices();
return Boolean(devices.devices.find((device) => device.device_id !== myDevice));
});
const hasDmRooms = useUserOnboardingContextValue(false, async () => {
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
return Boolean(Object.keys(dmRooms).length);
});
const showNotificationsPrompt = useShowNotificationsPrompt();
return useMemo(
() => ({ hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt }),
[hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt],
);
}