fix(notifications): timed mutes are lifted by any Lotus device, not just the one that set them (#333)
CI / Build & Quality Checks (pull_request) Successful in 3m3s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m45s
CI / Build & Quality Checks (pull_request) Successful in 3m3s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m45s
A timed mute writes a server-side push rule (muted everywhere), but the
unmute timer lived in one browser's localStorage: close that laptop and
the room stayed muted on the phone until it was reopened, and forever
after a logout there. Timers now live in account data
(io.lotus.mute_timers: { timers: { [roomId]: unmuteAt } }). Every
client schedules from it, reschedules when it changes, and the first one
online after expiry unmutes. An indefinite mute clears a pending timer.
Local timers from before are migrated once.
Fixes #333
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3878a85339
commit
69e05bbf8d
@@ -58,8 +58,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';
|
||||
@@ -91,6 +91,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.
|
||||
@@ -369,30 +370,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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user