Rework how the onboarding notifications task works (#12839)
* Rework how the onboarding notifications task works Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
dd20741b87
commit
02047243f0
6 changed files with 85 additions and 17 deletions
|
@ -29,6 +29,7 @@ import {
|
||||||
IRoomTimelineData,
|
IRoomTimelineData,
|
||||||
M_LOCATION,
|
M_LOCATION,
|
||||||
EventType,
|
EventType,
|
||||||
|
TypedEventEmitter,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
import { PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
||||||
|
@ -103,7 +104,15 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class NotifierClass {
|
export const enum NotifierEvent {
|
||||||
|
NotificationHiddenChange = "notification_hidden_change",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmittedEvents {
|
||||||
|
[NotifierEvent.NotificationHiddenChange]: (hidden: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> {
|
||||||
private notifsByRoom: Record<string, Notification[]> = {};
|
private notifsByRoom: Record<string, Notification[]> = {};
|
||||||
|
|
||||||
// A list of event IDs that we've received but need to wait until
|
// A list of event IDs that we've received but need to wait until
|
||||||
|
@ -357,6 +366,7 @@ class NotifierClass {
|
||||||
if (persistent && global.localStorage) {
|
if (persistent && global.localStorage) {
|
||||||
global.localStorage.setItem("notifications_hidden", String(hidden));
|
global.localStorage.setItem("notifications_hidden", String(hidden));
|
||||||
}
|
}
|
||||||
|
this.emit(NotifierEvent.NotificationHiddenChange, hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldShowPrompt(): boolean {
|
public shouldShowPrompt(): boolean {
|
||||||
|
|
|
@ -18,15 +18,17 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Notifier } from "../Notifier";
|
import { Notifier, NotifierEvent } from "../Notifier";
|
||||||
import DMRoomMap from "../utils/DMRoomMap";
|
import DMRoomMap from "../utils/DMRoomMap";
|
||||||
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||||
|
import { useSettingValue } from "./useSettings";
|
||||||
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
export interface UserOnboardingContext {
|
export interface UserOnboardingContext {
|
||||||
hasAvatar: boolean;
|
hasAvatar: boolean;
|
||||||
hasDevices: boolean;
|
hasDevices: boolean;
|
||||||
hasDmRooms: boolean;
|
hasDmRooms: boolean;
|
||||||
hasNotificationsEnabled: boolean;
|
showNotificationsPrompt: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;
|
const USER_ONBOARDING_CONTEXT_INTERVAL = 5000;
|
||||||
|
@ -82,6 +84,18 @@ function useUserOnboardingContextValue<T>(defaultValue: T, callback: (cli: Matri
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useShowNotificationsPrompt(): boolean {
|
||||||
|
const [value, setValue] = useState<boolean>(Notifier.shouldShowPrompt());
|
||||||
|
useEventEmitter(Notifier, NotifierEvent.NotificationHiddenChange, () => {
|
||||||
|
setValue(Notifier.shouldShowPrompt());
|
||||||
|
});
|
||||||
|
const setting = useSettingValue("notificationsEnabled");
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(Notifier.shouldShowPrompt());
|
||||||
|
}, [setting]);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function useUserOnboardingContext(): UserOnboardingContext {
|
export function useUserOnboardingContext(): UserOnboardingContext {
|
||||||
const hasAvatar = useUserOnboardingContextValue(false, async (cli) => {
|
const hasAvatar = useUserOnboardingContextValue(false, async (cli) => {
|
||||||
const profile = await cli.getProfileInfo(cli.getUserId()!);
|
const profile = await cli.getProfileInfo(cli.getUserId()!);
|
||||||
|
@ -96,12 +110,10 @@ export function useUserOnboardingContext(): UserOnboardingContext {
|
||||||
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
|
const dmRooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals() ?? {};
|
||||||
return Boolean(Object.keys(dmRooms).length);
|
return Boolean(Object.keys(dmRooms).length);
|
||||||
});
|
});
|
||||||
const hasNotificationsEnabled = useUserOnboardingContextValue(false, async () => {
|
const showNotificationsPrompt = useShowNotificationsPrompt();
|
||||||
return Notifier.isPossible();
|
|
||||||
});
|
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({ hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled }),
|
() => ({ hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt }),
|
||||||
[hasAvatar, hasDevices, hasDmRooms, hasNotificationsEnabled],
|
[hasAvatar, hasDevices, hasDmRooms, showNotificationsPrompt],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,14 +136,18 @@ const tasks: UserOnboardingTask[] = [
|
||||||
id: "permission-notifications",
|
id: "permission-notifications",
|
||||||
title: _t("onboarding|enable_notifications"),
|
title: _t("onboarding|enable_notifications"),
|
||||||
description: _t("onboarding|enable_notifications_description"),
|
description: _t("onboarding|enable_notifications_description"),
|
||||||
completed: (ctx: UserOnboardingContext) => ctx.hasNotificationsEnabled,
|
completed: (ctx: UserOnboardingContext) => !ctx.showNotificationsPrompt,
|
||||||
action: {
|
action: {
|
||||||
label: _t("onboarding|enable_notifications_action"),
|
label: _t("onboarding|enable_notifications_action"),
|
||||||
onClick: (ev: ButtonEvent) => {
|
onClick: (ev: ButtonEvent) => {
|
||||||
PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev);
|
PosthogTrackers.trackInteraction("WebUserOnboardingTaskEnableNotifications", ev);
|
||||||
Notifier.setEnabled(true);
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Notifications,
|
||||||
|
});
|
||||||
|
Notifier.setPromptHidden(true);
|
||||||
},
|
},
|
||||||
hideOnComplete: true,
|
hideOnComplete: !Notifier.isPossible(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1652,8 +1652,8 @@
|
||||||
"download_brand_desktop": "Download %(brand)s Desktop",
|
"download_brand_desktop": "Download %(brand)s Desktop",
|
||||||
"download_f_droid": "Get it on F-Droid",
|
"download_f_droid": "Get it on F-Droid",
|
||||||
"download_google_play": "Get it on Google Play",
|
"download_google_play": "Get it on Google Play",
|
||||||
"enable_notifications": "Turn on notifications",
|
"enable_notifications": "Turn on desktop notifications",
|
||||||
"enable_notifications_action": "Enable notifications",
|
"enable_notifications_action": "Open settings",
|
||||||
"enable_notifications_description": "Don’t miss a reply or important message",
|
"enable_notifications_description": "Don’t miss a reply or important message",
|
||||||
"explore_rooms": "Explore Public Rooms",
|
"explore_rooms": "Explore Public Rooms",
|
||||||
"find_community_members": "Find and invite your community members",
|
"find_community_members": "Find and invite your community members",
|
||||||
|
|
|
@ -20,9 +20,11 @@ import GenericToast from "../components/views/toasts/GenericToast";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { getLocalNotificationAccountDataEventType } from "../utils/notifications";
|
import { getLocalNotificationAccountDataEventType } from "../utils/notifications";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
|
|
||||||
const onAccept = (): void => {
|
const onAccept = async (): Promise<void> => {
|
||||||
Notifier.setEnabled(true);
|
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, true);
|
||||||
const cli = MatrixClientPeg.safeGet();
|
const cli = MatrixClientPeg.safeGet();
|
||||||
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
||||||
cli.setAccountData(eventType, {
|
cli.setAccountData(eventType, {
|
||||||
|
|
|
@ -14,9 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { renderHook } from "@testing-library/react-hooks";
|
import { renderHook } from "@testing-library/react-hooks";
|
||||||
|
import { waitFor } from "@testing-library/react";
|
||||||
|
|
||||||
import { useUserOnboardingTasks } from "../../src/hooks/useUserOnboardingTasks";
|
import { useUserOnboardingTasks } from "../../src/hooks/useUserOnboardingTasks";
|
||||||
|
import { useUserOnboardingContext } from "../../src/hooks/useUserOnboardingContext";
|
||||||
|
import { stubClient } from "../test-utils";
|
||||||
|
import MatrixClientContext from "../../src/contexts/MatrixClientContext";
|
||||||
|
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||||
|
import PlatformPeg from "../../src/PlatformPeg";
|
||||||
|
|
||||||
describe("useUserOnboardingTasks", () => {
|
describe("useUserOnboardingTasks", () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
@ -25,7 +32,7 @@ describe("useUserOnboardingTasks", () => {
|
||||||
hasAvatar: false,
|
hasAvatar: false,
|
||||||
hasDevices: false,
|
hasDevices: false,
|
||||||
hasDmRooms: false,
|
hasDmRooms: false,
|
||||||
hasNotificationsEnabled: false,
|
showNotificationsPrompt: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -33,7 +40,7 @@ describe("useUserOnboardingTasks", () => {
|
||||||
hasAvatar: true,
|
hasAvatar: true,
|
||||||
hasDevices: false,
|
hasDevices: false,
|
||||||
hasDmRooms: false,
|
hasDmRooms: false,
|
||||||
hasNotificationsEnabled: true,
|
showNotificationsPrompt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])("sequence should stay static", async ({ context }) => {
|
])("sequence should stay static", async ({ context }) => {
|
||||||
|
@ -46,4 +53,37 @@ describe("useUserOnboardingTasks", () => {
|
||||||
expect(result.current[3].id).toBe("setup-profile");
|
expect(result.current[3].id).toBe("setup-profile");
|
||||||
expect(result.current[4].id).toBe("permission-notifications");
|
expect(result.current[4].id).toBe("permission-notifications");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should mark desktop notifications task completed on click", async () => {
|
||||||
|
jest.spyOn(PlatformPeg, "get").mockReturnValue({
|
||||||
|
supportsNotifications: jest.fn().mockReturnValue(true),
|
||||||
|
maySendNotifications: jest.fn().mockReturnValue(false),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const cli = stubClient();
|
||||||
|
cli.pushRules = {
|
||||||
|
global: {
|
||||||
|
override: [
|
||||||
|
{
|
||||||
|
rule_id: ".m.rule.master",
|
||||||
|
enabled: false,
|
||||||
|
actions: [],
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
DMRoomMap.makeShared(cli);
|
||||||
|
const context = renderHook(() => useUserOnboardingContext(), {
|
||||||
|
wrapper: (props) => {
|
||||||
|
return <MatrixClientContext.Provider value={cli}>{props.children}</MatrixClientContext.Provider>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { result, rerender } = renderHook(() => useUserOnboardingTasks(context.result.current));
|
||||||
|
expect(result.current[4].id).toBe("permission-notifications");
|
||||||
|
await waitFor(() => expect(result.current[4].completed).toBe(false));
|
||||||
|
result.current[4].action!.onClick!({ type: "click" } as any);
|
||||||
|
rerender();
|
||||||
|
await waitFor(() => expect(result.current[4].completed).toBe(true));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue