Message: Support summaries (#6585)

Co-authored-by: Dmitry Kabanov <dmitrykabanovdev@gmail.com>
This commit is contained in:
zubiden 2026-01-20 12:00:39 +01:00 committed by Alexander Zinchuk
parent 1c602dddfa
commit 7ac577124e
28 changed files with 442 additions and 85 deletions

View File

@ -293,6 +293,7 @@ export function buildApiMessageWithChatId(
reportDeliveryUntilDate: mtpMessage.reportDeliveryUntilDate,
paidMessageStars: toJSNumber(mtpMessage.paidMessageStars),
restrictionReasons,
summaryLanguageCode: mtpMessage.summaryFromLanguage,
};
}

View File

@ -2353,6 +2353,22 @@ export async function translateText(params: TranslateTextParams) {
return formattedText;
}
export async function fetchMessageSummary({
chat, id, toLanguageCode,
}: {
chat: ApiChat; id: number; toLanguageCode?: string;
}) {
const result = await invokeRequest(new GramJs.messages.SummarizeText({
peer: buildInputPeer(chat.id, chat.accessHash),
id,
toLang: toLanguageCode,
}));
if (!result) return undefined;
return buildApiFormattedText(result);
}
function handleMultipleLocalMessagesUpdate(
localMessages: Record<string, ApiMessage>, update: GramJs.TypeUpdates,
) {

View File

@ -29,6 +29,7 @@ import {
} from '../apiBuilders/chats';
import {
buildApiFormattedText,
buildApiMessageEntity,
buildApiPhoto, buildApiUsernames, buildPrivacyRules,
} from '../apiBuilders/common';
import { buildApiStarGiftAuctionUserState, buildApiTypeStarGiftAuctionState } from '../apiBuilders/gifts';
@ -421,6 +422,7 @@ export function updater(update: Update) {
'@type': 'error',
error: {
message: update.message,
entities: update.entities.map(buildApiMessageEntity),
},
});
} else {

View File

@ -697,6 +697,7 @@ export interface ApiMessage {
reportDeliveryUntilDate?: number;
paidMessageStars?: number;
restrictionReasons?: ApiRestrictionReason[];
summaryLanguageCode?: string;
isTypingDraft?: boolean; // Local field
}

View File

@ -143,6 +143,7 @@ export type ApiNotification = {
export type ApiError = {
message: string;
entities?: ApiMessageEntity[];
hasErrorKey?: boolean;
isSlowMode?: boolean;
textParams?: Record<string, string>;

View File

@ -2591,6 +2591,10 @@
"AttachmentSendFile_other" = "Send {count} Files";
"AttachmentDragAddItems" = "Add Items";
"AttachmentCaptionPlaceholder" = "Add a caption...";
"MessageSummaryTitle" = "AI Summary";
"MessageSummaryDescription" = "Show original text";
"AriaShowSummary" = "Show AI Summary";
"AriaHideSummary" = "Hide AI Summary";
"SettingsDataClearMediaCache" = "Clear Media Cache";
"SettingsDataClearMediaCacheDescription" = "Deletes locally cached media for this account";
"SettingsDataClearMediaDone" = "Media cache cleared";

View File

@ -30,7 +30,7 @@ import MessageText from './MessageText';
type OwnProps = {
message: ApiMessage;
translatedText?: ApiFormattedText;
forcedText?: ApiFormattedText;
noEmoji?: boolean;
highlight?: string;
truncateLength?: number;
@ -49,7 +49,7 @@ type StateProps = {
function MessageSummary({
message,
translatedText,
forcedText,
noEmoji,
highlight,
truncateLength = TRUNCATED_SUMMARY_LENGTH,
@ -70,7 +70,7 @@ function MessageSummary({
const statefulContent = groupStatefulContent({ poll, story, webPage });
if (!extractedText && !hasPoll && !isAction) {
const summaryText = translatedText?.text
const summaryText = forcedText?.text
|| getMessageSummaryText(lang, message, statefulContent, noEmoji, truncateLength);
const trimmedText = trimText(summaryText, truncateLength);
@ -93,7 +93,7 @@ function MessageSummary({
return (
<MessageText
messageOrStory={message}
translatedText={translatedText}
forcedText={forcedText}
highlight={highlight}
asPreview
observeIntersectionForLoading={observeIntersectionForLoading}

View File

@ -21,7 +21,7 @@ import TypingWrapper from './TypingWrapper';
interface OwnProps {
messageOrStory: ApiMessage | ApiStory;
threadId?: ThreadId;
translatedText?: ApiFormattedText;
forcedText?: ApiFormattedText;
isForAnimation?: boolean;
emojiSize?: number;
highlight?: string;
@ -46,7 +46,7 @@ const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
function MessageText({
messageOrStory,
translatedText,
forcedText,
isForAnimation,
emojiSize,
highlight,
@ -74,7 +74,7 @@ function MessageText({
const lang = useLang();
const formattedText = translatedText || extractMessageText(messageOrStory, inChatList);
const formattedText = forcedText || extractMessageText(messageOrStory, inChatList);
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
const { text, entities } = adaptedFormattedText || {};

View File

@ -208,7 +208,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
<MessageSummary
message={message}
noEmoji={Boolean(mediaThumbnail)}
translatedText={translatedText}
forcedText={translatedText}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
emojiSize={EMOJI_SIZE}

View File

@ -8,7 +8,7 @@ import type { MessageList } from '../../types';
import { selectCurrentMessageList, selectTabState } from '../../global/selectors';
import getReadableErrorText from '../../util/getReadableErrorText';
import renderText from '../common/helpers/renderText';
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
@ -91,7 +91,7 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => {
title={getErrorHeader(error)}
>
{error.hasErrorKey ? getReadableErrorText(error)
: renderText(error.message, ['simple_markdown', 'emoji', 'br'])}
: renderTextWithEntities({ text: error.message, entities: error.entities })}
<div className="dialog-buttons mt-2">
<Button isText onClick={closeModal}>{lang('OK')}</Button>
</div>

View File

@ -709,10 +709,44 @@
}
}
.message-action-buttons {
.message-summary {
cursor: var(--custom-cursor, pointer);
display: flex;
flex-direction: column;
padding-block: 0.1875rem;
font-size: 0.875rem;
line-height: 1.25;
&-title {
font-weight: var(--font-weight-medium);
}
}
.message-action-buttons-container {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.message-action-buttons-sticky-zone {
position: relative;
height: 100%;
}
.message-action-button-sticky {
position: sticky;
top: 0.5rem;
}
.message-action-buttons {
display: flex;
flex-direction: column;
@ -729,9 +763,14 @@
}
.message-action-button {
overflow: visible;
width: 2.25rem;
height: 2.25rem;
color: white;
&.action-summary .icon {
font-size: 1.25rem;
}
}
&:hover,
@ -745,11 +784,11 @@
opacity: 1;
}
&.own .message-action-buttons {
&.own .message-action-buttons-container {
left: -3rem;
}
&:not(.own) .message-action-buttons {
&:not(.own) .message-action-buttons-container {
right: -3rem;
}

View File

@ -36,6 +36,7 @@ import type {
IAlbum,
MessageListType,
ScrollTargetPosition,
TextSummary,
ThemeKey,
ThreadId,
} from '../../../types';
@ -96,6 +97,7 @@ import {
selectIsMessageProtected,
selectIsMessageSelected,
selectMessageIdsByGroupId,
selectMessageSummary,
selectOutgoingStatus,
selectPeer,
selectPeerStory,
@ -141,6 +143,7 @@ import { calculateMediaDimensions, getMinMediaWidth, getMinMediaWidthWithText }
import useAppLayout from '../../../hooks/useAppLayout';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useEnsureStory from '../../../hooks/useEnsureStory';
import useFlag from '../../../hooks/useFlag';
@ -169,7 +172,9 @@ import FakeIcon from '../../common/FakeIcon';
import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon';
import MessageText from '../../common/MessageText';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import Sparkles from '../../common/Sparkles';
import TopicChip from '../../common/TopicChip';
import { animateSnap } from '../../main/visualEffects/SnapEffectContainer';
import Button from '../../ui/Button';
@ -324,6 +329,7 @@ type StateProps = {
minFutureTime?: number;
isMediaNsfw?: boolean;
isReplyMediaNsfw?: boolean;
summary?: TextSummary;
};
type MetaPosition =
@ -452,6 +458,7 @@ const Message = ({
isAccountFrozen,
minFutureTime,
webPage,
summary,
onIntersectPinnedMessage,
}: OwnProps & StateProps) => {
const {
@ -465,6 +472,7 @@ const Message = ({
focusMessage,
markMentionsRead,
openThread,
summarizeMessage,
} = getActions();
const ref = useRef<HTMLDivElement>();
@ -480,6 +488,7 @@ const Message = ({
const [shouldPlayEffect, requestEffect, hideEffect] = useFlag();
const [shouldPlayDiceEffect, requestDiceEffect, hideDiceEffect] = useFlag();
const [isDeclineDialogOpen, openDeclineDialog, closeDeclineDialog] = useFlag();
const [isShowingSummary, showSummary, hideSummary] = useFlag();
const [declineReason, setDeclineReason] = useState('');
const { isMobile, isTouchScreen } = useAppLayout();
@ -528,6 +537,7 @@ const Message = ({
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
isTypingDraft,
} = message;
const hasSummary = Boolean(message.summaryLanguageCode);
useUnmountCleanup(() => {
if (message.isPinned) {
@ -597,7 +607,8 @@ const Message = ({
const hasFactCheck = Boolean(factCheck?.text);
const hasForwardedCustomShape = asForwarded && isCustomShape;
const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply || hasForwardedCustomShape;
const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply || hasForwardedCustomShape
|| Boolean(isShowingSummary && summary?.text);
const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
if (isAccountFrozen) return;
@ -695,6 +706,16 @@ const Message = ({
lastPlaybackTimestamp,
});
useEffect(() => {
if (hasSummary && isShowingSummary && !summary) {
summarizeMessage({
chatId,
id: message.id,
toLanguageCode: requestedTranslationLanguage,
});
}
}, [hasSummary, chatId, message.id, requestedTranslationLanguage, isShowingSummary, summary]);
const handleEffectClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
@ -706,6 +727,7 @@ const Message = ({
chatId,
threadId,
messageId,
scrollTargetPosition: 'start',
noHighlight: true,
});
});
@ -801,9 +823,17 @@ const Message = ({
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
chatTranslations, chatId, shouldTranslate ? messageId : undefined, requestedTranslationLanguage,
);
const isSummaryPending = Boolean(summary?.isPending);
const isNewTextPending = isTranslationPending || isSummaryPending;
// Used to display previous result while new one is loading
const previousTranslatedText = usePreviousDeprecated(translatedText, Boolean(shouldTranslate));
useEffectWithPrevDeps(([prevIsShowingSummary]) => {
if (summary?.text || (prevIsShowingSummary && !isShowingSummary)) {
handleFocusSelf();
}
}, [isShowingSummary, summary?.text]);
const currentTranslatedText = translatedText || previousTranslatedText;
const phoneCall = action?.type === 'phoneCall' ? action : undefined;
@ -1019,10 +1049,13 @@ const Message = ({
function renderMessageText(isForAnimation?: boolean) {
if (!textMessage) return undefined;
const forcedText = (isShowingSummary && summary?.text)
|| (requestedTranslationLanguage ? currentTranslatedText : undefined);
return (
<MessageText
messageOrStory={textMessage}
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
forcedText={forcedText}
isForAnimation={isForAnimation}
focusedQuote={focusedQuote}
focusedQuoteOffset={focusedQuoteOffset}
@ -1041,6 +1074,16 @@ const Message = ({
);
}
function renderMessageTextAnimation() {
return (
<div className="translation-animation">
<div className="text-loading">
{renderMessageText(true)}
</div>
</div>
);
}
const renderQuickReactionButton = useCallback(() => {
if (!defaultReaction) return undefined;
@ -1174,6 +1217,20 @@ const Message = ({
onClick={handleStoryClick}
/>
)}
{hasSummary && isShowingSummary && !summary?.isPending && (
<PeerColorWrapper
className="message-summary"
onClick={hideSummary}
>
<Sparkles preset="button" className="message-summary-sparkles" />
<span className="message-summary-title">
{lang('MessageSummaryTitle')}
</span>
<span className="message-summary-description">
{lang('MessageSummaryDescription')}
</span>
</PeerColorWrapper>
)}
</div>
)}
{sticker && observeIntersectionForLoading && observeIntersectionForPlaying && (
@ -1347,13 +1404,7 @@ const Message = ({
{hasText && !hasAnimatedEmoji && (
<div className={textContentClass} dir="auto">
{renderMessageText()}
{isTranslationPending && (
<div className="translation-animation">
<div className="text-loading">
{renderMessageText(true)}
</div>
</div>
)}
{isNewTextPending && renderMessageTextAnimation()}
{hasFactCheck && (
<FactCheck factCheck={factCheck} isToggleDisabled={isInSelectMode} />
)}
@ -1409,13 +1460,7 @@ const Message = ({
{hasText && !hasAnimatedEmoji && (
<div className={textContentClass} dir="auto">
{renderMessageText()}
{isTranslationPending && (
<div className="translation-animation">
<div className="text-loading">
{renderMessageText(true)}
</div>
</div>
)}
{isNewTextPending && renderMessageTextAnimation()}
{!hasContentAfterText && isMetaInText && renderReactionsAndMeta()}
</div>
)}
@ -1820,40 +1865,57 @@ const Message = ({
{renderContent()}
{!isInDocumentGroupNotLast && metaPosition === 'standalone' && !isStoryMention && renderReactionsAndMeta()}
{canShowActionButton && (
<div className={buildClassName(
'message-action-buttons',
isLoadingComments && 'message-action-buttons-shown',
)}
>
{withCommentButton && isCustomShape && (
<CommentButton
threadInfo={commentsThreadInfo}
disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments}
isCustomShape
asActionButton
/>
)}
{canForward && (
<Button
className="message-action-button"
color="translucent-white"
round
ariaLabel={oldLang('lng_context_forward_msg')}
onClick={isLastInDocumentGroup ? handleGroupForward : handleForward}
iconName="share-filled"
/>
)}
{canFocus && (
<Button
className="message-action-button"
color="translucent-white"
round
ariaLabel={lang('FocusMessage')}
onClick={isPinnedList ? handleFocus : handleFocusForwarded}
iconName="arrow-right"
/>
<div className="message-action-buttons-container">
<div className="message-action-buttons-sticky-zone">
<div className="message-action-buttons message-action-button-sticky">
{hasSummary && (
<Button
className="message-action-button action-summary"
color="translucent-white"
round
withSparkleEffect
ariaLabel={isShowingSummary ? lang('AriaHideSummary') : lang('AriaShowSummary')}
onClick={isShowingSummary ? hideSummary : showSummary}
iconName={isShowingSummary ? 'expand' : 'collapse'}
/>
)}
</div>
</div>
<div className={buildClassName(
'message-action-buttons',
isLoadingComments && 'message-action-buttons-shown',
)}
>
{withCommentButton && isCustomShape && (
<CommentButton
threadInfo={commentsThreadInfo}
disabled={noComments || !commentsThreadInfo}
isLoading={isLoadingComments}
isCustomShape
asActionButton
/>
)}
{canForward && (
<Button
className="message-action-button"
color="translucent-white"
round
ariaLabel={oldLang('lng_context_forward_msg')}
onClick={isLastInDocumentGroup ? handleGroupForward : handleForward}
iconName="share-filled"
/>
)}
{canFocus && (
<Button
className="message-action-button"
color="translucent-white"
round
ariaLabel={lang('FocusMessage')}
onClick={isPinnedList ? handleFocus : handleFocusForwarded}
iconName="arrow-right"
/>
)}
</div>
</div>
)}
{withCommentButton && !isCustomShape && (
@ -2076,6 +2138,8 @@ export default memo(withGlobal<OwnProps>(
const isMediaNsfw = selectIsMediaNsfw(global, message);
const isReplyMediaNsfw = replyMessage && selectIsMediaNsfw(global, replyMessage);
const summary = selectMessageSummary(global, chatId, message.id, requestedTranslationLanguage);
return {
theme: selectTheme(global),
forceSenderName,
@ -2172,6 +2236,7 @@ export default memo(withGlobal<OwnProps>(
isMediaNsfw,
isReplyMediaNsfw,
webPage,
summary,
};
},
)(Message));

View File

@ -16,6 +16,7 @@ import type {
import type {
ForwardMessagesParams,
SendMessageParams,
TextSummary,
ThreadId,
} from '../../../types';
import type { MessageKey } from '../../../util/keys/messageKey';
@ -77,6 +78,7 @@ import {
import {
addChatMessagesById,
addUnreadMentions,
clearMessageSummary,
deleteSponsoredMessage,
removeOutlyingList,
removeRequestedMessageTranslation,
@ -91,6 +93,7 @@ import {
updateChatMessage,
updateGlobalSearch,
updateListedIds,
updateMessageSummary,
updateMessageTranslation,
updateOutlyingLists,
updatePeerFullInfo,
@ -2711,6 +2714,39 @@ addActionHandler('translateMessages', (global, actions, payload): ActionReturnTy
return global;
});
addActionHandler('summarizeMessage', async (global, actions, payload): Promise<void> => {
const { chatId, id, toLanguageCode } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const placeholderSummary: TextSummary = {
isPending: true,
text: undefined,
};
global = updateMessageSummary(global, chatId, id, placeholderSummary, toLanguageCode);
setGlobal(global);
const result = await callApi('fetchMessageSummary', { chat, id, toLanguageCode });
if (!result) {
global = getGlobal();
// Disable summary to prevent endless loading
global = updateChatMessage(global, chatId, id, { summaryLanguageCode: undefined });
global = clearMessageSummary(global, chatId, id);
setGlobal(global);
return;
}
const summary: TextSummary = {
isPending: false,
text: result,
};
global = getGlobal();
global = updateMessageSummary(global, chatId, id, summary, toLanguageCode);
setGlobal(global);
});
// https://github.com/telegramdesktop/tdesktop/blob/11906297d82b6ff57b277da5251d2e6eb3d8b6d0/Telegram/SourceFiles/api/api_views.cpp#L22
const SEND_VIEWS_TIMEOUT = 1000;
let viewsIncrementTimeout: number | undefined;

View File

@ -40,6 +40,7 @@ import {
import {
addMessages,
addViewportId,
clearMessageSummary,
clearMessageTranslation,
deleteChatMessages,
deleteChatScheduledMessages,
@ -358,6 +359,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
if (message.content?.text?.text !== currentMessage?.content?.text?.text) {
global = clearMessageTranslation(global, chatId, id);
global = clearMessageSummary(global, chatId, id);
}
if (poll) {

View File

@ -416,6 +416,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
const reducedGlobal: GlobalState = {
...INITIAL_GLOBAL_STATE,
...pick(global, [
'cacheVersion',
'appConfig',
'config',
'auth',
@ -722,6 +723,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
byChatId[chatId] = {
byId: cleanedById,
threadsById,
summaryById: {},
};
});

View File

@ -56,7 +56,7 @@ import {
} from '../selectors';
import { removeIdFromSearchResults } from './middleSearch';
import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations';
import { clearMessageSummary, clearMessageTranslation } from './translations';
type MessageStoreSections = GlobalState['messages']['byChatId'][string];
@ -153,7 +153,8 @@ export function updateThread<T extends GlobalState>(
export function updateMessageStore<T extends GlobalState>(
global: T, chatId: string, update: Partial<MessageStoreSections>,
): T {
const current = global.messages.byChatId[chatId] || { byId: {}, threadsById: {} };
const current = global.messages.byChatId[chatId]
|| { byId: {}, threadsById: {}, summaryById: {} };
return {
...global,
@ -399,6 +400,7 @@ export function deleteChatMessages<T extends GlobalState>(
threadMessages.push(messageId);
updatedThreads.set(threadId, threadMessages);
global = clearMessageTranslation(global, chatId, messageId);
global = clearMessageSummary(global, chatId, messageId);
});
const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter(

View File

@ -1,5 +1,5 @@
import type { ApiFormattedText } from '../../api/types';
import type { TranslatedMessage } from '../../types';
import type { TextSummary, TranslatedMessage } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
@ -162,3 +162,44 @@ export function removeRequestedMessageTranslation<T extends GlobalState>(
return global;
}
export function updateMessageSummary<T extends GlobalState>(
global: T, chatId: string, messageId: number, summary: TextSummary, toLanguageCode?: string,
) {
if (toLanguageCode) {
return updateMessageTranslation(global, chatId, messageId, toLanguageCode, { summary });
}
const chatSummaries = global.messages.byChatId[chatId]?.summaryById;
return replaceGeneralSummaryStore(global, chatId, { ...chatSummaries, [messageId]: summary });
}
export function clearMessageSummary<T extends GlobalState>(
global: T, chatId: string, messageId: number,
) {
const chatSummaries = global.messages.byChatId[chatId]?.summaryById;
if (!chatSummaries) return global;
const newSummaryById = omit(chatSummaries, [messageId]);
return replaceGeneralSummaryStore(global, chatId, newSummaryById);
}
function replaceGeneralSummaryStore<T extends GlobalState>(
global: T, chatId: string, update: Record<number, TextSummary>,
): T {
if (!global.messages.byChatId[chatId]) return global; // Unloaded chats should not have summaries
return {
...global,
messages: {
...global.messages,
byChatId: {
...global.messages.byChatId,
[chatId]: {
...global.messages.byChatId[chatId],
summaryById: update,
},
},
},
};
}

View File

@ -13,6 +13,7 @@ import type {
ChatTranslatedMessages,
MessageListType,
TabThread,
TextSummary,
Thread,
ThreadId,
} from '../../types';
@ -1464,6 +1465,20 @@ export function selectForwardsContainVoiceMessages<T extends GlobalState>(
});
}
export function selectMessageSummary<T extends GlobalState>(
global: T, chatId: string, messageId: number, toLanguageCode?: string,
): TextSummary | undefined {
const message = selectChatMessage(global, chatId, messageId);
if (!message?.summaryLanguageCode) return undefined;
if (toLanguageCode && toLanguageCode !== message.summaryLanguageCode) {
const messageTranslations = selectMessageTranslations(global, chatId, toLanguageCode);
return messageTranslations[messageId]?.summary;
}
return global.messages.byChatId[chatId].summaryById[messageId];
}
export function selectChatTranslations<T extends GlobalState>(
global: T, chatId: string,
): ChatTranslatedMessages {

View File

@ -1515,6 +1515,11 @@ export interface ActionPayloads {
messageIds: number[];
toLanguageCode?: string;
};
summarizeMessage: {
chatId: string;
id: number;
toLanguageCode?: string;
};
// Reactions
loadTopReactions: undefined;

View File

@ -70,6 +70,7 @@ import type {
StarGiftCategory,
StarsSubscriptions,
StarsTransactionHistory,
TextSummary,
ThemeKey,
Thread,
ThreadId,
@ -245,6 +246,7 @@ export type GlobalState = {
messages: {
byChatId: Record<string, {
byId: Record<number, ApiMessage>;
summaryById: Record<number, TextSummary>;
threadsById: Record<ThreadId, Thread>;
}>;
playbackByChatId: Record<string, {

View File

@ -12,6 +12,6 @@ for (const tl of Object.values(Api)) {
}
}
export const LAYER = 220;
export const LAYER = 221;
export { tlobjects };

File diff suppressed because one or more lines are too long

View File

@ -38,6 +38,7 @@ 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;
inputMediaStakeDice#f3a9244a game_hash:string ton_amount:long client_seed:bytes = 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;
@ -93,7 +94,7 @@ chatParticipants#3cbc93f8 chat_id:long participants:Vector<ChatParticipant> vers
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#b92f76cf 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 paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?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 suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int = Message;
message#9cb490e9 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 paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?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 suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = 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;
@ -107,7 +108,7 @@ messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia;
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
messageMediaDice#8cbec07 flags:# value:int emoticon:string game_outcome:flags.0?messages.EmojiGameOutcome = MessageMedia;
messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia;
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;
@ -399,6 +400,7 @@ updatePinnedForumTopics#def143d0 flags:# peer:Peer order:flags.0?Vector<int> = U
updateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector<int> = Update;
updateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update;
updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update;
updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference;
updates.difference#f49ca0 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;
@ -1520,10 +1522,14 @@ auth.passkeyLoginOptions#e2037789 options:DataJSON = auth.PasskeyLoginOptions;
inputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:bytes = InputPasskeyResponse;
inputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse;
inputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;
inputPasskeyCredentialFirebasePNV#5b1ccb28 pnv_token:string = InputPasskeyCredential;
starGiftBackground#aff56398 center_color:int edge_color:int text_color:int = StarGiftBackground;
starGiftAuctionRound#3aae0528 num:int duration:int = StarGiftAuctionRound;
starGiftAuctionRoundExtendable#aa021e5 num:int duration:int extend_top:int extend_window:int = StarGiftAuctionRound;
payments.starGiftUpgradeAttributes#46c6e36f attributes:Vector<StarGiftAttribute> = payments.StarGiftUpgradeAttributes;
messages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:long = messages.EmojiGameOutcome;
messages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo;
messages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector<int> plays_left:flags.0?int = messages.EmojiGameInfo;
---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;
@ -1771,6 +1777,7 @@ messages.editForumTopic#cecc1134 flags:# peer:InputPeer topic_id:int title:flags
messages.updatePinnedForumTopic#175df251 peer:InputPeer topic_id:int pinned:Bool = Updates;
messages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:InputPeer title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates;
messages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory;
messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities;
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

@ -245,6 +245,7 @@
"messages.deleteTopicHistory",
"messages.toggleTodoCompleted",
"messages.appendTodoList",
"messages.summarizeText",
"updates.getState",
"updates.getDifference",
"updates.getChannelDifference",

View File

@ -47,6 +47,7 @@ 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;
inputMediaStakeDice#f3a9244a game_hash:string ton_amount:long client_seed:bytes = 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;
@ -117,7 +118,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#b92f76cf 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 paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?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 suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int = Message;
message#9cb490e9 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 paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?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 suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = 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;
@ -132,7 +133,7 @@ messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia;
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
messageMediaDice#8cbec07 flags:# value:int emoticon:string game_outcome:flags.0?messages.EmojiGameOutcome = MessageMedia;
messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia;
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;
@ -452,6 +453,7 @@ updatePinnedForumTopics#def143d0 flags:# peer:Peer order:flags.0?Vector<int> = U
updateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector<int> = Update;
updateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update;
updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update;
updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -2097,6 +2099,7 @@ inputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:byte
inputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse;
inputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;
inputPasskeyCredentialFirebasePNV#5b1ccb28 pnv_token:string = InputPasskeyCredential;
starGiftBackground#aff56398 center_color:int edge_color:int text_color:int = StarGiftBackground;
@ -2105,6 +2108,11 @@ starGiftAuctionRoundExtendable#aa021e5 num:int duration:int extend_top:int exten
payments.starGiftUpgradeAttributes#46c6e36f attributes:Vector<StarGiftAttribute> = payments.StarGiftUpgradeAttributes;
messages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:long = messages.EmojiGameOutcome;
messages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo;
messages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector<int> plays_left:flags.0?int = messages.EmojiGameInfo;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -2544,6 +2552,8 @@ messages.updatePinnedForumTopic#175df251 peer:InputPeer topic_id:int pinned:Bool
messages.reorderPinnedForumTopics#e7841f0 flags:# force:flags.0?true peer:InputPeer order:Vector<int> = Updates;
messages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:InputPeer title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates;
messages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory;
messages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo;
messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities;
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

@ -642,6 +642,15 @@ export interface TopicsInfo {
export type TranslatedMessage = {
isPending?: boolean;
text?: ApiFormattedText;
summary?: TextSummary;
};
export type TextSummary = {
isPending?: false;
text: ApiFormattedText;
} | {
isPending: true;
text?: undefined;
};
export type ChatTranslatedMessages = {

View File

@ -1901,6 +1901,10 @@ export interface LangPair {
'AttachmentMenuDisableSpoiler': undefined;
'AttachmentDragAddItems': undefined;
'AttachmentCaptionPlaceholder': undefined;
'MessageSummaryTitle': undefined;
'MessageSummaryDescription': undefined;
'AriaShowSummary': undefined;
'AriaHideSummary': undefined;
'SettingsDataClearMediaCache': undefined;
'SettingsDataClearMediaCacheDescription': undefined;
'SettingsDataClearMediaDone': undefined;

View File

@ -10,7 +10,7 @@ import { isUsernameValid } from './entities/username';
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' | 'premium_multigift' | 'stars_topup'
| 'nft' | 'stars' | 'ton' | 'stargift_auction';
| 'nft' | 'stars' | 'ton' | 'stargift_auction' | 'premium';
interface PublicMessageLink {
type: 'publicMessageLink';
@ -91,7 +91,7 @@ interface BusinessChatLink {
interface PremiumReferrerLink {
type: 'premiumReferrerLink';
referrer: string;
ref?: string;
}
interface PremiumMultigiftLink {
@ -268,7 +268,7 @@ function parseTgLink(url: URL) {
case 'businessChatLink':
return buildBusinessChatLink({ slug: queryParams.slug });
case 'premiumReferrerLink':
return buildPremiumReferrerLink({ referrer: queryParams.ref });
return buildPremiumReferrerLink({ ref: queryParams.ref });
case 'premiumMultigiftLink':
return buildPremiumMultigiftLink({ referrer: queryParams.ref });
case 'chatBoostLink':
@ -488,6 +488,7 @@ function getTgDeepLinkType(
case 'message':
return 'businessChatLink';
case 'premium_offer':
case 'premium':
return 'premiumReferrerLink';
case 'premium_multigift':
return 'premiumMultigiftLink';
@ -774,16 +775,12 @@ function buildSettingsScreenLink(params: BuilderParams<SettingsScreenLink>): Bui
function buildPremiumReferrerLink(params: BuilderParams<PremiumReferrerLink>): BuilderReturnType<PremiumReferrerLink> {
const {
referrer,
ref,
} = params;
if (!referrer) {
return undefined;
}
return {
type: 'premiumReferrerLink',
referrer,
ref,
};
}