Poll: Refactor update handling (#5090)

This commit is contained in:
zubiden 2024-11-02 21:11:50 +04:00 committed by Alexander Zinchuk
parent 6ab84ff12f
commit c5c87347d9
38 changed files with 444 additions and 247 deletions

View File

@ -58,7 +58,7 @@ export function buildMessageContent(
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported; const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
if (mtpMessage.message && !hasUnsupportedMedia if (mtpMessage.message && !hasUnsupportedMedia
&& !content.sticker && !content.poll && !content.contact && !content.video?.isRound) { && !content.sticker && !content.pollId && !content.contact && !content.video?.isRound) {
content = { content = {
...content, ...content,
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities), text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
@ -130,8 +130,8 @@ export function buildMessageMediaContent(
const contact = buildContact(media); const contact = buildContact(media);
if (contact) return { contact }; if (contact) return { contact };
const poll = buildPollFromMedia(media); const pollId = buildPollIdFromMedia(media);
if (poll) return { poll }; if (pollId) return { pollId };
const webPage = buildWebPage(media); const webPage = buildWebPage(media);
if (webPage) return { webPage }; if (webPage) return { webPage };
@ -465,7 +465,15 @@ function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined {
}; };
} }
function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined { function buildPollIdFromMedia(media: GramJs.TypeMessageMedia): string | undefined {
if (!(media instanceof GramJs.MessageMediaPoll)) {
return undefined;
}
return media.poll.id.toString();
}
export function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined {
if (!(media instanceof GramJs.MessageMediaPoll)) { if (!(media instanceof GramJs.MessageMediaPoll)) {
return undefined; return undefined;
} }

View File

@ -935,30 +935,30 @@ export function buildLocalMessage(
story?: ApiStory | ApiStorySkipped, story?: ApiStory | ApiStorySkipped,
isInvertedMedia?: true, isInvertedMedia?: true,
effectId?: string, effectId?: string,
): ApiMessage { ) {
const localId = getNextLocalMessageId(lastMessageId); const localId = getNextLocalMessageId(lastMessageId);
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
const isChannel = chat.type === 'chatTypeChannel'; const isChannel = chat.type === 'chatTypeChannel';
const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum); const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
const localPoll = poll && buildNewPoll(poll, localId);
const message = { const message = {
id: localId, id: localId,
chatId: chat.id, chatId: chat.id,
content: { content: omitUndefined({
...(text && { text: text ? {
text: { text,
text, entities,
entities, } : undefined,
},
}),
...media, ...media,
...(sticker && { sticker }), sticker,
...(gif && { video: gif }), video: gif || media?.video,
...(poll && { poll: buildNewPoll(poll, localId) }), contact,
...(contact && { contact }), storyData: story && { mediaType: 'storyData', ...story },
...(story && { storyData: { mediaType: 'storyData', ...story } }), pollId: localPoll?.id,
}, }),
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(), date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
isOutgoing: !isChannel, isOutgoing: !isChannel,
senderId: sendAs?.id || currentUserId, senderId: sendAs?.id || currentUserId,
@ -975,10 +975,15 @@ export function buildLocalMessage(
const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId); const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId);
return { const finalMessage = {
...message, ...message,
...(emojiOnlyCount && { emojiOnlyCount }), ...(emojiOnlyCount && { emojiOnlyCount }),
}; };
return {
message: finalMessage,
poll: localPoll,
};
} }
export function buildLocalForwardedMessage({ export function buildLocalForwardedMessage({

View File

@ -293,7 +293,10 @@ export function sendMessage(
}, },
onProgress?: ApiOnProgress, onProgress?: ApiOnProgress,
) { ) {
const localMessage = buildLocalMessage( const {
message: localMessage,
poll: localPoll,
} = buildLocalMessage(
chat, chat,
lastMessageId, lastMessageId,
text, text,
@ -317,6 +320,7 @@ export function sendMessage(
id: localMessage.id, id: localMessage.id,
chatId: chat.id, chatId: chat.id,
message: localMessage, message: localMessage,
poll: localPoll,
wasDrafted, wasDrafted,
}); });

View File

@ -1,9 +1,12 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiChat, ApiThreadInfo, ApiUser } from '../../types'; import type {
ApiChat, ApiPoll, ApiThreadInfo, ApiUser,
} from '../../types';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildPollFromMedia } from '../apiBuilders/messageContent';
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages'; import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers'; import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers';
@ -19,7 +22,8 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
let userById: Record<string, ApiUser> | undefined; let userById: Record<string, ApiUser> | undefined;
let chatById: Record<string, ApiChat> | undefined; let chatById: Record<string, ApiChat> | undefined;
let threadInfos: ApiThreadInfo[] | undefined; const threadInfos: ApiThreadInfo[] | undefined = [];
const polls: ApiPoll[] | undefined = [];
if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) { if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) {
const users = response.users.map((user) => { const users = response.users.map((user) => {
@ -42,19 +46,29 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
} }
if ('messages' in response && Array.isArray(response.messages) && TYPE_MESSAGE.has(response.messages[0]?.className)) { if ('messages' in response && Array.isArray(response.messages) && TYPE_MESSAGE.has(response.messages[0]?.className)) {
threadInfos = response.messages.map((message) => { response.messages.forEach((message) => {
addMessageToLocalDb(message); addMessageToLocalDb(message);
return buildApiThreadInfoFromMessage(message);
}).filter(Boolean); const threadInfo = buildApiThreadInfoFromMessage(message);
if (threadInfo) {
threadInfos.push(threadInfo);
}
const poll = buildPollFromMedia(message.media);
if (poll) {
polls.push(poll);
}
});
} }
if (!userById && !chatById && !threadInfos) return; if (!userById && !chatById && !threadInfos?.length) return;
sendImmediateApiUpdate({ sendImmediateApiUpdate({
'@type': 'updateEntities', '@type': 'updateEntities',
users: userById, users: userById,
chats: chatById, chats: chatById,
threadInfos, threadInfos: threadInfos?.length ? threadInfos : undefined,
polls: polls?.length ? polls : undefined,
}); });
} }

View File

@ -9,7 +9,9 @@ import type {
ApiPremiumGiftCodeOption, ApiPremiumGiftCodeOption,
ApiStarGift, ApiStarGift,
} from './payments'; } from './payments';
import type { ApiMessageStoryData, ApiWebPageStickerData, ApiWebPageStoryData } from './stories'; import type {
ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData,
} from './stories';
import type { ApiUser } from './users'; import type { ApiUser } from './users';
export interface ApiDimensions { export interface ApiDimensions {
@ -667,7 +669,7 @@ export type MediaContent = {
document?: ApiDocument; document?: ApiDocument;
sticker?: ApiSticker; sticker?: ApiSticker;
contact?: ApiContact; contact?: ApiContact;
poll?: ApiPoll; pollId?: string;
action?: ApiAction; action?: ApiAction;
webPage?: ApiWebPage; webPage?: ApiWebPage;
audio?: ApiAudio; audio?: ApiAudio;
@ -687,6 +689,11 @@ export type MediaContainer = {
content: MediaContent; content: MediaContent;
}; };
export type StatefulMediaContent = {
poll?: ApiPoll;
story?: ApiStory;
};
export type BoughtPaidMedia = Pick<MediaContent, 'photo' | 'video'>; export type BoughtPaidMedia = Pick<MediaContent, 'photo' | 'video'>;
export interface ApiMessage { export interface ApiMessage {

View File

@ -204,6 +204,7 @@ export type ApiUpdateNewScheduledMessage = {
id: number; id: number;
message: ApiMessage; message: ApiMessage;
wasDrafted?: boolean; wasDrafted?: boolean;
poll?: ApiPoll;
}; };
export type ApiUpdateNewMessage = { export type ApiUpdateNewMessage = {
@ -213,6 +214,7 @@ export type ApiUpdateNewMessage = {
message: Partial<ApiMessage>; message: Partial<ApiMessage>;
shouldForceReply?: boolean; shouldForceReply?: boolean;
wasDrafted?: boolean; wasDrafted?: boolean;
poll?: ApiPoll;
}; };
export type ApiUpdateMessage = { export type ApiUpdateMessage = {
@ -764,6 +766,7 @@ export type ApiUpdateEntities = {
users?: Record<string, ApiUser>; users?: Record<string, ApiUser>;
chats?: Record<string, ApiChat>; chats?: Record<string, ApiChat>;
threadInfos?: ApiThreadInfo[]; threadInfos?: ApiThreadInfo[];
polls?: ApiPoll[];
}; };
export type ApiUpdatePaidReactionPrivacy = { export type ApiUpdatePaidReactionPrivacy = {

View File

@ -1,12 +1,16 @@
import React, { memo } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiFormattedText, ApiMessage } from '../../api/types'; import type {
ApiFormattedText, ApiMessage, ApiPoll, ApiTypeStory,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { ApiMessageEntityTypes } from '../../api/types'; import { ApiMessageEntityTypes } from '../../api/types';
import { import {
extractMessageText, extractMessageText,
getMessagePoll, getMessagePollId,
groupStatetefulContent,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
getMessageSummaryDescription, getMessageSummaryDescription,
@ -14,6 +18,7 @@ import {
getMessageSummaryText, getMessageSummaryText,
TRUNCATED_SUMMARY_LENGTH, TRUNCATED_SUMMARY_LENGTH,
} from '../../global/helpers/messageSummary'; } from '../../global/helpers/messageSummary';
import { selectPeerStory, selectPollFromMessage } from '../../global/selectors';
import trimText from '../../util/trimText'; import trimText from '../../util/trimText';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -21,7 +26,7 @@ import useOldLang from '../../hooks/useOldLang';
import MessageText from './MessageText'; import MessageText from './MessageText';
interface OwnProps { type OwnProps = {
message: ApiMessage; message: ApiMessage;
translatedText?: ApiFormattedText; translatedText?: ApiFormattedText;
noEmoji?: boolean; noEmoji?: boolean;
@ -32,7 +37,12 @@ interface OwnProps {
emojiSize?: number; emojiSize?: number;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
} };
type StateProps = {
poll?: ApiPoll;
story?: ApiTypeStory;
};
function MessageSummary({ function MessageSummary({
message, message,
@ -43,17 +53,22 @@ function MessageSummary({
withTranslucentThumbs = false, withTranslucentThumbs = false,
inChatList = false, inChatList = false,
emojiSize, emojiSize,
poll,
story,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
}: OwnProps) { }: OwnProps & StateProps) {
const lang = useOldLang(); const lang = useOldLang();
const { text, entities } = extractMessageText(message, inChatList) || {}; const { text, entities } = extractMessageText(message, inChatList) || {};
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji); const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
const hasPoll = Boolean(getMessagePoll(message)); const hasPoll = Boolean(getMessagePollId(message));
const statefulContent = groupStatetefulContent({ poll, story });
if ((!text || (!hasSpoilers && !hasCustomEmoji)) && !hasPoll) { if ((!text || (!hasSpoilers && !hasCustomEmoji)) && !hasPoll) {
const summaryText = translatedText?.text || getMessageSummaryText(lang, message, noEmoji, truncateLength); const summaryText = translatedText?.text
|| getMessageSummaryText(lang, message, statefulContent, noEmoji, truncateLength);
const trimmedText = trimText(summaryText, truncateLength); const trimmedText = trimText(summaryText, truncateLength);
return ( return (
@ -90,10 +105,21 @@ function MessageSummary({
<> <>
{[ {[
emoji ? renderText(`${emoji} `) : undefined, emoji ? renderText(`${emoji} `) : undefined,
getMessageSummaryDescription(lang, message, renderMessageText()), getMessageSummaryDescription(lang, message, statefulContent, renderMessageText()),
].flat().filter(Boolean)} ].flat().filter(Boolean)}
</> </>
); );
} }
export default memo(MessageSummary); export default memo(withGlobal<OwnProps>(
(global, { message }): StateProps => {
const poll = selectPollFromMessage(global, message);
const storyData = message.content.storyData;
const story = storyData && selectPeerStory(global, storyData.peerId, storyData.id);
return {
poll,
story,
};
},
)(MessageSummary));

View File

@ -164,7 +164,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
function renderMediaContentType(media?: MediaContainer) { function renderMediaContentType(media?: MediaContainer) {
if (!media || media.content.text) return NBSP; if (!media || media.content.text) return NBSP;
const description = getMediaContentTypeDescription(lang, media.content); const description = getMediaContentTypeDescription(lang, media.content, {});
if (!description || description === CONTENT_NOT_SUPPORTED) return NBSP; if (!description || description === CONTENT_NOT_SUPPORTED) return NBSP;
return ( return (
<span> <span>

View File

@ -278,7 +278,7 @@ function renderMessageContent(
const { asPlainText, isEmbedded } = options; const { asPlainText, isEmbedded } = options;
if (asPlainText) { if (asPlainText) {
return getMessageSummaryText(lang, message, undefined, MAX_LENGTH); return getMessageSummaryText(lang, message, undefined, undefined, MAX_LENGTH);
} }
const messageSummary = ( const messageSummary = (

View File

@ -1,9 +1,12 @@
import { getGlobal } from '../../../global';
import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types'; import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types';
import type { LangFn } from '../../../hooks/useOldLang'; import type { LangFn } from '../../../hooks/useOldLang';
import type { TextPart } from '../../../types'; import type { TextPart } from '../../../types';
import { ApiMessageEntityTypes } from '../../../api/types'; import { ApiMessageEntityTypes } from '../../../api/types';
import { import {
getMessageStatefulContent,
getMessageText, getMessageText,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { import {
@ -70,10 +73,13 @@ export function renderMessageSummary(
): TextPart[] { ): TextPart[] {
const { entities } = message.content.text || {}; const { entities } = message.content.text || {};
const global = getGlobal();
const statefulContent = getMessageStatefulContent(global, message);
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji); const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
if (!hasSpoilers && !hasCustomEmoji) { if (!hasSpoilers && !hasCustomEmoji) {
const text = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength); const text = trimText(getMessageSummaryText(lang, message, statefulContent, noEmoji), truncateLength);
if (highlight) { if (highlight) {
return renderText(text, ['emoji', 'highlight'], { highlight }); return renderText(text, ['emoji', 'highlight'], { highlight });
@ -88,7 +94,7 @@ export function renderMessageSummary(
const text = renderMessageText({ const text = renderMessageText({
message, highlight, isSimple: true, truncateLength, message, highlight, isSimple: true, truncateLength,
}); });
const description = getMessageSummaryDescription(lang, message, text); const description = getMessageSummaryDescription(lang, message, statefulContent, text);
return [ return [
...renderText(emojiWithSpace), ...renderText(emojiWithSpace),

View File

@ -8,6 +8,7 @@ import type {
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiPeer,
ApiTopic, ApiTopic,
ApiTypeStory,
ApiTypingStatus, ApiTypingStatus,
ApiUser, ApiUser,
ApiUserStatus, ApiUserStatus,
@ -21,6 +22,7 @@ import { StoryViewerOrigin } from '../../../types';
import { import {
getMessageAction, getMessageAction,
getPrivateChatUserId, getPrivateChatUserId,
groupStatetefulContent,
isUserId, isUserId,
isUserOnline, isUserOnline,
selectIsChatMuted, selectIsChatMuted,
@ -40,6 +42,7 @@ import {
selectNotifySettings, selectNotifySettings,
selectOutgoingStatus, selectOutgoingStatus,
selectPeer, selectPeer,
selectPeerStory,
selectTabState, selectTabState,
selectThreadParam, selectThreadParam,
selectTopicFromMessage, selectTopicFromMessage,
@ -92,6 +95,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
lastMessageStory?: ApiTypeStory;
listedTopicIds?: number[]; listedTopicIds?: number[];
topics?: Record<number, ApiTopic>; topics?: Record<number, ApiTopic>;
isMuted?: boolean; isMuted?: boolean;
@ -127,6 +131,7 @@ const Chat: FC<OwnProps & StateProps> = ({
topics, topics,
observeIntersection, observeIntersection,
chat, chat,
lastMessageStory,
isMuted, isMuted,
user, user,
userStatus, userStatus,
@ -187,6 +192,7 @@ const Chat: FC<OwnProps & StateProps> = ({
lastMessage, lastMessage,
typingStatus, typingStatus,
draft, draft,
statefulMediaContent: groupStatetefulContent({ story: lastMessageStory }),
actionTargetMessage, actionTargetMessage,
actionTargetUserIds, actionTargetUserIds,
actionTargetChatId, actionTargetChatId,
@ -483,6 +489,9 @@ export default memo(withGlobal<OwnProps>(
const topicsInfo = selectTopicsInfo(global, chatId); const topicsInfo = selectTopicsInfo(global, chatId);
const storyData = lastMessage?.content.storyData;
const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id);
return { return {
chat, chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)), isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
@ -510,6 +519,7 @@ export default memo(withGlobal<OwnProps>(
listedTopicIds: topicsInfo?.listedTopicIds, listedTopicIds: topicsInfo?.listedTopicIds,
topics: topicsInfo?.topicsById, topics: topicsInfo?.topicsById,
isSynced: global.isSynced, isSynced: global.isSynced,
lastMessageStory,
}; };
}, },
)(Chat)); )(Chat));

View File

@ -4,13 +4,13 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiMessage, ApiMessageOutgoingStatus,
ApiPeer, ApiTopic, ApiTypingStatus, ApiPeer, ApiTopic, ApiTypeStory, ApiTypingStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { ApiDraft } from '../../../global/types'; import type { ApiDraft } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { getMessageAction } from '../../../global/helpers'; import { getMessageAction, groupStatetefulContent } from '../../../global/helpers';
import { getMessageReplyInfo } from '../../../global/helpers/replies'; import { getMessageReplyInfo } from '../../../global/helpers/replies';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
@ -20,6 +20,7 @@ import {
selectCurrentMessageList, selectCurrentMessageList,
selectDraft, selectDraft,
selectOutgoingStatus, selectOutgoingStatus,
selectPeerStory,
selectThreadInfo, selectThreadInfo,
selectThreadParam, selectThreadParam,
selectTopics, selectTopics,
@ -59,6 +60,7 @@ type StateProps = {
chat: ApiChat; chat: ApiChat;
canDelete?: boolean; canDelete?: boolean;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
lastMessageStory?: ApiTypeStory;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
@ -79,6 +81,7 @@ const Topic: FC<OwnProps & StateProps> = ({
chat, chat,
style, style,
lastMessage, lastMessage,
lastMessageStory,
canScrollDown, canScrollDown,
lastMessageOutgoingStatus, lastMessageOutgoingStatus,
observeIntersection, observeIntersection,
@ -142,6 +145,7 @@ const Topic: FC<OwnProps & StateProps> = ({
isTopic: true, isTopic: true,
typingStatus, typingStatus,
topics, topics,
statefulMediaContent: groupStatetefulContent({ story: lastMessageStory }),
animationType, animationType,
withInterfaceAnimations, withInterfaceAnimations,
@ -262,6 +266,9 @@ export default memo(withGlobal<OwnProps>(
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
const storyData = lastMessage?.content.storyData;
const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id);
return { return {
chat, chat,
lastMessage, lastMessage,
@ -279,6 +286,7 @@ export default memo(withGlobal<OwnProps>(
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id, canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
wasTopicOpened, wasTopicOpened,
topics, topics,
lastMessageStory,
}; };
}, },
)(Topic)); )(Topic));

View File

@ -5,6 +5,7 @@ import { getGlobal } from '../../../../global';
import type { import type {
ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser,
StatefulMediaContent,
} from '../../../../api/types'; } from '../../../../api/types';
import type { ApiDraft } from '../../../../global/types'; import type { ApiDraft } from '../../../../global/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
@ -32,6 +33,7 @@ import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEn
import { ChatAnimationTypes } from './useChatAnimationType'; import { ChatAnimationTypes } from './useChatAnimationType';
import useEnsureMessage from '../../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../../hooks/useEnsureMessage';
import useEnsureStory from '../../../../hooks/useEnsureStory';
import useMedia from '../../../../hooks/useMedia'; import useMedia from '../../../../hooks/useMedia';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
@ -45,6 +47,7 @@ export default function useChatListEntry({
chat, chat,
topics, topics,
lastMessage, lastMessage,
statefulMediaContent,
chatId, chatId,
typingStatus, typingStatus,
draft, draft,
@ -64,6 +67,7 @@ export default function useChatListEntry({
chat?: ApiChat; chat?: ApiChat;
topics?: Record<number, ApiTopic>; topics?: Record<number, ApiTopic>;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
statefulMediaContent: StatefulMediaContent | undefined;
chatId: string; chatId: string;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
draft?: ApiDraft; draft?: ApiDraft;
@ -90,10 +94,16 @@ export default function useChatListEntry({
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage); useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage);
const mediaHasPreview = lastMessage && !getMessageSticker(lastMessage); const storyData = lastMessage?.content.storyData;
const shouldTryLoadingStory = statefulMediaContent && !statefulMediaContent.story;
const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(lastMessage) : undefined; useEnsureStory(shouldTryLoadingStory ? storyData?.peerId : undefined, storyData?.id, statefulMediaContent?.story);
const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(lastMessage, 'micro') : undefined);
const mediaContent = statefulMediaContent?.story || lastMessage;
const mediaHasPreview = mediaContent && !getMessageSticker(mediaContent);
const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(mediaContent) : undefined;
const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(mediaContent, 'micro') : undefined);
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage)); const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
const actionTargetUsers = useMemo(() => { const actionTargetUsers = useMemo(() => {

View File

@ -8,10 +8,12 @@ import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiChatReactions, ApiChatReactions,
ApiMessage, ApiMessage,
ApiPoll,
ApiReaction, ApiReaction,
ApiStickerSet, ApiStickerSet,
ApiStickerSetInfo, ApiStickerSetInfo,
ApiThreadInfo, ApiThreadInfo,
ApiTypeStory,
} from '../../../api/types'; } from '../../../api/types';
import type { ActiveDownloads, MessageListType } from '../../../global/types'; import type { ActiveDownloads, MessageListType } from '../../../global/types';
import type { IAlbum, IAnchorPosition, ThreadId } from '../../../types'; import type { IAlbum, IAnchorPosition, ThreadId } from '../../../types';
@ -50,6 +52,8 @@ import {
selectIsReactionPickerOpen, selectIsReactionPickerOpen,
selectMessageCustomEmojiSets, selectMessageCustomEmojiSets,
selectMessageTranslations, selectMessageTranslations,
selectPeerStory,
selectPollFromMessage,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
selectStickerSet, selectStickerSet,
@ -88,6 +92,8 @@ export type OwnProps = {
type StateProps = { type StateProps = {
threadId?: ThreadId; threadId?: ThreadId;
poll?: ApiPoll;
story?: ApiTypeStory;
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
topReactions?: ApiReaction[]; topReactions?: ApiReaction[];
defaultTagReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[];
@ -150,6 +156,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
customEmojiSetsInfo, customEmojiSetsInfo,
customEmojiSets, customEmojiSets,
album, album,
poll,
story,
anchor, anchor,
targetHref, targetHref,
noOptions, noOptions,
@ -642,6 +650,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
seenByRecentPeers={seenByRecentPeers} seenByRecentPeers={seenByRecentPeers}
isInSavedMessages={isInSavedMessages} isInSavedMessages={isInSavedMessages}
noReplies={noReplies} noReplies={noReplies}
poll={poll}
story={story}
onOpenThread={handleOpenThread} onOpenThread={handleOpenThread}
onReply={handleReply} onReply={handleReply}
onEdit={handleEdit} onEdit={handleEdit}
@ -795,6 +805,10 @@ export default memo(withGlobal<OwnProps>(
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId); const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
const poll = selectPollFromMessage(global, message);
const storyData = message.content.storyData;
const story = storyData ? selectPeerStory(global, storyData.peerId, storyData.id) : undefined;
return { return {
threadId, threadId,
availableReactions, availableReactions,
@ -845,6 +859,8 @@ export default memo(withGlobal<OwnProps>(
isChannel, isChannel,
canReplyInChat, canReplyInChat,
isWithPaidReaction: chatFullInfo?.isPaidReactionAvailable, isWithPaidReaction: chatFullInfo?.isPaidReactionAvailable,
poll,
story,
}; };
}, },
)(ContextMenuContainer)); )(ContextMenuContainer));

View File

@ -19,6 +19,7 @@ import type {
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiPeer,
ApiPoll,
ApiReaction, ApiReaction,
ApiReactionKey, ApiReactionKey,
ApiSavedReactionTag, ApiSavedReactionTag,
@ -90,6 +91,7 @@ import {
selectPeer, selectPeer,
selectPeerStory, selectPeerStory,
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
selectPollFromMessage,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
selectSender, selectSender,
@ -295,6 +297,7 @@ type StateProps = {
viaBusinessBot?: ApiUser; viaBusinessBot?: ApiUser;
effect?: ApiAvailableEffect; effect?: ApiAvailableEffect;
availableStars?: number; availableStars?: number;
poll?: ApiPoll;
}; };
type MetaPosition = type MetaPosition =
@ -416,6 +419,7 @@ const Message: FC<OwnProps & StateProps> = ({
viaBusinessBot, viaBusinessBot,
effect, effect,
availableStars, availableStars,
poll,
onIntersectPinnedMessage, onIntersectPinnedMessage,
}) => { }) => {
const { const {
@ -505,7 +509,7 @@ const Message: FC<OwnProps & StateProps> = ({
const { const {
photo = paidMediaPhoto, video = paidMediaVideo, audio, photo = paidMediaPhoto, video = paidMediaVideo, audio,
voice, document, sticker, contact, voice, document, sticker, contact,
poll, webPage, invoice, location, webPage, invoice, location,
action, game, storyData, giveaway, action, game, storyData, giveaway,
giveawayResults, giveawayResults,
} = getMessageContent(message); } = getMessageContent(message);
@ -741,6 +745,7 @@ const Message: FC<OwnProps & StateProps> = ({
&& (isCustomShape || ((photo || video || storyData || (location?.mediaType === 'geo')) && !hasText)); && (isCustomShape || ((photo || video || storyData || (location?.mediaType === 'geo')) && !hasText));
const contentClassName = buildContentClassName(message, album, { const contentClassName = buildContentClassName(message, album, {
poll,
hasSubheader, hasSubheader,
isCustomShape, isCustomShape,
isLastInGroup, isLastInGroup,
@ -1818,6 +1823,7 @@ export default memo(withGlobal<OwnProps>(
const effect = effectId ? global.availableEffectById[effectId] : undefined; const effect = effectId ? global.availableEffectById[effectId] : undefined;
const { balance: availableStars } = global.stars || {}; const { balance: availableStars } = global.stars || {};
const poll = selectPollFromMessage(global, message);
return { return {
theme: selectTheme(global), theme: selectTheme(global),
@ -1906,6 +1912,7 @@ export default memo(withGlobal<OwnProps>(
viaBusinessBot, viaBusinessBot,
effect, effect,
availableStars, availableStars,
poll,
}; };
}, },
)(Message)); )(Message));

View File

@ -10,15 +10,17 @@ import type {
ApiChatReactions, ApiChatReactions,
ApiMessage, ApiMessage,
ApiPeer, ApiPeer,
ApiPoll,
ApiReaction, ApiReaction,
ApiSponsoredMessage, ApiSponsoredMessage,
ApiStickerSet, ApiStickerSet,
ApiThreadInfo, ApiThreadInfo,
ApiTypeStory,
ApiUser, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { IAnchorPosition } from '../../../types'; import type { IAnchorPosition } from '../../../types';
import { getUserFullName, isUserId } from '../../../global/helpers'; import { getUserFullName, groupStatetefulContent, isUserId } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { disableScrolling } from '../../../util/scrollLock'; import { disableScrolling } from '../../../util/scrollLock';
import { REM } from '../../common/helpers/mediaDimensions'; import { REM } from '../../common/helpers/mediaDimensions';
@ -49,6 +51,8 @@ type OwnProps = {
anchor: IAnchorPosition; anchor: IAnchorPosition;
targetHref?: string; targetHref?: string;
message: ApiMessage | ApiSponsoredMessage; message: ApiMessage | ApiSponsoredMessage;
poll?: ApiPoll;
story?: ApiTypeStory;
canSendNow?: boolean; canSendNow?: boolean;
enabledReactions?: ApiChatReactions; enabledReactions?: ApiChatReactions;
isWithPaidReaction?: boolean; isWithPaidReaction?: boolean;
@ -138,6 +142,8 @@ const MessageContextMenu: FC<OwnProps> = ({
defaultTagReactions, defaultTagReactions,
isOpen, isOpen,
message, message,
poll,
story,
isPrivate, isPrivate,
isCurrentUserPremium, isCurrentUserPremium,
enabledReactions, enabledReactions,
@ -282,6 +288,7 @@ const MessageContextMenu: FC<OwnProps> = ({
? [] ? []
: getMessageCopyOptions( : getMessageCopyOptions(
message, message,
groupStatetefulContent({ poll, story }),
targetHref, targetHref,
canCopy, canCopy,
handleAfterCopy, handleAfterCopy,

View File

@ -39,9 +39,14 @@
.Checkbox, .Checkbox,
.Radio { .Radio {
padding-left: 2.25rem; padding-left: 2.25rem;
padding-bottom: 1rem;
&:last-child { &:last-child {
margin-bottom: 0.75rem; margin-bottom: 0;
}
&:first-child {
margin-top: 0;
} }
&.disabled { &.disabled {

View File

@ -7,7 +7,7 @@ import React, {
useRef, useRef,
useState, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import type { import type {
ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer, ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
@ -15,6 +15,7 @@ import type {
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../hooks/useOldLang'; import type { LangFn } from '../../../hooks/useOldLang';
import { selectPeer } from '../../../global/selectors';
import { formatMediaDuration } from '../../../util/dates/dateFormat'; import { formatMediaDuration } from '../../../util/dates/dateFormat';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
@ -39,10 +40,6 @@ type OwnProps = {
onSendVote: (options: string[]) => void; onSendVote: (options: string[]) => void;
}; };
type StateProps = {
recentVoterIds?: number[];
};
const SOLUTION_CONTAINER_ID = '#middle-column-portals'; const SOLUTION_CONTAINER_ID = '#middle-column-portals';
const SOLUTION_DURATION = 5000; const SOLUTION_DURATION = 5000;
const TIMER_RADIUS = 6; const TIMER_RADIUS = 6;
@ -50,10 +47,9 @@ const TIMER_CIRCUMFERENCE = TIMER_RADIUS * 2 * Math.PI;
const TIMER_UPDATE_INTERVAL = 1000; const TIMER_UPDATE_INTERVAL = 1000;
const NBSP = '\u00A0'; const NBSP = '\u00A0';
const Poll: FC<OwnProps & StateProps> = ({ const Poll: FC<OwnProps> = ({
message, message,
poll, poll,
recentVoterIds,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
onSendVote, onSendVote,
@ -80,6 +76,7 @@ const Poll: FC<OwnProps & StateProps> = ({
const canVote = !summary.closed && !hasVoted; const canVote = !summary.closed && !hasVoted;
const canViewResult = !canVote && summary.isPublic && Number(results.totalVoters) > 0; const canViewResult = !canVote && summary.isPublic && Number(results.totalVoters) > 0;
const isMultiple = canVote && summary.multipleChoice; const isMultiple = canVote && summary.multipleChoice;
const recentVoterIds = results.recentVoterIds;
const maxVotersCount = voteResults ? Math.max(...voteResults.map((r) => r.votersCount)) : totalVoters; const maxVotersCount = voteResults ? Math.max(...voteResults.map((r) => r.votersCount)) : totalVoters;
const correctResults = useMemo(() => { const correctResults = useMemo(() => {
return voteResults?.filter((r) => r.isCorrect).map((r) => r.option) || []; return voteResults?.filter((r) => r.isCorrect).map((r) => r.option) || [];
@ -147,15 +144,11 @@ const Poll: FC<OwnProps & StateProps> = ({
const recentVoters = useMemo(() => { const recentVoters = useMemo(() => {
// No need for expensive global updates on chats or users, so we avoid them // No need for expensive global updates on chats or users, so we avoid them
const chatsById = getGlobal().chats.byId; const global = getGlobal();
const usersById = getGlobal().users.byId;
return recentVoterIds ? recentVoterIds.reduce((result: ApiPeer[], id) => { return recentVoterIds ? recentVoterIds.reduce((result: ApiPeer[], id) => {
const chat = chatsById[id]; const peer = selectPeer(global, id);
const user = usersById[id]; if (peer) {
if (user) { result.push(peer);
result.push(user);
} else if (chat) {
result.push(chat);
} }
return result; return result;
@ -372,17 +365,4 @@ function stopPropagation(e: React.MouseEvent<HTMLDivElement>) {
e.stopPropagation(); e.stopPropagation();
} }
export default memo(withGlobal<OwnProps>( export default memo(Poll);
(global, { poll }) => {
const { recentVoterIds } = poll.results;
const { users: { byId: usersById } } = global;
if (!recentVoterIds || recentVoterIds.length === 0) {
return {};
}
return {
recentVoterIds,
usersById,
};
},
)(Poll));

View File

@ -44,8 +44,8 @@
background: var(--color-error); background: var(--color-error);
} }
.is-forwarded & > .icon { .poll-option-icon {
margin-left: 1px; line-height: 1rem;
} }
&.animate { &.animate {

View File

@ -9,6 +9,8 @@ import type { ApiPollAnswer, ApiPollResult } from '../../../api/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import Icon from '../../common/icons/Icon';
import './PollOption.scss'; import './PollOption.scss';
type OwnProps = { type OwnProps = {
@ -59,7 +61,7 @@ const PollOption: FC<OwnProps> = ({
shouldAnimate && 'animate', shouldAnimate && 'animate',
)} )}
> >
<i className={buildClassName('icon', correctAnswer ? 'icon-check' : 'icon-close')} /> <Icon name={correctAnswer ? 'check' : 'close'} className="poll-option-icon" />
</span> </span>
)} )}
</div> </div>

View File

@ -1,4 +1,4 @@
import type { ApiMessage } from '../../../../api/types'; import type { ApiMessage, ApiPoll } from '../../../../api/types';
import type { IAlbum } from '../../../../types'; import type { IAlbum } from '../../../../types';
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config'; import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
@ -9,6 +9,7 @@ export function buildContentClassName(
message: ApiMessage, message: ApiMessage,
album?: IAlbum, album?: IAlbum,
{ {
poll,
hasSubheader, hasSubheader,
isCustomShape, isCustomShape,
isLastInGroup, isLastInGroup,
@ -23,6 +24,7 @@ export function buildContentClassName(
peerColorClass, peerColorClass,
hasOutsideReactions, hasOutsideReactions,
}: { }: {
poll?: ApiPoll;
hasSubheader?: boolean; hasSubheader?: boolean;
isCustomShape?: boolean | number; isCustomShape?: boolean | number;
isLastInGroup?: boolean; isLastInGroup?: boolean;
@ -44,7 +46,7 @@ export function buildContentClassName(
const content = getMessageContent(message); const content = getMessageContent(message);
const { const {
photo = paidMediaPhoto, video = paidMediaVideo, photo = paidMediaPhoto, video = paidMediaVideo,
audio, voice, document, poll, webPage, contact, location, invoice, storyData, audio, voice, document, webPage, contact, location, invoice, storyData,
giveaway, giveawayResults, giveaway, giveawayResults,
} = content; } = content;
const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text; const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text;

View File

@ -1,4 +1,4 @@
import type { ApiMessage } from '../../../../api/types'; import type { ApiMessage, StatefulMediaContent } from '../../../../api/types';
import type { IconName } from '../../../../types/icons'; import type { IconName } from '../../../../types/icons';
import { ApiMediaFormat } from '../../../../api/types'; import { ApiMediaFormat } from '../../../../api/types';
@ -32,6 +32,7 @@ type ICopyOptions = {
export function getMessageCopyOptions( export function getMessageCopyOptions(
message: ApiMessage, message: ApiMessage,
statefulContent: StatefulMediaContent | undefined,
href?: string, href?: string,
canCopy?: boolean, canCopy?: boolean,
afterEffect?: () => void, afterEffect?: () => void,
@ -94,7 +95,12 @@ export function getMessageCopyOptions(
const clipboardText = renderMessageText( const clipboardText = renderMessageText(
{ message, shouldRenderAsHtml: true }, { message, shouldRenderAsHtml: true },
); );
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!); if (clipboardText) {
copyHtmlToClipboard(
clipboardText.join(''),
getMessageTextWithSpoilers(message, statefulContent)!,
);
}
} }
afterEffect?.(); afterEffect?.();

View File

@ -2,10 +2,11 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global'; import { withGlobal } from '../../global';
import type { ApiChat, ApiMessage } from '../../api/types'; import type { ApiChat, ApiMessage, ApiPoll } from '../../api/types';
import { getMessagePoll } from '../../global/helpers'; import {
import { selectChat, selectChatMessage, selectTabState } from '../../global/selectors'; selectChat, selectChatMessage, selectPollFromMessage, selectTabState,
} from '../../global/selectors';
import { buildCollectionByKey } from '../../util/iteratees'; import { buildCollectionByKey } from '../../util/iteratees';
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
@ -25,12 +26,14 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
message?: ApiMessage; message?: ApiMessage;
poll?: ApiPoll;
}; };
const PollResults: FC<OwnProps & StateProps> = ({ const PollResults: FC<OwnProps & StateProps> = ({
isActive, isActive,
chat, chat,
message, message,
poll,
onClose, onClose,
}) => { }) => {
const lang = useOldLang(); const lang = useOldLang();
@ -40,11 +43,11 @@ const PollResults: FC<OwnProps & StateProps> = ({
onBack: onClose, onBack: onClose,
}); });
if (!message || !chat) { if (!message || !poll || !chat) {
return <Loading />; return <Loading />;
} }
const { summary, results } = getMessagePoll(message)!; const { summary, results } = poll;
if (!results.results) { if (!results.results) {
return undefined; return undefined;
} }
@ -62,7 +65,7 @@ const PollResults: FC<OwnProps & StateProps> = ({
<div className="poll-results-list custom-scroll"> <div className="poll-results-list custom-scroll">
{summary.answers.map((answer) => ( {summary.answers.map((answer) => (
<PollAnswerResults <PollAnswerResults
key={`${message.id}-${answer.option}`} key={`${poll.id}-${answer.option}`}
chat={chat} chat={chat}
message={message} message={message}
answer={answer} answer={answer}
@ -87,10 +90,12 @@ export default memo(withGlobal(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chatId, messageId);
const poll = message && selectPollFromMessage(global, message);
return { return {
chat, chat,
message, message,
poll,
}; };
}, },
)(PollResults)); )(PollResults));

View File

@ -120,6 +120,7 @@ import {
selectOutlyingListByMessageId, selectOutlyingListByMessageId,
selectPeerStory, selectPeerStory,
selectPinnedIds, selectPinnedIds,
selectPollFromMessage,
selectRealLastReadId, selectRealLastReadId,
selectReplyCanBeSentToChat, selectReplyCanBeSentToChat,
selectScheduledMessage, selectScheduledMessage,
@ -977,7 +978,7 @@ addActionHandler('clearWebPagePreview', (global, actions, payload): ActionReturn
}); });
addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType => { addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, options } = payload!; const { chatId, messageId, options } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (chat) { if (chat) {
@ -986,7 +987,7 @@ addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType =>
}); });
addActionHandler('cancelPollVote', (global, actions, payload): ActionReturnType => { addActionHandler('cancelPollVote', (global, actions, payload): ActionReturnType => {
const { chatId, messageId } = payload!; const { chatId, messageId } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (chat) { if (chat) {
@ -997,7 +998,8 @@ addActionHandler('cancelPollVote', (global, actions, payload): ActionReturnType
addActionHandler('closePoll', (global, actions, payload): ActionReturnType => { addActionHandler('closePoll', (global, actions, payload): ActionReturnType => {
const { chatId, messageId } = payload; const { chatId, messageId } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const poll = selectChatMessage(global, chatId, messageId)?.content.poll; const message = selectChatMessage(global, chatId, messageId);
const poll = message && selectPollFromMessage(global, message);
if (chat && poll) { if (chat && poll) {
void callApi('closePoll', { chat, messageId, poll }); void callApi('closePoll', { chat, messageId, poll });
} }
@ -1006,7 +1008,7 @@ addActionHandler('closePoll', (global, actions, payload): ActionReturnType => {
addActionHandler('loadPollOptionResults', async (global, actions, payload): Promise<void> => { addActionHandler('loadPollOptionResults', async (global, actions, payload): Promise<void> => {
const { const {
chat, messageId, option, offset, limit, shouldResetVoters, tabId = getCurrentTabId(), chat, messageId, option, offset, limit, shouldResetVoters, tabId = getCurrentTabId(),
} = payload!; } = payload;
const result = await callApi('loadPollOptionResults', { const result = await callApi('loadPollOptionResults', {
chat, messageId, option, offset, limit, chat, messageId, option, offset, limit,
@ -1103,7 +1105,7 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
serviceMessages serviceMessages
.forEach((message) => { .forEach((message) => {
const { text, entities } = message.content.text || {}; const { text, entities } = message.content.text || {};
const { sticker, poll } = message.content; const { sticker } = message.content;
const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId); const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId);
@ -1113,7 +1115,6 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
text, text,
entities, entities,
sticker, sticker,
poll,
isSilent, isSilent,
scheduledAt, scheduledAt,
sendAs, sendAs,

View File

@ -1,5 +1,5 @@
import type { import type {
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiPollResult, ApiReactions, ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiReactions,
MediaContent, MediaContent,
} from '../../../api/types'; } from '../../../api/types';
import type { ThreadId } from '../../../types'; import type { ThreadId } from '../../../types';
@ -43,6 +43,8 @@ import {
updateChatMessage, updateChatMessage,
updateListedIds, updateListedIds,
updateMessageTranslations, updateMessageTranslations,
updatePoll,
updatePollVote,
updateQuickReplies, updateQuickReplies,
updateQuickReplyMessage, updateQuickReplyMessage,
updateScheduledMessage, updateScheduledMessage,
@ -57,7 +59,6 @@ import {
selectChat, selectChat,
selectChatLastMessageId, selectChatLastMessageId,
selectChatMessage, selectChatMessage,
selectChatMessageByPollId,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
selectCommonBoxChatId, selectCommonBoxChatId,
@ -74,7 +75,6 @@ import {
selectSavedDialogIdFromMessage, selectSavedDialogIdFromMessage,
selectScheduledIds, selectScheduledIds,
selectScheduledMessage, selectScheduledMessage,
selectSendAs,
selectTabState, selectTabState,
selectThreadByMessage, selectThreadByMessage,
selectThreadIdFromMessage, selectThreadIdFromMessage,
@ -90,7 +90,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'newMessage': { case 'newMessage': {
const { const {
chatId, id, message, shouldForceReply, wasDrafted, chatId, id, message, shouldForceReply, wasDrafted, poll,
} = update; } = update;
global = updateWithLocalMedia(global, chatId, id, message); global = updateWithLocalMedia(global, chatId, id, message);
global = updateListedAndViewportIds(global, actions, message as ApiMessage); global = updateListedAndViewportIds(global, actions, message as ApiMessage);
@ -154,6 +154,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
}); });
if (poll) {
global = updatePoll(global, poll.id, poll);
}
setGlobal(global); setGlobal(global);
// Reload dialogs if chat is not present in the list // Reload dialogs if chat is not present in the list
@ -208,7 +212,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
case 'newScheduledMessage': { case 'newScheduledMessage': {
const { chatId, id, message } = update; const {
chatId, id, message, poll,
} = update;
global = updateWithLocalMedia(global, chatId, id, message, true); global = updateWithLocalMedia(global, chatId, id, message, true);
@ -221,6 +227,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', unique([...threadScheduledIds, id])); global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', unique([...threadScheduledIds, id]));
} }
if (poll) {
global = updatePoll(global, poll.id, poll);
}
setGlobal(global); setGlobal(global);
break; break;
@ -560,97 +570,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateMessagePoll': { case 'updateMessagePoll': {
const { pollId, pollUpdate } = update; const { pollId, pollUpdate } = update;
const message = selectChatMessageByPollId(global, pollId); global = updatePoll(global, pollId, pollUpdate);
if (message?.content.poll) { setGlobal(global);
const oldResults = message.content.poll.results;
let newResults = oldResults;
if (pollUpdate.results?.results) {
if (!oldResults.results || !pollUpdate.results.isMin) {
newResults = pollUpdate.results;
} else if (oldResults.results) {
newResults = {
...pollUpdate.results,
results: pollUpdate.results.results.map((result) => ({
...result,
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
})),
isMin: undefined,
};
}
}
const updatedPoll = { ...message.content.poll, ...pollUpdate, results: newResults };
global = updateChatMessage(
global,
message.chatId,
message.id,
{
content: {
...message.content,
poll: updatedPoll,
},
},
);
setGlobal(global);
}
break; break;
} }
case 'updateMessagePollVote': { case 'updateMessagePollVote': {
const { pollId, peerId, options } = update; const { pollId, peerId, options } = update;
const message = selectChatMessageByPollId(global, pollId); global = updatePollVote(global, pollId, peerId, options);
if (!message || !message.content.poll || !message.content.poll.results) {
break;
}
const { poll } = message.content;
const currentSendAs = selectSendAs(global, message.chatId);
const { recentVoterIds, totalVoters, results } = poll.results;
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
const newResults = results ? [...results] : [];
newRecentVoterIds.push(peerId);
options.forEach((option) => {
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
const targetOption = newResults[targetOptionIndex];
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
updatedOption.votersCount += 1;
if (currentSendAs?.id === peerId || peerId === global.currentUserId) {
updatedOption.isChosen = true;
}
if (targetOptionIndex) {
newResults[targetOptionIndex] = updatedOption;
} else {
newResults.push(updatedOption);
}
});
global = updateChatMessage(
global,
message.chatId,
message.id,
{
content: {
...message.content,
poll: {
...poll,
results: {
...poll.results,
recentVoterIds: newRecentVoterIds,
totalVoters: newTotalVoters,
results: newResults,
},
},
},
},
);
setGlobal(global); setGlobal(global);
break; break;

View File

@ -14,6 +14,7 @@ import {
updateLastReadStoryForPeer, updateLastReadStoryForPeer,
updatePeerStory, updatePeerStory,
updatePeersWithStories, updatePeersWithStories,
updatePoll,
updateStealthMode, updateStealthMode,
updateThreadInfos, updateThreadInfos,
} from '../../reducers'; } from '../../reducers';
@ -22,10 +23,17 @@ import { selectPeerStories, selectPeerStory } from '../../selectors';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'updateEntities': { case 'updateEntities': {
const { users, chats, threadInfos } = update; const {
users, chats, threadInfos, polls,
} = update;
if (users) global = addUsers(global, users); if (users) global = addUsers(global, users);
if (chats) global = addChats(global, chats); if (chats) global = addChats(global, chats);
if (threadInfos) global = updateThreadInfos(global, threadInfos); if (threadInfos) global = updateThreadInfos(global, threadInfos);
if (polls) {
polls.forEach((poll) => {
global = updatePoll(global, poll.id, poll);
});
}
setGlobal(global); setGlobal(global);
break; break;
} }

View File

@ -29,6 +29,7 @@ import {
getMediaFormat, getMediaFormat,
getMediaHash, getMediaHash,
getMessageDownloadableMedia, getMessageDownloadableMedia,
getMessageStatefulContent,
getSenderTitle, getSenderTitle,
isChatChannel, isChatChannel,
isJoinedChannelMessage, isJoinedChannelMessage,
@ -985,12 +986,13 @@ function copyTextForMessages(global: GlobalState, chatId: string, messageIds: nu
messages.forEach((message) => { messages.forEach((message) => {
const sender = isChatChannel(chat) ? chat : selectSender(global, message); const sender = isChatChannel(chat) ? chat : selectSender(global, message);
const senderTitle = `> ${sender ? getSenderTitle(lang, sender) : message.forwardInfo?.hiddenUserName || ''}:`; const senderTitle = `> ${sender ? getSenderTitle(lang, sender) : message.forwardInfo?.hiddenUserName || ''}:`;
const statefulContent = getMessageStatefulContent(global, message);
resultHtml.push(senderTitle); resultHtml.push(senderTitle);
resultHtml.push(`${renderMessageSummaryHtml(lang, message)}\n`); resultHtml.push(`${renderMessageSummaryHtml(lang, message)}\n`);
resultText.push(senderTitle); resultText.push(senderTitle);
resultText.push(`${getMessageSummaryText(lang, message, false, 0, true)}\n`); resultText.push(`${getMessageSummaryText(lang, message, statefulContent, false, 0, true)}\n`);
}); });
copyHtmlToClipboard(resultHtml.join('\n'), resultText.join('\n')); copyHtmlToClipboard(resultHtml.join('\n'), resultText.join('\n'));

View File

@ -257,6 +257,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.chats.topicsInfoById = initialState.chats.topicsInfoById; cached.chats.topicsInfoById = initialState.chats.topicsInfoById;
} }
if (!cached.messages.pollById) {
cached.messages.pollById = initialState.messages.pollById;
}
if (!cached.stickers.starGifts) { if (!cached.stickers.starGifts) {
cached.stickers.starGifts = initialState.stickers.starGifts; cached.stickers.starGifts = initialState.stickers.starGifts;
cached.users.giftsById = initialState.users.giftsById; cached.users.giftsById = initialState.users.giftsById;
@ -495,6 +499,8 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
return acc; return acc;
}, {} as Record<string, Set<ThreadId>>); }, {} as Record<string, Set<ThreadId>>);
const pollIdsToSave: string[] = [];
chatIdsToSave.forEach((chatId) => { chatIdsToSave.forEach((chatId) => {
const current = global.messages.byChatId[chatId]; const current = global.messages.byChatId[chatId];
if (!current) { if (!current) {
@ -537,6 +543,11 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
let cleanedMessage = omitLocalMedia(message); let cleanedMessage = omitLocalMedia(message);
cleanedMessage = omitLocalPaidReactions(cleanedMessage); cleanedMessage = omitLocalPaidReactions(cleanedMessage);
acc[message.id] = cleanedMessage; acc[message.id] = cleanedMessage;
if (message.content.pollId) {
pollIdsToSave.push(message.content.pollId);
}
return acc; return acc;
}, {} as Record<number, ApiMessage>); }, {} as Record<number, ApiMessage>);
@ -548,6 +559,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
return { return {
byChatId, byChatId,
pollById: pickTruthy(global.messages.pollById, pollIdsToSave),
sponsoredByChatId: {}, sponsoredByChatId: {},
}; };
} }

View File

@ -50,7 +50,7 @@ export function hasMessageMedia(message: MediaContainer) {
|| getMessageDocument(message) || getMessageDocument(message)
|| getMessageSticker(message) || getMessageSticker(message)
|| getMessageContact(message) || getMessageContact(message)
|| getMessagePoll(message) || getMessagePollId(message)
|| getMessageAction(message) || getMessageAction(message)
|| getMessageAudio(message) || getMessageAudio(message)
|| getMessageVoice(message) || getMessageVoice(message)
@ -128,8 +128,8 @@ export function getMessageContact(message: MediaContainer) {
return message.content.contact; return message.content.contact;
} }
export function getMessagePoll(message: MediaContainer) { export function getMessagePollId(message: MediaContainer) {
return message.content.poll; return message.content.pollId;
} }
export function getMessageInvoice(message: MediaContainer) { export function getMessageInvoice(message: MediaContainer) {

View File

@ -1,17 +1,17 @@
import type { TeactNode } from '../../lib/teact/teact'; import type { TeactNode } from '../../lib/teact/teact';
import type { ApiMediaExtendedPreview, ApiMessage, MediaContent } from '../../api/types'; import type {
ApiMediaExtendedPreview, ApiMessage, MediaContent, StatefulMediaContent,
} from '../../api/types';
import type { LangFn } from '../../hooks/useOldLang'; import type { LangFn } from '../../hooks/useOldLang';
import { ApiMessageEntityTypes } from '../../api/types'; import { ApiMessageEntityTypes } from '../../api/types';
import { CONTENT_NOT_SUPPORTED } from '../../config'; import { CONTENT_NOT_SUPPORTED } from '../../config';
import trimText from '../../util/trimText'; import trimText from '../../util/trimText';
import { renderTextWithEntities } from '../../components/common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../components/common/helpers/renderTextWithEntities';
import { getGlobal } from '../index';
import { import {
getExpiredMessageContentDescription, getMessageText, getMessageTranscription, isExpiredMessageContent, getExpiredMessageContentDescription, getMessageText, getMessageTranscription, isExpiredMessageContent,
} from './messages'; } from './messages';
import { getUserFirstOrLastName } from './users';
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟']; const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
export const TRUNCATED_SUMMARY_LENGTH = 80; export const TRUNCATED_SUMMARY_LENGTH = 80;
@ -19,22 +19,23 @@ export const TRUNCATED_SUMMARY_LENGTH = 80;
export function getMessageSummaryText( export function getMessageSummaryText(
lang: LangFn, lang: LangFn,
message: ApiMessage, message: ApiMessage,
statefulContent: StatefulMediaContent | undefined,
noEmoji = false, noEmoji = false,
truncateLength = TRUNCATED_SUMMARY_LENGTH, truncateLength = TRUNCATED_SUMMARY_LENGTH,
isExtended = false, isExtended = false,
) { ) {
const emoji = !noEmoji && getMessageSummaryEmoji(message); const emoji = !noEmoji && getMessageSummaryEmoji(message);
const emojiWithSpace = emoji ? `${emoji} ` : ''; const emojiWithSpace = emoji ? `${emoji} ` : '';
const text = trimText(getMessageTextWithSpoilers(message), truncateLength); const text = trimText(getMessageTextWithSpoilers(message, statefulContent), truncateLength);
const description = getMessageSummaryDescription(lang, message, text, isExtended); const description = getMessageSummaryDescription(lang, message, statefulContent, text, isExtended);
return `${emojiWithSpace}${description}`; return `${emojiWithSpace}${description}`;
} }
export function getMessageTextWithSpoilers(message: ApiMessage) { export function getMessageTextWithSpoilers(message: ApiMessage, statefulContent: StatefulMediaContent | undefined) {
const transcription = getMessageTranscription(message); const transcription = getMessageTranscription(message);
const textWithoutTranscription = getMessageText(message); const textWithoutTranscription = getMessageText(statefulContent?.story || message);
if (!textWithoutTranscription) { if (!textWithoutTranscription) {
return transcription; return transcription;
} }
@ -69,7 +70,7 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
voice, voice,
document, document,
sticker, sticker,
poll, pollId,
paidMedia, paidMedia,
} = message.content; } = message.content;
@ -97,27 +98,31 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
return '📎'; return '📎';
} }
if (poll) { if (pollId) {
return '📊'; return '📊';
} }
return undefined; return undefined;
} }
export function getMediaContentTypeDescription(lang: LangFn, content: MediaContent) { export function getMediaContentTypeDescription(
return getSummaryDescription(lang, content); lang: LangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined,
) {
return getSummaryDescription(lang, content, statefulContent);
} }
export function getMessageSummaryDescription( export function getMessageSummaryDescription(
lang: LangFn, lang: LangFn,
message: ApiMessage, message: ApiMessage,
statefulContent: StatefulMediaContent | undefined,
truncatedText?: string | TeactNode, truncatedText?: string | TeactNode,
isExtended = false, isExtended = false,
) { ) {
return getSummaryDescription(lang, message.content, message, truncatedText, isExtended); return getSummaryDescription(lang, message.content, statefulContent, message, truncatedText, isExtended);
} }
function getSummaryDescription( function getSummaryDescription(
lang: LangFn, lang: LangFn,
mediaContent: MediaContent, mediaContent: MediaContent,
statefulContent: StatefulMediaContent | undefined,
message?: ApiMessage, message?: ApiMessage,
truncatedText?: string | TeactNode, truncatedText?: string | TeactNode,
isExtended = false, isExtended = false,
@ -131,7 +136,6 @@ function getSummaryDescription(
document, document,
sticker, sticker,
contact, contact,
poll,
invoice, invoice,
location, location,
game, game,
@ -140,6 +144,7 @@ function getSummaryDescription(
giveawayResults, giveawayResults,
paidMedia, paidMedia,
} = mediaContent; } = mediaContent;
const { poll } = statefulContent || {};
let hasUsedTruncatedText = false; let hasUsedTruncatedText = false;
let summary: string | TeactNode | undefined; let summary: string | TeactNode | undefined;
@ -231,16 +236,7 @@ function getSummaryDescription(
} }
if (storyData) { if (storyData) {
if (message && storyData.isMention) { summary = truncatedText || (message ? lang('ForwardedStory') : lang('Chat.ReplyStory'));
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
const global = getGlobal();
const firstName = getUserFirstOrLastName(global.users.byId[message.chatId]);
summary = message.isOutgoing
? lang('Chat.Service.StoryMentioned.You', firstName)
: lang('Chat.Service.StoryMentioned', firstName);
} else {
summary = message ? lang('ForwardedStory') : lang('Chat.ReplyStory');
}
} }
if (isExpiredMessageContent(mediaContent)) { if (isExpiredMessageContent(mediaContent)) {

View File

@ -3,13 +3,15 @@ import type {
ApiMessage, ApiMessage,
ApiMessageEntityTextUrl, ApiMessageEntityTextUrl,
ApiPeer, ApiPeer,
ApiSponsoredMessage,
ApiStory, ApiStory,
MediaContainer, ApiTypeStory,
} from '../../api/types'; } from '../../api/types';
import type { MediaContent } from '../../api/types/messages'; import type {
ApiPoll, MediaContainer, MediaContent, StatefulMediaContent,
} from '../../api/types/messages';
import type { LangFn } from '../../hooks/useOldLang'; import type { LangFn } from '../../hooks/useOldLang';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import { import {
@ -52,26 +54,48 @@ export function getMessageTranscription(message: ApiMessage) {
return transcriptionId && global.transcriptions[transcriptionId]?.text; return transcriptionId && global.transcriptions[transcriptionId]?.text;
} }
export function hasMessageText(message: ApiMessage | ApiStory | ApiSponsoredMessage | MediaContainer) { export function hasMessageText(message: MediaContainer) {
const { const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location, text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location,
game, action, storyData, giveaway, giveawayResults, isExpiredVoice, paidMedia, game, action, storyData, giveaway, giveawayResults, isExpiredVoice, paidMedia,
} = message.content; } = message.content;
return Boolean(text) || !( return Boolean(text) || !(
sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice || location sticker || photo || video || audio || voice || document || contact || pollId || webPage || invoice || location
|| game || action?.phoneCall || storyData || giveaway || giveawayResults || isExpiredVoice || paidMedia || game || action?.phoneCall || storyData || giveaway || giveawayResults || isExpiredVoice || paidMedia
); );
} }
export function getMessageText(message: ApiMessage | ApiStory | ApiSponsoredMessage | MediaContainer) { export function getMessageStatefulContent(global: GlobalState, message: ApiMessage): StatefulMediaContent {
const poll = message.content.pollId ? global.messages.pollById[message.content.pollId] : undefined;
const { peerId: storyPeerId, id: storyId } = message.content.storyData || {};
const story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined;
return groupStatetefulContent({ poll, story });
}
export function groupStatetefulContent({
poll,
story,
} : {
poll?: ApiPoll;
story?: ApiTypeStory;
}) {
return {
poll,
story: story && 'content' in story ? story : undefined,
};
}
export function getMessageText(message: MediaContainer) {
return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined; return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined;
} }
export function getMessageCustomShape(message: ApiMessage): boolean { export function getMessageCustomShape(message: ApiMessage): boolean {
const { const {
text, sticker, photo, video, audio, voice, text, sticker, photo, video, audio, voice,
document, poll, webPage, contact, action, document, pollId, webPage, contact, action,
game, invoice, location, storyData, game, invoice, location, storyData,
} = message.content; } = message.content;
@ -79,7 +103,7 @@ export function getMessageCustomShape(message: ApiMessage): boolean {
return true; return true;
} }
if (!text || photo || video || audio || voice || document || poll || webPage || contact || action || game || invoice if (!text || photo || video || audio || voice || document || pollId || webPage || contact || action || game || invoice
|| location || storyData) { || location || storyData) {
return false; return false;
} }

View File

@ -2,18 +2,24 @@ import type { ApiMessage } from '../../api/types';
import type { LangFn } from '../../hooks/useOldLang'; import type { LangFn } from '../../hooks/useOldLang';
import { renderMessageText } from '../../components/common/helpers/renderMessageText'; import { renderMessageText } from '../../components/common/helpers/renderMessageText';
import { getGlobal } from '..';
import { getMessageStatefulContent } from './messages';
import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary'; import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary';
export function renderMessageSummaryHtml( export function renderMessageSummaryHtml(
lang: LangFn, lang: LangFn,
message: ApiMessage, message: ApiMessage,
) { ) {
const global = getGlobal();
const emoji = getMessageSummaryEmoji(message); const emoji = getMessageSummaryEmoji(message);
const emojiWithSpace = emoji ? `${emoji} ` : ''; const emojiWithSpace = emoji ? `${emoji} ` : '';
const text = renderMessageText( const text = renderMessageText(
{ message, shouldRenderAsHtml: true }, { message, shouldRenderAsHtml: true },
)?.join(''); )?.join('');
const description = getMessageSummaryDescription(lang, message, text, true);
const statefulContent = getMessageStatefulContent(global, message);
const description = getMessageSummaryDescription(lang, message, statefulContent, text, true);
return `${emojiWithSpace}${description}`; return `${emojiWithSpace}${description}`;
} }

View File

@ -122,6 +122,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
messages: { messages: {
byChatId: {}, byChatId: {},
sponsoredByChatId: {}, sponsoredByChatId: {},
pollById: {},
}, },
stories: { stories: {

View File

@ -1,5 +1,5 @@
import type { import type {
ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo, ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
} from '../../api/types'; } from '../../api/types';
import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types'; import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types';
import type { import type {
@ -29,6 +29,7 @@ import {
selectMessageIdsByGroupId, selectMessageIdsByGroupId,
selectOutlyingLists, selectOutlyingLists,
selectPinnedIds, selectPinnedIds,
selectPoll,
selectQuickReplyMessage, selectQuickReplyMessage,
selectScheduledIds, selectScheduledIds,
selectScheduledMessage, selectScheduledMessage,
@ -934,3 +935,94 @@ export function deleteQuickReply<T extends GlobalState>(
}, },
}; };
} }
export function updatePoll<T extends GlobalState>(
global: T,
pollId: string,
pollUpdate: Partial<ApiPoll>,
) {
const poll = selectPoll(global, pollId);
const oldResults = poll?.results;
let newResults = oldResults || pollUpdate.results;
if (poll && pollUpdate.results?.results) {
if (!poll.results || !pollUpdate.results.isMin) {
newResults = pollUpdate.results;
} else if (oldResults.results) {
// Update voters counts, but keep local `isChosen` values
newResults = {
...pollUpdate.results,
results: pollUpdate.results.results.map((result) => ({
...result,
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
})),
isMin: undefined,
};
}
}
const updatedPoll = {
...poll,
...pollUpdate,
results: newResults,
} satisfies ApiPoll;
if (!updatedPoll.id) {
return global;
}
return {
...global,
messages: {
...global.messages,
pollById: {
...global.messages.pollById,
[pollId]: updatedPoll,
},
},
};
}
export function updatePollVote<T extends GlobalState>(
global: T,
pollId: string,
peerId: string,
options: string[],
) {
const poll = selectPoll(global, pollId);
if (!poll) {
return global;
}
const { recentVoterIds, totalVoters, results } = poll.results;
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
const newResults = results ? [...results] : [];
newRecentVoterIds.push(peerId);
options.forEach((option) => {
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
const targetOption = newResults[targetOptionIndex];
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
updatedOption.votersCount += 1;
if (peerId === global.currentUserId) {
updatedOption.isChosen = true;
}
if (targetOptionIndex) {
newResults[targetOptionIndex] = updatedOption;
} else {
newResults.push(updatedOption);
}
});
return updatePoll(global, pollId, {
results: {
...poll.results,
recentVoterIds: newRecentVoterIds,
totalVoters: newTotalVoters,
results: newResults,
},
});
}

View File

@ -357,23 +357,6 @@ export function selectEditingMessage<T extends GlobalState>(
} }
} }
export function selectChatMessageByPollId<T extends GlobalState>(global: T, pollId: string) {
let messageWithPoll: ApiMessage | undefined;
// eslint-disable-next-line no-restricted-syntax
for (const chatMessages of Object.values(global.messages.byChatId)) {
const { byId } = chatMessages;
messageWithPoll = Object.values(byId).find((message) => {
return message.content.poll && message.content.poll.id === pollId;
});
if (messageWithPoll) {
break;
}
}
return messageWithPoll;
}
export function selectFocusedMessageId<T extends GlobalState>( export function selectFocusedMessageId<T extends GlobalState>(
global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T> global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
@ -484,6 +467,15 @@ export function selectForwardedSender<T extends GlobalState>(
return undefined; return undefined;
} }
export function selectPoll<T extends GlobalState>(global: T, pollId: string) {
return global.messages.pollById[pollId];
}
export function selectPollFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
if (!message.content.pollId) return undefined;
return selectPoll(global, message.content.pollId);
}
export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) { export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
const { chatId } = message; const { chatId } = message;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -647,7 +639,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
canEditMessagesIndefinitely canEditMessagesIndefinitely
|| getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME || getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME
) && !( ) && !(
content.sticker || content.contact || content.poll || content.action content.sticker || content.contact || content.pollId || content.action
|| (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults || (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults
|| isDocumentSticker || isDocumentSticker
) )
@ -724,7 +716,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const canSaveGif = message.content.video?.isGif; const canSaveGif = message.content.video?.isGif;
const poll = content.poll; const poll = content.pollId ? selectPoll(global, content.pollId) : undefined;
const canRevote = !poll?.summary.closed && !poll?.summary.quiz && poll?.results.results?.some((r) => r.isChosen); const canRevote = !poll?.summary.closed && !poll?.summary.quiz && poll?.results.results?.some((r) => r.isChosen);
const canClosePoll = hasMessageEditRight && poll && !poll.summary.closed && !isForwarded; const canClosePoll = hasMessageEditRight && poll && !poll.summary.closed && !isForwarded;

View File

@ -50,6 +50,7 @@ import type {
ApiPeerStories, ApiPeerStories,
ApiPhoneCall, ApiPhoneCall,
ApiPhoto, ApiPhoto,
ApiPoll,
ApiPostStatistics, ApiPostStatistics,
ApiPremiumGiftCodeOption, ApiPremiumGiftCodeOption,
ApiPremiumPromo, ApiPremiumPromo,
@ -1048,6 +1049,7 @@ export type GlobalState = {
threadsById: Record<ThreadId, Thread>; threadsById: Record<ThreadId, Thread>;
}>; }>;
sponsoredByChatId: Record<string, ApiSponsoredMessage>; sponsoredByChatId: Record<string, ApiSponsoredMessage>;
pollById: Record<string, ApiPoll>;
}; };
stories: { stories: {

View File

@ -13,6 +13,7 @@ import {
getMessageAction, getMessageAction,
getMessageRecentReaction, getMessageRecentReaction,
getMessageSenderName, getMessageSenderName,
getMessageStatefulContent,
getPrivateChatUserId, getPrivateChatUserId,
getUserFullName, getUserFullName,
isActionMessage, isActionMessage,
@ -366,7 +367,8 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
} else { } else {
// TODO[forums] Support ApiChat // TODO[forums] Support ApiChat
const senderName = getMessageSenderName(oldTranslate, chat.id, isChat ? messageSenderChat : messageSenderUser); const senderName = getMessageSenderName(oldTranslate, chat.id, isChat ? messageSenderChat : messageSenderUser);
let summary = getMessageSummaryText(oldTranslate, message, hasReaction, 60); const statefulContent = getMessageStatefulContent(global, message);
let summary = getMessageSummaryText(oldTranslate, message, statefulContent, hasReaction, 60);
if (hasReaction) { if (hasReaction) {
const emoji = getReactionEmoji(reaction); const emoji = getReactionEmoji(reaction);