feat(chat): персистентные черновики сообщений — текст переживает перезагрузку страницы
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useEvents } from '@/hooks/useEvents';
|
import { useEvents } from '@/hooks/useEvents';
|
||||||
|
import { usePersistentDraft } from '@/hooks/usePersistentDraft';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AutoResizeTextarea } from '@/components/ui/auto-resize-textarea';
|
import { AutoResizeTextarea } from '@/components/ui/auto-resize-textarea';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
@@ -178,8 +179,9 @@ const TaskChat = ({ taskId }: TaskChatProps) => {
|
|||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const markReadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const markReadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
// Composer: локальное состояние вместо react-hook-form (форма одного поля)
|
// Composer: локальное состояние вместо react-hook-form (форма одного поля).
|
||||||
const [messageText, setMessageText] = useState('');
|
// Черновик персистентный: переживает перезагрузку страницы (SW-деплой, редирект авторизации)
|
||||||
|
const [messageText, setMessageText] = usePersistentDraft(`draft:task:${taskId}`);
|
||||||
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
|
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
|
||||||
const [mentionedBotIds, setMentionedBotIds] = useState<number[]>([]);
|
const [mentionedBotIds, setMentionedBotIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getAll as getAllQueued } from '@/lib/offlineQueue';
|
|||||||
import { reportActiveChat, clearActiveChat, setActiveChatInSw, clearActiveChatInSw } from '@/lib/pwa';
|
import { reportActiveChat, clearActiveChat, setActiveChatInSw, clearActiveChatInSw } from '@/lib/pwa';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useEvents } from '@/hooks/useEvents';
|
import { useEvents } from '@/hooks/useEvents';
|
||||||
|
import { usePersistentDraft } from '@/hooks/usePersistentDraft';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { isPWAInstalled } from '@/lib/pwa';
|
import { isPWAInstalled } from '@/lib/pwa';
|
||||||
import type { ChatAttachment } from '@/components/chat/ChatAttachments';
|
import type { ChatAttachment } from '@/components/chat/ChatAttachments';
|
||||||
@@ -35,7 +36,8 @@ export function useChatController() {
|
|||||||
const [groupMembersOpen, setGroupMembersOpen] = useState(false);
|
const [groupMembersOpen, setGroupMembersOpen] = useState(false);
|
||||||
const [replyTo, setReplyTo] = useState<ConvMessage | null>(null);
|
const [replyTo, setReplyTo] = useState<ConvMessage | null>(null);
|
||||||
const [editingMsg, setEditingMsg] = 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 [editText, setEditText] = useState('');
|
||||||
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
|
const [mentionedUserIds, setMentionedUserIds] = useState<number[]>([]);
|
||||||
const [mentionSearch, setMentionSearch] = useState<string | null>(null);
|
const [mentionSearch, setMentionSearch] = useState<string | null>(null);
|
||||||
@@ -724,7 +726,7 @@ export function useChatController() {
|
|||||||
setActiveConvId(id);
|
setActiveConvId(id);
|
||||||
setReplyTo(null);
|
setReplyTo(null);
|
||||||
setEditingMsg(null);
|
setEditingMsg(null);
|
||||||
setMessageText('');
|
// Черновик не очищаем: usePersistentDraft восстановит сохранённый текст этого диалога
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
markReadMutation.mutate(id);
|
markReadMutation.mutate(id);
|
||||||
if (isMobile) setMobileView('chat');
|
if (isMobile) setMobileView('chat');
|
||||||
|
|||||||
32
client/src/hooks/usePersistentDraft.ts
Normal file
32
client/src/hooks/usePersistentDraft.ts
Normal 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];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user