Draft: Fix conflict between with two active clients (#3817)

This commit is contained in:
Alexander Zinchuk 2023-09-13 12:21:33 +02:00
parent 369b0ac9ce
commit c43c95d3f1
4 changed files with 64 additions and 46 deletions

View File

@ -655,7 +655,15 @@ const Composer: FC<OwnProps & StateProps> = ({
chatBotCommands, chatBotCommands,
); );
useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage, isInStoryViewer); useDraft({
draft,
chatId,
threadId,
getHtml,
setHtml,
editedMessage: editingMessage,
isDisabled: isInStoryViewer,
});
const resetComposer = useLastCallback((shouldPreserveInput = false) => { const resetComposer = useLastCallback((shouldPreserveInput = false) => {
if (!shouldPreserveInput) { if (!shouldPreserveInput) {

View File

@ -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 { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
import { getActions } from '../../../../global'; import { getActions } from '../../../../global';
@ -17,8 +17,8 @@ import useLastCallback from '../../../../hooks/useLastCallback';
import useBackgroundMode from '../../../../hooks/useBackgroundMode'; import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useBeforeUnload from '../../../../hooks/useBeforeUnload'; import useBeforeUnload from '../../../../hooks/useBeforeUnload';
import { useStateRef } from '../../../../hooks/useStateRef'; import { useStateRef } from '../../../../hooks/useStateRef';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import useRunDebounced from '../../../../hooks/useRunDebounced'; import useRunDebounced from '../../../../hooks/useRunDebounced';
import useLayoutEffectWithPrevDeps from '../../../../hooks/useLayoutEffectWithPrevDeps';
let isFrozen = false; let isFrozen = false;
@ -30,21 +30,44 @@ function freeze() {
}); });
} }
const useDraft = ( const useDraft = ({
draft: ApiDraft | undefined, draft,
chatId: string, chatId,
threadId: number, threadId,
getHtml: Signal<string>, getHtml,
setHtml: (html: string) => void, setHtml,
editedMessage: ApiMessage | undefined, editedMessage,
isDisabled: boolean | undefined, isDisabled,
) => { } : {
draft?: ApiDraft;
chatId: string;
threadId: number;
getHtml: Signal<string>;
setHtml: (html: string) => void;
editedMessage?: ApiMessage;
isDisabled?: boolean;
}) => {
const { saveDraft, clearDraft, loadCustomEmojis } = getActions(); 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 isEditing = Boolean(editedMessage);
const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}, shouldForce = false) => { const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}) => {
if (isDisabled || isEditing) return; if (isDisabled || isEditing || !isTouchedRef.current) return;
const html = getHtml(); const html = getHtml();
@ -53,34 +76,31 @@ const useDraft = (
chatId: prevState.chatId ?? chatId, chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId, threadId: prevState.threadId ?? threadId,
draft: parseMessageInput(html), draft: parseMessageInput(html),
shouldForce,
}); });
} else { } else {
clearDraft({ clearDraft({
chatId: prevState.chatId ?? chatId, chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId, threadId: prevState.threadId ?? threadId,
shouldForce,
}); });
} }
}); });
const updateDraftRef = useStateRef(updateDraft);
const runDebouncedForSaveDraft = useRunDebounced(DRAFT_DEBOUNCE, true, undefined, [chatId, threadId]); const runDebouncedForSaveDraft = useRunDebounced(DRAFT_DEBOUNCE, true, undefined, [chatId, threadId]);
// Restore draft on chat change // Restore draft on chat change
useEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => { useLayoutEffectWithPrevDeps(([prevChatId, prevThreadId, prevDraft]) => {
if (isDisabled) { if (isDisabled) {
return; return;
} }
const isTouched = isTouchedRef.current;
if (chatId === prevChatId && threadId === prevThreadId) { if (chatId === prevChatId && threadId === prevThreadId) {
if (isTouched && !draft) return; // Prevent reset from other client if we have local edits
if (!draft && prevDraft) { if (!draft && prevDraft) {
setHtml(''); setHtml('');
} }
if (!draft?.shouldForce) { if (isTouched) return;
return;
}
} }
if (editedMessage || !draft) { 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 // Save draft on chat change
useEffect(() => { useEffect(() => {
@ -111,15 +131,13 @@ const useDraft = (
} }
return () => { return () => {
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
if (!isEditing) { if (!isEditing) {
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps updateDraft({ chatId, threadId });
updateDraftRef.current({ chatId, threadId });
} }
freeze(); freeze();
}; };
}, [chatId, threadId, isEditing, updateDraftRef, isDisabled]); }, [chatId, threadId, isEditing, updateDraft, isDisabled]);
const chatIdRef = useStateRef(chatId); const chatIdRef = useStateRef(chatId);
const threadIdRef = useStateRef(threadId); const threadIdRef = useStateRef(threadId);
@ -129,27 +147,23 @@ const useDraft = (
} }
if (!getHtml()) { if (!getHtml()) {
updateDraftRef.current(); updateDraft();
return; return;
} }
const scopedShatId = chatIdRef.current; const scopedСhatId = chatIdRef.current;
const scopedThreadId = threadIdRef.current; const scopedThreadId = threadIdRef.current;
runDebouncedForSaveDraft(() => { runDebouncedForSaveDraft(() => {
if (chatIdRef.current === scopedShatId && threadIdRef.current === scopedThreadId) { if (chatIdRef.current === scopedСhatId && threadIdRef.current === scopedThreadId) {
updateDraftRef.current(); updateDraft();
} }
}); });
}, [chatIdRef, getHtml, isDisabled, runDebouncedForSaveDraft, threadIdRef, updateDraftRef]); }, [chatIdRef, getHtml, isDisabled, runDebouncedForSaveDraft, threadIdRef, updateDraft]);
function forceUpdateDraft() { useBackgroundMode(updateDraft);
updateDraft(undefined, true); useBeforeUnload(updateDraft);
}
useBackgroundMode(forceUpdateDraft);
useBeforeUnload(forceUpdateDraft);
}; };
export default useDraft; export default useDraft;

View File

@ -4,7 +4,7 @@ import {
} from '../../index'; } from '../../index';
import type { import type {
ActionReturnType, ApiDraft, GlobalState, TabArgs, ActionReturnType, GlobalState, TabArgs,
} from '../../types'; } from '../../types';
import type { import type {
ApiAttachment, ApiAttachment,
@ -396,7 +396,7 @@ addActionHandler('cancelSendingMessage', (global, actions, payload): ActionRetur
addActionHandler('saveDraft', async (global, actions, payload): Promise<void> => { addActionHandler('saveDraft', async (global, actions, payload): Promise<void> => {
const { const {
chatId, threadId, draft, shouldForce, chatId, threadId, draft,
} = payload; } = payload;
if (!draft) { if (!draft) {
return; return;
@ -408,7 +408,6 @@ addActionHandler('saveDraft', async (global, actions, payload): Promise<void> =>
if (user && isDeletedUser(user)) return; if (user && isDeletedUser(user)) return;
draft.isLocal = true; draft.isLocal = true;
draft.shouldForce = shouldForce;
global = replaceThreadParam(global, chatId, threadId, 'draft', draft); global = replaceThreadParam(global, chatId, threadId, 'draft', draft);
global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) }); global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) });
@ -435,7 +434,7 @@ addActionHandler('saveDraft', async (global, actions, payload): Promise<void> =>
addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => { addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
const { const {
chatId, threadId = MAIN_THREAD_ID, localOnly, shouldForce, chatId, threadId = MAIN_THREAD_ID, localOnly,
} = payload; } = payload;
if (!selectDraft(global, chatId, threadId)) { if (!selectDraft(global, chatId, threadId)) {
return undefined; return undefined;
@ -447,8 +446,7 @@ addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
void callApi('clearDraft', chat, selectThreadTopMessageId(global, chatId, threadId)); void callApi('clearDraft', chat, selectThreadTopMessageId(global, chatId, threadId));
} }
const newDraft: ApiDraft | undefined = shouldForce ? { shouldForce, text: '' } : undefined; global = replaceThreadParam(global, chatId, threadId, 'draft', undefined);
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
global = updateChat(global, chatId, { draftDate: undefined }); global = updateChat(global, chatId, { draftDate: undefined });
return global; return global;

View File

@ -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 }; type WithTabId = { tabId?: number };
@ -1383,13 +1383,11 @@ export interface ActionPayloads {
chatId: string; chatId: string;
threadId: number; threadId: number;
draft: ApiDraft; draft: ApiDraft;
shouldForce?: boolean;
}; };
clearDraft: { clearDraft: {
chatId: string; chatId: string;
threadId?: number; threadId?: number;
localOnly?: boolean; localOnly?: boolean;
shouldForce?: boolean;
}; };
loadPinnedMessages: { loadPinnedMessages: {
chatId: string; chatId: string;