import { useEffect } from 'react'; import { useStore } from 'jotai'; import { roomIdToMsgDraftAtomFamily } from '../state/room/roomInputDrafts'; import { DRAFT_MSG_KEY_PREFIX, hasMsgDraft } from '../utils/draft'; import { useMatrixClient } from './useMatrixClient'; /** * On startup, pre-fill the per-room message-draft atoms from their localStorage * persistence (`draft-msg-`). RoomInput only restores a draft into its * atom when that room's composer mounts, so without this a background room's * persisted draft wouldn't reflect in the room-nav draft indicator until the * room is opened. Runs once; only touches the same atoms RoomInput restores from * (identical content), so composer restore is unaffected. * * Thread drafts (key contains `::`) are skipped — the nav indicator is room-level. * * [Gitea #41] Drafts are stored as `{ userId, nodes }` (RoomInput's persist * path) so a draft written by a different account never gets hydrated into the * currently logged-in user's session. A legacy draft (stored as a bare array, * pre-dating user-scoping) has no userId to check, so it's treated as foreign * and dropped rather than trusted. */ export function useHydrateMsgDrafts(): void { const store = useStore(); const mx = useMatrixClient(); useEffect(() => { const userId = mx.getUserId(); let keys: string[]; try { keys = Object.keys(localStorage); } catch { return; } keys.forEach((key) => { if (!key.startsWith(DRAFT_MSG_KEY_PREFIX)) return; const draftKey = key.slice(DRAFT_MSG_KEY_PREFIX.length); // Skip thread drafts (`::<$threadRootEventId>`) — room-level only. // Match `::$` specifically so an IPv6-literal server name (e.g. `[::1]`) // in a roomId isn't mistaken for a thread key. if (!draftKey || draftKey.includes('::$')) return; try { const stored = localStorage.getItem(key); if (!stored) return; const parsed = JSON.parse(stored); const foreign = !parsed || typeof parsed !== 'object' || Array.isArray(parsed) || parsed.userId !== userId; if (foreign) { // Another account's (or a pre-scoping legacy) draft — never hydrate it, // and drop it so it can't resurface for the next login either. localStorage.removeItem(key); return; } const nodes = parsed.nodes; if (Array.isArray(nodes) && hasMsgDraft(nodes)) { store.set(roomIdToMsgDraftAtomFamily(draftKey), nodes); } } catch { // Ignore a malformed stored draft. } }); }, [store, mx]); }