Layer 205: Introduce Checklists (#6010)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-07-04 14:12:22 +02:00
parent bcaeee1d94
commit 9bfd2d569c
54 changed files with 2029 additions and 111 deletions

View File

@ -9,6 +9,9 @@ import {
SERVICE_NOTIFICATIONS_USER_ID,
STORY_EXPIRE_PERIOD,
STORY_VIEWERS_EXPIRE_PERIOD,
TODO_ITEM_LENGTH_LIMIT,
TODO_ITEMS_LIMIT,
TODO_TITLE_LENGTH_LIMIT,
} from '../../../config';
import localDb from '../localDb';
import { buildJson } from './misc';
@ -99,6 +102,9 @@ export interface GramJsAppConfig extends LimitsConfig {
stars_stargift_resale_amount_min?: number;
stars_stargift_resale_commission_permille?: number;
poll_answers_max?: number;
todo_items_max?: number;
todo_title_length_max?: number;
todo_item_length_max?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -200,5 +206,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
starsStargiftResaleAmountMax: appConfig.stars_stargift_resale_amount_max,
starsStargiftResaleCommissionPermille: appConfig.stars_stargift_resale_commission_permille,
pollMaxAnswers: appConfig.poll_answers_max,
todoItemsMax: appConfig.todo_items_max ?? TODO_ITEMS_LIMIT,
todoTitleLengthMax: appConfig.todo_title_length_max ?? TODO_TITLE_LENGTH_LIMIT,
todoItemLengthMax: appConfig.todo_item_length_max ?? TODO_ITEM_LENGTH_LIMIT,
};
}

View File

@ -6,6 +6,7 @@ import type { ApiMessageAction } from '../../types/messageActions';
import { buildApiBotApp } from './bots';
import { buildApiFormattedText, buildApiPhoto } from './common';
import { buildApiStarGift } from './gifts';
import { buildTodoItem } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
const UNSUPPORTED_ACTION: ApiMessageAction = {
@ -446,6 +447,25 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
count,
};
}
if (action instanceof GramJs.MessageActionTodoCompletions) {
const {
completed, incompleted,
} = action;
return {
mediaType: 'action',
type: 'todoCompletions',
completedIds: completed,
incompletedIds: incompleted,
};
}
if (action instanceof GramJs.MessageActionTodoAppendTasks) {
const { list } = action;
return {
mediaType: 'action',
type: 'todoAppendTasks',
items: list.map(buildTodoItem),
};
}
return UNSUPPORTED_ACTION;
}

View File

@ -11,12 +11,14 @@ import type {
ApiLocation,
ApiMediaExtendedPreview,
ApiMediaInvoice,
ApiMediaTodo,
ApiMessageStoryData,
ApiPaidMedia,
ApiPhoto,
ApiPoll,
ApiStarGiftUnique,
ApiSticker,
ApiTodoItem,
ApiVideo,
ApiVoice,
ApiWebDocument,
@ -64,7 +66,7 @@ export function buildMessageContent(
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
if (mtpMessage.message && !hasUnsupportedMedia
&& !content.sticker && !content.pollId && !content.contact && !content.video?.isRound) {
&& !content.sticker && !content.pollId && !content.todo && !content.contact && !content.video?.isRound) {
const text = buildMessageTextContent(mtpMessage.message, mtpMessage.entities);
const textWithTimestamps = addTimestampEntities(text);
content = {
@ -153,6 +155,9 @@ export function buildMessageMediaContent(
const pollId = buildPollIdFromMedia(media);
if (pollId) return { pollId };
const todo = buildTodoFromMedia(media);
if (todo) return { todo };
const webPage = buildWebPage(media);
if (webPage) return { webPage };
@ -513,6 +518,14 @@ export function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | un
return buildPoll(media.poll, media.results);
}
function buildTodoFromMedia(media: GramJs.TypeMessageMedia): ApiMediaTodo | undefined {
if (!(media instanceof GramJs.MessageMediaToDo)) {
return undefined;
}
return buildTodo(media.todo, media.completions);
}
function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiMediaInvoice | undefined {
if (!(media instanceof GramJs.MessageMediaInvoice)) {
return undefined;
@ -711,6 +724,36 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
};
}
export function buildTodoItem(item: GramJs.TodoItem): ApiTodoItem {
return {
id: item.id,
title: buildApiFormattedText(item.title),
};
}
export function buildTodo(todo: GramJs.TodoList, completions?: GramJs.TodoCompletion[]): ApiMediaTodo {
const { title, list: items } = todo;
const todoItems = items.map(buildTodoItem);
const todoCompletions = completions?.map((completion) => ({
itemId: completion.id,
completedBy: completion.completedBy.toString(),
completedAt: completion.date,
}));
return {
mediaType: 'todo',
todo: {
title: buildApiFormattedText(title),
items: todoItems,
othersCanAppend: todo.othersCanAppend,
othersCanComplete: todo.othersCanComplete,
},
completions: todoCompletions,
};
}
export function buildMediaInvoice(media: GramJs.MessageMediaInvoice): ApiMediaInvoice {
const {
description, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,

View File

@ -8,10 +8,12 @@ import type {
ApiFactCheck,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
ApiMediaTodo,
ApiMessage,
ApiMessageEntity,
ApiMessageForwardInfo,
ApiMessageReportResult,
ApiNewMediaTodo,
ApiNewPoll,
ApiPeer,
ApiPhoto,
@ -382,6 +384,13 @@ function buildNewPoll(poll: ApiNewPoll, localId: number): ApiPoll {
};
}
function buildNewTodo(todo: ApiNewMediaTodo): ApiMediaTodo {
return {
mediaType: 'todo',
todo: todo.todo,
};
}
export function buildLocalMessage(
chat: ApiChat,
lastMessageId?: number,
@ -392,6 +401,7 @@ export function buildLocalMessage(
sticker?: ApiSticker,
gif?: ApiVideo,
poll?: ApiNewPoll,
todo?: ApiNewMediaTodo,
contact?: ApiContact,
groupedId?: string,
scheduledAt?: number,
@ -409,6 +419,7 @@ export function buildLocalMessage(
const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
const localPoll = poll && buildNewPoll(poll, localId);
const localTodo = todo && buildNewTodo(todo);
const formattedText = text ? addTimestampEntities({ text, entities }) : undefined;
@ -423,6 +434,7 @@ export function buildLocalMessage(
contact,
storyData: story && { mediaType: 'storyData', ...story },
pollId: localPoll?.id,
todo: localTodo,
}),
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
isOutgoing: !isChannel,

View File

@ -16,6 +16,7 @@ import type {
ApiInputReplyInfo,
ApiInputStorePaymentPurpose,
ApiMessageEntity,
ApiNewMediaTodo,
ApiNewPoll,
ApiPhoneCall,
ApiPhoto,
@ -263,6 +264,28 @@ export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
});
}
export function buildInputTodo(todo: ApiNewMediaTodo) {
const { title, items } = todo.todo;
const todoItems = items.map((item) => {
return new GramJs.TodoItem({
id: item.id,
title: buildInputTextWithEntities(item.title),
});
});
const todoList = new GramJs.TodoList({
title: buildInputTextWithEntities(title),
list: todoItems,
othersCanAppend: todo.todo.othersCanAppend || undefined,
othersCanComplete: todo.todo.othersCanComplete || undefined,
});
return new GramJs.InputMediaTodo({
todo: todoList,
});
}
export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFilter | GramJs.DialogFilterChatlist {
const {
emoticon,

View File

@ -18,11 +18,13 @@ import type {
ApiMessageEntity,
ApiMessageSearchContext,
ApiMessageSearchType,
ApiNewMediaTodo,
ApiOnProgress,
ApiPeer,
ApiPoll,
ApiReaction,
ApiSendMessageAction,
ApiTodoItem,
ApiUser,
ApiUserStatus,
MediaContent,
@ -47,7 +49,7 @@ import {
import { fetchFile } from '../../../util/files';
import { compact, split } from '../../../util/iteratees';
import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { getServerTime } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform';
import {
buildApiChatFromPreview,
@ -82,6 +84,7 @@ import {
buildInputReplyTo,
buildInputStory,
buildInputTextWithEntities,
buildInputTodo,
buildInputUser,
buildMessageFromUpdate,
buildMtpMessageEntity,
@ -263,7 +266,7 @@ export function sendMessageLocal(
params: SendMessageParams,
) {
const {
chat, lastMessageId, text, entities, replyInfo, attachment, sticker, story, gif, poll, contact,
chat, lastMessageId, text, entities, replyInfo, attachment, sticker, story, gif, poll, todo, contact,
scheduledAt, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending, messagePriceInStars,
} = params;
@ -282,6 +285,7 @@ export function sendMessageLocal(
sticker,
gif,
poll,
todo,
contact,
groupedId,
scheduledAt,
@ -311,7 +315,7 @@ export function sendApiMessage(
onProgress?: ApiOnProgress,
) {
const {
chat, text, entities, replyInfo, attachment, sticker, story, gif, poll, contact,
chat, text, entities, replyInfo, attachment, sticker, story, gif, poll, todo, contact,
isSilent, scheduledAt, groupedId, noWebPage, sendAs, shouldUpdateStickerSetOrder,
isInvertedMedia, effectId, webPageMediaSize, webPageUrl, messagePriceInStars,
} = params;
@ -368,6 +372,8 @@ export function sendApiMessage(
media = buildInputMediaDocument(gif);
} else if (poll) {
media = buildInputPoll(poll, randomId);
} else if (todo) {
media = buildInputTodo(todo);
} else if (story) {
media = buildInputStory(story);
} else if (webPageUrl && webPageMediaSize) {
@ -629,7 +635,7 @@ export async function editMessage({
attachment?: ApiAttachment;
noWebPage?: boolean;
}, onProgress?: ApiOnProgress) {
const isScheduled = message.date * 1000 > Date.now() + getServerTimeOffset() * 1000;
const isScheduled = message.date * 1000 > getServerTime() * 1000;
const media = attachment && buildUploadingMedia(attachment);
@ -702,6 +708,110 @@ export async function editMessage({
}
}
export async function editTodo({
chat,
message,
todo,
}: {
chat: ApiChat;
message: ApiMessage;
todo: ApiNewMediaTodo;
}) {
const media = buildInputTodo(todo);
const isScheduled = message.date * 1000 > getServerTime() * 1000;
const newContent: MediaContent = {
...message.content,
todo: {
mediaType: 'todo',
todo: todo.todo,
},
};
const messageUpdate: Partial<ApiMessage> = {
...message,
content: newContent,
};
sendApiUpdate({
'@type': isScheduled ? 'updateScheduledMessage' : 'updateMessage',
id: message.id,
chatId: chat.id,
message: messageUpdate,
});
try {
await invokeRequest(new GramJs.messages.EditMessage({
media,
peer: buildInputPeer(chat.id, chat.accessHash),
id: message.id,
}), { shouldThrow: true });
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn(err);
}
const { message: messageErr } = err as Error;
sendApiUpdate({
'@type': 'error',
error: {
message: messageErr,
hasErrorKey: true,
},
});
// Rollback changes
sendApiUpdate({
'@type': 'updateMessage',
id: message.id,
chatId: chat.id,
message,
});
}
}
export async function appendTodoList({
chat,
message,
items,
}: {
chat: ApiChat;
message: ApiMessage;
items: ApiTodoItem[];
}) {
const todoItems = items.map((item) => {
return new GramJs.TodoItem({
id: item.id,
title: buildInputTextWithEntities(item.title),
});
});
try {
await invokeRequest(new GramJs.messages.AppendTodoList({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: message.id,
list: todoItems,
}), { shouldThrow: true });
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn(err);
}
const { message: messageErr } = err as Error;
sendApiUpdate({
'@type': 'error',
error: {
message: messageErr,
hasErrorKey: true,
},
});
}
}
export async function rescheduleMessage({
chat,
message,
@ -1507,6 +1617,24 @@ export async function sendPollVote({
}));
}
export async function toggleTodoCompleted({
chat, messageId, completedIds, incompletedIds,
}: {
chat: ApiChat;
messageId: number;
completedIds: number[];
incompletedIds: number[];
}) {
const { id, accessHash } = chat;
await invokeRequest(new GramJs.messages.ToggleTodoCompleted({
peer: buildInputPeer(id, accessHash),
msgId: messageId,
completed: completedIds,
incompleted: incompletedIds,
}));
}
export async function closePoll({
chat, messageId, poll,
}: {

View File

@ -252,11 +252,11 @@ export async function deleteContact({
});
}
export async function addNoPaidMessagesException({ user, shouldRefundCharged }: {
export async function toggleNoPaidMessagesException({ user, shouldRefundCharged }: {
user: ApiUser;
shouldRefundCharged?: boolean;
}) {
const result = await invokeRequest(new GramJs.account.AddNoPaidMessagesException({
const result = await invokeRequest(new GramJs.account.ToggleNoPaidMessagesException ({
refundCharged: shouldRefundCharged ? true : undefined,
userId: buildInputUser(user.id, user.accessHash),
}));

View File

@ -1,5 +1,6 @@
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
import type { ApiBotApp, ApiFormattedText, ApiPhoto } from './messages';
import type { ApiTodoItem } from './messages';
import type { ApiStarGiftRegular, ApiStarGiftUnique } from './stars';
interface ActionMediaType {
@ -281,6 +282,17 @@ export interface ApiMessageActionPaidMessagesPrice extends ActionMediaType {
isAllowedInChannel?: boolean;
}
export interface ApiMessageActionTodoCompletions extends ActionMediaType {
type: 'todoCompletions';
completedIds: number[];
incompletedIds: number[];
}
export interface ApiMessageActionTodoAppendTasks extends ActionMediaType {
type: 'todoAppendTasks';
items: ApiTodoItem[];
}
export interface ApiMessageActionUnsupported extends ActionMediaType {
type: 'unsupported';
}
@ -298,4 +310,5 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
| ApiMessageActionChannelJoined | ApiMessageActionGiftCode | ApiMessageActionGiveawayLaunch
| ApiMessageActionGiveawayResults | ApiMessageActionPaymentRefunded | ApiMessageActionGiftStars
| ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice;
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionTodoCompletions
| ApiMessageActionTodoAppendTasks;

View File

@ -332,6 +332,34 @@ export type ApiNewPoll = {
};
};
export interface ApiTodoItem {
id: number;
title: ApiFormattedText;
}
export interface ApiTodoList {
title: ApiFormattedText;
items: ApiTodoItem[];
othersCanAppend?: boolean;
othersCanComplete?: boolean;
}
export interface ApiTodoCompletion {
itemId: number;
completedBy: string;
completedAt: number;
}
export interface ApiMediaTodo {
mediaType: 'todo';
todo: ApiTodoList;
completions?: ApiTodoCompletion[];
}
export type ApiNewMediaTodo = {
todo: ApiTodoList;
};
export interface ApiWebPage {
mediaType: 'webpage';
id: number;
@ -516,6 +544,7 @@ export type MediaContent = {
sticker?: ApiSticker;
contact?: ApiContact;
pollId?: string;
todo?: ApiMediaTodo;
action?: ApiMessageAction;
webPage?: ApiWebPage;
audio?: ApiAudio;

View File

@ -253,6 +253,9 @@ export interface ApiAppConfig {
starsStargiftResaleAmountMax?: number;
starsStargiftResaleCommissionPermille?: number;
pollMaxAnswers?: number;
todoItemsMax?: number;
todoTitleLengthMax?: number;
todoItemLengthMax?: number;
}
export interface ApiConfig {

View File

@ -68,6 +68,7 @@
"PremiumPreviewVoiceToTextDescription" = "Ability to read the transcript of any incoming voice message.";
"PremiumPreviewProfileBadgeDescription" = "An exclusive badge next to your name showing that you subscribe to Telegram Premium.";
"PremiumPreviewDownloadSpeedDescription" = "No more limits on the speed with which media and documents are downloaded.";
"PremiumPreviewTodoDescription" = "Plan, assign, and complete tasks - seamlessly and efficiently.";
"PremiumPreviewUploadsDescription" = "4 GB per each document, unlimited storage for your chats and media overall.";
"PremiumPreviewAdvancedChatManagementDescription" = "Tools to set the default folder, auto-archive and hide new chats from non-contacts.";
"PremiumPreviewAnimatedProfilesDescription" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
@ -2024,3 +2025,41 @@
"MonoforumComposerPlaceholder" = "Choose a message to reply";
"ChannelSendMessage" = "Direct Messages";
"AutomaticTranslation" = "Automatic Translation";
"TitleNewToDoList" = "New Checklist";
"TitleEditToDoList" = "Edit Checklist";
"TitleAppendToDoList" = "Append Checklist";
"InputTitle" = "Title";
"TitleToDoList" = "Checklist";
"TitleTask" = "Task";
"TitleAddTask" = "Add a task";
"AllowOthersAddTasks" = "Allow Others to Add Tasks";
"AllowOthersMarkAsDone" = "Allow Others to Mark As Done";
"AriaToDoCancel" = "Cancel checklist creation";
"TitleGroupToDoList" = "Group Checklist";
"TitleUserToDoList" = "{peer}'s Checklist";
"TitleYourToDoList" = "Your Checklist";
"DescriptionCompletedToDoTasks" = "{number} of {count} completed";
"MessageActionTodoCompletionsAsDone" = "{peer} marked \"{task}\" as done";
"MessageActionTodoCompletionsAsDoneYou" = "You marked \"{task}\" as done";
"MessageActionTodoCompletionsAsDoneMultiple" = "{peer} marked {tasks} as done";
"MessageActionTodoCompletionsAsDoneMultipleYou" = "You marked {tasks} as done";
"MessageActionTodoCompletionsAsNotDone" = "{peer} marked \"{task}\" as not done";
"MessageActionTodoCompletionsAsNotDoneYou" = "You marked \"{task}\" as not done";
"MessageActionTodoCompletionsAsNotDoneMultiple" = "{peer} marked {tasks} as not done";
"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}";
"MessageActionAppendTodoMultiple" = "{peer} added {tasks} to {list}";
"MessageActionAppendTodoMultipleYou" = "You added {tasks} to {list}";
"PremiumMore" = "More";
"SubscribeToTelegramPremiumForToggleTask" = "Subscribe to **Telegram Premium** to toggle tasks";
"SubscribeToTelegramPremiumForCreateToDo" = "Subscribe to **Telegram Premium** to create Checklists";
"SubscribeToTelegramPremiumForAppendToDo" = "Subscribe to **Telegram Premium** to append Checklists";
"HintTodoListTasksCount" = "You can add {count} more tasks";
"ToDoListErrorChooseTitle" = "Please enter a title.";
"ToDoListErrorChooseTasks" = "Please enter at least one task.";
"PremiumPreviewTodo" = "Checklists";

View File

@ -68,6 +68,7 @@ export { default as ReactionPicker } from '../components/middle/message/reaction
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
export { default as PollModal } from '../components/middle/composer/PollModal';
export { default as ToDoListModal } from '../components/middle/composer/ToDoListModal';
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
export { default as ChatCommandTooltip } from '../components/middle/composer/ChatCommandTooltip';
export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu';

View File

@ -220,7 +220,7 @@
padding: 0;
border-radius: 50%;
font-size: smaller;
font-size: 0.875rem;
color: white;
background: var(--color-primary);

View File

@ -20,6 +20,7 @@ import type {
ApiFormattedText,
ApiMessage,
ApiMessageEntity,
ApiNewMediaTodo,
ApiNewPoll,
ApiPeer,
ApiQuickReply,
@ -178,6 +179,7 @@ import PollModal from '../middle/composer/PollModal.async';
import SendAsMenu from '../middle/composer/SendAsMenu.async';
import StickerTooltip from '../middle/composer/StickerTooltip.async';
import SymbolMenuButton from '../middle/composer/SymbolMenuButton';
import ToDoListModal from '../middle/composer/ToDoListModal.async';
import WebPagePreview from '../middle/composer/WebPagePreview';
import MessageEffect from '../middle/message/MessageEffect';
import ReactionSelector from '../middle/message/reactions/ReactionSelector';
@ -235,6 +237,7 @@ type StateProps =
isForwarding?: boolean;
forwardedMessagesCount?: number;
pollModal: TabState['pollModal'];
todoListModal: TabState['todoListModal'];
botKeyboardMessageId?: number;
botKeyboardPlaceholder?: string;
withScheduledButton?: boolean;
@ -357,6 +360,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isForwarding,
forwardedMessagesCount,
pollModal,
todoListModal,
botKeyboardMessageId,
botKeyboardPlaceholder,
inputPlaceholder,
@ -433,6 +437,8 @@ const Composer: FC<OwnProps & StateProps> = ({
showDialog,
openPollModal,
closePollModal,
openTodoListModal,
closeTodoListModal,
loadScheduledHistory,
openThread,
addRecentEmoji,
@ -540,7 +546,7 @@ const Composer: FC<OwnProps & StateProps> = ({
const [nextText, setNextText] = useState<ApiFormattedText | undefined>(undefined);
const {
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks,
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, canAttachToDoLists,
canSendVoices, canSendPlainText, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments,
} = useMemo(
() => getAllowedAttachmentOptions(chat,
@ -1216,6 +1222,22 @@ const Composer: FC<OwnProps & StateProps> = ({
handleActionWithPaymentConfirmation(handleSend, isSilent, scheduledAt);
});
const handleTodoListCreate = useLastCallback(() => {
if (!isCurrentUserPremium) {
showNotification({
message: lang('SubscribeToTelegramPremiumForCreateToDo'),
action: {
action: 'openPremiumModal',
payload: { initialSection: 'todo' },
},
actionText: lang('PremiumMore'),
});
return;
}
openTodoListModal({ chatId });
});
const handleClickBotMenu = useLastCallback(() => {
if (botMenuButton?.type !== 'webApp') {
return;
@ -1455,6 +1477,28 @@ const Composer: FC<OwnProps & StateProps> = ({
}
});
const handleToDoListSend = useLastCallback((todo: ApiNewMediaTodo) => {
if (!currentMessageList) {
return;
}
if (isInScheduledList) {
requestCalendar((scheduledAt) => {
handleActionWithPaymentConfirmation(
handleMessageSchedule,
{ todo },
scheduledAt,
currentMessageList,
);
});
} else {
handleActionWithPaymentConfirmation(
sendMessage,
{ messageList: currentMessageList, todo, isSilent: isSilentPosting },
);
}
});
const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => {
if (isInScheduledList) {
requestCalendar((scheduledAt) => {
@ -1878,6 +1922,11 @@ const Composer: FC<OwnProps & StateProps> = ({
onClear={closePollModal}
onSend={handlePollSend}
/>
<ToDoListModal
modal={todoListModal}
onClear={closeTodoListModal}
onSend={handleToDoListSend}
/>
<SendAsMenu
isOpen={isSendAsMenuOpen}
onClose={closeSendAsMenu}
@ -2147,12 +2196,14 @@ const Composer: FC<OwnProps & StateProps> = ({
isButtonVisible={!activeVoiceRecording}
canAttachMedia={canAttachMedia}
canAttachPolls={canAttachPolls}
canAttachToDoLists={canAttachToDoLists}
canSendPhotos={canSendPhotos}
canSendVideos={canSendVideos}
canSendDocuments={canSendDocuments}
canSendAudios={canSendAudios}
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
onTodoListCreate={handleTodoListCreate}
isScheduled={isInScheduledList}
attachBots={isInMessageList ? attachBots : undefined}
peerType={attachMenuPeerType}
@ -2458,6 +2509,7 @@ export default memo(withGlobal<OwnProps>(
isForwarding,
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
pollModal: tabState.pollModal,
todoListModal: tabState.todoListModal,
stickersForEmoji: global.stickers.forEmoji.stickers,
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
chatFullInfo,

View File

@ -12,6 +12,7 @@ import type {
ApiPremiumSubscriptionOption,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { LangPair } from '../../../types/language';
import { PREMIUM_BOTTOM_VIDEOS, PREMIUM_FEATURE_SECTIONS, PREMIUM_LIMITS_ORDER } from '../../../config';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
@ -55,6 +56,7 @@ export const PREMIUM_FEATURE_TITLES: Record<ApiPremiumSection, string> = {
last_seen: 'PremiumPreviewLastSeen',
message_privacy: 'PremiumPreviewMessagePrivacy',
effects: 'Premium.MessageEffects',
todo: 'PremiumPreviewTodo',
};
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
@ -76,6 +78,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
last_seen: 'PremiumPreviewLastSeenDescription',
message_privacy: 'PremiumPreviewMessagePrivacyDescription',
effects: 'Premium.MessageEffectsInfo',
todo: 'PremiumPreviewTodoDescription',
};
const LIMITS_TITLES: Record<ApiLimitTypeForPromo, string> = {
@ -290,6 +293,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
const i = promo.videoSections.indexOf(section);
if (i === -1) return undefined;
const shouldUseNewLang = promo.videoSections[i] === 'todo';
return (
<div className={styles.slide}>
<div className={styles.frame}>
@ -303,10 +307,23 @@ const PremiumFeatureModal: FC<OwnProps> = ({
/>
</div>
<h1 className={styles.title}>
{oldLang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]])}
{shouldUseNewLang
? lang(
PREMIUM_FEATURE_TITLES[promo.videoSections[i]] as keyof LangPair,
undefined,
{ withNodes: true, renderTextFilters: ['br'] },
)
: oldLang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]])}
</h1>
<div className={styles.description}>
{renderText(oldLang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]]), ['br'])}
{renderText(shouldUseNewLang
? lang(
PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]] as keyof LangPair,
undefined,
{ withNodes: true, renderTextFilters: ['br'] },
)
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]]), ['br'],
)}
</div>
</div>
);

View File

@ -9,6 +9,7 @@ import type {
ApiPremiumPromo, ApiPremiumSection, ApiPremiumSubscriptionOption, ApiSticker, ApiStickerSet, ApiUser,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { LangPair } from '../../../types/language';
import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
@ -83,6 +84,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
last_seen: PremiumLastSeen,
message_privacy: PremiumMessagePrivacy,
effects: PremiumEffects,
todo: PremiumBadge,
};
export type OwnProps = {
@ -148,8 +150,10 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
if (!isOpen) {
setHeaderHidden(true);
setCurrentSection(undefined);
} else if (initialSection) {
setCurrentSection(initialSection);
}
}, [isOpen]);
}, [isOpen, initialSection]);
const handleOpenSection = useLastCallback((section: ApiPremiumSection) => {
setCurrentSection(section);
@ -401,14 +405,19 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
</div>
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
{filteredSections.map((section, index) => {
const shouldUseNewLang = section === 'todo';
return (
<PremiumFeatureItem
key={section}
title={oldLang(PREMIUM_FEATURE_TITLES[section])}
title={shouldUseNewLang
? lang(PREMIUM_FEATURE_TITLES[section] as keyof LangPair)
: oldLang(PREMIUM_FEATURE_TITLES[section])}
text={section === 'double_limits'
? oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section],
[limitChannels, limitFolders, limitPins, limitLinks, LIMIT_ACCOUNTS])
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
: shouldUseNewLang
? lang(PREMIUM_FEATURE_DESCRIPTIONS[section] as keyof LangPair)
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
index={index}
count={filteredSections.length}

View File

@ -47,6 +47,7 @@ export type OwnProps = {
isButtonVisible: boolean;
canAttachMedia: boolean;
canAttachPolls: boolean;
canAttachToDoLists: boolean;
canSendPhotos: boolean;
canSendVideos: boolean;
canSendDocuments: boolean;
@ -58,6 +59,7 @@ export type OwnProps = {
theme: ThemeKey;
onFileSelect: (files: File[]) => void;
onPollCreate: NoneToVoidFunction;
onTodoListCreate: NoneToVoidFunction;
onMenuOpen: NoneToVoidFunction;
onMenuClose: NoneToVoidFunction;
canEditMedia?: boolean;
@ -72,6 +74,7 @@ const AttachMenu: FC<OwnProps> = ({
isButtonVisible,
canAttachMedia,
canAttachPolls,
canAttachToDoLists,
canSendPhotos,
canSendVideos,
canSendDocuments,
@ -85,6 +88,7 @@ const AttachMenu: FC<OwnProps> = ({
onMenuOpen,
onMenuClose,
onPollCreate,
onTodoListCreate,
canEditMedia,
editingMessage,
messageListType,
@ -263,6 +267,9 @@ const AttachMenu: FC<OwnProps> = ({
{canAttachPolls && !editingMessage && (
<MenuItem icon="poll" onClick={onPollCreate}>{oldLang('Poll')}</MenuItem>
)}
{canAttachToDoLists && !editingMessage && (
<MenuItem icon="select" onClick={onTodoListCreate}>{lang('TitleToDoList')}</MenuItem>
)}
{!editingMessage && !canEditMedia && !isScheduled && bots?.map((bot) => (
<AttachBotItem

View File

@ -16,6 +16,8 @@
.options-header {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
@ -69,13 +71,13 @@
}
.note {
font-size: smaller;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.poll-error {
margin: -1rem 0 1rem 0.25rem;
font-size: smaller;
font-size: 0.875rem;
color: var(--color-error);
}

View File

@ -0,0 +1,16 @@
import type { FC } from '../../../lib/teact/teact';
import type { OwnProps } from './ToDoListModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const ToDoListModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const ToDoListModal = useModuleLoader(Bundles.Extra, 'ToDoListModal', !modal);
return ToDoListModal ? <ToDoListModal {...props} /> : undefined;
};
export default ToDoListModalAsync;

View File

@ -0,0 +1,105 @@
@use '../../../styles/mixins';
.ToDoListModal {
.modal-dialog {
max-width: 26.25rem;
max-height: calc(100vh - 5rem);
}
.modal-content {
min-height: 4.875rem;
}
.modal-header-condensed {
margin-bottom: 1rem;
}
.readonly-title {
margin-bottom: 1rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
}
.items-header {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.items-count-hint {
margin-bottom: 0.5rem;
margin-left: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.items-list {
overflow: auto;
overflow-y: scroll;
max-height: 20rem;
margin: 1rem -0.75rem -0.5rem;
padding: 0 0.75rem;
border-top: 1px solid var(--color-chat-hover);
@include mixins.adapt-padding-to-scrollbar(0.75rem);
@media (max-width: 600px) {
overflow: hidden;
max-height: none;
}
}
.item-wrapper {
position: relative;
.form-control {
padding-right: 3rem;
}
.item-remove-button {
position: absolute;
top: 0.125rem;
right: 0.3125rem;
}
}
.options-footer {
margin-top: 1.5rem;
.dialog-checkbox-group {
margin: 0 -1.125rem;
}
.options-header {
margin-bottom: 0.5rem;
}
}
.input-group:last-child {
margin-bottom: 0.5rem;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 0.8125rem;
margin-left: -1.125rem;
.Radio, &:hover {
background-color: transparent;
}
}
.Checkbox,
.Radio {
.Checkbox-main,
.Radio-main {
width: 100%;
max-height: 3rem;
}
}
}

View File

@ -0,0 +1,383 @@
import type { ChangeEvent } from 'react';
import type { ElementRef } from '../../../lib/teact/teact';
import {
memo, useEffect, 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 {
TODO_ITEM_LENGTH_LIMIT,
TODO_ITEMS_LIMIT,
TODO_TITLE_LENGTH_LIMIT,
} from '../../../config';
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import { selectChatMessage } from '../../../global/selectors';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
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;
};
const MAX_LIST_HEIGHT = 320;
const MAX_OPTION_LENGTH = 100;
const ToDoListModal = ({
modal,
maxItemsCount = TODO_ITEMS_LIMIT,
maxTitleLength = TODO_TITLE_LENGTH_LIMIT,
maxItemLength = TODO_ITEM_LENGTH_LIMIT,
editingMessage,
onSend,
onClear,
}: OwnProps & StateProps) => {
const { editTodo, closeTodoListModal, appendTodoList } = getActions();
const titleInputRef = useRef<HTMLInputElement>();
const itemsListRef = useRef<HTMLDivElement>();
const [title, setTitle] = useState<string>('');
const [items, setItems] = useState<Item[]>([{ id: generateUniqueNumberId(), text: '' }]);
const [isOthersCanAppend, setIsOthersCanAppend] = useState(true);
const [isOthersCanComplete, setIsOthersCanComplete] = useState(true);
const [hasErrors, setHasErrors] = useState<boolean>(false);
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const isAddTaskMode = renderingModal?.isAddTaskMode;
const lang = useLang();
const editingTodo = editingMessage?.content.todo?.todo;
const focusInput = useLastCallback((ref: ElementRef<HTMLInputElement>) => {
if (isOpen && ref.current) {
ref.current.focus();
}
});
useEffect(() => {
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(() => focusInput(titleInputRef), [focusInput, isOpen]);
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<HTMLInputElement>) => {
setIsOthersCanAppend(e.target.checked);
});
const handleIsOthersCanCompleteChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
setIsOthersCanComplete(e.target.checked);
});
const handleKeyPress = useLastCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleCreate();
}
});
const handleTitleChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
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 (
<div className="modal-header-condensed">
<Button round color="translucent" size="smaller" ariaLabel={lang('AriaToDoCancel')} onClick={onClear}>
<Icon name="close" />
</Button>
<div className="modal-title">{lang(title)}</div>
<Button
color="primary"
size="smaller"
className="modal-action-button"
onClick={handleCreate}
>
{lang(isAddTaskMode ? 'Add' : editingMessage ? 'Save' : 'Create')}
</Button>
</div>
);
}
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>
));
}
return (
<Modal isOpen={isOpen} onClose={onClear} header={renderHeader()} className="ToDoListModal">
{!isAddTaskMode && (
<InputText
ref={titleInputRef}
label={lang('InputTitle')}
value={title}
error={getTitleError()}
onChange={handleTitleChange}
onKeyPress={handleKeyPress}
/>
)}
{isAddTaskMode && (
<div className="readonly-title">
{title}
</div>
)}
<div className="options-divider" />
<div className="options-list custom-scroll" ref={itemsListRef}>
<h3 className="items-header">
{lang(isAddTaskMode ? 'ToDoListNewTasks' : 'TitleToDoList')}
</h3>
{renderItems()}
</div>
<div className="items-count-hint">
{lang('HintTodoListTasksCount', {
count: maxItemsCount - items.length - (isAddTaskMode && editingTodo ? editingTodo.items.length : 0),
})}
</div>
<div className="options-divider" />
{!isAddTaskMode && (
<div className="options-footer">
<div className="dialog-checkbox-group">
<Checkbox
label={lang('AllowOthersAddTasks')}
checked={isOthersCanAppend}
onChange={handleIsOthersCanAppendChange}
/>
<Checkbox
label={lang('AllowOthersMarkAsDone')}
checked={isOthersCanComplete}
onChange={handleIsOthersCanCompleteChange}
/>
</div>
</div>
)}
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(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));

View File

@ -64,7 +64,12 @@
.messageLink {
overflow: hidden;
min-width: 0;
min-width: 1ch;
}
.noEllipsis {
overflow: visible;
min-width: auto;
}
.singleLine, .messageLink {

View File

@ -94,6 +94,8 @@ const SINGLE_LINE_ACTIONS = new Set<ApiMessageAction['type']>([
'pinMessage',
'chatEditPhoto',
'chatDeletePhoto',
'todoCompletions',
'todoAppendTasks',
'unsupported',
]);
const HIDDEN_TEXT_ACTIONS = new Set<ApiMessageAction['type']>(['giftCode', 'prizeStars', 'suggestProfilePhoto']);

View File

@ -12,6 +12,7 @@ import {
getMainUsername,
getMessageInvoice, getMessageText, isChatChannel,
} from '../../../global/helpers';
import { getMessageContent } from '../../../global/helpers';
import { getPeerTitle } from '../../../global/helpers/peers';
import { getMessageReplyInfo } from '../../../global/helpers/replies';
import {
@ -534,14 +535,14 @@ const ActionMessageText = ({
if (isRecurringInit) {
return lang(
'ActionPaymentInitRecurringFor',
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) },
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage, invoiceTitle, asPreview) },
{ withNodes: true },
);
}
return lang(
'ActionPaymentDoneFor',
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) },
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage, invoiceTitle, asPreview) },
{ withNodes: true },
);
}
@ -749,6 +750,146 @@ const ActionMessageText = ({
}, { withNodes: true, withMarkdown: true });
}
case 'todoCompletions': {
const { completedIds, incompletedIds } = action;
let completedItem;
let incompletedItem;
const { todo } = replyMessage ? getMessageContent(replyMessage) : {};
if (todo) {
const todoItems = todo.todo.items;
completedItem = todoItems.find((item) => completedIds.includes(item.id));
incompletedItem = todoItems.find((item) => incompletedIds.includes(item.id));
}
if (incompletedItem) {
const incompletedTaskTitle = incompletedItem.title;
const incompletedTaskLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: incompletedTaskTitle.text,
entities: incompletedTaskTitle.entities,
asPreview: true,
}),
asPreview,
);
return translateWithYou(lang, 'MessageActionTodoCompletionsAsNotDone', isOutgoing, {
peer: senderLink,
task: incompletedTaskLink,
});
}
if (completedItem) {
const completedTaskTitle = completedItem.title;
const completedTaskLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: completedTaskTitle.text,
entities: completedTaskTitle.entities,
asPreview: true,
}),
asPreview,
);
return translateWithYou(lang, 'MessageActionTodoCompletionsAsDone', isOutgoing, {
peer: senderLink,
task: completedTaskLink,
});
}
if (completedIds) {
const completedText = lang('MessageActionTodoTaskCount', {
count: completedIds.length,
}, { pluralValue: completedIds.length });
const completedLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: completedText,
asPreview: true,
}),
asPreview,
{ noEllipsis: true },
);
return translateWithYou(lang, 'MessageActionTodoCompletionsAsDone', isOutgoing, {
peer: senderLink,
task: completedLink,
});
}
const incompletedText = lang('MessageActionTodoTaskCount', {
count: incompletedIds.length,
}, { pluralValue: incompletedIds.length });
const incompletedLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: incompletedText,
asPreview: true,
}),
asPreview,
{ noEllipsis: true },
);
return translateWithYou(lang, 'MessageActionTodoCompletionsAsNotDone', isOutgoing, {
peer: senderLink,
task: incompletedLink,
});
}
case 'todoAppendTasks': {
const { items } = action;
const { todo } = replyMessage ? getMessageContent(replyMessage) : {};
const listTitle = todo?.todo.title.text || '';
const listLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: listTitle,
asPreview: true,
}),
asPreview,
);
if (items.length === 1) {
const taskTitle = items[0].title;
const taskLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: taskTitle.text,
entities: taskTitle.entities,
asPreview: true,
}),
asPreview,
);
return translateWithYou(lang, 'MessageActionAppendTodo', isOutgoing, {
peer: senderLink,
task: taskLink,
list: listLink,
});
}
const tasksText = lang('MessageActionTodoTaskCount', {
count: items.length,
}, { pluralValue: items.length });
const tasksLink = renderMessageLink(
replyMessage,
renderTextWithEntities({
text: tasksText,
asPreview: true,
}),
asPreview,
{ noEllipsis: true },
);
return translateWithYou(lang, 'MessageActionAppendTodoMultiple', isOutgoing, {
peer: senderLink,
tasks: tasksLink,
list: listLink,
});
}
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
return lang(getCallMessageKey(action, isOutgoing));
default:

View File

@ -25,6 +25,9 @@ import type {
} from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import {
TODO_ITEMS_LIMIT,
} from '../../../config';
import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import {
areReactionsEmpty,
@ -79,6 +82,7 @@ import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedTe
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useSchedule from '../../../hooks/useSchedule';
@ -125,6 +129,7 @@ type StateProps = {
canDelete?: boolean;
canReport?: boolean;
canEdit?: boolean;
canAppendTodoList?: boolean;
canForward?: boolean;
canFaveSticker?: boolean;
canUnfaveSticker?: boolean;
@ -192,6 +197,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canReport,
canShowReactionList,
canEdit,
canAppendTodoList,
enabledReactions,
reactionsLimit,
isPrivate,
@ -265,9 +271,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
addLocalPaidReaction,
openPaidReactionModal,
reportMessages,
openTodoListModal,
showNotification,
} = getActions();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const { ref: containerRef } = useShowTransition({
isOpen,
onCloseAnimationEnd,
@ -435,7 +444,34 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
});
const handleEdit = useLastCallback(() => {
setEditingId({ messageId: message.id });
if (message.content.todo) {
openTodoListModal({
chatId: message.chatId,
messageId: message.id,
});
} else {
setEditingId({ messageId: message.id });
}
closeMenu();
});
const handleAppendTodoList = useLastCallback(() => {
if (!isCurrentUserPremium) {
showNotification({
message: lang('SubscribeToTelegramPremiumForAppendToDo'),
action: {
action: 'openPremiumModal',
payload: { initialSection: 'todo' },
},
actionText: oldLang('PremiumMore'),
});
} else {
openTodoListModal({
chatId: message.chatId,
messageId: message.id,
isAddTaskMode: true,
});
}
closeMenu();
});
@ -667,6 +703,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
repliesThreadInfo={repliesThreadInfo}
canUnpin={canUnpin}
canEdit={canEdit}
canAppendTodoList={canAppendTodoList}
canForward={canForward}
canFaveSticker={canFaveSticker}
canUnfaveSticker={canUnfaveSticker}
@ -695,6 +732,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
onOpenThread={handleOpenThread}
onReply={handleReply}
onEdit={handleEdit}
onAppendTodoList={handleAppendTodoList}
onPin={handlePin}
onUnpin={handleUnpin}
onForward={handleForward}
@ -734,8 +772,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
<ConfirmDialog
isOpen={isClosePollDialogOpen}
onClose={closeClosePollDialog}
text={lang('lng_polls_stop_warning')}
confirmLabel={lang('lng_polls_stop_sure')}
text={oldLang('lng_polls_stop_warning')}
confirmLabel={oldLang('lng_polls_stop_sure')}
confirmHandler={handlePollClose}
/>
{canReschedule && calendar}
@ -855,6 +893,9 @@ export default memo(withGlobal<OwnProps>(
const canGift = selectCanGift(global, message.chatId);
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
const todoItemsMax = global.appConfig?.todoItemsMax || TODO_ITEMS_LIMIT;
const canAppendTodoList = message.content.todo?.todo.othersCanAppend
&& message.content.todo?.todo.items?.length < todoItemsMax;
return {
threadId,
@ -871,6 +912,7 @@ export default memo(withGlobal<OwnProps>(
canUnpin: !isScheduled && canUnpin,
canDelete,
canEdit: !isPinned && canEdit,
canAppendTodoList,
canForward: !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker,
canUnfaveSticker: !isScheduled && canUnfaveSticker,

View File

@ -189,6 +189,7 @@ import RoundVideo from './RoundVideo';
import Sticker from './Sticker';
import Story from './Story';
import StoryMention from './StoryMention';
import TodoList from './TodoList';
import Video from './Video';
import WebPage from './WebPage';
@ -520,7 +521,7 @@ const Message: FC<OwnProps & StateProps> = ({
voice, document, sticker, contact,
webPage, invoice, location,
action, game, storyData, giveaway,
giveawayResults,
giveawayResults, todo,
} = getMessageContent(message);
const messageReplyInfo = getMessageReplyInfo(message);
@ -1241,6 +1242,9 @@ const Message: FC<OwnProps & StateProps> = ({
{poll && (
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
)}
{todo && (
<TodoList message={message} todoList={todo} />
)}
{(giveaway || giveawayResults) && (
<Giveaway message={message} />
)}

View File

@ -32,6 +32,7 @@ import { getMessageCopyOptions } from './helpers/copyOptions';
import useAppLayout from '../../../hooks/useAppLayout';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@ -73,6 +74,7 @@ type OwnProps = {
canShowReactionList?: boolean;
canBuyPremium?: boolean;
canEdit?: boolean;
canAppendTodoList?: boolean;
canForward?: boolean;
canFaveSticker?: boolean;
canUnfaveSticker?: boolean;
@ -101,6 +103,7 @@ type OwnProps = {
onReply?: NoneToVoidFunction;
onOpenThread?: VoidFunction;
onEdit?: NoneToVoidFunction;
onAppendTodoList?: NoneToVoidFunction;
onPin?: NoneToVoidFunction;
onUnpin?: NoneToVoidFunction;
onForward?: NoneToVoidFunction;
@ -159,6 +162,7 @@ const MessageContextMenu: FC<OwnProps> = ({
canReply,
canQuote,
canEdit,
canAppendTodoList,
noReplies,
canPin,
canUnpin,
@ -192,6 +196,7 @@ const MessageContextMenu: FC<OwnProps> = ({
onReply,
onOpenThread,
onEdit,
onAppendTodoList,
onPin,
onUnpin,
onForward,
@ -228,7 +233,8 @@ const MessageContextMenu: FC<OwnProps> = ({
} = getActions();
const menuRef = useRef<HTMLDivElement>();
const scrollableRef = useRef<HTMLDivElement>();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const noReactions = !isPrivate && !enabledReactions;
const areReactionsPossible = message.areReactionsPossible;
const withReactions = (canShowReactionList && !noReactions) || areReactionsPossible;
@ -248,7 +254,7 @@ const MessageContextMenu: FC<OwnProps> = ({
const handleAfterCopy = useLastCallback(() => {
showNotification({
message: lang('Share.Link.Copied'),
message: oldLang('Share.Link.Copied'),
});
onClose();
});
@ -395,40 +401,49 @@ const MessageContextMenu: FC<OwnProps> = ({
'MessageContextMenu_items scrollable-content custom-scroll',
areItemsHidden && 'MessageContextMenu_items-hidden',
)}
dir={lang.isRtl ? 'rtl' : undefined}
dir={oldLang.isRtl ? 'rtl' : undefined}
>
{shouldShowGiftButton
&& (
<MenuItem icon="gift" onClick={handleGiftClick}>
{message?.isOutgoing ? lang('SendAnotherGift')
: lang('Conversation.ContextMenuSendGiftTo', userFullName)}
{message?.isOutgoing ? oldLang('SendAnotherGift')
: oldLang('Conversation.ContextMenuSendGiftTo', userFullName)}
</MenuItem>
)}
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{oldLang('MessageScheduleSend')}</MenuItem>}
{canReschedule && (
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
<MenuItem icon="schedule" onClick={onReschedule}>{oldLang('MessageScheduleEditTime')}</MenuItem>
)}
{canReply && (
<MenuItem icon="reply" onClick={onReply}>
{lang(canQuote ? 'lng_context_quote_and_reply' : 'Reply')}
{oldLang(canQuote ? 'lng_context_quote_and_reply' : 'Reply')}
</MenuItem>
)}
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
<MenuItem icon="replies" onClick={onOpenThread}>
{lang('Conversation.ContextViewReplies', repliesThreadInfo.messagesCount, 'i')}
{oldLang('Conversation.ContextViewReplies', repliesThreadInfo.messagesCount, 'i')}
</MenuItem>
)}
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{oldLang('Edit')}</MenuItem>}
{canAppendTodoList && (
<MenuItem icon="add" onClick={onAppendTodoList}>
{lang('MenuButtonAppendTodoList')}
</MenuItem>
)}
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{lang('Edit')}</MenuItem>}
{canFaveSticker && (
<MenuItem icon="favorite" onClick={onFaveSticker}>{lang('AddToFavorites')}</MenuItem>
<MenuItem icon="favorite" onClick={onFaveSticker}>{oldLang('AddToFavorites')}</MenuItem>
)}
{canUnfaveSticker && (
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{oldLang('Stickers.RemoveFromFavorites')}</MenuItem>
)}
{canTranslate && <MenuItem icon="language" onClick={onTranslate}>{oldLang('TranslateMessage')}</MenuItem>}
{canShowOriginal && (
<MenuItem icon="language" onClick={onShowOriginal}>
{oldLang('ShowOriginalButton')}
</MenuItem>
)}
{canTranslate && <MenuItem icon="language" onClick={onTranslate}>{lang('TranslateMessage')}</MenuItem>}
{canShowOriginal && <MenuItem icon="language" onClick={onShowOriginal}>{lang('ShowOriginalButton')}</MenuItem>}
{canSelectLanguage && (
<MenuItem icon="web" onClick={onSelectLanguage}>{lang('lng_settings_change_lang')}</MenuItem>
<MenuItem icon="web" onClick={onSelectLanguage}>{oldLang('lng_settings_change_lang')}</MenuItem>
)}
{copyOptions.map((option) => (
<MenuItem
@ -437,23 +452,23 @@ const MessageContextMenu: FC<OwnProps> = ({
onClick={option.handler}
withPreventDefaultOnMouseDown
>
{lang(option.label)}
{oldLang(option.label)}
</MenuItem>
))}
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
{canSaveGif && <MenuItem icon="gifs" onClick={onSaveGif}>{lang('lng_context_save_gif')}</MenuItem>}
{canRevote && <MenuItem icon="revote" onClick={onCancelVote}>{lang('lng_polls_retract')}</MenuItem>}
{canClosePoll && <MenuItem icon="stop" onClick={onClosePoll}>{lang('lng_polls_stop')}</MenuItem>}
{canPin && <MenuItem icon="pin" onClick={onPin}>{oldLang('DialogPin')}</MenuItem>}
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{oldLang('DialogUnpin')}</MenuItem>}
{canSaveGif && <MenuItem icon="gifs" onClick={onSaveGif}>{oldLang('lng_context_save_gif')}</MenuItem>}
{canRevote && <MenuItem icon="revote" onClick={onCancelVote}>{oldLang('lng_polls_retract')}</MenuItem>}
{canClosePoll && <MenuItem icon="stop" onClick={onClosePoll}>{oldLang('lng_polls_stop')}</MenuItem>}
{canDownload && (
<MenuItem icon="download" onClick={onDownload}>
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
{isDownloading ? oldLang('lng_context_cancel_download') : oldLang('lng_media_download')}
</MenuItem>
)}
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
{canForward && <MenuItem icon="forward" onClick={onForward}>{oldLang('Forward')}</MenuItem>}
{canSelect && <MenuItem icon="select" onClick={onSelect}>{oldLang('Common.Select')}</MenuItem>}
{canReport && <MenuItem icon="flag" onClick={onReport}>{oldLang('lng_context_report_msg')}</MenuItem>}
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{oldLang('Delete')}</MenuItem>}
{hasCustomEmoji && (
<>
<MenuSeparator size="thick" />
@ -465,12 +480,14 @@ const MessageContextMenu: FC<OwnProps> = ({
)}
{customEmojiSets && customEmojiSets.length === 1 && (
<MenuItem withWrap onClick={handleOpenCustomEmojiSets} className="menu-custom-emoji-sets">
{renderText(lang('MessageContainsEmojiPack', customEmojiSets[0].title), ['simple_markdown', 'emoji'])}
{renderText(
oldLang('MessageContainsEmojiPack', customEmojiSets[0].title), ['simple_markdown', 'emoji'],
)}
</MenuItem>
)}
{customEmojiSets && customEmojiSets.length > 1 && (
<MenuItem withWrap onClick={handleOpenCustomEmojiSets} className="menu-custom-emoji-sets">
{renderText(lang('MessageContainsEmojiPacks', customEmojiSets.length), ['simple_markdown'])}
{renderText(oldLang('MessageContainsEmojiPacks', customEmojiSets.length), ['simple_markdown'])}
</MenuItem>
)}
</>
@ -484,14 +501,14 @@ const MessageContextMenu: FC<OwnProps> = ({
disabled={!canShowReactionsCount && !seenByDatesCount}
>
<span className="MessageContextMenu--seen-by-label-wrapper">
<span className="MessageContextMenu--seen-by-label" dir={lang.isRtl ? 'rtl' : undefined}>
<span className="MessageContextMenu--seen-by-label" dir={oldLang.isRtl ? 'rtl' : undefined}>
{canShowReactionsCount && message.reactors?.count ? (
canShowSeenBy && seenByDatesCount
? lang(
? oldLang(
'Chat.OutgoingContextMixedReactionCount',
[message.reactors.count, seenByDatesCount],
)
: lang('Chat.ContextReactionCount', message.reactors.count, 'i')
: oldLang('Chat.ContextReactionCount', message.reactors.count, 'i')
) : (
seenByDatesCount === 1 && seenByRecentPeers
? renderText(
@ -500,8 +517,8 @@ const MessageContextMenu: FC<OwnProps> = ({
: (seenByRecentPeers[0] as ApiChat).title,
) : (
seenByDatesCount
? lang('Conversation.ContextMenuSeen', seenByDatesCount, 'i')
: lang('Conversation.ContextMenuNoViews')
? oldLang('Conversation.ContextMenuSeen', seenByDatesCount, 'i')
: oldLang('Conversation.ContextMenuNoViews')
)
)}
</span>

View File

@ -0,0 +1,141 @@
.todo-list {
min-width: 15rem;
text-align: initial;
.todo-list-readonly-item {
display: flex;
align-items: center;
padding-bottom: 0.5rem;
}
.todo-readonly-item-checkbox {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
margin-inline-end: 0.5rem;
font-weight: var(--font-weight-semibold);
color: var(--accent-color);
}
.readonly-item-label {
overflow-wrap: anywhere;
}
.todo-item-bullet-point {
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
background-color: var(--accent-color);
}
.completed-label {
text-decoration: line-through;
}
.Checkbox {
min-height: 2.5rem;
padding-bottom: 1.25rem;
padding-left: 2.5rem;
&.withSubLabel {
padding-bottom: 0.25rem;
}
&.disabled {
opacity: 1 !important;
.Checkbox-main {
&::before,
&::after {
pointer-events: auto;
}
}
}
&:hover {
background: none;
}
.Checkbox-main {
padding: 0 !important;
&::before {
--color-borders-input: var(--secondary-color);
background-color: var(--background-color);
}
&::after {
background-color: var(--accent-color);
outline: 1px solid var(--background-color);
.theme-dark & {
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiM3NjZhYzgiLz48L3N2Zz4=);
}
}
.user-avatar,
&::before,
&::after {
top: 0.6875rem;
left: 0.125rem;
}
.user-avatar {
left: 0.875rem;
}
.label {
line-height: 1.3125rem;
}
}
input:checked ~ .Checkbox-main {
&::before {
border-color: var(--accent-color);
}
}
.Spinner {
top: 0.6875rem;
left: 0.125rem;
}
&.loading {
.Spinner {
top: 0;
}
}
}
.todo-list-title {
margin: 0.125rem 0;
font-weight: var(--font-weight-medium);
line-height: 1.25rem;
overflow-wrap: anywhere;
}
.list-type,
.completed-tasks-count {
font-size: 0.875rem;
color: var(--secondary-color);
}
.list-type {
margin-bottom: 0.5rem;
}
.completed-tasks-count {
margin: 0 0 1.125rem;
text-align: center;
}
@media (max-width: 600px) {
min-width: 50vw;
}
}

View File

@ -0,0 +1,197 @@
import {
memo, useEffect, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiMediaTodo,
ApiMessage,
ApiPeer,
} from '../../../api/types';
import { getPeerFullTitle, getPeerTitle } from '../../../global/helpers/peers';
import { selectIsCurrentUserPremium, selectSender, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedCounter from '../../common/AnimatedCounter';
import Icon from '../../common/icons/Icon';
import CheckboxGroup from '../../ui/CheckboxGroup';
import './TodoList.scss';
type OwnProps = {
message: ApiMessage;
todoList: ApiMediaTodo;
};
type StateProps = {
sender?: ApiPeer;
isCurrentUserPremium: boolean;
isSynced?: boolean;
};
const TodoList = ({
message,
todoList,
sender,
isCurrentUserPremium,
isSynced,
}: OwnProps & StateProps) => {
const { toggleTodoCompleted, showNotification } = getActions();
const { todo, completions } = todoList;
const { title, items, othersCanComplete } = todo;
const [completedTasks, setCompletedTasks] = useState<string[]>([]);
const completedTasksSet = useMemo(() => new Set(completedTasks), [completedTasks]);
const canToggle = !message.isScheduled && isCurrentUserPremium && isSynced;
useEffect(() => {
const completedIds = completions?.map((c) => c.itemId.toString()) || [];
setCompletedTasks(completedIds);
}, [completions]);
const lang = useLang();
const handleTaskLabelClick = useLastCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isCurrentUserPremium) {
showNotification({
message: lang('SubscribeToTelegramPremiumForToggleTask'),
action: {
action: 'openPremiumModal',
payload: { initialSection: 'todo' },
},
actionText: lang('PremiumMore'),
});
return;
}
});
const handleTaskToggle = useLastCallback((newCompletedTasks: string[]) => {
const newCompletedId = newCompletedTasks.find((id) => !completedTasksSet.has(id));
const newIncompletedId = Array.from(completedTasksSet).find((id) => !newCompletedTasks.includes(id));
toggleTodoCompleted({
chatId: message.chatId,
messageId: message.id,
completedIds: newCompletedId ? [Number(newCompletedId)] : [],
incompletedIds: newIncompletedId ? [Number(newIncompletedId)] : [],
});
});
const isReadOnly = Boolean(message.forwardInfo) || (!othersCanComplete && !message.isOutgoing);
const isOutgoing = message.isOutgoing;
const tasks = useMemo(() => items.map((task) => {
const user = !othersCanComplete ? undefined : selectUser(getGlobal(),
completions?.find((c) => c.itemId === task.id)?.completedBy || '');
const subLabel = user ? getPeerFullTitle(lang, user) : undefined;
return {
label: renderTextWithEntities(task.title),
value: task.id.toString(),
user,
subLabel,
};
}), [items, othersCanComplete, completions, lang]);
const renderCheckBoxGroup = () => {
return (
<CheckboxGroup
options={tasks}
selected={completedTasks}
onChange={handleTaskToggle}
onClickLabel={!isCurrentUserPremium ? handleTaskLabelClick : undefined}
disabled={!canToggle}
isRound
/>
);
};
const renderReadOnlyTodoList = () => {
return (
<div className="todo-list-items">
{tasks.map((task) => (
<div
key={task.value}
className="todo-list-readonly-item"
>
<div className="todo-readonly-item-checkbox">
{completedTasksSet.has(task.value)
? <Icon name="check" />
: <div className="todo-item-bullet-point" />}
</div>
<div
className={buildClassName(
'readonly-item-label',
completedTasksSet.has(task.value) && 'completed-label',
)}
>
{task.label}
</div>
</div>
))}
</div>
);
};
const renderTodoListType = () => {
if (message.forwardInfo) {
return lang('TitleToDoList');
}
if (othersCanComplete) {
return lang('TitleGroupToDoList');
}
if (isOutgoing) {
return lang('TitleYourToDoList');
}
if (sender) {
return lang('TitleUserToDoList', { peer: getPeerTitle(lang, sender) }, { withNodes: true });
}
return lang('TitleToDoList');
};
return (
<div className="todo-list" dir={lang.isRtl ? 'auto' : 'ltr'}>
<div className="todo-list-header">
<div className="todo-list-title">
{renderTextWithEntities(title)}
</div>
<div className="list-type">
{renderTodoListType()}
</div>
</div>
<div className="todo-list-items">
{isReadOnly ? renderReadOnlyTodoList() : renderCheckBoxGroup()}
</div>
<div className="completed-tasks-count">
<AnimatedCounter text={
lang('DescriptionCompletedToDoTasks', {
number: completedTasks.length,
count: tasks.length,
})
}
/>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>((global, { message }): StateProps => {
const sender = selectSender(global, message);
return {
sender,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
isSynced: global.isSynced,
};
},
)(TodoList));

View File

@ -103,12 +103,21 @@ export function renderPeerLink(peerId: string | undefined, text: string, asPrevi
);
}
export function renderMessageLink(targetMessage: ApiMessage, text: TeactNode, asPreview?: boolean) {
if (asPreview) return text;
export function renderMessageLink(
targetMessage: ApiMessage | undefined,
text: TeactNode,
asPreview: boolean | undefined,
params?: {
noEllipsis?: boolean;
},
) {
const { noEllipsis } = params || {};
if (asPreview || !targetMessage) return text;
return (
<Link
className={styles.messageLink}
className={buildClassName(styles.messageLink, noEllipsis && styles.noEllipsis)}
onClick={(e) => {
e.stopPropagation();
getActions().focusMessage({ chatId: targetMessage.chatId, messageId: targetMessage.id });

View File

@ -24,7 +24,7 @@ type StateProps = {
};
const ChatRefundModal = ({ modal, user }: OwnProps & StateProps) => {
const { closeChatRefundModal, addNoPaidMessagesException } = getActions();
const { closeChatRefundModal, toggleNoPaidMessagesException } = getActions();
const [shouldRefundStars, setShouldRefundStars] = useState(false);
@ -40,7 +40,7 @@ const ChatRefundModal = ({ modal, user }: OwnProps & StateProps) => {
const handleConfirmRemoveFee = useLastCallback(() => {
closeChatRefundModal();
if (!userId) return;
addNoPaidMessagesException({ userId, shouldRefundCharged: shouldRefundStars });
toggleNoPaidMessagesException ({ userId, shouldRefundCharged: shouldRefundStars });
});
return (

View File

@ -27,6 +27,16 @@
}
}
.user-avatar {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.15s ease;
}
&.round {
.Checkbox-main {
&::before, &::after {
@ -101,11 +111,6 @@
pointer-events: none;
content: "";
position: absolute;
top: 50%;
left: 1.125rem; // 1 + ((1.5 - 1.25) / 2)
transform: translateY(-50%);
display: block;
width: 1.25rem;
@ -127,6 +132,15 @@
transition: opacity 0.1s ease;
}
.user-avatar,
&::before,
&::after {
position: absolute;
top: 50%;
left: 1.125rem; // 1 + ((1.5 - 1.25) / 2)
transform: translateY(-50%);
}
.label {
unicode-bidi: plaintext;
line-height: 1.25rem;
@ -202,6 +216,11 @@
&::after {
opacity: 1;
}
.user-avatar {
&.user-avatar-visible {
opacity: 1;
}
}
}
&[dir="rtl"] {
@ -218,6 +237,7 @@
}
.Checkbox-main {
.user-avatar,
&::before,
&::after {
right: 1rem;

View File

@ -7,15 +7,19 @@ import {
useState,
} from '../../lib/teact/teact';
import type { ApiUser } from '../../api/types';
import type { IconName } from '../../types/icons';
import type { IRadioOption } from './CheckboxGroup';
import buildClassName from '../../util/buildClassName';
import { REM } from '../common/helpers/mediaDimensions';
import renderText from '../common/helpers/renderText';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Avatar from '../common/Avatar';
import Icon from '../common/icons/Icon';
import Button from './Button';
import Spinner from './Spinner';
@ -26,6 +30,7 @@ type OwnProps = {
id?: string;
name?: string;
value?: string;
user?: ApiUser;
label?: TeactNode;
labelText?: TeactNode;
subLabel?: string;
@ -50,12 +55,14 @@ type OwnProps = {
leftElement?: TeactNode;
values?: string[];
};
const AVATAR_SIZE = 1.25 * REM;
const Checkbox: FC<OwnProps> = ({
id,
name,
value,
label,
user,
labelText,
subLabel,
checked,
@ -81,6 +88,7 @@ const Checkbox: FC<OwnProps> = ({
const lang = useOldLang();
const labelRef = useRef<HTMLLabelElement>();
const [showNested, setShowNested] = useState(false);
const renderingUser = useCurrentOrPrev(user, true);
const handleChange = useLastCallback((event: ChangeEvent<HTMLInputElement>) => {
if (disabled) {
@ -151,6 +159,14 @@ const Checkbox: FC<OwnProps> = ({
Boolean(leftElement) && 'Nested-avatar-list',
)}
>
<div className={buildClassName('user-avatar', renderingUser && 'user-avatar-visible')}>
{renderingUser && (
<Avatar
peer={renderingUser}
size={AVATAR_SIZE}
/>
)}
</div>
<span className="label" dir="auto">
{leftElement}
{typeof label === 'string' ? renderText(label) : label}

View File

@ -28,6 +28,7 @@ type OwnProps = {
loadingOptions?: string[];
isRound?: boolean;
onChange: (value: string[]) => void;
onClickLabel?: (e: React.MouseEvent, value?: string) => void;
className?: string;
};
@ -40,6 +41,7 @@ const CheckboxGroup: FC<OwnProps> = ({
loadingOptions,
isRound,
onChange,
onClickLabel,
className,
}) => {
const handleChange = useLastCallback((event: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => {
@ -87,10 +89,12 @@ const CheckboxGroup: FC<OwnProps> = ({
label={option.label}
subLabel={option.subLabel}
value={option.value}
user={option.user}
checked={selected?.indexOf(option.value) !== -1}
disabled={option.disabled || disabled}
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
onChange={handleChange}
onClickLabel={onClickLabel}
nestedCheckbox={nestedCheckbox}
nestedCheckboxCount={getCheckedNestedCount(option.nestedOptions ?? [])}
nestedOptionList={option}

View File

@ -106,6 +106,9 @@ export const STORY_LIST_LIMIT = 100;
export const API_GENERAL_ID_LIMIT = 100;
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
export const RESALE_GIFTS_LIMIT = 50;
export const TODO_ITEMS_LIMIT = 30;
export const TODO_TITLE_LENGTH_LIMIT = 32;
export const TODO_ITEM_LENGTH_LIMIT = 64;
export const STORY_VIEWS_MIN_SEARCH = 15;
export const STORY_MIN_REACTIONS_SORT = 10;
@ -429,6 +432,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
'last_seen',
'message_privacy',
'effects',
'todo',
] as const;
export const PREMIUM_BOTTOM_VIDEOS: ApiPremiumSection[] = [
@ -444,6 +448,7 @@ export const PREMIUM_BOTTOM_VIDEOS: ApiPremiumSection[] = [
'last_seen',
'message_privacy',
'effects',
'todo',
];
export const PREMIUM_LIMITS_ORDER: ApiLimitTypeForPromo[] = [

View File

@ -53,6 +53,7 @@ import { getTranslationFn, type RegularLangFnParameters } from '../../../util/lo
import { formatStarsAsText } from '../../../util/localization/format';
import { oldTranslate } from '../../../util/oldLangProvider';
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime';
import { callApi, cancelApiProgress } from '../../../api/gramjs';
import {
getIsSavedDialog,
@ -146,6 +147,7 @@ import {
selectUserStatus,
selectViewportIds,
} from '../../selectors';
import { updateWithLocalMedia } from '../apiUpdaters/messages';
import { deleteMessages } from '../apiUpdaters/messages';
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
@ -557,6 +559,24 @@ addActionHandler('editMessage', (global, actions, payload): ActionReturnType =>
})();
});
addActionHandler('editTodo', (global, actions, payload): ActionReturnType => {
const {
chatId, todo, messageId,
} = payload;
const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId);
if (!chat || !message) {
return;
}
callApi('editTodo', {
chat,
message,
todo,
});
});
addActionHandler('cancelUploadMedia', (global, actions, payload): ActionReturnType => {
const { chatId, messageId } = payload;
@ -1138,6 +1158,71 @@ addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType =>
}
});
addActionHandler('toggleTodoCompleted', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, completedIds, incompletedIds } = payload;
const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId);
const currentUserId = global.currentUserId;
const currentTodo = message?.content.todo;
if (!currentTodo || !currentUserId || !chat) {
return;
}
const currentCompletions = currentTodo.completions || [];
const currentCompletionIds = currentCompletions.map((c) => c.itemId);
const newCompletions = [...currentCompletions];
const now = getServerTime();
completedIds.forEach((itemId) => {
if (!currentCompletionIds.includes(itemId)) {
newCompletions.push({
itemId,
completedBy: currentUserId,
completedAt: now,
});
}
});
const finalCompletions = newCompletions.filter((c) => !incompletedIds.includes(c.itemId));
const newContent = {
...message.content,
todo: {
...currentTodo,
completions: finalCompletions,
},
};
const messageUpdate: Partial<ApiMessage> = {
...message,
content: newContent,
};
global = updateWithLocalMedia(global, chatId, message.id, messageUpdate);
setGlobal(global);
callApi('toggleTodoCompleted', { chat, messageId: message.id, completedIds, incompletedIds });
});
addActionHandler('appendTodoList', (global, actions, payload): ActionReturnType => {
const {
chatId, items, messageId,
} = payload;
const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId);
if (!chat || !message) {
return;
}
callApi('appendTodoList', {
chat,
message,
items,
});
});
addActionHandler('cancelPollVote', (global, actions, payload): ActionReturnType => {
const { chatId, messageId } = payload;
const chat = selectChat(global, chatId);

View File

@ -14,6 +14,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider';
import { getStripeError } from '../../../util/payments/stripe';
import { buildQueryString } from '../../../util/requestQuery';
import { getServerTime } from '../../../util/serverTime';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import { isChatChannel, isChatSuperGroup } from '../../helpers';
@ -827,7 +828,7 @@ addActionHandler('applyBoost', async (global, actions, payload): Promise<void> =
const oldMyBoosts = tabState.boostModal?.myBoosts;
if (oldMyBoosts) {
const unixNow = Math.floor(Date.now() / 1000);
const unixNow = getServerTime();
const newMyBoosts = oldMyBoosts.map((boost) => {
if (slots.includes(boost.slot)) {
return {

View File

@ -191,14 +191,14 @@ addActionHandler('loadCommonChats', async (global, actions, payload): Promise<vo
setGlobal(global);
});
addActionHandler('addNoPaidMessagesException', async (global, actions, payload): Promise<void> => {
addActionHandler('toggleNoPaidMessagesException', async (global, actions, payload): Promise<void> => {
const { userId, shouldRefundCharged } = payload;
const user = selectUser(global, userId);
if (!user) {
return;
}
const result = await callApi('addNoPaidMessagesException',
const result = await callApi('toggleNoPaidMessagesException',
{ user, shouldRefundCharged });
if (!result) {
return;

View File

@ -940,7 +940,7 @@ function updateReactions<T extends GlobalState>(
return global;
}
function updateWithLocalMedia(
export function updateWithLocalMedia(
global: RequiredGlobalState,
chatId: string,
id: number,

View File

@ -32,6 +32,7 @@ import {
isChatChannel,
} from '../../helpers';
import { getMessageSummaryText } from '../../helpers/messageSummary';
import { addTabStateResetterAction } from '../../helpers/meta';
import { getPeerTitle } from '../../helpers/peers';
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
@ -762,6 +763,22 @@ addActionHandler('closePollModal', (global, actions, payload): ActionReturnType
}, tabId);
});
addActionHandler('openTodoListModal', (global, actions, payload): ActionReturnType => {
const {
chatId, messageId, isAddTaskMode, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
todoListModal: {
chatId,
messageId,
isAddTaskMode,
},
}, tabId);
});
addTabStateResetterAction('closeTodoListModal', 'todoListModal');
addActionHandler('checkVersionNotification', (global, actions): ActionReturnType => {
if (RELEASE_DATETIME && Date.now() > Number(RELEASE_DATETIME) + VERSION_NOTIFICATION_DURATION) {
return;

View File

@ -189,6 +189,7 @@ export interface IAllowedAttachmentOptions {
canSendVoices: boolean;
canSendPlainText: boolean;
canSendDocuments: boolean;
canAttachToDoLists: boolean;
}
export function getAllowedAttachmentOptions(
@ -214,6 +215,7 @@ export function getAllowedAttachmentOptions(
canSendVoices: false,
canSendPlainText: false,
canSendDocuments: false,
canAttachToDoLists: false,
};
}
@ -224,6 +226,7 @@ export function getAllowedAttachmentOptions(
canAttachPolls: !isStoryReply && !chat.isMonoforum
&& (isAdmin || !isUserRightBanned(chat, 'sendPolls', chatFullInfo))
&& (!isUserId(chat.id) || isChatWithBot || isSavedMessages),
canAttachToDoLists: !isStoryReply && !chat.isMonoforum && !isChatChannel(chat),
canSendStickers: isAdmin || isStoryReply || !isUserRightBanned(chat, 'sendStickers', chatFullInfo),
canSendGifs: isAdmin || isStoryReply || !isUserRightBanned(chat, 'sendGifs', chatFullInfo),
canAttachEmbedLinks: !isStoryReply && (isAdmin || !isUserRightBanned(chat, 'embedLinks', chatFullInfo)),

View File

@ -51,6 +51,7 @@ export function hasMessageMedia(message: MediaContainer) {
|| getMessageSticker(message)
|| getMessageContact(message)
|| getMessagePollId(message)
|| getMessageTodo(message)
|| getMessageAction(message)
|| getMessageAudio(message)
|| getMessageVoice(message)
@ -128,6 +129,10 @@ export function getMessagePollId(message: MediaContainer) {
return message.content.pollId;
}
export function getMessageTodo(message: MediaContainer) {
return message.content.todo;
}
export function getMessageInvoice(message: MediaContainer) {
return message.content.invoice;
}

View File

@ -72,6 +72,7 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
sticker,
pollId,
paidMedia,
todo,
} = message.content;
if (message.groupedId || photo || paidMedia) {
@ -102,6 +103,10 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
return '📊';
}
if (todo) {
return '📝';
}
return undefined;
}
@ -143,6 +148,7 @@ function getSummaryDescription(
giveaway,
giveawayResults,
paidMedia,
todo,
} = mediaContent;
const { poll } = statefulContent || {};
@ -239,6 +245,10 @@ function getSummaryDescription(
summary = truncatedText || (message ? lang('ForwardedStory') : lang('Chat.ReplyStory'));
}
if (todo) {
summary = lang('Chat.Todo.Message.Title');
}
return summary || CONTENT_NOT_SUPPORTED;
}

View File

@ -52,13 +52,14 @@ export function getMessageTranscription(message: ApiMessage) {
export function hasMessageText(message: MediaContainer) {
const {
action, text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location,
game, storyData, giveaway, giveawayResults, paidMedia,
action, text, sticker, photo, video, audio, voice, document, pollId, todo,
webPage, contact, invoice, location, game, storyData, giveaway, giveawayResults, paidMedia,
} = message.content;
return Boolean(text) || !(
sticker || photo || video || audio || voice || document || contact || pollId || webPage || invoice || location
|| game || storyData || giveaway || giveawayResults || paidMedia || action?.type === 'phoneCall'
sticker || photo || video || audio || voice || document || contact || pollId || todo || webPage
|| invoice || location || game || storyData || giveaway || giveawayResults
|| paidMedia || action?.type === 'phoneCall'
);
}

View File

@ -23,6 +23,7 @@ import type {
ApiMessage,
ApiMessageEntity,
ApiMessageSearchContext,
ApiNewMediaTodo,
ApiNotification,
ApiNotifyPeerType,
ApiPaymentStatus,
@ -45,6 +46,7 @@ import type {
ApiStickerSet,
ApiStickerSetInfo,
ApiThemeParameters,
ApiTodoItem,
ApiTypePrepaidGiveaway,
ApiUpdate,
ApiUser,
@ -502,6 +504,11 @@ export interface ActionPayloads {
attachments?: ApiAttachment[];
entities?: ApiMessageEntity[];
} & WithTabId;
editTodo: {
chatId: string;
todo: ApiNewMediaTodo;
messageId: number;
} & WithTabId;
deleteHistory: {
chatId: string;
shouldDeleteForAll?: boolean;
@ -1380,6 +1387,17 @@ export interface ActionPayloads {
messageId: number;
options: string[];
};
toggleTodoCompleted: {
chatId: string;
messageId: number;
completedIds: number[];
incompletedIds: number[];
};
appendTodoList: {
chatId: string;
items: ApiTodoItem[];
messageId: number;
} & WithTabId;
cancelPollVote: {
chatId: string;
messageId: number;
@ -1779,7 +1797,7 @@ export interface ActionPayloads {
isMuted?: boolean;
shouldSharePhoneNumber?: boolean;
} & WithTabId;
addNoPaidMessagesException: {
toggleNoPaidMessagesException: {
userId: string;
shouldRefundCharged: boolean;
};
@ -2182,6 +2200,12 @@ export interface ActionPayloads {
isQuiz?: boolean;
} & WithTabId) | undefined;
closePollModal: WithTabId | undefined;
openTodoListModal: {
chatId: string;
messageId?: number;
isAddTaskMode?: boolean;
} & WithTabId;
closeTodoListModal: WithTabId | undefined;
requestConfetti: (ConfettiParams & WithTabId) | WithTabId;
requestWave: {
startX: number;

View File

@ -20,6 +20,7 @@ import type {
ApiMessage,
ApiMissingInvitedUser,
ApiMyBoost,
ApiNewMediaTodo,
ApiNewPoll,
ApiNotification,
ApiPaymentFormRegular,
@ -141,6 +142,7 @@ export type TabState = {
gif?: ApiVideo;
sticker?: ApiSticker;
poll?: ApiNewPoll;
todo?: ApiNewMediaTodo;
isSilent?: boolean;
sendGrouped?: boolean;
sendCompressed?: boolean;
@ -530,6 +532,12 @@ export type TabState = {
isQuiz?: boolean;
};
todoListModal?: {
chatId: string;
messageId?: number;
isAddTaskMode?: boolean;
};
preparedMessageModal?: {
message: ApiPreparedInlineMessage;
webAppKey: string;

View File

@ -12,5 +12,5 @@ for (const tl of Object.values(Api)) {
}
}
export const LAYER = 204;
export const LAYER = 205;
export { tlobjects };

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,7 @@ inputMediaDice#e66fbf7b emoticon:string = InputMedia;
inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia;
inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia;
inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia;
inputMediaTodo#9fc55fde todo:TodoList = InputMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
@ -83,7 +84,7 @@ chatForbidden#6592a1a7 id:long title:string = Chat;
channel#fe685355 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector<Username> stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat;
channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat;
chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull;
channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull;
channelFull#e07429de flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long = ChatFull;
chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant;
chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant;
chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant;
@ -93,7 +94,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto;
messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;
message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message;
messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia;
messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;
@ -111,6 +112,7 @@ messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int sto
messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia;
messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia;
messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia;
messageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia;
messageActionEmpty#b6aef7b0 = MessageAction;
messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
messageActionChatEditTitle#b5a1ce5a title:string = MessageAction;
@ -162,6 +164,8 @@ messageActionStarGiftUnique#2e3ae60e flags:# upgrade:flags.0?true transferred:fl
messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction;
messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction;
messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction;
messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;
messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;
dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
photoEmpty#2331b22d id:long = Photo;
@ -1087,8 +1091,8 @@ botCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandSco
account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult;
account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult;
account.resetPasswordOk#e926d63e = account.ResetPasswordResult;
sponsoredMessage#4d93a990 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage;
messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;
sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage;
messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;
messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages;
searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod;
messages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector<SearchResultsCalendarPeriod> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SearchResultsCalendar;
@ -1283,7 +1287,7 @@ storyReactionPublicForward#bbab2643 message:Message = StoryReaction;
storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction;
stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList;
savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog;
monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;
monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;
messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;
messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;
messages.savedDialogsNotModified#c01f6fe8 count:int = messages.SavedDialogs;
@ -1438,6 +1442,9 @@ starGiftAttributeCounter#2eb1b658 attribute:StarGiftAttributeId count:int = Star
payments.resaleStarGifts#947a12df flags:# count:int gifts:Vector<StarGift> next_offset:flags.0?string attributes:flags.1?Vector<StarGiftAttribute> attributes_hash:flags.1?long chats:Vector<Chat> counters:flags.2?Vector<StarGiftAttributeCounter> users:Vector<User> = payments.ResaleStarGifts;
stories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount;
pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion;
todoItem#cba9a52f id:int title:TextWithEntities = TodoItem;
todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList;
todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
@ -1503,8 +1510,7 @@ account.toggleUsername#58d6b376 username:string active:Bool = Bool;
account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool;
account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue;
account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;
@ -1663,9 +1669,11 @@ messages.getPaidReactionPrivacy#472455aa = Updates;
messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool;
messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool;
messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult;
messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages;
messages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages;
messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage;
messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool;
messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;
messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;

View File

@ -61,7 +61,7 @@
"account.resolveBusinessChatLink",
"account.toggleSponsoredMessages",
"account.getCollectibleEmojiStatuses",
"account.addNoPaidMessagesException",
"account.toggleNoPaidMessagesException ",
"account.getPaidMessagesRevenue",
"account.getAccountTTL",
"account.setAccountTTL",
@ -225,6 +225,8 @@
"messages.getSponsoredMessages",
"messages.reportMessagesDelivery",
"messages.getPreparedInlineMessage",
"messages.toggleTodoCompleted",
"messages.appendTodoList",
"updates.getState",
"updates.getDifference",
"updates.getChannelDifference",

View File

@ -46,6 +46,7 @@ inputMediaDice#e66fbf7b emoticon:string = InputMedia;
inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia;
inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia;
inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia;
inputMediaTodo#9fc55fde todo:TodoList = InputMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;
@ -103,7 +104,7 @@ channel#fe685355 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.
channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat;
chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull;
channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull;
channelFull#e07429de flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long = ChatFull;
chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant;
chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant;
@ -117,7 +118,7 @@ chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:f
messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;
message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message;
messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia;
@ -136,6 +137,7 @@ messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int sto
messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia;
messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia;
messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia;
messageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia;
messageActionEmpty#b6aef7b0 = MessageAction;
messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;
@ -188,6 +190,8 @@ messageActionStarGiftUnique#2e3ae60e flags:# upgrade:flags.0?true transferred:fl
messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction;
messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction;
messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction;
messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;
messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;
dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
@ -1407,9 +1411,9 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR
account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult;
account.resetPasswordOk#e926d63e = account.ResetPasswordResult;
sponsoredMessage#4d93a990 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage;
sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage;
messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;
messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;
messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages;
searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod;
@ -1715,7 +1719,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction;
stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList;
savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog;
monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;
monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;
messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;
messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;
@ -1983,6 +1987,12 @@ stories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount
pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion;
todoItem#cba9a52f id:int title:TextWithEntities = TodoItem;
todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList;
todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -2134,8 +2144,8 @@ account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings;
account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings;
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool;
account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue;
account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;
account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
@ -2390,13 +2400,15 @@ messages.getPaidReactionPrivacy#472455aa = Updates;
messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool;
messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool;
messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult;
messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages;
messages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages;
messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector<InlineQueryPeerType> = messages.BotPreparedInlineMessage;
messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage;
messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers;
messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool;
messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs;
messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool;
messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;
messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;

View File

@ -21,6 +21,7 @@ import type {
ApiMediaFormat,
ApiMessage,
ApiMessageEntity,
ApiNewMediaTodo,
ApiNewPoll,
ApiPeer,
ApiPhoto,
@ -730,6 +731,7 @@ export type SendMessageParams = {
story?: ApiStory | ApiStorySkipped;
gif?: ApiVideo;
poll?: ApiNewPoll;
todo?: ApiNewMediaTodo;
contact?: ApiContact;
isSilent?: boolean;
scheduledAt?: number;

View File

@ -50,6 +50,7 @@ export interface LangPair {
'PremiumPreviewVoiceToTextDescription': undefined;
'PremiumPreviewProfileBadgeDescription': undefined;
'PremiumPreviewDownloadSpeedDescription': undefined;
'PremiumPreviewTodoDescription': undefined;
'PremiumPreviewUploadsDescription': undefined;
'PremiumPreviewAdvancedChatManagementDescription': undefined;
'PremiumPreviewAnimatedProfilesDescription': undefined;
@ -1537,6 +1538,27 @@ export interface LangPair {
'MonoforumComposerPlaceholder': undefined;
'ChannelSendMessage': undefined;
'AutomaticTranslation': undefined;
'TitleNewToDoList': undefined;
'TitleEditToDoList': undefined;
'TitleAppendToDoList': undefined;
'InputTitle': undefined;
'TitleToDoList': undefined;
'TitleTask': undefined;
'TitleAddTask': undefined;
'AllowOthersAddTasks': undefined;
'AllowOthersMarkAsDone': undefined;
'AriaToDoCancel': undefined;
'TitleGroupToDoList': undefined;
'TitleYourToDoList': undefined;
'ToDoListNewTasks': undefined;
'MenuButtonAppendTodoList': undefined;
'PremiumMore': undefined;
'SubscribeToTelegramPremiumForToggleTask': undefined;
'SubscribeToTelegramPremiumForCreateToDo': undefined;
'SubscribeToTelegramPremiumForAppendToDo': undefined;
'ToDoListErrorChooseTitle': undefined;
'ToDoListErrorChooseTasks': undefined;
'PremiumPreviewTodo': undefined;
}
export interface LangPairWithVariables<V = LangVariable> {
@ -2518,6 +2540,62 @@ export interface LangPairWithVariables<V = LangVariable> {
'ComposerTitleForwardFrom': {
'users': V;
};
'TitleUserToDoList': {
'peer': V;
};
'DescriptionCompletedToDoTasks': {
'number': V;
'count': V;
};
'MessageActionTodoCompletionsAsDone': {
'peer': V;
'task': V;
};
'MessageActionTodoCompletionsAsDoneYou': {
'task': V;
};
'MessageActionTodoCompletionsAsDoneMultiple': {
'peer': V;
'tasks': V;
};
'MessageActionTodoCompletionsAsDoneMultipleYou': {
'tasks': V;
};
'MessageActionTodoCompletionsAsNotDone': {
'peer': V;
'task': V;
};
'MessageActionTodoCompletionsAsNotDoneYou': {
'task': V;
};
'MessageActionTodoCompletionsAsNotDoneMultiple': {
'peer': V;
'tasks': V;
};
'MessageActionTodoCompletionsAsNotDoneMultipleYou': {
'tasks': V;
};
'MessageActionAppendTodo': {
'peer': V;
'task': V;
'list': V;
};
'MessageActionAppendTodoYou': {
'task': V;
'list': V;
};
'MessageActionAppendTodoMultiple': {
'peer': V;
'tasks': V;
'list': V;
};
'MessageActionAppendTodoMultipleYou': {
'tasks': V;
'list': V;
};
'HintTodoListTasksCount': {
'count': V;
};
}
export interface LangPairPlural {
@ -2830,6 +2908,9 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'GiftAttributeSymbolPlural': {
'count': V;
};
'MessageActionTodoTaskCount': {
'count': V;
};
}
export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;

View File

@ -1,3 +1,9 @@
export default function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
export function generateUniqueNumberId() {
const timestamp = Date.now() % 100000000;
const random = Math.floor(Math.random() * 1000);
return timestamp + random;
}