Address findings from 2 review agents on the room-nav draft indicator: - Icon semantics (a11y): Icons.Message read as message activity and collided with two existing bubble uses in the same row (call-chat toggle, mark-unread), especially next to the unread badge. Replace it with the composer's shared DraftDot (a small color.Success.Main dot), so both draft surfaces share one visual language. Rendered as a role="img" span with aria-label "Unsent draft" (reliably announced, unlike a bare aria-labelled svg). - Precise thread-key filter: hydration skipped any draftKey containing '::', which would also skip an IPv6-literal server name in a roomId. Match '::$' (thread root is an event id) so only real thread drafts are skipped. - Defensive hasMsgDraft: guard toPlainText so a corrupted/foreign draft value can't throw during a nav render. - Clear the draft atom on send: the send / scheduled-send handlers reset the editor and localStorage but left the jotai draft atom set, so the composer DraftIndicator could show a stale dot after sending a restored draft. Add setMsgDraft([]) to both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
20 lines
719 B
TypeScript
20 lines
719 B
TypeScript
import { Descendant } from 'slate';
|
|
import { toPlainText } from '../components/editor/output';
|
|
|
|
/** localStorage key prefix for a persisted composer draft (`draft-msg-<draftKey>`). */
|
|
export const DRAFT_MSG_KEY_PREFIX = 'draft-msg-';
|
|
|
|
/**
|
|
* Whether a Slate composer draft holds real (non-whitespace) text, as opposed to
|
|
* an empty editor (which is still a non-empty array of empty paragraphs). Shared
|
|
* by the composer's DraftIndicator and the room-nav draft indicator.
|
|
*/
|
|
export function hasMsgDraft(draft: Descendant[]): boolean {
|
|
try {
|
|
return toPlainText(draft, false).trim().length > 0;
|
|
} catch {
|
|
// A corrupted/foreign draft shape must never crash a render that calls this.
|
|
return false;
|
|
}
|
|
}
|