diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 933fd5433..c02393cd0 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -655,7 +655,15 @@ const Composer: FC = ({ chatBotCommands, ); - useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage, isInStoryViewer); + useDraft({ + draft, + chatId, + threadId, + getHtml, + setHtml, + editedMessage: editingMessage, + isDisabled: isInStoryViewer, + }); const resetComposer = useLastCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { diff --git a/src/components/middle/composer/hooks/useDraft.ts b/src/components/middle/composer/hooks/useDraft.ts index 3d6942cf8..2071bbbd3 100644 --- a/src/components/middle/composer/hooks/useDraft.ts +++ b/src/components/middle/composer/hooks/useDraft.ts @@ -1,4 +1,4 @@ -import { useEffect } from '../../../../lib/teact/teact'; +import { useEffect, useRef } from '../../../../lib/teact/teact'; import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom'; import { getActions } from '../../../../global'; @@ -17,8 +17,8 @@ import useLastCallback from '../../../../hooks/useLastCallback'; import useBackgroundMode from '../../../../hooks/useBackgroundMode'; import useBeforeUnload from '../../../../hooks/useBeforeUnload'; import { useStateRef } from '../../../../hooks/useStateRef'; -import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps'; import useRunDebounced from '../../../../hooks/useRunDebounced'; +import useLayoutEffectWithPrevDeps from '../../../../hooks/useLayoutEffectWithPrevDeps'; let isFrozen = false; @@ -30,21 +30,44 @@ function freeze() { }); } -const useDraft = ( - draft: ApiDraft | undefined, - chatId: string, - threadId: number, - getHtml: Signal, - setHtml: (html: string) => void, - editedMessage: ApiMessage | undefined, - isDisabled: boolean | undefined, -) => { +const useDraft = ({ + draft, + chatId, + threadId, + getHtml, + setHtml, + editedMessage, + isDisabled, +} : { + draft?: ApiDraft; + chatId: string; + threadId: number; + getHtml: Signal; + setHtml: (html: string) => void; + editedMessage?: ApiMessage; + isDisabled?: boolean; +}) => { const { saveDraft, clearDraft, loadCustomEmojis } = getActions(); + const isTouchedRef = useRef(false); + + useEffect(() => { + const html = getHtml(); + const isLocalDraft = draft?.isLocal !== undefined; + if (getTextWithEntitiesAsHtml(draft) === html && !isLocalDraft) { + isTouchedRef.current = false; + } else { + isTouchedRef.current = true; + } + }, [draft, getHtml]); + useEffect(() => { + isTouchedRef.current = false; + }, [chatId, threadId]); + const isEditing = Boolean(editedMessage); - const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}, shouldForce = false) => { - if (isDisabled || isEditing) return; + const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}) => { + if (isDisabled || isEditing || !isTouchedRef.current) return; const html = getHtml(); @@ -53,34 +76,31 @@ const useDraft = ( chatId: prevState.chatId ?? chatId, threadId: prevState.threadId ?? threadId, draft: parseMessageInput(html), - shouldForce, }); } else { clearDraft({ chatId: prevState.chatId ?? chatId, threadId: prevState.threadId ?? threadId, - shouldForce, }); } }); - const updateDraftRef = useStateRef(updateDraft); const runDebouncedForSaveDraft = useRunDebounced(DRAFT_DEBOUNCE, true, undefined, [chatId, threadId]); // Restore draft on chat change - useEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => { + useLayoutEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => { if (isDisabled) { return; } + const isTouched = isTouchedRef.current; if (chatId === prevChatId && threadId === prevThreadId) { + if (isTouched && !draft) return; // Prevent reset from other client if we have local edits if (!draft && prevDraft) { setHtml(''); } - if (!draft?.shouldForce) { - return; - } + if (isTouched) return; } if (editedMessage || !draft) { @@ -102,7 +122,7 @@ const useDraft = ( } }); } - }, [chatId, threadId, draft, setHtml, editedMessage, loadCustomEmojis, isDisabled]); + }, [chatId, threadId, draft, getHtml, setHtml, editedMessage, isDisabled]); // Save draft on chat change useEffect(() => { @@ -111,15 +131,13 @@ const useDraft = ( } return () => { - // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps if (!isEditing) { - // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps - updateDraftRef.current({ chatId, threadId }); + updateDraft({ chatId, threadId }); } freeze(); }; - }, [chatId, threadId, isEditing, updateDraftRef, isDisabled]); + }, [chatId, threadId, isEditing, updateDraft, isDisabled]); const chatIdRef = useStateRef(chatId); const threadIdRef = useStateRef(threadId); @@ -129,27 +147,23 @@ const useDraft = ( } if (!getHtml()) { - updateDraftRef.current(); + updateDraft(); return; } - const scopedShatId = chatIdRef.current; + const scopedСhatId = chatIdRef.current; const scopedThreadId = threadIdRef.current; runDebouncedForSaveDraft(() => { - if (chatIdRef.current === scopedShatId && threadIdRef.current === scopedThreadId) { - updateDraftRef.current(); + if (chatIdRef.current === scopedСhatId && threadIdRef.current === scopedThreadId) { + updateDraft(); } }); - }, [chatIdRef, getHtml, isDisabled, runDebouncedForSaveDraft, threadIdRef, updateDraftRef]); + }, [chatIdRef, getHtml, isDisabled, runDebouncedForSaveDraft, threadIdRef, updateDraft]); - function forceUpdateDraft() { - updateDraft(undefined, true); - } - - useBackgroundMode(forceUpdateDraft); - useBeforeUnload(forceUpdateDraft); + useBackgroundMode(updateDraft); + useBeforeUnload(updateDraft); }; export default useDraft; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index c352a8b06..38ab8e2e4 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -4,7 +4,7 @@ import { } from '../../index'; import type { - ActionReturnType, ApiDraft, GlobalState, TabArgs, + ActionReturnType, GlobalState, TabArgs, } from '../../types'; import type { ApiAttachment, @@ -396,7 +396,7 @@ addActionHandler('cancelSendingMessage', (global, actions, payload): ActionRetur addActionHandler('saveDraft', async (global, actions, payload): Promise => { const { - chatId, threadId, draft, shouldForce, + chatId, threadId, draft, } = payload; if (!draft) { return; @@ -408,7 +408,6 @@ addActionHandler('saveDraft', async (global, actions, payload): Promise => if (user && isDeletedUser(user)) return; draft.isLocal = true; - draft.shouldForce = shouldForce; global = replaceThreadParam(global, chatId, threadId, 'draft', draft); global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) }); @@ -435,7 +434,7 @@ addActionHandler('saveDraft', async (global, actions, payload): Promise => addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => { const { - chatId, threadId = MAIN_THREAD_ID, localOnly, shouldForce, + chatId, threadId = MAIN_THREAD_ID, localOnly, } = payload; if (!selectDraft(global, chatId, threadId)) { return undefined; @@ -447,8 +446,7 @@ addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => { void callApi('clearDraft', chat, selectThreadTopMessageId(global, chatId, threadId)); } - const newDraft: ApiDraft | undefined = shouldForce ? { shouldForce, text: '' } : undefined; - global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft); + global = replaceThreadParam(global, chatId, threadId, 'draft', undefined); global = updateChat(global, chatId, { draftDate: undefined }); return global; diff --git a/src/global/types.ts b/src/global/types.ts index 2d1fa1915..8fb4d367b 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -931,7 +931,7 @@ export type CallbackAction = Values<{ } }>; -export type ApiDraft = ApiFormattedText & { isLocal?: boolean; shouldForce?: boolean }; +export type ApiDraft = ApiFormattedText & { isLocal?: boolean }; type WithTabId = { tabId?: number }; @@ -1383,13 +1383,11 @@ export interface ActionPayloads { chatId: string; threadId: number; draft: ApiDraft; - shouldForce?: boolean; }; clearDraft: { chatId: string; threadId?: number; localOnly?: boolean; - shouldForce?: boolean; }; loadPinnedMessages: { chatId: string;