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,
);
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) {

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 { 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<string>,
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<string>;
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;

View File

@ -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<void> => {
const {
chatId, threadId, draft, shouldForce,
chatId, threadId, draft,
} = payload;
if (!draft) {
return;
@ -408,7 +408,6 @@ addActionHandler('saveDraft', async (global, actions, payload): Promise<void> =>
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<void> =>
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;

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 };
@ -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;