Fix Notifier import cycle (#34122)
* Fix Notifier import cycle * Fix tests * Delint * Iterate * Handle SDKContextClass `client` initialisation internally Rather than via MatrixChat - this is predominantly for Lifecycle tests as they don't use a MatrixChat and it doesn't make much sense for this component to own this state. * Fix tests * Iterate * Improve coverage * Improve coverage * Fix bad merge * Iterate
This commit is contained in:
Vendored
-2
@@ -19,7 +19,6 @@ import { type PlatformPeg } from "../PlatformPeg";
|
||||
import { type IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||
import { type ModalManager } from "../Modal";
|
||||
import type SettingsStore from "../settings/SettingsStore";
|
||||
import { type Notifier } from "../Notifier";
|
||||
import type RightPanelStore from "../stores/right-panel/RightPanelStore";
|
||||
import type WidgetStore from "../stores/WidgetStore";
|
||||
import type LegacyCallHandler from "../LegacyCallHandler";
|
||||
@@ -97,7 +96,6 @@ declare global {
|
||||
mxIntegrationManagers: typeof IntegrationManagers;
|
||||
singletonModalManager: ModalManager;
|
||||
mxSettingsStore: SettingsStore;
|
||||
mxNotifier: typeof Notifier;
|
||||
mxRightPanelStore: RightPanelStore;
|
||||
mxWidgetStore: WidgetStore;
|
||||
mxWidgetLayoutStore: WidgetLayoutStore;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { MatrixClientPeg, type MatrixClientPegAssignOpts } from "./MatrixClientP
|
||||
import { ModuleRunner } from "./modules/ModuleRunner";
|
||||
import EventIndexPeg from "./indexing/EventIndexPeg";
|
||||
import { createMatrixClient, createClientWithCreds, type IMatrixClientCreds } from "./utils/createMatrixClient";
|
||||
import Notifier from "./Notifier";
|
||||
import UserActivity from "./UserActivity";
|
||||
import Presence from "./Presence";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
@@ -1098,7 +1097,7 @@ async function startMatrixClient(
|
||||
ToastStore.sharedInstance().reset();
|
||||
|
||||
DialogOpener.instance.prepare(client);
|
||||
Notifier.start();
|
||||
SDKContextClass.instance.notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
DMRoomMap.makeShared(client).start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
@@ -1229,7 +1228,7 @@ export async function clearStorage(opts?: { deleteEverything?: boolean }): Promi
|
||||
* on MatrixClientPeg after stopping.
|
||||
*/
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
SDKContextClass.instance.notifier.stop();
|
||||
LegacyCallHandler.instance.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
SDKContextClass.instance.typingStore.reset();
|
||||
|
||||
+66
-66
@@ -27,13 +27,10 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
||||
import { type SessionMembershipData, type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import * as TextForEvent from "./TextForEvent";
|
||||
import * as Avatar from "./Avatar";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import { _t } from "./languageHandler";
|
||||
import Modal from "./Modal";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
@@ -43,12 +40,13 @@ import { isPushNotifyDisabled } from "./settings/controllers/NotificationControl
|
||||
import UserActivity from "./UserActivity";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import { SDKContextClass } from "./contexts/SDKContextClass";
|
||||
import { type SDKContextClass } from "./contexts/SDKContextClass.ts";
|
||||
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
|
||||
import { getIncomingCallToastKey, getNotificationEventSendTs, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import { stripPlainReply } from "./utils/Reply";
|
||||
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||
import { type MatrixDispatcher } from "./dispatcher/dispatcher.ts";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
@@ -83,27 +81,6 @@ const MAX_PENDING_ENCRYPTED = 20;
|
||||
*/
|
||||
export const NOTIFICATION_SOUND_THROTTLE_MS = 1000;
|
||||
|
||||
/*
|
||||
Override both the content body and the TextForEvent handler for specific msgtypes, in notifications.
|
||||
This is useful when the content body contains fallback text that would explain that the client can't handle a particular
|
||||
type of tile.
|
||||
*/
|
||||
const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
|
||||
[MsgType.KeyVerificationRequest]: (event: MatrixEvent) => {
|
||||
const name = event.sender?.name;
|
||||
return _t("notifier|m.key.verification.request", { name });
|
||||
},
|
||||
[M_LOCATION.name]: (event: MatrixEvent) => {
|
||||
return TextForEvent.textForLocationEvent(event)();
|
||||
},
|
||||
[M_LOCATION.altName]: (event: MatrixEvent) => {
|
||||
return TextForEvent.textForLocationEvent(event)();
|
||||
},
|
||||
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts plain text from a message body, replacing any spoilered content
|
||||
* with '[Spoiler]' to prevent spoilers in desktop notifications.
|
||||
@@ -175,7 +152,7 @@ export type NotificationSound = {
|
||||
size?: number;
|
||||
};
|
||||
|
||||
class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> {
|
||||
export default class Notifier extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> {
|
||||
private notifsByRoom: Record<string, Notification[]> = {};
|
||||
|
||||
// A list of event IDs that we've received but need to wait until
|
||||
@@ -196,19 +173,50 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
*/
|
||||
private readonly lastAudioNotificationMs = new Map<string, number>();
|
||||
|
||||
private msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null>;
|
||||
|
||||
public constructor(
|
||||
private readonly dispatcher: MatrixDispatcher,
|
||||
private readonly sdkContext: SDKContextClass,
|
||||
) {
|
||||
super();
|
||||
|
||||
/*
|
||||
Override both the content body and the TextForEvent handler for specific msgtypes, in notifications.
|
||||
This is useful when the content body contains fallback text that would explain that the client can't handle a particular
|
||||
type of tile.
|
||||
*/
|
||||
this.msgTypeHandlers = {
|
||||
[MsgType.KeyVerificationRequest]: (event: MatrixEvent) => {
|
||||
const name = event.sender?.name;
|
||||
return _t("notifier|m.key.verification.request", { name });
|
||||
},
|
||||
[M_LOCATION.name]: (event: MatrixEvent) => {
|
||||
return TextForEvent.textForLocationEvent(event)();
|
||||
},
|
||||
[M_LOCATION.altName]: (event: MatrixEvent) => {
|
||||
return TextForEvent.textForLocationEvent(event)();
|
||||
},
|
||||
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||
return TextForEvent.textForEvent(event, this.sdkContext.client!);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public notificationMessageForEvent(ev: MatrixEvent): string | null {
|
||||
if (!this.sdkContext.client) return null;
|
||||
const msgType = ev.getContent().msgtype;
|
||||
if (msgType && msgTypeHandlers.hasOwnProperty(msgType)) {
|
||||
return msgTypeHandlers[msgType](ev);
|
||||
if (msgType && this.msgTypeHandlers.hasOwnProperty(msgType)) {
|
||||
return this.msgTypeHandlers[msgType](ev);
|
||||
}
|
||||
return TextForEvent.textForEvent(ev, MatrixClientPeg.safeGet());
|
||||
return TextForEvent.textForEvent(ev, this.sdkContext.client);
|
||||
}
|
||||
|
||||
// XXX: exported for tests
|
||||
public displayPopupNotification(ev: MatrixEvent, room: Room): void {
|
||||
const plaf = PlatformPeg.get();
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
if (!plaf) {
|
||||
const cli = this.sdkContext.client;
|
||||
if (!plaf || !cli) {
|
||||
return;
|
||||
}
|
||||
if (!plaf.supportsNotifications() || !plaf.maySendNotifications()) {
|
||||
@@ -227,7 +235,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
title = room.name;
|
||||
// notificationMessageForEvent includes sender, but we already have the sender here
|
||||
const msgType = ev.getContent().msgtype;
|
||||
if (ev.getContent().body && (!msgType || !msgTypeHandlers.hasOwnProperty(msgType))) {
|
||||
if (ev.getContent().body && (!msgType || !this.msgTypeHandlers.hasOwnProperty(msgType))) {
|
||||
msg = stripPlainReply(getNotificationBodyWithoutSpoilers(ev));
|
||||
}
|
||||
} else if (ev.getType() === "m.room.member") {
|
||||
@@ -238,7 +246,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
title = ev.sender.name + " (" + room.name + ")";
|
||||
// notificationMessageForEvent includes sender, but we've just out sender in the title
|
||||
const msgType = ev.getContent().msgtype;
|
||||
if (ev.getContent().body && (!msgType || !msgTypeHandlers.hasOwnProperty(msgType))) {
|
||||
if (ev.getContent().body && (!msgType || !this.msgTypeHandlers.hasOwnProperty(msgType))) {
|
||||
msg = stripPlainReply(getNotificationBodyWithoutSpoilers(ev));
|
||||
}
|
||||
}
|
||||
@@ -300,8 +308,8 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
// XXX: Exported for tests
|
||||
public async playAudioNotification(ev: MatrixEvent, room: Room): Promise<void> {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
if (localNotificationsAreSilenced(cli)) {
|
||||
const cli = this.sdkContext.client;
|
||||
if (!cli || localNotificationsAreSilenced(cli)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,22 +339,19 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
cli.on(RoomEvent.Timeline, this.onEvent);
|
||||
cli.on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
cli.on(ClientEvent.Sync, this.onSyncStateChange);
|
||||
this.sdkContext.client!.on(RoomEvent.Timeline, this.onEvent);
|
||||
this.sdkContext.client!.on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
this.sdkContext.client!.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
this.sdkContext.client!.on(ClientEvent.Sync, this.onSyncStateChange);
|
||||
this.toolbarHidden = false;
|
||||
this.isSyncing = false;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get()!.removeListener(RoomEvent.Timeline, this.onEvent);
|
||||
MatrixClientPeg.get()!.removeListener(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
MatrixClientPeg.get()!.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
MatrixClientPeg.get()!.removeListener(ClientEvent.Sync, this.onSyncStateChange);
|
||||
}
|
||||
this.sdkContext.client?.removeListener(RoomEvent.Timeline, this.onEvent);
|
||||
this.sdkContext.client?.removeListener(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
this.sdkContext.client?.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
this.sdkContext.client?.removeListener(ClientEvent.Sync, this.onSyncStateChange);
|
||||
this.isSyncing = false;
|
||||
}
|
||||
|
||||
@@ -390,23 +395,23 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
if (callback) callback();
|
||||
|
||||
PosthogAnalytics.instance.trackEvent<PermissionChangedEvent>({
|
||||
this.sdkContext.posthogAnalytics.trackEvent<PermissionChangedEvent>({
|
||||
eventName: "PermissionChanged",
|
||||
permission: "Notification",
|
||||
granted: true,
|
||||
});
|
||||
dis.dispatch({
|
||||
this.dispatcher.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
PosthogAnalytics.instance.trackEvent<PermissionChangedEvent>({
|
||||
this.sdkContext.posthogAnalytics.trackEvent<PermissionChangedEvent>({
|
||||
eventName: "PermissionChanged",
|
||||
permission: "Notification",
|
||||
granted: false,
|
||||
});
|
||||
dis.dispatch({
|
||||
this.dispatcher.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: false,
|
||||
});
|
||||
@@ -450,7 +455,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
}
|
||||
|
||||
public shouldShowPrompt(): boolean {
|
||||
const client = MatrixClientPeg.get();
|
||||
const client = this.sdkContext.client;
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
@@ -475,6 +480,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
// XXX: Exported for tests
|
||||
public onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: SyncStateData): void => {
|
||||
if (!this.sdkContext.client) return;
|
||||
if (state === SyncState.Syncing) {
|
||||
this.isSyncing = true;
|
||||
} else if (state === SyncState.Stopped || state === SyncState.Error) {
|
||||
@@ -483,7 +489,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
// wait for first non-cached sync to complete
|
||||
if (![SyncState.Stopped, SyncState.Error].includes(state) && !data?.fromCache) {
|
||||
createLocalNotificationSettingsIfNeeded(MatrixClientPeg.safeGet());
|
||||
createLocalNotificationSettingsIfNeeded(this.sdkContext.client);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -494,13 +500,14 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
removed: boolean,
|
||||
data: IRoomTimelineData,
|
||||
): void => {
|
||||
if (!this.sdkContext.client) return;
|
||||
if (removed) return; // only notify for new events, not removed ones
|
||||
if (!data.liveEvent || !!toStartOfTimeline) return; // only notify for new things, not old.
|
||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||
if (ev.getSender() === MatrixClientPeg.safeGet().getUserId()) return;
|
||||
if (ev.getSender() === this.sdkContext.client.getUserId()) return;
|
||||
if (data.timeline.getTimelineSet().threadListType !== null) return; // Ignore events on the thread list generated timelines
|
||||
|
||||
MatrixClientPeg.safeGet().decryptEventIfNeeded(ev);
|
||||
this.sdkContext.client.decryptEventIfNeeded(ev);
|
||||
|
||||
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
||||
// it hasn't yet been decrypted, so wait until it is.
|
||||
@@ -546,20 +553,21 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
// XXX: exported for tests
|
||||
public evaluateEvent(ev: MatrixEvent): void {
|
||||
if (!this.sdkContext.client) return;
|
||||
const roomId = ev.getRoomId()!;
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
const room = this.sdkContext.client.getRoom(roomId);
|
||||
if (!room) {
|
||||
// e.g we are in the process of joining a room.
|
||||
// Seen in the Playwright lazy-loading test.
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = MatrixClientPeg.safeGet().getPushActionsForEvent(ev);
|
||||
const actions = this.sdkContext.client.getPushActionsForEvent(ev);
|
||||
|
||||
if (actions?.notify) {
|
||||
this.performCustomEventHandling(ev);
|
||||
|
||||
const store = SDKContextClass.instance.roomViewStore;
|
||||
const store = this.sdkContext.roomViewStore;
|
||||
const isViewingRoom = store.getRoomId() === room.roomId;
|
||||
const threadId: string | undefined = ev.getId() !== ev.threadRootId ? ev.threadRootId : undefined;
|
||||
const isViewingThread = store.getThreadId() === threadId;
|
||||
@@ -670,8 +678,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
*/
|
||||
private performCustomEventHandling(ev: MatrixEvent): void {
|
||||
const toaster = ToastStore.sharedInstance();
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const room = cli.getRoom(ev.getRoomId());
|
||||
const room = this.sdkContext.client?.getRoom(ev.getRoomId());
|
||||
|
||||
if (room && EventType.RTCNotification === ev.getType()) {
|
||||
// We don't need to await this.
|
||||
@@ -679,10 +686,3 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.mxNotifier) {
|
||||
window.mxNotifier = new NotifierClass();
|
||||
}
|
||||
|
||||
export default window.mxNotifier;
|
||||
export const Notifier: NotifierClass = window.mxNotifier;
|
||||
|
||||
@@ -37,7 +37,6 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig, { type ConfigOptions } from "../../SdkConfig";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Notifier from "../../Notifier";
|
||||
import Modal from "../../Modal";
|
||||
import { showRoomInviteDialog, showStartChatInviteDialog } from "../../RoomInvite";
|
||||
import * as Rooms from "../../Rooms";
|
||||
@@ -1638,8 +1637,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
if (Notifier.shouldShowPrompt() && !MatrixClientPeg.userRegisteredWithinLastHours(24)) {
|
||||
showNotificationsToast(false);
|
||||
if (this.stores.notifier.shouldShowPrompt() && !MatrixClientPeg.userRegisteredWithinLastHours(24)) {
|
||||
showNotificationsToast(this.stores.notifier, false);
|
||||
}
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
|
||||
@@ -88,7 +88,6 @@ import { containsEmoji } from "../../effects/utils";
|
||||
import { CHAT_EFFECTS } from "../../effects";
|
||||
import { CallView } from "../views/voip/CallView";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
@@ -1689,8 +1688,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (!this.state.room) return;
|
||||
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
|
||||
// if they are not alone prompt the user about notifications so they don't miss replies
|
||||
if (memberCount > 1 && Notifier.shouldShowPrompt()) {
|
||||
showNotificationsToast(true);
|
||||
if (memberCount > 1 && this.context.notifier.shouldShowPrompt()) {
|
||||
showNotificationsToast(this.context.notifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import AccessibleButton, { type ButtonEvent } from "../../../elements/AccessibleButton";
|
||||
import Notifier from "../../../../../Notifier";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import { type RoomEchoChamber } from "../../../../../stores/local-echo/RoomEchoChamber";
|
||||
@@ -26,6 +25,7 @@ import { chromeFileInputFix } from "../../../../../utils/BrowserWorkarounds";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||
import { SDKContextClass } from "../../../../../contexts/SDKContextClass.ts";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
@@ -50,7 +50,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
|
||||
this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId)!);
|
||||
|
||||
let currentSound = "default";
|
||||
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
||||
const soundData = SDKContextClass.instance.notifier.getSoundForRoom(this.props.roomId);
|
||||
if (soundData) {
|
||||
currentSound = soundData.name || soundData.url;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import { MultiRoomViewStore } from "../stores/MultiRoomViewStore";
|
||||
import { type ActionPayload, isAction } from "../dispatcher/payloads.ts";
|
||||
import { Action } from "../dispatcher/actions.ts";
|
||||
import { type OnLoggedInPayload } from "../dispatcher/payloads/OnLoggedInPayload.ts";
|
||||
import Notifier from "../Notifier.ts";
|
||||
import SettingController from "../settings/controllers/SettingController.ts";
|
||||
|
||||
/**
|
||||
* A class which (mostly) lazily initialises stores as and when they are requested, ensuring they remain
|
||||
@@ -71,8 +73,11 @@ export class SDKContextClass {
|
||||
protected _OidcClientStore?: OidcClientStore;
|
||||
protected _ResizeNotifier?: ResizeNotifier;
|
||||
protected _MultiRoomViewStore?: MultiRoomViewStore;
|
||||
protected _Notifier?: Notifier;
|
||||
|
||||
public constructor() {
|
||||
SettingController.sdkContext = this;
|
||||
|
||||
defaultDispatcher.register(this.onDispatch);
|
||||
}
|
||||
|
||||
@@ -205,6 +210,13 @@ export class SDKContextClass {
|
||||
return this._MultiRoomViewStore;
|
||||
}
|
||||
|
||||
public get notifier(): Notifier {
|
||||
if (!this._Notifier) {
|
||||
this._Notifier = new Notifier(defaultDispatcher, this);
|
||||
}
|
||||
return this._Notifier;
|
||||
}
|
||||
|
||||
public onLoggedOut(): void {
|
||||
this._UserProfilesStore = undefined;
|
||||
this._OidcClientStore = undefined;
|
||||
|
||||
@@ -29,14 +29,6 @@ export function isPushNotifyDisabled(): boolean {
|
||||
return masterRule.enabled && !masterRule.actions.includes(PushRuleActionName.Notify);
|
||||
}
|
||||
|
||||
function getNotifier(): any {
|
||||
// TODO: [TS] Formal type that doesn't cause a cyclical reference.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
let Notifier = require("../../Notifier"); // avoids cyclical references
|
||||
if (Notifier.default) Notifier = Notifier.default; // correct for webpack require() weirdness
|
||||
return Notifier;
|
||||
}
|
||||
|
||||
export class NotificationsEnabledController extends SettingController {
|
||||
public getValueOverride(
|
||||
level: SettingLevel,
|
||||
@@ -44,7 +36,7 @@ export class NotificationsEnabledController extends SettingController {
|
||||
calculatedValue: any,
|
||||
calculatedAtLevel: SettingLevel | null,
|
||||
): any {
|
||||
if (!getNotifier().isPossible()) return false;
|
||||
if (!this.sdkContext.notifier.isPossible()) return false;
|
||||
|
||||
if (calculatedValue === null || calculatedAtLevel === "default") {
|
||||
return !isPushNotifyDisabled();
|
||||
@@ -54,15 +46,15 @@ export class NotificationsEnabledController extends SettingController {
|
||||
}
|
||||
|
||||
public onChange(level: SettingLevel, roomId: string, newValue: any): void {
|
||||
if (getNotifier().supportsDesktopNotifications()) {
|
||||
getNotifier().setEnabled(newValue);
|
||||
if (this.sdkContext.notifier.supportsDesktopNotifications()) {
|
||||
this.sdkContext.notifier.setEnabled(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationBodyEnabledController extends SettingController {
|
||||
public getValueOverride(level: SettingLevel, roomId: string, calculatedValue: any): any {
|
||||
if (!getNotifier().isPossible()) return false;
|
||||
if (!this.sdkContext.notifier.isPossible()) return false;
|
||||
|
||||
if (calculatedValue === null) {
|
||||
return !isPushNotifyDisabled();
|
||||
|
||||
@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { type SettingLevel } from "../SettingLevel";
|
||||
import type SettingsStore from "../SettingsStore";
|
||||
import { type SDKContextClass } from "../../contexts/SDKContextClass.ts";
|
||||
|
||||
/**
|
||||
* Represents a controller for individual settings to alter the reading behaviour
|
||||
@@ -19,6 +20,7 @@ import type SettingsStore from "../SettingsStore";
|
||||
* intended to handle environmental factors for specific settings.
|
||||
*/
|
||||
export default abstract class SettingController {
|
||||
public static sdkContext: SDKContextClass;
|
||||
public static settingsStore: typeof SettingsStore;
|
||||
|
||||
/**
|
||||
@@ -77,4 +79,12 @@ export default abstract class SettingController {
|
||||
protected get settingsStore(): typeof SettingsStore {
|
||||
return SettingController.settingsStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to the SDKContext injected at runtime.
|
||||
* Preferred to direct imports in order to avoid import cycles.
|
||||
*/
|
||||
protected get sdkContext(): SDKContextClass {
|
||||
return SettingController.sdkContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { _t } from "../languageHandler";
|
||||
import Notifier from "../Notifier";
|
||||
import type Notifier from "../Notifier";
|
||||
import GenericToast from "../components/views/toasts/GenericToast";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
@@ -15,22 +15,22 @@ import { getLocalNotificationAccountDataEventType } from "../utils/notifications
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
|
||||
const onAccept = async (): Promise<void> => {
|
||||
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, true);
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
||||
cli.setAccountData(eventType, {
|
||||
is_silenced: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onReject = (): void => {
|
||||
Notifier.setPromptHidden(true);
|
||||
};
|
||||
|
||||
const TOAST_KEY = "desktopnotifications";
|
||||
|
||||
export const showToast = (fromMessageSend: boolean): void => {
|
||||
export const showToast = (notifier: Notifier, fromMessageSend: boolean): void => {
|
||||
const onAccept = async (): Promise<void> => {
|
||||
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, true);
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
||||
cli.setAccountData(eventType, {
|
||||
is_silenced: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onReject = (): void => {
|
||||
notifier.setPromptHidden(true);
|
||||
};
|
||||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: TOAST_KEY,
|
||||
title: fromMessageSend
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { waitFor } from "jest-matrix-react";
|
||||
import { CallMembership, type SessionMembershipData, type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
|
||||
|
||||
import type BasePlatform from "../../src/BasePlatform";
|
||||
import Notifier, { NOTIFICATION_SOUND_THROTTLE_MS } from "../../src/Notifier";
|
||||
@@ -38,7 +39,6 @@ import {
|
||||
mockPlatformPeg,
|
||||
} from "../test-utils";
|
||||
import { getIncomingCallToastKey, IncomingCallToast } from "../../src/toasts/IncomingCallToast";
|
||||
import { SDKContextClass } from "../../src/contexts/SDKContextClass";
|
||||
import UserActivity from "../../src/UserActivity";
|
||||
import Modal from "../../src/Modal";
|
||||
import { mkThread } from "../test-utils/threads";
|
||||
@@ -46,6 +46,7 @@ import dis from "../../src/dispatcher/dispatcher";
|
||||
import { type ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload";
|
||||
import { Action } from "../../src/dispatcher/actions";
|
||||
import { addReplyToMessageContent } from "../../src/utils/Reply";
|
||||
import { TestSDKContext } from "./TestSDKContext.ts";
|
||||
|
||||
jest.mock("../../src/utils/notifications", () => ({
|
||||
// @ts-ignore
|
||||
@@ -61,6 +62,9 @@ jest.mock("../../src/audio/compat", () => ({
|
||||
const settingsStoreGetValue = SettingsStore.getValue;
|
||||
|
||||
describe("Notifier", () => {
|
||||
const context = new TestSDKContext();
|
||||
let notifier: Notifier;
|
||||
|
||||
const roomId = "!room1:server";
|
||||
const testEvent = mkEvent({
|
||||
event: true,
|
||||
@@ -145,6 +149,8 @@ describe("Notifier", () => {
|
||||
mockClient.pushRules = {
|
||||
global: {},
|
||||
};
|
||||
// @ts-ignore
|
||||
mockClient.pushProcessor = new PushProcessor(mockClient);
|
||||
accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId!);
|
||||
|
||||
testRoom = new Room(roomId, mockClient, mockClient.getSafeUserId());
|
||||
@@ -154,9 +160,11 @@ describe("Notifier", () => {
|
||||
maySendNotifications: jest.fn().mockReturnValue(true),
|
||||
displayNotification: jest.fn().mockReturnValue({ close: jest.fn() }),
|
||||
loudNotification: jest.fn(),
|
||||
requestNotificationPermission: jest.fn(),
|
||||
});
|
||||
|
||||
Notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
|
||||
notifier = new Notifier(dis, context);
|
||||
notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
|
||||
|
||||
mockClient.getRoom.mockImplementation((id: string | undefined): Room | null => {
|
||||
if (id === roomId) return testRoom;
|
||||
@@ -165,13 +173,8 @@ describe("Notifier", () => {
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
Notifier.backgroundAudio.audioContext = mockAudioContext;
|
||||
|
||||
// Notifier is a singleton, so its audio-notification throttle state
|
||||
// (see NOTIFICATION_SOUND_THROTTLE_MS) leaks between tests. Reset it so
|
||||
// each test exercises a clean instance.
|
||||
// @ts-ignore - lastAudioNotificationMs is private
|
||||
Notifier.lastAudioNotificationMs.clear();
|
||||
notifier.backgroundAudio.audioContext = mockAudioContext;
|
||||
context._client = mockClient;
|
||||
});
|
||||
|
||||
describe("triggering notification from events", () => {
|
||||
@@ -191,9 +194,9 @@ describe("Notifier", () => {
|
||||
// and references them in stop
|
||||
// so blows up if stopped before it was started
|
||||
if (hasStartedNotiferBefore) {
|
||||
Notifier.stop();
|
||||
notifier.stop();
|
||||
}
|
||||
Notifier.start();
|
||||
notifier.start();
|
||||
hasStartedNotiferBefore = true;
|
||||
mockClient.getRoom.mockReturnValue(testRoom);
|
||||
mockClient.getPushActionsForEvent.mockReturnValue({
|
||||
@@ -215,7 +218,7 @@ describe("Notifier", () => {
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Notifier.stop();
|
||||
notifier.stop();
|
||||
});
|
||||
|
||||
it("does not create notifications before syncing has started", () => {
|
||||
@@ -328,13 +331,13 @@ describe("Notifier", () => {
|
||||
];
|
||||
it.each(testCases)("does not dispatch when notifications are silenced", ({ event, count }) => {
|
||||
mockClient.setAccountData(accountDataEventKey, event!);
|
||||
Notifier.displayPopupNotification(testEvent, testRoom);
|
||||
notifier.displayPopupNotification(testEvent, testRoom);
|
||||
expect(MockPlatform.displayNotification).toHaveBeenCalledTimes(count);
|
||||
});
|
||||
|
||||
it("should display a notification for a voice message", () => {
|
||||
const audioEvent = mkAudioEvent();
|
||||
Notifier.displayPopupNotification(audioEvent, testRoom);
|
||||
notifier.displayPopupNotification(audioEvent, testRoom);
|
||||
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
|
||||
"@user:example.com (!room1:server)",
|
||||
"@user:example.com: test audio message",
|
||||
@@ -358,7 +361,7 @@ describe("Notifier", () => {
|
||||
room: testRoom.roomId,
|
||||
});
|
||||
addReplyToMessageContent(reply.getContent(), event);
|
||||
Notifier.displayPopupNotification(reply, testRoom);
|
||||
notifier.displayPopupNotification(reply, testRoom);
|
||||
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
|
||||
"@bob:example.org (!room1:server)",
|
||||
"This was a triumph",
|
||||
@@ -398,7 +401,7 @@ describe("Notifier", () => {
|
||||
formatted_body: formattedBody,
|
||||
},
|
||||
});
|
||||
Notifier.displayPopupNotification(spoilerEvent, testRoom);
|
||||
notifier.displayPopupNotification(spoilerEvent, testRoom);
|
||||
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
|
||||
"@bob:example.org (!room1:server)",
|
||||
expected,
|
||||
@@ -414,7 +417,7 @@ describe("Notifier", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
||||
return { url: { content_uri: "foobar" } };
|
||||
});
|
||||
expect(Notifier.getSoundForRoom("!roomId:server")).toBeNull();
|
||||
expect(notifier.getSoundForRoom("!roomId:server")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -427,11 +430,11 @@ describe("Notifier", () => {
|
||||
it.each(testCases)("does not dispatch when notifications are silenced", ({ event, count }) => {
|
||||
// It's not ideal to only look at whether this function has been called
|
||||
// but avoids starting to look into DOM stuff
|
||||
Notifier.getSoundForRoom = jest.fn();
|
||||
notifier.getSoundForRoom = jest.fn();
|
||||
|
||||
mockClient.setAccountData(accountDataEventKey, event!);
|
||||
Notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(Notifier.getSoundForRoom).toHaveBeenCalledTimes(count);
|
||||
notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(notifier.getSoundForRoom).toHaveBeenCalledTimes(count);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,10 +457,10 @@ describe("Notifier", () => {
|
||||
mockClient.setAccountData(accountDataEventKey, { is_silenced: false });
|
||||
|
||||
// Default sound path (no custom room sound).
|
||||
Notifier.getSoundForRoom = jest.fn().mockReturnValue(null);
|
||||
jest.spyOn(notifier, "getSoundForRoom").mockReturnValue(null);
|
||||
|
||||
// @ts-ignore - backgroundAudio is private
|
||||
playSpy = jest.spyOn(Notifier.backgroundAudio, "pickFormatAndPlay").mockResolvedValue({} as any);
|
||||
playSpy = jest.spyOn(notifier.backgroundAudio, "pickFormatAndPlay").mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -467,36 +470,36 @@ describe("Notifier", () => {
|
||||
|
||||
it("plays at most one sound for a burst of notifications within the throttle window", async () => {
|
||||
// Simulate a backlog of notifications arriving back-to-back on wake.
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("plays again once the throttle window has elapsed", async () => {
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance the clock just past the throttle window.
|
||||
jest.setSystemTime(NOTIFICATION_SOUND_THROTTLE_MS + 1);
|
||||
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("throttles right up to the window boundary, then plays again (strict `<`)", async () => {
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// One ms before the window elapses: still throttled.
|
||||
jest.setSystemTime(NOTIFICATION_SOUND_THROTTLE_MS - 1);
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Exactly at the window boundary: plays again (the comparison is a strict `<`).
|
||||
jest.setSystemTime(NOTIFICATION_SOUND_THROTTLE_MS);
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -504,16 +507,16 @@ describe("Notifier", () => {
|
||||
const soundA = { url: "sound-a.mp3", name: "A", type: "audio/mpeg", size: 1 };
|
||||
const soundB = { url: "sound-b.mp3", name: "B", type: "audio/mpeg", size: 1 };
|
||||
const otherRoom = new Room("!other:server", mockClient, mockClient.getSafeUserId());
|
||||
(Notifier.getSoundForRoom as jest.Mock).mockImplementation((roomId: string) =>
|
||||
jest.mocked(notifier.getSoundForRoom).mockImplementation((roomId: string) =>
|
||||
roomId === testRoom.roomId ? soundA : soundB,
|
||||
);
|
||||
// @ts-ignore - backgroundAudio is private
|
||||
const customPlaySpy = jest.spyOn(Notifier.backgroundAudio, "play").mockResolvedValue({} as any);
|
||||
const customPlaySpy = jest.spyOn(notifier.backgroundAudio, "play").mockResolvedValue({} as any);
|
||||
|
||||
// Two different sounds back-to-back within the window: BOTH must play (only identical
|
||||
// backlogged sounds are coalesced).
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await Notifier.playAudioNotification(testEvent, otherRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, otherRoom);
|
||||
|
||||
expect(customPlaySpy).toHaveBeenCalledTimes(2);
|
||||
expect(customPlaySpy).toHaveBeenNthCalledWith(1, soundA.url);
|
||||
@@ -524,8 +527,8 @@ describe("Notifier", () => {
|
||||
it("does not play, and does not arm the throttle, when notifications are silenced", async () => {
|
||||
mockClient.setAccountData(accountDataEventKey, { is_silenced: true });
|
||||
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
|
||||
// Silencing gate short-circuits before the sound is played.
|
||||
expect(playSpy).not.toHaveBeenCalled();
|
||||
@@ -533,7 +536,7 @@ describe("Notifier", () => {
|
||||
// ...and the silenced calls did NOT arm the throttle: once un-silenced, the next event plays
|
||||
// immediately (a regression arming the throttle on silenced events would suppress this).
|
||||
mockClient.setAccountData(accountDataEventKey, { is_silenced: false });
|
||||
await Notifier.playAudioNotification(testEvent, testRoom);
|
||||
await notifier.playAudioNotification(testEvent, testRoom);
|
||||
expect(playSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -567,8 +570,8 @@ describe("Notifier", () => {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
Notifier.start();
|
||||
Notifier.onSyncStateChange(SyncState.Syncing, null);
|
||||
notifier.start();
|
||||
notifier.onSyncStateChange(SyncState.Syncing, null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -738,15 +741,15 @@ describe("Notifier", () => {
|
||||
// and references them in stop
|
||||
// so blows up if stopped before it was started
|
||||
if (hasStartedNotiferBefore) {
|
||||
Notifier.stop();
|
||||
notifier.stop();
|
||||
}
|
||||
Notifier.start();
|
||||
notifier.start();
|
||||
hasStartedNotiferBefore = true;
|
||||
createLocalNotificationSettingsIfNeededMock.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Notifier.stop();
|
||||
notifier.stop();
|
||||
});
|
||||
|
||||
it("does not create local notifications event after a sync error", () => {
|
||||
@@ -774,14 +777,14 @@ describe("Notifier", () => {
|
||||
|
||||
describe("evaluateEvent", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(testRoom.roomId);
|
||||
jest.spyOn(context.roomViewStore, "getRoomId").mockReturnValue(testRoom.roomId);
|
||||
|
||||
jest.spyOn(UserActivity.sharedInstance(), "userActiveRecently").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(Modal, "hasDialogs").mockReturnValue(false);
|
||||
|
||||
jest.spyOn(Notifier, "displayPopupNotification").mockReset();
|
||||
jest.spyOn(Notifier, "isEnabled").mockReturnValue(true);
|
||||
jest.spyOn(notifier, "displayPopupNotification").mockReset();
|
||||
jest.spyOn(notifier, "isEnabled").mockReturnValue(true);
|
||||
|
||||
mockClient.getPushActionsForEvent.mockReturnValue({
|
||||
notify: true,
|
||||
@@ -792,9 +795,9 @@ describe("Notifier", () => {
|
||||
});
|
||||
|
||||
it("should show a pop-up", () => {
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
Notifier.evaluateEvent(testEvent);
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
notifier.evaluateEvent(testEvent);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
const eventFromOtherRoom = mkEvent({
|
||||
event: true,
|
||||
@@ -804,8 +807,8 @@ describe("Notifier", () => {
|
||||
content: {},
|
||||
});
|
||||
|
||||
Notifier.evaluateEvent(eventFromOtherRoom);
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
notifier.evaluateEvent(eventFromOtherRoom);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should a pop-up for thread event", async () => {
|
||||
@@ -816,34 +819,34 @@ describe("Notifier", () => {
|
||||
participantUserIds: ["@bob:example.org"],
|
||||
});
|
||||
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
Notifier.evaluateEvent(rootEvent);
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
notifier.evaluateEvent(rootEvent);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(0);
|
||||
|
||||
Notifier.evaluateEvent(events[1]);
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
notifier.evaluateEvent(events[1]);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
|
||||
dis.dispatch<ThreadPayload>({
|
||||
action: Action.ViewThread,
|
||||
thread_id: rootEvent.getId()!,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(SDKContextClass.instance.roomViewStore.getThreadId()).toBe(rootEvent.getId()));
|
||||
await waitFor(() => expect(context.roomViewStore.getThreadId()).toBe(rootEvent.getId()));
|
||||
|
||||
Notifier.evaluateEvent(events[1]);
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
notifier.evaluateEvent(events[1]);
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should show a pop-up for an audio message", () => {
|
||||
Notifier.evaluateEvent(mkAudioEvent());
|
||||
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
notifier.evaluateEvent(mkAudioEvent());
|
||||
expect(notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPromptHidden", () => {
|
||||
it("should persist by default", () => {
|
||||
Notifier.setPromptHidden(true);
|
||||
notifier.setPromptHidden(true);
|
||||
expect(localStorage.getItem("notifications_hidden")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -852,7 +855,7 @@ describe("Notifier", () => {
|
||||
it("should not evaluate events from the thread list fake timeline sets", async () => {
|
||||
mockClient.supportsThreads.mockReturnValue(true);
|
||||
|
||||
const fn = jest.spyOn(Notifier, "evaluateEvent");
|
||||
const fn = jest.spyOn(notifier, "evaluateEvent");
|
||||
|
||||
await testRoom.createThreadsTimelineSets();
|
||||
testRoom.threadsTimelineSets[0]!.addEventToTimeline(
|
||||
@@ -870,4 +873,34 @@ describe("Notifier", () => {
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setEnabled", () => {
|
||||
it("should call fire notifier_enabled value=true when permission is granted", async () => {
|
||||
const dispatchSpy = jest.spyOn(dis, "dispatch");
|
||||
const notifier = new Notifier(dis, context);
|
||||
|
||||
jest.mocked(MockPlatform.requestNotificationPermission).mockResolvedValue("granted");
|
||||
|
||||
const resolvers = Promise.withResolvers<void>();
|
||||
notifier.setEnabled(true, resolvers.resolve);
|
||||
await resolvers.promise;
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "notifier_enabled",
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call fire notifier_enabled value=false when disabling", async () => {
|
||||
const dispatchSpy = jest.spyOn(dis, "dispatch");
|
||||
const notifier = new Notifier(dis, context);
|
||||
|
||||
notifier.setEnabled(false);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: "notifier_enabled",
|
||||
value: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ import { type FeatureSettingKey, type SettingKey } from "../../src/settings/Sett
|
||||
import { SettingLevel } from "../../src/settings/SettingLevel.ts";
|
||||
import SdkConfig from "../../src/SdkConfig.ts";
|
||||
import { BugReportEndpointURLLocal } from "../../src/IConfigOptions.ts";
|
||||
import { Notifier } from "../../src/Notifier.ts";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg.ts";
|
||||
import { SDKContextClass } from "../../src/contexts/SDKContextClass.ts";
|
||||
|
||||
describe("Rageshakes", () => {
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
@@ -360,7 +360,7 @@ describe("Rageshakes", () => {
|
||||
|
||||
describe("Settings Store", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Notifier, "isPossible").mockReturnValue(true);
|
||||
jest.spyOn(SDKContextClass.instance.notifier, "isPossible").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -406,7 +406,7 @@ describe("Rageshakes", () => {
|
||||
it("should handle settings throwing when logged out", async () => {
|
||||
jest.mocked(MatrixClientPeg.get).mockRestore();
|
||||
jest.mocked(MatrixClientPeg.safeGet).mockRestore();
|
||||
jest.spyOn(Notifier, "isPossible").mockImplementation(() => {
|
||||
jest.spyOn(SDKContextClass.instance.notifier, "isPossible").mockImplementation(() => {
|
||||
throw new Error("Test");
|
||||
});
|
||||
|
||||
@@ -419,7 +419,7 @@ describe("Rageshakes", () => {
|
||||
it("should handle reading notification settings when logged out", async () => {
|
||||
jest.mocked(MatrixClientPeg.get).mockRestore();
|
||||
jest.mocked(MatrixClientPeg.safeGet).mockRestore();
|
||||
jest.spyOn(Notifier, "isPossible").mockReturnValue(true);
|
||||
jest.spyOn(SDKContextClass.instance.notifier, "isPossible").mockReturnValue(true);
|
||||
|
||||
const formData = await collectBugReport();
|
||||
expect(JSON.parse(formData.get("mx_local_settings") as string)["notificationsEnabled"]).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user