unmuteRoom unconditionally reset the room to Unset when a timed mute expired (in-session timer and boot-time restore alike), silently reverting a mode the user had changed by hand during the window. Mute-timer helpers move to muteTimers.ts; unmuteRoom now reads the live push-rule mode and only resets when it is still Mute, always dropping the persisted timer. Unit-tested. Fixes #21 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { IPushRule, IPushRules, MatrixClient } from 'matrix-js-sdk';
|
|
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
|
import { getAccountData } from '../../utils/room';
|
|
import { getNotificationMode, NotificationMode } from '../../hooks/useNotificationMode';
|
|
import {
|
|
RoomNotificationMode,
|
|
setRoomNotificationPreference,
|
|
} from '../../hooks/useRoomsNotificationPreferences';
|
|
|
|
// localStorage key for timed mute timers
|
|
export const MUTE_TIMERS_KEY = 'io.lotus.mute_timers';
|
|
|
|
// setTimeout's delay is a signed 32-bit int; larger values overflow and fire
|
|
// immediately. Clamp long delays to this max (~24.8 days).
|
|
export const MAX_MUTE_TIMEOUT_MS = 2_147_483_647;
|
|
|
|
export type MuteTimerEntry = { roomId: string; unmuteAt: number };
|
|
|
|
export function loadMuteTimers(): MuteTimerEntry[] {
|
|
try {
|
|
const parsed = JSON.parse(localStorage.getItem(MUTE_TIMERS_KEY) ?? '[]');
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function saveMuteTimers(timers: MuteTimerEntry[]): void {
|
|
localStorage.setItem(MUTE_TIMERS_KEY, JSON.stringify(timers));
|
|
}
|
|
|
|
// Pure decision for the unmute guard: a timed mute should only be reset back to
|
|
// Unset if the room's notification mode is still Mute at expiry time. If the user
|
|
// manually changed it (e.g. to All messages) while the timer was pending, leave
|
|
// their choice alone — just let the stale timer entry get dropped.
|
|
export function shouldResetMuteOnUnmute(currentMode: RoomNotificationMode): boolean {
|
|
return currentMode === RoomNotificationMode.Mute;
|
|
}
|
|
|
|
// Reads the room's live notification mode straight from account data push rules,
|
|
// mirroring useRoomsNotificationPreferences' per-room derivation, without needing
|
|
// the React hook (this runs from plain timers/effects, not components).
|
|
export function getLiveRoomNotificationMode(
|
|
mx: MatrixClient,
|
|
roomId: string,
|
|
): RoomNotificationMode {
|
|
const pushRules = getAccountData(mx, AccountDataEvent.PushRules)?.getContent<IPushRules>();
|
|
const global = pushRules?.global;
|
|
|
|
const overrideRule = global?.override?.find((rule: IPushRule) => rule.rule_id === roomId);
|
|
if (overrideRule && getNotificationMode(overrideRule.actions) === NotificationMode.OFF) {
|
|
return RoomNotificationMode.Mute;
|
|
}
|
|
|
|
const roomRule = global?.room?.find((rule: IPushRule) => rule.rule_id === roomId);
|
|
if (roomRule) {
|
|
return getNotificationMode(roomRule.actions) === NotificationMode.OFF
|
|
? RoomNotificationMode.SpecialMessages
|
|
: RoomNotificationMode.AllMessages;
|
|
}
|
|
|
|
return RoomNotificationMode.Unset;
|
|
}
|
|
|
|
// Reverse a timed mute: restore the room's notification mode to Unset and drop
|
|
// its persisted timer. Shared by the in-session timer and the boot-time restore.
|
|
// Only resets the mode if it is still Mute — otherwise a manual change made
|
|
// during the mute window (e.g. switching to "All messages") would silently get
|
|
// reverted when the stale timer fires.
|
|
export async function unmuteRoom(mx: MatrixClient, roomId: string): Promise<void> {
|
|
const currentMode = getLiveRoomNotificationMode(mx, roomId);
|
|
if (shouldResetMuteOnUnmute(currentMode)) {
|
|
await setRoomNotificationPreference(
|
|
mx,
|
|
roomId,
|
|
RoomNotificationMode.Unset,
|
|
RoomNotificationMode.Mute,
|
|
).catch(() => {});
|
|
}
|
|
saveMuteTimers(loadMuteTimers().filter((e) => e.roomId !== roomId));
|
|
}
|
|
|
|
export function scheduleMuteTimer(roomId: string, durationMs: number, onUnmute: () => void): void {
|
|
const unmuteAt = Date.now() + durationMs;
|
|
const existing = loadMuteTimers().filter((e) => e.roomId !== roomId);
|
|
saveMuteTimers([...existing, { roomId, unmuteAt }]);
|
|
setTimeout(onUnmute, Math.min(durationMs, MAX_MUTE_TIMEOUT_MS));
|
|
}
|