2023-03-30 18:28:50 -05:00

179 lines
6.0 KiB
TypeScript

import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
import type { MessageListType } from '../../../../global/types';
import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types';
import { EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import parseMessageInput from '../../../../util/parseMessageInput';
import focusEditableElement from '../../../../util/focusEditableElement';
import { hasMessageMedia } from '../../../../global/helpers';
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
import { fastRaf } from '../../../../util/schedulers';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
import useDerivedSignal from '../../../../hooks/useDerivedSignal';
const URL_ENTITIES = new Set<string>([ApiMessageEntityTypes.TextUrl, ApiMessageEntityTypes.Url]);
const DEBOUNCE_MS = 300;
const useEditing = (
getHtml: Signal<string>,
setHtml: (html: string) => void,
editedMessage: ApiMessage | undefined,
resetComposer: (shouldPreserveInput?: boolean) => void,
openDeleteModal: () => void,
chatId: string,
threadId: number,
type: MessageListType,
draft?: ApiFormattedText,
editingDraft?: ApiFormattedText,
replyingToId?: number,
): [VoidFunction, VoidFunction, boolean] => {
const { editMessage, setEditingDraft, toggleMessageWebPage } = getActions();
const [shouldForceShowEditing, setShouldForceShowEditing] = useState<boolean>();
useEffectWithPrevDeps(([prevEditedMessage, prevReplyingToId]) => {
if (!editedMessage) {
return;
}
if (replyingToId && prevReplyingToId !== replyingToId) {
setHtml('');
setShouldForceShowEditing(false);
return;
}
if (prevEditedMessage?.id === editedMessage.id && replyingToId === prevReplyingToId) {
return;
}
const text = !prevEditedMessage && editingDraft?.text.length ? editingDraft : editedMessage.content.text;
const html = getTextWithEntitiesAsHtml(text);
setHtml(html);
setShouldForceShowEditing(true);
// `fastRaf` would execute syncronously in this case
requestAnimationFrame(() => {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (messageInput) {
focusEditableElement(messageInput, true);
}
});
}, [editedMessage, replyingToId, editingDraft, setHtml]);
useEffect(() => {
if (!editedMessage) {
return;
}
const shouldSetNoWebPage = !('webPage' in editedMessage.content)
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type));
toggleMessageWebPage({
chatId,
threadId,
noWebPage: shouldSetNoWebPage,
});
}, [chatId, threadId, editedMessage]);
useEffect(() => {
if (!editedMessage) return undefined;
return () => {
const edited = parseMessageInput(getHtml());
const update = edited.text.length ? edited : undefined;
setEditingDraft({
chatId, threadId, type, text: update,
});
};
}, [chatId, editedMessage, getHtml, setEditingDraft, threadId, type]);
const detectLinkDebounced = useDebouncedResolver(() => {
if (!editedMessage) return false;
const edited = parseMessageInput(getHtml());
return !('webPage' in editedMessage.content)
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type))
&& !(edited.entities?.some((entity) => URL_ENTITIES.has(entity.type)));
}, [editedMessage, getHtml], DEBOUNCE_MS, true);
const getShouldResetNoWebPageDebounced = useDerivedSignal(detectLinkDebounced, [detectLinkDebounced, getHtml], true);
useEffectWithPrevDeps(([prevEditedMessage]) => {
if (!editedMessage || prevEditedMessage?.id !== editedMessage.id) {
return;
}
if (getShouldResetNoWebPageDebounced()) {
toggleMessageWebPage({
chatId,
threadId,
noWebPage: false,
});
}
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `as const` not yet supported by linter
}, [editedMessage, chatId, getHtml, threadId] as const);
const restoreNewDraftAfterEditing = useCallback(() => {
if (!draft) return;
// Run 1 frame after editing draft reset
fastRaf(() => {
setHtml(getTextWithEntitiesAsHtml(draft));
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (messageInput) {
requestAnimationFrame(() => {
focusEditableElement(messageInput, true);
});
}
});
}, [draft, setHtml]);
const handleEditCancel = useCallback(() => {
resetComposer();
restoreNewDraftAfterEditing();
}, [resetComposer, restoreNewDraftAfterEditing]);
const handleEditComplete = useCallback(() => {
const { text, entities } = parseMessageInput(getHtml());
if (!editedMessage) {
return;
}
if (!text && !hasMessageMedia(editedMessage)) {
openDeleteModal();
return;
}
editMessage({
text,
entities,
});
resetComposer();
restoreNewDraftAfterEditing();
}, [editMessage, editedMessage, getHtml, openDeleteModal, resetComposer, restoreNewDraftAfterEditing]);
const handleBlur = useCallback(() => {
if (!editedMessage) return;
const edited = parseMessageInput(getHtml());
const update = edited.text.length ? edited : undefined;
setEditingDraft({
chatId, threadId, type, text: update,
});
}, [chatId, editedMessage, getHtml, setEditingDraft, threadId, type]);
useBackgroundMode(handleBlur);
useBeforeUnload(handleBlur);
return [handleEditComplete, handleEditCancel, shouldForceShowEditing];
};
export default useEditing;