Merge pull request 'fix(notifications): timed mutes are lifted by any Lotus device, not just the one that set them (#333)' (#393) from fix/mute-timers-account-data into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

This commit was merged in pull request #393.
This commit is contained in:
2026-10-18 02:22:21 -04:00
6 changed files with 131 additions and 45 deletions
+27 -21
View File
@@ -59,8 +59,8 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { presenceStateFromSetting, usePresenceUpdater } from '../../hooks/usePresenceUpdater';
import {
MAX_MUTE_TIMEOUT_MS,
MuteTimerEntry,
loadMuteTimers,
migrateLocalMuteTimers,
unmuteRoom,
} from '../../features/room-nav/muteTimers';
import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../features/settings/account/Profile';
@@ -92,6 +92,7 @@ import {
shouldNotifyThreadReply,
THREAD_NOTIFICATIONS_FALLBACK_BEHAVIOR,
} from '../../utils/threadNotifications';
import { AccountDataEvent } from '../../../types/matrix/accountData';
// Grace period after the initial sync settles before invite notifications arm, so
// the async invite-atom population lands first and isn't mistaken for new invites.
@@ -370,30 +371,35 @@ function PresenceUpdater() {
function MuteTimerRestore() {
const mx = useMatrixClient();
// [Gitea #333] Timers are in account data, so a mute set on another device
// is lifted here too, and changes there (a new mute, a manual unmute)
// reschedule. Moves any timers this browser kept locally (pre-#333) first.
useEffect(() => {
const timers = loadMuteTimers();
if (timers.length === 0) return undefined;
const now = Date.now();
const pastDue: MuteTimerEntry[] = [];
const future: MuteTimerEntry[] = [];
timers.forEach((entry) => (entry.unmuteAt <= now ? pastDue : future).push(entry));
pastDue.forEach((entry) => {
unmuteRoom(mx, entry.roomId);
});
const handles = future.map((entry) =>
setTimeout(
() => {
unmuteRoom(mx, entry.roomId);
},
Math.min(entry.unmuteAt - now, MAX_MUTE_TIMEOUT_MS),
),
);
migrateLocalMuteTimers(mx).catch(() => {});
}, [mx]);
useEffect(() => {
let handles: ReturnType<typeof setTimeout>[] = [];
const schedule = () => {
handles.forEach(clearTimeout);
const now = Date.now();
handles = loadMuteTimers(mx).map((entry) =>
setTimeout(
() => {
unmuteRoom(mx, entry.roomId);
},
Math.max(0, Math.min(entry.unmuteAt - now, MAX_MUTE_TIMEOUT_MS)),
),
);
};
const onAccountData = (event: MatrixEvent) => {
if (event.getType() === AccountDataEvent.LotusMuteTimers) schedule();
};
schedule();
mx.on(ClientEvent.AccountData, onAccountData);
return () => {
handles.forEach(clearTimeout);
mx.removeListener(ClientEvent.AccountData, onAccountData);
};
}, [mx]);