From 8fc3df855df4a1d9914e90e9e4ac8c85c2d3dd61 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 10 Oct 2023 13:35:16 +0200 Subject: [PATCH] Introduce Channel Stories (#3888) --- src/@types/global.d.ts | 4 + src/api/gramjs/apiBuilders/chats.ts | 6 + src/api/gramjs/apiBuilders/messageContent.ts | 10 +- src/api/gramjs/apiBuilders/messages.ts | 4 +- src/api/gramjs/apiBuilders/payments.ts | 4 +- src/api/gramjs/apiBuilders/reactions.ts | 2 +- src/api/gramjs/apiBuilders/stories.ts | 39 ++- src/api/gramjs/gramjsBuilders/index.ts | 4 +- src/api/gramjs/helpers.ts | 4 +- src/api/gramjs/localDb.ts | 2 +- src/api/gramjs/methods/account.ts | 6 +- src/api/gramjs/methods/bots.ts | 14 +- src/api/gramjs/methods/chats.ts | 9 +- src/api/gramjs/methods/client.ts | 9 +- src/api/gramjs/methods/messages.ts | 14 +- src/api/gramjs/methods/stories.ts | 163 ++++++---- src/api/gramjs/methods/users.ts | 4 +- src/api/gramjs/updater.ts | 14 +- src/api/types/chats.ts | 18 +- src/api/types/messages.ts | 2 +- src/api/types/stories.ts | 30 +- src/api/types/updates.ts | 8 +- .../calls/group/GroupCallParticipant.tsx | 4 +- .../calls/group/GroupCallParticipantList.tsx | 5 +- src/components/common/Avatar.tsx | 30 +- src/components/common/AvatarList.tsx | 4 +- src/components/common/AvatarStoryCircle.tsx | 14 +- src/components/common/ChatExtra.tsx | 12 +- src/components/common/Composer.tsx | 21 +- src/components/common/CustomEmoji.tsx | 3 + src/components/common/EmbeddedMessage.tsx | 5 +- src/components/common/EmbeddedStory.tsx | 4 +- src/components/common/FullNameTitle.tsx | 4 +- src/components/common/GroupChatInfo.tsx | 9 +- src/components/common/Icon.tsx | 27 ++ src/components/common/PickerSelectedItem.tsx | 6 +- src/components/common/PrivateChatInfo.tsx | 2 +- src/components/common/ReportModal.tsx | 18 +- src/components/common/UserLink.tsx | 4 +- .../reactions/CustomEmojiEffect.module.scss | 5 +- .../common/reactions/CustomEmojiEffect.tsx | 27 +- .../ReactionAnimatedEmoji.module.scss | 9 +- .../reactions/ReactionAnimatedEmoji.tsx | 60 ++-- src/components/left/main/Chat.tsx | 9 +- src/components/left/main/ChatFolders.tsx | 2 +- src/components/left/main/ChatList.tsx | 4 +- src/components/left/main/Topic.tsx | 5 +- .../left/main/hooks/useChatListEntry.tsx | 4 +- .../left/search/LeftSearchResultChat.tsx | 8 +- .../left/search/helpers/getSenderName.ts | 2 +- .../PremiumLimitReachedModal.module.scss | 8 +- .../common/PremiumLimitReachedModal.tsx | 6 +- src/components/mediaViewer/MediaViewer.tsx | 8 +- .../mediaViewer/MediaViewerActions.tsx | 4 +- .../mediaViewer/MediaViewerContent.tsx | 4 +- src/components/mediaViewer/SenderInfo.tsx | 11 +- .../mediaViewer/helpers/ghostAnimation.ts | 15 +- .../mediaViewer/hooks/useMediaProps.ts | 4 +- src/components/middle/AudioPlayer.tsx | 4 +- src/components/middle/ChatReportPanel.tsx | 4 +- src/components/middle/HeaderMenuContainer.tsx | 2 +- src/components/middle/MessageList.tsx | 16 +- src/components/middle/MiddleHeader.tsx | 6 +- .../composer/ComposerEmbeddedMessage.tsx | 13 +- src/components/middle/message/BaseStory.tsx | 25 +- .../middle/message/CommentButton.tsx | 6 +- src/components/middle/message/Location.tsx | 4 +- src/components/middle/message/MentionLink.tsx | 4 +- src/components/middle/message/Message.tsx | 16 +- .../middle/message/MessageContextMenu.tsx | 3 +- src/components/middle/message/Poll.tsx | 4 +- .../middle/message/ReactionButton.tsx | 4 +- .../middle/message/ReactionPicker.tsx | 22 +- src/components/middle/message/Story.tsx | 6 +- .../middle/message/StoryMention.tsx | 31 +- src/components/middle/message/WebPage.tsx | 2 +- .../middle/message/hooks/useInnerHandlers.ts | 9 +- src/components/payment/Checkout.module.scss | 1 + src/components/payment/Checkout.tsx | 4 +- src/components/right/Profile.tsx | 38 +-- src/components/right/RightSearch.tsx | 4 +- .../management/ManageGroupAdminRights.tsx | 42 ++- .../right/management/ManageGroupMembers.tsx | 5 +- src/components/story/MediaAreaOverlay.tsx | 55 ---- src/components/story/MediaStory.tsx | 17 +- src/components/story/Story.tsx | 204 ++++-------- .../story/StoryDeleteConfirmModal.tsx | 14 +- src/components/story/StoryFooter.module.scss | 84 +++++ src/components/story/StoryFooter.tsx | 160 +++++++++ src/components/story/StoryPreview.tsx | 52 +-- src/components/story/StoryRibbon.module.scss | 2 +- src/components/story/StoryRibbon.tsx | 30 +- src/components/story/StoryRibbonButton.tsx | 65 ++-- src/components/story/StorySettings.tsx | 15 +- src/components/story/StorySlides.tsx | 160 ++++----- src/components/story/StoryToggler.tsx | 43 +-- src/components/story/StoryViewModal.tsx | 5 +- src/components/story/StoryViewer.module.scss | 71 +--- src/components/story/StoryViewer.tsx | 40 +-- .../story/helpers/ghostAnimation.ts | 4 +- .../story/helpers/ribbonAnimation.ts | 48 +-- .../story/hooks/useStoryPreloader.ts | 32 +- src/components/story/hooks/useStoryProps.ts | 2 - .../story/mediaArea/MediaArea.module.scss | 124 +++++++ .../story/mediaArea/MediaAreaOverlay.tsx | 117 +++++++ .../mediaArea/MediaAreaSuggestedReaction.tsx | 107 ++++++ src/config.ts | 6 +- src/global/actions/api/bots.ts | 4 +- src/global/actions/api/chats.ts | 15 +- src/global/actions/api/messages.ts | 18 +- src/global/actions/api/stories.ts | 230 +++++++------ src/global/actions/api/users.ts | 10 +- src/global/actions/apiUpdaters/chats.ts | 8 + src/global/actions/apiUpdaters/misc.ts | 26 +- src/global/actions/apiUpdaters/users.ts | 8 +- src/global/actions/ui/reactions.ts | 6 +- src/global/actions/ui/stories.ts | 122 +++---- src/global/cache.ts | 15 +- src/global/helpers/chats.ts | 7 +- src/global/helpers/messages.ts | 4 +- src/global/helpers/reactions.ts | 29 ++ src/global/helpers/users.ts | 8 +- src/global/initialState.ts | 4 +- src/global/intervals.ts | 8 +- src/global/reducers/general.ts | 32 ++ src/global/reducers/index.ts | 1 + src/global/reducers/payments.ts | 4 +- src/global/reducers/reactions.ts | 27 +- src/global/reducers/stories.ts | 307 ++++++++++-------- src/global/reducers/users.ts | 6 +- src/global/selectors/chats.ts | 16 +- src/global/selectors/messages.ts | 25 +- src/global/selectors/stories.ts | 46 +-- src/global/selectors/users.ts | 6 +- src/global/types.ts | 57 ++-- src/hooks/polling/usePeerStoriesPolling.ts | 67 ++++ src/hooks/polling/useUserStoriesPolling.ts | 58 ---- src/hooks/useEnsureStory.ts | 14 +- src/hooks/useMessageMediaMetadata.ts | 4 +- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 289 ++++++++++++----- src/lib/gramjs/tl/apiTl.js | 70 ++-- src/lib/gramjs/tl/static/api.json | 9 +- src/lib/gramjs/tl/static/api.tl | 95 +++--- src/util/iteratees.ts | 4 + src/util/notifications.ts | 4 +- 146 files changed, 2568 insertions(+), 1566 deletions(-) create mode 100644 src/components/common/Icon.tsx delete mode 100644 src/components/story/MediaAreaOverlay.tsx create mode 100644 src/components/story/StoryFooter.module.scss create mode 100644 src/components/story/StoryFooter.tsx create mode 100644 src/components/story/mediaArea/MediaArea.module.scss create mode 100644 src/components/story/mediaArea/MediaAreaOverlay.tsx create mode 100644 src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx create mode 100644 src/global/reducers/general.ts create mode 100644 src/hooks/polling/usePeerStoriesPolling.ts delete mode 100644 src/hooks/polling/useUserStoriesPolling.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 156557043..3bc24ea3c 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -123,6 +123,10 @@ type Undefined = { }; type OptionalCombine = (A & B) | (A & Undefined); +type CommonProperties = { + [K in keyof T & keyof U]: T[K] & U[K]; +}; + // Fix to make Boolean() work as !! // https://github.com/microsoft/TypeScript/issues/16655 type Falsy = false | 0 | '' | null | undefined; diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index ac47c71f8..98bb9b400 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -51,6 +51,9 @@ function buildApiChatFieldsFromPeerEntity( const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest); const usernames = buildApiUsernames(peerEntity); const isForum = Boolean('forum' in peerEntity && peerEntity.forum); + const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden); + const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; + const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable); return { isMin, @@ -77,6 +80,9 @@ function buildApiChatFieldsFromPeerEntity( isJoinToSend, isJoinRequest, isForum, + areStoriesHidden, + maxStoryId, + hasStories: Boolean(maxStoryId) && !storiesUnavailable, }; } diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index d1f2b5a8c..845d211eb 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -471,9 +471,9 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag return undefined; } - const userId = buildApiPeerId(media.userId, 'user'); + const peerId = getApiChatIdFromMtpPeer(media.peer); - return { id: media.id, userId, ...(media.viaMention && { isMention: true }) }; + return { id: media.id, peerId, ...(media.viaMention && { isMention: true }) }; } export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll { @@ -564,14 +564,14 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef const attributeStory = attributes ?.find((a: any): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); if (attributeStory) { - const userId = buildApiPeerId(attributeStory.userId, 'user'); + const peerId = getApiChatIdFromMtpPeer(attributeStory.peer); story = { id: attributeStory.id, - userId, + peerId, }; if (attributeStory.story instanceof GramJs.StoryItem) { - addStoryToLocalDb(attributeStory.story, userId); + addStoryToLocalDb(attributeStory.story, peerId); } } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 90608182e..d43c62441 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -11,6 +11,7 @@ import type { ApiMessageEntity, ApiMessageForwardInfo, ApiNewPoll, + ApiPeer, ApiPhoto, ApiReplyKeyboard, ApiSponsoredMessage, @@ -19,7 +20,6 @@ import type { ApiStorySkipped, ApiThreadInfo, ApiTypeReplyTo, - ApiUser, ApiVideo, PhoneCallAction, } from '../../types'; @@ -690,7 +690,7 @@ export function buildLocalMessage( contact?: ApiContact, groupedId?: string, scheduledAt?: number, - sendAs?: ApiChat | ApiUser, + sendAs?: ApiPeer, story?: ApiStory | ApiStorySkipped, ): ApiMessage { const localId = getNextLocalMessageId(chat.lastMessage?.id); diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 30962b074..070c536b7 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -146,7 +146,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI invoice, description: text, title, photo, } = form; const { - test, currency, prices, recurring, recurringTermsUrl, maxTipAmount, suggestedTipAmounts, + test, currency, prices, recurring, termsUrl, maxTipAmount, suggestedTipAmounts, } = invoice; const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0); @@ -159,7 +159,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiI currency, isTest: test, isRecurring: recurring, - recurringTermsUrl, + termsUrl, maxTipAmount: maxTipAmount?.toJSNumber(), ...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }), }; diff --git a/src/api/gramjs/apiBuilders/reactions.ts b/src/api/gramjs/apiBuilders/reactions.ts index e54e9f682..73d2449b4 100644 --- a/src/api/gramjs/apiBuilders/reactions.ts +++ b/src/api/gramjs/apiBuilders/reactions.ts @@ -35,7 +35,7 @@ function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) { return 0; } -function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { +export function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { const { chosenOrder, count, reaction } = reactionCount; const apiReaction = buildApiReaction(reaction); diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index 3eb2b46d5..1f264c501 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -7,14 +7,14 @@ import type { import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildPrivacyRules } from './common'; import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; -import { buildApiPeerId } from './peers'; -import { buildApiReaction } from './reactions'; +import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; +import { buildApiReaction, buildReactionCount } from './reactions'; -export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiTypeStory { +export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiTypeStory { if (story instanceof GramJs.StoryItemDeleted) { return { id: story.id, - userId, + peerId, isDeleted: true, }; } @@ -26,7 +26,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT return { id, - userId, + peerId, ...(closeFriends && { isForCloseFriends: true }), date, expireDate, @@ -37,7 +37,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT edited, pinned, expireDate, id, date, caption, entities, media, privacy, views, public: isPublic, noforwards, closeFriends, contacts, selectedContacts, - mediaAreas, sentReaction, + mediaAreas, sentReaction, out, } = story; const content: ApiMessage['content'] = { @@ -50,7 +50,7 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT return { id, - userId, + peerId, date, expireDate, content, @@ -63,9 +63,11 @@ export function buildApiStory(userId: string, story: GramJs.TypeStoryItem): ApiT ...(noforwards && { noForwards: true }), ...(views?.viewsCount && { viewsCount: views.viewsCount }), ...(views?.reactionsCount && { reactionsCount: views.reactionsCount }), + ...(views?.reactions && { reactions: views.reactions.map(buildReactionCount) }), ...(views?.recentViewers && { recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')), }), + ...(out && { isOut: true }), ...(privacy && { visibility: buildPrivacyRules(privacy) }), ...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }), ...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }), @@ -134,11 +136,28 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un }; } + if (area instanceof GramJs.MediaAreaSuggestedReaction) { + const { + coordinates, reaction, dark, flipped, + } = area; + + const apiReaction = buildApiReaction(reaction); + if (!apiReaction) return undefined; + + return { + type: 'suggestedReaction', + coordinates: buildApiMediaAreaCoordinates(coordinates), + reaction: apiReaction, + ...(dark && { isDark: true }), + ...(flipped && { isFlipped: true }), + }; + } + return undefined; } -export function buildApiUsersStories(userStories: GramJs.UserStories) { - const userId = buildApiPeerId(userStories.userId, 'user'); +export function buildApiPeerStories(peerStories: GramJs.PeerStories) { + const peerId = getApiChatIdFromMtpPeer(peerStories.peer); - return buildCollectionByCallback(userStories.stories, (story) => [story.id, buildApiStory(userId, story)]); + return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]); } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 5d77a8cda..37f4be045 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -278,9 +278,9 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi } export function buildInputStory(story: ApiStory | ApiStorySkipped) { - const user = localDb.users[story.userId]; + const peer = buildInputPeerFromLocalDb(story.peerId)!; return new GramJs.InputMediaStory({ - userId: new GramJs.InputUser({ userId: BigInt(user!.id), accessHash: user!.accessHash! }), + peer, id: story.id, }); } diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index e597d3c23..eadce2c12 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -81,14 +81,14 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ } } -export function addStoryToLocalDb(story: GramJs.TypeStoryItem, userId: string) { +export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) { if (!(story instanceof GramJs.StoryItem)) { return; } const storyData = { id: story.id, - userId, + peerId, }; if (story.media instanceof GramJs.MessageMediaPhoto) { diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index 35fde74b5..38964bfce 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -12,7 +12,7 @@ const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self; export type StoryRepairInfo = { storyData?: { - userId: string; + peerId: string; id: number; }; }; diff --git a/src/api/gramjs/methods/account.ts b/src/api/gramjs/methods/account.ts index 88602c189..b7856610a 100644 --- a/src/api/gramjs/methods/account.ts +++ b/src/api/gramjs/methods/account.ts @@ -2,7 +2,7 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiChat, ApiPhoto, ApiReportReason, ApiUser, + ApiPeer, ApiPhoto, ApiReportReason, } from '../../types'; import { buildInputPeer, buildInputPhoto, buildInputReportReason } from '../gramjsBuilders'; @@ -13,7 +13,7 @@ export async function reportPeer({ reason, description, }: { - peer: ApiChat | ApiUser; reason: ApiReportReason; description?: string; + peer: ApiPeer; reason: ApiReportReason; description?: string; }) { const result = await invokeRequest(new GramJs.account.ReportPeer({ peer: buildInputPeer(peer.id, peer.accessHash), @@ -30,7 +30,7 @@ export async function reportProfilePhoto({ reason, description, }: { - peer: ApiChat | ApiUser; photo: ApiPhoto; reason: ApiReportReason; description?: string; + peer: ApiPeer; photo: ApiPhoto; reason: ApiReportReason; description?: string; }) { const photoId = buildInputPhoto(photo); if (!photoId) return undefined; diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 734a0696d..1f32baaa8 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -3,7 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiBotApp, - ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate, + ApiChat, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, } from '../../types'; import { WEB_APP_PLATFORM } from '../../../config'; @@ -131,7 +131,7 @@ export async function sendInlineBotResult({ resultId: string; queryId: string; replyingTo?: number; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; isSilent?: boolean; scheduleDate?: number; }) { @@ -180,14 +180,14 @@ export async function requestWebView({ isFromBotMenu, }: { isSilent?: boolean; - peer: ApiChat | ApiUser; + peer: ApiPeer; bot: ApiUser; url?: string; startParam?: string; replyToMessageId?: number; threadId?: number; theme?: ApiThemeParameters; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; isFromBotMenu?: boolean; }) { const result = await invokeRequest(new GramJs.messages.RequestWebView({ @@ -269,7 +269,7 @@ export async function requestAppWebView({ theme, isWriteAllowed, }: { - peer: ApiChat | ApiUser; + peer: ApiPeer; app: ApiBotApp; startParam?: string; theme?: ApiThemeParameters; @@ -297,12 +297,12 @@ export function prolongWebView({ sendAs, }: { isSilent?: boolean; - peer: ApiChat | ApiUser; + peer: ApiPeer; bot: ApiUser; queryId: string; replyToMessageId?: number; threadId?: number; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; }) { return invokeRequest(new GramJs.messages.ProlongWebView({ silent: isSilent || undefined, diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index ae9337aa7..028fb5d2e 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -12,6 +12,7 @@ import type { ApiGroupCall, ApiMessage, ApiMessageEntity, + ApiPeer, ApiPhoto, ApiTopic, ApiUser, @@ -495,6 +496,7 @@ async function getFullChannelInfo( chatPhoto, participantsHidden, translationsDisabled, + storiesPinnedAvailable, } = result.fullChat; if (chatPhoto instanceof GramJs.Photo) { @@ -569,6 +571,7 @@ async function getFullChannelInfo( stickerSet: stickerset ? buildStickerSet(stickerset) : undefined, areParticipantsHidden: participantsHidden, isTranslationDisabled: translationsDisabled, + hasPinnedStories: Boolean(storiesPinnedAvailable), }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], userStatusesById: statusesById, @@ -1487,7 +1490,7 @@ export async function createTopic({ title: string; iconColor?: number; iconEmojiId?: string; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; }) { const { id, accessHash } = chat; @@ -1746,7 +1749,7 @@ export async function createChalistInvite({ }: { folderId: number; title?: string; - peers: (ApiChat | ApiUser)[]; + peers: ApiPeer[]; }) { const result = await invokeRequest(new GramJs.chatlists.ExportChatlistInvite({ chatlist: new GramJs.InputChatlistDialogFilter({ @@ -1786,7 +1789,7 @@ export async function editChatlistInvite({ folderId: number; slug: string; title?: string; - peers: (ApiChat | ApiUser)[]; + peers: ApiPeer[]; }) { const result = await invokeRequest(new GramJs.chatlists.EditExportedInvite({ chatlist: new GramJs.InputChatlistDialogFilter({ diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index de9799f7e..c8296c5af 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -22,6 +22,7 @@ import { pause } from '../../../util/schedulers'; import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users'; +import { buildInputPeerFromLocalDb } from '../gramjsBuilders'; import { addEntitiesToLocalDb, addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log, @@ -444,17 +445,17 @@ export async function repairFileReference({ if (mediaMatchType === 'document' || mediaMatchType === 'photo') { const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId]; if (!entity.storyData) return false; - const user = localDb.users[entity.storyData.userId]; - if (!user?.accessHash) return false; + const peer = buildInputPeerFromLocalDb(entity.storyData.peerId); + if (!peer) return false; const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ - userId: new GramJs.InputUser({ userId: user.id, accessHash: user.accessHash }), + peer, id: [entity.storyData.id], })); if (!result) return false; addEntitiesToLocalDb(result.users); - result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.userId)); + result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.peerId)); return true; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 9a7abb2f7..ca3f92692 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -11,6 +11,7 @@ import type { ApiMessageSearchType, ApiNewPoll, ApiOnProgress, + ApiPeer, ApiPoll, ApiReportReason, ApiSendMessageAction, @@ -18,7 +19,6 @@ import type { ApiStory, ApiStorySkipped, ApiTypeReplyTo, - ApiUser, ApiVideo, OnApiUpdate, } from '../../types'; @@ -267,7 +267,7 @@ export function sendMessage( scheduledAt?: number; groupedId?: string; noWebPage?: boolean; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; shouldUpdateStickerSetOrder?: boolean; }, onProgress?: ApiOnProgress, @@ -417,7 +417,7 @@ function sendGroupedMedia( groupedId: string; isSilent?: boolean; scheduledAt?: number; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; }, randomId: GramJs.long, localMessage: ApiMessage, @@ -785,7 +785,7 @@ export async function deleteHistory({ export async function reportMessages({ peer, messageIds, reason, description, }: { - peer: ApiChat | ApiUser; messageIds: number[]; reason: ApiReportReason; description?: string; + peer: ApiPeer; messageIds: number[]; reason: ApiReportReason; description?: string; }) { const result = await invokeRequest(new GramJs.messages.Report({ peer: buildInputPeer(peer.id, peer.accessHash), @@ -800,7 +800,7 @@ export async function reportMessages({ export async function sendMessageAction({ peer, threadId, action, }: { - peer: ApiChat | ApiUser; threadId?: number; action: ApiSendMessageAction; + peer: ApiPeer; threadId?: number; action: ApiSendMessageAction; }) { const gramAction = buildSendMessageAction(action); if (!gramAction) { @@ -1299,7 +1299,7 @@ export async function forwardMessages({ messages: ApiMessage[]; isSilent?: boolean; scheduledAt?: number; - sendAs?: ApiUser | ApiChat; + sendAs?: ApiPeer; withMyScore?: boolean; noAuthors?: boolean; noCaptions?: boolean; @@ -1517,7 +1517,7 @@ export async function fetchSendAs({ export function saveDefaultSendAs({ sendAs, chat, }: { - sendAs: ApiChat | ApiUser; chat: ApiChat; + sendAs: ApiPeer; chat: ApiChat; }) { return invokeRequest(new GramJs.messages.SaveDefaultSendAs({ peer: buildInputPeer(chat.id, chat.accessHash), diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index 9a3dab5e5..89f7e6023 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -2,22 +2,29 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiInputPrivacyRules } from '../../../types'; import type { - ApiReaction, ApiReportReason, ApiStealthMode, - ApiTypeStory, ApiUser, ApiUserStories, + ApiChat, + ApiPeer, + ApiPeerStories, + ApiReaction, + ApiReportReason, + ApiStealthMode, + ApiTypeStory, + ApiUser, } from '../../types'; import { STORY_LIST_LIMIT } from '../../../config'; import { buildCollectionByCallback } from '../../../util/iteratees'; -import { buildApiPeerId } from '../apiBuilders/peers'; +import { buildApiChatFromPreview } from '../apiBuilders/chats'; +import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { + buildApiPeerStories, buildApiStealthMode, - buildApiStory, buildApiStoryView, buildApiUsersStories, + buildApiStory, + buildApiStoryView, } from '../apiBuilders/stories'; import { buildApiUser } from '../apiBuilders/users'; import { - buildInputEntity, buildInputPeer, - buildInputPeerFromLocalDb, buildInputPrivacyRules, buildInputReaction, buildInputReportReason, @@ -38,7 +45,8 @@ export async function fetchAllStories({ | { state: string; stealthMode: ApiStealthMode } | { users: ApiUser[]; - userStories: Record; + chats: ApiChat[]; + peerStories: Record; hasMore?: true; state: string; stealthMode: ApiStealthMode; @@ -60,13 +68,14 @@ export async function fetchAllStories({ } addEntitiesToLocalDb(result.users); - result.userStories.forEach((userStories) => ( - userStories.stories.forEach((story) => addStoryToLocalDb(story, buildApiPeerId(userStories.userId, 'user'))) + addEntitiesToLocalDb(result.chats); + result.peerStories.forEach((peerStories) => ( + peerStories.stories.forEach((story) => addStoryToLocalDb(story, getApiChatIdFromMtpPeer(peerStories.peer))) )); - const allUserStories = result.userStories.reduce>((acc, userStories) => { - const userId = buildApiPeerId(userStories.userId, 'user'); - const stories = buildApiUsersStories(userStories); + const allUserStories = result.peerStories.reduce>((acc, peerStories) => { + const peerId = getApiChatIdFromMtpPeer(peerStories.peer); + const stories = buildApiPeerStories(peerStories); const { pinnedIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce< { pinnedIds: number[]; @@ -93,12 +102,12 @@ export async function fetchAllStories({ return acc; } - acc[userId] = { + acc[peerId] = { byId: stories, orderedIds, pinnedIds, lastUpdatedAt, - lastReadId: userStories.maxReadId, + lastReadId: peerStories.maxReadId, }; return acc; @@ -106,20 +115,21 @@ export async function fetchAllStories({ return { users: result.users.map(buildApiUser).filter(Boolean), - userStories: allUserStories, + chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean), + peerStories: allUserStories, hasMore: result.hasMore, state: result.state, stealthMode: buildApiStealthMode(result.stealthMode), }; } -export async function fetchUserStories({ - user, +export async function fetchPeerStories({ + peer, }: { - user: ApiUser; + peer: ApiPeer; }) { - const result = await invokeRequest(new GramJs.stories.GetUserStories({ - userId: buildInputPeer(user.id, user.accessHash), + const result = await invokeRequest(new GramJs.stories.GetPeerStories({ + peer: buildInputPeer(peer.id, peer.accessHash), })); if (!result) { @@ -127,55 +137,58 @@ export async function fetchUserStories({ } addEntitiesToLocalDb(result.users); - result.stories.stories.forEach((story) => addStoryToLocalDb(story, user.id)); + result.stories.stories.forEach((story) => addStoryToLocalDb(story, peer.id)); const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const stories = buildCollectionByCallback(result.stories.stories, (story) => ( - [story.id, buildApiStory(user.id, story)] + [story.id, buildApiStory(peer.id, story)] )); return { + chats, users, stories, lastReadStoryId: result.stories.maxReadId, }; } -export function fetchUserPinnedStories({ - user, offsetId, +export function fetchPeerPinnedStories({ + peer, offsetId, }: { - user: ApiUser; + peer: ApiPeer; offsetId?: number; }) { return fetchCommonStoriesRequest({ method: new GramJs.stories.GetPinnedStories({ - userId: buildInputPeer(user.id, user.accessHash), + peer: buildInputPeer(peer.id, peer.accessHash), offsetId, limit: STORY_LIST_LIMIT, }), - userId: user.id, + peerId: peer.id, }); } export function fetchStoriesArchive({ - currentUserId, + peer, offsetId, }: { - currentUserId: string; + peer: ApiPeer; offsetId?: number; }) { return fetchCommonStoriesRequest({ method: new GramJs.stories.GetStoriesArchive({ + peer: peer && buildInputPeer(peer.id, peer.accessHash), offsetId, limit: STORY_LIST_LIMIT, }), - userId: currentUserId, + peerId: peer.id, }); } -export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids: number[] }) { +export async function fetchPeerStoriesByIds({ peer, ids }: { peer: ApiPeer; ids: number[] }) { const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ - userId: buildInputPeer(user.id, user.accessHash), + peer: buildInputPeer(peer.id, peer.accessHash), id: ids, })); @@ -184,17 +197,19 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids: } addEntitiesToLocalDb(result.users); - result.stories.forEach((story) => addStoryToLocalDb(story, user.id)); + addEntitiesToLocalDb(result.chats); + result.stories.forEach((story) => addStoryToLocalDb(story, peer.id)); const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const stories = ids.reduce>((acc, id) => { const story = result.stories.find(({ id: currentId }) => currentId === id); if (story) { - acc[id] = buildApiStory(user.id, story); + acc[id] = buildApiStory(peer.id, story); } else { acc[id] = { id, - userId: user.id, + peerId: peer.id, isDeleted: true, }; } @@ -203,34 +218,43 @@ export async function fetchUserStoriesByIds({ user, ids }: { user: ApiUser; ids: }, {}); return { + chats, users, stories, }; } -export function viewStory({ user, storyId }: { user: ApiUser; storyId: number }) { +export function viewStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) { return invokeRequest(new GramJs.stories.IncrementStoryViews({ - userId: buildInputPeer(user.id, user.accessHash), + peer: buildInputPeer(peer.id, peer.accessHash), id: [storyId], })); } -export function markStoryRead({ user, storyId }: { user: ApiUser; storyId: number }) { +export function markStoryRead({ peer, storyId }: { peer: ApiPeer; storyId: number }) { return invokeRequest(new GramJs.stories.ReadStories({ - userId: buildInputPeer(user.id, user.accessHash), + peer: buildInputPeer(peer.id, peer.accessHash), maxId: storyId, })); } -export function deleteStory({ storyId }: { storyId: number }) { - return invokeRequest(new GramJs.stories.DeleteStories({ id: [storyId] })); +export function deleteStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) { + return invokeRequest(new GramJs.stories.DeleteStories({ + peer: buildInputPeer(peer.id, peer.accessHash), + id: [storyId], + })); } -export function toggleStoryPinned({ storyId, isPinned }: { storyId: number; isPinned?: boolean }) { - return invokeRequest(new GramJs.stories.TogglePinned({ id: [storyId], pinned: isPinned })); +export function toggleStoryPinned({ peer, storyId, isPinned }: { peer: ApiPeer; storyId: number; isPinned?: boolean }) { + return invokeRequest(new GramJs.stories.TogglePinned({ + peer: buildInputPeer(peer.id, peer.accessHash), + id: [storyId], + pinned: isPinned, + })); } export async function fetchStoryViewList({ + peer, storyId, areJustContacts, query, @@ -238,6 +262,7 @@ export async function fetchStoryViewList({ limit = STORY_LIST_LIMIT, offset = '', }: { + peer: ApiPeer; storyId: number; areJustContacts?: true; areReactionsFirst?: true; @@ -246,6 +271,7 @@ export async function fetchStoryViewList({ offset?: string; }) { const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({ + peer: buildInputPeer(peer.id, peer.accessHash), id: storyId, justContacts: areJustContacts, q: query, @@ -271,14 +297,9 @@ export async function fetchStoryViewList({ }; } -export async function fetchStoryLink({ userId, storyId }: { userId: string; storyId: number }) { - const inputUser = buildInputPeerFromLocalDb(userId); - if (!inputUser) { - return undefined; - } - +export async function fetchStoryLink({ peer, storyId }: { peer: ApiPeer ; storyId: number }) { const result = await invokeRequest(new GramJs.stories.ExportStoryLink({ - userId: inputUser, + peer: buildInputPeer(peer.id, peer.accessHash), id: storyId, })); @@ -290,15 +311,15 @@ export async function fetchStoryLink({ userId, storyId }: { userId: string; stor } export function reportStory({ - user, + peer, storyId, reason, description, }: { - user: ApiUser; storyId: number; reason: ApiReportReason; description?: string; + peer: ApiPeer; storyId: number; reason: ApiReportReason; description?: string; }) { return invokeRequest(new GramJs.stories.Report({ - userId: buildInputPeer(user.id, user.accessHash), + peer: buildInputPeer(peer.id, peer.accessHash), id: [storyId], reason: buildInputReportReason(reason), message: description, @@ -306,13 +327,16 @@ export function reportStory({ } export function editStoryPrivacy({ + peer, id, privacy, }: { + peer: ApiPeer; id: number; privacy: ApiInputPrivacyRules; }) { return invokeRequest(new GramJs.stories.EditStory({ + peer: buildInputPeer(peer.id, peer.accessHash), id, privacyRules: buildInputPrivacyRules(privacy), }), { @@ -321,31 +345,31 @@ export function editStoryPrivacy({ } export function toggleStoriesHidden({ - user, + peer, isHidden, }: { - user: ApiUser; + peer: ApiPeer; isHidden: boolean; }) { - return invokeRequest(new GramJs.contacts.ToggleStoriesHidden({ - id: buildInputPeer(user.id, user.accessHash), + return invokeRequest(new GramJs.stories.TogglePeerStoriesHidden({ + peer: buildInputPeer(peer.id, peer.accessHash), hidden: isHidden, })); } export function fetchStoriesMaxIds({ - users, + peers, }: { - users: ApiUser[]; + peers: ApiPeer[]; }) { - return invokeRequest(new GramJs.users.GetStoriesMaxIDs({ - id: users.map((user) => buildInputPeer(user.id, user.accessHash)), + return invokeRequest(new GramJs.stories.GetPeerMaxIDs({ + id: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), })); } -async function fetchCommonStoriesRequest({ method, userId }: { +async function fetchCommonStoriesRequest({ method, peerId }: { method: GramJs.stories.GetPinnedStories | GramJs.stories.GetStoriesArchive; - userId: string; + peerId: string; }) { const result = await invokeRequest(method); @@ -354,30 +378,33 @@ async function fetchCommonStoriesRequest({ method, userId }: { } addEntitiesToLocalDb(result.users); - result.stories.forEach((story) => addStoryToLocalDb(story, userId)); + addEntitiesToLocalDb(result.chats); + result.stories.forEach((story) => addStoryToLocalDb(story, peerId)); const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const stories = buildCollectionByCallback(result.stories, (story) => ( - [story.id, buildApiStory(userId, story)] + [story.id, buildApiStory(peerId, story)] )); return { users, + chats, stories, }; } export function sendStoryReaction({ - user, storyId, reaction, shouldAddToRecent, + peer, storyId, reaction, shouldAddToRecent, }: { - user: ApiUser; + peer: ApiPeer; storyId: number; reaction?: ApiReaction; shouldAddToRecent?: boolean; }) { return invokeRequest(new GramJs.stories.SendReaction({ reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(), - userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, + peer: buildInputPeer(peer.id, peer.accessHash), storyId, ...(shouldAddToRecent && { addToRecent: true }), }), { diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index c94c39191..bfeb4ff8a 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -2,7 +2,7 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiChat, ApiSticker, + ApiChat, ApiPeer, ApiSticker, ApiUser, OnApiUpdate, } from '../../types'; @@ -285,7 +285,7 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { }; } -export function reportSpam(userOrChat: ApiUser | ApiChat) { +export function reportSpam(userOrChat: ApiPeer) { const { id, accessHash } = userOrChat; return invokeRequest(new GramJs.messages.ReportSpam({ diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 3f9e3404f..33b3a61b1 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -1070,32 +1070,32 @@ export function updater(update: Update) { } const { story } = update; - const userId = buildApiPeerId(update.userId, 'user'); - addStoryToLocalDb(story, userId); + const peerId = getApiChatIdFromMtpPeer(update.peer); + addStoryToLocalDb(story, peerId); if (story instanceof GramJs.StoryItemDeleted) { onUpdate({ '@type': 'deleteStory', - userId, + peerId, storyId: story.id, }); } else { onUpdate({ '@type': 'updateStory', - userId, - story: buildApiStory(userId, story) as ApiStory | ApiStorySkipped, + peerId, + story: buildApiStory(peerId, story) as ApiStory | ApiStorySkipped, }); } } else if (update instanceof GramJs.UpdateReadStories) { onUpdate({ '@type': 'updateReadStories', - userId: buildApiPeerId(update.userId, 'user'), + peerId: getApiChatIdFromMtpPeer(update.peer), lastReadId: update.maxId, }); } else if (update instanceof GramJs.UpdateSentStoryReaction) { onUpdate({ '@type': 'updateSentStoryReaction', - userId: buildApiPeerId(update.userId, 'user'), + peerId: getApiChatIdFromMtpPeer(update.peer), storyId: update.storyId, reaction: buildApiReaction(update.reaction), }); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 2384052dd..1f9e437f1 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -3,7 +3,7 @@ import type { ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet, } from './messages'; import type { ApiChatInviteImporter } from './misc'; -import type { ApiFakeType, ApiUsername } from './users'; +import type { ApiFakeType, ApiUser, ApiUsername } from './users'; type ApiChatType = ( 'chatTypePrivate' | 'chatTypeSecret' | @@ -11,6 +11,8 @@ type ApiChatType = ( 'chatTypeChannel' ); +export type ApiPeer = ApiChat | ApiUser; + export interface ApiChat { id: string; folderId?: number; @@ -23,7 +25,7 @@ export interface ApiChat { unreadCount?: number; unreadMentionsCount?: number; unreadReactionsCount?: number; - isVerified?: boolean; + isVerified?: true; isMuted?: boolean; muteUntil?: number; isSignaturesShown?: boolean; @@ -35,7 +37,7 @@ export interface ApiChat { usernames?: ApiUsername[]; membersCount?: number; joinDate?: number; - isSupport?: boolean; + isSupport?: true; photos?: ApiPhoto[]; draftDate?: number; isProtected?: boolean; @@ -77,6 +79,12 @@ export interface ApiChat { unreadReactions?: number[]; unreadMentions?: number[]; + // Stories + areStoriesHidden?: boolean; + hasStories?: boolean; + hasUnreadStories?: boolean; + maxStoryId?: number; + // Locally determined field detectedLanguage?: string; } @@ -118,6 +126,7 @@ export interface ApiChatFullInfo { profilePhoto?: ApiPhoto; areParticipantsHidden?: boolean; isTranslationDisabled?: true; + hasPinnedStories?: boolean; } export interface ApiChatMember { @@ -145,6 +154,9 @@ export interface ApiChatAdminRights { anonymous?: true; manageCall?: true; manageTopics?: true; + postStories?: true; + editStories?: true; + deleteStories?: true; } export interface ApiChatBannedRights { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 5427f5dc8..90cfb8771 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -194,7 +194,7 @@ export interface ApiInvoice { receiptMsgId?: number; isTest?: boolean; isRecurring?: boolean; - recurringTermsUrl?: string; + termsUrl?: string; extendedMedia?: ApiMessageExtendedMediaPreview; maxTipAmount?: number; suggestedTipAmounts?: number[]; diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index ddd9e8787..24d94335e 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -1,10 +1,12 @@ import type { ApiPrivacySettings } from '../../types'; -import type { ApiGeoPoint, ApiMessage, ApiReaction } from './messages'; +import type { + ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, +} from './messages'; export interface ApiStory { '@type'?: 'story'; id: number; - userId: string; + peerId: string; date: number; expireDate: number; content: ApiMessage['content']; @@ -14,9 +16,11 @@ export interface ApiStory { isForContacts?: boolean; isForSelectedContacts?: boolean; isPublic?: boolean; + isOut?: true; noForwards?: boolean; viewsCount?: number; reactionsCount?: number; + reactions?: ApiReactionCount[]; recentViewerIds?: string[]; visibility?: ApiPrivacySettings; sentReaction?: ApiReaction; @@ -26,7 +30,7 @@ export interface ApiStory { export interface ApiStorySkipped { '@type'?: 'storySkipped'; id: number; - userId: string; + peerId: string; isForCloseFriends?: boolean; date: number; expireDate: number; @@ -35,15 +39,15 @@ export interface ApiStorySkipped { export interface ApiStoryDeleted { '@type'?: 'storyDeleted'; id: number; - userId: string; + peerId: string; isDeleted: true; } export type ApiTypeStory = ApiStory | ApiStorySkipped | ApiStoryDeleted; -export type ApiUserStories = { +export type ApiPeerStories = { byId: Record; - orderedIds: number[]; // Actual user stories + orderedIds: number[]; // Actual peer stories pinnedIds: number[]; // Profile Shared Media: Pinned Stories tab archiveIds?: number[]; // Profile Shared Media: Archive Stories tab lastUpdatedAt?: number; @@ -52,13 +56,13 @@ export type ApiUserStories = { export type ApiMessageStoryData = { id: number; - userId: string; + peerId: string; isMention?: boolean; }; export type ApiWebPageStoryData = { id: number; - userId: string; + peerId: string; }; export type ApiStoryView = { @@ -95,4 +99,12 @@ export type ApiMediaAreaGeoPoint = { geo: ApiGeoPoint; }; -export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint; +export type ApiMediaAreaSuggestedReaction = { + type: 'suggestedReaction'; + coordinates: ApiMediaAreaCoordinates; + reaction: ApiReaction; + isDark?: boolean; + isFlipped?: boolean; +}; + +export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index f54dcd5a4..21637ec6f 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -629,25 +629,25 @@ export type ApiRequestReconnectApi = { export type ApiUpdateStory = { '@type': 'updateStory'; - userId: string; + peerId: string; story: ApiStory | ApiStorySkipped; }; export type ApiUpdateDeleteStory = { '@type': 'deleteStory'; - userId: string; + peerId: string; storyId: number; }; export type ApiUpdateReadStories = { '@type': 'updateReadStories'; - userId: string; + peerId: string; lastReadId: number; }; export type ApiUpdateSentStoryReaction = { '@type': 'updateSentStoryReaction'; - userId: string; + peerId: string; storyId: number; reaction?: ApiReaction; }; diff --git a/src/components/calls/group/GroupCallParticipant.tsx b/src/components/calls/group/GroupCallParticipant.tsx index cd5254769..f39517754 100644 --- a/src/components/calls/group/GroupCallParticipant.tsx +++ b/src/components/calls/group/GroupCallParticipant.tsx @@ -4,7 +4,7 @@ import React, { } from '../../../lib/teact/teact'; import { withGlobal } from '../../../global'; -import type { ApiChat, ApiUser } from '../../../api/types'; +import type { ApiPeer } from '../../../api/types'; import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; import { GROUP_CALL_DEFAULT_VOLUME } from '../../../config'; @@ -31,7 +31,7 @@ type OwnProps = { }; type StateProps = { - peer?: ApiUser | ApiChat; + peer?: ApiPeer; }; const GroupCallParticipant: FC = ({ diff --git a/src/components/calls/group/GroupCallParticipantList.tsx b/src/components/calls/group/GroupCallParticipantList.tsx index 3fb8e9433..bd7540488 100644 --- a/src/components/calls/group/GroupCallParticipantList.tsx +++ b/src/components/calls/group/GroupCallParticipantList.tsx @@ -6,6 +6,7 @@ import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../ import { selectActiveGroupCall } from '../../../global/selectors/calls'; import buildClassName from '../../../util/buildClassName'; +import { compareFields } from '../../../util/iteratees'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -72,10 +73,6 @@ const GroupCallParticipantList: FC = ({ ); }; -function compareFields(a: T, b: T) { - return Number(b) - Number(a); -} - function compareParticipants(a: TypeGroupCallParticipant, b: TypeGroupCallParticipant) { return compareFields(!a.isMuted, !b.isMuted) || compareFields(a.presentation, b.presentation) diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 63897d812..a7321191c 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -3,7 +3,9 @@ import type { FC, TeactNode } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; +import type { + ApiChat, ApiPeer, ApiPhoto, ApiUser, +} from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { StoryViewerOrigin } from '../../types'; import { ApiMediaFormat } from '../../api/types'; @@ -13,8 +15,8 @@ import { getChatAvatarHash, getChatTitle, getPeerColorKey, + getPeerStoryHtmlId, getUserFullName, - getUserStoryHtmlId, isChatWithRepliesBot, isDeletedUser, isUserId, @@ -45,7 +47,7 @@ cn.icon = cn('icon'); type OwnProps = { className?: string; size?: AvatarSize; - peer?: ApiChat | ApiUser; + peer?: ApiPeer; photo?: ApiPhoto; text?: string; isSavedMessages?: boolean; @@ -55,7 +57,7 @@ type OwnProps = { withStoryGap?: boolean; withStorySolid?: boolean; storyViewerOrigin?: StoryViewerOrigin; - storyViewerMode?: 'full' | 'single-user' | 'disabled'; + storyViewerMode?: 'full' | 'single-peer' | 'disabled'; loopIndefinitely?: boolean; noPersonalPhoto?: boolean; observeIntersection?: ObserveFn; @@ -75,7 +77,7 @@ const Avatar: FC = ({ withStoryGap, withStorySolid, storyViewerOrigin, - storyViewerMode = 'single-user', + storyViewerMode = 'single-peer', loopIndefinitely, noPersonalPhoto, onClick, @@ -213,9 +215,9 @@ const Avatar: FC = ({ isDeleted && 'deleted-account', isReplies && 'replies-bot-account', isForum && 'forum', - ((withStory && user?.hasStories) || forPremiumPromo) && 'with-story-circle', - withStorySolid && user?.hasStories && 'with-story-solid', - withStorySolid && user?.hasUnreadStories && 'has-unread-story', + ((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle', + withStorySolid && peer?.hasStories && 'with-story-solid', + withStorySolid && peer?.hasUnreadStories && 'has-unread-story', onClick && 'interactive', (!isSavedMessages && !imgBlobUrl) && 'no-photo', ); @@ -223,12 +225,12 @@ const Avatar: FC = ({ const hasMedia = Boolean(isSavedMessages || imgBlobUrl); const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent) => { - if (withStory && storyViewerMode !== 'disabled' && user?.hasStories) { + if (withStory && storyViewerMode !== 'disabled' && peer?.hasStories) { e.stopPropagation(); openStoryViewer({ - userId: user.id, - isSingleUser: storyViewerMode === 'single-user', + peerId: peer.id, + isSinglePeer: storyViewerMode === 'single-peer', origin: storyViewerOrigin, }); return; @@ -243,7 +245,7 @@ const Avatar: FC = ({
= ({
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
- {withStory && user?.hasStories && ( - + {withStory && peer?.hasStories && ( + )}
); diff --git a/src/components/common/AvatarList.tsx b/src/components/common/AvatarList.tsx index 61959ba41..e8db4206b 100644 --- a/src/components/common/AvatarList.tsx +++ b/src/components/common/AvatarList.tsx @@ -1,7 +1,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact'; -import type { ApiChat, ApiUser } from '../../api/types'; +import type { ApiPeer } from '../../api/types'; import type { AvatarSize } from './Avatar'; import buildClassName from '../../util/buildClassName'; @@ -14,7 +14,7 @@ import styles from './AvatarList.module.scss'; type OwnProps = { size: AvatarSize; - peers?: (ApiUser | ApiChat)[]; + peers?: ApiPeer[]; className?: string; }; diff --git a/src/components/common/AvatarStoryCircle.tsx b/src/components/common/AvatarStoryCircle.tsx index e5f212f1e..1d5eda837 100644 --- a/src/components/common/AvatarStoryCircle.tsx +++ b/src/components/common/AvatarStoryCircle.tsx @@ -6,14 +6,14 @@ import { withGlobal } from '../../global'; import type { ThemeKey } from '../../types'; import type { AvatarSize } from './Avatar'; -import { selectTheme, selectUser, selectUserStories } from '../../global/selectors'; +import { selectPeerStories, selectTheme, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { DPR } from '../../util/windowEnvironment'; import { REM } from './helpers/mediaDimensions'; interface OwnProps { // eslint-disable-next-line react/no-unused-prop-types - userId: string; + peerId: string; className?: string; size: AvatarSize; withExtraGap?: boolean; @@ -108,15 +108,15 @@ function AvatarStoryCircle({ ); } -export default memo(withGlobal((global, { userId }): StateProps => { - const user = selectUser(global, userId); - const userStories = selectUserStories(global, userId); +export default memo(withGlobal((global, { peerId }): StateProps => { + const user = selectUser(global, peerId); + const peerStories = selectPeerStories(global, peerId); const appTheme = selectTheme(global); return { isCloseFriend: user?.isCloseFriend, - storyIds: userStories?.orderedIds, - lastReadId: userStories?.lastReadId, + storyIds: peerStories?.orderedIds, + lastReadId: peerStories?.lastReadId, appTheme, }; })(AvatarStoryCircle)); diff --git a/src/components/common/ChatExtra.tsx b/src/components/common/ChatExtra.tsx index 3dff47f25..cbfc61f56 100644 --- a/src/components/common/ChatExtra.tsx +++ b/src/components/common/ChatExtra.tsx @@ -34,6 +34,7 @@ import { debounce } from '../../util/schedulers'; import stopEvent from '../../util/stopEvent'; import renderText from './helpers/renderText'; +import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -77,7 +78,7 @@ const ChatExtra: FC = ({ showNotification, updateChatMutedState, updateTopicMutedState, - loadUserStories, + loadPeerStories, } = getActions(); const { @@ -87,6 +88,7 @@ const ChatExtra: FC = ({ isSelf, } = user || {}; const { id: chatId, usernames: chatUsernames } = chat || {}; + const peerId = userId || chatId; const lang = useLang(); const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted); @@ -98,9 +100,15 @@ const ChatExtra: FC = ({ useEffect(() => { if (!userId) return; loadFullUser({ userId }); - loadUserStories({ userId }); }, [userId]); + useEffectWithPrevDeps(([prevPeerId]) => { + if (!peerId || prevPeerId === peerId) return; + if (user || (chat && isChatChannel(chat))) { + loadPeerStories({ peerId }); + } + }, [peerId, chat, user]); + const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); const activeUsernames = useMemo(() => { diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 63a3de90a..c90a9ae5d 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -34,6 +34,7 @@ import type { IAnchorPosition, InlineBotSettings, ISettings } from '../../types' import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_MODAL_ID, + HEART_REACTION, MAX_UPLOAD_FILEPART_SIZE, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, @@ -68,6 +69,7 @@ import { selectIsReactionPickerOpen, selectIsRightColumnShown, selectNewestMessageWithBotKeyboardButtons, + selectPeerStory, selectReplyingToId, selectRequestedDraftFiles, selectRequestedDraftText, @@ -76,7 +78,6 @@ import { selectTheme, selectUser, selectUserFullInfo, - selectUserStory, } from '../../global/selectors'; import { selectCurrentLimit } from '../../global/selectors/limits'; import buildClassName from '../../util/buildClassName'; @@ -261,10 +262,6 @@ const MESSAGE_MAX_LENGTH = 4096; const SENDING_ANIMATION_DURATION = 350; const MOUNT_ANIMATION_DURATION = 430; -const HEART_REACTION: ApiReaction = { - emoticon: '❤', -}; - const Composer: FC = ({ type, isOnActiveTab, @@ -779,7 +776,7 @@ const Composer: FC = ({ if (storyReactionPickerPosition) { openStoryReactionPicker({ - storyUserId: chatId, + peerId: chatId, storyId: storyId!, position: storyReactionPickerPosition, }); @@ -1412,7 +1409,7 @@ const Composer: FC = ({ const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { openStoryReactionPicker({ - storyUserId: chatId, + peerId: chatId, storyId: storyId!, position, sendAsMessage: true, @@ -1421,7 +1418,12 @@ const Composer: FC = ({ const handleLikeStory = useLastCallback(() => { const reaction = sentStoryReaction ? undefined : HEART_REACTION; - sendStoryReaction({ userId: chatId, storyId: storyId!, reaction }); + sendStoryReaction({ + peerId: chatId, + storyId: storyId!, + containerId: getStoryKey(chatId, storyId!), + reaction, + }); }); const handleSendScheduled = useLastCallback(() => { @@ -1816,6 +1818,7 @@ const Composer: FC = ({ > {sentStoryReaction && ( ( const replyingToId = selectReplyingToId(global, chatId, threadId); - const story = storyId && selectUserStory(global, chatId, storyId); + const story = storyId && selectPeerStory(global, chatId, storyId); const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; return { diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx index 49fd44058..24eccff6b 100644 --- a/src/components/common/CustomEmoji.tsx +++ b/src/components/common/CustomEmoji.tsx @@ -41,6 +41,7 @@ type OwnProps = { observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; onClick?: NoneToVoidFunction; + onAnimationEnd?: NoneToVoidFunction; }; const STICKER_SIZE = 20; @@ -65,6 +66,7 @@ const CustomEmoji: FC = ({ observeIntersectionForLoading, observeIntersectionForPlaying, onClick, + onAnimationEnd, }) => { // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); @@ -119,6 +121,7 @@ const CustomEmoji: FC = ({ withGridFix && styles.withGridFix, )} onClick={onClick} + onAnimationEnd={onAnimationEnd} data-entity-type={ApiMessageEntityTypes.CustomEmoji} data-document-id={documentId} data-alt={customEmoji?.emoji} diff --git a/src/components/common/EmbeddedMessage.tsx b/src/components/common/EmbeddedMessage.tsx index 663c595bc..68a34faa2 100644 --- a/src/components/common/EmbeddedMessage.tsx +++ b/src/components/common/EmbeddedMessage.tsx @@ -2,8 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { useRef } from '../../lib/teact/teact'; import type { - ApiChat, - ApiMessage, ApiUser, + ApiMessage, ApiPeer, } from '../../api/types'; import type { ChatTranslatedMessages } from '../../global/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; @@ -37,7 +36,7 @@ import './EmbeddedMessage.scss'; type OwnProps = { className?: string; message?: ApiMessage; - sender?: ApiUser | ApiChat; + sender?: ApiPeer; title?: string; customText?: string; noUserColors?: boolean; diff --git a/src/components/common/EmbeddedStory.tsx b/src/components/common/EmbeddedStory.tsx index 1c282706d..e24f0b5cd 100644 --- a/src/components/common/EmbeddedStory.tsx +++ b/src/components/common/EmbeddedStory.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { useRef } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiChat, ApiTypeStory, ApiUser } from '../../api/types'; +import type { ApiPeer, ApiTypeStory } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { @@ -24,7 +24,7 @@ import './EmbeddedMessage.scss'; type OwnProps = { story?: ApiTypeStory; - sender?: ApiUser | ApiChat; + sender?: ApiPeer; noUserColors?: boolean; isProtected?: boolean; observeIntersectionForLoading?: ObserveFn; diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index bae926448..8bd5a53ab 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiChat, ApiUser } from '../../api/types'; +import type { ApiChat, ApiPeer, ApiUser } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../config'; @@ -23,7 +23,7 @@ import VerifiedIcon from './VerifiedIcon'; import styles from './FullNameTitle.module.scss'; type OwnProps = { - peer: ApiChat | ApiUser; + peer: ApiPeer; className?: string; noVerified?: boolean; noFake?: boolean; diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 2dca491a7..0f36cbdd4 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -6,7 +6,7 @@ import type { ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; -import { MediaViewerOrigin } from '../../types'; +import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types'; import { getChatTypeString, @@ -51,6 +51,8 @@ type OwnProps = { noRtl?: boolean; noAvatar?: boolean; noStatusOrTyping?: boolean; + withStory?: boolean; + storyViewerOrigin?: StoryViewerOrigin; onClick?: VoidFunction; }; @@ -84,6 +86,8 @@ const GroupChatInfo: FC = ({ topic, messagesCount, noStatusOrTyping, + withStory, + storyViewerOrigin, onClick, }) => { const { @@ -186,6 +190,9 @@ const GroupChatInfo: FC = ({ key={chat.id} size={avatarSize} peer={chat} + withStory={withStory} + storyViewerOrigin={storyViewerOrigin} + storyViewerMode="single-peer" onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} /> )} diff --git a/src/components/common/Icon.tsx b/src/components/common/Icon.tsx new file mode 100644 index 000000000..e147e8cd2 --- /dev/null +++ b/src/components/common/Icon.tsx @@ -0,0 +1,27 @@ +import React from '../../lib/teact/teact'; + +import type { IconName } from '../../types/icons'; + +import buildClassName from '../../util/buildClassName'; + +type OwnProps = { + name: IconName; + className?: string; + style?: string; +}; + +const Icon = ({ + name, + className, + style, +}: OwnProps) => { + return ( + + ); +}; + +export default Icon; diff --git a/src/components/common/PickerSelectedItem.tsx b/src/components/common/PickerSelectedItem.tsx index 23de1976c..ee54e9c32 100644 --- a/src/components/common/PickerSelectedItem.tsx +++ b/src/components/common/PickerSelectedItem.tsx @@ -5,7 +5,7 @@ import { withGlobal } from '../../global'; import type { ApiChat, ApiUser } from '../../api/types'; import type { IconName } from '../../types/icons'; -import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers'; +import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers'; import { selectChat, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import renderText from './helpers/renderText'; @@ -111,8 +111,8 @@ export default memo(withGlobal( return {}; } - const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; - const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; + const chat = selectChat(global, chatOrUserId); + const user = selectUser(global, chatOrUserId); const isSavedMessages = !forceShowSelf && user && user.isSelf; return { diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index b7c2c4e43..6261bdd21 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -188,7 +188,7 @@ const PrivateChatInfo: FC = ({ isSavedMessages={isSavedMessages} withStory={withStory} storyViewerOrigin={storyViewerOrigin} - storyViewerMode="single-user" + storyViewerMode="single-peer" onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} />
diff --git a/src/components/common/ReportModal.tsx b/src/components/common/ReportModal.tsx index 4c3942a34..4b7cba088 100644 --- a/src/components/common/ReportModal.tsx +++ b/src/components/common/ReportModal.tsx @@ -18,8 +18,7 @@ import RadioGroup from '../ui/RadioGroup'; export type OwnProps = { isOpen: boolean; subject?: 'peer' | 'messages' | 'media' | 'story'; - chatId?: string; - userId?: string; + peerId?: string; photo?: ApiPhoto; messageIds?: number[]; storyId?: number; @@ -30,8 +29,7 @@ export type OwnProps = { const ReportModal: FC = ({ isOpen, subject = 'messages', - chatId, - userId, + peerId, photo, messageIds, storyId, @@ -56,16 +54,16 @@ const ReportModal: FC = ({ exitMessageSelectMode(); break; case 'peer': - reportPeer({ chatId, reason: selectedReason, description }); + reportPeer({ chatId: peerId, reason: selectedReason, description }); break; case 'media': reportProfilePhoto({ - chatId, photo, reason: selectedReason, description, + chatId: peerId, photo, reason: selectedReason, description, }); break; case 'story': reportStory({ - userId: userId!, storyId: storyId!, reason: selectedReason, description, + peerId: peerId!, storyId: storyId!, reason: selectedReason, description, }); } onClose(); @@ -94,9 +92,9 @@ const ReportModal: FC = ({ if ( (subject === 'messages' && !messageIds) - || (subject === 'peer' && !chatId) - || (subject === 'media' && (!chatId || !photo)) - || (subject === 'story' && (!storyId || !userId)) + || (subject === 'peer' && !peerId) + || (subject === 'media' && (!peerId || !photo)) + || (subject === 'story' && (!storyId || !peerId)) ) { return undefined; } diff --git a/src/components/common/UserLink.tsx b/src/components/common/UserLink.tsx index 0e37dd566..3af0df2a4 100644 --- a/src/components/common/UserLink.tsx +++ b/src/components/common/UserLink.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { useCallback } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiChat, ApiUser } from '../../api/types'; +import type { ApiPeer } from '../../api/types'; import buildClassName from '../../util/buildClassName'; @@ -10,7 +10,7 @@ import Link from '../ui/Link'; type OwnProps = { className?: string; - sender?: ApiUser | ApiChat; + sender?: ApiPeer; children: React.ReactNode; }; diff --git a/src/components/common/reactions/CustomEmojiEffect.module.scss b/src/components/common/reactions/CustomEmojiEffect.module.scss index e4b962414..8ceeaea42 100644 --- a/src/components/common/reactions/CustomEmojiEffect.module.scss +++ b/src/components/common/reactions/CustomEmojiEffect.module.scss @@ -4,11 +4,12 @@ } .particle { + --custom-emoji-size: var(--particle-size, 1rem); color: var(--color-primary); position: absolute; - width: 1rem; - height: 1rem; + width: var(--particle-size, 1rem); + height: var(--particle-size, 1rem); border-radius: 0.25rem; offset-path: var(--offset-path); offset-rotate: 0deg; diff --git a/src/components/common/reactions/CustomEmojiEffect.tsx b/src/components/common/reactions/CustomEmojiEffect.tsx index 210d6d23e..f986332bd 100644 --- a/src/components/common/reactions/CustomEmojiEffect.tsx +++ b/src/components/common/reactions/CustomEmojiEffect.tsx @@ -5,6 +5,7 @@ import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types' import { getStickerPreviewHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; +import buildStyle from '../../../util/buildStyle'; import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment'; import useMedia from '../../../hooks/useMedia'; @@ -17,6 +18,8 @@ type OwnProps = { reaction: ApiReactionCustomEmoji | ApiEmojiStatus; className?: string; isLottie?: boolean; + particleSize?: number; + onEnded?: NoneToVoidFunction; }; const EFFECT_AMOUNT = 7; @@ -25,6 +28,8 @@ const CustomEmojiEffect: FC = ({ reaction, isLottie, className, + particleSize, + onEnded, }) => { const stickerHash = getStickerPreviewHash(reaction.documentId); @@ -32,16 +37,19 @@ const CustomEmojiEffect: FC = ({ const paths: string[] = useMemo(() => { if (!IS_OFFSET_PATH_SUPPORTED) return []; - return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath()); - }, []); + return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath(particleSize)); + }, [particleSize]); if (!previewMediaData && !isLottie) { return undefined; } return ( -
- {paths.map((path) => { +
+ {paths.map((path, i) => { const style = `--offset-path: path('${path}');`; if (isLottie) { return ( @@ -50,6 +58,8 @@ const CustomEmojiEffect: FC = ({ className={styles.particle} style={style} withSharedAnimation + size={particleSize} + onAnimationEnd={i === 0 ? onEnded : undefined} /> ); } @@ -61,6 +71,7 @@ const CustomEmojiEffect: FC = ({ className={styles.particle} style={style} draggable={false} + onAnimationEnd={i === 0 ? onEnded : undefined} /> ); })} @@ -70,9 +81,9 @@ const CustomEmojiEffect: FC = ({ export default memo(CustomEmojiEffect); -function generateRandomDropPath() { - const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1); - const y = 20 + Math.random() * 80; +function generateRandomDropPath(particleSize = 20) { + const x = (particleSize / 2 + Math.random() * particleSize * 3) * (Math.random() > 0.5 ? 1 : -1); + const y = particleSize + Math.random() * particleSize * 4; - return `M 0 0 C 0 0 ${x} ${-y - 20} ${x} ${y}`; + return `M 0 0 C 0 0 ${x} ${-y - particleSize} ${x} ${y}`; } diff --git a/src/components/common/reactions/ReactionAnimatedEmoji.module.scss b/src/components/common/reactions/ReactionAnimatedEmoji.module.scss index 7189da4a1..337e49f71 100644 --- a/src/components/common/reactions/ReactionAnimatedEmoji.module.scss +++ b/src/components/common/reactions/ReactionAnimatedEmoji.module.scss @@ -12,13 +12,6 @@ z-index: 1; } -.animated-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - .effect { position: fixed; top: -2.25rem; @@ -44,7 +37,7 @@ contain: layout; } -.withEffectOnly { +.withEffectOnly, .animated-icon { position: absolute; top: 50%; left: 50%; diff --git a/src/components/common/reactions/ReactionAnimatedEmoji.tsx b/src/components/common/reactions/ReactionAnimatedEmoji.tsx index c8319dcd0..29658e174 100644 --- a/src/components/common/reactions/ReactionAnimatedEmoji.tsx +++ b/src/components/common/reactions/ReactionAnimatedEmoji.tsx @@ -33,6 +33,8 @@ type OwnProps = { size?: number; effectSize?: number; withEffectOnly?: boolean; + shouldPause?: boolean; + shouldLoop?: boolean; observeIntersection?: ObserveFn; }; @@ -46,6 +48,8 @@ type StateProps = { const ICON_SIZE = 1.5 * REM; const CENTER_ICON_MULTIPLIER = 1.9; const EFFECT_SIZE = 6.5 * REM; +const CUSTOM_EMOJI_EFFECT_MULTIPLIER = 0.5; +const MIN_PARTICLE_SIZE = REM; const ReactionAnimatedEmoji = ({ containerId, @@ -58,6 +62,8 @@ const ReactionAnimatedEmoji = ({ genericEffects, withEffects, withEffectOnly, + shouldPause, + shouldLoop, observeIntersection, }: OwnProps & StateProps) => { const { stopActiveReaction } = getActions(); @@ -110,18 +116,25 @@ const ReactionAnimatedEmoji = ({ activeReactions?.find((active) => isSameReaction(active, reaction)) ), [activeReactions, reaction]); - const shouldPlay = Boolean(withEffects && activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect); + const shouldPlayEffect = Boolean( + withEffects && activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect, + ); + const shouldPlayCenter = isIntersecting && ((shouldPlayEffect && !withEffectOnly) || shouldLoop); const { - shouldRender: shouldRenderAnimation, + shouldRender: shouldRenderEffect, transitionClassNames: animationClassNames, - } = useShowTransition(shouldPlay, undefined, true, 'slow'); + } = useShowTransition(shouldPlayEffect, undefined, true, 'slow'); + const { + shouldRender: shouldRenderCenter, + transitionClassNames: centerAnimationClassNames, + } = useShowTransition(shouldPlayCenter, undefined, true, 'slow'); const handleEnded = useLastCallback(() => { stopActiveReaction({ containerId, reaction }); }); const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag(); - const shouldShowStatic = !isCustom && (!shouldPlay || !isAnimationLoaded); + const shouldShowStatic = !isCustom && (!shouldPlayCenter || !isAnimationLoaded); const { shouldRender: shouldRenderStatic, transitionClassNames: staticClassNames, @@ -129,7 +142,7 @@ const ReactionAnimatedEmoji = ({ const rootClassName = buildClassName( styles.root, - shouldRenderAnimation && styles.animating, + shouldRenderEffect && styles.animating, withEffectOnly && styles.withEffectOnly, className, ); @@ -150,13 +163,28 @@ const ReactionAnimatedEmoji = ({ documentId={reaction.documentId} className={styles.customEmoji} size={size} + noPlay={shouldPause} + forceAlways observeIntersectionForPlaying={observeIntersection} /> )} - {shouldRenderAnimation && ( + {shouldRenderCenter && !isCustom && ( + + )} + {shouldRenderEffect && ( <> - {isCustom && !assignedEffectId && isIntersecting && } - {!isCustom && !withEffectOnly && ( - )} diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 9efbdedbc..b896a7782 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -7,6 +7,7 @@ import type { ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus, + ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, @@ -86,7 +87,7 @@ type StateProps = { actionTargetUserIds?: string[]; actionTargetMessage?: ApiMessage; actionTargetChatId?: string; - lastMessageSender?: ApiUser | ApiChat; + lastMessageSender?: ApiPeer; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; draft?: ApiFormattedText; isSelected?: boolean; @@ -264,10 +265,10 @@ const Chat: FC = ({
@@ -328,7 +329,7 @@ const Chat: FC = ({ isOpen={isReportModalOpen} onClose={closeReportModal} onCloseAnimationEnd={unmarkRenderReportModal} - chatId={chatId} + peerId={chatId} subject="peer" /> )} diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 21f19d8c4..0b473540c 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -355,7 +355,7 @@ export default memo(withGlobal( }, }, stories: { - orderedUserIds: { + orderedPeerIds: { archived: archivedStories, }, }, diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 49d46f69f..c599c9457 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -23,7 +23,7 @@ import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager'; import { getServerTime } from '../../../util/serverTime'; import { IS_APP, IS_MAC_OS } from '../../../util/windowEnvironment'; -import useUserStoriesPolling from '../../../hooks/polling/useUserStoriesPolling'; +import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling'; import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll'; import useDebouncedCallback from '../../../hooks/useDebouncedCallback'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; @@ -89,7 +89,7 @@ const ChatList: FC = ({ const shouldDisplayArchive = isAllFolder && canDisplayArchive; const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId); - useUserStoriesPolling(orderedIds); + usePeerStoriesPolling(orderedIds); const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX; const archiveHeight = shouldDisplayArchive diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index 2633360a5..32c740f7a 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -4,8 +4,7 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiChat, ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus, - ApiTopic, ApiTypingStatus, - ApiUser, + ApiPeer, ApiTopic, ApiTypingStatus, } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ChatAnimationTypes } from './hooks'; @@ -61,7 +60,7 @@ type StateProps = { lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; actionTargetMessage?: ApiMessage; actionTargetUserIds?: string[]; - lastMessageSender?: ApiUser | ApiChat; + lastMessageSender?: ApiPeer; actionTargetChatId?: string; typingStatus?: ApiTypingStatus; draft?: ApiFormattedText; diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index bf92f3219..1603c9584 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -4,7 +4,7 @@ import React, { import { getGlobal } from '../../../../global'; import type { - ApiChat, ApiMessage, ApiTopic, ApiTypingStatus, ApiUser, + ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, } from '../../../../api/types'; import type { Thread } from '../../../../global/types'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; @@ -64,7 +64,7 @@ export default function useChatListEntry({ actionTargetMessage?: ApiMessage; actionTargetUserIds?: string[]; lastMessageTopic?: ApiTopic; - lastMessageSender?: ApiUser | ApiChat; + lastMessageSender?: ApiPeer; actionTargetChatId?: string; observeIntersection?: ObserveFn; isTopic?: boolean; diff --git a/src/components/left/search/LeftSearchResultChat.tsx b/src/components/left/search/LeftSearchResultChat.tsx index 5d14b50d3..4171f3b5d 100644 --- a/src/components/left/search/LeftSearchResultChat.tsx +++ b/src/components/left/search/LeftSearchResultChat.tsx @@ -96,7 +96,13 @@ const LeftSearchResultChat: FC = ({ storyViewerOrigin={StoryViewerOrigin.SearchResult} /> ) : ( - + )} {shouldRenderMuteModal && ( = ({ {canUpgrade && ( )}
diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 1de0fbff1..379b9d106 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -5,7 +5,7 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { - ApiChat, ApiMessage, ApiPhoto, ApiUser, + ApiMessage, ApiPeer, ApiPhoto, ApiUser, } from '../../api/types'; import { MediaViewerOrigin } from '../../types'; @@ -63,7 +63,7 @@ type StateProps = { isChatWithSelf?: boolean; canUpdateMedia?: boolean; origin?: MediaViewerOrigin; - avatarOwner?: ApiChat | ApiUser; + avatarOwner?: ApiPeer; avatarOwnerFallbackPhoto?: ApiPhoto; message?: ApiMessage; chatMessages?: Record; @@ -205,7 +205,7 @@ const MediaViewer: FC = ({ const prevIsHidden = usePrevious(isHidden); const prevOrigin = usePrevious(origin); const prevMediaId = usePrevious(mediaId); - const prevAvatarOwner = usePrevious(avatarOwner); + const prevAvatarOwner = usePrevious(avatarOwner); const prevBestImageData = usePrevious(bestImageData); const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined; const hasFooter = Boolean(textParts); @@ -362,7 +362,7 @@ const MediaViewer: FC = ({ onClose={closeReportModal} subject="media" photo={avatarPhoto} - chatId={avatarOwner?.id} + peerId={avatarOwner?.id} />
void; diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index 783d05214..fabf6ea5c 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; import type { - ApiChat, ApiDimensions, ApiMessage, ApiUser, + ApiDimensions, ApiMessage, ApiPeer, } from '../../api/types'; import { MediaViewerOrigin } from '../../types'; @@ -46,7 +46,7 @@ type StateProps = { mediaId?: number; senderId?: string; threadId?: number; - avatarOwner?: ApiChat | ApiUser; + avatarOwner?: ApiPeer; message?: ApiMessage; origin?: MediaViewerOrigin; isProtected?: boolean; diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index 577c7cebf..1bf471c23 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -2,14 +2,13 @@ import type { FC } from '../../lib/teact/teact'; import React from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiChat, ApiMessage, ApiUser } from '../../api/types'; +import type { ApiMessage, ApiPeer } from '../../api/types'; -import { getSenderTitle, isUserId } from '../../global/helpers'; +import { getSenderTitle } from '../../global/helpers'; import { - selectChat, selectChatMessage, + selectPeer, selectSender, - selectUser, } from '../../global/selectors'; import { formatMediaDateTime } from '../../util/dateFormat'; import renderText from '../common/helpers/renderText'; @@ -30,7 +29,7 @@ type OwnProps = { }; type StateProps = { - sender?: ApiUser | ApiChat; + sender?: ApiPeer; message?: ApiMessage; }; @@ -96,7 +95,7 @@ export default withGlobal( (global, { chatId, messageId, isAvatar }): StateProps => { if (isAvatar && chatId) { return { - sender: isUserId(chatId) ? selectUser(global, chatId) : selectChat(global, chatId), + sender: selectPeer(global, chatId), }; } diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index 11d5f674d..3c4d7a3ed 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -1,21 +1,20 @@ -import { requestMutation } from '../../../lib/fasterdom/fasterdom'; - import type { ApiDimensions, ApiMessage } from '../../../api/types'; import { MediaViewerOrigin } from '../../../types'; import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config'; +import { requestMutation } from '../../../lib/fasterdom/fasterdom'; +import { getMessageHtmlId } from '../../../global/helpers'; +import { applyStyles } from '../../../util/animation'; +import { isElementInViewport } from '../../../util/isElementInViewport'; +import stopEvent from '../../../util/stopEvent'; +import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; +import windowSize from '../../../util/windowSize'; import { calculateDimensions, getMediaViewerAvailableDimensions, MEDIA_VIEWER_MEDIA_QUERY, REM, } from '../../common/helpers/mediaDimensions'; -import windowSize from '../../../util/windowSize'; -import stopEvent from '../../../util/stopEvent'; -import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; -import { getMessageHtmlId } from '../../../global/helpers'; -import { isElementInViewport } from '../../../util/isElementInViewport'; -import { applyStyles } from '../../../util/animation'; const ANIMATION_DURATION = 200; diff --git a/src/components/mediaViewer/hooks/useMediaProps.ts b/src/components/mediaViewer/hooks/useMediaProps.ts index ec4d06c2b..4c83498fe 100644 --- a/src/components/mediaViewer/hooks/useMediaProps.ts +++ b/src/components/mediaViewer/hooks/useMediaProps.ts @@ -1,7 +1,7 @@ import { useMemo } from '../../../lib/teact/teact'; import type { - ApiChat, ApiMessage, ApiUser, + ApiMessage, ApiPeer, } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types'; import { MediaViewerOrigin } from '../../../types'; @@ -34,7 +34,7 @@ import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; type UseMediaProps = { mediaId?: number; message?: ApiMessage; - avatarOwner?: ApiChat | ApiUser; + avatarOwner?: ApiPeer; origin?: MediaViewerOrigin; delay: number | false; }; diff --git a/src/components/middle/AudioPlayer.tsx b/src/components/middle/AudioPlayer.tsx index 92bd2657c..61daa2b2c 100644 --- a/src/components/middle/AudioPlayer.tsx +++ b/src/components/middle/AudioPlayer.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useRef } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { - ApiAudio, ApiChat, ApiMessage, ApiUser, + ApiAudio, ApiChat, ApiMessage, ApiPeer, } from '../../api/types'; import type { AudioOrigin } from '../../types'; @@ -42,7 +42,7 @@ type OwnProps = { }; type StateProps = { - sender?: ApiChat | ApiUser; + sender?: ApiPeer; chat?: ApiChat; volume: number; playbackRate: number; diff --git a/src/components/middle/ChatReportPanel.tsx b/src/components/middle/ChatReportPanel.tsx index f41632b26..70485c195 100644 --- a/src/components/middle/ChatReportPanel.tsx +++ b/src/components/middle/ChatReportPanel.tsx @@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global'; import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types'; import { - getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup, isUserId, + getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup, } from '../../global/helpers'; import { selectChat, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; @@ -174,6 +174,6 @@ export default memo(withGlobal( (global, { chatId }): StateProps => ({ currentUserId: global.currentUserId, chat: selectChat(global, chatId), - user: isUserId(chatId) ? selectUser(global, chatId) : undefined, + user: selectUser(global, chatId), }), )(ChatReportPanel)); diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index ddcc4d79a..6e592fd9f 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -630,7 +630,7 @@ const HeaderMenuContainer: FC = ({ isOpen={isReportModalOpen} onClose={closeReportModal} subject="peer" - chatId={chat.id} + peerId={chat.id} /> )}
diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index f0d783cae..6fa5f884a 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -173,7 +173,7 @@ const MessageList: FC = ({ }) => { const { loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, - loadMessageViews, loadUserStoriesByIds, + loadMessageViews, loadPeerStoriesByIds, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -267,17 +267,17 @@ const MessageList: FC = ({ if (!storyDataList.length) return; - const storiesByUserIds = storyDataList.reduce((acc, storyData) => { - const { userId, id } = storyData!; - if (!acc[userId]) { - acc[userId] = []; + const storiesByPeerIds = storyDataList.reduce((acc, storyData) => { + const { peerId, id } = storyData!; + if (!acc[peerId]) { + acc[peerId] = []; } - acc[userId].push(id); + acc[peerId].push(id); return acc; }, {} as Record); - Object.entries(storiesByUserIds).forEach(([userId, storyIds]) => { - loadUserStoriesByIds({ userId, storyIds }); + Object.entries(storiesByPeerIds).forEach(([peerId, storyIds]) => { + loadPeerStoriesByIds({ peerId, storyIds }); }); }, MESSAGE_STORY_POLLING_INTERVAL); diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 2c8966f8c..f46d950f9 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -5,7 +5,7 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { - ApiChat, ApiMessage, ApiTypingStatus, ApiUser, + ApiChat, ApiMessage, ApiPeer, ApiTypingStatus, } from '../../api/types'; import type { GlobalState, MessageListType } from '../../global/types'; import type { Signal } from '../../util/signals'; @@ -98,7 +98,7 @@ type StateProps = { pinnedMessageIds?: number[] | number; messagesById?: Record; canUnpin?: boolean; - topMessageSender?: ApiChat | ApiUser; + topMessageSender?: ApiPeer; typingStatus?: ApiTypingStatus; isSelectModeActive?: boolean; isLeftColumnShown?: boolean; @@ -401,6 +401,8 @@ const MiddleHeader: FC = ({ withMediaViewer={threadId === MAIN_THREAD_ID} withFullInfo={threadId === MAIN_THREAD_ID} withUpdatingStatus + withStory + storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar} noRtl /> )} diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index a97a02ccc..d9fb4c2db 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -4,12 +4,11 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; +import type { ApiMessage, ApiPeer } from '../../../api/types'; -import { isUserId, stripCustomEmoji } from '../../../global/helpers'; +import { stripCustomEmoji } from '../../../global/helpers'; import { selectCanAnimateInterface, - selectChat, selectChatMessage, selectCurrentMessageList, selectEditingId, @@ -18,10 +17,10 @@ import { selectForwardedSender, selectIsChatWithSelf, selectIsCurrentUserPremium, + selectPeer, selectReplyingToId, selectSender, selectTabState, - selectUser, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; @@ -45,7 +44,7 @@ type StateProps = { replyingToId?: number; editingId?: number; message?: ApiMessage; - sender?: ApiUser | ApiChat; + sender?: ApiPeer; shouldAnimate?: boolean; forwardedMessagesCount?: number; noAuthors?: boolean; @@ -313,7 +312,7 @@ export default memo(withGlobal( message = forwardedMessages?.[0]; } - let sender: ApiChat | ApiUser | undefined; + let sender: ApiPeer | undefined; if (replyingToId && message && !shouldForceShowEditing) { const { forwardInfo } = message; const isChatWithSelf = selectIsChatWithSelf(global, chatId); @@ -332,7 +331,7 @@ export default memo(withGlobal( } } if (!sender) { - sender = isUserId(fromChatId!) ? selectUser(global, fromChatId!) : selectChat(global, fromChatId!); + sender = selectPeer(global, fromChatId!); } } diff --git a/src/components/middle/message/BaseStory.tsx b/src/components/middle/message/BaseStory.tsx index dc3045e2e..108cec1ac 100644 --- a/src/components/middle/message/BaseStory.tsx +++ b/src/components/middle/message/BaseStory.tsx @@ -16,6 +16,8 @@ import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import useShowTransition from '../../../hooks/useShowTransition'; +import MediaAreaOverlay from '../../story/mediaArea/MediaAreaOverlay'; + import styles from './BaseStory.module.scss'; interface OwnProps { @@ -28,7 +30,7 @@ interface OwnProps { function BaseStory({ story, isPreview, isProtected, isConnected, }: OwnProps) { - const { openStoryViewer, loadUserStoriesByIds, showNotification } = getActions(); + const { openStoryViewer, loadPeerStoriesByIds, showNotification } = getActions(); const lang = useLang(); const { isMobile } = useAppLayout(); @@ -56,7 +58,7 @@ function BaseStory({ useEffect(() => { if (story && !(isLoaded || isExpired)) { - loadUserStoriesByIds({ userId: story.userId, storyIds: [story.id] }); + loadPeerStoriesByIds({ peerId: story.peerId, storyIds: [story.id] }); } }, [story, isExpired, isLoaded]); @@ -69,9 +71,9 @@ function BaseStory({ } openStoryViewer({ - userId: story!.userId, + peerId: story!.peerId, storyId: story!.id, - isSingleUser: true, + isSinglePeer: true, isSingleStory: true, }); }); @@ -83,12 +85,15 @@ function BaseStory({ > {!isExpired && isPreview && } {shouldRender && ( - + <> + + {isLoaded && } + )} {isExpired && ( diff --git a/src/components/middle/message/CommentButton.tsx b/src/components/middle/message/CommentButton.tsx index b95984d41..b20fe63e2 100644 --- a/src/components/middle/message/CommentButton.tsx +++ b/src/components/middle/message/CommentButton.tsx @@ -6,7 +6,7 @@ import type { ApiThreadInfo, } from '../../../api/types'; -import { isUserId } from '../../../global/helpers'; +import { selectPeer } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { formatIntegerCompact } from '../../../util/textFormat'; @@ -44,10 +44,10 @@ const CommentButton: FC = ({ } // No need for expensive global updates on chats and users, so we avoid them - const { users: { byId: usersById }, chats: { byId: chatsById } } = getGlobal(); + const global = getGlobal(); return recentReplierIds.map((peerId) => { - return isUserId(peerId) ? usersById[peerId] : chatsById[peerId]; + return selectPeer(global, peerId); }).filter(Boolean); }, [recentReplierIds]); diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index 554553b19..651e59756 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -4,7 +4,7 @@ import React, { } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; -import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; +import type { ApiMessage, ApiPeer } from '../../../api/types'; import type { ISettings } from '../../../types'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; @@ -47,7 +47,7 @@ const DEFAULT_MAP_CONFIG = { type OwnProps = { message: ApiMessage; - peer?: ApiUser | ApiChat; + peer?: ApiPeer; isInSelectMode?: boolean; isSelected?: boolean; theme: ISettings['theme']; diff --git a/src/components/middle/message/MentionLink.tsx b/src/components/middle/message/MentionLink.tsx index bc71ccb64..0b57db01f 100644 --- a/src/components/middle/message/MentionLink.tsx +++ b/src/components/middle/message/MentionLink.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiChat, ApiUser } from '../../../api/types'; +import type { ApiPeer } from '../../../api/types'; import { ApiMessageEntityTypes } from '../../../api/types'; import { selectUser } from '../../../global/selectors'; @@ -14,7 +14,7 @@ type OwnProps = { }; type StateProps = { - userOrChat?: ApiUser | ApiChat; + userOrChat?: ApiPeer; }; const MentionLink: FC = ({ diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 483f01e5a..cb4db987e 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -6,10 +6,10 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiAvailableReaction, - ApiChat, ApiChatMember, ApiMessage, ApiMessageOutgoingStatus, + ApiPeer, ApiReaction, ApiThreadInfo, ApiTopic, @@ -75,6 +75,7 @@ import { selectIsMessageSelected, selectMessageIdsByGroupId, selectOutgoingStatus, + selectPeerStory, selectPerformanceSettingsValue, selectReplySender, selectRequestedChatTranslationLanguage, @@ -89,7 +90,6 @@ import { selectTopicFromMessage, selectUploadProgress, selectUser, - selectUserStory, } from '../../../global/selectors'; import { isAnimatingScroll } from '../../../util/animateScroll'; import buildClassName from '../../../util/buildClassName'; @@ -199,14 +199,14 @@ type OwnProps = type StateProps = { theme: ISettings['theme']; forceSenderName?: boolean; - sender?: ApiUser | ApiChat; + sender?: ApiPeer; canShowSender: boolean; - originSender?: ApiUser | ApiChat; + originSender?: ApiPeer; botSender?: ApiUser; isThreadTop?: boolean; shouldHideReply?: boolean; replyMessage?: ApiMessage; - replyMessageSender?: ApiUser | ApiChat; + replyMessageSender?: ApiPeer; replyStory?: ApiTypeStory; storySender?: ApiUser; outgoingStatus?: ApiMessageOutgoingStatus; @@ -499,7 +499,7 @@ const Message: FC = ({ const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender); const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; - const senderPeer = forwardInfo ? originSender : messageSender; + const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender; const { handleMouseDown, @@ -1440,7 +1440,7 @@ export default memo(withGlobal( const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const webPageStoryData = message.content.webPage?.story; const webPageStory = webPageStoryData - ? selectUserStory(global, webPageStoryData.userId, webPageStoryData.id) + ? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id) : undefined; const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id); @@ -1463,7 +1463,7 @@ export default memo(withGlobal( const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo)); const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const replyStory = replyToStoryId && replyToStoryUserId - ? selectUserStory(global, replyToStoryUserId, replyToStoryId) + ? selectPeerStory(global, replyToStoryUserId, replyToStoryId) : undefined; const storySender = replyToStoryUserId ? selectUser(global, replyToStoryUserId) : undefined; diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 6dcbccde0..d40fece0d 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -9,6 +9,7 @@ import type { ApiChat, ApiChatReactions, ApiMessage, + ApiPeer, ApiReaction, ApiSponsoredMessage, ApiStickerSet, @@ -78,7 +79,7 @@ type OwnProps = { canClosePoll?: boolean; isDownloading?: boolean; canShowSeenBy?: boolean; - seenByRecentPeers?: (ApiChat | ApiUser)[]; + seenByRecentPeers?: ApiPeer[]; noReplies?: boolean; hasCustomEmoji?: boolean; customEmojiSets?: ApiStickerSet[]; diff --git a/src/components/middle/message/Poll.tsx b/src/components/middle/message/Poll.tsx index 6b9590b3f..9aeb506f2 100644 --- a/src/components/middle/message/Poll.tsx +++ b/src/components/middle/message/Poll.tsx @@ -10,7 +10,7 @@ import React, { import { getActions, getGlobal, withGlobal } from '../../../global'; import type { - ApiChat, ApiMessage, ApiPoll, ApiPollAnswer, ApiUser, + ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer, } from '../../../api/types'; import type { LangFn } from '../../../hooks/useLang'; @@ -137,7 +137,7 @@ const Poll: FC = ({ // No need for expensive global updates on chats or users, so we avoid them const chatsById = getGlobal().chats.byId; const usersById = getGlobal().users.byId; - return recentVoterIds ? recentVoterIds.reduce((result: (ApiChat | ApiUser)[], id) => { + return recentVoterIds ? recentVoterIds.reduce((result: ApiPeer[], id) => { const chat = chatsById[id]; const user = usersById[id]; if (user) { diff --git a/src/components/middle/message/ReactionButton.tsx b/src/components/middle/message/ReactionButton.tsx index fee069e97..66c145d65 100644 --- a/src/components/middle/message/ReactionButton.tsx +++ b/src/components/middle/message/ReactionButton.tsx @@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, getGlobal } from '../../../global'; import type { - ApiChat, ApiMessage, ApiReactionCount, ApiUser, + ApiMessage, ApiPeer, ApiReactionCount, } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; @@ -49,7 +49,7 @@ const ReactionButton: FC<{ return recentReactions .filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction)) .map((recentReaction) => usersById[recentReaction.peerId] || chatsById[recentReaction.peerId]) - .filter(Boolean) as (ApiChat | ApiUser)[]; + .filter(Boolean) as ApiPeer[]; }, [reaction.reaction, recentReactions, withRecentReactors]); const handleClick = useLastCallback(() => { diff --git a/src/components/middle/message/ReactionPicker.tsx b/src/components/middle/message/ReactionPicker.tsx index 43bfdd607..6df80c052 100644 --- a/src/components/middle/message/ReactionPicker.tsx +++ b/src/components/middle/message/ReactionPicker.tsx @@ -8,10 +8,10 @@ import type { } from '../../../api/types'; import type { IAnchorPosition } from '../../../types'; -import { isUserId } from '../../../global/helpers'; +import { getStoryKey, isUserId } from '../../../global/helpers'; import { selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium, - selectTabState, selectUserStory, + selectPeerStory, selectTabState, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import parseMessageInput from '../../../util/parseMessageInput'; @@ -66,7 +66,7 @@ const ReactionPicker: FC = ({ const renderedMessageId = useCurrentOrPrev(message?.id, true); const renderedChatId = useCurrentOrPrev(message?.chatId, true); - const renderedStoryUserId = useCurrentOrPrev(story?.userId, true); + const renderedStoryPeerId = useCurrentOrPrev(story?.peerId, true); const renderedStoryId = useCurrentOrPrev(story?.id); const storedPosition = useCurrentOrPrev(position, true); // eslint-disable-next-line no-null/no-null @@ -89,7 +89,7 @@ const ReactionPicker: FC = ({ const getMenuElement = useLastCallback(() => menuRef.current); const getLayout = useLastCallback(() => ({ withPortal: true, - isDense: !renderedStoryUserId, + isDense: !renderedStoryPeerId, deltaX: !getIsMobile() && menuRef.current ? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2 : 0, @@ -146,7 +146,11 @@ const ReactionPicker: FC = ({ if (!sendAsMessage) { sendStoryReaction({ - userId: renderedStoryUserId!, storyId: renderedStoryId!, reaction, shouldAddToRecent: true, + peerId: renderedStoryPeerId!, + storyId: renderedStoryId!, + containerId: getStoryKey(renderedStoryPeerId!, renderedStoryId!), + reaction, + shouldAddToRecent: true, }); closeReactionPicker(); return; @@ -223,15 +227,15 @@ const ReactionPicker: FC = ({ export default memo(withGlobal((global): StateProps => { const state = selectTabState(global); const { - chatId, messageId, storyUserId, storyId, position, sendAsMessage, + chatId, messageId, storyPeerId, storyId, position, sendAsMessage, } = state.reactionPicker || {}; - const story = storyUserId && storyId - ? selectUserStory(global, storyUserId, storyId) as ApiStory | ApiStorySkipped + const story = storyPeerId && storyId + ? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped : undefined; const chat = chatId ? selectChat(global, chatId) : undefined; const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined; - const isPrivateChat = chatId ? isUserId(chatId) : Boolean(storyUserId); + const isPrivateChat = isUserId(chatId || storyPeerId || ''); const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some'; const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all' && chatFullInfo?.enabledReactions?.areCustomAllowed; diff --git a/src/components/middle/message/Story.tsx b/src/components/middle/message/Story.tsx index b9432ec53..55e2a5138 100644 --- a/src/components/middle/message/Story.tsx +++ b/src/components/middle/message/Story.tsx @@ -5,7 +5,7 @@ import type { ApiMessage, ApiTypeStory, } from '../../../api/types'; -import { selectUserStory } from '../../../global/selectors'; +import { selectPeerStory } from '../../../global/selectors'; import BaseStory from './BaseStory'; @@ -34,10 +34,10 @@ function Story({ } export default memo(withGlobal((global, { message }): StateProps => { - const { id, userId } = message.content.storyData!; + const { id, peerId } = message.content.storyData!; return { - story: selectUserStory(global, userId, id), + story: selectPeerStory(global, peerId, id), isConnected: global.connectionState === 'connectionStateReady', }; })(Story)); diff --git a/src/components/middle/message/StoryMention.tsx b/src/components/middle/message/StoryMention.tsx index 090dd2cfb..9bcc2e58c 100644 --- a/src/components/middle/message/StoryMention.tsx +++ b/src/components/middle/message/StoryMention.tsx @@ -2,12 +2,15 @@ import React, { memo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { - ApiMessage, ApiTypeStory, ApiUser, + ApiMessage, ApiPeer, ApiTypeStory, ApiUser, } from '../../../api/types'; -import { getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers'; +import { getSenderTitle, getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers'; import { - selectUser, selectUserStories, selectUserStory, + selectPeer, + selectPeerStories, + selectPeerStory, + selectUser, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import renderText from '../../common/helpers/renderText'; @@ -23,13 +26,13 @@ interface OwnProps { interface StateProps { story?: ApiTypeStory; - user?: ApiUser; + peer?: ApiPeer; targetUser?: ApiUser; isUnread?: boolean; } function StoryMention({ - message, story, user, isUnread, targetUser, + message, story, peer, isUnread, targetUser, }: OwnProps & StateProps) { const { openStoryViewer } = getActions(); @@ -39,9 +42,9 @@ function StoryMention({ const handleClick = useLastCallback(() => { openStoryViewer({ - userId: story!.userId, + peerId: story!.peerId, storyId: story!.id, - isSingleUser: true, + isSinglePeer: true, isSingleStory: true, }); }); @@ -55,10 +58,10 @@ function StoryMention({ const imgBlobUrl = useMedia(imageHash); const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri; - useEnsureStory(storyData!.userId, storyData!.id, story); + useEnsureStory(storyData!.peerId, storyData!.id, story); function getTitle() { - if (user?.isSelf) { + if (peer && 'isSelf' in peer && peer.isSelf) { return isDeleted ? lang('ExpiredStoryMentioned', getUserFirstOrLastName(targetUser)) : lang('StoryYouMentionedTitle', getUserFirstOrLastName(targetUser)); @@ -66,7 +69,7 @@ function StoryMention({ return isDeleted ? lang('ExpiredStoryMention') - : lang('StoryMentionedTitle', getUserFirstOrLastName(user)); + : lang('StoryMentionedTitle', getSenderTitle(lang, peer!)); } return ( @@ -90,12 +93,12 @@ function StoryMention({ } export default memo(withGlobal((global, { message }): StateProps => { - const { id, userId } = message.content.storyData!; - const lastReadId = selectUserStories(global, userId)?.lastReadId; + const { id, peerId } = message.content.storyData!; + const lastReadId = selectPeerStories(global, peerId)?.lastReadId; return { - story: selectUserStory(global, userId, id), - user: selectUser(global, userId), + story: selectPeerStory(global, peerId, id), + peer: selectPeer(global, peerId), targetUser: selectUser(global, message.chatId), isUnread: Boolean(lastReadId && lastReadId < id), }; diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index d7d4d1474..b0b6da015 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -81,7 +81,7 @@ const WebPage: FC = ({ const { story: storyData } = webPage || {}; - useEnsureStory(storyData?.userId, storyData?.id, story); + useEnsureStory(storyData?.peerId, storyData?.id, story); if (!webPage) { return undefined; diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts index 25459fd1c..4b68a5507 100644 --- a/src/components/middle/message/hooks/useInnerHandlers.ts +++ b/src/components/middle/message/hooks/useInnerHandlers.ts @@ -2,8 +2,7 @@ import type React from '../../../../lib/teact/teact'; import { getActions } from '../../../../global'; import type { - ApiChat, ApiMessage, ApiStory, - ApiTopic, ApiUser, + ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, } from '../../../../api/types'; import type { LangFn } from '../../../../hooks/useLang'; import type { IAlbum } from '../../../../types'; @@ -23,8 +22,8 @@ export default function useInnerHandlers( isScheduled?: boolean, isChatWithRepliesBot?: boolean, album?: IAlbum, - avatarPeer?: ApiUser | ApiChat, - senderPeer?: ApiUser | ApiChat, + avatarPeer?: ApiPeer, + senderPeer?: ApiPeer, botSender?: ApiUser, messageTopic?: ApiTopic, isTranslatingChat?: boolean, @@ -185,7 +184,7 @@ export default function useInnerHandlers( const handleStoryClick = useLastCallback(() => { if (!story) return; openStoryViewer({ - userId: story.userId, + peerId: story.peerId, storyId: story.id, isSingleStory: true, }); diff --git a/src/components/payment/Checkout.module.scss b/src/components/payment/Checkout.module.scss index 568b407bf..03002887a 100644 --- a/src/components/payment/Checkout.module.scss +++ b/src/components/payment/Checkout.module.scss @@ -136,6 +136,7 @@ } .tos-checkbox { + margin-left: 0.5rem; padding-left: 4rem; :global(.Checkbox-main) { diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 6588abff2..2f850d515 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -71,7 +71,7 @@ const Checkout: FC = ({ const isInteractive = Boolean(dispatch); const { - photo, title, text, isRecurring, recurringTermsUrl, suggestedTipAmounts, maxTipAmount, + photo, title, text, termsUrl, suggestedTipAmounts, maxTipAmount, } = invoice || {}; const { paymentMethod, @@ -218,7 +218,7 @@ const Checkout: FC = ({ icon: 'truck', onClick: isInteractive ? handleShippingMethodClick : undefined, })} - {isRecurring && renderTos(recurringTermsUrl!)} + {termsUrl && renderTos(termsUrl)}
); diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 168280bfa..42c1a4378 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -35,17 +35,17 @@ import { selectChatMessages, selectCurrentMediaSearch, selectIsRightColumnShown, + selectPeerFullInfo, + selectPeerStories, selectTabState, selectTheme, selectUser, - selectUserFullInfo, - selectUserStories, } from '../../global/selectors'; import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { getSenderName } from '../left/search/helpers/getSenderName'; -import useUserStoriesPolling from '../../hooks/polling/useUserStoriesPolling'; +import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling'; import useCacheBuster from '../../hooks/useCacheBuster'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; @@ -170,7 +170,7 @@ const Profile: FC = ({ focusMessage, loadProfilePhotos, setNewChatMembersDialogState, - loadUserPinnedStories, + loadPeerPinnedStories, loadStoriesArchive, } = getActions(); @@ -215,18 +215,18 @@ const Profile: FC = ({ const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab; const tabType = tabs[renderingActiveTab].type as ProfileTabType; - const handleLoadUserStories = useCallback(({ offsetId }: { offsetId: number }) => { - loadUserPinnedStories({ userId: chatId, offsetId }); + const handleLoadPeerStories = useCallback(({ offsetId }: { offsetId: number }) => { + loadPeerPinnedStories({ peerId: chatId, offsetId }); }, [chatId]); const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => { - loadStoriesArchive({ offsetId }); - }, []); + loadStoriesArchive({ peerId: currentUserId!, offsetId }); + }, [currentUserId]); const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds( loadMoreMembers, loadCommonChats, searchMediaMessagesLocal, - handleLoadUserStories, + handleLoadPeerStories, handleLoadStoriesArchive, tabType, mediaSearchType, @@ -246,7 +246,7 @@ const Profile: FC = ({ || (!hasMembersTab && resultType === 'media'); const activeKey = tabs.findIndex(({ type }) => type === resultType); - useUserStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined); + usePeerStoriesPolling(resultType === 'members' ? viewportIds as string[] : undefined); const { handleScroll } = useProfileState(containerRef, resultType, profileState, onProfileStateChange); @@ -606,24 +606,22 @@ export default memo(withGlobal( const activeDownloads = selectActiveDownloads(global, chatId); let hasCommonChatsTab; - let hasStoriesTab; let resolvedUserId; let user; - let storyIds; - let archiveStoryIds; - let storyByIds; if (isUserId(chatId)) { resolvedUserId = chatId; user = selectUser(global, resolvedUserId); - const userFullInfo = selectUserFullInfo(global, chatId); hasCommonChatsTab = user && !user.isSelf && !isUserBot(user); - hasStoriesTab = user && (user.isSelf || (!user.areStoriesHidden && userFullInfo?.hasPinnedStories)); - const userStories = hasStoriesTab ? selectUserStories(global, user!.id) : undefined; - storyIds = userStories?.pinnedIds; - storyByIds = userStories?.byId; - archiveStoryIds = userStories?.archiveIds; } + const peer = user || chat; + const peerFullInfo = selectPeerFullInfo(global, chatId); + const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories)); + const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined; + const storyIds = peerStories?.pinnedIds; + const storyByIds = peerStories?.byId; + const archiveStoryIds = peerStories?.archiveIds; + return { theme: selectTheme(global), isChannel, diff --git a/src/components/right/RightSearch.tsx b/src/components/right/RightSearch.tsx index 38270c094..1f8a2fa7e 100644 --- a/src/components/right/RightSearch.tsx +++ b/src/components/right/RightSearch.tsx @@ -5,7 +5,7 @@ import React, { } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; -import type { ApiChat, ApiMessage, ApiUser } from '../../api/types'; +import type { ApiMessage, ApiPeer } from '../../api/types'; import { selectChat, @@ -130,7 +130,7 @@ const RightSearch: FC = ({ message, senderPeer, onClick, }: { message: ApiMessage; - senderPeer: ApiUser | ApiChat; + senderPeer: ApiPeer; onClick: NoneToVoidFunction; }) => { const text = renderMessageSummary(lang, message, undefined, query); diff --git a/src/components/right/management/ManageGroupAdminRights.tsx b/src/components/right/management/ManageGroupAdminRights.tsx index e9986596e..c416aad40 100644 --- a/src/components/right/management/ManageGroupAdminRights.tsx +++ b/src/components/right/management/ManageGroupAdminRights.tsx @@ -29,9 +29,9 @@ type OwnProps = { selectedUserId?: string; isPromotedByCurrentUser?: boolean; isNewAdmin?: boolean; + isActive: boolean; onScreenSelect: (screen: ManagementScreens) => void; onClose: NoneToVoidFunction; - isActive: boolean; }; type StateProps = { @@ -49,10 +49,10 @@ type StateProps = { const CUSTOM_TITLE_MAX_LENGTH = 16; const ManageGroupAdminRights: FC = ({ + isActive, isNewAdmin, selectedUserId, defaultRights, - onScreenSelect, chat, usersById, currentUserId, @@ -62,7 +62,7 @@ const ManageGroupAdminRights: FC = ({ isForum, isFormFullyDisabled, onClose, - isActive, + onScreenSelect, }) => { const { updateChatAdmin } = getActions(); @@ -260,6 +260,42 @@ const ManageGroupAdminRights: FC = ({ onChange={handlePermissionChange} /> + {isChannel && ( +
+ +
+ )} + {isChannel && ( +
+ +
+ )} + {isChannel && ( +
+ +
+ )} {!isChannel && (
= ({ return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds; }, [members, userStatusesById, noAdmins, adminIds]); + usePeerStoriesPolling(memberIds); + const displayedIds = useMemo(() => { // No need for expensive global updates on users, so we avoid them const usersById = getGlobal().users.byId; @@ -223,7 +226,7 @@ const ManageGroupMembers: FC = ({ onClick={() => handleMemberClick(id)} contextActions={getMemberContextAction(id)} > - + ))} diff --git a/src/components/story/MediaAreaOverlay.tsx b/src/components/story/MediaAreaOverlay.tsx deleted file mode 100644 index 1401523f1..000000000 --- a/src/components/story/MediaAreaOverlay.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { memo } from '../../lib/teact/teact'; -import { getActions } from '../../global'; - -import type { ApiMediaArea } from '../../api/types'; -import type { IDimensions } from '../../global/types'; - -import buildClassName from '../../util/buildClassName'; -import buildStyle from '../../util/buildStyle'; - -import styles from './StoryViewer.module.scss'; - -type OwnProps = { - mediaAreas?: ApiMediaArea[]; - mediaDimensions: IDimensions; -}; - -const MediaAreaOverlay = ({ mediaAreas, mediaDimensions }: OwnProps) => { - const { openMapModal } = getActions(); - const handleMediaAreaClick = (mediaArea: ApiMediaArea) => { - if (mediaArea.geo) { - openMapModal({ geoPoint: mediaArea.geo }); - } - }; - - return ( -
- {mediaAreas?.map((mediaArea) => ( -
handleMediaAreaClick(mediaArea)} - /> - ))} -
- ); -}; - -function prepareStyle(mediaArea: ApiMediaArea) { - const { - x, y, width, height, rotation, - } = mediaArea.coordinates; - - return buildStyle( - `left: ${x}%`, - `top: ${y}%`, - `width: ${width}%`, - `height: ${height}%`, - `transform: rotate(${rotation}deg) translate(-50%, -50%)`, - ); -} - -export default memo(MediaAreaOverlay); diff --git a/src/components/story/MediaStory.tsx b/src/components/story/MediaStory.tsx index d10d55ab5..1edd8a1d7 100644 --- a/src/components/story/MediaStory.tsx +++ b/src/components/story/MediaStory.tsx @@ -18,6 +18,7 @@ import useMenuPosition from '../../hooks/useMenuPosition'; import Menu from '../ui/Menu'; import MenuItem from '../ui/MenuItem'; +import MediaAreaOverlay from './mediaArea/MediaAreaOverlay'; import styles from './MediaStory.module.scss'; @@ -30,7 +31,7 @@ interface OwnProps { function MediaStory({ story, isProtected, isArchive }: OwnProps) { const { openStoryViewer, - loadUserSkippedStories, + loadPeerSkippedStories, toggleStoryPinned, showNotification, } = getActions(); @@ -44,6 +45,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { const getMenuElement = useLastCallback(() => document.querySelector('#portals .story-context-menu .bubble')); const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true })); + const peerId = story && story.peerId; const isFullyLoaded = story && 'content' in story; const isDeleted = story && 'isDeleted' in story; const video = isFullyLoaded ? (story as ApiStory).content.video : undefined; @@ -53,7 +55,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { useEffect(() => { if (story && !(isFullyLoaded || isDeleted)) { - loadUserSkippedStories({ userId: story.userId }); + loadPeerSkippedStories({ peerId: story.peerId }); } }, [isDeleted, isFullyLoaded, story]); @@ -74,13 +76,13 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { const handleClick = useCallback(() => { openStoryViewer({ - userId: story.userId, + peerId: story.peerId, storyId: story.id, - isSingleUser: true, + isSinglePeer: true, isPrivate: true, isArchive, }); - }, [isArchive, story.id, story.userId]); + }, [isArchive, story.id, story.peerId]); const handleMouseDown = useLastCallback((e: React.MouseEvent) => { preventMessageInputBlurWithBubbling(e); @@ -90,7 +92,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { const handlePinClick = useLastCallback((e: React.SyntheticEvent) => { stopEvent(e); - toggleStoryPinned({ storyId: story.id, isPinned: true }); + toggleStoryPinned({ peerId, storyId: story.id, isPinned: true }); showNotification({ message: lang('Story.ToastSavedToProfileText'), }); @@ -100,7 +102,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { const handleUnpinClick = useLastCallback((e: React.SyntheticEvent) => { stopEvent(e); - toggleStoryPinned({ storyId: story.id, isPinned: false }); + toggleStoryPinned({ peerId, storyId: story.id, isPinned: false }); showNotification({ message: lang('Story.ToastRemovedFromProfileText'), }); @@ -125,6 +127,7 @@ function MediaStory({ story, isProtected, isArchive }: OwnProps) { {thumbUrl && ( )} + {isFullyLoaded && } {isProtected && }
{contextMenuPosition !== undefined && ( diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 56947aca1..8e55212d1 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -2,20 +2,21 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; -import { getActions, getGlobal, withGlobal } from '../../global'; +import { getActions, withGlobal } from '../../global'; import type { - ApiStealthMode, ApiStory, ApiTypeStory, ApiUser, + ApiPeer, ApiStealthMode, ApiStory, ApiTypeStory, } from '../../api/types'; import type { IDimensions } from '../../global/types'; import type { Signal } from '../../util/signals'; import { MAIN_THREAD_ID } from '../../api/types'; import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config'; -import { getUserFirstOrLastName } from '../../global/helpers'; +import { getSenderTitle, isUserId } from '../../global/helpers'; import { selectChat, selectIsCurrentUserPremium, - selectTabState, selectUserStories, selectUserStory, + selectPeerStories, selectPeerStory, + selectTabState, selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; @@ -41,21 +42,21 @@ import useStoryPreloader from './hooks/useStoryPreloader'; import useStoryProps from './hooks/useStoryProps'; import Avatar from '../common/Avatar'; -import AvatarList from '../common/AvatarList'; import Composer from '../common/Composer'; import Button from '../ui/Button'; import DropdownMenu from '../ui/DropdownMenu'; import MenuItem from '../ui/MenuItem'; import OptimizedVideo from '../ui/OptimizedVideo'; import Skeleton from '../ui/placeholder/Skeleton'; -import MediaAreaOverlay from './MediaAreaOverlay'; +import MediaAreaOverlay from './mediaArea/MediaAreaOverlay'; import StoryCaption from './StoryCaption'; +import StoryFooter from './StoryFooter'; import StoryProgress from './StoryProgress'; import styles from './StoryViewer.module.scss'; interface OwnProps { - userId: string; + peerId: string; storyId: number; dimensions: IDimensions; // eslint-disable-next-line react/no-unused-prop-types @@ -66,16 +67,15 @@ interface OwnProps { isArchivedStories?: boolean; isSingleStory?: boolean; getIsAnimating: Signal; - onDelete: (storyId: number) => void; + onDelete: (story: ApiTypeStory) => void; onClose: NoneToVoidFunction; onReport: NoneToVoidFunction; } interface StateProps { - user: ApiUser; + peer: ApiPeer; story?: ApiTypeStory; isMuted: boolean; - isSelf: boolean; orderedIds?: number[]; shouldForcePause?: boolean; storyChangelogUserId?: string; @@ -95,10 +95,9 @@ const SECONDARY_VIDEO_MIME = 'video/mp4; codecs=avc1.64001E'; const STEALTH_MODE_NOTIFICATION_DURATION = 4000; function Story({ - isSelf, - userId, + peerId, storyId, - user, + peer, isMuted, isArchivedStories, isPrivateStories, @@ -123,9 +122,8 @@ function Story({ setStoryViewerMuted, openPreviousStory, openNextStory, - loadUserSkippedStories, + loadPeerSkippedStories, openForwardMenu, - openStoryViewModal, copyStoryLink, toggleStoryPinned, openChat, @@ -163,31 +161,32 @@ function Story({ altMediaData, hasFullData, hasThumb, - mediaAreas, canDownload, downloadMediaData, } = useStoryProps(story, isCurrentUserPremium, isDropdownMenuOpen); const isLoadedStory = story && 'content' in story; - const isChangelog = userId === storyChangelogUserId; + const isChangelog = peerId === storyChangelogUserId; + const isChannel = !isUserId(peerId); + const isOut = isLoadedStory && story.isOut; const canPinToProfile = useCurrentOrPrev( - isSelf && isLoadedStory ? !story.isPinned : undefined, + isOut ? !story.isPinned : undefined, true, ); const canUnpinFromProfile = useCurrentOrPrev( - isSelf && isLoadedStory ? story.isPinned : undefined, + isOut ? story.isPinned : undefined, true, ); const areViewsExpired = Boolean( - isSelf && isLoadedStory && (story!.date + viewersExpirePeriod) < getServerTime(), + isOut && (story!.date + viewersExpirePeriod) < getServerTime(), ); const canCopyLink = Boolean( isLoadedStory && story.isPublic && !isChangelog - && user?.usernames?.length, + && peer?.usernames?.length, ); const canShare = Boolean( @@ -197,18 +196,14 @@ function Story({ && !isChangelog && !isCaptionExpanded, ); - const canShareOwn = Boolean( - isSelf - && isLoadedStory - && story.isPublic - && !story.noForwards, - ); const canPlayStory = Boolean( hasFullData && !shouldForcePause && isAppFocused && !isComposerHasFocus && !isCaptionExpanded && !isPausedBySpacebar && !isPausedByLongPress, ); + const shouldShowFooter = isLoadedStory && (isOut || isChannel); + const { shouldRender: shouldRenderSkeleton, transitionClassNames: skeletonTransitionClassNames, } = useShowTransition(!hasFullData); @@ -223,7 +218,7 @@ function Story({ const { shouldRender: shouldRenderComposer, transitionClassNames: composerAppearanceAnimationClassNames, - } = useShowTransition(!isSelf && !isChangelog); + } = useShowTransition(!isOut && !isChangelog && !isChannel); const { shouldRender: shouldRenderCaptionBackdrop, @@ -232,29 +227,30 @@ function Story({ const { transitionClassNames: appearanceAnimationClassNames } = useShowTransition(true); - useStoryPreloader(userId, storyId); + useStoryPreloader(peerId, storyId); useEffect(() => { if (storyId) { - viewStory({ userId, storyId }); + viewStory({ peerId, storyId }); } - }, [storyId, userId]); + }, [storyId, peerId]); useEffect(() => { - loadUserSkippedStories({ userId }); - }, [userId]); + loadPeerSkippedStories({ peerId }); + }, [peerId]); // Fetching user privacy settings for use in Composer useEffect(() => { - if (!isChatExist) { - fetchChat({ chatId: userId }); + const canWrite = isUserId(peerId); + if (!isChatExist && canWrite) { + fetchChat({ chatId: peerId }); } - }, [isChatExist, userId]); + }, [isChatExist, peerId]); useEffect(() => { if (isChatExist && !areChatSettingsLoaded) { - loadChatSettings({ chatId: userId }); + loadChatSettings({ chatId: peerId }); } - }, [areChatSettingsLoaded, isChatExist, userId]); + }, [areChatSettingsLoaded, isChatExist, peerId]); const handlePauseStory = useLastCallback(() => { if (isVideo) { @@ -310,11 +306,11 @@ function Story({ }, [hasAllData]); useEffect(() => { - if (!isSelf || isDeletedStory || areViewsExpired) return; + if (!isOut || isDeletedStory || areViewsExpired) return; // Refresh recent viewers list each time - loadStoryViews({ storyId, isPreload: true }); - }, [isDeletedStory, areViewsExpired, isSelf, storyId]); + loadStoryViews({ peerId, storyId, isPreload: true }); + }, [isDeletedStory, areViewsExpired, isOut, peerId, storyId]); useEffect(() => { if ( @@ -382,7 +378,7 @@ function Story({ const handleOpenChat = useLastCallback(() => { onClose(); - openChat({ id: userId }); + openChat({ id: peerId }); }); const handleOpenPrevStory = useLastCallback(() => { @@ -403,20 +399,20 @@ function Story({ }, [getIsAnimating, isComposerHasFocus]); const handleCopyStoryLink = useLastCallback(() => { - copyStoryLink({ userId, storyId }); + copyStoryLink({ peerId, storyId }); }); const handlePinClick = useLastCallback(() => { - toggleStoryPinned({ storyId, isPinned: true }); + toggleStoryPinned({ peerId, storyId, isPinned: true }); }); const handleUnpinClick = useLastCallback(() => { - toggleStoryPinned({ storyId, isPinned: false }); + toggleStoryPinned({ peerId, storyId, isPinned: false }); }); const handleDeleteStoryClick = useLastCallback(() => { setCurrentTime(0); - onDelete(story!.id); + onDelete(story!); }); const handleReportStoryClick = useLastCallback(() => { @@ -424,12 +420,7 @@ function Story({ }); const handleForwardClick = useLastCallback(() => { - openForwardMenu({ fromChatId: userId, storyId }); - handlePauseStory(); - }); - - const handleOpenStoryViewModal = useLastCallback(() => { - openStoryViewModal({ storyId }); + openForwardMenu({ fromChatId: peerId, storyId }); }); const handleInfoPrivacyEdit = useLastCallback(() => { @@ -442,7 +433,7 @@ function Story({ : story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody'); let message; - const myName = getUserFirstOrLastName(user); + const myName = getSenderTitle(lang, peer); switch (visibility) { case 'nobody': message = lang('StorySelectedContactsHint', myName); @@ -487,7 +478,7 @@ function Story({ const handleDownload = useLastCallback(() => { if (!downloadMediaData) return; - download(downloadMediaData, `story-${userId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`); + download(downloadMediaData, `story-${peerId}-${storyId}.${isVideo ? 'mp4' : 'jpg'}`); }); useEffect(() => { @@ -539,6 +530,8 @@ function Story({ } function renderStoryPrivacyButton() { + if (isChannel) return undefined; + let privacyIcon = 'channel-filled'; const gradient: Record = { 'channel-filled': ['#50ABFF', '#007AFF'], @@ -547,7 +540,7 @@ function Story({ 'group-filled': ['#FFB743', '#F69A36'], }; - if (isSelf) { + if (isOut) { const { visibility } = (story && 'visibility' in story && story.visibility) || {}; switch (visibility) { @@ -575,12 +568,12 @@ function Story({ return (
- {isSelf && } + {isOut && }
); } @@ -589,13 +582,13 @@ function Story({ return (
- {renderText(getUserFirstOrLastName(user) || '')} + {renderText(getSenderTitle(lang, peer) || '')}
{story && 'date' in story && ( @@ -650,62 +643,14 @@ function Story({ )} {lang('StealthMode')} - {!isSelf && {lang('lng_report_story')}} - {isSelf && {lang('Delete')}} + {!isOut && {lang('lng_report_story')}} + {isOut && {lang('Delete')}}
); } - const recentViewers = useMemo(() => { - const { users: { byId: usersById } } = getGlobal(); - - const recentViewerIds = story && 'recentViewerIds' in story ? story.recentViewerIds : undefined; - if (!recentViewerIds) return undefined; - - return recentViewerIds.map((id) => usersById[id]).filter(Boolean); - }, [story]); - - function renderRecentViewers() { - const { viewsCount, reactionsCount } = story as ApiStory; - - if (!viewsCount) { - return ( -
- {lang('NobodyViewed')} -
- ); - } - - return ( -
- {!areViewsExpired && Boolean(recentViewers?.length) && ( - - )} - - {lang('Views', viewsCount, 'i')} - {Boolean(reactionsCount) && ( - - - {reactionsCount} - - )} -
- ); - } - return (
)} - + {isLoadedStory && fullMediaData && ( + + )}
- {isSelf && renderRecentViewers()} - {canShareOwn && ( - + {shouldShowFooter && ( + )} {shouldRenderCaptionBackdrop && (
} {hasText && ( ((global, { - userId, storyId, isPrivateStories, isArchivedStories, isReportModalOpen, isDeleteModalOpen, + peerId, storyId, isPrivateStories, isArchivedStories, isReportModalOpen, isDeleteModalOpen, }): StateProps => { - const { currentUserId, appConfig } = global; - const user = global.users.byId[userId]; - const chat = selectChat(global, userId); + const { appConfig } = global; + const user = selectUser(global, peerId); + const chat = selectChat(global, peerId); const tabState = selectTabState(global); const { storyViewer: { @@ -860,19 +797,18 @@ export default memo(withGlobal((global, { mapModal, } = tabState; const { isOpen: isPremiumModalOpen } = premiumModal || {}; - const { orderedIds, pinnedIds, archiveIds } = selectUserStories(global, userId) || {}; - const story = selectUserStory(global, userId, storyId); + const { orderedIds, pinnedIds, archiveIds } = selectPeerStories(global, peerId) || {}; + const story = selectPeerStory(global, peerId, storyId); const shouldForcePause = Boolean( viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen || isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal, ); return { - user, + peer: (user || chat)!, story, orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds), isMuted, - isSelf: currentUserId === userId, isCurrentUserPremium: selectIsCurrentUserPremium(global), shouldForcePause, storyChangelogUserId: appConfig!.storyChangelogUserId, diff --git a/src/components/story/StoryDeleteConfirmModal.tsx b/src/components/story/StoryDeleteConfirmModal.tsx index 816f95d62..45dce1d10 100644 --- a/src/components/story/StoryDeleteConfirmModal.tsx +++ b/src/components/story/StoryDeleteConfirmModal.tsx @@ -1,30 +1,34 @@ import React, { memo, useCallback } from '../../lib/teact/teact'; import { getActions } from '../../global'; +import type { ApiTypeStory } from '../../api/types'; + import useLang from '../../hooks/useLang'; import ConfirmDialog from '../ui/ConfirmDialog'; interface OwnProps { isOpen: boolean; - storyId?: number; + story?: ApiTypeStory; onClose: NoneToVoidFunction; } -function StoryDeleteConfirmModal({ isOpen, storyId, onClose }: OwnProps) { +function StoryDeleteConfirmModal({ + isOpen, story, onClose, +}: OwnProps) { const { deleteStory, openNextStory } = getActions(); const lang = useLang(); const handleDeleteStoryClick = useCallback(() => { - if (!storyId) { + if (!story) { return; } openNextStory(); - deleteStory({ storyId }); + deleteStory({ peerId: story.peerId, storyId: story.id }); onClose(); - }, [onClose, storyId]); + }, [onClose, story]); return ( { + const { openStoryViewModal, openForwardMenu, sendStoryReaction } = getActions(); + const lang = useLang(); + + const { + viewsCount, reactionsCount, isOut, peerId, id: storyId, sentReaction, + } = story; + const isChannel = !isUserId(peerId); + + const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction + ? sentReaction.emoticon === HEART_REACTION.emoticon : false; + + const canForward = Boolean( + (isOut || isChannel) + && story.isPublic + && !story.noForwards, + ); + + const containerId = getStoryKey(peerId, storyId); + + const recentViewers = useMemo(() => { + const { users: { byId: usersById } } = getGlobal(); + + const recentViewerIds = story && 'recentViewerIds' in story ? story.recentViewerIds : undefined; + if (!recentViewerIds) return undefined; + + return recentViewerIds.map((id) => usersById[id]).filter(Boolean); + }, [story]); + + const handleOpenStoryViewModal = useLastCallback(() => { + openStoryViewModal({ storyId }); + }); + + const handleForwardClick = useLastCallback(() => { + openForwardMenu({ fromChatId: peerId, storyId }); + }); + + const handleLikeStory = useLastCallback(() => { + const reaction = sentReaction ? undefined : HEART_REACTION; + sendStoryReaction({ + peerId, + storyId, + containerId, + reaction, + }); + }); + + if (!viewsCount) { + return ( +
+ {lang('NobodyViewed')} +
+ ); + } + + return ( +
+
+ {!areViewsExpired && Boolean(recentViewers?.length) && ( + + )} + + {isChannel ? ( + {viewsCount} + ) : ( + {lang('Views', viewsCount, 'i')} + )} + {Boolean(reactionsCount) && !isChannel && ( + + + {reactionsCount} + + )} +
+
+ {canForward && ( + + )} + {isChannel && ( +
+ + {Boolean(reactionsCount) && ({reactionsCount})} +
+ )} +
+ ); +}; + +export default memo(StoryFooter); diff --git a/src/components/story/StoryPreview.tsx b/src/components/story/StoryPreview.tsx index 9c8a41278..09b37710b 100644 --- a/src/components/story/StoryPreview.tsx +++ b/src/components/story/StoryPreview.tsx @@ -1,22 +1,26 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiTypeStory, ApiUser, ApiUserStories } from '../../api/types'; +import type { + ApiPeer, ApiPeerStories, ApiTypeStory, +} from '../../api/types'; import type { StoryViewerOrigin } from '../../types'; -import { getStoryMediaHash, getUserFirstOrLastName } from '../../global/helpers'; +import { getSenderTitle, getStoryMediaHash } from '../../global/helpers'; import { selectTabState } from '../../global/selectors'; import renderText from '../common/helpers/renderText'; +import useLang from '../../hooks/useLang'; import useMedia from '../../hooks/useMedia'; import Avatar from '../common/Avatar'; +import MediaAreaOverlay from './mediaArea/MediaAreaOverlay'; import styles from './StoryViewer.module.scss'; interface OwnProps { - user?: ApiUser; - userStories?: ApiUserStories; + peer?: ApiPeer; + peerStories?: ApiPeerStories; } interface StateProps { @@ -25,73 +29,75 @@ interface StateProps { } function StoryPreview({ - user, userStories, lastViewedId, origin, + peer, peerStories, lastViewedId, origin, }: OwnProps & StateProps) { - const { openStoryViewer, loadUserSkippedStories } = getActions(); + const { openStoryViewer, loadPeerSkippedStories } = getActions(); + const lang = useLang(); const story = useMemo(() => { - if (!userStories) { + if (!peerStories) { return undefined; } const { orderedIds, lastReadId, byId, - } = userStories; + } = peerStories; const hasUnreadStories = orderedIds[orderedIds.length - 1] !== lastReadId; const previewIndexId = lastViewedId ?? (hasUnreadStories ? (lastReadId ?? -1) : -1); const resultId = byId[previewIndexId]?.id || orderedIds[0]; return byId[resultId]; - }, [lastViewedId, userStories]); + }, [lastViewedId, peerStories]); + + const isLoaded = story && 'content' in story; useEffect(() => { - if (story && !('content' in story)) { - loadUserSkippedStories({ userId: story.userId }); + if (story && !isLoaded) { + loadPeerSkippedStories({ peerId: story.peerId }); } - }, [story]); + }, [story, isLoaded]); - const video = story && 'content' in story ? story.content.video : undefined; - const imageHash = story && 'content' in story - ? getStoryMediaHash(story) - : undefined; + const video = isLoaded ? story.content.video : undefined; + const imageHash = isLoaded ? getStoryMediaHash(story) : undefined; const imgBlobUrl = useMedia(imageHash); const thumbUrl = imgBlobUrl || video?.thumbnail?.dataUri; - if (!user || !story || 'isDeleted' in story) { + if (!peer || !story || 'isDeleted' in story) { return undefined; } return (
{ openStoryViewer({ userId: story.userId, storyId: story.id, origin }); }} + onClick={() => { openStoryViewer({ peerId: story.peerId, storyId: story.id, origin }); }} > {thumbUrl && ( )} + {isLoaded && }
-
{renderText(getUserFirstOrLastName(user) || '')}
+
{renderText(getSenderTitle(lang, peer) || '')}
); } -export default memo(withGlobal((global, { user }): StateProps => { +export default memo(withGlobal((global, { peer }): StateProps => { const { storyViewer: { - lastViewedByUserIds, + lastViewedByPeerIds, origin, }, } = selectTabState(global); return { - lastViewedId: user?.id ? lastViewedByUserIds?.[user.id] : undefined, + lastViewedId: peer?.id ? lastViewedByPeerIds?.[peer.id] : undefined, origin, }; })(StoryPreview)); diff --git a/src/components/story/StoryRibbon.module.scss b/src/components/story/StoryRibbon.module.scss index 040b019c3..70a96fe99 100644 --- a/src/components/story/StoryRibbon.module.scss +++ b/src/components/story/StoryRibbon.module.scss @@ -18,7 +18,7 @@ opacity: 0; } -.user { +.peer { flex: 0 0 3.75rem; width: 3.75rem; display: flex; diff --git a/src/components/story/StoryRibbon.tsx b/src/components/story/StoryRibbon.tsx index 5f9c8f681..f5cca9954 100644 --- a/src/components/story/StoryRibbon.tsx +++ b/src/components/story/StoryRibbon.tsx @@ -1,7 +1,7 @@ import React, { memo, useRef } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; -import type { ApiUser } from '../../api/types'; +import type { ApiChat, ApiUser } from '../../api/types'; import buildClassName from '../../util/buildClassName'; @@ -20,17 +20,23 @@ interface OwnProps { } interface StateProps { - orderedUserIds: string[]; + orderedPeerIds: string[]; usersById: Record; + chatsById: Record; } function StoryRibbon({ - isArchived, className, orderedUserIds, usersById, isClosing, + isArchived, + className, + orderedPeerIds, + usersById, + chatsById, + isClosing, }: OwnProps & StateProps) { const lang = useLang(); const fullClassName = buildClassName( styles.root, - !orderedUserIds.length && styles.hidden, + !orderedPeerIds.length && styles.hidden, isClosing && styles.closing, className, 'no-scrollbar', @@ -48,17 +54,17 @@ function StoryRibbon({ className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined} > - {orderedUserIds.map((userId) => { - const user = usersById[userId]; + {orderedPeerIds.map((peerId) => { + const peer = usersById[peerId] || chatsById[peerId]; - if (!user) { + if (!peer) { return undefined; } return ( ); @@ -69,12 +75,14 @@ function StoryRibbon({ export default memo(withGlobal( (global, { isArchived }): StateProps => { - const { orderedUserIds: { active, archived } } = global.stories; + const { orderedPeerIds: { active, archived } } = global.stories; const usersById = global.users.byId; + const chatsById = global.chats.byId; return { - orderedUserIds: isArchived ? archived : active, + orderedPeerIds: isArchived ? archived : active, usersById, + chatsById, }; }, )(StoryRibbon)); diff --git a/src/components/story/StoryRibbonButton.tsx b/src/components/story/StoryRibbonButton.tsx index 5fd393db4..671283690 100644 --- a/src/components/story/StoryRibbonButton.tsx +++ b/src/components/story/StoryRibbonButton.tsx @@ -1,10 +1,10 @@ import React, { memo, useRef } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiUser } from '../../api/types'; +import type { ApiPeer } from '../../api/types'; import { StoryViewerOrigin } from '../../types'; -import { getUserFirstOrLastName } from '../../global/helpers'; +import { getSenderTitle, isUserId } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; @@ -21,11 +21,11 @@ import MenuItem from '../ui/MenuItem'; import styles from './StoryRibbon.module.scss'; interface OwnProps { - user: ApiUser; + peer: ApiPeer; isArchived?: boolean; } -function StoryRibbonButton({ user, isArchived }: OwnProps) { +function StoryRibbonButton({ peer, isArchived }: OwnProps) { const { openChat, openChatWithInfo, @@ -37,7 +37,10 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) { // eslint-disable-next-line no-null/no-null const ref = useRef(null); - useStoryPreloader(user.id); + const isSelf = 'isSelf' in peer && peer.isSelf; + const isChannel = !isUserId(peer.id); + + useStoryPreloader(peer.id); const { isContextMenuOpen, contextMenuPosition, @@ -47,7 +50,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) { const getTriggerElement = useLastCallback(() => ref.current); const getRootElement = useLastCallback(() => document.body); - const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-user-context-menu .bubble')); + const getMenuElement = useLastCallback(() => ref.current!.querySelector('.story-peer-context-menu .bubble')); const getLayout = useLastCallback(() => ({ withPortal: true, isDense: true })); const { @@ -63,7 +66,7 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) { const handleClick = useLastCallback(() => { if (isContextMenuOpen) return; - openStoryViewer({ userId: user.id, origin: StoryViewerOrigin.StoryRibbon }); + openStoryViewer({ peerId: peer.id, origin: StoryViewerOrigin.StoryRibbon }); }); const handleMouseDown = useLastCallback((e: React.MouseEvent) => { @@ -72,44 +75,44 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) { }); const handleSavedStories = useLastCallback(() => { - openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'stories' }); + openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'stories' }); }); const handleArchivedStories = useLastCallback(() => { - openChatWithInfo({ id: user.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' }); + openChatWithInfo({ id: peer.id, shouldReplaceHistory: true, profileTab: 'storiesArchive' }); }); const handleOpenChat = useLastCallback(() => { - openChat({ id: user.id, shouldReplaceHistory: true }); + openChat({ id: peer.id, shouldReplaceHistory: true }); }); const handleOpenProfile = useLastCallback(() => { - openChatWithInfo({ id: user.id, shouldReplaceHistory: true }); + openChatWithInfo({ id: peer.id, shouldReplaceHistory: true }); }); - const handleArchiveUser = useLastCallback(() => { - toggleStoriesHidden({ userId: user.id, isHidden: !isArchived }); + const handleArchivePeer = useLastCallback(() => { + toggleStoriesHidden({ peerId: peer.id, isHidden: !isArchived }); }); return (
-
- {user.isSelf ? lang('MyStory') : getUserFirstOrLastName(user)} +
+ {isSelf ? lang('MyStory') : getSenderTitle(lang, peer)}
{contextMenuPosition !== undefined && ( - {user.isSelf ? ( + {isSelf ? ( <> {lang('StoryList.Context.SavedStories')} @@ -136,14 +139,22 @@ function StoryRibbonButton({ user, isArchived }: OwnProps) { ) : ( <> - - {lang('SendMessageTitle')} - - - {lang('StoryList.Context.ViewProfile')} - + {!isChannel && ( + + {lang('SendMessageTitle')} + + )} + {isChannel ? ( + + {lang('ChatList.ContextOpenChannel')} + + ) : ( + + {lang('StoryList.Context.ViewProfile')} + + )} {lang(isArchived ? 'StoryList.Context.Unarchive' : 'StoryList.Context.Archive')} diff --git a/src/components/story/StorySettings.tsx b/src/components/story/StorySettings.tsx index a0c3c6973..123524823 100644 --- a/src/components/story/StorySettings.tsx +++ b/src/components/story/StorySettings.tsx @@ -7,8 +7,8 @@ import type { ApiStory, ApiUser } from '../../api/types'; import type { ApiPrivacySettings, PrivacyVisibility } from '../../types'; import type { IconName } from '../../types/icons'; -import { getUserFullName } from '../../global/helpers'; -import { selectTabState, selectUserStory } from '../../global/selectors'; +import { getSenderTitle, getUserFullName } from '../../global/helpers'; +import { selectPeerStory, selectTabState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import stopEvent from '../../util/stopEvent'; @@ -191,11 +191,12 @@ function StorySettings({ const handleSubmit = useLastCallback(() => { editStoryPrivacy({ + peerId: story!.peerId, storyId: story!.id, privacy: privacy!, }); if (story!.isPinned !== isPinned) { - toggleStoryPinned({ storyId: story!.id, isPinned }); + toggleStoryPinned({ peerId: story!.peerId, storyId: story!.id, isPinned }); } closeModal(); }); @@ -207,7 +208,7 @@ function StorySettings({ } if (closeFriendIds.length === 1) { - return getUserFullName(usersById[closeFriendIds[0]]); + return getSenderTitle(lang, usersById[closeFriendIds[0]]); } return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i'); @@ -401,11 +402,11 @@ function StorySettings({ export default memo(withGlobal((global): StateProps => { const { storyViewer: { - storyId, userId, + storyId, peerId, }, } = selectTabState(global); - const story = (userId && storyId) - ? selectUserStory(global, userId, storyId) + const story = (peerId && storyId) + ? selectPeerStory(global, peerId, storyId) : undefined; return { diff --git a/src/components/story/StorySlides.tsx b/src/components/story/StorySlides.tsx index d2996a3c9..1262dc160 100644 --- a/src/components/story/StorySlides.tsx +++ b/src/components/story/StorySlides.tsx @@ -3,11 +3,11 @@ import React, { } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; -import type { ApiUserStories } from '../../api/types'; +import type { ApiPeerStories, ApiTypeStory } from '../../api/types'; import { ANIMATION_END_DELAY } from '../../config'; import { getStoryKey } from '../../global/helpers'; -import { selectIsStoryViewerOpen, selectTabState, selectUser } from '../../global/selectors'; +import { selectIsStoryViewerOpen, selectPeer, selectTabState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import { IS_FIREFOX, IS_SAFARI } from '../../util/windowEnvironment'; @@ -29,17 +29,17 @@ interface OwnProps { isOpen?: boolean; isReportModalOpen?: boolean; isDeleteModalOpen?: boolean; - onDelete: (storyId: number) => void; + onDelete: (story: ApiTypeStory) => void; onReport: NoneToVoidFunction; onClose: NoneToVoidFunction; } interface StateProps { - userIds: string[]; - currentUserId?: string; + peerIds: string[]; + currentPeerId?: string; currentStoryId?: number; - byUserId?: Record; - isSingleUser?: boolean; + byPeerId?: Record; + isSinglePeer?: boolean; isSingleStory?: boolean; isPrivate?: boolean; isArchive?: boolean; @@ -52,15 +52,15 @@ const ANIMATION_TO_ACTIVE_SCALE = '3'; const ANIMATION_FROM_ACTIVE_SCALE = `${FROM_ACTIVE_SCALE_VALUE}`; function StorySlides({ - userIds, - currentUserId, + peerIds, + currentPeerId, currentStoryId, isOpen, - isSingleUser, + isSinglePeer, isSingleStory, isPrivate, isArchive, - byUserId, + byPeerId, isReportModalOpen, isDeleteModalOpen, onDelete, @@ -68,12 +68,12 @@ function StorySlides({ onReport, }: OwnProps & StateProps) { const { stopActiveReaction } = getActions(); - const [renderingUserId, setRenderingUserId] = useState(currentUserId); + const [renderingPeerId, setRenderingPeerId] = useState(currentPeerId); const [renderingStoryId, setRenderingStoryId] = useState(currentStoryId); - const prevUserId = usePrevious(currentUserId); + const prevPeerId = usePrevious(currentPeerId); const renderingIsArchive = useCurrentOrPrev(isArchive, true); const renderingIsPrivate = useCurrentOrPrev(isPrivate, true); - const renderingIsSingleUser = useCurrentOrPrev(isSingleUser, true); + const renderingIsSinglePeer = useCurrentOrPrev(isSinglePeer, true); const renderingIsSingleStory = useCurrentOrPrev(isSingleStory, true); const slideSizes = useSlideSizes(); @@ -86,62 +86,62 @@ function StorySlides({ shouldBeReplaced: true, }); - function setRef(ref: HTMLDivElement | null, userId: string) { + function setRef(ref: HTMLDivElement | null, peerId: string) { if (!ref) { return; } - if (!rendersRef.current[userId]) { - rendersRef.current[userId] = { current: ref }; + if (!rendersRef.current[peerId]) { + rendersRef.current[peerId] = { current: ref }; } else { - rendersRef.current[userId].current = ref; + rendersRef.current[peerId].current = ref; } } - const renderingUserIds = useMemo(() => { - if (renderingUserId && (renderingIsSingleUser || renderingIsSingleStory)) { - return [renderingUserId]; + const renderingPeerIds = useMemo(() => { + if (renderingPeerId && (renderingIsSinglePeer || renderingIsSingleStory)) { + return [renderingPeerId]; } - const index = renderingUserId ? userIds.indexOf(renderingUserId) : -1; - if (!renderingUserId || index === -1) { + const index = renderingPeerId ? peerIds.indexOf(renderingPeerId) : -1; + if (!renderingPeerId || index === -1) { return []; } const start = Math.max(index - 4, 0); - const end = Math.min(index + 5, userIds.length); + const end = Math.min(index + 5, peerIds.length); - return userIds.slice(start, end); - }, [renderingIsSingleStory, renderingIsSingleUser, renderingUserId, userIds]); + return peerIds.slice(start, end); + }, [renderingIsSingleStory, renderingIsSinglePeer, renderingPeerId, peerIds]); - const renderingUserPosition = useMemo(() => { - if (!renderingUserIds.length || !renderingUserId) { + const renderingPeerPosition = useMemo(() => { + if (!renderingPeerIds.length || !renderingPeerId) { return -1; } - return renderingUserIds.indexOf(renderingUserId); - }, [renderingUserId, renderingUserIds]); + return renderingPeerIds.indexOf(renderingPeerId); + }, [renderingPeerId, renderingPeerIds]); - const currentUserPosition = useMemo(() => { - if (!renderingUserIds.length || !currentUserId) { + const currentPeerPosition = useMemo(() => { + if (!renderingPeerIds.length || !currentPeerId) { return -1; } - return renderingUserIds.indexOf(currentUserId); - }, [currentUserId, renderingUserIds]); + return renderingPeerIds.indexOf(currentPeerId); + }, [currentPeerId, renderingPeerIds]); useEffect(() => { const timeoutId = window.setTimeout(() => { - setRenderingUserId(currentUserId); + setRenderingPeerId(currentPeerId); }, ANIMATION_DURATION_MS); return () => { window.clearTimeout(timeoutId); }; - }, [currentUserId]); + }, [currentPeerId]); useEffect(() => { let timeOutId: number | undefined; - if (renderingUserId !== currentUserId) { + if (renderingPeerId !== currentPeerId) { timeOutId = window.setTimeout(() => { setRenderingStoryId(currentStoryId); }, ANIMATION_DURATION_MS); @@ -152,12 +152,12 @@ function StorySlides({ return () => { window.clearTimeout(timeOutId); }; - }, [renderingUserId, currentStoryId, currentUserId, renderingStoryId]); + }, [renderingPeerId, currentStoryId, currentPeerId, renderingStoryId]); useEffect(() => { let timeOutId: number | undefined; - if (prevUserId && prevUserId !== currentUserId) { + if (prevPeerId && prevPeerId !== currentPeerId) { setIsAnimating(true); timeOutId = window.setTimeout(() => { setIsAnimating(false); @@ -168,24 +168,24 @@ function StorySlides({ setIsAnimating(false); window.clearTimeout(timeOutId); }; - }, [prevUserId, currentUserId, setIsAnimating]); + }, [prevPeerId, currentPeerId, setIsAnimating]); useEffect(() => { return () => { - if (!currentStoryId || !currentUserId) return; + if (!currentStoryId || !currentPeerId) return; stopActiveReaction({ - containerId: getStoryKey(currentUserId, currentStoryId), + containerId: getStoryKey(currentPeerId, currentStoryId), }); }; - }, [currentStoryId, currentUserId]); + }, [currentStoryId, currentPeerId]); - const slideAmount = currentUserPosition - renderingUserPosition; - const isBackward = renderingUserPosition > currentUserPosition; + const slideAmount = currentPeerPosition - renderingPeerPosition; + const isBackward = renderingPeerPosition > currentPeerPosition; const calculateTransformX = useLastCallback(() => { - return userIds.reduce>((transformX, userId, index) => { - if (userId === renderingUserId) { - transformX[userId] = calculateOffsetX({ + return peerIds.reduce>((transformX, peerId, index) => { + if (peerId === renderingPeerId) { + transformX[peerId] = calculateOffsetX({ scale: slideSizes.scale, slideAmount, isBackward, @@ -193,18 +193,18 @@ function StorySlides({ }); } else { let isMoveThroughActiveSlide = false; - if (!isBackward && index > 0 && userIds[index - 1] === renderingUserId) { + if (!isBackward && index > 0 && peerIds[index - 1] === renderingPeerId) { isMoveThroughActiveSlide = true; } - if (isBackward && index < userIds.length - 1 && userIds[index + 1] === renderingUserId) { + if (isBackward && index < peerIds.length - 1 && peerIds[index + 1] === renderingPeerId) { isMoveThroughActiveSlide = true; } - transformX[userId] = calculateOffsetX({ + transformX[peerId] = calculateOffsetX({ scale: slideSizes.scale, slideAmount, isBackward, - isActiveSlideSize: currentUserId === userId && !isBackward, + isActiveSlideSize: currentPeerId === peerId && !isBackward, isMoveThroughActiveSlide, }); } @@ -216,7 +216,7 @@ function StorySlides({ useLayoutEffect(() => { const transformX = calculateTransformX(); - Object.entries(rendersRef.current).forEach(([userId, { current }]) => { + Object.entries(rendersRef.current).forEach(([peerId, { current }]) => { if (!current) return; if (!getIsAnimating()) { @@ -228,28 +228,28 @@ function StorySlides({ return; } - const scale = currentUserId === userId + const scale = currentPeerId === peerId ? ANIMATION_TO_ACTIVE_SCALE - : userId === renderingUserId ? ANIMATION_FROM_ACTIVE_SCALE : '1'; + : peerId === renderingPeerId ? ANIMATION_FROM_ACTIVE_SCALE : '1'; let offsetY = 0; - if (userId === renderingUserId) { + if (peerId === renderingPeerId) { offsetY = -ACTIVE_SLIDE_VERTICAL_CORRECTION_REM * FROM_ACTIVE_SCALE_VALUE; current.classList.add(styles.slideAnimationFromActive); } - if (userId === currentUserId) { + if (peerId === currentPeerId) { offsetY = ACTIVE_SLIDE_VERTICAL_CORRECTION_REM; current.classList.add(styles.slideAnimationToActive); } current.classList.add(styles.slideAnimation); - current.style.setProperty('--slide-translate-x', `${transformX[userId] || 0}px`); + current.style.setProperty('--slide-translate-x', `${transformX[peerId] || 0}px`); current.style.setProperty('--slide-translate-y', `${offsetY}rem`); current.style.setProperty('--slide-translate-scale', scale); }); - }, [currentUserId, getIsAnimating, renderingUserId]); + }, [currentPeerId, getIsAnimating, renderingPeerId]); - function renderStoryPreview(userId: string, index: number, position: number) { + function renderStoryPreview(peerId: string, index: number, position: number) { const style = buildStyle( `width: ${slideSizes.slide.width}px`, `height: ${slideSizes.slide.height}px`, @@ -262,20 +262,20 @@ function StorySlides({ return (
setRef(ref, userId)} + key={peerId} + ref={(ref) => setRef(ref, peerId)} className={className} style={style} >
); } - function renderStory(userId: string) { + function renderStory(peerId: string) { const style = buildStyle( `width: ${slideSizes.activeSlide.width}px`, `--slide-media-height: ${slideSizes.activeSlide.height}px`, @@ -283,13 +283,13 @@ function StorySlides({ return (
setRef(ref, userId)} + key={peerId} + ref={(ref) => setRef(ref, peerId)} className={buildClassName(styles.slide, styles.activeSlide)} style={style} >
- {renderingUserIds.length > 1 && ( + {renderingPeerIds.length > 1 && (
)} - {renderingUserIds.map((userId, index) => { - if (userId === renderingUserId) { - return renderStory(renderingUserId); + {renderingPeerIds.map((peerId, index) => { + if (peerId === renderingPeerId) { + return renderStory(renderingPeerId); } - return renderStoryPreview(userId, index, index - renderingUserPosition); + return renderStoryPreview(peerId, index, index - renderingPeerPosition); })}
); @@ -326,18 +326,18 @@ function StorySlides({ export default memo(withGlobal((global): StateProps => { const { storyViewer: { - userId: currentUserId, storyId: currentStoryId, isSingleUser, isSingleStory, isPrivate, isArchive, + peerId: currentPeerId, storyId: currentStoryId, isSinglePeer, isSingleStory, isPrivate, isArchive, }, } = selectTabState(global); - const { byUserId, orderedUserIds: { archived, active } } = global.stories; - const user = currentUserId ? selectUser(global, currentUserId) : undefined; + const { byPeerId, orderedPeerIds: { archived, active } } = global.stories; + const peer = currentPeerId ? selectPeer(global, currentPeerId) : undefined; return { - byUserId, - userIds: user?.areStoriesHidden ? archived : active, - currentUserId, + byPeerId, + peerIds: peer?.areStoriesHidden ? archived : active, + currentPeerId, currentStoryId, - isSingleUser, + isSinglePeer, isSingleStory, isPrivate, isArchive, diff --git a/src/components/story/StoryToggler.tsx b/src/components/story/StoryToggler.tsx index 70742930f..380c0f5c0 100644 --- a/src/components/story/StoryToggler.tsx +++ b/src/components/story/StoryToggler.tsx @@ -1,7 +1,7 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiUser } from '../../api/types'; +import type { ApiChat, ApiUser } from '../../api/types'; import { ANIMATION_END_DELAY, PREVIEW_AVATAR_COUNT } from '../../config'; import { selectPerformanceSettingsValue, selectTabState } from '../../global/selectors'; @@ -24,18 +24,20 @@ interface OwnProps { interface StateProps { currentUserId: string; - orderedUserIds: string[]; + orderedPeerIds: string[]; isShown: boolean; withAnimation?: boolean; usersById: Record; + chatsById: Record; } -const PRELOAD_USERS = 5; +const PRELOAD_PEERS = 5; function StoryToggler({ currentUserId, - orderedUserIds, + orderedPeerIds, usersById, + chatsById, canShow, isShown, isArchived, @@ -45,22 +47,22 @@ function StoryToggler({ const lang = useLang(); - const users = useMemo(() => { - if (orderedUserIds.length === 1) { - return [usersById[orderedUserIds[0]]]; + const peers = useMemo(() => { + if (orderedPeerIds.length === 1) { + return [usersById[orderedPeerIds[0]] || chatsById[orderedPeerIds[0]]]; } - return orderedUserIds - .map((id) => usersById[id]) - .filter((user) => user && user.id !== currentUserId) + return orderedPeerIds + .map((id) => usersById[id] || chatsById[id]) + .filter((peer) => peer && peer.id !== currentUserId) .slice(0, PREVIEW_AVATAR_COUNT) .reverse(); - }, [currentUserId, orderedUserIds, usersById]); + }, [currentUserId, orderedPeerIds, usersById, chatsById]); - const preloadUserIds = useMemo(() => { - return orderedUserIds.slice(0, PRELOAD_USERS); - }, [orderedUserIds]); - useStoryPreloader(preloadUserIds); + const preloadPeerIds = useMemo(() => { + return orderedPeerIds.slice(0, PRELOAD_PEERS); + }, [orderedPeerIds]); + useStoryPreloader(preloadPeerIds); const isVisible = canShow && isShown; // For some reason, setting 'slow' here also fixes scroll freezes on iOS when collapsing Story Ribbon @@ -90,10 +92,10 @@ function StoryToggler({ onClick={() => toggleStoryRibbon({ isShown: true, isArchived })} dir={lang.isRtl ? 'rtl' : undefined} > - {users.map((user) => ( + {peers.map((peer) => ( ((global, { isArchived }): StateProps => { - const { orderedUserIds: { archived, active } } = global.stories; + const { orderedPeerIds: { archived, active } } = global.stories; const { storyViewer: { isRibbonShown, isArchivedRibbonShown } } = selectTabState(global); const withAnimation = selectPerformanceSettingsValue(global, 'storyRibbonAnimations'); return { currentUserId: global.currentUserId!, - orderedUserIds: isArchived ? archived : active, + orderedPeerIds: isArchived ? archived : active, isShown: isArchived ? !isArchivedRibbonShown : !isRibbonShown, withAnimation, usersById: global.users.byId, + chatsById: global.chats.byId, }; })(StoryToggler)); diff --git a/src/components/story/StoryViewModal.tsx b/src/components/story/StoryViewModal.tsx index 85810fd4b..80298e264 100644 --- a/src/components/story/StoryViewModal.tsx +++ b/src/components/story/StoryViewModal.tsx @@ -13,8 +13,8 @@ import { } from '../../config'; import { selectIsCurrentUserPremium, + selectPeerStory, selectTabState, - selectUserStory, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { getServerTime } from '../../util/serverTime'; @@ -110,6 +110,7 @@ function StoryViewModal({ const handleLoadMore = useLastCallback(() => { if (!story?.id || nextOffset === undefined) return; loadStoryViews({ + peerId: story.peerId, storyId: story.id, offset: nextOffset, areReactionsFirst: areReactionsFirst || undefined, @@ -278,7 +279,7 @@ export default memo(withGlobal((global) => { const { storyId, viewsById, nextOffset, isLoading, } = viewModal || {}; - const story = storyId ? selectUserStory(global, global.currentUserId!, storyId) : undefined; + const story = storyId ? selectPeerStory(global, global.currentUserId!, storyId) : undefined; return { storyId, diff --git a/src/components/story/StoryViewer.module.scss b/src/components/story/StoryViewer.module.scss index 31b565366..d525082a0 100644 --- a/src/components/story/StoryViewer.module.scss +++ b/src/components/story/StoryViewer.module.scss @@ -244,7 +244,7 @@ @media (max-width: 600px) { width: 100% !important; - height: calc(100% - 4rem) !important; + height: calc(100% - 4rem) !important; // Update `MOBILE_MEDIA_BOTTOM_MARGIN` in MediaAreaOverlay on change border-radius: 0; } @@ -276,6 +276,18 @@ } } +.mediaAreaOverlay { + @media (max-width: 600px) { + right: auto; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + max-width: 100%; + max-height: 100%; + } +} + .content { position: absolute; top: 50%; @@ -644,48 +656,6 @@ right: 0; } -.ownForward { - position: absolute; - bottom: 0.25rem; - right: 0; -} - -.recentViewers { - position: absolute; - bottom: 0; - left: 0; - display: flex; - align-items: center; - transition: background-color 200ms, opacity 350ms !important; - padding: 0.5rem; - border-radius: var(--border-radius-default); - color: #fff; -} - -.recentViewersInteractive { - cursor: var(--custom-cursor, pointer); - - &:hover { - background-color: rgba(var(--color-text-secondary-rgb), 0.2); - } -} - -.recentViewersAvatars { - margin-inline-end: 0.5rem; -} - -.reactionCount { - margin-inline-start: 0.5rem; - display: flex; - gap: 0.125rem; - align-items: center; -} - -.reactionCountHeart { - color: var(--color-heart); - font-size: 1.25rem; -} - .modal :global(.modal-content) { padding: 0.5rem !important; max-height: 35rem; @@ -705,21 +675,6 @@ object-fit: cover; } -.mediaAreaOverlay { - position: absolute; - width: auto; - left: 50%; - transform: translateX(-50%); - pointer-events: none; -} - -.mediaArea { - position: absolute; - transform-origin: top left; - pointer-events: all; - cursor: var(--custom-cursor, pointer); -} - .ghost { position: absolute; z-index: 1; diff --git a/src/components/story/StoryViewer.tsx b/src/components/story/StoryViewer.tsx index 63c02e387..677bfe2a4 100644 --- a/src/components/story/StoryViewer.tsx +++ b/src/components/story/StoryViewer.tsx @@ -9,9 +9,9 @@ import type { StoryViewerOrigin } from '../../types'; import { ANIMATION_END_DELAY } from '../../config'; import { selectIsStoryViewerOpen, + selectPeerStory, selectPerformanceSettingsValue, selectTabState, - selectUserStory, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureEscKeyListener from '../../util/captureEscKeyListener'; @@ -41,7 +41,7 @@ const ANIMATION_DURATION = 250; interface StateProps { isOpen: boolean; - userId?: string; + peerId: string; storyId?: number; story?: ApiTypeStory; origin?: StoryViewerOrigin; @@ -52,7 +52,7 @@ interface StateProps { function StoryViewer({ isOpen, - userId, + peerId, storyId, story, origin, @@ -63,7 +63,7 @@ function StoryViewer({ const { closeStoryViewer, closeStoryPrivacyEditor } = getActions(); const lang = useLang(); - const [idStoryForDelete, setIdStoryForDelete] = useState(undefined); + const [storyToDelete, setStoryToDelete] = useState(undefined); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false); const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false); @@ -71,13 +71,13 @@ function StoryViewer({ const slideSizes = useSlideSizes(); const isPrevOpen = usePrevious(isOpen); const prevBestImageData = usePrevious(bestImageData); - const prevUserId = usePrevious(userId); + const prevPeerId = usePrevious(peerId); const prevOrigin = usePrevious(origin); const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations); useEffect(() => { if (!isOpen) { - setIdStoryForDelete(undefined); + setStoryToDelete(undefined); closeReportModal(); closeDeleteModal(); } @@ -101,14 +101,14 @@ function StoryViewer({ closeStoryViewer(); }, [closeStoryViewer]); - const handleOpenDeleteModal = useCallback((id: number) => { - setIdStoryForDelete(id); + const handleOpenDeleteModal = useCallback((s: ApiTypeStory) => { + setStoryToDelete(s); openDeleteModal(); }, []); const handleCloseDeleteModal = useCallback(() => { closeDeleteModal(); - setIdStoryForDelete(undefined); + setStoryToDelete(undefined); }, []); useEffect(() => (isOpen ? captureEscKeyListener(() => { @@ -116,13 +116,13 @@ function StoryViewer({ }) : undefined), [handleClose, isOpen]); useEffect(() => { - if (isGhostAnimation && !isPrevOpen && isOpen && userId && thumbnail && origin !== undefined) { + if (isGhostAnimation && !isPrevOpen && isOpen && peerId && thumbnail && origin !== undefined) { dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); - animateOpening(userId, origin, thumbnail, bestImageData, slideSizes.activeSlide); + animateOpening(peerId, origin, thumbnail, bestImageData, slideSizes.activeSlide); } - if (isGhostAnimation && isPrevOpen && !isOpen && prevUserId && prevBestImageData && prevOrigin !== undefined) { + if (isGhostAnimation && isPrevOpen && !isOpen && prevPeerId && prevBestImageData && prevOrigin !== undefined) { dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); - animateClosing(prevUserId, prevOrigin, prevBestImageData); + animateClosing(prevPeerId, prevOrigin, prevBestImageData); } }, [ isGhostAnimation, @@ -132,8 +132,8 @@ function StoryViewer({ isPrevOpen, slideSizes.activeSlide, thumbnail, - userId, - prevUserId, + peerId, + prevPeerId, origin, prevOrigin, ]); @@ -169,7 +169,7 @@ function StoryViewer({ @@ -179,7 +179,7 @@ function StoryViewer({ isOpen={isReportModalOpen} onClose={closeReportModal} subject="story" - userId={userId} + peerId={peerId!} storyId={storyId} /> @@ -189,16 +189,16 @@ function StoryViewer({ export default memo(withGlobal((global): StateProps => { const { shouldSkipHistoryAnimations, storyViewer: { - storyId, userId, isPrivacyModalOpen, origin, + storyId, peerId, isPrivacyModalOpen, origin, }, } = selectTabState(global); - const story = userId && storyId ? selectUserStory(global, userId, storyId) : undefined; + const story = peerId && storyId ? selectPeerStory(global, peerId, storyId) : undefined; const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations'); return { isOpen: selectIsStoryViewerOpen(global), shouldSkipHistoryAnimations, - userId, + peerId: peerId!, storyId, story, origin, diff --git a/src/components/story/helpers/ghostAnimation.ts b/src/components/story/helpers/ghostAnimation.ts index 6ce172755..9356490a0 100644 --- a/src/components/story/helpers/ghostAnimation.ts +++ b/src/components/story/helpers/ghostAnimation.ts @@ -4,7 +4,7 @@ import { StoryViewerOrigin } from '../../../types'; import { ANIMATION_END_DELAY } from '../../../config'; import fastBlur from '../../../lib/fastBlur'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; -import { getUserStoryHtmlId } from '../../../global/helpers'; +import { getPeerStoryHtmlId } from '../../../global/helpers'; import { applyStyles } from '../../../util/animation'; import stopEvent from '../../../util/stopEvent'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment'; @@ -207,7 +207,7 @@ function createGhost(source: string, hasBlur = false, isGhost2 = false) { function getNodes(origin: StoryViewerOrigin, userId: string) { let containerSelector; - const mediaSelector = `#${getUserStoryHtmlId(userId)}`; + const mediaSelector = `#${getPeerStoryHtmlId(userId)}`; switch (origin) { case StoryViewerOrigin.StoryRibbon: diff --git a/src/components/story/helpers/ribbonAnimation.ts b/src/components/story/helpers/ribbonAnimation.ts index 6999a0b20..eca522bd2 100644 --- a/src/components/story/helpers/ribbonAnimation.ts +++ b/src/components/story/helpers/ribbonAnimation.ts @@ -19,10 +19,10 @@ export function animateOpening(isArchived?: boolean) { cancelDelayedCallbacks(); const { - container, toggler, leftMainHeader, ribbonUsers, toggleAvatars, + container, toggler, leftMainHeader, ribbonPeers, toggleAvatars, } = getHTMLElements(isArchived); - if (!toggler || !toggleAvatars || !ribbonUsers || !container || !leftMainHeader) { + if (!toggler || !toggleAvatars || !ribbonPeers || !container || !leftMainHeader) { return; } @@ -32,13 +32,13 @@ export function animateOpening(isArchived?: boolean) { // Toggle avatars are in the reverse order const lastToggleAvatar = toggleAvatars[0]; const firstToggleAvatar = toggleAvatars[toggleAvatars.length - 1]; - const lastId = getUserId(lastToggleAvatar); + const lastId = getPeerId(lastToggleAvatar); - Array.from(ribbonUsers).reverse().forEach((user, index, { length }) => { - const id = getUserId(user); + Array.from(ribbonPeers).reverse().forEach((peer, index, { length }) => { + const id = getPeerId(peer); if (!id) return; const isLast = id === lastId; - let toggleAvatar = selectByUserId(toggler, id); + let toggleAvatar = selectByPeerId(toggler, id); let zIndex = RIBBON_Z_INDEX + index + 1; if (!toggleAvatar) { const isSelf = index === length - 1; @@ -60,7 +60,7 @@ export function animateOpening(isArchived?: boolean) { const { left: toLeft, width: toWidth, - } = user.getBoundingClientRect(); + } = peer.getBoundingClientRect(); if (toLeft > headerRight) { return; @@ -81,7 +81,7 @@ export function animateOpening(isArchived?: boolean) { requestMutation(() => { if (!toggleAvatar) return; - const ghost = createGhost(user); + const ghost = createGhost(peer); let ghost2: HTMLElement | undefined; @@ -117,7 +117,7 @@ export function animateOpening(isArchived?: boolean) { container.appendChild(ghost2); } toggleAvatar.classList.add('animating'); - user.classList.add('animating'); + peer.classList.add('animating'); requestMutation(() => { applyStyles(ghost, { @@ -143,7 +143,7 @@ export function animateOpening(isArchived?: boolean) { container.removeChild(ghost2); } toggleAvatar?.classList.remove('animating'); - user.classList.remove('animating'); + peer.classList.remove('animating'); }); }, ANIMATION_DURATION + ANIMATION_END_DELAY); @@ -160,11 +160,11 @@ export function animateClosing(isArchived?: boolean) { container, toggler, toggleAvatars, - ribbonUsers, + ribbonPeers, leftMainHeader, } = getHTMLElements(isArchived); - if (!toggler || !toggleAvatars || !ribbonUsers || !container || !leftMainHeader) { + if (!toggler || !toggleAvatars || !ribbonPeers || !container || !leftMainHeader) { return; } const { right: headerRight } = leftMainHeader.getBoundingClientRect(); @@ -172,13 +172,13 @@ export function animateClosing(isArchived?: boolean) { // Toggle avatars are in the reverse order const lastToggleAvatar = toggleAvatars[0]; const firstToggleAvatar = toggleAvatars[toggleAvatars.length - 1]; - const lastId = getUserId(lastToggleAvatar); + const lastId = getPeerId(lastToggleAvatar); - Array.from(ribbonUsers).reverse().forEach((user, index, { length }) => { - const id = getUserId(user); + Array.from(ribbonPeers).reverse().forEach((peer, index, { length }) => { + const id = getPeerId(peer); if (!id) return; const isLast = id === lastId; - let toggleAvatar = selectByUserId(toggler, id); + let toggleAvatar = selectByPeerId(toggler, id); let zIndex = RIBBON_Z_INDEX + index + 1; if (!toggleAvatar) { const isSelf = index === length - 1; @@ -194,7 +194,7 @@ export function animateClosing(isArchived?: boolean) { top: fromTop, left: fromLeft, width: fromWidth, - } = user.getBoundingClientRect(); + } = peer.getBoundingClientRect(); let { left: toLeft, @@ -220,7 +220,7 @@ export function animateClosing(isArchived?: boolean) { const fromScale = fromWidth / (toWidth + 2 * STROKE_OFFSET); requestMutation(() => { - const ghost = createGhost(user); + const ghost = createGhost(peer); let ghost2: HTMLElement | undefined; if (zIndex > RIBBON_Z_INDEX) { @@ -249,7 +249,7 @@ export function animateClosing(isArchived?: boolean) { }); } - user.classList.add('animating'); + peer.classList.add('animating'); toggleAvatar!.classList.add('animating'); container.appendChild(ghost); @@ -280,7 +280,7 @@ export function animateClosing(isArchived?: boolean) { if (ghost2 && container.contains(ghost2)) { container.removeChild(ghost2); } - user.classList.remove('animating'); + peer.classList.remove('animating'); toggleAvatar!.classList.remove('animating'); }); }, ANIMATION_DURATION + ANIMATION_END_DELAY); @@ -300,14 +300,14 @@ function getHTMLElements(isArchived?: boolean) { const toggler = container.querySelector('#StoryToggler'); const ribbon = container.querySelector('#StoryRibbon'); const leftMainHeader = container.querySelector('.left-header'); - const ribbonUsers = ribbon?.querySelectorAll(`.${ribbonStyles.user}`); + const ribbonPeers = ribbon?.querySelectorAll(`.${ribbonStyles.peer}`); const toggleAvatars = toggler?.querySelectorAll('.Avatar'); return { container, toggler, leftMainHeader, - ribbonUsers, + ribbonPeers, toggleAvatars, }; } @@ -331,11 +331,11 @@ function createGhost(sourceEl: HTMLElement) { return ghost; } -function getUserId(el: HTMLElement) { +function getPeerId(el: HTMLElement) { return el.getAttribute('data-peer-id'); } -function selectByUserId(el: HTMLElement, id: string) { +function selectByPeerId(el: HTMLElement, id: string) { return el.querySelector(`[data-peer-id="${id}"]`); } diff --git a/src/components/story/hooks/useStoryPreloader.ts b/src/components/story/hooks/useStoryPreloader.ts index eda9f42a7..d7dcd0abd 100644 --- a/src/components/story/hooks/useStoryPreloader.ts +++ b/src/components/story/hooks/useStoryPreloader.ts @@ -4,20 +4,20 @@ import { getGlobal } from '../../../global'; import { ApiMediaFormat } from '../../../api/types'; import { getStoryMediaHash } from '../../../global/helpers'; -import { selectUserStories } from '../../../global/selectors'; +import { selectPeerStories } from '../../../global/selectors'; import * as mediaLoader from '../../../util/mediaLoader'; import { pause } from '../../../util/schedulers'; const preloadedStories: Record> = {}; -const USER_STORIES_FOR_PRELOAD = 5; +const PEER_STORIES_FOR_PRELOAD = 5; const PROGRESSIVE_PRELOAD_DURATION = 1000; const FIRST_PRELOAD_DELAY = 1000; const canPreload = pause(FIRST_PRELOAD_DELAY); -function useStoryPreloader(userIds: string[]): void; -function useStoryPreloader(userId: string, aroundStoryId?: number): void; -function useStoryPreloader(userId: string | string[], aroundStoryId?: number) { +function useStoryPreloader(peerIds: string[]): void; +function useStoryPreloader(peerId: string, aroundStoryId?: number): void; +function useStoryPreloader(peerId: string | string[], aroundStoryId?: number) { useEffect(() => { const preloadHashes = async (mediaHashes: { hash: string; format: ApiMediaFormat }[]) => { await canPreload; @@ -30,14 +30,14 @@ function useStoryPreloader(userId: string | string[], aroundStoryId?: number) { }); }; - const userIds = Array.isArray(userId) ? userId : [userId]; + const peerIds = Array.isArray(peerId) ? peerId : [peerId]; - userIds.forEach((id) => { - const storyId = aroundStoryId || getGlobal().stories.byUserId[id]?.orderedIds?.[0]; + peerIds.forEach((id) => { + const storyId = aroundStoryId || getGlobal().stories.byPeerId[id]?.orderedIds?.[0]; if (!storyId) return; preloadHashes(getPreloadMediaHashes(id, storyId)); }); - }, [aroundStoryId, userId]); + }, [aroundStoryId, peerId]); } function findIdsAroundCurrentId(ids: T[], currentId: T, aroundAmount: number): T[] { @@ -46,21 +46,21 @@ function findIdsAroundCurrentId(ids: T[], currentId: T, aroundAmount: number) return ids.slice(currentIndex - aroundAmount, currentIndex + aroundAmount); } -function getPreloadMediaHashes(userId: string, storyId: number) { - const userStories = selectUserStories(getGlobal(), userId); - if (!userStories || !userStories.orderedIds?.length) { +function getPreloadMediaHashes(peerId: string, storyId: number) { + const peerStories = selectPeerStories(getGlobal(), peerId); + if (!peerStories || !peerStories.orderedIds?.length) { return []; } - const preloadIds = findIdsAroundCurrentId(userStories.orderedIds, storyId, USER_STORIES_FOR_PRELOAD); + const preloadIds = findIdsAroundCurrentId(peerStories.orderedIds, storyId, PEER_STORIES_FOR_PRELOAD); const mediaHashes: { hash: string; format: ApiMediaFormat }[] = []; preloadIds.forEach((currentStoryId) => { - if (preloadedStories[userId]?.has(currentStoryId)) { + if (preloadedStories[peerId]?.has(currentStoryId)) { return; } - const story = userStories.byId[currentStoryId]; + const story = peerStories.byId[currentStoryId]; if (!story || !('content' in story)) { return; } @@ -77,7 +77,7 @@ function getPreloadMediaHashes(userId: string, storyId: number) { mediaHashes.push({ hash: getStoryMediaHash(story, 'full', true)!, format: ApiMediaFormat.Progressive }); } - preloadedStories[userId] = (preloadedStories[userId] || new Set()).add(currentStoryId); + preloadedStories[peerId] = (preloadedStories[peerId] || new Set()).add(currentStoryId); }); return mediaHashes; diff --git a/src/components/story/hooks/useStoryProps.ts b/src/components/story/hooks/useStoryProps.ts index 8f688a4e3..b3f93b22a 100644 --- a/src/components/story/hooks/useStoryProps.ts +++ b/src/components/story/hooks/useStoryProps.ts @@ -34,7 +34,6 @@ export default function useStoryProps( const hasFullData = Boolean(fullMediaData || altMediaData); const bestImageData = isVideo ? previewBlobUrl : fullMediaData || previewBlobUrl; const hasThumb = !previewBlobUrl && !hasFullData; - const mediaAreas = isLoadedStory ? story.mediaAreas : undefined; const canDownload = isCurrentUserPremium && isLoadedStory && !story.noForwards; const downloadHash = isLoadedStory ? getStoryMediaHash(story, 'download') : undefined; @@ -56,7 +55,6 @@ export default function useStoryProps( hasFullData, bestImageData, hasThumb, - mediaAreas, canDownload, downloadMediaData, }; diff --git a/src/components/story/mediaArea/MediaArea.module.scss b/src/components/story/mediaArea/MediaArea.module.scss new file mode 100644 index 000000000..b2ef2fe43 --- /dev/null +++ b/src/components/story/mediaArea/MediaArea.module.scss @@ -0,0 +1,124 @@ +.overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: var(--media-width, auto); + aspect-ratio: 9 / 16; + pointer-events: none; +} + +.mediaArea { + position: absolute; + transform-origin: top left; + pointer-events: all; + cursor: var(--custom-cursor, pointer); +} + +.preview .mediaArea { + pointer-events: none; +} + +.shiny { + overflow: hidden; +} + +.shiny::before { + --color-shine: rgb(255, 255, 255, 0.5); + content: ""; + position: absolute; + top: 0; + + display: block; + width: 100%; + height: 100%; + background: linear-gradient(to right, transparent 0%, var(--color-shine) 50%, transparent 100%); + animation: wave 1s cubic-bezier(0.4, 0, 0.6, 1) forwards; + + @keyframes wave { + from { + transform: translateX(-100%); + } + to { + transform: translateX(100%); + } + } +} + +.suggestedReaction { + --background-color: white; + color: black; +} + +.dark { + --background-color: black; + color: white; +} + +.background { + width: 100%; + height: 100%; + background-color: var(--background-color); + border-radius: 50%; + filter: drop-shadow(0 0.125rem 0.25rem var(--color-default-shadow)); + cursor: var(--custom-cursor, pointer); + + &::before, + &::after { + position: absolute; + content: ""; + background-color: var(--background-color); + border-radius: 50%; + } + + &::before { + bottom: 5%; + right: 5%; + width: 30%; + height: 30%; + } + + &::after { + bottom: 0; + right: -5%; + width: 10%; + height: 10%; + } + + &.flipped { + &::before { + right: auto; + left: 5%; + } + + &::after { + right: auto; + left: -5%; + } + } + + .dark & { + opacity: 0.5; + } +} + +.reaction { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: transform 200ms ease-out; + + &.withCount { + transform: (translate(-50%, -65%) scale(0.7)); + } +} + +.reactionCount { + position: absolute; + bottom: 8%; + left: 50%; + transform: translateX(-50%); + font-weight: 500; +} diff --git a/src/components/story/mediaArea/MediaAreaOverlay.tsx b/src/components/story/mediaArea/MediaAreaOverlay.tsx new file mode 100644 index 000000000..403296398 --- /dev/null +++ b/src/components/story/mediaArea/MediaAreaOverlay.tsx @@ -0,0 +1,117 @@ +import React, { memo, useEffect, useRef } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import type { ApiMediaArea, ApiStory } from '../../../api/types'; + +import { MOBILE_SCREEN_MAX_WIDTH } from '../../../config'; +import { requestMutation } from '../../../lib/fasterdom/fasterdom'; +import buildClassName from '../../../util/buildClassName'; +import buildStyle from '../../../util/buildStyle'; +import { REM } from '../../common/helpers/mediaDimensions'; + +import useWindowSize from '../../../hooks/useWindowSize'; + +import MediaAreaSuggestedReaction from './MediaAreaSuggestedReaction'; + +import styles from './MediaArea.module.scss'; + +type OwnProps = { + story: ApiStory; + isActive?: boolean; + className?: string; +}; + +const STORY_ASPECT_RATIO = 9 / 16; +const MOBILE_MEDIA_BOTTOM_MARGIN = 4 * REM; + +const MediaAreaOverlay = ({ + story, isActive, className, +}: OwnProps) => { + const { openMapModal } = getActions(); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + const windowSize = useWindowSize(); + + useEffect(() => { + if (!ref.current || !isActive) return; + const element = ref.current; + + if (windowSize.width > MOBILE_SCREEN_MAX_WIDTH) { + requestMutation(() => { + element.style.removeProperty('--media-width'); + }); + return; + } + + const adaptedHeight = windowSize.height - MOBILE_MEDIA_BOTTOM_MARGIN; + + const screenAspectRatio = windowSize.width / adaptedHeight; + + const width = screenAspectRatio > STORY_ASPECT_RATIO ? adaptedHeight * STORY_ASPECT_RATIO : windowSize.width; + requestMutation(() => { + element.style.setProperty('--media-width', `${width}px`); + }); + }, [isActive, windowSize]); + + const handleMediaAreaClick = (mediaArea: ApiMediaArea) => { + if (mediaArea.type === 'geoPoint' || mediaArea.type === 'venue') { + openMapModal({ geoPoint: mediaArea.geo }); + } + }; + + const mediaAreas = story.mediaAreas; + + return ( +
+ {mediaAreas?.map((mediaArea, i) => { + switch (mediaArea.type) { + case 'geoPoint': + case 'venue': + return ( +
handleMediaAreaClick(mediaArea)} + /> + ); + case 'suggestedReaction': + return ( + + ); + default: + return undefined; + } + })} +
+ ); +}; + +function prepareStyle(mediaArea: ApiMediaArea) { + const { + x, y, width, height, rotation, + } = mediaArea.coordinates; + + return buildStyle( + `left: ${x}%`, + `top: ${y}%`, + `width: ${width}%`, + `height: ${height}%`, + `transform: rotate(${rotation}deg) translate(-50%, -50%)`, + ); +} + +export default memo(MediaAreaOverlay); diff --git a/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx b/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx new file mode 100644 index 000000000..c5dae8b0b --- /dev/null +++ b/src/components/story/mediaArea/MediaAreaSuggestedReaction.tsx @@ -0,0 +1,107 @@ +import React, { + memo, useMemo, useRef, useState, +} from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import type { ApiMediaAreaSuggestedReaction, ApiStory } from '../../../api/types'; + +import { getStoryKey, isSameReaction, isUserId } from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; +import buildStyle from '../../../util/buildStyle'; +import { REM } from '../../common/helpers/mediaDimensions'; + +import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useResizeObserver from '../../../hooks/useResizeObserver'; + +import ReactionAnimatedEmoji from '../../common/reactions/ReactionAnimatedEmoji'; + +import styles from './MediaArea.module.scss'; + +type OwnProps = { + story: ApiStory; + mediaArea: ApiMediaAreaSuggestedReaction; + index: number; + isPreview?: boolean; + className?: string; + style?: string; +}; + +const REACTION_SIZE_MULTIPLIER = 0.6; +const EFFECT_SIZE_MULTIPLIER = 2; + +const MediaAreaSuggestedReaction = ({ + story, + mediaArea, + index, + className, + style, + isPreview, +}: OwnProps) => { + const { sendStoryReaction } = getActions(); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const [customEmojiSize, setCustomEmojiSize] = useState(1.5 * REM); + + const { peerId, id, reactions } = story; + const { reaction, isDark, isFlipped } = mediaArea; + + const isChannel = !isUserId(peerId); + const containerId = `${getStoryKey(peerId, id)}-${index}-${isPreview ? 'preview' : 'viewer'}`; + + const reactionCount = useMemo(() => ( + reactions?.find((r) => isSameReaction(r.reaction, reaction))?.count + ), [reaction, reactions]); + const shouldDisplayCount = !isPreview && Boolean(reactionCount) && isChannel; + + const updateCustomEmojiSize = useLastCallback(() => { + if (!ref.current) return; + const height = ref.current.clientHeight; + setCustomEmojiSize(Math.round(height * REACTION_SIZE_MULTIPLIER)); + }); + + useEffectWithPrevDeps(([prevReactionCount]) => { + if (Boolean(reactionCount) !== Boolean(prevReactionCount)) { + updateCustomEmojiSize(); + } + }, [reactionCount]); + + useResizeObserver(ref, updateCustomEmojiSize); + + const handleClick = useLastCallback(() => { + sendStoryReaction({ + peerId, + storyId: id, + containerId, + reaction, + }); + }); + + return ( +
+
+ + {shouldDisplayCount && ( + {reactionCount} + )} +
+ ); +}; + +export default memo(MediaAreaSuggestedReaction); diff --git a/src/config.ts b/src/config.ts index 065faeff5..6c6127e64 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import type { ApiReactionEmoji } from './api/types'; import type { ApiLimitType } from './global/types'; export const APP_CODE_NAME = 'A'; @@ -213,7 +214,6 @@ export const MENU_TRANSITION_DURATION = 200; export const SLIDE_TRANSITION_DURATION = 450; export const VIDEO_WEBM_TYPE = 'video/webm'; - export const GIF_MIME_TYPE = 'image/gif'; export const SUPPORTED_IMAGE_CONTENT_TYPES = new Set([ @@ -279,6 +279,10 @@ export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', ' export const API_CHAT_TYPES = ['bots', 'channels', 'chats', 'users'] as const; +export const HEART_REACTION: ApiReactionEmoji = { + emoticon: '❤', +}; + // MTProto constants export const SERVICE_NOTIFICATIONS_USER_ID = '777000'; export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must be equal to 708513 diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index 33fae244d..dc77c4d2f 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -1,5 +1,5 @@ import type { - ApiChat, ApiChatType, ApiContact, ApiUrlAuthResult, ApiUser, + ApiChat, ApiChatType, ApiContact, ApiPeer, ApiUrlAuthResult, } from '../../../api/types'; import type { InlineBotSettings } from '../../../types'; import type { RequiredGlobalActions } from '../../index'; @@ -1068,7 +1068,7 @@ async function searchInlineBot(global: T, { } async function sendBotCommand( - chat: ApiChat, threadId = MAIN_THREAD_ID, command: string, replyingTo?: number, sendAs?: ApiChat | ApiUser, + chat: ApiChat, threadId = MAIN_THREAD_ID, command: string, replyingTo?: number, sendAs?: ApiPeer, ) { await callApi('sendMessage', { chat, diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 87ee00ba1..9d61b5472 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -42,7 +42,6 @@ import { isChatSummaryOnly, isChatSuperGroup, isUserBot, - isUserId, } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal, @@ -68,11 +67,11 @@ import { updateChats, updateListedTopicIds, updateManagementProgress, + updatePeerFullInfo, updateThreadInfo, updateTopic, updateTopics, updateUser, - updateUserFullInfo, } from '../../reducers'; import { updateGroupCall } from '../../reducers/calls'; import { updateTabState } from '../../reducers/tabs'; @@ -2249,15 +2248,9 @@ addActionHandler('togglePeerTranslations', async (global, actions, payload): Pro if (result === undefined) return; global = getGlobal(); - if (isUserId(chatId)) { - global = updateUserFullInfo(global, chatId, { - isTranslationDisabled: isEnabled ? undefined : true, - }); - } else { - global = updateChatFullInfo(global, chatId, { - isTranslationDisabled: isEnabled ? undefined : true, - }); - } + global = updatePeerFullInfo(global, chatId, { + isTranslationDisabled: isEnabled ? undefined : true, + }); setGlobal(global); }); diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 11bbda551..0028ba8ce 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -5,11 +5,11 @@ import type { ApiMessageEntity, ApiNewPoll, ApiOnProgress, + ApiPeer, ApiSticker, ApiStory, ApiStorySkipped, ApiTypeReplyTo, - ApiUser, ApiVideo, } from '../../../api/types'; import type { RequiredGlobalActions } from '../../index'; @@ -95,6 +95,7 @@ import { selectListedIds, selectNoWebPage, selectOutlyingListByMessageId, + selectPeerStory, selectPinnedIds, selectRealLastReadId, selectReplyingToId, @@ -108,7 +109,6 @@ import { selectTranslationLanguage, selectUser, selectUserFullInfo, - selectUserStory, selectViewportIds, } from '../../selectors'; import { deleteMessages } from '../apiUpdaters/messages'; @@ -245,8 +245,8 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => { const { messageList, tabId = getCurrentTabId() } = payload; - const { storyId, userId: storyUserId } = selectCurrentViewedStory(global, tabId); - const isStoryReply = Boolean(storyId && storyUserId); + const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId); + const isStoryReply = Boolean(storyId && storyPeerId); if (!messageList && !isStoryReply) { return undefined; @@ -254,7 +254,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => let { chatId, threadId, type } = messageList || {}; if (isStoryReply) { - chatId = storyUserId!; + chatId = storyPeerId!; threadId = MAIN_THREAD_ID; type = 'thread'; } @@ -276,7 +276,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => : replyingToMessage?.replyToTopMessageId || replyingToMessage?.replyToMessageId; const replyingTo: ApiTypeReplyTo | undefined = replyingToId ? { replyingTo: replyingToId, replyingToTopId } - : (isStoryReply ? { userId: storyUserId!, storyId: storyId! } : undefined); + : (isStoryReply ? { userId: storyPeerId!, storyId: storyId! } : undefined); const params = { ...payload, @@ -1182,7 +1182,7 @@ async function sendMessage(global: T, params: { poll?: ApiNewPoll; isSilent?: boolean; scheduledAt?: number; - sendAs?: ApiChat | ApiUser; + sendAs?: ApiPeer; currentThreadId: number; groupedId?: string; }) { @@ -1418,7 +1418,7 @@ addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { const { url, shouldSkipModal, tabId = getCurrentTabId() } = payload; const urlWithProtocol = ensureProtocol(url)!; - const isStoriesViewerOpen = Boolean(selectTabState(global, tabId).storyViewer.userId); + const isStoriesViewerOpen = Boolean(selectTabState(global, tabId).storyViewer.peerId); if (urlWithProtocol.match(RE_TME_LINK) || urlWithProtocol.match(RE_TG_LINK)) { if (isStoriesViewerOpen) { @@ -1523,7 +1523,7 @@ addActionHandler('forwardStory', (global, actions, payload): ActionReturnType => const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined; const toChat = toChatId ? selectChat(global, toChatId) : undefined; const story = fromChatId && storyId - ? selectUserStory(global, fromChatId, storyId) + ? selectPeerStory(global, fromChatId, storyId) : undefined; if (!fromChat || !toChat || !story || 'isDeleted' in story) { diff --git a/src/global/actions/api/stories.ts b/src/global/actions/api/stories.ts index 6799a4fc9..bcb31a0db 100644 --- a/src/global/actions/api/stories.ts +++ b/src/global/actions/api/stories.ts @@ -6,26 +6,28 @@ import { buildCollectionByKey } from '../../../util/iteratees'; import { translate } from '../../../util/langProvider'; import { getServerTime } from '../../../util/serverTime'; import { callApi } from '../../../api/gramjs'; -import { buildApiInputPrivacyRules, getStoryKey } from '../../helpers'; +import { buildApiInputPrivacyRules } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { + addChats, addStories, - addStoriesForUser, + addStoriesForPeer, addUsers, - removeUserStory, - toggleUserStoriesHidden, - updateLastReadStoryForUser, - updateLastViewedStoryForUser, + removePeerStory, + updateLastReadStoryForPeer, + updateLastViewedStoryForPeer, + updatePeer, + updatePeerPinnedStory, + updatePeerStoriesHidden, + updatePeerStory, + updatePeersWithStories, + updateSentStoryReaction, updateStealthMode, updateStoryViews, updateStoryViewsLoading, - updateUser, - updateUserPinnedStory, - updateUserStory, - updateUsersWithStories, } from '../../reducers'; import { - selectUser, selectUserStories, selectUserStory, + selectPeer, selectPeerStories, selectPeerStory, } from '../../selectors'; const INFINITE_LOOP_MARKER = 100; @@ -61,10 +63,11 @@ addActionHandler('loadAllStories', async (global): Promise => { global = getGlobal(); global.stories.stateHash = result.state; - if ('userStories' in result) { + if ('peerStories' in result) { global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStories(global, result.userStories); - global = updateUsersWithStories(global, result.userStories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStories(global, result.peerStories); + global = updatePeersWithStories(global, result.peerStories); global = updateStealthMode(global, result.stealthMode); global.stories.hasNext = result.hasMore; } @@ -105,10 +108,11 @@ addActionHandler('loadAllHiddenStories', async (global): Promise => { global = getGlobal(); global.stories.archiveStateHash = result.state; - if ('userStories' in result) { + if ('peerStories' in result) { global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStories(global, result.userStories); - global = updateUsersWithStories(global, result.userStories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStories(global, result.peerStories); + global = updatePeersWithStories(global, result.peerStories); global = updateStealthMode(global, result.stealthMode); global.stories.hasNextInArchive = result.hasMore; } @@ -117,14 +121,14 @@ addActionHandler('loadAllHiddenStories', async (global): Promise => { } }); -addActionHandler('loadUserSkippedStories', async (global, actions, payload): Promise => { - const { userId } = payload; - const user = selectUser(global, userId); - const userStories = selectUserStories(global, userId); - if (!user || !userStories) { +addActionHandler('loadPeerSkippedStories', async (global, actions, payload): Promise => { + const { peerId } = payload; + const peer = selectPeer(global, peerId); + const peerStories = selectPeerStories(global, peerId); + if (!peer || !peerStories) { return; } - const skippedStoryIds = Object.values(userStories.byId).reduce((acc, story) => { + const skippedStoryIds = Object.values(peerStories.byId).reduce((acc, story) => { if (!('content' in story)) { acc.push(story.id); } @@ -136,8 +140,8 @@ addActionHandler('loadUserSkippedStories', async (global, actions, payload): Pro return; } - const result = await callApi('fetchUserStoriesByIds', { - user, + const result = await callApi('fetchPeerStoriesByIds', { + peer, ids: skippedStoryIds, }); @@ -147,34 +151,35 @@ addActionHandler('loadUserSkippedStories', async (global, actions, payload): Pro global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, userId, result.stories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories); setGlobal(global); }); addActionHandler('viewStory', async (global, actions, payload): Promise => { - const { userId, storyId, tabId = getCurrentTabId() } = payload; - const user = selectUser(global, userId); - const story = selectUserStory(global, userId, storyId); - if (!user || !story || !('content' in story)) { + const { peerId, storyId, tabId = getCurrentTabId() } = payload; + const peer = selectPeer(global, peerId); + const story = selectPeerStory(global, peerId, storyId); + if (!peer || !story || !('content' in story)) { return; } - global = updateLastViewedStoryForUser(global, userId, storyId, tabId); + global = updateLastViewedStoryForPeer(global, peerId, storyId, tabId); setGlobal(global); const serverTime = getServerTime(); if (story.expireDate < serverTime && story.isPinned) { - void callApi('viewStory', { user, storyId }); + void callApi('viewStory', { peer, storyId }); } - const isUnread = (global.stories.byUserId[userId].lastReadId || 0) < story.id; + const isUnread = (global.stories.byPeerId[peerId].lastReadId || 0) < story.id; if (!isUnread) { return; } const result = await callApi('markStoryRead', { - user, + peer, storyId, }); @@ -183,116 +188,130 @@ addActionHandler('viewStory', async (global, actions, payload): Promise => } global = getGlobal(); - global = updateLastReadStoryForUser(global, userId, storyId); + global = updateLastReadStoryForPeer(global, peerId, storyId); setGlobal(global); }); addActionHandler('deleteStory', async (global, actions, payload): Promise => { - const { storyId } = payload; + const { peerId, storyId } = payload; - const result = await callApi('deleteStory', { storyId }); + const peer = selectPeer(global, peerId); + if (!peer) { + return; + } + + const result = await callApi('deleteStory', { peer, storyId }); if (!result) { return; } global = getGlobal(); - global = removeUserStory(global, global.currentUserId!, storyId); + global = removePeerStory(global, peerId, storyId); setGlobal(global); }); addActionHandler('toggleStoryPinned', async (global, actions, payload): Promise => { - const { storyId, isPinned } = payload; + const { peerId, storyId, isPinned } = payload; - const story = selectUserStory(global, global.currentUserId!, storyId); + const peer = selectPeer(global, peerId); + if (!peer) { + return; + } + + const story = selectPeerStory(global, peerId, storyId); const currentIsPinned = story && 'content' in story ? story.isPinned : undefined; - global = updateUserStory(global, global.currentUserId!, storyId, { isPinned }); - global = updateUserPinnedStory(global, global.currentUserId!, storyId, isPinned); + global = updatePeerStory(global, peerId, storyId, { isPinned }); + global = updatePeerPinnedStory(global, peerId, storyId, isPinned); setGlobal(global); - const result = await callApi('toggleStoryPinned', { storyId, isPinned }); + const result = await callApi('toggleStoryPinned', { peer, storyId, isPinned }); if (!result) { global = getGlobal(); - global = updateUserStory(global, global.currentUserId!, storyId, { isPinned: currentIsPinned }); - global = updateUserPinnedStory(global, global.currentUserId!, storyId, currentIsPinned); + global = updatePeerStory(global, peerId, storyId, { isPinned: currentIsPinned }); + global = updatePeerPinnedStory(global, peerId, storyId, currentIsPinned); setGlobal(global); } }); -addActionHandler('loadUserStories', async (global, actions, payload): Promise => { - const { userId } = payload; - const user = selectUser(global, userId); - if (!user) { - return; - } +addActionHandler('loadPeerStories', async (global, actions, payload): Promise => { + const { peerId } = payload; + const peer = selectPeer(global, peerId); + if (!peer) return; - const result = await callApi('fetchUserStories', { user }); + const result = await callApi('fetchPeerStories', { peer }); if (!result) { return; } global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, userId, result.stories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories); if (result.lastReadStoryId) { - global = updateLastReadStoryForUser(global, userId, result.lastReadStoryId); + global = updateLastReadStoryForPeer(global, peerId, result.lastReadStoryId); } setGlobal(global); }); -addActionHandler('loadUserPinnedStories', async (global, actions, payload): Promise => { - const { userId, offsetId } = payload; - const user = selectUser(global, userId); - if (!user) { +addActionHandler('loadPeerPinnedStories', async (global, actions, payload): Promise => { + const { peerId, offsetId } = payload; + const peer = selectPeer(global, peerId); + if (!peer) { return; } - const result = await callApi('fetchUserPinnedStories', { user, offsetId }); + const result = await callApi('fetchPeerPinnedStories', { peer, offsetId }); if (!result) { return; } global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, userId, result.stories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories); setGlobal(global); }); addActionHandler('loadStoriesArchive', async (global, actions, payload): Promise => { - const { offsetId } = payload; - const currentUserId = global.currentUserId!; + const { peerId, offsetId } = payload; + const peer = selectPeer(global, peerId); + if (!peer) return; - const result = await callApi('fetchStoriesArchive', { currentUserId, offsetId }); + const result = await callApi('fetchStoriesArchive', { peer, offsetId }); if (!result) { return; } global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, currentUserId, result.stories, true); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories, true); setGlobal(global); }); -addActionHandler('loadUserStoriesByIds', async (global, actions, payload): Promise => { - const { userId, storyIds } = payload; - const user = selectUser(global, userId); - if (!user) { +addActionHandler('loadPeerStoriesByIds', async (global, actions, payload): Promise => { + const { peerId, storyIds } = payload; + const peer = selectPeer(global, peerId); + if (!peer) { return; } - const result = await callApi('fetchUserStoriesByIds', { user, ids: storyIds }); + const result = await callApi('fetchPeerStoriesByIds', { peer, ids: storyIds }); if (!result) { return; } global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, userId, result.stories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories); setGlobal(global); }); addActionHandler('loadStoryViews', async (global, actions, payload): Promise => { const { + peerId, storyId, tabId = getCurrentTabId(), } = payload; @@ -307,12 +326,18 @@ addActionHandler('loadStoryViews', async (global, actions, payload): Promise view.userId); - global = updateUserStory(global, global.currentUserId!, storyId, { + global = updatePeerStory(global, peerId, storyId, { recentViewerIds, viewsCount: result.viewsCount, reactionsCount: result.reactionsCount, @@ -346,19 +371,19 @@ addActionHandler('loadStoryViews', async (global, actions, payload): Promise => { const { - userId, + peerId, storyId, reason, description, tabId = getCurrentTabId(), } = payload; - const user = selectUser(global, userId); - if (!user) { + const peer = selectPeer(global, peerId); + if (!peer) { return; } const result = await callApi('reportStory', { - user, + peer, storyId, reason, description, @@ -374,10 +399,16 @@ addActionHandler('reportStory', async (global, actions, payload): Promise addActionHandler('editStoryPrivacy', (global, actions, payload): ActionReturnType => { const { + peerId, storyId, privacy, } = payload; + const peer = selectPeer(global, peerId); + if (!peer) { + return; + } + const allowedIds = [...privacy.allowUserIds, ...privacy.allowChatIds]; const blockedIds = [...privacy.blockUserIds, ...privacy.blockChatIds]; @@ -389,67 +420,66 @@ addActionHandler('editStoryPrivacy', (global, actions, payload): ActionReturnTyp }); void callApi('editStoryPrivacy', { + peer, id: storyId, privacy: inputPrivacy, }); }); addActionHandler('toggleStoriesHidden', async (global, actions, payload): Promise => { - const { userId, isHidden } = payload; - const user = selectUser(global, userId); - if (!user) return; + const { peerId, isHidden } = payload; + const peer = selectPeer(global, peerId); + if (!peer) return; - const result = await callApi('toggleStoriesHidden', { user, isHidden }); + const result = await callApi('toggleStoriesHidden', { peer, isHidden }); if (!result) return; global = getGlobal(); - global = toggleUserStoriesHidden(global, userId, isHidden); + global = updatePeerStoriesHidden(global, peerId, isHidden); setGlobal(global); }); addActionHandler('loadStoriesMaxIds', async (global, actions, payload): Promise => { - const { userIds } = payload; - const users = userIds.map((userId) => selectUser(global, userId)).filter(Boolean); - if (!users.length) return; + const { peerIds } = payload; + const peers = peerIds.map((peerId) => selectPeer(global, peerId)).filter(Boolean); + if (!peers.length) return; - const result = await callApi('fetchStoriesMaxIds', { users }); + const result = await callApi('fetchStoriesMaxIds', { peers }); if (!result) return; - const userIdsToLoad: string[] = []; + const peerIdsToLoad: string[] = []; global = getGlobal(); result.forEach((maxId, i) => { - const user = users[i]; - global = updateUser(global, user.id, { + const peer = peers[i]; + global = updatePeer(global, peer.id, { maxStoryId: maxId, hasStories: maxId !== 0, }); + if (maxId !== 0) { - userIdsToLoad.push(user.id); + peerIdsToLoad.push(peer.id); } }); setGlobal(global); - userIdsToLoad?.forEach((userId) => actions.loadUserStories({ userId })); + peerIdsToLoad?.forEach((peerId) => actions.loadPeerStories({ peerId })); }); addActionHandler('sendStoryReaction', async (global, actions, payload): Promise => { const { - userId, storyId, reaction, shouldAddToRecent, tabId = getCurrentTabId(), + peerId, storyId, containerId, reaction, shouldAddToRecent, tabId = getCurrentTabId(), } = payload; - const user = selectUser(global, userId); - if (!user) return; + const peer = selectPeer(global, peerId); + if (!peer) return; - const story = selectUserStory(global, userId, storyId); + const story = selectPeerStory(global, peerId, storyId); if (!story || !('content' in story)) return; const previousReaction = story.sentReaction; - global = updateUserStory(global, userId, storyId, { - sentReaction: reaction, - }); + global = updateSentStoryReaction(global, peerId, storyId, reaction); setGlobal(global); - const containerId = getStoryKey(userId, storyId); if (reaction) { actions.startActiveReaction({ containerId, reaction, tabId }); } else { @@ -457,14 +487,12 @@ addActionHandler('sendStoryReaction', async (global, actions, payload): Promise< } const result = await callApi('sendStoryReaction', { - user, storyId, reaction, shouldAddToRecent, + peer, storyId, reaction, shouldAddToRecent, }); global = getGlobal(); if (!result) { - global = updateUserStory(global, userId, storyId, { - sentReaction: previousReaction, - }); + global = updateSentStoryReaction(global, peerId, storyId, previousReaction); } setGlobal(global); }); diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 242c214fe..23d45fb15 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -29,7 +29,7 @@ import { updateUserSearchFetchingStatus, } from '../../reducers'; import { - selectChat, selectCurrentMessageList, selectTabState, selectUser, selectUserFullInfo, + selectChat, selectCurrentMessageList, selectPeer, selectTabState, selectUser, selectUserFullInfo, } from '../../selectors'; const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min @@ -213,7 +213,7 @@ addActionHandler('updateContact', async (global, actions, payload): Promise { const { chatId } = payload!; - const userOrChat = isUserId(chatId) ? selectUser(global, chatId) : selectChat(global, chatId); - if (!userOrChat) { + const peer = selectPeer(global, chatId); + if (!peer) { return; } - void callApi('reportSpam', userOrChat); + void callApi('reportSpam', peer); }); addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => { diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index acdf31466..d7079e6eb 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -15,6 +15,7 @@ import { updateChatFullInfo, updateChatListIds, updateChatListType, + updatePeerStoriesHidden, updateTopic, } from '../../reducers'; import { updateUnreadReactions } from '../../reducers/reactions'; @@ -46,7 +47,14 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { }; } + const localChat = selectChat(global, update.id); + global = updateChat(global, update.id, update.chat, update.newProfilePhoto); + + if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) { + global = updatePeerStoriesHidden(global, update.id, update.chat.areStoriesHidden || false); + } + setGlobal(global); if (!update.noTopChatsRequest && !selectIsChatListed(global, update.id)) { diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 18f5cfaf1..fdeeff9ac 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -4,17 +4,17 @@ import { PaymentStep } from '../../../types'; import { addActionHandler, setGlobal } from '../../index'; import { addBlockedUser, - addStoriesForUser, + addStoriesForPeer, removeBlockedUser, - removeUserStory, + removePeerStory, setConfirmPaymentUrl, setPaymentStep, - updateLastReadStoryForUser, + updateLastReadStoryForPeer, + updatePeerStory, + updatePeersWithStories, updateStealthMode, - updateUserStory, - updateUsersWithStories, } from '../../reducers'; -import { selectUserStories, selectUserStory } from '../../selectors'; +import { selectPeerStories, selectPeerStory } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { switch (update['@type']) { @@ -119,26 +119,26 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { break; case 'updateStory': - global = addStoriesForUser(global, update.userId, { [update.story.id]: update.story }); - global = updateUsersWithStories(global, { [update.userId]: selectUserStories(global, update.userId)! }); + global = addStoriesForPeer(global, update.peerId, { [update.story.id]: update.story }); + global = updatePeersWithStories(global, { [update.peerId]: selectPeerStories(global, update.peerId)! }); setGlobal(global); break; case 'deleteStory': - global = removeUserStory(global, update.userId, update.storyId); + global = removePeerStory(global, update.peerId, update.storyId); setGlobal(global); break; case 'updateReadStories': - global = updateLastReadStoryForUser(global, update.userId, update.lastReadId); + global = updateLastReadStoryForPeer(global, update.peerId, update.lastReadId); setGlobal(global); break; case 'updateSentStoryReaction': { - const { userId, storyId, reaction } = update; - const story = selectUserStory(global, userId, storyId); + const { peerId, storyId, reaction } = update; + const story = selectPeerStory(global, peerId, storyId); if (!story) return global; - global = updateUserStory(global, userId, storyId, { sentReaction: reaction }); + global = updatePeerStory(global, peerId, storyId, { sentReaction: reaction }); setGlobal(global); break; } diff --git a/src/global/actions/apiUpdaters/users.ts b/src/global/actions/apiUpdaters/users.ts index dbb576ca2..3ed40083f 100644 --- a/src/global/actions/apiUpdaters/users.ts +++ b/src/global/actions/apiUpdaters/users.ts @@ -4,7 +4,7 @@ import type { ActionReturnType, RequiredGlobalState } from '../../types'; import { throttle } from '../../../util/schedulers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { - deleteContact, replaceUserStatuses, toggleUserStoriesHidden, updateUser, updateUserFullInfo, + deleteContact, replaceUserStatuses, updatePeerStoriesHidden, updateUser, updateUserFullInfo, } from '../../reducers'; import { selectIsCurrentUserPremium, selectUser, selectUserFullInfo } from '../../selectors'; @@ -55,15 +55,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { } }); - const currentUser = selectUser(global, update.id); + const localUser = selectUser(global, update.id); global = updateUser(global, update.id, update.user); if (update.fullInfo) { global = updateUserFullInfo(global, update.id, update.fullInfo); } - if (currentUser?.areStoriesHidden !== update.user.areStoriesHidden) { - global = toggleUserStoriesHidden(global, update.id, update.user.areStoriesHidden || false); + if (localUser?.areStoriesHidden !== update.user.areStoriesHidden) { + global = updatePeerStoriesHidden(global, update.id, update.user.areStoriesHidden || false); } return global; diff --git a/src/global/actions/ui/reactions.ts b/src/global/actions/ui/reactions.ts index fbbdd081f..70fb5c44f 100644 --- a/src/global/actions/ui/reactions.ts +++ b/src/global/actions/ui/reactions.ts @@ -45,7 +45,7 @@ addActionHandler('openMessageReactionPicker', (global, actions, payload): Action addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionReturnType => { const { - storyUserId, + peerId, storyId, position, sendAsMessage, @@ -54,7 +54,7 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe return updateTabState(global, { reactionPicker: { - storyUserId, + storyPeerId: peerId, storyId, sendAsMessage, position, @@ -72,7 +72,7 @@ addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturn messageId: undefined, position: undefined, storyId: undefined, - storyUserId: undefined, + storyPeerId: undefined, }, }, tabId); }); diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index ef32866f4..fe2f06cbc 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -7,48 +7,49 @@ import { buildCollectionByKey, omit } from '../../../util/iteratees'; import * as langProvider from '../../../util/langProvider'; import { callApi } from '../../../api/gramjs'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; -import { addStoriesForUser, addUsers } from '../../reducers'; +import { addChats, addStoriesForPeer, addUsers } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { selectCurrentViewedStory, + selectPeer, + selectPeerFirstStoryId, + selectPeerFirstUnreadStoryId, + selectPeerStories, selectTabState, - selectUser, - selectUserFirstStoryId, - selectUserFirstUnreadStoryId, - selectUserStories, } from '../../selectors'; import { fetchChatByUsername } from '../api/chats'; addActionHandler('openStoryViewer', async (global, actions, payload): Promise => { const { - userId, storyId, isSingleUser, isSingleStory, isPrivate, isArchive, origin, tabId = getCurrentTabId(), + peerId, storyId, isSinglePeer, isSingleStory, isPrivate, isArchive, origin, tabId = getCurrentTabId(), } = payload; - const user = selectUser(global, userId); - if (!user) { + const peer = selectPeer(global, peerId); + if (!peer) { return; } const tabState = selectTabState(global, tabId); - const userStories = selectUserStories(global, userId); + const peerStories = selectPeerStories(global, peerId); - if (storyId && (!userStories || !userStories.byId[storyId])) { - const result = await callApi('fetchUserStoriesByIds', { user, ids: [storyId] }); + if (storyId && (!peerStories || !peerStories.byId[storyId])) { + const result = await callApi('fetchPeerStoriesByIds', { peer, ids: [storyId] }); if (!result) { return; } global = getGlobal(); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = addStoriesForUser(global, userId, result.stories); + global = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addStoriesForPeer(global, peerId, result.stories); } global = updateTabState(global, { storyViewer: { ...tabState.storyViewer, - userId, - storyId: storyId || selectUserFirstUnreadStoryId(global, userId) || selectUserFirstStoryId(global, userId), - isSingleUser, + peerId, + storyId: storyId || selectPeerFirstUnreadStoryId(global, peerId) || selectPeerFirstStoryId(global, peerId), + isSinglePeer, isPrivate, isArchive, isSingleStory, @@ -71,9 +72,9 @@ addActionHandler('openStoryViewerByUsername', async (global, actions, payload): } actions.openStoryViewer({ - userId: chat.id, + peerId: chat.id, storyId, - isSingleUser: true, + isSinglePeer: true, isSingleStory: true, origin, tabId, @@ -93,7 +94,7 @@ addActionHandler('closeStoryViewer', (global, actions, payload): ActionReturnTyp isMuted, isRibbonShown, isArchivedRibbonShown, - lastViewedByUserIds: undefined, + lastViewedByPeerIds: undefined, }, }, tabId); @@ -117,7 +118,7 @@ addActionHandler('setStoryViewerMuted', (global, actions, payload): ActionReturn addActionHandler('toggleStoryRibbon', (global, actions, payload): ActionReturnType => { const { isShown, isArchived, tabId = getCurrentTabId() } = payload; - const orderedIds = global.stories.orderedUserIds[isArchived ? 'archived' : 'active']; + const orderedIds = global.stories.orderedPeerIds[isArchived ? 'archived' : 'active']; if (!orderedIds?.length) { return global; } @@ -134,7 +135,7 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy const { tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); const { - userId, storyId, isSingleUser, isSingleStory, isPrivate, isArchive, + peerId, storyId, isSinglePeer, isSingleStory, isPrivate, isArchive, } = tabState.storyViewer; if (isSingleStory) { @@ -142,38 +143,38 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy return undefined; } - const { orderedUserIds: { active, archived } } = global.stories; - if (!userId || !storyId) { + const { orderedPeerIds: { active, archived } } = global.stories; + if (!peerId || !storyId) { return undefined; } - const user = selectUser(global, userId); - const userStories = selectUserStories(global, userId); - if (!userStories || !user) { + const peer = selectPeer(global, peerId); + const peerStories = selectPeerStories(global, peerId); + if (!peerStories || !peer) { return undefined; } - const orderedUserIds = (user.areStoriesHidden ? archived : active) ?? []; + const orderedPeerIds = (peer.areStoriesHidden ? archived : active) ?? []; const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds'; - const userStoryIds = userStories[storySourceProp] ?? []; - const currentStoryIndex = userStoryIds.indexOf(storyId); + const peerStoryIds = peerStories[storySourceProp] ?? []; + const currentStoryIndex = peerStoryIds.indexOf(storyId); let previousStoryIndex: number; - let previousUserId: string; + let previousPeerId: string; if (currentStoryIndex > 0) { previousStoryIndex = currentStoryIndex - 1; - previousUserId = userId; + previousPeerId = peerId; } else { - const previousUserIdIndex = orderedUserIds.indexOf(userId) - 1; - if (isSingleUser || previousUserIdIndex < 0) { + const previousPeerIdIndex = orderedPeerIds.indexOf(peerId) - 1; + if (isSinglePeer || previousPeerIdIndex < 0) { return undefined; } - previousUserId = orderedUserIds[previousUserIdIndex]; - previousStoryIndex = (selectUserStories(global, previousUserId)?.orderedIds.length || 1) - 1; + previousPeerId = orderedPeerIds[previousPeerIdIndex]; + previousStoryIndex = (selectPeerStories(global, previousPeerId)?.orderedIds.length || 1) - 1; } - const previousStoryId = selectUserStories(global, previousUserId)?.[storySourceProp]?.[previousStoryIndex]; + const previousStoryId = selectPeerStories(global, previousPeerId)?.[storySourceProp]?.[previousStoryIndex]; if (!previousStoryId) { return undefined; } @@ -181,7 +182,7 @@ addActionHandler('openPreviousStory', (global, actions, payload): ActionReturnTy return updateTabState(global, { storyViewer: { ...tabState.storyViewer, - userId: previousUserId, + peerId: previousPeerId, storyId: previousStoryId, }, }, tabId); @@ -191,46 +192,46 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType = const { tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); const { - userId, storyId, isSingleUser, isSingleStory, isPrivate, isArchive, + peerId, storyId, isSinglePeer, isSingleStory, isPrivate, isArchive, } = tabState.storyViewer; if (isSingleStory) { actions.closeStoryViewer({ tabId }); return undefined; } - const { orderedUserIds: { active, archived } } = global.stories; - if (!userId || !storyId) { + const { orderedPeerIds: { active, archived } } = global.stories; + if (!peerId || !storyId) { return undefined; } - const user = selectUser(global, userId); - const userStories = selectUserStories(global, userId); - if (!userStories || !user) { + const peer = selectPeer(global, peerId); + const peerStories = selectPeerStories(global, peerId); + if (!peerStories || !peer) { return undefined; } - const orderedUserIds = (user.areStoriesHidden ? archived : active) ?? []; + const orderedPeerIds = (peer.areStoriesHidden ? archived : active) ?? []; const storySourceProp = isArchive ? 'archiveIds' : isPrivate ? 'pinnedIds' : 'orderedIds'; - const userStoryIds = userStories[storySourceProp] ?? []; - const currentStoryIndex = userStoryIds.indexOf(storyId); + const peerStoryIds = peerStories[storySourceProp] ?? []; + const currentStoryIndex = peerStoryIds.indexOf(storyId); let nextStoryIndex: number; - let nextUserId: string; + let nextPeerId: string; - if (currentStoryIndex < userStoryIds.length - 1) { + if (currentStoryIndex < peerStoryIds.length - 1) { nextStoryIndex = currentStoryIndex + 1; - nextUserId = userId; + nextPeerId = peerId; } else { - const nextUserIdIndex = orderedUserIds.indexOf(userId) + 1; - if (isSingleUser || nextUserIdIndex > orderedUserIds.length - 1) { + const nextPeerIdIndex = orderedPeerIds.indexOf(peerId) + 1; + if (isSinglePeer || nextPeerIdIndex > orderedPeerIds.length - 1) { actions.closeStoryViewer({ tabId }); return undefined; } - nextUserId = orderedUserIds[nextUserIdIndex]; + nextPeerId = orderedPeerIds[nextPeerIdIndex]; nextStoryIndex = 0; } - const nextStoryId = selectUserStories(global, nextUserId)?.[storySourceProp]?.[nextStoryIndex]; + const nextStoryId = selectPeerStories(global, nextPeerId)?.[storySourceProp]?.[nextStoryIndex]; if (!nextStoryId) { return undefined; } @@ -238,7 +239,7 @@ addActionHandler('openNextStory', (global, actions, payload): ActionReturnType = return updateTabState(global, { storyViewer: { ...tabState.storyViewer, - userId: nextUserId, + peerId: nextPeerId, storyId: nextStoryId, }, }, tabId); @@ -270,9 +271,14 @@ addActionHandler('closeStoryViewModal', (global, actions, payload): ActionReturn }); addActionHandler('copyStoryLink', async (global, actions, payload): Promise => { - const { userId, storyId, tabId = getCurrentTabId() } = payload; + const { peerId, storyId, tabId = getCurrentTabId() } = payload; - const link = await callApi('fetchStoryLink', { userId, storyId }); + const peer = selectPeer(global, peerId); + if (!peer) { + return; + } + + const link = await callApi('fetchStoryLink', { peer, storyId }); if (!link) { return; } @@ -286,8 +292,8 @@ addActionHandler('copyStoryLink', async (global, actions, payload): Promise { const { tabId = getCurrentTabId() } = payload; - const { storyId, userId: storyUserId } = selectCurrentViewedStory(global, tabId); - const isStoryReply = Boolean(storyId && storyUserId); + const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId); + const isStoryReply = Boolean(storyId && storyPeerId); if (!isStoryReply) { return; @@ -314,7 +320,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => payload: undefined, }, { action: 'openChat', - payload: { id: storyUserId }, + payload: { id: storyPeerId }, }], tabId, }); diff --git a/src/global/cache.ts b/src/global/cache.ts index fd300d948..97180a174 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -198,6 +198,11 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.stories.stealthMode) { cached.stories.stealthMode = initialState.stories.stealthMode; } + + if (!cached.stories.byPeerId) { + cached.stories.byPeerId = initialState.stories.byPeerId; + cached.stories.orderedPeerIds = initialState.stories.orderedPeerIds; + } } function updateCache() { @@ -302,8 +307,8 @@ function reduceUsers(global: T): GlobalState['users'] { const chatStoriesUserIds = currentChatIds .flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {})) - .map((message) => message.content.storyData?.userId || message.content.webPage?.story?.userId) - .filter(Boolean); + .map((message) => message.content.storyData?.peerId || message.content.webPage?.story?.peerId) + .filter((id): id is string => Boolean(id) && isUserId(id)); const idsToSave = unique([ ...currentUserId ? [currentUserId] : [], @@ -343,9 +348,15 @@ function reduceChats(global: T): GlobalState['chats'] { }), ).map(({ chatId }) => chatId); + const chatStoriesChannelIds = currentChatIds + .flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {})) + .map((message) => message.content.storyData?.peerId || message.content.webPage?.story?.peerId) + .filter((id): id is string => Boolean(id) && !isUserId(id)); + const idsToSave = unique([ ...currentUserId ? [currentUserId] : [], ...currentChatIds, + ...chatStoriesChannelIds, ...getOrderedIds(ALL_FOLDER_ID) || [], ...getOrderedIds(ARCHIVED_FOLDER_ID) || [], ...global.recentlyFoundChatIds || [], diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 8ef0887a8..e4676c132 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -3,6 +3,7 @@ import type { ApiChatAdminRights, ApiChatBannedRights, ApiChatFolder, + ApiPeer, ApiTopic, ApiUser, } from '../../api/types'; @@ -93,7 +94,7 @@ export function getChatLink(chat: ApiChat) { } export function getChatAvatarHash( - owner: ApiChat | ApiUser, + owner: ApiPeer, size: 'normal' | 'big' = 'normal', avatarHash = owner.avatarHash, ) { @@ -350,7 +351,7 @@ export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, ch } } -export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiUser | ApiChat) { +export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiPeer) { if (!sender || isUserId(chatId)) { return undefined; } @@ -463,7 +464,7 @@ export function getPeerIdDividend(peerId: string) { } // https://github.com/telegramdesktop/tdesktop/blob/371510cfe23b0bd226de8c076bc49248fbe40c26/Telegram/SourceFiles/data/data_peer.cpp#L53 -export function getPeerColorKey(peer: ApiUser | ApiChat | undefined) { +export function getPeerColorKey(peer: ApiPeer | undefined) { const index = peer ? getPeerIdDividend(peer.id) % 7 : 0; return USER_COLOR_KEYS[index]; diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index 9b84f6ee8..381a8b66a 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -1,5 +1,5 @@ import type { - ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiStory, ApiUser, + ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiPeer, ApiStory, ApiUser, } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; import { ApiMessageEntityTypes } from '../../api/types'; @@ -184,7 +184,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) { return Boolean(message.senderId) && !isUserId(message.senderId!) && isOwnMessage(message); } -export function getSenderTitle(lang: LangFn, sender: ApiUser | ApiChat) { +export function getSenderTitle(lang: LangFn, sender: ApiPeer) { return isUserId(sender.id) ? getUserFullName(sender as ApiUser) : getChatTitle(lang, sender as ApiChat); } diff --git a/src/global/helpers/reactions.ts b/src/global/helpers/reactions.ts index 16093ab79..173d0251f 100644 --- a/src/global/helpers/reactions.ts +++ b/src/global/helpers/reactions.ts @@ -82,3 +82,32 @@ export function getReactionUniqueKey(reaction: ApiReaction) { export function isReactionChosen(reaction: ApiReactionCount) { return reaction.chosenOrder !== undefined; } + +export function updateReactionCount(reactionCount: ApiReactionCount[], newReactions: ApiReaction[]) { + const results = reactionCount.map((current) => ( + isReactionChosen(current) ? { + ...current, + chosenOrder: undefined, + count: current.count - 1, + } : current + )).filter(({ count }) => count > 0); + + newReactions.forEach((reaction, i) => { + const existingIndex = results.findIndex((r) => isSameReaction(r.reaction, reaction)); + if (existingIndex > -1) { + results[existingIndex] = { + ...results[existingIndex], + chosenOrder: i, + count: results[existingIndex].count + 1, + }; + } else { + results.push({ + reaction, + chosenOrder: i, + count: 1, + }); + } + }); + + return results; +} diff --git a/src/global/helpers/users.ts b/src/global/helpers/users.ts index d847c4d3f..dd4e5cf84 100644 --- a/src/global/helpers/users.ts +++ b/src/global/helpers/users.ts @@ -1,4 +1,4 @@ -import type { ApiChat, ApiUser, ApiUserStatus } from '../../api/types'; +import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; @@ -259,10 +259,10 @@ export function filterUsersByName( }); } -export function getMainUsername(userOrChat: ApiUser | ApiChat) { +export function getMainUsername(userOrChat: ApiPeer) { return userOrChat.usernames?.find((u) => u.isActive)?.username; } -export function getUserStoryHtmlId(userId: string) { - return `user-story${userId}`; +export function getPeerStoryHtmlId(userId: string) { + return `peer-story${userId}`; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index c2593e8e3..bd606a3e1 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -113,8 +113,8 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { }, stories: { - byUserId: {}, - orderedUserIds: { + byPeerId: {}, + orderedPeerIds: { archived: [], active: [], }, diff --git a/src/global/intervals.ts b/src/global/intervals.ts index 04ae215c1..fbaa85379 100644 --- a/src/global/intervals.ts +++ b/src/global/intervals.ts @@ -3,7 +3,7 @@ import { addCallback } from '../lib/teact/teactn'; import type { GlobalState } from './types'; import { getServerTime } from '../util/serverTime'; -import { removeUserStory } from './reducers'; +import { removePeerStory } from './reducers'; import { selectTabState } from './selectors'; import { getGlobal, setGlobal } from '.'; @@ -43,15 +43,15 @@ function checkStoryExpiration() { let global = getGlobal(); const serverTime = getServerTime(); - Object.values(global.stories.byUserId).forEach((userStories) => { - const stories = Object.values(userStories.byId); + Object.values(global.stories.byPeerId).forEach((peerStories) => { + const stories = Object.values(peerStories.byId); stories.forEach((story) => { if (!('expireDate' in story)) return; if (story.expireDate > serverTime) return; if ('isPinned' in story && story.isPinned) return; if ('isPublic' in story && !story.isPublic) return; - global = removeUserStory(global, story.userId, story.id); + global = removePeerStory(global, story.peerId, story.id); }); }); diff --git a/src/global/reducers/general.ts b/src/global/reducers/general.ts new file mode 100644 index 000000000..fde900ab5 --- /dev/null +++ b/src/global/reducers/general.ts @@ -0,0 +1,32 @@ +import type { + ApiChat, ApiChatFullInfo, ApiUser, ApiUserFullInfo, +} from '../../api/types'; +import type { GlobalState } from '../types'; + +import { isUserId } from '../helpers'; +import { updateChat, updateChatFullInfo } from './chats'; +import { updateUser, updateUserFullInfo } from './users'; + +// `type` has different types in ApiChat and ApiUser +type ApiPeerSharedFields = Omit, 'type'>; +type ApiPeerFullInfoSharedFields = CommonProperties; + +export function updatePeer( + global: T, peerId: string, peerUpdate: Partial, +) { + if (isUserId(peerId)) { + return updateUser(global, peerId, peerUpdate); + } + + return updateChat(global, peerId, peerUpdate); +} + +export function updatePeerFullInfo( + global: T, peerId: string, peerFullInfoUpdate: Partial, +) { + if (isUserId(peerId)) { + return updateUserFullInfo(global, peerId, peerFullInfoUpdate); + } + + return updateChatFullInfo(global, peerId, peerFullInfoUpdate); +} diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index ece9e18cd..14813ff81 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -12,3 +12,4 @@ export * from './payments'; export * from './statistics'; export * from './stories'; export * from './translations'; +export * from './general'; diff --git a/src/global/reducers/payments.ts b/src/global/reducers/payments.ts index 14bd1a95d..45dfdb473 100644 --- a/src/global/reducers/payments.ts +++ b/src/global/reducers/payments.ts @@ -55,7 +55,7 @@ export function setInvoiceInfo( isTest, photo, isRecurring, - recurringTermsUrl, + termsUrl, maxTipAmount, suggestedTipAmounts, } = invoice; @@ -69,7 +69,7 @@ export function setInvoiceInfo( currency, isTest, isRecurring, - recurringTermsUrl, + termsUrl, maxTipAmount, suggestedTipAmounts, }, diff --git a/src/global/reducers/reactions.ts b/src/global/reducers/reactions.ts index 45da98f44..1f8f002ef 100644 --- a/src/global/reducers/reactions.ts +++ b/src/global/reducers/reactions.ts @@ -7,7 +7,7 @@ import { MIN_LEFT_COLUMN_WIDTH, SIDE_COLUMN_MAX_WIDTH, } from '../../components/middle/helpers/calculateMiddleFooterTransforms'; -import { isReactionChosen, isSameReaction } from '../helpers'; +import { updateReactionCount } from '../helpers'; import { selectSendAs, selectTabState } from '../selectors'; import { updateChat } from './chats'; import { updateChatMessage } from './messages'; @@ -46,30 +46,7 @@ export function addMessageReaction( const currentSendAs = selectSendAs(global, message.chatId); // Update UI without waiting for server response - const results = currentReactions.results.map((current) => ( - isReactionChosen(current) ? { - ...current, - chosenOrder: undefined, - count: current.count - 1, - } : current - )).filter(({ count }) => count > 0); - - userReactions.forEach((reaction, i) => { - const existingIndex = results.findIndex((r) => isSameReaction(r.reaction, reaction)); - if (existingIndex > -1) { - results[existingIndex] = { - ...results[existingIndex], - chosenOrder: i, - count: results[existingIndex].count + 1, - }; - } else { - results.push({ - reaction, - chosenOrder: i, - count: 1, - }); - } - }); + const results = updateReactionCount(currentReactions.results, userReactions); let { recentReactions = [] } = currentReactions; diff --git a/src/global/reducers/stories.ts b/src/global/reducers/stories.ts index f8a42ce77..b007b0dac 100644 --- a/src/global/reducers/stories.ts +++ b/src/global/reducers/stories.ts @@ -1,51 +1,60 @@ import type { + ApiPeerStories, + ApiReaction, ApiStealthMode, - ApiStory, ApiStoryDeleted, ApiStorySkipped, ApiStoryView, ApiTypeStory, ApiUserStories, + ApiStory, + ApiStoryDeleted, + ApiStorySkipped, + ApiStoryView, + ApiTypeStory, } from '../../api/types'; import type { GlobalState, TabArgs } from '../types'; import { getCurrentTabId } from '../../util/establishMultitabRole'; -import { orderBy, unique } from '../../util/iteratees'; +import { compareFields, unique } from '../../util/iteratees'; import { getServerTime } from '../../util/serverTime'; -import { selectTabState, selectUser, selectUserStories } from '../selectors'; +import { isUserId, updateReactionCount } from '../helpers'; +import { + selectPeer, selectPeerStories, selectPeerStory, selectTabState, selectUser, +} from '../selectors'; +import { updatePeer } from './general'; import { updateTabState } from './tabs'; -import { updateUser } from './users'; -export function addStories(global: T, newStoriesByUserId: Record): T { - const updatedByUserId = Object.entries(newStoriesByUserId).reduce((acc, [userId, newUserStories]) => { - if (!acc[userId]) { - acc[userId] = newUserStories; +export function addStories(global: T, newStoriesByPeerId: Record): T { + const updatedByPeerId = Object.entries(newStoriesByPeerId).reduce((acc, [peerId, newPeerStories]) => { + if (!acc[peerId]) { + acc[peerId] = newPeerStories; } else { - acc[userId].byId = { ...acc[userId].byId, ...newUserStories.byId }; - acc[userId].orderedIds = unique(newUserStories.orderedIds.concat(acc[userId].orderedIds)); - acc[userId].pinnedIds = unique(newUserStories.pinnedIds.concat(acc[userId].pinnedIds)).sort((a, b) => b - a); - acc[userId].lastUpdatedAt = newUserStories.lastUpdatedAt; - acc[userId].lastReadId = newUserStories.lastReadId; + acc[peerId].byId = { ...acc[peerId].byId, ...newPeerStories.byId }; + acc[peerId].orderedIds = unique(newPeerStories.orderedIds.concat(acc[peerId].orderedIds)); + acc[peerId].pinnedIds = unique(newPeerStories.pinnedIds.concat(acc[peerId].pinnedIds)).sort((a, b) => b - a); + acc[peerId].lastUpdatedAt = newPeerStories.lastUpdatedAt; + acc[peerId].lastReadId = newPeerStories.lastReadId; } return acc; - }, global.stories.byUserId); + }, global.stories.byPeerId); global = { ...global, stories: { ...global.stories, - byUserId: updatedByUserId, + byPeerId: updatedByPeerId, }, }; - return updateOrderedStoriesUserIds(global, Object.keys(newStoriesByUserId)); + return updateOrderedStoriesPeerIds(global, Object.keys(newStoriesByPeerId)); } -export function addStoriesForUser( +export function addStoriesForPeer( global: T, - userId: string, + peerId: string, newStories: Record, addToArchive?: boolean, ): T { const { byId, orderedIds, pinnedIds, archiveIds, - } = global.stories.byUserId[userId] || {}; + } = global.stories.byPeerId[peerId] || {}; const deletedIds = Object.keys(newStories).filter((id) => 'isDeleted' in newStories[Number(id)]).map(Number); const updatedById = { ...byId, ...newStories }; let updatedOrderedIds = [...(orderedIds || [])]; @@ -68,7 +77,7 @@ export function addStoriesForUser( return acc; }, updatedOrderedIds)).filter((storyId) => !deletedIds.includes(storyId)); - if (addToArchive && userId === global.currentUserId) { + if (addToArchive && peerId === global.currentUserId) { updatedArchiveIds = unique(updatedArchiveIds.concat(Object.keys(newStories).map(Number))) .sort((a, b) => b - a) .filter((storyId) => !deletedIds.includes(storyId)); @@ -78,10 +87,10 @@ export function addStoriesForUser( ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: { - ...global.stories.byUserId[userId], + byPeerId: { + ...global.stories.byPeerId, + [peerId]: { + ...global.stories.byPeerId[peerId], byId: updatedById, orderedIds: updatedOrderedIds, pinnedIds: updatedPinnedIds, @@ -91,45 +100,45 @@ export function addStoriesForUser( }, }; - if (userId === global.currentUserId - || selectUser(global, userId)?.isContact - || userId === global.appConfig?.storyChangelogUserId) { - global = updateUserLastUpdatedAt(global, userId); - global = updateOrderedStoriesUserIds(global, [userId]); + if (peerId === global.currentUserId + || selectUser(global, peerId)?.isContact + || peerId === global.appConfig?.storyChangelogUserId) { + global = updatePeerLastUpdatedAt(global, peerId); + global = updateOrderedStoriesPeerIds(global, [peerId]); } return global; } -export function updateStoriesForUser( +export function updateStoriesForPeer( global: T, - userId: string, - userStories: ApiUserStories, + peerId: string, + peerStories: ApiPeerStories, ): T { return { ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: userStories, + byPeerId: { + ...global.stories.byPeerId, + [peerId]: peerStories, }, }, }; } -export function updateLastReadStoryForUser( +export function updateLastReadStoryForPeer( global: T, - userId: string, + peerId: string, lastReadId: number, ): T { - const { orderedIds } = selectUserStories(global, userId) || {}; + const { orderedIds } = selectPeerStories(global, peerId) || {}; if (!orderedIds) { return global; } if (lastReadId >= orderedIds[orderedIds.length - 1]) { - global = updateUser(global, userId, { + global = updatePeer(global, peerId, { hasUnreadStories: false, }); } @@ -138,10 +147,10 @@ export function updateLastReadStoryForUser( ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: { - ...global.stories.byUserId[userId], + byPeerId: { + ...global.stories.byPeerId, + [peerId]: { + ...global.stories.byPeerId[peerId], lastReadId, }, }, @@ -149,13 +158,13 @@ export function updateLastReadStoryForUser( }; } -export function updateLastViewedStoryForUser( +export function updateLastViewedStoryForPeer( global: T, - userId: string, + peerId: string, lastViewedId: number, ...[tabId = getCurrentTabId()]: TabArgs ): T { - const { orderedIds } = selectUserStories(global, userId) || {}; + const { orderedIds } = selectPeerStories(global, peerId) || {}; if (!orderedIds || !orderedIds.includes(lastViewedId)) { return global; } @@ -165,28 +174,27 @@ export function updateLastViewedStoryForUser( return updateTabState(global, { storyViewer: { ...storyViewer, - lastViewedByUserIds: { - ...storyViewer.lastViewedByUserIds, - [userId]: lastViewedId, + lastViewedByPeerIds: { + ...storyViewer.lastViewedByPeerIds, + [peerId]: lastViewedId, }, }, }, tabId); } -export function updateUsersWithStories( +export function updatePeersWithStories( global: T, - storiesByUserId: Record, + storiesByPeerId: Record, ): T { - Object.entries(storiesByUserId).forEach(([userId, { lastReadId, orderedIds }]) => { - const user = global.users.byId[userId]; + Object.entries(storiesByPeerId).forEach(([peerId, { lastReadId, orderedIds }]) => { + const peer = selectPeer(global, peerId); + if (!peer) return; - if (user) { - global = updateUser(global, userId, { - hasStories: true, - hasUnreadStories: !lastReadId - || Boolean(lastReadId && lastReadId < (user.maxStoryId || orderedIds[orderedIds.length - 1])), - }); - } + global = updatePeer(global, peerId, { + hasStories: true, + hasUnreadStories: !lastReadId + || Boolean(lastReadId && lastReadId < (peer.maxStoryId || orderedIds[orderedIds.length - 1])), + }); }); return global; @@ -242,14 +250,14 @@ export function updateStoryViewsLoading( }, tabId); } -export function removeUserStory( +export function removePeerStory( global: T, - userId: string, + peerId: string, storyId: number, ): T { const { orderedIds, pinnedIds, lastReadId, byId, - } = selectUserStories(global, userId) || { orderedIds: [] as number[], pinnedIds: [] as number[] }; + } = selectPeerStories(global, peerId) || { orderedIds: [] as number[], pinnedIds: [] as number[] }; const newOrderedIds = orderedIds.filter((id) => id !== storyId); const newPinnedIds = pinnedIds.filter((id) => id !== storyId); @@ -260,16 +268,17 @@ export function removeUserStory( const newById = { ...byId, - [storyId]: { id: storyId, userId, isDeleted: true } as ApiStoryDeleted, + [storyId]: { id: storyId, peerId, isDeleted: true } as ApiStoryDeleted, }; const lastUpdatedAt = lastStoryId ? (newById[lastStoryId] as ApiStory | undefined)?.date : undefined; const hasStories = Boolean(newOrderedIds.length); - global = updateUser(global, userId, { + global = updatePeer(global, peerId, { hasStories, hasUnreadStories: Boolean(hasStories && lastReadId && lastStoryId && lastReadId < lastStoryId), }); - global = updateStoriesForUser(global, userId, { + + global = updateStoriesForPeer(global, peerId, { byId: newById, orderedIds: newOrderedIds, pinnedIds: newPinnedIds, @@ -278,8 +287,8 @@ export function removeUserStory( }); Object.values(global.byTabId).forEach((tab) => { - if (tab.storyViewer.lastViewedByUserIds?.[userId] === storyId) { - global = updateLastViewedStoryForUser(global, userId, previousStoryId, tab.id); + if (tab.storyViewer.lastViewedByPeerIds?.[peerId] === storyId) { + global = updateLastViewedStoryForPeer(global, peerId, previousStoryId, tab.id); } }); @@ -288,9 +297,9 @@ export function removeUserStory( ...global, stories: { ...global.stories, - orderedUserIds: { - active: global.stories.orderedUserIds.active.filter((id) => id !== userId), - archived: global.stories.orderedUserIds.archived.filter((id) => id !== userId), + orderedPeerIds: { + active: global.stories.orderedPeerIds.active.filter((id) => id !== peerId), + archived: global.stories.orderedPeerIds.archived.filter((id) => id !== peerId), }, }, }; @@ -299,13 +308,38 @@ export function removeUserStory( return global; } -export function updateUserStory( +export function updateSentStoryReaction( global: T, - userId: string, + peerId: string, + storyId: number, + reaction: ApiReaction | undefined, +): T { + const story = selectPeerStory(global, peerId, storyId); + if (!story || !('content' in story)) return global; + + const reactionsCount = story.reactionsCount || 0; + const hasReaction = story.reactions?.some((r) => r.chosenOrder); + const reactions = updateReactionCount(story.reactions || [], [reaction].filter(Boolean)); + + const countDiff = !reaction ? -1 : hasReaction ? 0 : 1; + const newReactionsCount = reactionsCount + countDiff; + + global = updatePeerStory(global, peerId, storyId, { + sentReaction: reaction, + reactionsCount: newReactionsCount, + reactions, + }); + + return global; +} + +export function updatePeerStory( + global: T, + peerId: string, storyId: number, storyUpdate: Partial, ): T { - const userStories = selectUserStories(global, userId) || { + const peerStories = selectPeerStories(global, peerId) || { byId: {}, orderedIds: [], pinnedIds: [], archiveIds: [], }; @@ -313,14 +347,14 @@ export function updateUserStory( ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: { - ...userStories, + byPeerId: { + ...global.stories.byPeerId, + [peerId]: { + ...peerStories, byId: { - ...userStories.byId, + ...peerStories.byId, [storyId]: { - ...userStories.byId[storyId], + ...peerStories.byId[storyId], ...storyUpdate, }, }, @@ -330,28 +364,28 @@ export function updateUserStory( }; } -export function updateUserPinnedStory( +export function updatePeerPinnedStory( global: T, - userId: string, + peerId: string, storyId: number, isPinned?: boolean, ): T { - const userStories = selectUserStories(global, userId) || { + const peerStories = selectPeerStories(global, peerId) || { byId: {}, orderedIds: [], pinnedIds: [], archiveIds: [], }; const newPinnedIds = isPinned - ? unique(userStories.pinnedIds.concat(storyId)).sort((a, b) => b - a) - : userStories.pinnedIds.filter((id) => storyId !== id); + ? unique(peerStories.pinnedIds.concat(storyId)).sort((a, b) => b - a) + : peerStories.pinnedIds.filter((id) => storyId !== id); return { ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: { - ...userStories, + byPeerId: { + ...global.stories.byPeerId, + [peerId]: { + ...peerStories, pinnedIds: newPinnedIds, }, }, @@ -359,68 +393,81 @@ export function updateUserPinnedStory( }; } -export function toggleUserStoriesHidden(global: T, userId: string, isHidden: boolean) { - global = updateUser(global, userId, { - areStoriesHidden: isHidden ? true : undefined, +export function updatePeerStoriesHidden(global: T, peerId: string, areHidden: boolean) { + const peer = selectPeer(global, peerId); + if (!peer) return global; + + const currentState = peer.areStoriesHidden; + if (currentState === areHidden) return global; // `updateOrderedStoriesPeerIds` is computationally expensive + + global = updatePeer(global, peerId, { + areStoriesHidden: areHidden, }); - return updateOrderedStoriesUserIds(global, [userId]); + return updateOrderedStoriesPeerIds(global, [peerId]); } -function updateOrderedStoriesUserIds(global: T, updateUserIds: string[]): T { - const { currentUserId, stories: { byUserId, orderedUserIds } } = global; +function updateOrderedStoriesPeerIds(global: T, updatePeerIds: string[]): T { + const { currentUserId, stories: { byPeerId, orderedPeerIds } } = global; - const allUserIds = orderedUserIds.active.concat(orderedUserIds.archived).concat(updateUserIds); - const newOrderedUserIds = allUserIds.reduce<{ active: string[]; archived: string[] }>((acc, userId) => { - if (!byUserId[userId]?.orderedIds?.length) return acc; + const allPeerIds = orderedPeerIds.active.concat(orderedPeerIds.archived).concat(updatePeerIds); + const newOrderedPeerIds = allPeerIds.reduce<{ active: string[]; archived: string[] }>((acc, peerId) => { + if (!byPeerId[peerId]?.orderedIds?.length) return acc; + const peer = selectPeer(global, peerId); - if (selectUser(global, userId)?.areStoriesHidden) { - acc.archived.push(userId); + if (peer?.areStoriesHidden) { + acc.archived.push(peerId); } else { - acc.active.push(userId); + acc.active.push(peerId); } return acc; }, { active: [], archived: [] }); - function sort(userId: string) { - const UNREAD_PRIORITY = 1e12; - const PREMIUM_PRIORITY = 1e6; - const isPremium = selectUser(global, userId)?.isPremium; - const { lastUpdatedAt = 0, orderedIds, lastReadId = 0 } = byUserId[userId] || {}; - const hasUnread = lastReadId < orderedIds?.[orderedIds.length - 1]; + function compare(peerIdA: string, peerIdB: string) { + const peerA = selectPeer(global, peerIdA)!; + const peerB = selectPeer(global, peerIdB)!; - const priority = (hasUnread ? UNREAD_PRIORITY : 0) + (isPremium ? PREMIUM_PRIORITY : 0); + const diffCurrentUser = compareFields(currentUserId === peerIdA, currentUserId === peerIdB); + if (diffCurrentUser) return diffCurrentUser; - return currentUserId === userId ? Infinity : (lastUpdatedAt + priority); + const { lastUpdatedAt: luaA = 0, orderedIds: orderedA, lastReadId: lriA = 0 } = byPeerId[peerIdA] || {}; + const hasUnreadA = lriA < orderedA?.[orderedA.length - 1]; + const { lastUpdatedAt: luaB = 0, orderedIds: orderedB, lastReadId: lriB = 0 } = byPeerId[peerIdB] || {}; + const hasUnreadB = lriB < orderedB?.[orderedB.length - 1]; + + const diffUnread = compareFields(hasUnreadA, hasUnreadB); + if (diffUnread) return diffUnread; + + const diffPremium = compareFields('isPremium' in peerA, 'isPremium' in peerB); + if (diffPremium) return diffPremium; + + const diffType = compareFields(isUserId(peerIdA), isUserId(peerIdB)); + if (diffType) return diffType; + + return compareFields(luaA, luaB); } - newOrderedUserIds.archived = orderBy( - unique(newOrderedUserIds.archived) - .filter((userId) => byUserId[userId]?.orderedIds?.length), - sort, - 'desc', - ); - newOrderedUserIds.active = orderBy( - unique(newOrderedUserIds.active) - .filter((userId) => byUserId[userId]?.orderedIds?.length), - sort, - 'desc', - ); + newOrderedPeerIds.archived = unique(newOrderedPeerIds.archived) + .filter((peerId) => byPeerId[peerId]?.orderedIds?.length) + .sort(compare); + newOrderedPeerIds.active = unique(newOrderedPeerIds.active) + .filter((peerId) => byPeerId[peerId]?.orderedIds?.length) + .sort(compare); return { ...global, stories: { ...global.stories, - orderedUserIds: newOrderedUserIds, + orderedPeerIds: newOrderedPeerIds, }, }; } -function updateUserLastUpdatedAt(global: T, userId: string): T { - const userStories = global.stories.byUserId[userId]; - const lastUpdatedAt = userStories.orderedIds.reduce((acc, storyId) => { - const { date } = userStories.byId[storyId] as ApiStorySkipped || {}; +function updatePeerLastUpdatedAt(global: T, peerId: string): T { + const peerStories = global.stories.byPeerId[peerId]; + const lastUpdatedAt = peerStories.orderedIds.reduce((acc, storyId) => { + const { date } = peerStories.byId[storyId] as ApiStorySkipped || {}; if (date && (!acc || acc < date)) { acc = date; } @@ -432,10 +479,10 @@ function updateUserLastUpdatedAt(global: T, userId: strin ...global, stories: { ...global.stories, - byUserId: { - ...global.stories.byUserId, - [userId]: { - ...userStories, + byPeerId: { + ...global.stories.byPeerId, + [peerId]: { + ...peerStories, lastUpdatedAt, }, }, diff --git a/src/global/reducers/users.ts b/src/global/reducers/users.ts index c7905cd46..e96149bdb 100644 --- a/src/global/reducers/users.ts +++ b/src/global/reducers/users.ts @@ -164,9 +164,9 @@ export function deleteContact(global: T, userId: string): ...global, stories: { ...global.stories, - orderedUserIds: { - active: global.stories.orderedUserIds.active.filter((id) => id !== userId), - archived: global.stories.orderedUserIds.archived.filter((id) => id !== userId), + orderedPeerIds: { + active: global.stories.orderedPeerIds.active.filter((id) => id !== userId), + archived: global.stories.orderedPeerIds.archived.filter((id) => id !== userId), }, }, }; diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 7bf07d7b5..f3bf07233 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -1,4 +1,6 @@ -import type { ApiChat, ApiChatFullInfo, ApiChatType } from '../../api/types'; +import type { + ApiChat, ApiChatFullInfo, ApiChatType, ApiPeer, +} from '../../api/types'; import type { GlobalState, TabArgs } from '../types'; import { MAIN_THREAD_ID } from '../../api/types'; @@ -22,6 +24,10 @@ import { selectBot, selectIsCurrentUserPremium, selectUser, selectUserFullInfo, } from './users'; +export function selectPeer(global: T, peerId: string): ApiPeer | undefined { + return selectUser(global, peerId) || selectChat(global, peerId); +} + export function selectChat(global: T, chatId: string): ApiChat | undefined { return global.chats.byId[chatId]; } @@ -30,6 +36,11 @@ export function selectChatFullInfo(global: T, chatId: str return global.chats.fullInfoById[chatId]; } +export function selectPeerFullInfo(global: T, peerId: string) { + if (isUserId(peerId)) return selectUserFullInfo(global, peerId); + return selectChatFullInfo(global, peerId); +} + export function selectChatUser(global: T, chat: ApiChat) { const userId = getPrivateChatUserId(chat); if (!userId) { @@ -271,8 +282,7 @@ export function selectShouldDetectChatLanguage( global: T, chatId: string, ) { const chat = selectChat(global, chatId); - const fullInfo = isUserId(chatId) ? selectUserFullInfo(global, chatId) : selectChatFullInfo(global, chatId); - if (!chat || !fullInfo) return false; + if (!chat) return false; const { canTranslateChats } = global.settings.byKey; const isPremium = selectIsCurrentUserPremium(global); diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 844a35f22..4b35461ed 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -3,8 +3,8 @@ import type { ApiMessage, ApiMessageEntityCustomEmoji, ApiMessageOutgoingStatus, + ApiPeer, ApiStickerSetInfo, - ApiUser, } from '../../api/types'; import type { ChatTranslatedMessages, @@ -50,9 +50,9 @@ import { isUserRightBanned, } from '../helpers'; import { - selectChat, selectChatFullInfo, selectIsChatWithSelf, selectRequestedChatTranslationLanguage, + selectChat, selectChatFullInfo, selectIsChatWithSelf, selectPeer, selectRequestedChatTranslationLanguage, } from './chats'; -import { selectUserStory } from './stories'; +import { selectPeerStory } from './stories'; import { selectIsStickerFavorite } from './symbols'; import { selectTabState } from './tabs'; import { @@ -416,20 +416,20 @@ export function selectOutgoingStatus( return getSendingState(message); } -export function selectSender(global: T, message: ApiMessage): ApiUser | ApiChat | undefined { +export function selectSender(global: T, message: ApiMessage): ApiPeer | undefined { const { senderId } = message; if (!senderId) { return undefined; } - return isUserId(senderId) ? selectUser(global, senderId) : selectChat(global, senderId); + return selectPeer(global, senderId); } export function selectReplySender(global: T, message: ApiMessage, isForwarded = false) { if (isForwarded) { const { senderUserId, hiddenUserName } = message.forwardInfo || {}; if (senderUserId) { - return isUserId(senderUserId) ? selectUser(global, senderUserId) : selectChat(global, senderUserId); + return selectPeer(global, senderUserId); } if (hiddenUserName) return undefined; } @@ -439,15 +439,16 @@ export function selectReplySender(global: T, message: Api return undefined; } - return isUserId(senderId) ? selectUser(global, senderId) : selectChat(global, senderId); + return selectPeer(global, senderId); } export function selectForwardedSender( global: T, message: ApiMessage, -): ApiUser | ApiChat | undefined { +): ApiPeer | undefined { const isStoryForward = Boolean(message.content.storyData); if (isStoryForward) { - return selectUser(global, message.content.storyData!.userId); + const peerId = message.content.storyData!.peerId; + return selectPeer(global, peerId); } const { forwardInfo } = message; @@ -615,9 +616,9 @@ export function selectAllowedMessageActions(global: T, me const canEdit = !isLocal && !isAction && isMessageEditable && hasMessageEditRight; const story = content.storyData - ? selectUserStory(global, content.storyData.userId, content.storyData.id) + ? selectPeerStory(global, content.storyData.peerId, content.storyData.id) : (content.webPage?.story - ? selectUserStory(global, content.webPage.story.userId, content.webPage.story.id) + ? selectPeerStory(global, content.webPage.story.peerId, content.webPage.story.id) : undefined ); @@ -1101,7 +1102,7 @@ function canAutoLoadMedia({ canAutoLoadMediaInPrivateChats: boolean; canAutoLoadMediaInGroups: boolean; canAutoLoadMediaInChannels: boolean; - sender?: ApiChat | ApiUser; + sender?: ApiPeer; }) { const isMediaFromContact = Boolean(sender && ( sender.id === global.currentUserId || selectIsUserOrChatContact(global, sender) diff --git a/src/global/selectors/stories.ts b/src/global/selectors/stories.ts index 9500663d0..993891d0d 100644 --- a/src/global/selectors/stories.ts +++ b/src/global/selectors/stories.ts @@ -1,4 +1,4 @@ -import type { ApiTypeStory, ApiUserStories } from '../../api/types'; +import type { ApiPeerStories, ApiTypeStory } from '../../api/types'; import type { GlobalState, TabArgs } from '../types'; import { getCurrentTabId } from '../../util/establishMultitabRole'; @@ -8,51 +8,51 @@ export function selectCurrentViewedStory( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { - const { storyViewer: { userId, storyId } } = selectTabState(global, tabId); + const { storyViewer: { peerId, storyId } } = selectTabState(global, tabId); - return { userId, storyId }; + return { peerId, storyId }; } export function selectIsStoryViewerOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { - const { userId, storyId } = selectCurrentViewedStory(global, tabId); + const { peerId, storyId } = selectCurrentViewedStory(global, tabId); - return Boolean(userId) && Boolean(storyId); + return Boolean(peerId) && Boolean(storyId); } -export function selectUserStories( - global: T, userId: string, -): ApiUserStories | undefined { - return global.stories.byUserId[userId]; +export function selectPeerStories( + global: T, peerId: string, +): ApiPeerStories | undefined { + return global.stories.byPeerId[peerId]; } -export function selectUserStory( - global: T, userId: string, storyId: number, +export function selectPeerStory( + global: T, peerId: string, storyId: number, ): ApiTypeStory | undefined { - return selectUserStories(global, userId)?.byId[storyId]; + return selectPeerStories(global, peerId)?.byId[storyId]; } -export function selectUserFirstUnreadStoryId( - global: T, userId: string, +export function selectPeerFirstUnreadStoryId( + global: T, peerId: string, ) { - const userStories = selectUserStories(global, userId); - if (!userStories) { + const peerStories = selectPeerStories(global, peerId); + if (!peerStories) { return undefined; } - if (!userStories.lastReadId) { - return userStories.orderedIds?.[0]; + if (!peerStories.lastReadId) { + return peerStories.orderedIds?.[0]; } - const lastReadIndex = userStories.orderedIds.findIndex((id) => id === userStories.lastReadId); + const lastReadIndex = peerStories.orderedIds.findIndex((id) => id === peerStories.lastReadId); - return userStories.orderedIds?.[lastReadIndex + 1]; + return peerStories.orderedIds?.[lastReadIndex + 1]; } -export function selectUserFirstStoryId( - global: T, userId: string, +export function selectPeerFirstStoryId( + global: T, peerId: string, ) { - return selectUserStories(global, userId)?.orderedIds?.[0]; + return selectPeerStories(global, peerId)?.orderedIds?.[0]; } diff --git a/src/global/selectors/users.ts b/src/global/selectors/users.ts index fb086e743..f46205071 100644 --- a/src/global/selectors/users.ts +++ b/src/global/selectors/users.ts @@ -1,5 +1,5 @@ import type { - ApiChat, ApiUser, ApiUserFullInfo, ApiUserStatus, + ApiPeer, ApiUser, ApiUserFullInfo, ApiUserStatus, } from '../../api/types'; import type { GlobalState } from '../types'; @@ -38,8 +38,8 @@ export function selectUserByPhoneNumber(global: T, phoneN return Object.values(global.users.byId).find((user) => user?.phoneNumber === phoneNumberCleaned); } -export function selectIsUserOrChatContact(global: T, userOrChat: ApiUser | ApiChat) { - return global.contactList && global.contactList.userIds.includes(userOrChat.id); +export function selectIsUserOrChatContact(global: T, peer: ApiPeer) { + return global.contactList && global.contactList.userIds.includes(peer.id); } export function selectBot(global: T, userId: string): ApiUser | undefined { diff --git a/src/global/types.ts b/src/global/types.ts index 172a80e75..27be0ee0e 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -36,6 +36,7 @@ import type { ApiPaymentCredentials, ApiPaymentFormNativeParams, ApiPaymentSavedInfo, + ApiPeerStories, ApiPhoneCall, ApiPhoto, ApiPremiumPromo, @@ -61,7 +62,6 @@ import type { ApiUser, ApiUserFullInfo, ApiUserStatus, - ApiUserStories, ApiVideo, ApiWallpaper, ApiWebPage, @@ -271,7 +271,7 @@ export type TabState = { reactionPicker?: { chatId?: string; messageId?: number; - storyUserId?: string; + storyPeerId?: string; storyId?: number; position?: IAnchorPosition; sendAsMessage?: boolean; @@ -349,16 +349,16 @@ export type TabState = { storyViewer: { isRibbonShown?: boolean; isArchivedRibbonShown?: boolean; - userId?: string; + peerId?: string; storyId?: number; isMuted: boolean; - isSingleUser?: boolean; + isSinglePeer?: boolean; isSingleStory?: boolean; isPrivate?: boolean; isArchive?: boolean; // Last viewed story id in current view session. - // Used for better switch animation between users. - lastViewedByUserIds?: Record; + // Used for better switch animation between peers. + lastViewedByPeerIds?: Record; isPrivacyModalOpen?: boolean; isStealthModalOpen?: boolean; viewModal?: { @@ -744,12 +744,12 @@ export type GlobalState = { }; stories: { - byUserId: Record; + byPeerId: Record; hasNext?: boolean; stateHash?: string; hasNextInArchive?: boolean; archiveStateHash?: string; - orderedUserIds: { + orderedPeerIds: { active: string[]; archived: string[]; }; @@ -1939,7 +1939,7 @@ export interface ActionPayloads { position: IAnchorPosition; } & WithTabId; openStoryReactionPicker: { - storyUserId: string; + peerId: string; storyId: number; position: IAnchorPosition; sendAsMessage?: boolean; @@ -1949,31 +1949,34 @@ export interface ActionPayloads { // Stories loadAllStories: undefined; loadAllHiddenStories: undefined; - loadUserStories: { - userId: string; + loadPeerStories: { + peerId: string; }; - loadUserPinnedStories: { - userId: string; + loadPeerPinnedStories: { + peerId: string; offsetId?: number; } & WithTabId; loadStoriesArchive: { + peerId: string; offsetId?: number; } & WithTabId; - loadUserSkippedStories: { - userId: string; + loadPeerSkippedStories: { + peerId: string; } & WithTabId; - loadUserStoriesByIds: { - userId: string; + loadPeerStoriesByIds: { + peerId: string; storyIds: number[]; } & WithTabId; viewStory: { - userId: string; + peerId: string; storyId: number; } & WithTabId; deleteStory: { + peerId: string; storyId: number; } & WithTabId; toggleStoryPinned: { + peerId: string; storyId: number; isPinned?: boolean; } & WithTabId; @@ -1982,9 +1985,9 @@ export interface ActionPayloads { isArchived?: boolean; } & WithTabId; openStoryViewer: { - userId: string; + peerId: string; storyId?: number; - isSingleUser?: boolean; + isSinglePeer?: boolean; isSingleStory?: boolean; isPrivate?: boolean; isArchive?: boolean; @@ -2002,9 +2005,11 @@ export interface ActionPayloads { } & WithTabId; closeStoryViewer: WithTabId | undefined; loadStoryViews: ({ + peerId: string; storyId: number; isPreload: true; } | { + peerId: string; storyId: number; offset?: string; query?: string; @@ -2025,11 +2030,11 @@ export interface ActionPayloads { } & WithTabId; closeStoryViewModal: WithTabId | undefined; copyStoryLink: { - userId: string; + peerId: string; storyId: number; } & WithTabId; reportStory: { - userId: string; + peerId: string; storyId: number; reason: ApiReportReason; description: string; @@ -2037,19 +2042,21 @@ export interface ActionPayloads { openStoryPrivacyEditor: WithTabId | undefined; closeStoryPrivacyEditor: WithTabId | undefined; editStoryPrivacy: { + peerId: string; storyId: number; privacy: ApiPrivacySettings; }; toggleStoriesHidden: { - userId : string; + peerId : string; isHidden: boolean; }; loadStoriesMaxIds: { - userIds: string[]; + peerIds: string[]; }; sendStoryReaction: { - userId: string; + peerId: string; storyId: number; + containerId: string; reaction?: ApiReaction; shouldAddToRecent?: boolean; } & WithTabId; diff --git a/src/hooks/polling/usePeerStoriesPolling.ts b/src/hooks/polling/usePeerStoriesPolling.ts new file mode 100644 index 000000000..c70670faa --- /dev/null +++ b/src/hooks/polling/usePeerStoriesPolling.ts @@ -0,0 +1,67 @@ +import { useEffect, useMemo } from '../../lib/teact/teact'; +import { getActions, getGlobal } from '../../global'; + +import type { ApiChat, ApiUser } from '../../api/types'; + +import { isChatChannel, isUserBot, isUserId } from '../../global/helpers'; +import { selectPeer, selectUserStatus } from '../../global/selectors'; +import { throttle } from '../../util/schedulers'; + +const POLLING_INTERVAL = 60 * 60 * 1000; +const PEER_LAST_POLLING_TIME = new Map(); +let PEER_ID_QUEUE = new Set(); +const LIMIT_PER_REQUEST = 100; +const REQUEST_THROTTLE = 500; + +const loadFromQueue = throttle(() => { + const queue = Array.from(PEER_ID_QUEUE); + const queueToLoad = queue.slice(0, LIMIT_PER_REQUEST); + const otherQueue = queue.slice(LIMIT_PER_REQUEST + 1); + + getActions().loadStoriesMaxIds({ + peerIds: queueToLoad, + }); + + queueToLoad.forEach((id) => PEER_LAST_POLLING_TIME.set(id, Date.now())); + + PEER_ID_QUEUE = new Set(otherQueue); + + // Schedule next load + if (PEER_ID_QUEUE.size) { + loadFromQueue(); + } +}, REQUEST_THROTTLE); + +export default function usePeerStoriesPolling(ids?: string[]) { + const peers = useMemo(() => { + const global = getGlobal(); + return ids?.map((id) => selectPeer(global, id)).filter(Boolean); + }, [ids]); + + const pollablePeerIds = useMemo(() => { + const global = getGlobal(); + return peers?.filter((peer) => { + const lastPollingTime = PEER_LAST_POLLING_TIME.get(peer.id) || 0; + if (Date.now() - lastPollingTime < POLLING_INTERVAL) { + return false; + } + + if (isUserId(peer.id)) { + const user = peer as ApiUser; + const status = selectUserStatus(global, user.id); + const isStatusAvailable = status && status.type !== 'userStatusEmpty'; + return !user.isContact && !user.isSelf && !isUserBot(user) && !peer.isSupport && isStatusAvailable; + } else { + const chat = peer as ApiChat; + return isChatChannel(chat); + } + }).map((user) => user.id); + }, [peers]); + + useEffect(() => { + if (pollablePeerIds?.length) { + pollablePeerIds.forEach((id) => PEER_ID_QUEUE.add(id)); + loadFromQueue(); + } + }, [pollablePeerIds]); +} diff --git a/src/hooks/polling/useUserStoriesPolling.ts b/src/hooks/polling/useUserStoriesPolling.ts deleted file mode 100644 index 78b1de531..000000000 --- a/src/hooks/polling/useUserStoriesPolling.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useMemo } from '../../lib/teact/teact'; -import { getActions, getGlobal } from '../../global'; - -import { isUserBot } from '../../global/helpers'; -import { selectUserStatus } from '../../global/selectors'; -import { throttle } from '../../util/schedulers'; - -const POLLING_INTERVAL = 60 * 60 * 1000; -const USER_LAST_POLLING_TIME = new Map(); -let USER_ID_QUEUE = new Set(); -const LIMIT_PER_REQUEST = 100; -const REQUEST_THROTTLE = 500; - -const loadFromQueue = throttle(() => { - const queue = Array.from(USER_ID_QUEUE); - const queueToLoad = queue.slice(0, LIMIT_PER_REQUEST); - const otherQueue = queue.slice(LIMIT_PER_REQUEST + 1); - - getActions().loadStoriesMaxIds({ - userIds: queueToLoad, - }); - - queueToLoad.forEach((id) => USER_LAST_POLLING_TIME.set(id, Date.now())); - - USER_ID_QUEUE = new Set(otherQueue); - - // Schedule next load - if (USER_ID_QUEUE.size) { - loadFromQueue(); - } -}, REQUEST_THROTTLE); - -export default function useUserStoriesPolling(ids?: string[]) { - const users = useMemo(() => { - return ids?.map((id) => getGlobal().users.byId[id]).filter(Boolean); - }, [ids]); - - const pollableUserIds = useMemo(() => { - const global = getGlobal(); - return users?.filter((user) => { - const lastPollingTime = USER_LAST_POLLING_TIME.get(user.id) || 0; - if (Date.now() - lastPollingTime < POLLING_INTERVAL) { - return false; - } - - const status = selectUserStatus(global, user.id); - const isStatusAvailable = status && status.type !== 'userStatusEmpty'; - return !user.isContact && !user.isSelf && !isUserBot(user) && !user.isSupport && isStatusAvailable; - }).map((user) => user.id); - }, [users]); - - useEffect(() => { - if (pollableUserIds?.length) { - pollableUserIds.forEach((id) => USER_ID_QUEUE.add(id)); - loadFromQueue(); - } - }, [pollableUserIds]); -} diff --git a/src/hooks/useEnsureStory.ts b/src/hooks/useEnsureStory.ts index eaa680c71..1ef5f5124 100644 --- a/src/hooks/useEnsureStory.ts +++ b/src/hooks/useEnsureStory.ts @@ -8,25 +8,25 @@ import { throttle } from '../util/schedulers'; const THROTTLE_THRESHOLD_MS = 200; function useEnsureStory( - userId?: string, + peerId?: string, storyId?: number, story?: ApiTypeStory, ) { - const { loadUserStoriesByIds } = getActions(); + const { loadPeerStoriesByIds } = getActions(); const loadStoryThrottled = useMemo(() => { - const throttled = throttle(loadUserStoriesByIds, THROTTLE_THRESHOLD_MS, true); + const throttled = throttle(loadPeerStoriesByIds, THROTTLE_THRESHOLD_MS, true); return () => { - throttled({ userId: userId!, storyIds: [storyId!] }); + throttled({ peerId: peerId!, storyIds: [storyId!] }); }; - }, [storyId, userId]); + }, [storyId, peerId]); useEffect(() => { const shouldLoadStory = !story || !('content' in story || 'isDeleted' in story); - if (userId && storyId && shouldLoadStory) { + if (peerId && storyId && shouldLoadStory) { loadStoryThrottled(); } - }, [loadStoryThrottled, story, storyId, userId]); + }, [loadStoryThrottled, story, storyId, peerId]); } export default useEnsureStory; diff --git a/src/hooks/useMessageMediaMetadata.ts b/src/hooks/useMessageMediaMetadata.ts index eeebe8351..396b28dd1 100644 --- a/src/hooks/useMessageMediaMetadata.ts +++ b/src/hooks/useMessageMediaMetadata.ts @@ -1,7 +1,7 @@ import { useMemo } from '../lib/teact/teact'; import type { - ApiAudio, ApiChat, ApiMessage, ApiUser, ApiVoice, + ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiVoice, } from '../api/types'; import { @@ -21,7 +21,7 @@ const MINIMAL_SIZE = 115; // spec says 100, but on Chrome 93 it's not showing // TODO Add support for video in future const useMessageMediaMetadata = ( - message: ApiMessage, sender?: ApiUser | ApiChat, chat?: ApiChat, + message: ApiMessage, sender?: ApiPeer, chat?: ApiChat, ): MediaMetadata | undefined => { const lang = useLang(); diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js index 1de3d92a3..8849d3032 100644 --- a/src/lib/gramjs/tl/AllTLObjects.js +++ b/src/lib/gramjs/tl/AllTLObjects.js @@ -1,6 +1,6 @@ const api = require('./api'); -const LAYER = 163; +const LAYER = 165; const tlobjects = {}; for (const tl of Object.values(api)) { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index e14b9d725..b78211038 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -314,13 +314,14 @@ namespace Api { export type TypeSponsoredWebPage = SponsoredWebPage; export type TypeStoryViews = StoryViews; export type TypeStoryItem = StoryItemDeleted | StoryItemSkipped | StoryItem; - export type TypeUserStories = UserStories; export type TypeStoryView = StoryView; export type TypeInputReplyTo = InputReplyToMessage | InputReplyToStory; export type TypeExportedStoryLink = ExportedStoryLink; export type TypeStoriesStealthMode = StoriesStealthMode; export type TypeMediaAreaCoordinates = MediaAreaCoordinates; - export type TypeMediaArea = MediaAreaVenue | InputMediaAreaVenue | MediaAreaGeoPoint; + export type TypeMediaArea = MediaAreaVenue | InputMediaAreaVenue | MediaAreaGeoPoint | MediaAreaSuggestedReaction; + export type TypePeerStories = PeerStories; + export type TypeBooster = Booster; export type TypeResPQ = ResPQ; export type TypeP_Q_inner_data = PQInnerData | PQInnerDataDc | PQInnerDataTemp | PQInnerDataTempDc; export type TypeServer_DH_Params = ServerDHParamsFail | ServerDHParamsOk; @@ -539,9 +540,12 @@ namespace Api { export namespace stories { export type TypeAllStories = stories.AllStoriesNotModified | stories.AllStories; export type TypeStories = stories.Stories; - export type TypeUserStories = stories.UserStories; export type TypeStoryViewsList = stories.StoryViewsList; export type TypeStoryViews = stories.StoryViews; + export type TypePeerStories = stories.PeerStories; + export type TypeBoostsStatus = stories.BoostsStatus; + export type TypeCanApplyBoostResult = stories.CanApplyBoostOk | stories.CanApplyBoostReplace; + export type TypeBoostersList = stories.BoostersList; } export class InputPeerEmpty extends VirtualClass {}; @@ -808,10 +812,10 @@ namespace Api { emoticon: string; }; export class InputMediaStory extends VirtualClass<{ - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int; }> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int; }; export class InputChatPhotoEmpty extends VirtualClass {}; @@ -1152,6 +1156,9 @@ namespace Api { joinRequest?: true; forum?: true; // flags2: undefined; + storiesHidden?: true; + storiesHiddenMin?: true; + storiesUnavailable?: true; id: long; accessHash?: long; title: string; @@ -1164,6 +1171,7 @@ namespace Api { defaultBannedRights?: Api.TypeChatBannedRights; participantsCount?: int; usernames?: Api.TypeUsername[]; + storiesMaxId?: int; }> { // flags: undefined; creator?: true; @@ -1187,6 +1195,9 @@ namespace Api { joinRequest?: true; forum?: true; // flags2: undefined; + storiesHidden?: true; + storiesHiddenMin?: true; + storiesUnavailable?: true; id: long; accessHash?: long; title: string; @@ -1199,6 +1210,7 @@ namespace Api { defaultBannedRights?: Api.TypeChatBannedRights; participantsCount?: int; usernames?: Api.TypeUsername[]; + storiesMaxId?: int; }; export class ChannelForbidden extends VirtualClass<{ // flags: undefined; @@ -1275,6 +1287,7 @@ namespace Api { antispam?: true; participantsHidden?: true; translationsDisabled?: true; + storiesPinnedAvailable?: true; id: long; about: string; participantsCount?: int; @@ -1310,6 +1323,7 @@ namespace Api { recentRequesters?: long[]; defaultSendAs?: Api.TypePeer; availableReactions?: Api.TypeChatReactions; + stories?: Api.TypePeerStories; }> { // flags: undefined; canViewParticipants?: true; @@ -1325,6 +1339,7 @@ namespace Api { antispam?: true; participantsHidden?: true; translationsDisabled?: true; + storiesPinnedAvailable?: true; id: long; about: string; participantsCount?: int; @@ -1360,6 +1375,7 @@ namespace Api { recentRequesters?: long[]; defaultSendAs?: Api.TypePeer; availableReactions?: Api.TypeChatReactions; + stories?: Api.TypePeerStories; }; export class ChatParticipant extends VirtualClass<{ userId: long; @@ -1647,13 +1663,13 @@ namespace Api { export class MessageMediaStory extends VirtualClass<{ // flags: undefined; viaMention?: true; - userId: long; + peer: Api.TypePeer; id: int; story?: Api.TypeStoryItem; }> { // flags: undefined; viaMention?: true; - userId: long; + peer: Api.TypePeer; id: int; story?: Api.TypeStoryItem; }; @@ -2230,7 +2246,7 @@ namespace Api { botBroadcastAdminRights?: Api.TypeChatAdminRights; premiumGifts?: Api.TypePremiumGiftOption[]; wallpaper?: Api.TypeWallPaper; - stories?: Api.TypeUserStories; + stories?: Api.TypePeerStories; }> { // flags: undefined; blocked?: true; @@ -2261,7 +2277,7 @@ namespace Api { botBroadcastAdminRights?: Api.TypeChatAdminRights; premiumGifts?: Api.TypePremiumGiftOption[]; wallpaper?: Api.TypeWallPaper; - stories?: Api.TypeUserStories; + stories?: Api.TypePeerStories; }; export class Contact extends VirtualClass<{ userId: long; @@ -3268,17 +3284,17 @@ namespace Api { userId: long; }; export class UpdateStory extends VirtualClass<{ - userId: long; + peer: Api.TypePeer; story: Api.TypeStoryItem; }> { - userId: long; + peer: Api.TypePeer; story: Api.TypeStoryItem; }; export class UpdateReadStories extends VirtualClass<{ - userId: long; + peer: Api.TypePeer; maxId: int; }> { - userId: long; + peer: Api.TypePeer; maxId: int; }; export class UpdateStoryID extends VirtualClass<{ @@ -3294,11 +3310,11 @@ namespace Api { stealthMode: Api.TypeStoriesStealthMode; }; export class UpdateSentStoryReaction extends VirtualClass<{ - userId: long; + peer: Api.TypePeer; storyId: int; reaction: Api.TypeReaction; }> { - userId: long; + peer: Api.TypePeer; storyId: int; reaction: Api.TypeReaction; }; @@ -5532,7 +5548,7 @@ namespace Api { prices: Api.TypeLabeledPrice[]; maxTipAmount?: long; suggestedTipAmounts?: long[]; - recurringTermsUrl?: string; + termsUrl?: string; }> { // flags: undefined; test?: true; @@ -5548,7 +5564,7 @@ namespace Api { prices: Api.TypeLabeledPrice[]; maxTipAmount?: long; suggestedTipAmounts?: long[]; - recurringTermsUrl?: string; + termsUrl?: string; }; export class PaymentCharge extends VirtualClass<{ id: string; @@ -6905,6 +6921,9 @@ namespace Api { manageCall?: true; other?: true; manageTopics?: true; + postStories?: true; + editStories?: true; + deleteStories?: true; } | void> { // flags: undefined; changeInfo?: true; @@ -6919,6 +6938,9 @@ namespace Api { manageCall?: true; other?: true; manageTopics?: true; + postStories?: true; + editStories?: true; + deleteStories?: true; }; export class ChatBannedRights extends VirtualClass<{ // flags: undefined; @@ -7258,12 +7280,12 @@ namespace Api { }; export class WebPageAttributeStory extends VirtualClass<{ // flags: undefined; - userId: long; + peer: Api.TypePeer; id: int; story?: Api.TypeStoryItem; }> { // flags: undefined; - userId: long; + peer: Api.TypePeer; id: int; story?: Api.TypeStoryItem; }; @@ -8348,13 +8370,17 @@ namespace Api { // flags: undefined; hasViewers?: true; viewsCount: int; - reactionsCount: int; + forwardsCount?: int; + reactions?: Api.TypeReactionCount[]; + reactionsCount?: int; recentViewers?: long[]; }> { // flags: undefined; hasViewers?: true; viewsCount: int; - reactionsCount: int; + forwardsCount?: int; + reactions?: Api.TypeReactionCount[]; + reactionsCount?: int; recentViewers?: long[]; }; export class StoryItemDeleted extends VirtualClass<{ @@ -8385,6 +8411,7 @@ namespace Api { edited?: true; contacts?: true; selectedContacts?: true; + out?: true; id: int; date: int; expireDate: int; @@ -8405,6 +8432,7 @@ namespace Api { edited?: true; contacts?: true; selectedContacts?: true; + out?: true; id: int; date: int; expireDate: int; @@ -8416,17 +8444,6 @@ namespace Api { views?: Api.TypeStoryViews; sentReaction?: Api.TypeReaction; }; - export class UserStories extends VirtualClass<{ - // flags: undefined; - userId: long; - maxReadId?: int; - stories: Api.TypeStoryItem[]; - }> { - // flags: undefined; - userId: long; - maxReadId?: int; - stories: Api.TypeStoryItem[]; - }; export class StoryView extends VirtualClass<{ // flags: undefined; blocked?: true; @@ -8518,6 +8535,37 @@ namespace Api { coordinates: Api.TypeMediaAreaCoordinates; geo: Api.TypeGeoPoint; }; + export class MediaAreaSuggestedReaction extends VirtualClass<{ + // flags: undefined; + dark?: true; + flipped?: true; + coordinates: Api.TypeMediaAreaCoordinates; + reaction: Api.TypeReaction; + }> { + // flags: undefined; + dark?: true; + flipped?: true; + coordinates: Api.TypeMediaAreaCoordinates; + reaction: Api.TypeReaction; + }; + export class PeerStories extends VirtualClass<{ + // flags: undefined; + peer: Api.TypePeer; + maxReadId?: int; + stories: Api.TypeStoryItem[]; + }> { + // flags: undefined; + peer: Api.TypePeer; + maxReadId?: int; + stories: Api.TypeStoryItem[]; + }; + export class Booster extends VirtualClass<{ + userId: long; + expires: int; + }> { + userId: long; + expires: int; + }; export class ResPQ extends VirtualClass<{ nonce: int128; serverNonce: int128; @@ -10695,7 +10743,8 @@ namespace Api { hasMore?: true; count: int; state: string; - userStories: Api.TypeUserStories[]; + peerStories: Api.TypePeerStories[]; + chats: Api.TypeChat[]; users: Api.TypeUser[]; stealthMode: Api.TypeStoriesStealthMode; }> { @@ -10703,24 +10752,20 @@ namespace Api { hasMore?: true; count: int; state: string; - userStories: Api.TypeUserStories[]; + peerStories: Api.TypePeerStories[]; + chats: Api.TypeChat[]; users: Api.TypeUser[]; stealthMode: Api.TypeStoriesStealthMode; }; export class Stories extends VirtualClass<{ count: int; stories: Api.TypeStoryItem[]; + chats: Api.TypeChat[]; users: Api.TypeUser[]; }> { count: int; stories: Api.TypeStoryItem[]; - users: Api.TypeUser[]; - }; - export class UserStories extends VirtualClass<{ - stories: Api.TypeUserStories; - users: Api.TypeUser[]; - }> { - stories: Api.TypeUserStories; + chats: Api.TypeChat[]; users: Api.TypeUser[]; }; export class StoryViewsList extends VirtualClass<{ @@ -10745,6 +10790,53 @@ namespace Api { views: Api.TypeStoryViews[]; users: Api.TypeUser[]; }; + export class PeerStories extends VirtualClass<{ + stories: Api.TypePeerStories; + chats: Api.TypeChat[]; + users: Api.TypeUser[]; + }> { + stories: Api.TypePeerStories; + chats: Api.TypeChat[]; + users: Api.TypeUser[]; + }; + export class BoostsStatus extends VirtualClass<{ + // flags: undefined; + myBoost?: true; + level: int; + currentLevelBoosts: int; + boosts: int; + nextLevelBoosts?: int; + premiumAudience?: Api.TypeStatsPercentValue; + }> { + // flags: undefined; + myBoost?: true; + level: int; + currentLevelBoosts: int; + boosts: int; + nextLevelBoosts?: int; + premiumAudience?: Api.TypeStatsPercentValue; + }; + export class CanApplyBoostOk extends VirtualClass {}; + export class CanApplyBoostReplace extends VirtualClass<{ + currentBoost: Api.TypePeer; + chats: Api.TypeChat[]; + }> { + currentBoost: Api.TypePeer; + chats: Api.TypeChat[]; + }; + export class BoostersList extends VirtualClass<{ + // flags: undefined; + count: int; + boosters: Api.TypeBooster[]; + nextOffset?: string; + users: Api.TypeUser[]; + }> { + // flags: undefined; + count: int; + boosters: Api.TypeBooster[]; + nextOffset?: string; + users: Api.TypeUser[]; + }; } export class InvokeAfterMsg extends Request, int[]> { - id: Api.TypeInputUser[]; - }; } export namespace contacts { @@ -11793,13 +11880,6 @@ namespace Api { }>, Bool> { id: long[]; }; - export class ToggleStoriesHidden extends Request, Bool> { - id: Api.TypeInputUser; - hidden: Bool; - }; export class SetBlocked extends Request {}; + export class CanSendStory extends Request, Bool> { + peer: Api.TypeInputPeer; + }; export class SendStory extends Request, Api.TypeUpdates> { // flags: undefined; + peer: Api.TypeInputPeer; id: int; media?: Api.TypeInputMedia; mediaAreas?: Api.TypeMediaArea[]; @@ -15164,14 +15252,18 @@ namespace Api { privacyRules?: Api.TypeInputPrivacyRule[]; }; export class DeleteStories extends Request, int[]> { + peer: Api.TypeInputPeer; id: int[]; }; export class TogglePinned extends Request, int[]> { + peer: Api.TypeInputPeer; id: int[]; pinned: Bool; }; @@ -15186,32 +15278,29 @@ namespace Api { hidden?: true; state?: string; }; - export class GetUserStories extends Request, stories.TypeUserStories> { - userId: Api.TypeInputUser; - }; export class GetPinnedStories extends Request, stories.TypeStories> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; offsetId: int; limit: int; }; export class GetStoriesArchive extends Request, stories.TypeStories> { + peer: Api.TypeInputPeer; offsetId: int; limit: int; }; export class GetStoriesByID extends Request, stories.TypeStories> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int[]; }; export class ToggleAllStoriesHidden extends Request, Bool> { hidden: Bool; }; - export class GetAllReadUserStories extends Request {}; export class ReadStories extends Request, int[]> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; maxId: int; }; export class IncrementStoryViews extends Request, Bool> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int[]; }; export class GetStoryViewsList extends Request, stories.TypeStoryViews> { + peer: Api.TypeInputPeer; id: int[]; }; export class ExportStoryLink extends Request, Api.TypeExportedStoryLink> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int; }; export class Report extends Request, Bool> { - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; id: int[]; reason: Api.TypeReportReason; message: string; @@ -15286,23 +15378,66 @@ namespace Api { export class SendReaction extends Request, Api.TypeUpdates> { // flags: undefined; addToRecent?: true; - userId: Api.TypeInputUser; + peer: Api.TypeInputPeer; storyId: int; reaction: Api.TypeReaction; }; + export class GetPeerStories extends Request, stories.TypePeerStories> { + peer: Api.TypeInputPeer; + }; + export class GetAllReadPeerStories extends Request {}; + export class GetPeerMaxIDs extends Request, int[]> { + id: Api.TypeInputPeer[]; + }; + export class GetChatsToSend extends Request {}; + export class TogglePeerStoriesHidden extends Request, Bool> { + peer: Api.TypeInputPeer; + hidden: Bool; + }; + export class GetBoostsStatus extends Request, stories.TypeBoostsStatus> { + peer: Api.TypeInputPeer; + }; + export class GetBoostersList extends Request, stories.TypeBoostersList> { + peer: Api.TypeInputPeer; + offset: string; + limit: int; + }; + export class CanApplyBoost extends Request, stories.TypeCanApplyBoostResult> { + peer: Api.TypeInputPeer; + }; + export class ApplyBoost extends Request, Bool> { + peer: Api.TypeInputPeer; + }; } export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | ReqPq | ReqPqMulti | ReqPqMultiNew | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession | auth.SendCode | auth.SignUp | auth.SignIn | auth.LogOut | auth.ResetAuthorizations | auth.ExportAuthorization | auth.ImportAuthorization | auth.BindTempAuthKey | auth.ImportBotAuthorization | auth.CheckPassword | auth.RequestPasswordRecovery | auth.RecoverPassword | auth.ResendCode | auth.CancelCode | auth.DropTempAuthKeys | auth.ExportLoginToken | auth.ImportLoginToken | auth.AcceptLoginToken | auth.CheckRecoveryPassword | auth.ImportWebTokenAuthorization | auth.RequestFirebaseSms | auth.ResetLoginEmail | account.RegisterDevice | account.UnregisterDevice | account.UpdateNotifySettings | account.GetNotifySettings | account.ResetNotifySettings | account.UpdateProfile | account.UpdateStatus | account.GetWallPapers | account.ReportPeer | account.CheckUsername | account.UpdateUsername | account.GetPrivacy | account.SetPrivacy | account.DeleteAccount | account.GetAccountTTL | account.SetAccountTTL | account.SendChangePhoneCode | account.ChangePhone | account.UpdateDeviceLocked | account.GetAuthorizations | account.ResetAuthorization | account.GetPassword | account.GetPasswordSettings | account.UpdatePasswordSettings | account.SendConfirmPhoneCode | account.ConfirmPhone | account.GetTmpPassword | account.GetWebAuthorizations | account.ResetWebAuthorization | account.ResetWebAuthorizations | account.GetAllSecureValues | account.GetSecureValue | account.SaveSecureValue | account.DeleteSecureValue | account.GetAuthorizationForm | account.AcceptAuthorization | account.SendVerifyPhoneCode | account.VerifyPhone | account.SendVerifyEmailCode | account.VerifyEmail | account.InitTakeoutSession | account.FinishTakeoutSession | account.ConfirmPasswordEmail | account.ResendPasswordEmail | account.CancelPasswordEmail | account.GetContactSignUpNotification | account.SetContactSignUpNotification | account.GetNotifyExceptions | account.GetWallPaper | account.UploadWallPaper | account.SaveWallPaper | account.InstallWallPaper | account.ResetWallPapers | account.GetAutoDownloadSettings | account.SaveAutoDownloadSettings | account.UploadTheme | account.CreateTheme | account.UpdateTheme | account.SaveTheme | account.InstallTheme | account.GetTheme | account.GetThemes | account.SetContentSettings | account.GetContentSettings | account.GetMultiWallPapers | account.GetGlobalPrivacySettings | account.SetGlobalPrivacySettings | account.ReportProfilePhoto | account.ResetPassword | account.DeclinePasswordReset | account.GetChatThemes | account.SetAuthorizationTTL | account.ChangeAuthorizationSettings | account.GetSavedRingtones | account.SaveRingtone | account.UploadRingtone | account.UpdateEmojiStatus | account.GetDefaultEmojiStatuses | account.GetRecentEmojiStatuses | account.ClearRecentEmojiStatuses | account.ReorderUsernames | account.ToggleUsername | account.GetDefaultProfilePhotoEmojis | account.GetDefaultGroupPhotoEmojis | account.GetAutoSaveSettings | account.SaveAutoSaveSettings | account.DeleteAutoSaveExceptions | account.InvalidateSignInCodes - | users.GetUsers | users.GetFullUser | users.SetSecureValueErrors | users.GetStoriesMaxIDs - | contacts.GetContactIDs | contacts.GetStatuses | contacts.GetContacts | contacts.ImportContacts | contacts.DeleteContacts | contacts.DeleteByPhones | contacts.Block | contacts.Unblock | contacts.GetBlocked | contacts.Search | contacts.ResolveUsername | contacts.GetTopPeers | contacts.ResetTopPeerRating | contacts.ResetSaved | contacts.GetSaved | contacts.ToggleTopPeers | contacts.AddContact | contacts.AcceptContact | contacts.GetLocated | contacts.BlockFromReplies | contacts.ResolvePhone | contacts.ExportContactToken | contacts.ImportContactToken | contacts.EditCloseFriends | contacts.ToggleStoriesHidden | contacts.SetBlocked + | users.GetUsers | users.GetFullUser | users.SetSecureValueErrors + | contacts.GetContactIDs | contacts.GetStatuses | contacts.GetContacts | contacts.ImportContacts | contacts.DeleteContacts | contacts.DeleteByPhones | contacts.Block | contacts.Unblock | contacts.GetBlocked | contacts.Search | contacts.ResolveUsername | contacts.GetTopPeers | contacts.ResetTopPeerRating | contacts.ResetSaved | contacts.GetSaved | contacts.ToggleTopPeers | contacts.AddContact | contacts.AcceptContact | contacts.GetLocated | contacts.BlockFromReplies | contacts.ResolvePhone | contacts.ExportContactToken | contacts.ImportContactToken | contacts.EditCloseFriends | contacts.SetBlocked | messages.GetMessages | messages.GetDialogs | messages.GetHistory | messages.Search | messages.ReadHistory | messages.DeleteHistory | messages.DeleteMessages | messages.ReceivedMessages | messages.SetTyping | messages.SendMessage | messages.SendMedia | messages.ForwardMessages | messages.ReportSpam | messages.GetPeerSettings | messages.Report | messages.GetChats | messages.GetFullChat | messages.EditChatTitle | messages.EditChatPhoto | messages.AddChatUser | messages.DeleteChatUser | messages.CreateChat | messages.GetDhConfig | messages.RequestEncryption | messages.AcceptEncryption | messages.DiscardEncryption | messages.SetEncryptedTyping | messages.ReadEncryptedHistory | messages.SendEncrypted | messages.SendEncryptedFile | messages.SendEncryptedService | messages.ReceivedQueue | messages.ReportEncryptedSpam | messages.ReadMessageContents | messages.GetStickers | messages.GetAllStickers | messages.GetWebPagePreview | messages.ExportChatInvite | messages.CheckChatInvite | messages.ImportChatInvite | messages.GetStickerSet | messages.InstallStickerSet | messages.UninstallStickerSet | messages.StartBot | messages.GetMessagesViews | messages.EditChatAdmin | messages.MigrateChat | messages.SearchGlobal | messages.ReorderStickerSets | messages.GetDocumentByHash | messages.GetSavedGifs | messages.SaveGif | messages.GetInlineBotResults | messages.SetInlineBotResults | messages.SendInlineBotResult | messages.GetMessageEditData | messages.EditMessage | messages.EditInlineBotMessage | messages.GetBotCallbackAnswer | messages.SetBotCallbackAnswer | messages.GetPeerDialogs | messages.SaveDraft | messages.GetAllDrafts | messages.GetFeaturedStickers | messages.ReadFeaturedStickers | messages.GetRecentStickers | messages.SaveRecentSticker | messages.ClearRecentStickers | messages.GetArchivedStickers | messages.GetMaskStickers | messages.GetAttachedStickers | messages.SetGameScore | messages.SetInlineGameScore | messages.GetGameHighScores | messages.GetInlineGameHighScores | messages.GetCommonChats | messages.GetWebPage | messages.ToggleDialogPin | messages.ReorderPinnedDialogs | messages.GetPinnedDialogs | messages.SetBotShippingResults | messages.SetBotPrecheckoutResults | messages.UploadMedia | messages.SendScreenshotNotification | messages.GetFavedStickers | messages.FaveSticker | messages.GetUnreadMentions | messages.ReadMentions | messages.GetRecentLocations | messages.SendMultiMedia | messages.UploadEncryptedFile | messages.SearchStickerSets | messages.GetSplitRanges | messages.MarkDialogUnread | messages.GetDialogUnreadMarks | messages.ClearAllDrafts | messages.UpdatePinnedMessage | messages.SendVote | messages.GetPollResults | messages.GetOnlines | messages.EditChatAbout | messages.EditChatDefaultBannedRights | messages.GetEmojiKeywords | messages.GetEmojiKeywordsDifference | messages.GetEmojiKeywordsLanguages | messages.GetEmojiURL | messages.GetSearchCounters | messages.RequestUrlAuth | messages.AcceptUrlAuth | messages.HidePeerSettingsBar | messages.GetScheduledHistory | messages.GetScheduledMessages | messages.SendScheduledMessages | messages.DeleteScheduledMessages | messages.GetPollVotes | messages.ToggleStickerSets | messages.GetDialogFilters | messages.GetSuggestedDialogFilters | messages.UpdateDialogFilter | messages.UpdateDialogFiltersOrder | messages.GetOldFeaturedStickers | messages.GetReplies | messages.GetDiscussionMessage | messages.ReadDiscussion | messages.UnpinAllMessages | messages.DeleteChat | messages.DeletePhoneCallHistory | messages.CheckHistoryImport | messages.InitHistoryImport | messages.UploadImportedMedia | messages.StartHistoryImport | messages.GetExportedChatInvites | messages.GetExportedChatInvite | messages.EditExportedChatInvite | messages.DeleteRevokedExportedChatInvites | messages.DeleteExportedChatInvite | messages.GetAdminsWithInvites | messages.GetChatInviteImporters | messages.SetHistoryTTL | messages.CheckHistoryImportPeer | messages.SetChatTheme | messages.GetMessageReadParticipants | messages.GetSearchResultsCalendar | messages.GetSearchResultsPositions | messages.HideChatJoinRequest | messages.HideAllChatJoinRequests | messages.ToggleNoForwards | messages.SaveDefaultSendAs | messages.SendReaction | messages.GetMessagesReactions | messages.GetMessageReactionsList | messages.SetChatAvailableReactions | messages.GetAvailableReactions | messages.SetDefaultReaction | messages.TranslateText | messages.GetUnreadReactions | messages.ReadReactions | messages.SearchSentMedia | messages.GetAttachMenuBots | messages.GetAttachMenuBot | messages.ToggleBotInAttachMenu | messages.RequestWebView | messages.ProlongWebView | messages.RequestSimpleWebView | messages.SendWebViewResultMessage | messages.SendWebViewData | messages.TranscribeAudio | messages.RateTranscribedAudio | messages.GetCustomEmojiDocuments | messages.GetEmojiStickers | messages.GetFeaturedEmojiStickers | messages.ReportReaction | messages.GetTopReactions | messages.GetRecentReactions | messages.ClearRecentReactions | messages.GetExtendedMedia | messages.SetDefaultHistoryTTL | messages.GetDefaultHistoryTTL | messages.SendBotRequestedPeer | messages.GetEmojiGroups | messages.GetEmojiStatusGroups | messages.GetEmojiProfilePhotoGroups | messages.SearchCustomEmoji | messages.TogglePeerTranslations | messages.GetBotApp | messages.RequestAppWebView | messages.SetChatWallPaper | updates.GetState | updates.GetDifference | updates.GetChannelDifference | photos.UpdateProfilePhoto | photos.UploadProfilePhoto | photos.DeletePhotos | photos.GetUserPhotos | photos.UploadContactProfilePhoto @@ -15317,6 +15452,6 @@ namespace Api { | folders.EditPeerFolders | stats.GetBroadcastStats | stats.LoadAsyncGraph | stats.GetMegagroupStats | stats.GetMessagePublicForwards | stats.GetMessageStats | chatlists.ExportChatlistInvite | chatlists.DeleteExportedInvite | chatlists.EditExportedInvite | chatlists.GetExportedInvites | chatlists.CheckChatlistInvite | chatlists.JoinChatlistInvite | chatlists.GetChatlistUpdates | chatlists.JoinChatlistUpdates | chatlists.HideChatlistUpdates | chatlists.GetLeaveChatlistSuggestions | chatlists.LeaveChatlist - | stories.CanSendStory | stories.SendStory | stories.EditStory | stories.DeleteStories | stories.TogglePinned | stories.GetAllStories | stories.GetUserStories | stories.GetPinnedStories | stories.GetStoriesArchive | stories.GetStoriesByID | stories.ToggleAllStoriesHidden | stories.GetAllReadUserStories | stories.ReadStories | stories.IncrementStoryViews | stories.GetStoryViewsList | stories.GetStoriesViews | stories.ExportStoryLink | stories.Report | stories.ActivateStealthMode | stories.SendReaction; + | stories.CanSendStory | stories.SendStory | stories.EditStory | stories.DeleteStories | stories.TogglePinned | stories.GetAllStories | stories.GetPinnedStories | stories.GetStoriesArchive | stories.GetStoriesByID | stories.ToggleAllStoriesHidden | stories.ReadStories | stories.IncrementStoryViews | stories.GetStoryViewsList | stories.GetStoriesViews | stories.ExportStoryLink | stories.Report | stories.ActivateStealthMode | stories.SendReaction | stories.GetPeerStories | stories.GetAllReadPeerStories | stories.GetPeerMaxIDs | stories.GetChatsToSend | stories.TogglePeerStoriesHidden | stories.GetBoostsStatus | stories.GetBoostersList | stories.CanApplyBoost | stories.ApplyBoost; } diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index d60725f88..2121fac43 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -33,7 +33,7 @@ inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0 inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; -inputMediaStory#9a86b58f user_id:InputUser id:int = InputMedia; +inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; @@ -77,10 +77,10 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#83259464 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector = Chat; +channel#94f592db flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; -channelFull#f2355507 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions = ChatFull; +channelFull#723027bd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions stories:flags2.4?PeerStories = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; @@ -104,7 +104,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; -messageMediaStory#cbb20d88 flags:# via_mention:flags.1?true user_id:long id:int story:flags.0?StoryItem = MessageMedia; +messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; messageActionChatEditTitle#b5a1ce5a title:string = MessageAction; @@ -181,7 +181,7 @@ inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; +userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; @@ -330,11 +330,11 @@ updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector< updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; -updateStory#205a4133 user_id:long story:StoryItem = Update; -updateReadStories#feb5345a user_id:long max_id:int = Update; +updateStory#75b3b798 peer:Peer story:StoryItem = Update; +updateReadStories#f74e932b peer:Peer max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; -updateSentStoryReaction#e3a73d20 user_id:long story_id:int reaction:Reaction = Update; +updateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; @@ -689,7 +689,7 @@ phoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason; phoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason; dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#3e85a91b flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector recurring_terms_url:flags.9?string = Invoice; +invoice#5db95a15 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; postAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress; paymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo; @@ -883,7 +883,7 @@ pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true optio pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights; chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; @@ -929,7 +929,7 @@ baseThemeArctic#5b11125a = BaseTheme; inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; -webPageAttributeStory#939a4671 flags:# user_id:long id:int story:flags.0?StoryItem = WebPageAttribute; +webPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector = payments.BankCardData; @@ -1127,15 +1127,13 @@ messagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote; messagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote; messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = MessagePeerVote; sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#c64c0b97 flags:# has_viewers:flags.1?true views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector reactions_count:flags.4?int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; -userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; +storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; -stories.allStories#519d899e flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; -stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; -stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; +stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector chats:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; +stories.stories#5dd8c3c8 count:int stories:Vector chats:Vector users:Vector = stories.Stories; storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; @@ -1147,6 +1145,14 @@ mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; +mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; +peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; +stories.peerStories#cae68768 stories:PeerStories chats:Vector users:Vector = stories.PeerStories; +stories.boostsStatus#66ea1fef flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue = stories.BoostsStatus; +stories.canApplyBoostOk#c3173587 = stories.CanApplyBoostResult; +stories.canApplyBoostReplace#712c4655 current_boost:Peer chats:Vector = stories.CanApplyBoostResult; +booster#e9e6380 user_id:long expires:int = Booster; +stories.boostersList#f3dd3d1d flags:# count:int boosters:Vector next_offset:flags.0?string users:Vector = stories.BoostersList; ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; @@ -1209,7 +1215,6 @@ account.reorderUsernames#ef500eab order:Vector = Bool; account.toggleUsername#58d6b376 username:string active:Bool = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; -users.getStoriesMaxIDs#ca1cb9ab id:Vector = Vector; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; @@ -1222,7 +1227,6 @@ contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags. contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates; contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; -contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; @@ -1448,18 +1452,20 @@ chatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite; chatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector = Updates; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; -stories.deleteStories#b5d501d7 id:Vector = Vector; -stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; +stories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.deleteStories#ae59db5f peer:InputPeer id:Vector = Vector; +stories.togglePinned#9a75a1ef peer:InputPeer id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; -stories.getUserStories#96d528e0 user_id:InputUser = stories.UserStories; -stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = stories.Stories; -stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories; -stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; -stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; -stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; -stories.getStoryViewsList#f95f61a4 flags:# just_contacts:flags.0?true reactions_first:flags.2?true q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; -stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; -stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; +stories.getPinnedStories#5821a5dc peer:InputPeer offset_id:int limit:int = stories.Stories; +stories.getStoriesArchive#b4352016 peer:InputPeer offset_id:int limit:int = stories.Stories; +stories.getStoriesByID#5774ca74 peer:InputPeer id:Vector = stories.Stories; +stories.readStories#a556dac8 peer:InputPeer max_id:int = Vector; +stories.incrementStoryViews#b2028afb peer:InputPeer id:Vector = Bool; +stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; +stories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink; +stories.report#1923fa8c peer:InputPeer id:Vector reason:ReportReason message:string = Bool; stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; -stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates;`; \ No newline at end of file +stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates; +stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories; +stories.getPeerMaxIDs#535983c3 id:Vector = Vector; +stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;`; \ No newline at end of file diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 1c33b7b70..f528dde15 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -60,7 +60,6 @@ "account.toggleUsername", "users.getUsers", "users.getFullUser", - "users.getStoriesMaxIDs", "contacts.getContacts", "contacts.importContacts", "contacts.deleteContacts", @@ -73,7 +72,6 @@ "contacts.addContact", "contacts.resolvePhone", "contacts.editCloseFriends", - "contacts.toggleStoriesHidden", "messages.getMessages", "messages.getDialogs", "messages.getHistory", @@ -303,7 +301,7 @@ "stories.deleteStories", "stories.togglePinned", "stories.getAllStories", - "stories.getUserStories", + "stories.getPeerStories", "stories.getPinnedStories", "stories.getStoriesArchive", "stories.getStoriesByID", @@ -313,5 +311,8 @@ "stories.exportStoryLink", "stories.report", "stories.activateStealthMode", - "stories.sendReaction" + "stories.sendReaction", + "stories.getPeerMaxIDs", + "stories.togglePeerStoriesHidden", + "stories.getPeerStories" ] diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 42471d70b..8a386d988 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -42,7 +42,7 @@ inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0 inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; -inputMediaStory#9a86b58f user_id:InputUser id:int = InputMedia; +inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -96,11 +96,11 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#83259464 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector = Chat; +channel#94f592db flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; -channelFull#f2355507 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions = ChatFull; +channelFull#723027bd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions stories:flags2.4?PeerStories = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -129,7 +129,7 @@ messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true tes messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; -messageMediaStory#cbb20d88 flags:# via_mention:flags.1?true user_id:long id:int story:flags.0?StoryItem = MessageMedia; +messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; @@ -221,7 +221,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; +userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -383,11 +383,11 @@ updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector< updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; -updateStory#205a4133 user_id:long story:StoryItem = Update; -updateReadStories#feb5345a user_id:long max_id:int = Update; +updateStory#75b3b798 peer:Peer story:StoryItem = Update; +updateReadStories#f74e932b peer:Peer max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; -updateSentStoryReaction#e3a73d20 user_id:long story_id:int reaction:Reaction = Update; +updateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -843,7 +843,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#3e85a91b flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector recurring_terms_url:flags.9?string = Invoice; +invoice#5db95a15 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -1123,7 +1123,7 @@ chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights; chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; @@ -1198,7 +1198,7 @@ inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_th themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; -webPageAttributeStory#939a4671 flags:# user_id:long id:int story:flags.0?StoryItem = WebPageAttribute; +webPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -1529,20 +1529,16 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#c64c0b97 flags:# has_viewers:flags.1?true views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector reactions_count:flags.4?int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; - -userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; +storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; -stories.allStories#519d899e flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; +stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector chats:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; -stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; - -stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; +stories.stories#5dd8c3c8 count:int stories:Vector chats:Vector users:Vector = stories.Stories; storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; @@ -1562,6 +1558,22 @@ mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; +mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; + +peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; + +stories.peerStories#cae68768 stories:PeerStories chats:Vector users:Vector = stories.PeerStories; + +stories.boostsStatus#e5c1aa5c flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string = stories.BoostsStatus; + +stories.canApplyBoostOk#c3173587 = stories.CanApplyBoostResult; +stories.canApplyBoostReplace#712c4655 current_boost:Peer chats:Vector = stories.CanApplyBoostResult; + +booster#e9e6380 user_id:long expires:int = Booster; + +stories.boostersList#f3dd3d1d flags:# count:int boosters:Vector next_offset:flags.0?string users:Vector = stories.BoostersList; + +messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector = messages.WebPage; ---functions--- @@ -1688,7 +1700,6 @@ account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; -users.getStoriesMaxIDs#ca1cb9ab id:Vector = Vector; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; @@ -1714,7 +1725,6 @@ contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; -contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool; contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; @@ -1793,7 +1803,7 @@ messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:fla messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats; -messages.getWebPage#32ca8f91 url:string hash:int = WebPage; +messages.getWebPage#8d9692a3 url:string hash:int = messages.WebPage; messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector = Bool; messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs; @@ -2103,23 +2113,30 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.canSendStory#b100d45d = Bool; -stories.sendStory#d455fcec flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; -stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; -stories.deleteStories#b5d501d7 id:Vector = Vector; -stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; +stories.canSendStory#c7dfdfdd peer:InputPeer = Bool; +stories.sendStory#bcb73644 flags:# pinned:flags.2?true noforwards:flags.4?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; +stories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.deleteStories#ae59db5f peer:InputPeer id:Vector = Vector; +stories.togglePinned#9a75a1ef peer:InputPeer id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; -stories.getUserStories#96d528e0 user_id:InputUser = stories.UserStories; -stories.getPinnedStories#b471137 user_id:InputUser offset_id:int limit:int = stories.Stories; -stories.getStoriesArchive#1f5bc5d2 offset_id:int limit:int = stories.Stories; -stories.getStoriesByID#6a15cf46 user_id:InputUser id:Vector = stories.Stories; +stories.getPinnedStories#5821a5dc peer:InputPeer offset_id:int limit:int = stories.Stories; +stories.getStoriesArchive#b4352016 peer:InputPeer offset_id:int limit:int = stories.Stories; +stories.getStoriesByID#5774ca74 peer:InputPeer id:Vector = stories.Stories; stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool; -stories.getAllReadUserStories#729c562c = Updates; -stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; -stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; -stories.getStoryViewsList#f95f61a4 flags:# just_contacts:flags.0?true reactions_first:flags.2?true q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; -stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; -stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; -stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; +stories.readStories#a556dac8 peer:InputPeer max_id:int = Vector; +stories.incrementStoryViews#b2028afb peer:InputPeer id:Vector = Bool; +stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; +stories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector = stories.StoryViews; +stories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink; +stories.report#1923fa8c peer:InputPeer id:Vector reason:ReportReason message:string = Bool; stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; -stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates; +stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates; +stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories; +stories.getAllReadPeerStories#9b5ae7f9 = Updates; +stories.getPeerMaxIDs#535983c3 id:Vector = Vector; +stories.getChatsToSend#a56a8b60 = messages.Chats; +stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; +stories.getBoostsStatus#4c449472 peer:InputPeer = stories.BoostsStatus; +stories.getBoostersList#337ef980 peer:InputPeer offset:string limit:int = stories.BoostersList; +stories.canApplyBoost#db05c1bd peer:InputPeer = stories.CanApplyBoostResult; +stories.applyBoost#f29d7c2b peer:InputPeer = Bool; diff --git a/src/util/iteratees.ts b/src/util/iteratees.ts index 248cce259..0438ba23e 100644 --- a/src/util/iteratees.ts +++ b/src/util/iteratees.ts @@ -171,3 +171,7 @@ export function findLast(array: Array, predicate: (value: T, index: number return undefined; } + +export function compareFields(a: T, b: T) { + return Number(b) - Number(a); +} diff --git a/src/util/notifications.ts b/src/util/notifications.ts index e7eab83f6..236dd93bb 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.ts @@ -1,7 +1,7 @@ import { getActions, getGlobal, setGlobal } from '../global'; import type { - ApiChat, ApiMessage, ApiPeerReaction, + ApiChat, ApiMessage, ApiPeer, ApiPeerReaction, ApiPhoneCall, ApiUser, } from '../api/types'; import { ApiMediaFormat } from '../api/types'; @@ -389,7 +389,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A return { title, body }; } -async function getAvatar(chat: ApiChat | ApiUser) { +async function getAvatar(chat: ApiPeer) { const imageHash = getChatAvatarHash(chat); if (!imageHash) return undefined; let mediaData = mediaLoader.getFromMemory(imageHash);