Files
cinny/src/app/utils/retention.ts
T
jaredandClaude Opus 4.8 82e52e1bc7 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>
2026-07-03 11:23:14 -04:00

33 lines
1.3 KiB
TypeScript

import { Room } from 'matrix-js-sdk';
import { StateEvent } from '../../types/matrix/room';
// MSC1763 — per-room message retention (`m.room.retention`). `max_lifetime` is a
// duration in milliseconds after which a message is considered expired.
export type RetentionContent = {
max_lifetime?: number;
};
const DAY_MS = 24 * 60 * 60 * 1000;
// Floor to avoid foot-guns (an admin fat-fingering a tiny value nuking a room).
export const RETENTION_MIN_MS = 10 * 60 * 1000;
export type RetentionPreset = { label: string; ms: number };
export const RETENTION_PRESETS: RetentionPreset[] = [
{ label: 'Off', ms: 0 },
{ label: '1 Day', ms: DAY_MS },
{ label: '1 Week', ms: 7 * DAY_MS },
{ label: '1 Month', ms: 30 * DAY_MS },
];
/** The room's active retention window in ms, or `undefined` when unset/disabled. */
export const getRoomRetentionMs = (room: Room): number | undefined => {
const event = room.currentState.getStateEvents(StateEvent.RoomRetention, '');
const ms = event?.getContent<RetentionContent>()?.max_lifetime;
return typeof ms === 'number' && ms > 0 ? ms : undefined;
};
/** True when an event at `tsMs` has passed the `maxLifetimeMs` retention window. */
export const isExpired = (tsMs: number, maxLifetimeMs: number, nowMs: number): boolean =>
maxLifetimeMs > 0 && nowMs - tsMs > maxLifetimeMs;