feat(nav): draft indicator on room-nav items
Composer drafts persist per room, but nothing in the room list showed which OTHER rooms had an unsent draft. Add a subtle chat-bubble icon on a room's nav item when it has a message draft (and isn't the open room), so half-written messages elsewhere are visible at a glance. - Shared pure helper hasMsgDraft (utils/draft.ts, unit-tested) replaces the inline emptiness check; the composer DraftIndicator now reuses it. - RoomNavItem reads a memoized selectAtom(draftAtom, hasMsgDraft) so a row re-renders only when its draft flag flips (the draft atom is written on room-leave, not per keystroke). Uses Icons.Message (pencil is reserved for the custom-name marker), muted, aria-label "Unsent draft". - useHydrateMsgDrafts (mounted in ClientNonUIFeatures) pre-fills the per-room draft atoms from draft-msg-* localStorage on startup, so indicators are correct after a page reload, not only after revisiting a room. Thread drafts (key contains ::) are skipped; room-level only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { Descendant } from 'slate';
|
||||
import { roomIdToMsgDraftAtomFamily } from '../state/room/roomInputDrafts';
|
||||
import { DRAFT_MSG_KEY_PREFIX, hasMsgDraft } from '../utils/draft';
|
||||
|
||||
/**
|
||||
* On startup, pre-fill the per-room message-draft atoms from their localStorage
|
||||
* persistence (`draft-msg-<roomId>`). 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.
|
||||
*/
|
||||
export function useHydrateMsgDrafts(): void {
|
||||
const store = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
if (!draftKey || draftKey.includes('::')) return; // room-level drafts only
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (!stored) return;
|
||||
const nodes = JSON.parse(stored) as Descendant[];
|
||||
if (Array.isArray(nodes) && hasMsgDraft(nodes)) {
|
||||
store.set(roomIdToMsgDraftAtomFamily(draftKey), nodes);
|
||||
}
|
||||
} catch {
|
||||
// Ignore a malformed stored draft.
|
||||
}
|
||||
});
|
||||
}, [store]);
|
||||
}
|
||||
Reference in New Issue
Block a user