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;
if (mtpMessage.message && !hasUnsupportedMedia
&& !content.sticker && !content.poll && !content.contact && !content.video?.isRound) {
&& !content.sticker && !content.pollId && !content.contact && !content.video?.isRound) {
content = {
...content,
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
@ -130,8 +130,8 @@ export function buildMessageMediaContent(
const contact = buildContact(media);
if (contact) return { contact };
const poll = buildPollFromMedia(media);
if (poll) return { poll };
const pollId = buildPollIdFromMedia(media);
if (pollId) return { pollId };
const webPage = buildWebPage(media);
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)) {
return undefined;
}

View File

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

View File

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

View File

@ -1,9 +1,12 @@
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 { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildPollFromMedia } from '../apiBuilders/messageContent';
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users';
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers';
@ -19,7 +22,8 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
let userById: Record<string, ApiUser> | 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)) {
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)) {
threadInfos = response.messages.map((message) => {
response.messages.forEach((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({
'@type': 'updateEntities',
users: userById,
chats: chatById,
threadInfos,
threadInfos: threadInfos?.length ? threadInfos : undefined,
polls: polls?.length ? polls : undefined,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import type { ApiPollAnswer, ApiPollResult } from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import Icon from '../../common/icons/Icon';
import './PollOption.scss';
type OwnProps = {
@ -59,7 +61,7 @@ const PollOption: FC<OwnProps> = ({
shouldAnimate && 'animate',
)}
>
<i className={buildClassName('icon', correctAnswer ? 'icon-check' : 'icon-close')} />
<Icon name={correctAnswer ? 'check' : 'close'} className="poll-option-icon" />
</span>
)}
</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 { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
@ -9,6 +9,7 @@ export function buildContentClassName(
message: ApiMessage,
album?: IAlbum,
{
poll,
hasSubheader,
isCustomShape,
isLastInGroup,
@ -23,6 +24,7 @@ export function buildContentClassName(
peerColorClass,
hasOutsideReactions,
}: {
poll?: ApiPoll;
hasSubheader?: boolean;
isCustomShape?: boolean | number;
isLastInGroup?: boolean;
@ -44,7 +46,7 @@ export function buildContentClassName(
const content = getMessageContent(message);
const {
photo = paidMediaPhoto, video = paidMediaVideo,
audio, voice, document, poll, webPage, contact, location, invoice, storyData,
audio, voice, document, webPage, contact, location, invoice, storyData,
giveaway, giveawayResults,
} = content;
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 { ApiMediaFormat } from '../../../../api/types';
@ -32,6 +32,7 @@ type ICopyOptions = {
export function getMessageCopyOptions(
message: ApiMessage,
statefulContent: StatefulMediaContent | undefined,
href?: string,
canCopy?: boolean,
afterEffect?: () => void,
@ -94,7 +95,12 @@ export function getMessageCopyOptions(
const clipboardText = renderMessageText(
{ message, shouldRenderAsHtml: true },
);
if (clipboardText) copyHtmlToClipboard(clipboardText.join(''), getMessageTextWithSpoilers(message)!);
if (clipboardText) {
copyHtmlToClipboard(
clipboardText.join(''),
getMessageTextWithSpoilers(message, statefulContent)!,
);
}
}
afterEffect?.();

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type {
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiPollResult, ApiReactions,
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiReactions,
MediaContent,
} from '../../../api/types';
import type { ThreadId } from '../../../types';
@ -43,6 +43,8 @@ import {
updateChatMessage,
updateListedIds,
updateMessageTranslations,
updatePoll,
updatePollVote,
updateQuickReplies,
updateQuickReplyMessage,
updateScheduledMessage,
@ -57,7 +59,6 @@ import {
selectChat,
selectChatLastMessageId,
selectChatMessage,
selectChatMessageByPollId,
selectChatMessages,
selectChatScheduledMessages,
selectCommonBoxChatId,
@ -74,7 +75,6 @@ import {
selectSavedDialogIdFromMessage,
selectScheduledIds,
selectScheduledMessage,
selectSendAs,
selectTabState,
selectThreadByMessage,
selectThreadIdFromMessage,
@ -90,7 +90,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'newMessage': {
const {
chatId, id, message, shouldForceReply, wasDrafted,
chatId, id, message, shouldForceReply, wasDrafted, poll,
} = update;
global = updateWithLocalMedia(global, chatId, id, message);
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);
// Reload dialogs if chat is not present in the list
@ -208,7 +212,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}
case 'newScheduledMessage': {
const { chatId, id, message } = update;
const {
chatId, id, message, poll,
} = update;
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]));
}
if (poll) {
global = updatePoll(global, poll.id, poll);
}
setGlobal(global);
break;
@ -560,97 +570,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateMessagePoll': {
const { pollId, pollUpdate } = update;
const message = selectChatMessageByPollId(global, pollId);
global = updatePoll(global, pollId, pollUpdate);
if (message?.content.poll) {
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);
}
setGlobal(global);
break;
}
case 'updateMessagePollVote': {
const { pollId, peerId, options } = update;
const message = selectChatMessageByPollId(global, pollId);
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,
},
},
},
},
);
global = updatePollVote(global, pollId, peerId, options);
setGlobal(global);
break;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,15 @@ import type {
ApiMessage,
ApiMessageEntityTextUrl,
ApiPeer,
ApiSponsoredMessage,
ApiStory,
MediaContainer,
ApiTypeStory,
} 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 { ThreadId } from '../../types';
import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import {
@ -52,26 +54,48 @@ export function getMessageTranscription(message: ApiMessage) {
return transcriptionId && global.transcriptions[transcriptionId]?.text;
}
export function hasMessageText(message: ApiMessage | ApiStory | ApiSponsoredMessage | MediaContainer) {
export function hasMessageText(message: MediaContainer) {
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,
} = message.content;
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
);
}
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;
}
export function getMessageCustomShape(message: ApiMessage): boolean {
const {
text, sticker, photo, video, audio, voice,
document, poll, webPage, contact, action,
document, pollId, webPage, contact, action,
game, invoice, location, storyData,
} = message.content;
@ -79,7 +103,7 @@ export function getMessageCustomShape(message: ApiMessage): boolean {
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) {
return false;
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type {
ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
} from '../../api/types';
import type { FocusDirection, ScrollTargetPosition, ThreadId } from '../../types';
import type {
@ -29,6 +29,7 @@ import {
selectMessageIdsByGroupId,
selectOutlyingLists,
selectPinnedIds,
selectPoll,
selectQuickReplyMessage,
selectScheduledIds,
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>(
global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
@ -484,6 +467,15 @@ export function selectForwardedSender<T extends GlobalState>(
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) {
const { chatId } = message;
const chat = selectChat(global, chatId);
@ -647,7 +639,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
canEditMessagesIndefinitely
|| 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
|| isDocumentSticker
)
@ -724,7 +716,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
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 canClosePoll = hasMessageEditRight && poll && !poll.summary.closed && !isForwarded;

View File

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

View File

@ -13,6 +13,7 @@ import {
getMessageAction,
getMessageRecentReaction,
getMessageSenderName,
getMessageStatefulContent,
getPrivateChatUserId,
getUserFullName,
isActionMessage,
@ -366,7 +367,8 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
} else {
// TODO[forums] Support ApiChat
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) {
const emoji = getReactionEmoji(reaction);