import type { ChangeEvent } from 'react'; import type { ElementRef } from '../../../lib/teact/teact'; import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiNewMediaTodo } from '../../../api/types'; import type { ApiMessage } from '../../../api/types'; import type { TabState } from '../../../global/types/tabState'; import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import { selectChatMessage } from '../../../global/selectors'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import { generateUniqueNumberId } from '../../../util/generateUniqueId'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import Checkbox from '../../ui/Checkbox'; import InputText from '../../ui/InputText'; import Modal from '../../ui/Modal'; import './ToDoListModal.scss'; export type OwnProps = { modal: TabState['todoListModal']; onSend: (todoList: ApiNewMediaTodo) => void; onClear: () => void; }; export type StateProps = { editingMessage?: ApiMessage; maxItemsCount: number; maxTitleLength: number; maxItemLength: number; }; type Item = { id: number; text: string; isDisabled?: boolean; }; const MAX_LIST_HEIGHT = 320; const MAX_OPTION_LENGTH = 100; const ToDoListModal = ({ modal, maxItemsCount, maxTitleLength, maxItemLength, editingMessage, onSend, onClear, }: OwnProps & StateProps) => { const { editTodo, closeTodoListModal, appendTodoList } = getActions(); const titleInputRef = useRef(); const itemsListRef = useRef(); const [title, setTitle] = useState(''); const [items, setItems] = useState([{ id: generateUniqueNumberId(), text: '' }]); const [isOthersCanAppend, setIsOthersCanAppend] = useState(true); const [isOthersCanComplete, setIsOthersCanComplete] = useState(true); const [hasErrors, setHasErrors] = useState(false); const lang = useLang(); const isOpen = Boolean(modal); const renderingModal = useCurrentOrPrev(modal); // Treat "Add task" as edit mode for own checklists const isAddTaskMode = renderingModal?.forNewTask && !editingMessage?.isOutgoing; const editingTodo = editingMessage?.content.todo?.todo; const frozenTasks = useMemo(() => { if (!isAddTaskMode || !editingTodo) { return MEMO_EMPTY_ARRAY; } return editingTodo.items.map((item) => ({ id: item.id, text: item.title.text, isDisabled: true, })); }, [isAddTaskMode, editingTodo]); const focusInput = useLastCallback((ref: ElementRef) => { if (isOpen && ref.current) { ref.current.focus(); } }); useLayoutEffect(() => { if (editingTodo) { setTitle(editingTodo.title.text); setIsOthersCanAppend(editingTodo.othersCanAppend ?? false); setIsOthersCanComplete(editingTodo.othersCanComplete ?? false); if (!isAddTaskMode) { const editingItems = editingTodo.items.map((item) => ({ id: item.id, text: item.title.text, })); if (editingItems.length < maxItemsCount) { editingItems.push({ id: generateUniqueNumberId(), text: '' }); } setItems(editingItems); } } }, [editingTodo, isAddTaskMode, maxItemsCount]); useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]); useEffect(() => { if (!isOpen) { setTitle(''); setItems([{ id: generateUniqueNumberId(), text: '' }]); setIsOthersCanAppend(true); setIsOthersCanComplete(true); setHasErrors(false); } }, [isOpen]); useEffect(() => { if (isOpen) { // Wait for the DOM to be updated requestMeasure(() => { if (renderingModal?.forNewTask) { const inputs = itemsListRef.current?.querySelectorAll('input'); const lastInput = inputs?.[inputs.length - 1]; lastInput?.focus(); } else { focusInput(titleInputRef); } }); } }, [focusInput, isOpen, renderingModal?.forNewTask]); const addNewItem = useLastCallback((newItems: Item[]) => { const id = generateUniqueNumberId(); setItems([...newItems, { id, text: '' }]); requestNextMutation(() => { const list = itemsListRef.current; if (!list) { return; } requestMeasure(() => { list.scrollTo({ top: list.scrollHeight, behavior: 'smooth' }); }); }); }); const handleCreate = useLastCallback(() => { setHasErrors(false); if (!isOpen) { return; } const todoItems = items .map((item) => { const text = item.text.trim(); if (!text) return undefined; return { id: item.id, title: { text: text.substring(0, maxItemLength), }, }; }).filter(Boolean); const titleTrimmed = title.trim().substring(0, maxTitleLength); if (!titleTrimmed || todoItems.length === 0) { setTitle(titleTrimmed); if (todoItems.length) { const itemsTrimmed = items.map((o) => ( { ...o, text: o.text.trim().substring(0, maxItemLength) })) .filter((o) => o.text.length); if (itemsTrimmed.length === 0) { addNewItem([]); } else { setItems([...itemsTrimmed, { id: generateUniqueNumberId(), text: '' }]); } } else { addNewItem([]); } setHasErrors(true); return; } if (isAddTaskMode && editingMessage) { appendTodoList({ chatId: editingMessage.chatId, messageId: editingMessage.id, items: todoItems, }); closeTodoListModal(); return; } const payload: ApiNewMediaTodo = { todo: { title: { text: titleTrimmed, }, items: todoItems, othersCanAppend: isOthersCanAppend, othersCanComplete: isOthersCanComplete, }, }; if (editingMessage) { editTodo({ chatId: editingMessage.chatId, todo: payload, messageId: editingMessage.id, }); } else { onSend(payload); } closeTodoListModal(); }); const updateItem = useLastCallback((index: number, text: string) => { const newItems = [...items]; newItems[index] = { ...newItems[index], text }; if (newItems[newItems.length - 1].text.trim().length && newItems.length < maxItemsCount) { addNewItem(newItems); } else { setItems(newItems); } }); const removeItem = useLastCallback((index: number) => { const newItems = [...items]; newItems.splice(index, 1); setItems(newItems); requestNextMutation(() => { if (!itemsListRef.current) { return; } itemsListRef.current.classList.toggle('overflown', itemsListRef.current.scrollHeight > MAX_LIST_HEIGHT); }); }); const handleIsOthersCanAppendChange = useLastCallback((e: ChangeEvent) => { setIsOthersCanAppend(e.target.checked); }); const handleIsOthersCanCompleteChange = useLastCallback((e: ChangeEvent) => { setIsOthersCanComplete(e.target.checked); }); const handleKeyPress = useLastCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleCreate(); } }); const handleTitleChange = useLastCallback((e: ChangeEvent) => { setTitle(e.target.value); }); const getTitleError = useLastCallback(() => { if (hasErrors && !title.trim().length) { return lang('ToDoListErrorChooseTitle'); } return undefined; }); const getItemsError = useLastCallback((index: number) => { const itemsTrimmed = items.map((o) => o.text.trim()).filter((o) => o.length); if (hasErrors && itemsTrimmed.length < 1 && !items[index].text.trim().length) { return lang('ToDoListErrorChooseTasks'); } return undefined; }); function renderHeader() { const title = isAddTaskMode ? 'TitleAppendToDoList' : editingMessage ? 'TitleEditToDoList' : 'TitleNewToDoList'; return (
{lang(title)}
); } function renderItems() { const tasksToRender = [...frozenTasks, ...items]; return tasksToRender.map((item, index) => { const stateIndex = index - frozenTasks.length; return (
updateItem(stateIndex, e.currentTarget.value)} onKeyPress={handleKeyPress} /> {index !== tasksToRender.length - 1 && !item.isDisabled && ( )}
); }); } const moreTasksCount = maxItemsCount - items.length - (isAddTaskMode && editingTodo ? editingTodo.items.length : 0); return ( {!isAddTaskMode && ( )} {isAddTaskMode && (
{title}
)}

{lang('TitleToDoList')}

{renderItems()}
{lang('HintTodoListTasksCount2', { count: moreTasksCount, }, { pluralValue: moreTasksCount, })}
{!isAddTaskMode && (
)} ); }; export default memo(withGlobal( (global, { modal }): StateProps => { const { appConfig } = global; const editingMessage = modal?.messageId ? selectChatMessage(global, modal.chatId, modal.messageId) : undefined; return { editingMessage, maxItemsCount: appConfig.todoItemsMax, maxTitleLength: appConfig.todoTitleLengthMax, maxItemLength: appConfig.todoItemLengthMax, }; }, )(ToDoListModal));