Chat: Support unread poll votes (#6847)

This commit is contained in:
zubiden 2026-04-17 13:37:59 +02:00 committed by Alexander Zinchuk
parent 741c6f532f
commit 86684cd1c1
31 changed files with 1329 additions and 827 deletions

View File

@ -710,7 +710,7 @@ export function buildThreadReadState(
const forumTopic = input instanceof GramJs.ForumTopic ? input : undefined;
const { unreadReactionsCount } = dialog || monoForumDialog || forumTopic || {};
const { unreadMentionsCount } = dialog || forumTopic || {};
const { unreadMentionsCount, unreadPollVotesCount } = dialog || forumTopic || {};
const { unreadMark } = dialog || monoForumDialog || {};
return omitUndefined<ThreadReadState>({
@ -718,6 +718,7 @@ export function buildThreadReadState(
lastReadInboxMessageId: readInboxMaxId,
lastReadOutboxMessageId: readOutboxMaxId,
unreadReactionsCount,
unreadPollVotesCount,
unreadMentionsCount,
hasUnreadMark: unreadMark,
});

View File

@ -45,6 +45,7 @@ import {
MAX_INT_32,
MENTION_UNREAD_SLICE,
MESSAGE_ID_REQUIRED_ERROR,
POLL_UNREAD_SLICE,
REACTION_UNREAD_SLICE,
SUPPORTED_PHOTO_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
@ -2298,6 +2299,27 @@ export async function readAllReactions({
}
}
export async function readAllPollVotes({
chat,
threadId,
}: {
chat: ApiChat;
threadId?: ThreadId;
}) {
const result = await invokeRequest(new GramJs.messages.ReadPollVotes({
peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
}));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await readAllPollVotes({ chat, threadId });
}
}
export async function fetchUnreadMentions({
chat, threadId, offsetId, addOffset, maxId, minId,
}: {
@ -2376,6 +2398,45 @@ export async function fetchUnreadReactions({
};
}
export async function fetchUnreadPollVotes({
chat, threadId, offsetId, addOffset, maxId, minId,
}: {
chat: ApiChat;
threadId?: ThreadId;
offsetId?: number;
addOffset?: number;
maxId?: number;
minId?: number;
}) {
const result = await invokeRequest(new GramJs.messages.GetUnreadPollVotes({
peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: threadId ? Number(threadId) : undefined,
limit: POLL_UNREAD_SLICE,
offsetId: offsetId ?? DEFAULT_PRIMITIVES.INT,
addOffset: addOffset ?? DEFAULT_PRIMITIVES.INT,
maxId: maxId ?? DEFAULT_PRIMITIVES.INT,
minId: minId ?? DEFAULT_PRIMITIVES.INT,
}));
if (
!result
|| result instanceof GramJs.messages.MessagesNotModified
|| !result.messages
) {
return undefined;
}
const totalCount = 'count' in result ? result.count : result.messages.length;
const messages = result.messages.map(buildApiMessage).filter(Boolean);
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
return {
totalCount,
messages,
topics,
};
}
export async function transcribeAudio({
chat, messageId,
}: {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M16 28a3 3 0 0 1-3-3V7a3 3 0 1 1 6 0v18a3 3 0 0 1-3 3M25.325 28a3 3 0 0 1-3-3v-7.318a3 3 0 0 1 6 0V25a3 3 0 0 1-3 3M6.675 28a3 3 0 0 1-3-3V13.713a3 3 0 1 1 6 0V25a3 3 0 0 1-3 3"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -109,6 +109,7 @@
"AnonymousPoll" = "Anonymous Poll";
"AccDescrReactionMentionDown" = "Go to next unread reactions";
"AccDescrMentionDown" = "Go to next mention";
"AccDescrPollVoteDown" = "Go to next unread poll vote";
"AccDescrPageDown" = "Go to bottom";
"ChannelPrivate" = "Private Channel";
"ChannelPrivateInfo" = "Private channels can only be joined via invite link.";

View File

@ -90,6 +90,10 @@
background-color: #ed504f;
}
.poll {
background-color: var(--color-primary);
}
.round {
display: grid;
place-content: center;

View File

@ -69,6 +69,7 @@ const ChatBadge = ({
const {
unreadMentionsCount: stateUnreadMentionsCount = 0,
unreadPollVotesCount: stateUnreadPollVotesCount = 0,
unreadReactionsCount: stateUnreadReactionsCount = 0,
unreadCount: stateUnreadCount = 0,
hasUnreadMark,
@ -95,12 +96,29 @@ const ChatBadge = ({
const topicsWithUnreadMentionsIds = useMemo(() => (
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadMentionsCount) : undefined
), [listedTopicIds, isForum, topicsReadStates]);
const topicsWithUnreadPollVotesIds = useMemo(() => (
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadPollVotesCount) : undefined
), [listedTopicIds, isForum, topicsReadStates]);
const topicsWithUnreadReactionsIds = useMemo(() => (
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadReactionsCount) : undefined
), [listedTopicIds, isForum, topicsReadStates]);
const topicsWithStatefulUnreadIds = useMemo(() => {
if (!isForum) {
return undefined;
}
const allTopicIds = [
...(topicsWithUnreadIds || []),
...(topicsWithUnreadPollVotesIds || []),
...(topicsWithUnreadReactionsIds || []),
];
return allTopicIds.length ? [...new Set(allTopicIds)] : [];
}, [isForum, topicsWithUnreadIds, topicsWithUnreadPollVotesIds, topicsWithUnreadReactionsIds]);
const unreadCount = isForum ? topicsWithUnreadIds?.length : stateUnreadCount;
const unreadMentionsCount = isForum ? topicsWithUnreadMentionsIds?.length : stateUnreadMentionsCount;
const unreadPollVotesCount = isForum ? topicsWithUnreadPollVotesIds?.length : stateUnreadPollVotesCount;
const unreadReactionsCount = isForum ? topicsWithUnreadReactionsIds?.length : stateUnreadReactionsCount;
const shouldBeUnMuted = useMemo(() => {
@ -108,17 +126,21 @@ const ChatBadge = ({
return !isMuted || topic?.notifySettings.mutedUntil === 0;
}
if (isMuted) {
return topicsWithUnreadIds?.some((tId) => topicsById?.[tId]?.notifySettings.mutedUntil === 0);
if (!topicsWithStatefulUnreadIds?.length) {
return !isMuted;
}
const isEveryUnreadMuted = topicsWithUnreadIds?.every((tId) => {
if (isMuted) {
return topicsWithStatefulUnreadIds.some((tId) => topicsById?.[tId]?.notifySettings.mutedUntil === 0);
}
const isEveryUnreadMuted = topicsWithStatefulUnreadIds.every((tId) => {
const mutedUntil = topicsById?.[tId]?.notifySettings.mutedUntil;
return mutedUntil && mutedUntil > getServerTime();
});
return !isEveryUnreadMuted;
}, [isForum, isMuted, topicsWithUnreadIds, topicsById, topic?.notifySettings.mutedUntil]);
}, [isForum, isMuted, topicsById, topic?.notifySettings.mutedUntil, topicsWithStatefulUnreadIds]);
const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog);
@ -127,7 +149,7 @@ const ChatBadge = ({
[forceHidden],
);
const isShown = !resolvedForceHidden && Boolean(
unreadCount || unreadMentionsCount || hasUnreadMark || isPinned || unreadReactionsCount
unreadCount || unreadMentionsCount || unreadPollVotesCount || hasUnreadMark || isPinned || unreadReactionsCount
|| isTopicUnopened || hasMiniApp,
);
@ -152,6 +174,12 @@ const ChatBadge = ({
</div>
);
const unreadPollVotesElement = unreadPollVotesCount && (
<div className={buildClassName(statefulClassName, styles.poll, styles.round)}>
<Icon name="poll-badge" />
</div>
);
const unreadMentionsElement = unreadMentionsCount && (
<div className={buildClassName(baseClassName, styles.mention, styles.round)}>
<Icon name="mention" />
@ -187,10 +215,16 @@ const ChatBadge = ({
);
const visiblePinnedElement = !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement
&& !unreadPollVotesElement
&& pinnedElement;
const elements = [
unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, visiblePinnedElement,
unopenedTopicElement,
unreadPollVotesElement,
unreadReactionsElement,
unreadMentionsElement,
unreadCountElement,
visiblePinnedElement,
].filter(Boolean);
if (isSavedDialog) return pinnedElement;
@ -204,7 +238,11 @@ const ChatBadge = ({
if (shouldShowOnlyMostImportant) {
const importanceOrderedElements = [
unreadMentionsElement, unreadCountElement, unreadReactionsElement, pinnedElement,
unreadPollVotesElement,
unreadReactionsElement,
unreadMentionsElement,
unreadCountElement,
pinnedElement,
].filter(Boolean);
return importanceOrderedElements[0];
}

View File

@ -1,6 +1,7 @@
.root {
--base-bottom-pos: 5.3125rem;
--translate-y: 4.5rem;
--unread-count-button-offset: 3.5rem;
pointer-events: none;
@ -14,23 +15,6 @@
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
.hidden {
pointer-events: none;
opacity: 0;
}
.reactions {
transition: 0.2s ease opacity, 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) transform;
}
.transform-down {
transform: translateY(3.5rem);
}
.unread {
pointer-events: none;
}
:global(body.no-page-transitions) & {
--translate-y: 0 !important;
@ -52,7 +36,7 @@
}
&.hide-scroll-down {
--translate-y: 3.5rem;
--translate-y: var(--unread-count-button-offset);
.unread {
pointer-events: none;
@ -75,3 +59,24 @@
}
}
}
.hidden {
pointer-events: none;
opacity: 0;
}
.unreadCountButton {
transition: opacity 0.2s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.transformDown {
transform: translateY(var(--unread-count-button-offset));
}
.transformDownDouble {
transform: translateY(calc(var(--unread-count-button-offset) * 2));
}
.unread {
pointer-events: none;
}

View File

@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { MessageListType, ThreadId, ThreadReadState } from '../../types';
import type { IconName } from '../../types/icons';
import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors';
import { selectThreadReadState } from '../../global/selectors/threads';
@ -27,6 +28,16 @@ type StateProps = {
shouldShowCount?: boolean;
};
type UnreadCountButton = {
icon: IconName;
ariaLabelLang: string;
unreadCount?: number;
isHidden: boolean;
hiddenUnreadCountButtonsBelow: number;
onClick: VoidFunction;
onReadAll: VoidFunction;
};
const FloatingActionButtons = ({
withScrollDown,
canPost,
@ -38,19 +49,40 @@ const FloatingActionButtons = ({
shouldShowCount,
}: OwnProps & StateProps) => {
const {
focusNextReply, focusNextReaction, focusNextMention, loadUnreadReactions,
readAllMentions, readAllReactions, loadUnreadMentions, scrollMessageListToBottom,
focusNextPollVote,
focusNextReply,
focusNextReaction,
focusNextMention,
loadUnreadPollVotes,
loadUnreadReactions,
readAllMentions,
readAllPollVotes,
readAllReactions,
loadUnreadMentions,
scrollMessageListToBottom,
} = getActions();
const elementRef = useRef<HTMLDivElement>();
const {
unreadReactionsCount, unreadMentionsCount, unreadCount, unreadReactions, unreadMentions,
unreadPollVotesCount,
unreadReactionsCount,
unreadMentionsCount,
unreadCount,
unreadPollVotes,
unreadReactions,
unreadMentions,
} = (shouldShowCount && threadReadState) || {};
const hasUnreadPollVotes = Boolean(unreadPollVotesCount);
const hasUnreadReactions = Boolean(unreadReactionsCount);
const hasUnreadMentions = Boolean(unreadMentionsCount);
const handleReadAllPollVotes = useLastCallback(() => {
if (!chatId) return;
readAllPollVotes({ chatId, threadId });
});
const handleReadAllReactions = useLastCallback(() => {
if (!chatId) return;
readAllReactions({ chatId, threadId });
@ -61,6 +93,12 @@ const FloatingActionButtons = ({
readAllMentions({ chatId, threadId });
});
useEffect(() => {
if (hasUnreadPollVotes && chatId && !unreadPollVotes?.length) {
loadUnreadPollVotes({ chatId, threadId });
}
}, [chatId, threadId, hasUnreadPollVotes, unreadPollVotes?.length]);
useEffect(() => {
if (hasUnreadReactions && chatId && !unreadReactions?.length) {
loadUnreadReactions({ chatId, threadId });
@ -90,42 +128,87 @@ const FloatingActionButtons = ({
focusNextReaction({ chatId, threadId });
});
const handleFocusNextPollVote = useLastCallback(() => {
if (!chatId) return;
focusNextPollVote({ chatId, threadId });
});
const handleFocusNextMention = useLastCallback(() => {
if (!chatId) return;
focusNextMention({ chatId, threadId });
});
const unreadCountButtonsConfig = [
{
icon: 'poll',
ariaLabelLang: 'AccDescrPollVoteDown',
unreadCount: unreadPollVotesCount,
isHidden: !hasUnreadPollVotes,
onClick: handleFocusNextPollVote,
onReadAll: handleReadAllPollVotes,
},
{
icon: 'heart-outline',
ariaLabelLang: 'AccDescrReactionMentionDown',
unreadCount: unreadReactionsCount,
isHidden: !hasUnreadReactions,
onClick: handleFocusNextReaction,
onReadAll: handleReadAllReactions,
},
{
icon: 'mention',
ariaLabelLang: 'AccDescrMentionDown',
unreadCount: unreadMentionsCount,
isHidden: !hasUnreadMentions,
onClick: handleFocusNextMention,
onReadAll: handleReadAllMentions,
},
] satisfies Omit<UnreadCountButton, 'hiddenUnreadCountButtonsBelow'>[];
let hiddenUnreadCountButtonsBelow = 0;
const unreadCountButtons = unreadCountButtonsConfig.reduceRight<UnreadCountButton[]>((result, button) => {
result.unshift({
...button,
hiddenUnreadCountButtonsBelow,
});
if (button.isHidden) {
hiddenUnreadCountButtonsBelow++;
}
return result;
}, []);
const hasUnreadCountButtons = unreadCountButtons.some((button) => !button.isHidden);
const buildUnreadCountButtonClassName = (button: UnreadCountButton) => buildClassName(
styles.unreadCountButton,
button.isHidden && styles.hidden,
button.hiddenUnreadCountButtonsBelow === 1 && styles.transformDown,
button.hiddenUnreadCountButtonsBelow === 2 && styles.transformDownDouble,
);
const fabClassName = buildClassName(
styles.root,
(withScrollDown || hasUnreadReactions || hasUnreadMentions) && styles.revealed,
(hasUnreadReactions || hasUnreadMentions) && !withScrollDown && styles.hideScrollDown,
(withScrollDown || hasUnreadCountButtons) && styles.revealed,
hasUnreadCountButtons && !withScrollDown && styles.hideScrollDown,
!canPost && styles.noComposer,
!withExtraShift && styles.noExtraShift,
);
return (
<div ref={elementRef} className={fabClassName}>
<ScrollDownButton
icon="heart-outline"
ariaLabelLang="AccDescrReactionMentionDown"
onClick={handleFocusNextReaction}
onReadAll={handleReadAllReactions}
unreadCount={unreadReactionsCount}
className={buildClassName(
styles.reactions,
!hasUnreadReactions && styles.hidden,
!hasUnreadMentions && styles.transformDown,
)}
/>
<ScrollDownButton
icon="mention"
ariaLabelLang="AccDescrMentionDown"
onClick={handleFocusNextMention}
onReadAll={handleReadAllMentions}
unreadCount={unreadMentionsCount}
className={!hasUnreadMentions && styles.hidden}
/>
{unreadCountButtons.map((button) => (
<ScrollDownButton
key={button.icon}
icon={button.icon}
ariaLabelLang={button.ariaLabelLang}
onClick={button.onClick}
onReadAll={button.onReadAll}
unreadCount={button.unreadCount}
className={buildUnreadCountButtonClassName(button)}
/>
))}
<ScrollDownButton
icon="arrow-down"
@ -142,7 +225,13 @@ export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const currentMessageList = selectCurrentMessageList(global);
if (!currentMessageList) {
return {} as Complete<StateProps>;
return {
chatId: undefined,
messageListType: undefined,
shouldShowCount: undefined,
threadId: undefined,
threadReadState: undefined,
};
}
const { chatId, threadId, type: messageListType } = currentMessageList;

View File

@ -33,7 +33,7 @@ export default function useMessageObservers({
onIntersectPinnedMessage: OnIntersectPinnedMessage | undefined;
}) {
const {
markMessageListRead, markMentionsRead, animateUnreadReaction,
markMessageListRead, markMentionsRead, markPollVotesRead, animateUnreadReaction,
scheduleForViewsIncrement,
} = getActions();
@ -61,6 +61,7 @@ export default function useMessageObservers({
let maxId = 0;
const mentionIds: number[] = [];
const pollVoteIds: number[] = [];
const reactionIds: number[] = [];
const scheduledToUpdateViews: number[] = [];
@ -91,6 +92,10 @@ export default function useMessageObservers({
mentionIds.push(messageId);
}
if (dataset.hasUnreadPollVote) {
pollVoteIds.push(messageId);
}
if (dataset.hasUnreadReaction) {
reactionIds.push(messageId);
}
@ -113,6 +118,10 @@ export default function useMessageObservers({
markMentionsRead({ chatId, messageIds: mentionIds });
}
if (pollVoteIds.length) {
markPollVotesRead({ chatId, messageIds: pollVoteIds });
}
if (scheduledToUpdateViews.length) {
scheduleForViewsIncrement({ chatId, ids: scheduledToUpdateViews });
}

View File

@ -102,6 +102,7 @@ type StateProps = {
isCurrentUserPremium?: boolean;
isInSelectMode?: boolean;
hasUnreadReaction?: boolean;
hasUnreadPollVote?: boolean;
isResizingContainer?: boolean;
scrollTargetPosition?: ScrollTargetPosition;
isAccountFrozen?: boolean;
@ -140,6 +141,7 @@ const ActionMessage = ({
isCurrentUserPremium,
isInSelectMode,
hasUnreadReaction,
hasUnreadPollVote,
isResizingContainer,
scrollTargetPosition,
isAccountFrozen,
@ -161,6 +163,7 @@ const ActionMessage = ({
toggleChannelRecommendations,
animateUnreadReaction,
markMentionsRead,
markPollVotesRead,
focusMessage,
openGiftOfferAcceptModal,
declineStarGiftOffer,
@ -335,10 +338,22 @@ const ActionMessage = ({
animateUnreadReaction({ chatId, messageIds: [id] });
}
if (hasUnreadPollVote) {
markPollVotesRead({ chatId, messageIds: [id] });
}
if (message.hasUnreadMention) {
markMentionsRead({ chatId, messageIds: [id] });
}
}, [hasUnreadReaction, chatId, id, animateUnreadReaction, message.hasUnreadMention]);
}, [
hasUnreadReaction,
hasUnreadPollVote,
chatId,
id,
animateUnreadReaction,
markPollVotesRead,
message.hasUnreadMention,
]);
useEffect(() => {
if (action.type !== 'giftPremium') return;
@ -619,6 +634,7 @@ const ActionMessage = ({
data-message-id={message.id}
data-is-pinned={message.isPinned || undefined}
data-has-unread-mention={message.hasUnreadMention || undefined}
data-has-unread-poll-vote={hasUnreadPollVote || undefined}
data-has-unread-reaction={hasUnreadReaction || undefined}
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
@ -737,6 +753,7 @@ export default memo(withGlobal<OwnProps>(
const readState = selectThreadReadState(global, message.chatId, threadId);
const hasUnreadReaction = readState?.unreadReactions?.includes(message.id);
const hasUnreadPollVote = readState?.unreadPollVotes?.includes(message.id);
const isAccountFrozen = selectIsCurrentUserFrozen(global);
return {
@ -751,6 +768,7 @@ export default memo(withGlobal<OwnProps>(
isInSelectMode: selectIsInSelectMode(global),
actionMessageBg: selectActionMessageBg(global),
hasUnreadReaction,
hasUnreadPollVote,
isResizingContainer,
scrollTargetPosition,
isAccountFrozen,

View File

@ -301,6 +301,7 @@ type StateProps = {
defaultReaction?: ApiReaction;
activeEmojiInteractions?: ActiveEmojiInteraction[];
hasUnreadReaction?: boolean;
hasUnreadPollVote?: boolean;
isTranscribing?: boolean;
transcribedText?: string;
isPremium: boolean;
@ -431,6 +432,7 @@ const Message = ({
autoLoadFileMaxSizeMb,
repliesThreadInfo,
hasUnreadReaction,
hasUnreadPollVote,
memoFirstUnreadIdRef,
senderChatMember,
messageTopic,
@ -478,6 +480,7 @@ const Message = ({
animateUnreadReaction,
focusMessage,
markMentionsRead,
markPollVotesRead,
openThread,
summarizeMessage,
} = getActions();
@ -963,6 +966,10 @@ const Message = ({
animateUnreadReaction({ chatId, messageIds: [messageId] });
}
if (hasUnreadPollVote) {
markPollVotesRead({ chatId, messageIds: [messageId] });
}
let unreadMentionIds: number[] = [];
if (message.hasUnreadMention) {
unreadMentionIds = [messageId];
@ -975,7 +982,16 @@ const Message = ({
if (unreadMentionIds.length) {
markMentionsRead({ chatId, messageIds: unreadMentionIds });
}
}, [hasUnreadReaction, album, chatId, messageId, animateUnreadReaction, message.hasUnreadMention]);
}, [
hasUnreadReaction,
hasUnreadPollVote,
album,
chatId,
messageId,
animateUnreadReaction,
markPollVotesRead,
message.hasUnreadMention,
]);
const albumLayout = useMemo(() => {
return isAlbum
@ -1848,6 +1864,7 @@ const Message = ({
data-last-message-id={album ? album.messages[album.messages.length - 1].id : undefined}
data-album-main-id={album ? album.mainMessage.id : undefined}
data-has-unread-mention={message.hasUnreadMention || undefined}
data-has-unread-poll-vote={hasUnreadPollVote || undefined}
data-has-unread-reaction={hasUnreadReaction || undefined}
data-is-pinned={isPinned || undefined}
data-should-update-views={message.viewsCount !== undefined}
@ -2123,6 +2140,7 @@ export default memo(withGlobal<OwnProps>(
const readState = selectThreadReadState(global, chatId, threadId);
const hasUnreadReaction = readState?.unreadReactions?.includes(message.id);
const hasUnreadPollVote = readState?.unreadPollVotes?.includes(message.id);
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && !chat.isBotForum && isFirstInGroup;
const messageTopic = selectTopicFromMessage(global, message);
@ -2220,6 +2238,7 @@ export default memo(withGlobal<OwnProps>(
hasActiveReactions,
activeEmojiInteractions,
hasUnreadReaction,
hasUnreadPollVote,
isTranscribing: transcriptionId !== undefined && global.transcriptions[transcriptionId]?.isPending,
transcribedText: transcriptionId !== undefined ? global.transcriptions[transcriptionId]?.text : undefined,
isPremium,

View File

@ -116,6 +116,7 @@ export const GLOBAL_SUGGESTED_CHANNELS_ID = 'global';
// https://github.com/DrKLO/Telegram/blob/51e9947527/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java#L7781
export const REACTION_UNREAD_SLICE = 100;
export const MENTION_UNREAD_SLICE = 100;
export const POLL_UNREAD_SLICE = 100;
export const TOPICS_SLICE = 20;
export const TOPICS_SLICE_SECOND_LOAD = 500;

View File

@ -1461,6 +1461,7 @@ addActionHandler('markChatMessagesRead', async (global, actions, payload): Promi
await callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID });
actions.readAllMentions({ chatId: id });
actions.readAllReactions({ chatId: id });
actions.readAllPollVotes({ chatId: id });
if (chatReadState?.hasUnreadMark) {
actions.markChatRead({ id });
}
@ -1487,7 +1488,13 @@ addActionHandler('markChatMessagesRead', async (global, actions, payload): Promi
global = updateTopicWithState(global, id, topicWithState);
const { readState } = topicWithState;
if (readState && !readState.unreadCount && !readState.unreadMentionsCount && !readState.unreadReactionsCount) {
if (
readState
&& !readState.unreadCount
&& !readState.unreadMentionsCount
&& !readState.unreadReactionsCount
&& !readState.unreadPollVotesCount
) {
return;
}
@ -1530,6 +1537,7 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
});
actions.readAllMentions({ chatId, threadId: topicId });
actions.readAllReactions({ chatId, threadId: topicId });
actions.readAllPollVotes({ chatId, threadId: topicId });
global = getGlobal();
global = replaceThreadReadStateParam(global, chatId, topicId, 'lastReadInboxMessageId', lastTopicMessageId);

View File

@ -81,12 +81,12 @@ import {
} from '../../index';
import {
addChatMessagesById,
addUnreadMentions,
clearMessageSummary,
deleteSponsoredMessage,
removeOutlyingList,
removeRequestedMessageTranslation,
removeUnreadMentions,
removeUnreadPollVotes,
replaceSettings,
replaceUserStatuses,
safeReplacePinnedIds,
@ -106,6 +106,7 @@ import {
updateScheduledMessages,
updateSponsoredMessage,
updateTopicWithState,
updateUnreadCounters,
updateUploadByMessageKey,
updateUserFullInfo,
} from '../../reducers';
@ -172,7 +173,6 @@ import {
selectThreadReadState,
} from '../../selectors/threads';
import { deleteMessages, updateWithLocalMedia } from '../apiUpdaters/messages';
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
const uploadProgressCallbacks = new Map<MessageKey, ApiOnProgress>();
@ -2287,19 +2287,45 @@ addActionHandler('loadUnreadMentions', async (global, actions, payload): Promise
const { messages, topics, totalCount } = result;
const byId = buildCollectionByKey(messages, 'id');
const ids = Object.keys(byId).map(Number);
global = getGlobal();
global = addChatMessagesById(global, chat.id, byId);
topics.forEach((topicState) => {
global = updateTopicWithState(global, chat.id, topicState);
});
global = addUnreadMentions({
global = updateUnreadCounters({
global,
chatId,
ids,
threadId,
messages,
topics,
totalCount,
unreadCountKey: 'unreadMentionsCount',
});
setGlobal(global);
});
addActionHandler('loadUnreadPollVotes', async (global, actions, payload): Promise<void> => {
const { chatId, threadId = MAIN_THREAD_ID, offsetId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchUnreadPollVotes', {
chat,
threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined,
offsetId,
});
if (!result) return;
const { messages, topics, totalCount } = result;
global = getGlobal();
global = updateUnreadCounters({
global,
chatId,
threadId,
messages,
topics,
totalCount,
unreadCountKey: 'unreadPollVotesCount',
});
setGlobal(global);
@ -2391,6 +2417,21 @@ addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnTyp
actions.markMessagesRead({ chatId, messageIds });
});
addActionHandler('markPollVotesRead', (global, actions, payload): ActionReturnType => {
const { chatId, messageIds } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
global = removeUnreadPollVotes({
global,
chatId,
ids: messageIds,
});
setGlobal(global);
actions.markMessagesRead({ chatId, messageIds });
});
addActionHandler('focusNextMention', async (global, actions, payload): Promise<void> => {
const { chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId() } = payload;
@ -2407,6 +2448,28 @@ addActionHandler('focusNextMention', async (global, actions, payload): Promise<v
actions.focusMessage({ chatId, messageId: readState.unreadMentions[0], tabId });
});
addActionHandler('focusNextPollVote', async (global, actions, payload): Promise<void> => {
const { chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId() } = payload;
let readState = selectThreadReadState(global, chatId, threadId);
if (!readState?.unreadPollVotes?.length) {
await getPromiseActions().loadUnreadPollVotes({ chatId, threadId });
global = getGlobal();
readState = selectThreadReadState(global, chatId, threadId);
if (!readState?.unreadPollVotes?.length) return;
}
actions.focusMessage({
chatId,
threadId,
messageId: readState.unreadPollVotes[0],
tabId,
scrollTargetPosition: 'end',
});
});
addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType => {
const { chatId, threadId = MAIN_THREAD_ID } = payload;
@ -2422,6 +2485,21 @@ addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType
return global;
});
addActionHandler('readAllPollVotes', (global, actions, payload): ActionReturnType => {
const { chatId, threadId = MAIN_THREAD_ID } = payload;
const chat = selectChat(global, chatId);
if (!chat) return undefined;
callApi('readAllPollVotes', { chat, threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined });
global = updateThreadReadState(global, chatId, threadId, {
unreadPollVotesCount: 0,
unreadPollVotes: undefined,
});
return global;
});
addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
const {
url, shouldSkipModal, ignoreDeepLinks, linkContext, tabId = getCurrentTabId(),

View File

@ -1,6 +1,11 @@
import type { ApiError, ApiReaction, ApiReactionEmoji } from '../../../api/types';
import type { ActionReturnType } from '../../types';
import { ApiMediaFormat, MAIN_THREAD_ID } from '../../../api/types';
import {
type ApiError,
ApiMediaFormat,
type ApiReaction,
type ApiReactionEmoji,
MAIN_THREAD_ID,
} from '../../../api/types';
import { GENERAL_REFETCH_INTERVAL } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -21,13 +26,11 @@ import {
} from '../../helpers';
import { addActionHandler, getGlobal, getPromiseActions, setGlobal } from '../../index';
import {
addChatMessagesById,
updateChatMessage,
updateTopicWithState,
updateUnreadCounters,
} from '../../reducers';
import {
addMessageReaction,
addUnreadReactions,
removeUnreadReactions,
subtractXForEmojiInteraction,
} from '../../reducers/reactions';
@ -490,19 +493,15 @@ addActionHandler('loadUnreadReactions', async (global, actions, payload): Promis
const { messages, topics, totalCount } = result;
const byId = buildCollectionByKey(messages, 'id');
const ids = Object.keys(byId).map(Number);
global = getGlobal();
global = addChatMessagesById(global, chat.id, byId);
topics.forEach((topicState) => {
global = updateTopicWithState(global, chat.id, topicState);
});
global = addUnreadReactions({
global = updateUnreadCounters({
global,
chatId,
ids,
threadId,
messages,
topics,
totalCount,
unreadCountKey: 'unreadReactionsCount',
});
setGlobal(global);

View File

@ -8,12 +8,11 @@ import {
import { ARCHIVED_FOLDER_ID } from '../../config';
import { areDeepEqual } from '../../util/areDeepEqual';
import {
areSortedArraysEqual, buildCollectionByKey, omit, omitUndefined, pick, unique,
areSortedArraysEqual, buildCollectionByKey, omit, omitUndefined, pick,
} from '../../util/iteratees';
import { groupMessageIdsByThreadId } from '../helpers';
import { selectChatFullInfo } from '../selectors';
import { selectThreadReadState } from '../selectors/threads';
import { replaceThreadReadStateParam, updateThreadInfoLastMessageId } from './threads';
import { updateThreadInfoLastMessageId } from './threads';
import { addUnreadCount, removeUnreadCount } from './unreadCounters';
const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived'];
@ -132,28 +131,13 @@ export function addUnreadMentions<T extends GlobalState>({
ids: number[];
totalCount?: number;
}): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
for (const threadId in messageIdsByThreadId) {
const messageIds = messageIdsByThreadId[threadId];
if (totalCount !== undefined) { // Assume that when `totalCount` is passed, server returned full id list
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', messageIds);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', totalCount);
continue;
}
const readState = selectThreadReadState(global, chatId, threadId);
const prevChatUnreadMentions = readState?.unreadMentions || [];
const updatedUnreadMentions = unique([...prevChatUnreadMentions, ...messageIds]).sort((a, b) => b - a);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', updatedUnreadMentions);
const updatedUnreadMentionsCount = (readState?.unreadMentionsCount || 0)
+ Math.max(0, updatedUnreadMentions.length - prevChatUnreadMentions.length);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', updatedUnreadMentionsCount);
}
return global;
return addUnreadCount({
global,
chatId,
messageIds: ids,
totalCount,
unreadCountKey: 'unreadMentionsCount',
});
}
export function removeUnreadMentions<T extends GlobalState>({
@ -163,24 +147,12 @@ export function removeUnreadMentions<T extends GlobalState>({
chatId: string;
ids: number[];
}): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
for (const threadId in messageIdsByThreadId) {
const messageIds = messageIdsByThreadId[threadId];
const readState = selectThreadReadState(global, chatId, threadId);
const prevChatUnreadMentions = (readState?.unreadMentions || []);
const updatedUnreadMentions = prevChatUnreadMentions?.filter((id) => !messageIds.includes(id));
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', updatedUnreadMentions);
const removedCount = prevChatUnreadMentions.length - updatedUnreadMentions.length;
if (removedCount && readState?.unreadMentionsCount) {
const updatedUnreadMentionsCount = Math.max(readState.unreadMentionsCount - removedCount, 0);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', updatedUnreadMentionsCount);
}
}
return global;
return removeUnreadCount({
global,
chatId,
messageIds: ids,
unreadCountKey: 'unreadMentionsCount',
});
}
export function updateChat<T extends GlobalState>(

View File

@ -2,6 +2,7 @@ export * from './chats';
export * from './messages';
export * from './symbols';
export * from './users';
export * from './unreadCounters';
export * from './globalSearch';
export * from './middleSearch';
export * from './management';
@ -13,5 +14,6 @@ export * from './statistics';
export * from './stories';
export * from './translations';
export * from './peers';
export * from './polls';
export * from './topics';
export * from './gifts';

View File

@ -52,6 +52,7 @@ import {
import { selectThreadIdFromMessage, selectThreadInfo, selectThreadLocalStateParam } from '../selectors/threads';
import { removeUnreadMentions } from './chats';
import { removeIdFromSearchResults } from './middleSearch';
import { removeUnreadPollVotes } from './polls';
import { removeUnreadReactions } from './reactions';
import { updateTabState } from './tabs';
import {
@ -437,6 +438,7 @@ export function deleteChatMessages<T extends GlobalState>(
global = removeUnreadReactions({ global, chatId, ids: messageIds });
global = removeUnreadMentions({ global, chatId, ids: messageIds });
global = removeUnreadPollVotes({ global, chatId, ids: messageIds });
const newById = omit(byId, messageIds);
global = replaceChatMessages(global, chatId, newById);

View File

@ -0,0 +1,35 @@
import type { GlobalState } from '../types';
import { addUnreadCount, removeUnreadCount } from './unreadCounters';
export function addUnreadPollVotes<T extends GlobalState>({
global, chatId, ids, totalCount,
}: {
global: T;
chatId: string;
ids: number[];
totalCount?: number;
}): T {
return addUnreadCount({
global,
chatId,
messageIds: ids,
totalCount,
unreadCountKey: 'unreadPollVotesCount',
});
}
export function removeUnreadPollVotes<T extends GlobalState>({
global, chatId, ids,
}: {
global: T;
chatId: string;
ids: number[];
}): T {
return removeUnreadCount({
global,
chatId,
messageIds: ids,
unreadCountKey: 'unreadPollVotesCount',
});
}

View File

@ -2,17 +2,15 @@ import type { GlobalState } from '../types';
import { type ApiMessage, type ApiReactionWithPaid } from '../../api/types';
import { MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
import { unique } from '../../util/iteratees';
import windowSize from '../../util/windowSize';
import {
MIN_LEFT_COLUMN_WIDTH,
SIDE_COLUMN_MAX_WIDTH,
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
import { groupMessageIdsByThreadId, updateReactionCount } from '../helpers';
import { updateReactionCount } from '../helpers';
import { selectIsChatWithSelf, selectSendAs, selectTabState } from '../selectors';
import { selectThreadReadState } from '../selectors/threads';
import { updateChatMessage } from './messages';
import { replaceThreadReadStateParam } from './threads';
import { addUnreadCount, removeUnreadCount } from './unreadCounters';
import { getIsMobile } from '../../hooks/useAppLayout';
@ -85,29 +83,13 @@ export function addUnreadReactions<T extends GlobalState>({
ids: number[];
totalCount?: number;
}): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
for (const threadId in messageIdsByThreadId) {
const messageIds = messageIdsByThreadId[threadId];
if (totalCount !== undefined) { // Assume that when `totalCount` is passed, server returned full id list
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', messageIds);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', totalCount);
continue;
}
const readState = selectThreadReadState(global, chatId, threadId);
const prevChatUnreadReactions = readState?.unreadReactions || [];
const updatedUnreadReactions = unique([...prevChatUnreadReactions, ...messageIds]).sort((a, b) => b - a);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', updatedUnreadReactions);
const delta = updatedUnreadReactions.length - prevChatUnreadReactions.length;
if (delta > 0) {
const unreadReactionsCount = (readState?.unreadReactionsCount || 0) + delta;
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', unreadReactionsCount);
}
}
return global;
return addUnreadCount({
global,
chatId,
messageIds: ids,
totalCount,
unreadCountKey: 'unreadReactionsCount',
});
}
export function removeUnreadReactions<T extends GlobalState>({
@ -117,20 +99,10 @@ export function removeUnreadReactions<T extends GlobalState>({
chatId: string;
ids: number[];
}): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
for (const threadId in messageIdsByThreadId) {
const messageIds = messageIdsByThreadId[threadId];
const readState = selectThreadReadState(global, chatId, threadId);
const prevChatUnreadReactions = readState?.unreadReactions || [];
const updatedUnreadReactions = prevChatUnreadReactions.filter((id) => !messageIds.includes(id));
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', updatedUnreadReactions);
const delta = prevChatUnreadReactions.length - updatedUnreadReactions.length;
if (delta > 0 && readState?.unreadReactionsCount) {
const unreadReactionsCount = Math.max(readState.unreadReactionsCount - delta, 0);
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', unreadReactionsCount);
}
}
return global;
return removeUnreadCount({
global,
chatId,
messageIds: ids,
unreadCountKey: 'unreadReactionsCount',
});
}

View File

@ -0,0 +1,170 @@
import type { ApiMessage, ApiTopicWithState } from '../../api/types';
import type { ThreadId, ThreadReadState } from '../../types';
import type { GlobalState } from '../types';
import { buildCollectionByKey, unique } from '../../util/iteratees';
import { groupMessageIdsByThreadId } from '../helpers';
import { selectThreadReadState } from '../selectors/threads';
import { addChatMessagesById } from './messages';
import { replaceThreadReadStateParam } from './threads';
import { updateTopicWithState } from './topics';
type UnreadCountIdsKey = 'unreadMentions' | 'unreadReactions' | 'unreadPollVotes';
export type UnreadCountKey = 'unreadMentionsCount' | 'unreadReactionsCount' | 'unreadPollVotesCount';
type AddUnreadCountArgs<T extends GlobalState, TKey extends UnreadCountKey> = {
global: T;
chatId: string;
messageIds: number[];
totalCount?: number;
unreadCountKey: TKey;
};
type RemoveUnreadCountArgs<T extends GlobalState, TKey extends UnreadCountKey> = {
global: T;
chatId: string;
messageIds: number[];
unreadCountKey: TKey;
};
type UpdateUnreadCountersArgs<T extends GlobalState, TKey extends UnreadCountKey> = {
global: T;
chatId: string;
threadId: ThreadId;
messages: ApiMessage[];
topics: ApiTopicWithState[];
totalCount: number;
unreadCountKey: TKey;
};
const unreadIdsKeyByCountKey = {
unreadMentionsCount: 'unreadMentions',
unreadReactionsCount: 'unreadReactions',
unreadPollVotesCount: 'unreadPollVotes',
} satisfies Record<UnreadCountKey, UnreadCountIdsKey>;
function getUnreadIdsKey<TKey extends UnreadCountKey>(unreadCountKey: TKey) {
return unreadIdsKeyByCountKey[unreadCountKey];
}
function replaceThreadUnreadIds<T extends GlobalState, TKey extends UnreadCountKey>(
global: T,
chatId: string,
threadId: ThreadId,
unreadCountKey: TKey,
messageIds: number[] | undefined,
) {
return replaceThreadReadStateParam(global, chatId, threadId, getUnreadIdsKey(unreadCountKey), messageIds);
}
function replaceThreadUnreadCount<T extends GlobalState, TKey extends UnreadCountKey>(
global: T,
chatId: string,
threadId: ThreadId,
unreadCountKey: TKey,
count: number | undefined,
) {
return replaceThreadReadStateParam(global, chatId, threadId, unreadCountKey, count);
}
function selectThreadUnreadIds<TKey extends UnreadCountKey>(
readState: ThreadReadState | undefined,
unreadCountKey: TKey,
) {
return readState?.[getUnreadIdsKey(unreadCountKey)];
}
export function addUnreadCount<T extends GlobalState, TKey extends UnreadCountKey>({
global,
chatId,
messageIds,
totalCount,
unreadCountKey,
}: AddUnreadCountArgs<T, TKey>): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, messageIds, false);
for (const threadId in messageIdsByThreadId) {
const threadMessageIds = messageIdsByThreadId[threadId];
if (totalCount !== undefined) {
global = replaceThreadUnreadIds(global, chatId, threadId, unreadCountKey, threadMessageIds);
global = replaceThreadUnreadCount(global, chatId, threadId, unreadCountKey, totalCount);
continue;
}
const readState = selectThreadReadState(global, chatId, threadId);
const previousIds = selectThreadUnreadIds(readState, unreadCountKey) || [];
const updatedIds = unique([...previousIds, ...threadMessageIds]).sort((a, b) => b - a);
global = replaceThreadUnreadIds(global, chatId, threadId, unreadCountKey, updatedIds);
const delta = updatedIds.length - previousIds.length;
if (delta > 0) {
global = replaceThreadUnreadCount(
global,
chatId,
threadId,
unreadCountKey,
(readState?.[unreadCountKey] || 0) + delta,
);
}
}
return global;
}
export function removeUnreadCount<T extends GlobalState, TKey extends UnreadCountKey>({
global,
chatId,
messageIds,
unreadCountKey,
}: RemoveUnreadCountArgs<T, TKey>): T {
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, messageIds, false);
for (const threadId in messageIdsByThreadId) {
const threadMessageIds = messageIdsByThreadId[threadId];
const threadMessageIdSet = new Set(threadMessageIds);
const readState = selectThreadReadState(global, chatId, threadId);
const previousIds = selectThreadUnreadIds(readState, unreadCountKey) || [];
const updatedIds = previousIds.filter((id) => !threadMessageIdSet.has(id));
global = replaceThreadUnreadIds(global, chatId, threadId, unreadCountKey, updatedIds);
const previousCount = readState?.[unreadCountKey];
const delta = previousIds.length - updatedIds.length;
if (delta > 0 && previousCount) {
global = replaceThreadUnreadCount(
global,
chatId,
threadId,
unreadCountKey,
Math.max(previousCount - delta, 0),
);
}
}
return global;
}
export function updateUnreadCounters<T extends GlobalState, TKey extends UnreadCountKey>({
global,
chatId,
threadId,
messages,
topics,
totalCount,
unreadCountKey,
}: UpdateUnreadCountersArgs<T, TKey>): T {
const messagesById = buildCollectionByKey(messages, 'id');
global = addChatMessagesById(global, chatId, messagesById);
topics.forEach((topicState) => {
global = updateTopicWithState(global, chatId, topicState);
});
const messageIds = Object.keys(messagesById).map(Number).sort((a, b) => b - a);
global = replaceThreadUnreadIds(global, chatId, threadId, unreadCountKey, messageIds);
global = replaceThreadUnreadCount(global, chatId, threadId, unreadCountKey, totalCount);
return global;
}

View File

@ -1434,6 +1434,11 @@ export interface ActionPayloads {
threadId?: ThreadId;
offsetId?: number;
};
loadUnreadPollVotes: {
chatId: string;
threadId?: ThreadId;
offsetId?: number;
};
scheduleForViewsIncrement: {
chatId: string;
ids: number[];
@ -1468,6 +1473,10 @@ export interface ActionPayloads {
chatId: string;
threadId?: ThreadId;
} & WithTabId;
focusNextPollVote: {
chatId: string;
threadId?: ThreadId;
} & WithTabId;
readAllReactions: {
chatId: string;
threadId?: ThreadId;
@ -1476,10 +1485,18 @@ export interface ActionPayloads {
chatId: string;
threadId?: ThreadId;
};
readAllPollVotes: {
chatId: string;
threadId?: ThreadId;
};
markMentionsRead: {
chatId: string;
messageIds: number[];
};
markPollVotesRead: {
chatId: string;
messageIds: number[];
};
copyMessageLink: {
chatId: string;
messageId: number;

View File

@ -1812,6 +1812,8 @@ messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer r
messages.declineUrlAuth#35436bbc url:string = Bool;
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
messages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI;
messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
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

@ -224,6 +224,8 @@
"messages.clearRecentReactions",
"messages.readMentions",
"messages.getUnreadMentions",
"messages.readPollVotes",
"messages.getUnreadPollVotes",
"messages.getSavedDialogs",
"messages.getSavedHistory",
"messages.deleteSavedHistory",

File diff suppressed because it is too large Load Diff

View File

@ -24,330 +24,328 @@ $icons-map: (
"add-user": "\f106",
"add": "\f107",
"admin": "\f108",
"ai-edit": "\f109",
"ai-fix": "\f10a",
"ai": "\f10b",
"allow-share": "\f10c",
"allow-speak": "\f10d",
"animals": "\f10e",
"animations": "\f10f",
"archive-filled": "\f110",
"archive-from-main": "\f111",
"archive-to-main": "\f112",
"archive": "\f113",
"arrow-down-circle": "\f114",
"arrow-down": "\f115",
"arrow-left": "\f116",
"arrow-right": "\f117",
"ask-support": "\f118",
"attach": "\f119",
"auction-drop": "\f11a",
"auction-filled": "\f11b",
"auction-next-round": "\f11c",
"auction": "\f11d",
"author-hidden": "\f11e",
"avatar-archived-chats": "\f11f",
"avatar-deleted-account": "\f120",
"avatar-saved-messages": "\f121",
"bold": "\f122",
"boost-craft-chance": "\f123",
"boost-outline": "\f124",
"boost": "\f125",
"boostcircle": "\f126",
"boosts": "\f127",
"bot-command": "\f128",
"bot-commands-filled": "\f129",
"bots": "\f12a",
"brush": "\f12b",
"bug": "\f12c",
"calendar-filter": "\f12d",
"calendar": "\f12e",
"camera-add": "\f12f",
"camera": "\f130",
"car": "\f131",
"card": "\f132",
"cash-circle": "\f133",
"channel-filled": "\f134",
"channel": "\f135",
"channelviews": "\f136",
"chat-badge": "\f137",
"chats-badge": "\f138",
"check-bold": "\f139",
"check": "\f13a",
"clock-edit": "\f13b",
"clock": "\f13c",
"close-circle": "\f13d",
"close-topic": "\f13e",
"close": "\f13f",
"closed-gift": "\f140",
"cloud-download": "\f141",
"collapse-modal": "\f142",
"collapse": "\f143",
"colorize": "\f144",
"combine-craft": "\f145",
"comments-sticker": "\f146",
"comments": "\f147",
"copy-media": "\f148",
"copy": "\f149",
"craft": "\f14a",
"crop": "\f14b",
"crown-take-off-outline": "\f14c",
"crown-take-off": "\f14d",
"crown-wear-outline": "\f14e",
"crown-wear": "\f14f",
"darkmode": "\f150",
"data": "\f151",
"delete-filled": "\f152",
"delete-left": "\f153",
"delete-user": "\f154",
"delete": "\f155",
"diamond": "\f156",
"document": "\f157",
"double-badge": "\f158",
"down": "\f159",
"download": "\f15a",
"dropdown-arrows": "\f15b",
"eats": "\f15c",
"edit": "\f15d",
"email": "\f15e",
"enter": "\f15f",
"expand-modal": "\f160",
"expand": "\f161",
"eye-crossed-outline": "\f162",
"eye-crossed": "\f163",
"eye-outline": "\f164",
"eye": "\f165",
"favorite-filled": "\f166",
"favorite": "\f167",
"file-badge": "\f168",
"flag": "\f169",
"flip": "\f16a",
"folder-badge": "\f16b",
"folder-tabs-bot": "\f16c",
"folder-tabs-channel": "\f16d",
"folder-tabs-chat": "\f16e",
"folder-tabs-chats": "\f16f",
"folder-tabs-folder": "\f170",
"folder-tabs-group": "\f171",
"folder-tabs-star": "\f172",
"folder-tabs-user": "\f173",
"folder": "\f174",
"fontsize": "\f175",
"forums": "\f176",
"forward": "\f177",
"fragment": "\f178",
"frozen-time": "\f179",
"fullscreen": "\f17a",
"gifs": "\f17b",
"gift-transfer-inline": "\f17c",
"gift": "\f17d",
"group-filled": "\f17e",
"group": "\f17f",
"grouped-disable": "\f180",
"grouped": "\f181",
"hand-stop-filled": "\f182",
"hand-stop": "\f183",
"hashtag": "\f184",
"hd-photo": "\f185",
"heart-outline": "\f186",
"heart": "\f187",
"help": "\f188",
"info-filled": "\f189",
"info": "\f18a",
"install": "\f18b",
"italic": "\f18c",
"key": "\f18d",
"keyboard": "\f18e",
"lamp": "\f18f",
"language": "\f190",
"large-pause": "\f191",
"large-play": "\f192",
"link-badge": "\f193",
"link-broken": "\f194",
"link": "\f195",
"location": "\f196",
"lock-badge": "\f197",
"lock": "\f198",
"logout": "\f199",
"loop": "\f19a",
"mention": "\f19b",
"menu": "\f19c",
"message-failed": "\f19d",
"message-pending": "\f19e",
"message-read": "\f19f",
"message-succeeded": "\f1a0",
"message": "\f1a1",
"microphone-alt": "\f1a2",
"microphone": "\f1a3",
"monospace": "\f1a4",
"more-circle": "\f1a5",
"more": "\f1a6",
"move-caption-down": "\f1a7",
"move-caption-up": "\f1a8",
"mute": "\f1a9",
"muted": "\f1aa",
"my-notes": "\f1ab",
"new-chat-filled": "\f1ac",
"new-send": "\f1ad",
"next-link": "\f1ae",
"next": "\f1af",
"no-download": "\f1b0",
"no-share": "\f1b1",
"nochannel": "\f1b2",
"noise-suppression": "\f1b3",
"non-contacts": "\f1b4",
"note": "\f1b5",
"one-filled": "\f1b6",
"open-in-new-tab": "\f1b7",
"password-off": "\f1b8",
"pause": "\f1b9",
"permissions": "\f1ba",
"phone-discard-outline": "\f1bb",
"phone-discard": "\f1bc",
"phone": "\f1bd",
"photo": "\f1be",
"pin-badge": "\f1bf",
"pin-list": "\f1c0",
"pin": "\f1c1",
"pinned-chat": "\f1c2",
"pinned-message": "\f1c3",
"pip": "\f1c4",
"play-story": "\f1c5",
"play": "\f1c6",
"poll": "\f1c7",
"previous-link": "\f1c8",
"previous": "\f1c9",
"privacy-policy": "\f1ca",
"proof-of-ownership": "\f1cb",
"quote-text": "\f1cc",
"quote": "\f1cd",
"radial-badge": "\f1ce",
"rating-icons-level1": "\f1cf",
"rating-icons-level10": "\f1d0",
"rating-icons-level2": "\f1d1",
"rating-icons-level20": "\f1d2",
"rating-icons-level3": "\f1d3",
"rating-icons-level30": "\f1d4",
"rating-icons-level4": "\f1d5",
"rating-icons-level40": "\f1d6",
"rating-icons-level5": "\f1d7",
"rating-icons-level50": "\f1d8",
"rating-icons-level6": "\f1d9",
"rating-icons-level60": "\f1da",
"rating-icons-level7": "\f1db",
"rating-icons-level70": "\f1dc",
"rating-icons-level8": "\f1dd",
"rating-icons-level80": "\f1de",
"rating-icons-level9": "\f1df",
"rating-icons-level90": "\f1e0",
"rating-icons-negative": "\f1e1",
"readchats": "\f1e2",
"recent": "\f1e3",
"redo": "\f1e4",
"refund": "\f1e5",
"reload": "\f1e6",
"remove-quote": "\f1e7",
"remove": "\f1e8",
"reopen-topic": "\f1e9",
"reorder-tabs": "\f1ea",
"replace": "\f1eb",
"replies": "\f1ec",
"reply-filled": "\f1ed",
"reply": "\f1ee",
"revenue-split": "\f1ef",
"revote": "\f1f0",
"rotate": "\f1f1",
"save-story": "\f1f2",
"saved-messages": "\f1f3",
"schedule": "\f1f4",
"scheduled": "\f1f5",
"sd-photo": "\f1f6",
"search": "\f1f7",
"select-filled": "\f1f8",
"select": "\f1f9",
"sell-outline": "\f1fa",
"sell": "\f1fb",
"send-outline": "\f1fc",
"send": "\f1fd",
"settings-filled": "\f1fe",
"settings": "\f1ff",
"share-filled": "\f200",
"share-screen-outlined": "\f201",
"share-screen-stop": "\f202",
"share-screen": "\f203",
"show-message": "\f204",
"sidebar": "\f205",
"skip-next": "\f206",
"skip-previous": "\f207",
"smallscreen": "\f208",
"smile": "\f209",
"sort-by-date": "\f20a",
"sort-by-number": "\f20b",
"sort-by-price": "\f20c",
"sort": "\f20d",
"speaker-muted-story": "\f20e",
"speaker-outline": "\f20f",
"speaker-story": "\f210",
"speaker": "\f211",
"spoiler-disable": "\f212",
"spoiler": "\f213",
"sport": "\f214",
"star": "\f215",
"stars-lock": "\f216",
"stars-refund": "\f217",
"stats": "\f218",
"stealth-future": "\f219",
"stealth-past": "\f21a",
"stickers": "\f21b",
"stop-raising-hand": "\f21c",
"stop": "\f21d",
"story-caption": "\f21e",
"story-expired": "\f21f",
"story-priority": "\f220",
"story-reply": "\f221",
"strikethrough": "\f222",
"tag-add": "\f223",
"tag-crossed": "\f224",
"tag-filter": "\f225",
"tag-name": "\f226",
"tag": "\f227",
"timer": "\f228",
"toncoin": "\f229",
"tools": "\f22a",
"topic-new": "\f22b",
"trade": "\f22c",
"transcribe": "\f22d",
"truck": "\f22e",
"unarchive": "\f22f",
"underlined": "\f230",
"understood": "\f231",
"undo": "\f232",
"unique-profile": "\f233",
"unlist-outline": "\f234",
"unlist": "\f235",
"unlock-badge": "\f236",
"unlock": "\f237",
"unmute": "\f238",
"unpin": "\f239",
"unread": "\f23a",
"up": "\f23b",
"user-filled": "\f23c",
"user-online": "\f23d",
"user-stars": "\f23e",
"user-tag": "\f23f",
"user": "\f240",
"video-outlined": "\f241",
"video-stop": "\f242",
"video": "\f243",
"view-once": "\f244",
"voice-chat": "\f245",
"volume-1": "\f246",
"volume-2": "\f247",
"volume-3": "\f248",
"warning": "\f249",
"web": "\f24a",
"webapp": "\f24b",
"word-wrap": "\f24c",
"zoom-in": "\f24d",
"zoom-out": "\f24e",
"allow-share": "\f109",
"allow-speak": "\f10a",
"animals": "\f10b",
"animations": "\f10c",
"archive-filled": "\f10d",
"archive-from-main": "\f10e",
"archive-to-main": "\f10f",
"archive": "\f110",
"arrow-down-circle": "\f111",
"arrow-down": "\f112",
"arrow-left": "\f113",
"arrow-right": "\f114",
"ask-support": "\f115",
"attach": "\f116",
"auction-drop": "\f117",
"auction-filled": "\f118",
"auction-next-round": "\f119",
"auction": "\f11a",
"author-hidden": "\f11b",
"avatar-archived-chats": "\f11c",
"avatar-deleted-account": "\f11d",
"avatar-saved-messages": "\f11e",
"bold": "\f11f",
"boost-craft-chance": "\f120",
"boost-outline": "\f121",
"boost": "\f122",
"boostcircle": "\f123",
"boosts": "\f124",
"bot-command": "\f125",
"bot-commands-filled": "\f126",
"bots": "\f127",
"brush": "\f128",
"bug": "\f129",
"calendar-filter": "\f12a",
"calendar": "\f12b",
"camera-add": "\f12c",
"camera": "\f12d",
"car": "\f12e",
"card": "\f12f",
"cash-circle": "\f130",
"channel-filled": "\f131",
"channel": "\f132",
"channelviews": "\f133",
"chat-badge": "\f134",
"chats-badge": "\f135",
"check-bold": "\f136",
"check": "\f137",
"clock-edit": "\f138",
"clock": "\f139",
"close-circle": "\f13a",
"close-topic": "\f13b",
"close": "\f13c",
"closed-gift": "\f13d",
"cloud-download": "\f13e",
"collapse-modal": "\f13f",
"collapse": "\f140",
"colorize": "\f141",
"combine-craft": "\f142",
"comments-sticker": "\f143",
"comments": "\f144",
"copy-media": "\f145",
"copy": "\f146",
"craft": "\f147",
"crop": "\f148",
"crown-take-off-outline": "\f149",
"crown-take-off": "\f14a",
"crown-wear-outline": "\f14b",
"crown-wear": "\f14c",
"darkmode": "\f14d",
"data": "\f14e",
"delete-filled": "\f14f",
"delete-left": "\f150",
"delete-user": "\f151",
"delete": "\f152",
"diamond": "\f153",
"document": "\f154",
"double-badge": "\f155",
"down": "\f156",
"download": "\f157",
"dropdown-arrows": "\f158",
"eats": "\f159",
"edit": "\f15a",
"email": "\f15b",
"enter": "\f15c",
"expand-modal": "\f15d",
"expand": "\f15e",
"eye-crossed-outline": "\f15f",
"eye-crossed": "\f160",
"eye-outline": "\f161",
"eye": "\f162",
"favorite-filled": "\f163",
"favorite": "\f164",
"file-badge": "\f165",
"flag": "\f166",
"flip": "\f167",
"folder-badge": "\f168",
"folder-tabs-bot": "\f169",
"folder-tabs-channel": "\f16a",
"folder-tabs-chat": "\f16b",
"folder-tabs-chats": "\f16c",
"folder-tabs-folder": "\f16d",
"folder-tabs-group": "\f16e",
"folder-tabs-star": "\f16f",
"folder-tabs-user": "\f170",
"folder": "\f171",
"fontsize": "\f172",
"forums": "\f173",
"forward": "\f174",
"fragment": "\f175",
"frozen-time": "\f176",
"fullscreen": "\f177",
"gifs": "\f178",
"gift-transfer-inline": "\f179",
"gift": "\f17a",
"group-filled": "\f17b",
"group": "\f17c",
"grouped-disable": "\f17d",
"grouped": "\f17e",
"hand-stop-filled": "\f17f",
"hand-stop": "\f180",
"hashtag": "\f181",
"hd-photo": "\f182",
"heart-outline": "\f183",
"heart": "\f184",
"help": "\f185",
"info-filled": "\f186",
"info": "\f187",
"install": "\f188",
"italic": "\f189",
"key": "\f18a",
"keyboard": "\f18b",
"lamp": "\f18c",
"language": "\f18d",
"large-pause": "\f18e",
"large-play": "\f18f",
"link-badge": "\f190",
"link-broken": "\f191",
"link": "\f192",
"location": "\f193",
"lock-badge": "\f194",
"lock": "\f195",
"logout": "\f196",
"loop": "\f197",
"mention": "\f198",
"menu": "\f199",
"message-failed": "\f19a",
"message-pending": "\f19b",
"message-read": "\f19c",
"message-succeeded": "\f19d",
"message": "\f19e",
"microphone-alt": "\f19f",
"microphone": "\f1a0",
"monospace": "\f1a1",
"more-circle": "\f1a2",
"more": "\f1a3",
"move-caption-down": "\f1a4",
"move-caption-up": "\f1a5",
"mute": "\f1a6",
"muted": "\f1a7",
"my-notes": "\f1a8",
"new-chat-filled": "\f1a9",
"new-send": "\f1aa",
"next-link": "\f1ab",
"next": "\f1ac",
"no-download": "\f1ad",
"no-share": "\f1ae",
"nochannel": "\f1af",
"noise-suppression": "\f1b0",
"non-contacts": "\f1b1",
"note": "\f1b2",
"one-filled": "\f1b3",
"open-in-new-tab": "\f1b4",
"password-off": "\f1b5",
"pause": "\f1b6",
"permissions": "\f1b7",
"phone-discard-outline": "\f1b8",
"phone-discard": "\f1b9",
"phone": "\f1ba",
"photo": "\f1bb",
"pin-badge": "\f1bc",
"pin-list": "\f1bd",
"pin": "\f1be",
"pinned-chat": "\f1bf",
"pinned-message": "\f1c0",
"pip": "\f1c1",
"play-story": "\f1c2",
"play": "\f1c3",
"poll-badge": "\f1c4",
"poll": "\f1c5",
"previous-link": "\f1c6",
"previous": "\f1c7",
"privacy-policy": "\f1c8",
"proof-of-ownership": "\f1c9",
"quote-text": "\f1ca",
"quote": "\f1cb",
"radial-badge": "\f1cc",
"rating-icons-level1": "\f1cd",
"rating-icons-level10": "\f1ce",
"rating-icons-level2": "\f1cf",
"rating-icons-level20": "\f1d0",
"rating-icons-level3": "\f1d1",
"rating-icons-level30": "\f1d2",
"rating-icons-level4": "\f1d3",
"rating-icons-level40": "\f1d4",
"rating-icons-level5": "\f1d5",
"rating-icons-level50": "\f1d6",
"rating-icons-level6": "\f1d7",
"rating-icons-level60": "\f1d8",
"rating-icons-level7": "\f1d9",
"rating-icons-level70": "\f1da",
"rating-icons-level8": "\f1db",
"rating-icons-level80": "\f1dc",
"rating-icons-level9": "\f1dd",
"rating-icons-level90": "\f1de",
"rating-icons-negative": "\f1df",
"readchats": "\f1e0",
"recent": "\f1e1",
"redo": "\f1e2",
"refund": "\f1e3",
"reload": "\f1e4",
"remove-quote": "\f1e5",
"remove": "\f1e6",
"reopen-topic": "\f1e7",
"reorder-tabs": "\f1e8",
"replace": "\f1e9",
"replies": "\f1ea",
"reply-filled": "\f1eb",
"reply": "\f1ec",
"revenue-split": "\f1ed",
"revote": "\f1ee",
"rotate": "\f1ef",
"save-story": "\f1f0",
"saved-messages": "\f1f1",
"schedule": "\f1f2",
"scheduled": "\f1f3",
"sd-photo": "\f1f4",
"search": "\f1f5",
"select-filled": "\f1f6",
"select": "\f1f7",
"sell-outline": "\f1f8",
"sell": "\f1f9",
"send-outline": "\f1fa",
"send": "\f1fb",
"settings-filled": "\f1fc",
"settings": "\f1fd",
"share-filled": "\f1fe",
"share-screen-outlined": "\f1ff",
"share-screen-stop": "\f200",
"share-screen": "\f201",
"show-message": "\f202",
"sidebar": "\f203",
"skip-next": "\f204",
"skip-previous": "\f205",
"smallscreen": "\f206",
"smile": "\f207",
"sort-by-date": "\f208",
"sort-by-number": "\f209",
"sort-by-price": "\f20a",
"sort": "\f20b",
"speaker-muted-story": "\f20c",
"speaker-outline": "\f20d",
"speaker-story": "\f20e",
"speaker": "\f20f",
"spoiler-disable": "\f210",
"spoiler": "\f211",
"sport": "\f212",
"star": "\f213",
"stars-lock": "\f214",
"stars-refund": "\f215",
"stats": "\f216",
"stealth-future": "\f217",
"stealth-past": "\f218",
"stickers": "\f219",
"stop-raising-hand": "\f21a",
"stop": "\f21b",
"story-caption": "\f21c",
"story-expired": "\f21d",
"story-priority": "\f21e",
"story-reply": "\f21f",
"strikethrough": "\f220",
"tag-add": "\f221",
"tag-crossed": "\f222",
"tag-filter": "\f223",
"tag-name": "\f224",
"tag": "\f225",
"timer": "\f226",
"toncoin": "\f227",
"tools": "\f228",
"topic-new": "\f229",
"trade": "\f22a",
"transcribe": "\f22b",
"truck": "\f22c",
"unarchive": "\f22d",
"underlined": "\f22e",
"understood": "\f22f",
"undo": "\f230",
"unique-profile": "\f231",
"unlist-outline": "\f232",
"unlist": "\f233",
"unlock-badge": "\f234",
"unlock": "\f235",
"unmute": "\f236",
"unpin": "\f237",
"unread": "\f238",
"up": "\f239",
"user-filled": "\f23a",
"user-online": "\f23b",
"user-stars": "\f23c",
"user-tag": "\f23d",
"user": "\f23e",
"video-outlined": "\f23f",
"video-stop": "\f240",
"video": "\f241",
"view-once": "\f242",
"voice-chat": "\f243",
"volume-1": "\f244",
"volume-2": "\f245",
"volume-3": "\f246",
"warning": "\f247",
"web": "\f248",
"webapp": "\f249",
"word-wrap": "\f24a",
"zoom-in": "\f24b",
"zoom-out": "\f24c",
);

Binary file not shown.

Binary file not shown.

View File

@ -197,6 +197,7 @@ export type FontIconName =
| 'pip'
| 'play-story'
| 'play'
| 'poll-badge'
| 'poll'
| 'previous-link'
| 'previous'

View File

@ -623,8 +623,10 @@ export interface ThreadReadState {
unreadCount?: number;
unreadMentionsCount?: number;
unreadReactionsCount?: number;
unreadPollVotesCount?: number;
unreadReactions?: number[];
unreadMentions?: number[];
unreadPollVotes?: number[];
hasUnreadMark?: boolean;
lastReadOutboxMessageId?: number;

View File

@ -91,6 +91,7 @@ export interface LangPair {
'AnonymousPoll': undefined;
'AccDescrReactionMentionDown': undefined;
'AccDescrMentionDown': undefined;
'AccDescrPollVoteDown': undefined;
'AccDescrPageDown': undefined;
'ChannelPrivate': undefined;
'ChannelPrivateInfo': undefined;