fix(desktop): persist chat drafts across section navigation (#381)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-04-21 14:29:15 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent d26fda9e4d
commit 4079e483a4
3 changed files with 43 additions and 25 deletions
+23 -8
View File
@@ -8,31 +8,46 @@ export type DraftState = {
selectionEnd: number;
};
export function useDrafts() {
const draftsRef = React.useRef(new Map<string, DraftState>());
const sharedDrafts = new Map<string, DraftState>();
export function useDrafts() {
const saveDraft = React.useCallback(
(channelId: string, draft: DraftState) => {
if (draft.content.trim().length === 0) {
return;
}
const drafts = draftsRef.current;
drafts.set(channelId, draft);
trimMapToSize(drafts, 50);
sharedDrafts.set(channelId, draft);
trimMapToSize(sharedDrafts, 50);
},
[],
);
const loadDraft = React.useCallback(
(channelId: string): DraftState | undefined => {
return draftsRef.current.get(channelId);
return sharedDrafts.get(channelId);
},
[],
);
const clearDraft = React.useCallback((channelId: string) => {
draftsRef.current.delete(channelId);
sharedDrafts.delete(channelId);
}, []);
return { saveDraft, loadDraft, clearDraft };
/** Save draft if content is non-empty, otherwise clear it. */
const persistDraft = React.useCallback(
(channelId: string, content: string) => {
if (content.trim().length > 0) {
saveDraft(channelId, {
content,
selectionEnd: content.length,
selectionStart: content.length,
});
} else {
clearDraft(channelId);
}
},
[saveDraft, clearDraft],
);
return { saveDraft, loadDraft, clearDraft, persistDraft };
}
@@ -30,6 +30,7 @@ type MessageComposerProps = {
channelId?: string | null;
channelName: string;
disabled?: boolean;
draftKey?: string;
editTarget?: {
author: string;
body: string;
@@ -58,6 +59,7 @@ export function MessageComposer({
channelId = null,
channelName,
disabled = false,
draftKey,
editTarget = null,
isSending = false,
onCancelEdit,
@@ -83,7 +85,8 @@ export function MessageComposer({
}, []);
const drafts = useDrafts();
const previousChannelIdRef = React.useRef<string | null>(null);
const effectiveDraftKey = draftKey ?? channelId;
const previousDraftKeyRef = React.useRef<string | null>(null);
const mentions = useMentions(channelId);
const channelLinks = useChannelLinks();
@@ -151,24 +154,17 @@ export function MessageComposer({
});
// ── Channel switching: save/restore drafts ──────────────────────────
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the sole trigger
// biome-ignore lint/correctness/useExhaustiveDependencies: effectiveDraftKey is the sole trigger
React.useEffect(() => {
const prevId = previousChannelIdRef.current;
if (prevId) {
const currentContent = contentRef.current;
if (currentContent.trim().length > 0) {
drafts.saveDraft(prevId, {
content: currentContent,
selectionEnd: currentContent.length,
selectionStart: currentContent.length,
});
} else {
drafts.clearDraft(prevId);
}
const prevKey = previousDraftKeyRef.current;
if (prevKey) {
drafts.persistDraft(prevKey, contentRef.current);
}
previousChannelIdRef.current = channelId;
previousDraftKeyRef.current = effectiveDraftKey;
const saved = channelId ? drafts.loadDraft(channelId) : undefined;
const saved = effectiveDraftKey
? drafts.loadDraft(effectiveDraftKey)
: undefined;
if (saved) {
setContent(saved.content);
contentRef.current = saved.content;
@@ -184,7 +180,13 @@ export function MessageComposer({
setIsEmojiPickerOpen(false);
mentions.clearMentions();
channelLinks.clearChannels();
}, [channelId]);
return () => {
if (effectiveDraftKey) {
drafts.persistDraft(effectiveDraftKey, contentRef.current);
}
};
}, [effectiveDraftKey]);
// ── Edit mode: pre-fill content ─────────────────────────────────────
// biome-ignore lint/correctness/useExhaustiveDependencies: editTarget?.id is the trigger
@@ -258,6 +258,7 @@ export function MessageThreadPanel({
channelId={channelId}
channelName={channelName}
disabled={disabled || isSending || !channelId}
draftKey={`thread:${threadHead.id}`}
isSending={isSending}
onCancelReply={composerReplyTarget ? onCancelReply : undefined}
onSend={onSend}