feat(rooms): Disappearing Messages (MSC1763 m.room.retention)
B1 of the Matrix protocol-gaps roadmap, gate-green (688 tests): - StateEvent.RoomRetention + a shared utils/retention.ts (presets, isExpired, getRoomRetentionMs) with tests. - RoomRetention settings control (PL-gated preset buttons Off/1d/1w/1m) in Room Settings → General → Message Retention. - Timeline hides events past the room's max_lifetime (gated behind Show Hidden Events, like redactions) — messages visually disappear, losslessly. - Opt-in setting enforceRetentionLocally (default OFF) + a headless RetentionSweeper that permanently redacts the user's OWN expired messages (own-only, loaded-timeline scope, dedupe + retry). Nothing auto-deletes unless the user opts in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../features/settings/accou
|
||||
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
|
||||
import { toastQueueAtom } from '../../state/toast';
|
||||
import { useReminders } from '../../hooks/useReminders';
|
||||
import { getRoomRetentionMs, isExpired } from '../../utils/retention';
|
||||
import { useTauriUpdater } from '../../hooks/useTauriUpdater';
|
||||
import { TauriDesktopFeatures } from '../../components/TauriDesktopFeatures';
|
||||
import { KeyboardShortcutsDialog, useKeyboardShortcutsTrigger } from '../../features/shortcuts';
|
||||
@@ -687,6 +688,62 @@ function ReminderMonitor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MSC1763: opt-in local enforcement of room retention. When enabled, permanently
|
||||
// redacts the user's OWN messages once a room's retention window passes. Own-only
|
||||
// (no redact PL needed); scoped to loaded live-timeline events; dedupes in-flight
|
||||
// redactions and retries on the next tick. Default-off, so nothing auto-deletes
|
||||
// unless the user turns it on.
|
||||
function RetentionSweeper() {
|
||||
const mx = useMatrixClient();
|
||||
const [enforceRetentionLocally] = useSetting(settingsAtom, 'enforceRetentionLocally');
|
||||
const enabledRef = useRef(enforceRetentionLocally);
|
||||
enabledRef.current = enforceRetentionLocally;
|
||||
const redactingRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => {
|
||||
if (!enabledRef.current) return;
|
||||
const myId = mx.getUserId();
|
||||
if (!myId) return;
|
||||
const now = Date.now();
|
||||
mx.getRooms().forEach((room) => {
|
||||
const maxLifetime = getRoomRetentionMs(room);
|
||||
if (!maxLifetime) return;
|
||||
room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.forEach((ev) => {
|
||||
const evId = ev.getId();
|
||||
if (!evId || ev.getSender() !== myId) return;
|
||||
if (ev.isState() || ev.isRedacted() || ev.isSending()) return;
|
||||
const t = ev.getType();
|
||||
// Only actual messages — never our membership/topic/reactions.
|
||||
if (t !== 'm.room.message' && t !== 'm.room.encrypted' && t !== 'm.sticker') return;
|
||||
if (!isExpired(ev.getTs(), maxLifetime, now)) return;
|
||||
if (redactingRef.current.has(evId)) return;
|
||||
redactingRef.current.add(evId);
|
||||
mx.redactEvent(room.roomId, evId, undefined, { reason: 'expired' }).catch(() => {
|
||||
redactingRef.current.delete(evId);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
check();
|
||||
const interval = setInterval(check, 30_000);
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') check();
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
document.removeEventListener('visibilitychange', onVisible);
|
||||
};
|
||||
}, [mx]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const TAURI_UPDATE_CHECK_INTERVAL = 12 * 60 * 60_000; // 12 hours
|
||||
const TAURI_UPDATE_LAST_CHECK_KEY = 'lotus.tauriUpdateLastCheck';
|
||||
|
||||
@@ -773,6 +830,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
|
||||
<InviteNotifications />
|
||||
<MessageNotifications />
|
||||
<ReminderMonitor />
|
||||
<RetentionSweeper />
|
||||
<TauriUpdateFeature />
|
||||
<TauriDesktopFeatures />
|
||||
<LotusDenoiseFeature />
|
||||
|
||||
Reference in New Issue
Block a user