feat(chat): персистентные черновики сообщений — текст переживает перезагрузку страницы

This commit is contained in:
2026-09-21 15:14:30 +03:00
parent 006d020d15
commit 1f878b61a1
3 changed files with 40 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import {
import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth';
import { useEvents } from '@/hooks/useEvents';
import { usePersistentDraft } from '@/hooks/usePersistentDraft';
import { Button } from '@/components/ui/button';
import { AutoResizeTextarea } from '@/components/ui/auto-resize-textarea';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -178,8 +179,9 @@ const TaskChat = ({ taskId }: TaskChatProps) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const markReadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Composer: локальное состояние вместо react-hook-form (форма одного поля)
const [messageText, setMessageText] = useState('');
// Composer: локальное состояние вместо react-hook-form (форма одного поля).
// Черновик персистентный: переживает перезагрузку страницы (SW-деплой, редирект авторизации)
const [messageText, setMessageText] = usePersistentDraft(`draft:task:${taskId}`);
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
const [mentionedBotIds, setMentionedBotIds] = useState<number[]>([]);

View File

@@ -13,6 +13,7 @@ import { getAll as getAllQueued } from '@/lib/offlineQueue';
import { reportActiveChat, clearActiveChat, setActiveChatInSw, clearActiveChatInSw } from '@/lib/pwa';
import { useAuth } from '@/hooks/useAuth';
import { useEvents } from '@/hooks/useEvents';
import { usePersistentDraft } from '@/hooks/usePersistentDraft';
import { useToast } from '@/hooks/use-toast';
import { isPWAInstalled } from '@/lib/pwa';
import type { ChatAttachment } from '@/components/chat/ChatAttachments';
@@ -35,7 +36,8 @@ export function useChatController() {
const [groupMembersOpen, setGroupMembersOpen] = useState(false);
const [replyTo, setReplyTo] = useState<ConvMessage | null>(null);
const [editingMsg, setEditingMsg] = useState<ConvMessage | null>(null);
const [messageText, setMessageText] = useState('');
// Черновик персистентный: переживает перезагрузку страницы (SW-деплой, редирект авторизации)
const [messageText, setMessageText] = usePersistentDraft(`draft:conv:${activeConvId ?? 'none'}`);
const [editText, setEditText] = useState('');
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
const [mentionSearch, setMentionSearch] = useState<string | null>(null);
@@ -724,7 +726,7 @@ export function useChatController() {
setActiveConvId(id);
setReplyTo(null);
setEditingMsg(null);
setMessageText('');
// Черновик не очищаем: usePersistentDraft восстановит сохранённый текст этого диалога
setSearchQuery('');
markReadMutation.mutate(id);
if (isMobile) setMobileView('chat');

View File

@@ -0,0 +1,32 @@
import { useState, useEffect, useRef, useCallback } from 'react';
/**
* Черновик сообщения с персистентностью в localStorage.
*
* Текст неотправленного сообщения живёт только в памяти (useState) и пропадает
* при перезагрузке страницы (обновление Service Worker после деплоя, редирект
* авторизации, дискард вкладки). Хук сохраняет текст по ключу и восстанавливает
* при монтировании и при смене ключа (другой чат). Очистка — обычным setValue('').
*/
export function usePersistentDraft(storageKey: string): [string, (value: string) => void] {
const [value, setValue] = useState<string>(() => {
try { return localStorage.getItem(storageKey) ?? ''; } catch { return ''; }
});
const keyRef = useRef(storageKey);
keyRef.current = storageKey;
// Смена чата — восстанавливаем черновик этого чата
useEffect(() => {
try { setValue(localStorage.getItem(storageKey) ?? ''); } catch { setValue(''); }
}, [storageKey]);
const set = useCallback((next: string) => {
setValue(next);
try {
if (next) localStorage.setItem(keyRef.current, next);
else localStorage.removeItem(keyRef.current);
} catch { /* localStorage недоступен (приватный режим и т.п.) */ }
}, []);
return [value, set];
}