Checklists: Follow-up (#6044)

This commit is contained in:
zubiden 2025-07-06 13:52:05 +02:00 committed by Alexander Zinchuk
parent 310b5490a5
commit 2236f3de37
10 changed files with 83 additions and 43 deletions

View File

@ -2049,7 +2049,6 @@
"MessageActionTodoCompletionsAsNotDoneMultipleYou" = "You marked {tasks} as not done";
"MessageActionTodoTaskCount_one" = "{count} task";
"MessageActionTodoTaskCount_other" = "{count} tasks";
"ToDoListNewTasks" = "New Tasks";
"MenuButtonAppendTodoList" = "Add a Task";
"MessageActionAppendTodo" = "{peer} added a new task \"{task}\" to {list}";
"MessageActionAppendTodoYou" = "You added a new task \"{task}\" to {list}";

View File

@ -33,7 +33,7 @@
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.4375rem !important;
padding: 0 0.4375rem;
border-radius: 0.75rem;
font-size: 0.875rem !important;

View File

@ -1,7 +1,7 @@
import type { ChangeEvent } from 'react';
import type { ElementRef } from '../../../lib/teact/teact';
import {
memo, useEffect, useRef, useState,
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -18,6 +18,7 @@ import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fast
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';
@ -47,6 +48,7 @@ export type StateProps = {
type Item = {
id: number;
text: string;
isDisabled?: boolean;
};
const MAX_LIST_HEIGHT = 320;
@ -72,20 +74,34 @@ const ToDoListModal = ({
const [isOthersCanComplete, setIsOthersCanComplete] = useState(true);
const [hasErrors, setHasErrors] = useState<boolean>(false);
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const isAddTaskMode = renderingModal?.isAddTaskMode;
// Treat "Add task" as edit mode for own checklists
const isAddTaskMode = renderingModal?.forNewTask && !editingMessage?.isOutgoing;
const lang = useLang();
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<HTMLInputElement>) => {
if (isOpen && ref.current) {
ref.current.focus();
}
});
useEffect(() => {
useLayoutEffect(() => {
if (editingTodo) {
setTitle(editingTodo.title.text);
setIsOthersCanAppend(editingTodo.othersCanAppend ?? false);
@ -114,7 +130,20 @@ const ToDoListModal = ({
}
}, [isOpen]);
useEffect(() => focusInput(titleInputRef), [focusInput, 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();
@ -283,34 +312,37 @@ const ToDoListModal = ({
}
function renderItems() {
return items.map((item, index) => (
<div className="item-wrapper">
<InputText
maxLength={MAX_OPTION_LENGTH}
label={index !== items.length - 1 || items.length === maxItemsCount
? lang('TitleTask')
: lang('TitleAddTask')}
error={getItemsError(index)}
value={item.text}
onChange={(e) => updateItem(index, e.currentTarget.value)}
onKeyPress={handleKeyPress}
/>
{index !== items.length - 1 && (
<Button
className="item-remove-button"
round
color="translucent"
size="smaller"
ariaLabel={lang('Delete')}
onClick={() => removeItem(index)}
>
<Icon name="close" />
</Button>
)}
</div>
));
const tasksToRender = [...frozenTasks, ...items];
return tasksToRender.map((item, index) => {
const stateIndex = index - frozenTasks.length;
return (
<div className="item-wrapper">
<InputText
maxLength={MAX_OPTION_LENGTH}
label={index !== tasksToRender.length - 1 || tasksToRender.length === maxItemsCount
? lang('TitleTask')
: lang('TitleAddTask')}
error={getItemsError(stateIndex)}
value={item.text}
disabled={item.isDisabled}
onChange={(e) => updateItem(stateIndex, e.currentTarget.value)}
onKeyPress={handleKeyPress}
/>
{index !== tasksToRender.length - 1 && !item.isDisabled && (
<Button
className="item-remove-button"
round
color="translucent"
size="smaller"
ariaLabel={lang('Delete')}
onClick={() => removeItem(stateIndex)}
>
<Icon name="close" />
</Button>
)}
</div>
);
});
}
return (
@ -334,7 +366,7 @@ const ToDoListModal = ({
<div className="options-list custom-scroll" ref={itemsListRef}>
<h3 className="items-header">
{lang(isAddTaskMode ? 'ToDoListNewTasks' : 'TitleToDoList')}
{lang('TitleToDoList')}
</h3>
{renderItems()}

View File

@ -469,7 +469,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
openTodoListModal({
chatId: message.chatId,
messageId: message.id,
isAddTaskMode: true,
forNewTask: true,
});
}
closeMenu();

View File

@ -93,6 +93,12 @@
.label {
line-height: 1.3125rem;
}
.subLabel {
margin-top: 0;
font-size: 0.75rem;
color: var(--secondary-color);
}
}
input:checked ~ .Checkbox-main {

View File

@ -41,7 +41,7 @@ const TodoList = ({
isCurrentUserPremium,
isSynced,
}: OwnProps & StateProps) => {
const { toggleTodoCompleted, showNotification } = getActions();
const { toggleTodoCompleted, showNotification, requestConfetti } = getActions();
const { todo, completions } = todoList;
const { title, items, othersCanComplete } = todo;
const [completedTasks, setCompletedTasks] = useState<string[]>([]);
@ -83,6 +83,10 @@ const TodoList = ({
completedIds: newCompletedId ? [Number(newCompletedId)] : [],
incompletedIds: newIncompletedId ? [Number(newIncompletedId)] : [],
});
if (newCompletedTasks.length === items.length) {
requestConfetti({});
}
});
const isReadOnly = Boolean(message.forwardInfo) || (!othersCanComplete && !message.isOutgoing);
const isOutgoing = message.isOutgoing;

View File

@ -765,14 +765,14 @@ addActionHandler('closePollModal', (global, actions, payload): ActionReturnType
addActionHandler('openTodoListModal', (global, actions, payload): ActionReturnType => {
const {
chatId, messageId, isAddTaskMode, tabId = getCurrentTabId(),
chatId, messageId, forNewTask, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
todoListModal: {
chatId,
messageId,
isAddTaskMode,
forNewTask,
},
}, tabId);
});

View File

@ -2203,7 +2203,7 @@ export interface ActionPayloads {
openTodoListModal: {
chatId: string;
messageId?: number;
isAddTaskMode?: boolean;
forNewTask?: boolean;
} & WithTabId;
closeTodoListModal: WithTabId | undefined;
requestConfetti: (ConfettiParams & WithTabId) | WithTabId;

View File

@ -535,7 +535,7 @@ export type TabState = {
todoListModal?: {
chatId: string;
messageId?: number;
isAddTaskMode?: boolean;
forNewTask?: boolean;
};
preparedMessageModal?: {

View File

@ -1550,7 +1550,6 @@ export interface LangPair {
'AriaToDoCancel': undefined;
'TitleGroupToDoList': undefined;
'TitleYourToDoList': undefined;
'ToDoListNewTasks': undefined;
'MenuButtonAppendTodoList': undefined;
'PremiumMore': undefined;
'SubscribeToTelegramPremiumForToggleTask': undefined;