From aad2ed366d9c2769b27982abfae32b750b888fc2 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:52:43 +0200 Subject: [PATCH] Introduce Paid Media (#4729) --- src/api/gramjs/apiBuilders/bots.ts | 4 +- src/api/gramjs/apiBuilders/common.ts | 4 +- src/api/gramjs/apiBuilders/messageContent.ts | 102 ++++- src/api/gramjs/apiBuilders/messages.ts | 32 +- src/api/gramjs/apiBuilders/payments.ts | 17 +- src/api/gramjs/apiBuilders/stories.ts | 13 +- src/api/gramjs/apiBuilders/symbols.ts | 1 + src/api/gramjs/gramjsBuilders/index.ts | 30 -- src/api/gramjs/helpers.ts | 167 +++++--- src/api/gramjs/localDb.ts | 22 +- src/api/gramjs/methods/bots.ts | 18 +- src/api/gramjs/methods/chats.ts | 14 +- src/api/gramjs/methods/client.ts | 141 ++++--- src/api/gramjs/methods/media.ts | 93 +---- src/api/gramjs/methods/messages.ts | 16 +- src/api/gramjs/methods/payments.ts | 6 +- src/api/gramjs/methods/users.ts | 16 +- src/api/gramjs/updates/updater.ts | 103 ++--- src/api/gramjs/worker/connector.ts | 1 - src/api/types/bots.ts | 1 + src/api/types/messages.ts | 46 ++- src/api/types/payments.ts | 15 +- src/api/types/stories.ts | 10 +- src/api/types/updates.ts | 22 +- src/assets/font-icons/stars-lock.svg | 1 + src/assets/premium/PremiumEffects.svg | 6 + .../common/AnimatedIconFromSticker.tsx | 4 +- src/components/common/Audio.tsx | 26 +- src/components/common/Avatar.tsx | 3 +- src/components/common/Composer.scss | 19 +- src/components/common/Composer.tsx | 16 +- src/components/common/Document.tsx | 51 +-- src/components/common/GifButton.tsx | 7 +- src/components/common/GroupChatInfo.tsx | 5 +- .../common/MediaSpoiler.module.scss | 1 + src/components/common/MediaSpoiler.tsx | 6 +- src/components/common/MessageText.tsx | 6 +- src/components/common/PrivateChatInfo.tsx | 5 +- src/components/common/ProfileInfo.tsx | 27 +- src/components/common/ReactionEmoji.tsx | 2 +- src/components/common/StickerSet.tsx | 2 + src/components/common/StickerView.tsx | 4 +- src/components/common/WebLink.tsx | 4 +- .../common/embedded/EmbeddedMessage.tsx | 24 +- .../common/embedded/EmojiIconBackground.tsx | 4 +- .../common/helpers/mediaDimensions.ts | 33 +- .../common/profile/UserBirthday.tsx | 4 +- .../common/reactions/CustomEmojiEffect.tsx | 4 +- .../left/main/hooks/useChatListEntry.tsx | 8 +- src/components/left/search/AudioResults.tsx | 8 +- src/components/left/search/FileResults.tsx | 9 +- src/components/left/search/LinkResults.tsx | 5 +- src/components/left/search/MediaResults.tsx | 2 +- .../search/helpers/createMapStateToProps.ts | 4 +- src/components/main/DownloadManager.tsx | 82 ++-- .../main/premium/PremiumFeatureModal.tsx | 2 + .../main/premium/PremiumMainModal.tsx | 2 + src/components/mediaViewer/MediaViewer.tsx | 380 +++++++++--------- .../mediaViewer/MediaViewerActions.tsx | 121 +++--- .../mediaViewer/MediaViewerContent.tsx | 166 +++----- .../mediaViewer/MediaViewerSlides.tsx | 124 +++--- src/components/mediaViewer/SenderInfo.tsx | 97 +++-- .../mediaViewer/helpers/getViewableMedia.ts | 129 ++++++ .../mediaViewer/helpers/ghostAnimation.ts | 33 +- .../mediaViewer/hooks/useMediaProps.ts | 151 +++---- .../middle/ActionMessageSuggestedAvatar.tsx | 20 +- src/components/middle/MessageListBotInfo.tsx | 4 +- .../middle/composer/AttachBotIcon.tsx | 2 +- .../middle/composer/StickerSetCover.tsx | 9 +- .../helpers/renderKeyboardButtonText.tsx | 3 +- .../middle/helpers/groupMessages.ts | 11 +- src/components/middle/message/Album.scss | 4 + src/components/middle/message/Album.tsx | 74 ++-- .../middle/message/ContextMenuContainer.tsx | 27 +- src/components/middle/message/Invoice.tsx | 2 +- src/components/middle/message/Location.tsx | 22 +- src/components/middle/message/Message.tsx | 97 +++-- .../message/PaidMediaOverlay.module.scss | 25 ++ .../middle/message/PaidMediaOverlay.tsx | 83 ++++ src/components/middle/message/Photo.tsx | 86 ++-- src/components/middle/message/RoundVideo.tsx | 14 +- src/components/middle/message/Sticker.tsx | 4 +- src/components/middle/message/Video.tsx | 99 +++-- src/components/middle/message/WebPage.tsx | 19 +- .../middle/message/_message-content.scss | 8 +- .../message/helpers/buildContentClassName.ts | 16 +- .../message/helpers/calculateAlbumLayout.ts | 24 +- .../middle/message/helpers/copyOptions.ts | 8 +- .../message/helpers/getSingularPaidMedia.ts | 17 + .../middle/message/helpers/mediaDimensions.ts | 53 ++- .../middle/message/hocs/withSelectControl.tsx | 22 +- .../message/hooks/useBlurredMediaThumbRef.ts | 19 +- .../middle/message/hooks/useInnerHandlers.ts | 20 +- .../modals/common/TableInfoModal.tsx | 15 +- .../modals/stars/PaidMediaThumb.module.scss | 90 +++++ .../modals/stars/PaidMediaThumb.tsx | 92 +++++ .../stars/StarsBalanceModal.module.scss | 4 + .../modals/stars/StarsPaymentModal.tsx | 54 ++- .../stars/StarsTransactionItem.module.scss | 4 + .../modals/stars/StarsTransactionItem.tsx | 26 +- .../payment/ReceiptModal.module.scss | 13 + src/components/payment/ReceiptModal.tsx | 104 +++-- src/components/right/Profile.tsx | 37 +- .../story/mediaArea/MediaAreaOverlay.tsx | 16 +- src/components/ui/Modal.scss | 1 + src/config.ts | 7 +- src/global/actions/api/bots.ts | 1 + src/global/actions/api/chats.ts | 15 +- src/global/actions/api/messages.ts | 10 +- src/global/actions/api/payments.ts | 4 +- src/global/actions/api/reactions.ts | 2 +- src/global/actions/api/settings.ts | 1 + src/global/actions/api/users.ts | 24 +- src/global/actions/apiUpdaters/chats.ts | 25 +- src/global/actions/apiUpdaters/messages.ts | 47 ++- src/global/actions/ui/mediaViewer.ts | 21 +- src/global/actions/ui/messages.ts | 52 ++- src/global/actions/ui/misc.ts | 2 +- src/global/helpers/media.ts | 40 +- src/global/helpers/messageMedia.ts | 375 ++++++++++------- src/global/helpers/messageSummary.ts | 4 +- src/global/helpers/messages.ts | 70 ++-- src/global/helpers/payments.ts | 10 + src/global/helpers/symbols.ts | 9 +- src/global/initialState.ts | 4 +- src/global/reducers/messages.ts | 34 +- src/global/reducers/payments.ts | 6 +- src/global/selectors/messages.ts | 30 +- src/global/selectors/ui.ts | 11 +- src/global/types.ts | 49 +-- src/hooks/useMessageMediaMetadata.ts | 4 +- src/hooks/useThumbnail.ts | 4 +- src/lib/gramjs/client/TelegramClient.js | 12 +- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 273 +++++++++++-- src/lib/gramjs/tl/apiTl.js | 42 +- src/lib/gramjs/tl/static/api.tl | 57 ++- src/styles/icons.scss | 90 +++-- src/styles/icons.woff | Bin 30384 -> 30548 bytes src/styles/icons.woff2 | Bin 25384 -> 25576 bytes src/types/icons/font.ts | 1 + src/types/index.ts | 7 + src/util/emoji/customEmojiManager.ts | 4 +- src/util/formatCurrency.tsx | 8 +- src/util/getReadableErrorText.ts | 2 + src/util/themeStyle.ts | 2 + 146 files changed, 3139 insertions(+), 1951 deletions(-) create mode 100644 src/assets/font-icons/stars-lock.svg create mode 100644 src/assets/premium/PremiumEffects.svg create mode 100644 src/components/mediaViewer/helpers/getViewableMedia.ts create mode 100644 src/components/middle/message/PaidMediaOverlay.module.scss create mode 100644 src/components/middle/message/PaidMediaOverlay.tsx create mode 100644 src/components/middle/message/helpers/getSingularPaidMedia.ts create mode 100644 src/components/modals/stars/PaidMediaThumb.module.scss create mode 100644 src/components/modals/stars/PaidMediaThumb.tsx diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 17272849b..385000ef4 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -17,7 +17,7 @@ import type { } from '../../types'; import { pick } from '../../../util/iteratees'; -import localDb from '../localDb'; +import { addDocumentToLocalDb } from '../helpers'; import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent'; @@ -100,7 +100,7 @@ function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIco if (!document) return undefined; - localDb.documents[String(icon.icon.id)] = icon.icon; + addDocumentToLocalDb(icon.icon); return { name: icon.name, diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index 793f04a7d..f44e6148e 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -85,10 +85,12 @@ export function buildApiPhoto(photo: GramJs.Photo, isSpoiler?: boolean): ApiPhot .map(buildApiPhotoSize); return { + mediaType: 'photo', id: String(photo.id), thumbnail: buildApiThumbnailFromStripped(photo.sizes), sizes, isSpoiler, + date: photo.date, ...(photo.videoSizes && { videoSizes: compact(photo.videoSizes.map(buildApiVideoSize)), isVideo: true }), }; } @@ -115,7 +117,7 @@ export function buildApiPhotoSize(photoSize: GramJs.PhotoSize): ApiPhotoSize { return { width: w, height: h, - type: type as ('m' | 'x' | 'y'), + type: type as ('s' | 'm' | 'x' | 'y' | 'w'), }; } diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 999fb2f4a..8d9d03071 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -10,8 +10,9 @@ import type { ApiGiveawayResults, ApiInvoice, ApiLocation, - ApiMessageExtendedMediaPreview, + ApiMediaExtendedPreview, ApiMessageStoryData, + ApiPaidMedia, ApiPhoto, ApiPoll, ApiSticker, @@ -27,7 +28,7 @@ import type { UniversalMessage } from './messages'; import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_WEBM_TYPE } from '../../../config'; import { pick } from '../../../util/iteratees'; -import { addStoryToLocalDb, serializeBytes } from '../helpers'; +import { addMediaToLocalDb, addStoryToLocalDb, serializeBytes } from '../helpers'; import { buildApiFormattedText, buildApiMessageEntity, @@ -74,6 +75,8 @@ export function buildMessageTextContent( } export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaContent | undefined { + addMediaToLocalDb(media); + const ttlSeconds = 'ttlSeconds' in media ? media.ttlSeconds : undefined; const isExpiredVoice = isExpiredVoiceMessage(media); @@ -98,7 +101,7 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC return undefined; } - if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) { + if (media instanceof GramJs.MessageMediaInvoice && media.extendedMedia instanceof GramJs.MessageExtendedMedia) { return buildMessageMediaContent(media.extendedMedia.media); } @@ -145,6 +148,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC const giveawayResults = buildGiweawayResultsFromMedia(media); if (giveawayResults) return { giveawayResults }; + const paidMedia = buildPaidMedia(media); + if (paidMedia) return { paidMedia }; + return undefined; } @@ -202,6 +208,7 @@ export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: bo } = videoAttr; return { + mediaType: 'video', id: String(id), mimeType, duration, @@ -241,6 +248,7 @@ export function buildAudioFromDocument(document: GramJs.Document): ApiAudio | un } = audioAttributes; return { + mediaType: 'audio', id: String(id), mimeType, duration, @@ -298,6 +306,7 @@ function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined { .map((thumb) => buildApiPhotoSize(thumb)); return { + mediaType: 'audio', id: String(media.document.id), fileName: getFilenameFromDocument(media.document, 'audio'), thumbnailSizes, @@ -342,7 +351,9 @@ function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined { const { duration, waveform } = audioAttribute; return { + mediaType: 'voice', id: String(media.document.id), + size: media.document.size.toJSNumber(), duration, waveform: waveform ? Array.from(waveform) : undefined, }; @@ -374,7 +385,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u } } - let mediaType: ApiDocument['mediaType'] | undefined; + let innerMediaType: ApiDocument['innerMediaType'] | undefined; let mediaSize: ApiDocument['mediaSize'] | undefined; if (photoSize) { mediaSize = { @@ -383,7 +394,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u }; if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) { - mediaType = 'photo'; + innerMediaType = 'photo'; const imageAttribute = attributes .find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize); @@ -396,7 +407,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u }; } } else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) { - mediaType = 'video'; + innerMediaType = 'video'; const videoAttribute = attributes .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo); @@ -411,13 +422,14 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u } return { + mediaType: 'document', id: String(id), size: size.toJSNumber(), mimeType, timestamp: date, fileName: getFilenameFromDocument(document), thumbnail, - mediaType, + innerMediaType, mediaSize, }; } @@ -432,7 +444,11 @@ function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined { } = media; return { - firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'), + mediaType: 'contact', + firstName, + lastName, + phoneNumber, + userId: buildApiPeerId(userId, 'user'), }; } @@ -470,7 +486,7 @@ function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | u function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined { const point = buildGeoPoint(media.geo); - return point && { type: 'geo', geo: point }; + return point && { mediaType: 'geo', geo: point }; } function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined { @@ -479,7 +495,7 @@ function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined { } = media; const point = buildGeoPoint(geo); return point && { - type: 'venue', + mediaType: 'venue', geo: point, title, provider, @@ -493,7 +509,7 @@ function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefine const { geo, period, heading } = media; const point = buildGeoPoint(geo); return point && { - type: 'geoLive', + mediaType: 'geoLive', geo: point, period, heading, @@ -530,6 +546,7 @@ function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined { const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined; return { + mediaType: 'game', id: id.toString(), accessHash: accessHash.toString(), shortName, @@ -556,6 +573,7 @@ function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefi const channelIds = channels.map((channel) => buildApiPeerId(channel, 'channel')); return { + mediaType: 'giveaway', channelIds, months, quantity, @@ -583,6 +601,7 @@ function buildGiveawayResults(media: GramJs.MessageMediaGiveawayResults): ApiGiv const winnerIds = winners.map((winner) => buildApiPeerId(winner, 'user')); return { + mediaType: 'giveawayResults', months, untilDate, isOnlyForNewSubscribers: onlyNewSubscribers, @@ -604,7 +623,12 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag const peerId = getApiChatIdFromMtpPeer(media.peer); - return { id: media.id, peerId, ...(media.viaMention && { isMention: true }) }; + return { + mediaType: 'storyData', + id: media.id, + peerId, + ...(media.viaMention && { isMention: true }), + }; } export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll { @@ -615,6 +639,7 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A })); return { + mediaType: 'poll', id: String(id), summary: { isPublic: poll.publicVoters, @@ -641,6 +666,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { ? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined; return { + mediaType: 'invoice', title, text, photo: buildApiWebDocument(photo), @@ -722,6 +748,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef } return { + mediaType: 'webpage', id: Number(id), ...pick(media.webpage, [ 'url', @@ -741,6 +768,40 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef }; } +function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined { + if (!(media instanceof GramJs.MessageMediaPaidMedia)) { + return undefined; + } + + const { starsAmount, extendedMedia } = media; + + const isBought = extendedMedia[0] instanceof GramJs.MessageExtendedMedia; + + if (isBought) { + return { + mediaType: 'paidMedia', + starsAmount: starsAmount.toJSNumber(), + isBought, + extendedMedia: extendedMedia + .filter((paidMedia): paidMedia is GramJs.MessageExtendedMedia => ( + paidMedia instanceof GramJs.MessageExtendedMedia + )) + .map((paidMedia) => buildMessageMediaContent(paidMedia.media)) + .filter(Boolean), + }; + } + + return { + mediaType: 'paidMedia', + starsAmount: starsAmount.toJSNumber(), + extendedMedia: extendedMedia + .filter((paidMedia): paidMedia is GramJs.MessageExtendedMediaPreview => ( + paidMedia instanceof GramJs.MessageExtendedMediaPreview + )) + .map((paidMedia) => buildApiMessageExtendedMediaPreview(paidMedia)), + }; +} + function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') { const { mimeType, attributes } = document; const filenameAttribute = attributes @@ -757,12 +818,13 @@ function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file' export function buildApiMessageExtendedMediaPreview( preview: GramJs.MessageExtendedMediaPreview, -): ApiMessageExtendedMediaPreview { +): ApiMediaExtendedPreview { const { w, h, thumb, videoDuration, } = preview; return { + mediaType: 'extendedMediaPreview', width: w, height: h, duration: videoDuration, @@ -783,6 +845,7 @@ export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDo const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h }; return { + mediaType: 'webDocument', url, accessHash, size, @@ -790,3 +853,16 @@ export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDo dimensions, }; } + +export function buildBoughtMediaContent(media: GramJs.TypeMessageExtendedMedia[]): MediaContent[] | undefined { + const boughtMedia = media + .filter((m): m is GramJs.MessageExtendedMedia => m instanceof GramJs.MessageExtendedMedia) + .map((m) => buildMessageMediaContent(m.media)) + .filter(Boolean); + + if (!boughtMedia.length) { + return undefined; + } + + return boughtMedia; +} diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 28cb4f61a..dc94f0464 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -17,6 +17,7 @@ import type { ApiNewPoll, ApiPeer, ApiPhoto, + ApiPoll, ApiQuickReply, ApiReplyInfo, ApiReplyKeyboard, @@ -246,7 +247,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un } const { - message, entities, replyTo, date, + message, entities, replyTo, date, effect, } = draft; const replyInfo = replyTo instanceof GramJs.InputReplyToMessage ? { @@ -261,6 +262,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un text: message ? buildMessageTextContent(message, entities) : undefined, replyInfo, date, + effectId: effect?.toString(), }; } @@ -610,6 +612,7 @@ function buildAction( } return { + mediaType: 'action', text, type, targetUserIds, @@ -777,13 +780,12 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool }; } -function buildNewPoll(poll: ApiNewPoll, localId: number) { +function buildNewPoll(poll: ApiNewPoll, localId: number): ApiPoll { return { - poll: { - id: String(localId), - summary: pick(poll.summary, ['question', 'answers']), - results: {}, - }, + mediaType: 'poll', + id: String(localId), + summary: pick(poll.summary, ['question', 'answers']), + results: {}, }; } @@ -824,9 +826,12 @@ export function buildLocalMessage( ...media, ...(sticker && { sticker }), ...(gif && { video: gif }), - ...(poll && buildNewPoll(poll, localId)), - ...(contact && { contact }), - ...(story && { storyData: story }), + poll: poll && buildNewPoll(poll, localId), + contact, + storyData: story && { + mediaType: 'storyData', + ...story, + }, }, date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(), isOutgoing: !isChannel, @@ -981,10 +986,12 @@ export function buildUploadingMedia( const { width, height } = attachment.quick; return { photo: { + mediaType: 'photo', id: LOCAL_MEDIA_UPLOADING_TEMP_ID, sizes: [], thumbnail: { width, height, dataUri: previewBlobUrl || blobUrl }, blobUrl, + date: Math.round(Date.now() / 1000), isSpoiler: shouldSendAsSpoiler, }, }; @@ -993,6 +1000,7 @@ export function buildUploadingMedia( const { width, height, duration } = attachment.quick; return { video: { + mediaType: 'video', id: LOCAL_MEDIA_UPLOADING_TEMP_ID, mimeType, duration: duration || 0, @@ -1012,9 +1020,11 @@ export function buildUploadingMedia( const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH); return { voice: { + mediaType: 'voice', id: LOCAL_MEDIA_UPLOADING_TEMP_ID, duration, waveform: inputWaveform, + size, }, ttlSeconds, }; @@ -1023,6 +1033,7 @@ export function buildUploadingMedia( const { duration, performer, title } = audio || {}; return { audio: { + mediaType: 'audio', id: LOCAL_MEDIA_UPLOADING_TEMP_ID, mimeType, fileName, @@ -1036,6 +1047,7 @@ export function buildUploadingMedia( } return { document: { + mediaType: 'document', mimeType, fileName, size, diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 02d9d862b..903bfc4b7 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -12,12 +12,13 @@ import type { ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarTopupOption, + BoughtPaidMedia, } from '../../types'; import { addWebDocumentToLocalDb } from '../helpers'; import { buildApiMessageEntity } from './common'; import { omitVirtualClassFields } from './helpers'; -import { buildApiDocument, buildApiWebDocument } from './messageContent'; +import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildPrepaidGiveaway, buildStatisticsPercentage } from './statistics'; @@ -214,6 +215,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.TypePaymentForm): const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0); return { + mediaType: 'invoice', text, title, photo: buildApiWebDocument(photo), @@ -398,6 +400,10 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe return { type: 'fragment' }; } + if (peer instanceof GramJs.StarsTransactionPeerAds) { + return { type: 'ads' }; + } + if (peer instanceof GramJs.StarsTransactionPeer) { return { type: 'peer', id: getApiChatIdFromMtpPeer(peer.peer) }; } @@ -407,13 +413,16 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction { const { - date, id, peer, stars, description, photo, title, refund, + date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, } = transaction; if (photo) { addWebDocumentToLocalDb(photo); } + const boughtExtendedMedia = extendedMedia?.map((m) => buildMessageMediaContent(m)) + .filter(Boolean) as BoughtPaidMedia[]; + return { id, date, @@ -423,6 +432,10 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): description, photo: photo && buildApiWebDocument(photo), isRefund: refund, + hasFailed: failed, + isPending: pending, + messageId: msgId, + extendedMedia: boughtExtendedMedia, }; } diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index dde0947c3..e8de34c78 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -150,7 +150,7 @@ export function buildApiStealthMode(stealthMode: GramJs.TypeStoriesStealthMode): function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinates): ApiMediaAreaCoordinates { const { - x, y, w, h, rotation, + x, y, w, h, rotation, radius, } = coordinates; return { @@ -159,6 +159,7 @@ function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinat width: w, height: h, rotation, + radius, }; } @@ -218,6 +219,16 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un }; } + if (area instanceof GramJs.MediaAreaUrl) { + const { coordinates, url } = area; + + return { + type: 'url', + coordinates: buildApiMediaAreaCoordinates(coordinates), + url, + }; + } + return undefined; } diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index 8a4abd80e..57f1d5402 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -81,6 +81,7 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, .some(({ type }) => type === 'f'); return { + mediaType: 'sticker', id: String(document.id), stickerSetInfo, emoji, diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 3ff36226e..092b6b1b2 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -353,36 +353,6 @@ export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMess } } -export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServiceNotification) { - const { media } = message; - if (!media) { - return false; - } - - return ( - media instanceof GramJs.MessageMediaPhoto - || media instanceof GramJs.MessageMediaDocument - || ( - media instanceof GramJs.MessageMediaWebPage - && media.webpage instanceof GramJs.WebPage - && ( - media.webpage.photo instanceof GramJs.Photo || ( - media.webpage.document instanceof GramJs.Document - ) - ) - ) || ( - media instanceof GramJs.MessageMediaGame - && (media.game.document instanceof GramJs.Document || media.game.photo instanceof GramJs.Photo) - ) || ( - media instanceof GramJs.MessageMediaInvoice && (media.photo || media.extendedMedia) - ) - ); -} - -export function isServiceMessageWithMedia(message: GramJs.MessageService) { - return 'photo' in message.action && message.action.photo instanceof GramJs.Photo; -} - export function buildChatPhotoForLocalDb(photo: GramJs.TypePhoto) { if (photo instanceof GramJs.PhotoEmpty) { return new GramJs.ChatPhotoEmpty(); diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index 50d9dcb4a..bfddc5b3d 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -1,6 +1,6 @@ import { Api as GramJs } from '../../lib/gramjs'; -import type { StoryRepairInfo } from './localDb'; +import type { RepairInfo } from './localDb'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import localDb from './localDb'; @@ -34,57 +34,76 @@ export function isChatFolder( return filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist; } -export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) { - const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`; +export function addMessageToLocalDb(message: GramJs.TypeMessage | GramJs.TypeSponsoredMessage) { + if (message instanceof GramJs.Message) { + if (message.media) addMediaToLocalDb(message.media, message); - let mockMessage = message; - if (message instanceof GramJs.Message - && message.media instanceof GramJs.MessageMediaInvoice - && message.media.extendedMedia instanceof GramJs.MessageExtendedMedia) { - mockMessage = new GramJs.Message({ - ...message, - media: message.media.extendedMedia.media, - }); - } - - localDb.messages[messageFullId] = mockMessage; - - if (mockMessage instanceof GramJs.Message) { - if (mockMessage.media) addMediaToLocalDb(mockMessage.media); - - if (mockMessage.replyTo instanceof GramJs.MessageReplyHeader && mockMessage.replyTo.replyMedia) { - addMediaToLocalDb(mockMessage.replyTo.replyMedia); + if (message.replyTo instanceof GramJs.MessageReplyHeader && message.replyTo.replyMedia) { + addMediaToLocalDb(message.replyTo.replyMedia, message); } } - if (mockMessage instanceof GramJs.MessageService && 'photo' in mockMessage.action) { - addPhotoToLocalDb(mockMessage.action.photo); + if (message instanceof GramJs.MessageService && 'photo' in message.action) { + const photo = addMessageRepairInfo(message.action.photo, message); + addPhotoToLocalDb(photo); + } + + if (message instanceof GramJs.SponsoredMessage && message.photo) { + addPhotoToLocalDb(message.photo); } } -function addMediaToLocalDb(media: GramJs.TypeMessageMedia) { - if (media instanceof GramJs.MessageMediaDocument - && media.document instanceof GramJs.Document - ) { - localDb.documents[String(media.document.id)] = media.document; +export function addMediaToLocalDb(media: GramJs.TypeMessageMedia, message?: GramJs.TypeMessage) { + if (media instanceof GramJs.MessageMediaDocument && media.document) { + const document = addMessageRepairInfo(media.document, message); + addDocumentToLocalDb(document); } if (media instanceof GramJs.MessageMediaWebPage && media.webpage instanceof GramJs.WebPage - && media.webpage.document instanceof GramJs.Document ) { - localDb.documents[String(media.webpage.document.id)] = media.webpage.document; + if (media.webpage.document) { + const document = addMessageRepairInfo(media.webpage.document, message); + addDocumentToLocalDb(document); + } + if (media.webpage.photo) { + const photo = addMessageRepairInfo(media.webpage.photo, message); + addPhotoToLocalDb(photo); + } } if (media instanceof GramJs.MessageMediaGame) { - if (media.game.document instanceof GramJs.Document) { - localDb.documents[String(media.game.document.id)] = media.game.document; + if (media.game.document) { + const document = addMessageRepairInfo(media.game.document, message); + addDocumentToLocalDb(document); } - addPhotoToLocalDb(media.game.photo); + + const photo = addMessageRepairInfo(media.game.photo, message); + addPhotoToLocalDb(photo); } - if (media instanceof GramJs.MessageMediaInvoice && media.photo) { - addWebDocumentToLocalDb(media.photo); + if (media instanceof GramJs.MessageMediaPhoto && media.photo) { + const photo = addMessageRepairInfo(media.photo, message); + addPhotoToLocalDb(photo); + } + + if (media instanceof GramJs.MessageMediaInvoice) { + if (media.photo) { + const photo = addMessageRepairInfo(media.photo, message); + addWebDocumentToLocalDb(photo); + } + + if (media.extendedMedia instanceof GramJs.MessageExtendedMedia) { + addMediaToLocalDb(media.extendedMedia.media, message); + } + } + + if (media instanceof GramJs.MessageMediaPaidMedia) { + media.extendedMedia.forEach((extendedMedia) => { + if (extendedMedia instanceof GramJs.MessageExtendedMedia) { + addMediaToLocalDb(extendedMedia.media, message); + } + }); } } @@ -93,27 +112,20 @@ export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) { return; } - const storyData = { - id: story.id, - peerId, - }; - - if (story.media instanceof GramJs.MessageMediaPhoto) { - const photo = story.media.photo as GramJs.Photo & StoryRepairInfo; - photo.storyData = storyData; + if (story.media instanceof GramJs.MessageMediaPhoto && story.media.photo) { + const photo = addStoryRepairInfo(story.media.photo, peerId, story); addPhotoToLocalDb(photo); } + if (story.media instanceof GramJs.MessageMediaDocument) { if (story.media.document instanceof GramJs.Document) { - const doc = story.media.document as GramJs.Document & StoryRepairInfo; - doc.storyData = storyData; - localDb.documents[String(story.media.document.id)] = doc; + const doc = addStoryRepairInfo(story.media.document, peerId, story); + addDocumentToLocalDb(doc); } if (story.media.altDocument instanceof GramJs.Document) { - const doc = story.media.altDocument as GramJs.Document & StoryRepairInfo; - doc.storyData = storyData; - localDb.documents[String(story.media.altDocument.id)] = doc; + const doc = addStoryRepairInfo(story.media.altDocument, peerId, story); + addDocumentToLocalDb(doc); } } } @@ -124,6 +136,42 @@ export function addPhotoToLocalDb(photo: GramJs.TypePhoto) { } } +export function addDocumentToLocalDb(document: GramJs.TypeDocument) { + if (document instanceof GramJs.Document) { + localDb.documents[String(document.id)] = document; + } +} + +export function addStoryRepairInfo( + media: T, peerId: string, story: GramJs.TypeStoryItem, +) : T & RepairInfo { + if (!(media instanceof GramJs.Document && media instanceof GramJs.Photo)) return media; + const repairableMedia = media as T & RepairInfo; + repairableMedia.localRepairInfo = { + type: 'story', + peerId, + id: story.id, + }; + return repairableMedia; +} + +export function addMessageRepairInfo( + media: T, message?: GramJs.TypeMessage, +) : T & RepairInfo { + if (!message?.peerId) return media; + if (!(media instanceof GramJs.Document && media instanceof GramJs.Photo && media instanceof GramJs.WebDocument)) { + return media; + } + + const repairableMedia = media as T & RepairInfo; + repairableMedia.localRepairInfo = { + type: 'message', + peerId: getApiChatIdFromMtpPeer(message.peerId), + id: message.id, + }; + return repairableMedia; +} + export function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel) { const id = buildApiPeerId(chat.id, chat instanceof GramJs.Chat ? 'chat' : 'channel'); const storedChat = localDb.chats[id]; @@ -138,6 +186,11 @@ export function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel) { export function addUserToLocalDb(user: GramJs.User) { const id = buildApiPeerId(user.id, 'user'); const storedUser = localDb.users[id]; + + if (user.photo instanceof GramJs.Photo) { + addPhotoToLocalDb(user.photo); + } + if (storedUser && !storedUser.min && user.min) return; localDb.users[id] = user; @@ -157,24 +210,6 @@ export function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) { localDb.webDocuments[webDocument.url] = webDocument; } -export function swapLocalInvoiceMedia( - chatId: string, messageId: number, extendedMedia: GramJs.TypeMessageExtendedMedia, -) { - const localMessage = localDb.messages[`${chatId}-${messageId}`]; - if (!(localMessage instanceof GramJs.Message) || !localMessage.media) return; - - if (extendedMedia instanceof GramJs.MessageExtendedMediaPreview) { - if (!(localMessage.media instanceof GramJs.MessageMediaInvoice)) { - return; - } - localMessage.media.extendedMedia = extendedMedia; - } - - if (extendedMedia instanceof GramJs.MessageExtendedMedia) { - localMessage.media = extendedMedia.media; - } -} - export function serializeBytes(value: Buffer) { return String.fromCharCode(...value); } diff --git a/src/api/gramjs/localDb.ts b/src/api/gramjs/localDb.ts index 38964bfce..3fdaf45fe 100644 --- a/src/api/gramjs/localDb.ts +++ b/src/api/gramjs/localDb.ts @@ -11,20 +11,28 @@ import { omitVirtualClassFields } from './apiBuilders/helpers'; const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self; export type StoryRepairInfo = { - storyData?: { - peerId: string; - id: number; - }; + type: 'story'; + peerId: string; + id: number; +}; + +export type MessageRepairInfo = { + type: 'message'; + peerId: string; + id: number; +}; + +export type RepairInfo = { + localRepairInfo?: StoryRepairInfo | MessageRepairInfo; }; export interface LocalDb { // Used for loading avatars and media through in-memory Gram JS instances. chats: Record; users: Record; - messages: Record; - documents: Record; + documents: Record; stickerSets: Record; - photos: Record; + photos: Record; webDocuments: Record; commonBoxState: Record; channelPtsById: Record; diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index e1d2fcc44..2a3ddb7d4 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -34,9 +34,13 @@ import { generateRandomBigInt, } from '../gramjsBuilders'; import { - addEntitiesToLocalDb, addUserToLocalDb, addWebDocumentToLocalDb, deserializeBytes, + addDocumentToLocalDb, + addEntitiesToLocalDb, + addPhotoToLocalDb, + addUserToLocalDb, + addWebDocumentToLocalDb, + deserializeBytes, } from '../helpers'; -import localDb from '../localDb'; import { invokeRequest } from './client'; let onUpdate: OnApiUpdate; @@ -209,7 +213,7 @@ export async function requestWebView({ if (result instanceof GramJs.WebViewResultUrl) { return { url: result.url, - queryId: result.queryId.toString(), + queryId: result.queryId?.toString(), }; } @@ -555,14 +559,6 @@ function getInlineBotResultsNextOffset(username: string, nextOffset?: string) { return username === 'gif' && nextOffset === '0' ? '' : nextOffset; } -function addDocumentToLocalDb(document: GramJs.Document) { - localDb.documents[String(document.id)] = document; -} - -function addPhotoToLocalDb(photo: GramJs.Photo) { - localDb.photos[String(photo.id)] = photo; -} - export function setBotInfo({ bot, langCode, diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 52173e2e7..b0d756008 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -65,7 +65,6 @@ import { buildInputReplyTo, buildMtpMessageEntity, generateRandomBigInt, - isMessageWithMedia, } from '../gramjsBuilders'; import { addEntitiesToLocalDb, @@ -74,7 +73,6 @@ import { deserializeBytes, isChatFolder, } from '../helpers'; -import localDb from '../localDb'; import { scheduleMutedChatUpdate } from '../scheduleUnmute'; import { applyState, processAffectedHistory, updateChannelState, @@ -518,8 +516,8 @@ async function getFullChatInfo(chatId: string): Promise { - if (message instanceof GramJs.Message && isMessageWithMedia(message)) { - addMessageToLocalDb(message); - } + addMessageToLocalDb(message); }); } } diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index f1ab980bd..0a03c4932 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -20,15 +20,15 @@ import { DEBUG, DEBUG_GRAMJS, IS_TEST, UPLOAD_WORKERS, } from '../../../config'; import { pause } from '../../../util/schedulers'; -import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; +import { buildApiMessage, setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; import { buildApiPeerId } from '../apiBuilders/peers'; +import { buildApiStory } from '../apiBuilders/stories'; import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users'; import { buildInputPeerFromLocalDb } from '../gramjsBuilders'; import { - addEntitiesToLocalDb, - addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log, + addEntitiesToLocalDb, addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log, } from '../helpers'; -import localDb, { clearLocalDb } from '../localDb'; +import localDb, { clearLocalDb, type RepairInfo } from '../localDb'; import { getDifference, init as initUpdatesManager, @@ -381,9 +381,6 @@ export async function fetchCurrentUser() { const user = userFull.users[0]; - if (user.photo instanceof GramJs.Photo) { - localDb.photos[user.photo.id.toString()] = user.photo; - } addUserToLocalDb(user); const currentUserFullInfo = buildApiUserFullInfo(userFull); const currentUser = buildApiUser(user)!; @@ -441,62 +438,100 @@ export async function repairFileReference({ if (!parsed) return undefined; const { - entityType, entityId, mediaMatchType, + entityId, mediaMatchType, } = parsed; - if (mediaMatchType === 'document' || mediaMatchType === 'photo') { - const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId]; - if (!entity.storyData) return false; - const peer = buildInputPeerFromLocalDb(entity.storyData.peerId); - if (!peer) return false; + if (mediaMatchType === 'document' || mediaMatchType === 'photo' || mediaMatchType === 'webDocument') { + const entity = mediaMatchType === 'document' + ? localDb.documents[entityId] : mediaMatchType === 'webDocument' + ? localDb.webDocuments[entityId] : localDb.photos[entityId]; + if (!entity) return false; + const repairableEntity = entity as RepairInfo; + if (!repairableEntity.localRepairInfo) return false; + const { localRepairInfo } = repairableEntity; - const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ - peer, - id: [entity.storyData.id], - })); - if (!result) return false; - - addEntitiesToLocalDb(result.users); - result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.peerId)); - return true; - } - - if (entityType === 'msg') { - const entity = localDb.messages[entityId]!; - const messageId = entity.id; - - const peer = 'channelId' in entity.peerId ? new GramJs.InputChannel({ - channelId: entity.peerId.channelId, - accessHash: (localDb.chats[buildApiPeerId(entity.peerId.channelId, 'channel')] as GramJs.Channel).accessHash!, - }) : undefined; - const result = await invokeRequest( - peer - ? new GramJs.channels.GetMessages({ - channel: peer, - id: [new GramJs.InputMessageID({ id: messageId })], - }) - : new GramJs.messages.GetMessages({ - id: [new GramJs.InputMessageID({ id: messageId })], - }), - ); - - if (!result || result instanceof GramJs.messages.MessagesNotModified) return false; - - if (peer && 'pts' in result) { - updateChannelState(buildApiPeerId(peer.channelId, 'channel'), result.pts); + if (localRepairInfo.type === 'story') { + const result = await repairStoryMedia(localRepairInfo.peerId, localRepairInfo.id); + return result; } - const message = result.messages[0]; - if (message instanceof GramJs.MessageEmpty) return false; - addEntitiesToLocalDb(result.users); - addEntitiesToLocalDb(result.chats); - addMessageToLocalDb(message); - return true; + if (localRepairInfo.type === 'message') { + const result = await repairMessageMedia(localRepairInfo.peerId, localRepairInfo.id); + return result; + } } return false; } +async function repairMessageMedia(peerId: string, messageId: number) { + const peer = buildInputPeerFromLocalDb(peerId); + if (!peer) return false; + const result = await invokeRequest( + peer + ? new GramJs.channels.GetMessages({ + channel: peer, + id: [new GramJs.InputMessageID({ id: messageId })], + }) + : new GramJs.messages.GetMessages({ + id: [new GramJs.InputMessageID({ id: messageId })], + }), + { + shouldIgnoreErrors: true, + }, + ); + + if (!result || result instanceof GramJs.messages.MessagesNotModified) return false; + + if (peer && 'pts' in result) { + updateChannelState(peerId, result.pts); + } + + const message = result.messages[0]; + if (message instanceof GramJs.MessageEmpty) return false; + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); + addMessageToLocalDb(message); + + const apiMessage = buildApiMessage(message); + if (apiMessage) { + onUpdate({ + '@type': 'updateMessage', + chatId: apiMessage.chatId, + id: apiMessage.id, + message: apiMessage, + }); + } + return true; +} + +async function repairStoryMedia(peerId: string, storyId: number) { + const peer = buildInputPeerFromLocalDb(peerId); + if (!peer) return false; + + const result = await invokeRequest(new GramJs.stories.GetStoriesByID({ + peer, + id: [storyId], + }), { + shouldIgnoreErrors: true, + }); + if (!result) return false; + + addEntitiesToLocalDb(result.users); + result.stories.forEach((story) => { + addStoryToLocalDb(story, peerId); + + const apiStory = buildApiStory(peerId, story); + if (!apiStory || 'isDeleted' in apiStory) return; + onUpdate({ + '@type': 'updateStory', + peerId, + story: apiStory, + }); + }); + return true; +} + export function setForceHttpTransport(forceHttpTransport: boolean) { client.setForceHttpTransport(forceHttpTransport); } diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index f6845818d..21796638f 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -17,11 +17,12 @@ import * as cacheApi from '../../../util/cacheApi'; import { getEntityTypeById } from '../gramjsBuilders'; import localDb from '../localDb'; -const MEDIA_ENTITY_TYPES = new Set([ - 'msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document', 'videoAvatar', +const MEDIA_ENTITY_TYPES: Set = new Set([ + 'sticker', 'wallpaper', 'photo', 'webDocument', 'document', ]); const JPEG_SIZE_TYPES = new Set(['s', 'm', 'x', 'y', 'w', 'a', 'b', 'c', 'd']); +const MP4_SIZES_TYPES = new Set(['u', 'v']); export default async function downloadMedia( { @@ -66,8 +67,8 @@ export default async function downloadMedia( } export type EntityType = ( - 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' | - 'document' | 'staticMap' | 'videoAvatar' + 'sticker' | 'wallpaper' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' | + 'document' | 'staticMap' ); async function download( @@ -118,15 +119,11 @@ async function download( case 'user': entity = localDb.users[entityId]; break; - case 'msg': - entity = localDb.messages[entityId]; - break; case 'sticker': - case 'gif': case 'wallpaper': + case 'document': entity = localDb.documents[entityId]; break; - case 'videoAvatar': case 'photo': entity = localDb.photos[entityId]; break; @@ -136,9 +133,6 @@ async function download( case 'webDocument': entity = localDb.webDocuments[entityId]; break; - case 'document': - entity = localDb.documents[entityId]; - break; } if (!entity) { @@ -152,36 +146,18 @@ async function download( let mimeType; let fullSize; - if (entity instanceof GramJs.MessageService && entity.action instanceof GramJs.MessageActionSuggestProfilePhoto) { + if (sizeType && JPEG_SIZE_TYPES.has(sizeType)) { mimeType = 'image/jpeg'; - } else if (entity instanceof GramJs.Message) { - mimeType = getMessageMediaMimeType(entity, sizeType); - if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) { - fullSize = entity.media.document.size.toJSNumber(); - } - if (entity.media instanceof GramJs.MessageMediaWebPage - && entity.media.webpage instanceof GramJs.WebPage - && entity.media.webpage.document instanceof GramJs.Document) { - fullSize = entity.media.webpage.document.size.toJSNumber(); - } + } else if (sizeType && MP4_SIZES_TYPES.has(sizeType)) { + mimeType = 'video/mp4'; } else if (entity instanceof GramJs.Photo) { - if (entityType === 'videoAvatar') { - mimeType = 'video/mp4'; - } else { - mimeType = 'image/jpeg'; - } - } else if (entityType === 'sticker' && sizeType) { - mimeType = (entity as GramJs.Document).mimeType; - } else if (entityType === 'webDocument') { - mimeType = (entity as GramJs.TypeWebDocument).mimeType; - fullSize = (entity as GramJs.TypeWebDocument).size; - } else { - if (JPEG_SIZE_TYPES.has(sizeType || '')) { - mimeType = 'image/jpeg'; - } else { - mimeType = (entity as GramJs.Document).mimeType; - } - fullSize = (entity as GramJs.Document).size.toJSNumber(); + mimeType = 'image/jpeg'; + } else if (entity instanceof GramJs.WebDocument) { + mimeType = entity.mimeType; + fullSize = entity.size; + } else if (entity instanceof GramJs.Document) { + mimeType = entity.mimeType; + fullSize = entity.size.toJSNumber(); } // Prevent HTML-in-video attacks @@ -203,41 +179,6 @@ async function download( } } -function getMessageMediaMimeType(message: GramJs.Message, sizeType?: string) { - if (!message || !message.media) { - return undefined; - } - - if (message.media instanceof GramJs.MessageMediaPhoto) { - return 'image/jpeg'; - } - - if (message.media instanceof GramJs.MessageMediaGeo - || message.media instanceof GramJs.MessageMediaVenue - || message.media instanceof GramJs.MessageMediaGeoLive) { - return 'image/png'; - } - - if (message.media instanceof GramJs.MessageMediaDocument) { - const document = message.media.document; - if (document instanceof GramJs.Document) { - return document.mimeType; - } - } - - if (message.media instanceof GramJs.MessageMediaWebPage - && message.media.webpage instanceof GramJs.WebPage - && message.media.webpage.document instanceof GramJs.Document) { - if (sizeType) { - return 'image/jpeg'; - } - - return message.media.webpage.document.mimeType; - } - - return undefined; -} - // eslint-disable-next-line no-async-without-await/no-async-without-await async function parseMedia( data: Buffer, mediaFormat: ApiMediaFormat, mimeType?: string, @@ -294,7 +235,7 @@ export function parseMediaUrl(url: string) { ? url.match(/(webDocument):(.+)/) : url.match( // eslint-disable-next-line max-len - /(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|document|videoAvatar)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/, + /(avatar|profile|photo|stickerSet|sticker|wallpaper|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/, ); if (!mediaMatch) { return undefined; diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 2751dc7a6..b982fe88a 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -77,8 +77,6 @@ import { buildSendMessageAction, generateRandomBigInt, getEntityTypeById, - isMessageWithMedia, - isServiceMessageWithMedia, } from '../gramjsBuilders'; import { addEntitiesToLocalDb, @@ -239,9 +237,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message return undefined; } - if (mtpMessage instanceof GramJs.Message) { - addMessageToLocalDb(mtpMessage); - } + addMessageToLocalDb(mtpMessage); const users = result.users.map(buildApiUser).filter(Boolean); @@ -1576,11 +1572,7 @@ function updateLocalDb(result: ( addEntitiesToLocalDb(result.chats); result.messages.forEach((message) => { - if ((message instanceof GramJs.Message && isMessageWithMedia(message)) - || (message instanceof GramJs.MessageService && isServiceMessageWithMedia(message)) - ) { - addMessageToLocalDb(message); - } + addMessageToLocalDb(message); }); } @@ -1924,9 +1916,7 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU } const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate); - if (isMessageWithMedia(mtpMessage)) { - addMessageToLocalDb(mtpMessage); - } + addMessageToLocalDb(mtpMessage); } // Edge case for "Send When Online" diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 1cb6c8f3f..ee104ef4f 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiChat, ApiInputStorePaymentPurpose, ApiPeer, ApiRequestInputInvoice, + ApiThemeParameters, OnApiUpdate, } from '../../types'; @@ -25,7 +26,7 @@ import { } from '../apiBuilders/payments'; import { buildApiUser } from '../apiBuilders/users'; import { - buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildShippingInfo, + buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo, } from '../gramjsBuilders'; import { addEntitiesToLocalDb, @@ -156,9 +157,10 @@ export async function sendStarPaymentForm({ return Boolean(result); } -export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) { +export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) { const result = await invokeRequest(new GramJs.payments.GetPaymentForm({ invoice: buildInputInvoice(inputInvoice), + themeParams: theme ? buildInputThemeParams(theme) : undefined, })); if (!result) { diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 682eecf67..f48929458 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -52,21 +52,21 @@ export async function fetchFullUser({ addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.chats); - if (result.fullUser.profilePhoto instanceof GramJs.Photo) { - localDb.photos[result.fullUser.profilePhoto.id.toString()] = result.fullUser.profilePhoto; + if (result.fullUser.profilePhoto) { + addPhotoToLocalDb(result.fullUser.profilePhoto); } - if (result.fullUser.personalPhoto instanceof GramJs.Photo) { - localDb.photos[result.fullUser.personalPhoto.id.toString()] = result.fullUser.personalPhoto; + if (result.fullUser.personalPhoto) { + addPhotoToLocalDb(result.fullUser.personalPhoto); } - if (result.fullUser.fallbackPhoto instanceof GramJs.Photo) { - localDb.photos[result.fullUser.fallbackPhoto.id.toString()] = result.fullUser.fallbackPhoto; + if (result.fullUser.fallbackPhoto) { + addPhotoToLocalDb(result.fullUser.fallbackPhoto); } const botInfo = result.fullUser.botInfo; - if (botInfo?.descriptionPhoto instanceof GramJs.Photo) { - localDb.photos[botInfo.descriptionPhoto.id.toString()] = botInfo.descriptionPhoto; + if (botInfo?.descriptionPhoto) { + addPhotoToLocalDb(botInfo.descriptionPhoto); } if (botInfo?.descriptionDocument instanceof GramJs.Document) { localDb.documents[botInfo.descriptionDocument.id.toString()] = botInfo.descriptionDocument; diff --git a/src/api/gramjs/updates/updater.ts b/src/api/gramjs/updates/updater.ts index e56826805..659d41ab3 100644 --- a/src/api/gramjs/updates/updater.ts +++ b/src/api/gramjs/updates/updater.ts @@ -2,8 +2,8 @@ import { Api as GramJs, connection } from '../../../lib/gramjs'; import type { GroupCallConnectionData } from '../../../lib/secret-sauce'; import type { - ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped, - ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate, + ApiMessage, ApiStory, ApiStorySkipped, + ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate, } from '../../types'; import { DEBUG, GENERAL_TOPIC_ID } from '../../../config'; @@ -29,7 +29,7 @@ import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuild import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { buildApiMessageExtendedMediaPreview, - buildMessageMediaContent, + buildBoughtMediaContent, buildPoll, buildPollResults, } from '../apiBuilders/messageContent'; @@ -61,7 +61,6 @@ import { import { buildChatPhotoForLocalDb, buildMessageFromUpdate, - isMessageWithMedia, } from '../gramjsBuilders'; import { addEntitiesToLocalDb, @@ -72,7 +71,6 @@ import { log, resolveMessageApiChatId, serializeBytes, - swapLocalInvoiceMedia, } from '../helpers'; import localDb from '../localDb'; import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from '../scheduleUnmute'; @@ -84,8 +82,6 @@ export type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } ) | typeof connection.UpdateConnectionState | UpdatePts | LocalUpdatePremiumFloodWait; -const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000; - let onUpdate: OnApiUpdate; export function init(_onUpdate: OnApiUpdate) { @@ -205,12 +201,7 @@ export function updater(update: Update) { return; } - if ((update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) - || (update.message instanceof GramJs.MessageService - && update.message.action instanceof GramJs.MessageActionSuggestProfilePhoto) - ) { - addMessageToLocalDb(update.message); - } + addMessageToLocalDb(update.message); message = buildApiMessage(update.message)!; dispatchThreadInfoUpdates([update.message]); @@ -385,9 +376,7 @@ export function updater(update: Update) { return; } - if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) { - addMessageToLocalDb(update.message); - } + addMessageToLocalDb(update.message); // Workaround for a weird server behavior when own message is marked as incoming const message = omit(buildApiMessage(update.message)!, ['isOutgoing']); @@ -407,28 +396,35 @@ export function updater(update: Update) { reactions: buildMessageReactions(update.reactions), }); } else if (update instanceof GramJs.UpdateMessageExtendedMedia) { - let media: MediaContent | undefined; - if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) { - media = buildMessageMediaContent(update.extendedMedia.media); - } - - let preview: ApiMessageExtendedMediaPreview | undefined; - if (update.extendedMedia instanceof GramJs.MessageExtendedMediaPreview) { - preview = buildApiMessageExtendedMediaPreview(update.extendedMedia); - } - - if (!media && !preview) return; - const chatId = getApiChatIdFromMtpPeer(update.peer); + const isBought = update.extendedMedia[0] instanceof GramJs.MessageExtendedMedia; + if (isBought) { + const boughtMedia = buildBoughtMediaContent(update.extendedMedia); - swapLocalInvoiceMedia(chatId, update.msgId, update.extendedMedia); + if (!boughtMedia?.length) return; + + onUpdate({ + '@type': 'updateMessageExtendedMedia', + id: update.msgId, + chatId, + isBought, + extendedMedia: boughtMedia, + }); + return; + } + + const previewMedia = !isBought ? update.extendedMedia + .filter((m): m is GramJs.MessageExtendedMediaPreview => m instanceof GramJs.MessageExtendedMediaPreview) + .map((m) => buildApiMessageExtendedMediaPreview(m)) + .filter(Boolean) : undefined; + + if (!previewMedia?.length) return; onUpdate({ '@type': 'updateMessageExtendedMedia', id: update.msgId, chatId, - media, - preview, + extendedMedia: previewMedia, }); } else if (update instanceof GramJs.UpdateDeleteMessages) { onUpdate({ @@ -443,43 +439,12 @@ export function updater(update: Update) { }); } else if (update instanceof GramJs.UpdateDeleteChannelMessages) { const chatId = buildApiPeerId(update.channelId, 'channel'); - const ids = update.messages; - const existingIds = ids.filter((id) => localDb.messages[`${chatId}-${id}`]); - const missingIds = ids.filter((id) => !localDb.messages[`${chatId}-${id}`]); - const profilePhotoIds = ids.map((id) => { - const message = localDb.messages[`${chatId}-${id}`]; - return message && message instanceof GramJs.MessageService && 'photo' in message.action - ? String(message.action.photo.id) - : undefined; - }).filter(Boolean); - - if (existingIds.length) { - onUpdate({ - '@type': 'deleteMessages', - ids: existingIds, - chatId, - }); - } - - if (profilePhotoIds.length) { - onUpdate({ - '@type': 'deleteProfilePhotos', - ids: profilePhotoIds, - chatId, - }); - } - - // For some reason delete message update sometimes comes before new message update - if (missingIds.length) { - setTimeout(() => { - onUpdate({ - '@type': 'deleteMessages', - ids: missingIds, - chatId, - }); - }, DELETE_MISSING_CHANNEL_MESSAGE_DELAY); - } + onUpdate({ + '@type': 'deleteMessages', + ids: update.messages, + chatId, + }); } else if (update instanceof GramJs.UpdateServiceNotification) { if (update.popup) { onUpdate({ @@ -492,9 +457,7 @@ export function updater(update: Update) { const currentDate = Date.now() / 1000 + getServerTimeOffset(); const message = buildApiMessageFromNotification(update, currentDate); - if (isMessageWithMedia(update)) { - addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update)); - } + addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update)); onUpdate({ '@type': 'updateServiceNotification', diff --git a/src/api/gramjs/worker/connector.ts b/src/api/gramjs/worker/connector.ts index f1d6fa8a3..94cc9ba92 100644 --- a/src/api/gramjs/worker/connector.ts +++ b/src/api/gramjs/worker/connector.ts @@ -30,7 +30,6 @@ const requestStatesByCallback = new Map(); const savedLocalDb: LocalDb = { chats: {}, users: {}, - messages: {}, documents: {}, stickerSets: {}, photos: {}, diff --git a/src/api/types/bots.ts b/src/api/types/bots.ts index 226c69a3b..127aa56bf 100644 --- a/src/api/types/bots.ts +++ b/src/api/types/bots.ts @@ -9,6 +9,7 @@ export type ApiInlineResultType = ( ); export interface ApiWebDocument { + mediaType: 'webDocument'; url: string; size: number; mimeType: string; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 0f8acf3bf..4e658527a 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -11,7 +11,7 @@ export interface ApiDimensions { } export interface ApiPhotoSize extends ApiDimensions { - type: 's' | 'm' | 'x' | 'y' | 'z'; + type: 's' | 'm' | 'x' | 'y' | 'w'; } export interface ApiVideoSize extends ApiDimensions { @@ -25,7 +25,9 @@ export interface ApiThumbnail extends ApiDimensions { } export interface ApiPhoto { + mediaType: 'photo'; id: string; + date: number; thumbnail?: ApiThumbnail; isVideo?: boolean; sizes: ApiPhotoSize[]; @@ -35,6 +37,7 @@ export interface ApiPhoto { } export interface ApiSticker { + mediaType: 'sticker'; id: string; stickerSetInfo: ApiStickerSetInfo; emoji?: string; @@ -85,6 +88,7 @@ type ApiStickerSetInfoMissing = { export type ApiStickerSetInfo = ApiStickerSetInfoShortName | ApiStickerSetInfoId | ApiStickerSetInfoMissing; export interface ApiVideo { + mediaType: 'video'; id: string; mimeType: string; duration: number; @@ -103,6 +107,7 @@ export interface ApiVideo { } export interface ApiAudio { + mediaType: 'audio'; id: string; size: number; mimeType: string; @@ -114,12 +119,15 @@ export interface ApiAudio { } export interface ApiVoice { + mediaType: 'voice'; id: string; duration: number; waveform?: number[]; + size: number; } export interface ApiDocument { + mediaType: 'document'; id?: string; fileName: string; size: number; @@ -127,17 +135,29 @@ export interface ApiDocument { mimeType: string; thumbnail?: ApiThumbnail; previewBlobUrl?: string; - mediaType?: 'photo' | 'video'; + innerMediaType?: 'photo' | 'video'; mediaSize?: ApiDimensions; } export interface ApiContact { + mediaType: 'contact'; firstName: string; lastName: string; phoneNumber: string; userId: string; } +export type ApiPaidMedia = { + mediaType: 'paidMedia'; + starsAmount: number; +} & ({ + isBought?: true; + extendedMedia: BoughtPaidMedia[]; +} | { + isBought?: undefined; + extendedMedia: ApiMediaExtendedPreview[]; +}); + export interface ApiPollAnswer { text: ApiFormattedText; option: string; @@ -151,6 +171,7 @@ export interface ApiPollResult { } export interface ApiPoll { + mediaType: 'poll'; id: string; summary: { closed?: true; @@ -243,6 +264,7 @@ export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestI | ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars; export interface ApiInvoice { + mediaType: 'invoice'; text: string; title: string; photo?: ApiWebDocument; @@ -252,12 +274,13 @@ export interface ApiInvoice { isTest?: boolean; isRecurring?: boolean; termsUrl?: string; - extendedMedia?: ApiMessageExtendedMediaPreview; + extendedMedia?: ApiMediaExtendedPreview; maxTipAmount?: number; suggestedTipAmounts?: number[]; } -export interface ApiMessageExtendedMediaPreview { +export interface ApiMediaExtendedPreview { + mediaType: 'extendedMediaPreview'; width?: number; height?: number; thumbnail?: ApiThumbnail; @@ -277,12 +300,12 @@ export interface ApiGeoPoint { } interface ApiGeo { - type: 'geo'; + mediaType: 'geo'; geo: ApiGeoPoint; } interface ApiVenue { - type: 'venue'; + mediaType: 'venue'; geo: ApiGeoPoint; title: string; address: string; @@ -292,7 +315,7 @@ interface ApiVenue { } interface ApiGeoLive { - type: 'geoLive'; + mediaType: 'geoLive'; geo: ApiGeoPoint; heading?: number; period: number; @@ -301,6 +324,7 @@ interface ApiGeoLive { export type ApiLocation = ApiGeo | ApiVenue | ApiGeoLive; export type ApiGame = { + mediaType: 'game'; title: string; description: string; photo?: ApiPhoto; @@ -311,6 +335,7 @@ export type ApiGame = { }; export type ApiGiveaway = { + mediaType: 'giveaway'; quantity: number; months: number; untilDate: number; @@ -321,6 +346,7 @@ export type ApiGiveaway = { }; export type ApiGiveawayResults = { + mediaType: 'giveawayResults'; months: number; untilDate: number; isRefunded?: true; @@ -344,6 +370,7 @@ export type ApiNewPoll = { }; export interface ApiAction { + mediaType: 'action'; text: string; targetUserIds?: string[]; targetChatId?: string; @@ -378,6 +405,7 @@ export interface ApiAction { } export interface ApiWebPage { + mediaType: 'webpage'; id: number; url: string; displayUrl: string; @@ -546,6 +574,7 @@ export type MediaContent = { storyData?: ApiMessageStoryData; giveaway?: ApiGiveaway; giveawayResults?: ApiGiveawayResults; + paidMedia?: ApiPaidMedia; isExpiredVoice?: boolean; isExpiredRoundVideo?: boolean; ttlSeconds?: number; @@ -554,6 +583,8 @@ export type MediaContainer = { content: MediaContent; }; +export type BoughtPaidMedia = Pick; + export interface ApiMessage { id: number; chatId: string; @@ -841,6 +872,7 @@ export type ApiThemeParameters = { section_header_text_color: string; subtitle_text_color: string; destructive_text_color: string; + section_separator_color: string; }; export type ApiBotApp = { diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index d7716bed1..f44b40836 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -2,7 +2,9 @@ import type { ApiPremiumSection } from '../../global/types'; import type { ApiInvoiceContainer } from '../../types'; import type { ApiWebDocument } from './bots'; import type { ApiChat } from './chats'; -import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages'; +import type { + ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia, MediaContent, +} from './messages'; import type { PrepaidGiveaway, StatisticsOverviewPercentage } from './statistics'; import type { ApiUser } from './users'; @@ -67,9 +69,11 @@ export interface ApiReceiptStars { title?: string; text?: string; photo?: ApiWebDocument; + media?: BoughtPaidMedia[]; currency: string; totalAmount: number; transactionId: string; + messageId?: number; } export interface ApiReceiptRegular { @@ -227,6 +231,10 @@ export interface ApiStarsTransactionPeerFragment { type: 'fragment'; } +export interface ApiStarsTransactionPeerAds { + type: 'ads'; +} + export interface ApiStarsTransactionPeerPeer { type: 'peer'; id: string; @@ -238,17 +246,22 @@ export type ApiStarsTransactionPeer = | ApiStarsTransactionPeerPlayMarket | ApiStarsTransactionPeerPremiumBot | ApiStarsTransactionPeerFragment +| ApiStarsTransactionPeerAds | ApiStarsTransactionPeerPeer; export interface ApiStarsTransaction { id: string; peer: ApiStarsTransactionPeer; + messageId?: number; stars: number; isRefund?: true; + hasFailed?: true; + isPending?: true; date: number; title?: string; description?: string; photo?: ApiWebDocument; + extendedMedia?: MediaContent[]; } export interface ApiStarTopupOption { diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index a3f84e8a4..4aa0e1118 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -66,6 +66,7 @@ export type ApiPeerStories = { }; export type ApiMessageStoryData = { + mediaType: 'storyData'; id: number; peerId: string; isMention?: boolean; @@ -124,6 +125,7 @@ export type ApiMediaAreaCoordinates = { width: number; height: number; rotation: number; + radius?: number; }; export type ApiMediaAreaVenue = { @@ -154,5 +156,11 @@ export type ApiMediaAreaChannelPost = { messageId: number; }; +export type ApiMediaAreaUrl = { + type: 'url'; + coordinates: ApiMediaAreaCoordinates; + url: string; +}; + export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction -| ApiMediaAreaChannelPost; +| ApiMediaAreaChannelPost | ApiMediaAreaUrl; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 8056071f1..3fd06a97c 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -21,8 +21,8 @@ import type { import type { ApiFormattedText, ApiInputInvoice, + ApiMediaExtendedPreview, ApiMessage, - ApiMessageExtendedMediaPreview, ApiPhoto, ApiPoll, ApiQuickReply, @@ -30,7 +30,7 @@ import type { ApiReactions, ApiStickerSet, ApiThreadInfo, - MediaContent, + BoughtPaidMedia, } from './messages'; import type { ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, @@ -344,12 +344,6 @@ export type ApiUpdateDeleteSavedHistory = { chatId: string; }; -export type ApiUpdateDeleteProfilePhotos = { - '@type': 'deleteProfilePhotos'; - ids: string[]; - chatId: string; -}; - export type ApiUpdateResetMessages = { '@type': 'resetMessages'; id: string; @@ -373,9 +367,13 @@ export type ApiUpdateMessageExtendedMedia = { '@type': 'updateMessageExtendedMedia'; id: number; chatId: string; - media?: MediaContent; - preview?: ApiMessageExtendedMediaPreview; -}; +} & ({ + isBought?: true; + extendedMedia: BoughtPaidMedia[]; +} | { + isBought?: undefined; + extendedMedia: ApiMediaExtendedPreview[]; +}); export type ApiDeleteContact = { '@type': 'deleteContact'; @@ -753,7 +751,7 @@ export type ApiUpdate = ( ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfos | ApiUpdateCommonBoxMessages | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification | - ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos | + ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction | ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder | diff --git a/src/assets/font-icons/stars-lock.svg b/src/assets/font-icons/stars-lock.svg new file mode 100644 index 000000000..90bfe11fe --- /dev/null +++ b/src/assets/font-icons/stars-lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/premium/PremiumEffects.svg b/src/assets/premium/PremiumEffects.svg new file mode 100644 index 000000000..205bc60e5 --- /dev/null +++ b/src/assets/premium/PremiumEffects.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/common/AnimatedIconFromSticker.tsx b/src/components/common/AnimatedIconFromSticker.tsx index 9ccf70383..40d761421 100644 --- a/src/components/common/AnimatedIconFromSticker.tsx +++ b/src/components/common/AnimatedIconFromSticker.tsx @@ -4,7 +4,7 @@ import type { ApiSticker } from '../../api/types'; import type { OwnProps as AnimatedIconProps } from './AnimatedIcon'; import { ApiMediaFormat } from '../../api/types'; -import { getStickerPreviewHash } from '../../global/helpers'; +import { getStickerMediaHash } from '../../global/helpers'; import useMedia from '../../hooks/useMedia'; @@ -22,7 +22,7 @@ function AnimatedIconFromSticker(props: OwnProps) { const thumbDataUri = sticker?.thumbnail?.dataUri; const localMediaHash = sticker && `sticker${sticker.id}`; const previewBlobUrl = useMedia( - sticker ? getStickerPreviewHash(sticker.id) : undefined, + sticker ? getStickerMediaHash(sticker, 'preview') : undefined, noLoad && !forcePreview, ApiMediaFormat.BlobUrl, ); diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index 963216a0e..32851fa4f 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -13,9 +13,9 @@ import { AudioOrigin } from '../../types'; import { getMediaDuration, + getMediaFormat, + getMediaHash, getMediaTransferState, - getMessageMediaFormat, - getMessageMediaHash, getMessageWebPageAudio, hasMessageTtl, isMessageLocal, @@ -72,7 +72,7 @@ type OwnProps = { onPause?: NoneToVoidFunction; onReadMedia?: () => void; onCancelUpload?: () => void; - onDateClick?: (messageId: number, chatId: string) => void; + onDateClick?: (arg: ApiMessage) => void; }; export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)'); @@ -108,7 +108,7 @@ const Audio: FC = ({ onDateClick, }) => { const { - cancelMessageMediaDownload, downloadMessageMedia, transcribeAudio, openOneTimeMediaModal, + cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal, } = getActions(); const { @@ -117,6 +117,7 @@ const Audio: FC = ({ }, isMediaUnread, } = message; const audio = contentAudio || getMessageWebPageAudio(message); + const media = (voice || video || audio)!; const isVoice = Boolean(voice || video); const isSeeking = useRef(false); // eslint-disable-next-line no-null/no-null @@ -127,22 +128,22 @@ const Audio: FC = ({ const { isMobile } = useAppLayout(); const [isActivated, setIsActivated] = useState(false); const shouldLoad = isActivated || PRELOAD; - const coverHash = getMessageMediaHash(message, 'pictogram'); + const coverHash = getMediaHash(media, 'pictogram'); const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl); const hasTtl = hasMessageTtl(message); const isInOneTimeModal = origin === AudioOrigin.OneTimeModal; const trackType = isVoice ? (hasTtl ? 'oneTimeVoice' : 'voice') : 'audio'; const mediaData = useMedia( - getMessageMediaHash(message, 'inline'), + getMediaHash(media, 'inline'), !shouldLoad, - getMessageMediaFormat(message, 'inline'), + getMediaFormat(media, 'inline'), ); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( - getMessageMediaHash(message, 'download'), + getMediaHash(media, 'download'), !isDownloading, - getMessageMediaFormat(message, 'download'), + getMediaFormat(media, 'download'), ); const handleForcePlay = useLastCallback(() => { @@ -204,7 +205,6 @@ const Audio: FC = ({ const { isUploading, isTransferring, transferProgress, } = getMediaTransferState( - message, uploadProgress || downloadProgress, isLoadingForPlaying || isDownloading, uploadProgress !== undefined, @@ -246,9 +246,9 @@ const Audio: FC = ({ const handleDownloadClick = useLastCallback(() => { if (isDownloading) { - cancelMessageMediaDownload({ message }); + cancelMediaDownload({ media }); } else { - downloadMessageMedia({ message }); + downloadMedia({ media }); } }); @@ -273,7 +273,7 @@ const Audio: FC = ({ }); const handleDateClick = useLastCallback(() => { - onDateClick!(message.id, message.chatId); + onDateClick!(message); }); const handleTranscribe = useLastCallback(() => { diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 20f5e80e8..3dfece0d0 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -17,6 +17,7 @@ import { getChatTitle, getPeerStoryHtmlId, getUserFullName, + getVideoAvatarMediaHash, getWebDocumentHash, isAnonymousForwardsChat, isChatWithRepliesBot, @@ -120,7 +121,7 @@ const Avatar: FC = ({ } else if (photo) { imageHash = `photo${photo.id}?size=m`; if (photo.isVideo && withVideo) { - videoHash = `videoAvatar${photo.id}?size=u`; + videoHash = getVideoAvatarMediaHash(photo); } } else if (webPhoto) { imageHash = getWebDocumentHash(webPhoto); diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index 3c04cc7d7..1d536ce9e 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -31,11 +31,24 @@ } .effect-icon { - font-size: 1.25rem; - position: absolute; - right: 0; + display: grid; + width: 1.5rem; + height: 1.5rem; + place-items: center; + font-size: 1rem; + line-height: 1; + position: absolute; + right: -0.25rem; + bottom: -0.25rem; + + background-color: var(--color-background); + border: 1px solid var(--color-borders); color: var(--color-text); + + border-radius: 50%; + cursor: var(--custom-cursor, pointer); + & > .emoji { width: 1rem !important; height: 1rem !important; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index cb84aa9fc..1e8f177de 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -93,6 +93,8 @@ import { import { selectCurrentLimit } from '../../global/selectors/limits'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates/dateFormat'; +import { processDeepLink } from '../../util/deeplink'; +import { tryParseDeepLink } from '../../util/deepLinkParser'; import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection'; import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager'; import focusEditableElement from '../../util/focusEditableElement'; @@ -1093,9 +1095,15 @@ const Composer: FC = ({ return; } - callAttachBot({ - chatId, url: botMenuButton.url, threadId, - }); + const parsedLink = tryParseDeepLink(botMenuButton.url); + + if (parsedLink?.type === 'publicUsernameOrBotLink' && parsedLink.appName) { + processDeepLink(botMenuButton.url); + } else { + callAttachBot({ + chatId, url: botMenuButton.url, threadId, + }); + } }); const handleActivateBotCommandMenu = useLastCallback(() => { @@ -2033,7 +2041,7 @@ const Composer: FC = ({ {isInMessageList && } {effectEmoji && ( - + {renderText(effectEmoji)} )} diff --git a/src/components/common/Document.tsx b/src/components/common/Document.tsx index f3e479034..940bb8c88 100644 --- a/src/components/common/Document.tsx +++ b/src/components/common/Document.tsx @@ -1,20 +1,18 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useRef, useState, } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { ApiMessage } from '../../api/types'; +import type { ApiDocument, ApiMessage } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../config'; import { + getDocumentMediaHash, + getMediaFormat, + getMediaThumbUri, getMediaTransferState, - getMessageMediaFormat, - getMessageMediaHash, - getMessageMediaThumbDataUri, - getMessageWebPageDocument, - isMessageDocumentVideo, + isDocumentVideo, } from '../../global/helpers'; import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo'; @@ -30,7 +28,7 @@ import ConfirmDialog from '../ui/ConfirmDialog'; import File from './File'; type OwnProps = { - message: ApiMessage; + document: ApiDocument; observeIntersection?: ObserveFn; smaller?: boolean; isSelected?: boolean; @@ -46,14 +44,19 @@ type OwnProps = { shouldWarnAboutSvg?: boolean; onCancelUpload?: () => void; onMediaClick?: () => void; - onDateClick?: (messageId: number, chatId: string) => void; -}; +} & ({ + message: ApiMessage; + onDateClick: (arg: ApiMessage) => void; +} | { + message?: never; + onDateClick?: never; +}); const BYTES_PER_MB = 1024 * 1024; const SVG_EXTENSIONS = new Set(['svg', 'svgz']); -const Document: FC = ({ - message, +const Document = ({ + document, observeIntersection, smaller, canAutoLoad, @@ -67,11 +70,12 @@ const Document: FC = ({ isSelectable, shouldWarnAboutSvg, isDownloading, + message, onCancelUpload, onMediaClick, onDateClick, -}) => { - const { cancelMessageMediaDownload, downloadMessageMedia, setSettingOption } = getActions(); +}: OwnProps) => { + const { cancelMediaDownload, downloadMedia, setSettingOption } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); @@ -80,8 +84,6 @@ const Document: FC = ({ const [isSvgDialogOpen, openSvgDialog, closeSvgDialog] = useFlag(); const [shouldNotWarnAboutSvg, setShouldNotWarnAboutSvg] = useState(false); - const document = message.content.document! || getMessageWebPageDocument(message); - const { fileName, size, timestamp } = document; const extension = getDocumentExtension(document) || ''; @@ -100,32 +102,31 @@ const Document: FC = ({ const shouldDownload = Boolean(isDownloading || (isLoadAllowed && wasIntersected)); - const documentHash = getMessageMediaHash(message, 'download'); + const documentHash = getDocumentMediaHash(document, 'download'); const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress( - documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, true, + documentHash, !shouldDownload, getMediaFormat(document, 'download'), undefined, true, ); const isLoaded = Boolean(mediaData); const { isUploading, isTransferring, transferProgress, } = getMediaTransferState( - message, uploadProgress || downloadProgress, shouldDownload && !isLoaded, uploadProgress !== undefined, ); const hasPreview = getDocumentHasPreview(document); - const thumbDataUri = hasPreview ? getMessageMediaThumbDataUri(message) : undefined; + const thumbDataUri = hasPreview ? getMediaThumbUri(document) : undefined; const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined; - const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting); + const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting); const withMediaViewer = onMediaClick && Boolean(document.mediaType) && ( SUPPORTED_VIDEO_CONTENT_TYPES.has(document.mimeType) || SUPPORTED_IMAGE_CONTENT_TYPES.has(document.mimeType) ); const handleDownload = useLastCallback(() => { - downloadMessageMedia({ message }); + downloadMedia({ media: document }); }); const handleClick = useLastCallback(() => { @@ -137,7 +138,7 @@ const Document: FC = ({ } if (isDownloading) { - cancelMessageMediaDownload({ message }); + cancelMediaDownload({ media: document }); return; } @@ -166,7 +167,7 @@ const Document: FC = ({ }); const handleDateClick = useLastCallback(() => { - onDateClick!(message.id, message.chatId); + onDateClick?.(message); }); return ( @@ -187,7 +188,7 @@ const Document: FC = ({ sender={sender} isSelectable={isSelectable} isSelected={isSelected} - actionIcon={withMediaViewer ? (isMessageDocumentVideo(message) ? 'play' : 'eye') : 'download'} + actionIcon={withMediaViewer ? (isDocumentVideo(document) ? 'play' : 'eye') : 'download'} onClick={handleClick} onDateClick={onDateClick ? handleDateClick : undefined} /> diff --git a/src/components/common/GifButton.tsx b/src/components/common/GifButton.tsx index 21af7e679..0f3688c95 100644 --- a/src/components/common/GifButton.tsx +++ b/src/components/common/GifButton.tsx @@ -7,6 +7,7 @@ import type { ApiVideo } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../api/types'; +import { getVideoMediaHash } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; @@ -52,13 +53,12 @@ const GifButton: FC = ({ const lang = useOldLang(); - const localMediaHash = `gif${gif.id}`; const isIntersecting = useIsIntersecting(ref, observeIntersection); const loadAndPlay = isIntersecting && !isDisabled; - const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !loadAndPlay, ApiMediaFormat.BlobUrl); + const previewBlobUrl = useMedia(getVideoMediaHash(gif, 'preview'), !loadAndPlay, ApiMediaFormat.BlobUrl); const [withThumb] = useState(gif.thumbnail?.dataUri && !previewBlobUrl); const thumbRef = useCanvasBlur(gif.thumbnail?.dataUri, !withThumb); - const videoData = useMedia(localMediaHash, !loadAndPlay, ApiMediaFormat.BlobUrl); + const videoData = useMedia(getVideoMediaHash(gif, 'full'), !loadAndPlay, ApiMediaFormat.BlobUrl); const shouldRenderVideo = Boolean(loadAndPlay && videoData); const { isBuffered, bufferingHandlers } = useBuffering(true); const shouldRenderSpinner = loadAndPlay && !isBuffered; @@ -128,7 +128,6 @@ const GifButton: FC = ({ 'GifButton', gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal', onClick && 'interactive', - localMediaHash, className, ); diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index 3d80d3641..00d07904d 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -130,8 +130,9 @@ const GroupChatInfo: FC = ({ if (chat && hasMedia) { e.stopPropagation(); openMediaViewer({ - avatarOwnerId: chat.id, - mediaId: 0, + isAvatarView: true, + chatId: chat.id, + mediaIndex: 0, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, }); } diff --git a/src/components/common/MediaSpoiler.module.scss b/src/components/common/MediaSpoiler.module.scss index 314857752..2ce1e9b68 100644 --- a/src/components/common/MediaSpoiler.module.scss +++ b/src/components/common/MediaSpoiler.module.scss @@ -29,6 +29,7 @@ display: block; width: 100%; height: 100%; + object-fit: cover; } .dots { diff --git a/src/components/common/MediaSpoiler.tsx b/src/components/common/MediaSpoiler.tsx index e530e6e6b..9fc3fa754 100644 --- a/src/components/common/MediaSpoiler.tsx +++ b/src/components/common/MediaSpoiler.tsx @@ -1,6 +1,7 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact'; +import { requestMutation } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; import useCanvasBlur from '../../hooks/useCanvasBlur'; @@ -39,12 +40,15 @@ const MediaSpoiler: FC = ({ const handleClick = useLastCallback((e: React.MouseEvent) => { if (!ref.current) return; + const el = ref.current; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const shiftX = x - (rect.width / 2); const shiftY = y - (rect.height / 2); - ref.current.setAttribute('style', `--click-shift-x: ${shiftX}px; --click-shift-y: ${shiftY}px`); + requestMutation(() => { + el.setAttribute('style', `--click-shift-x: ${shiftX}px; --click-shift-y: ${shiftY}px`); + }); }); if (!shouldRender) { diff --git a/src/components/common/MessageText.tsx b/src/components/common/MessageText.tsx index d67743a46..752f589be 100644 --- a/src/components/common/MessageText.tsx +++ b/src/components/common/MessageText.tsx @@ -6,7 +6,8 @@ import type { ApiFormattedText, ApiMessage, ApiStory } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { ApiMessageEntityTypes } from '../../api/types'; -import { extractMessageText, getMessageText, stripCustomEmoji } from '../../global/helpers'; +import { CONTENT_NOT_SUPPORTED } from '../../config'; +import { extractMessageText, stripCustomEmoji } from '../../global/helpers'; import trimText from '../../util/trimText'; import { renderTextWithEntities } from './helpers/renderTextWithEntities'; @@ -80,8 +81,7 @@ function MessageText({ }, [entities]) || 0; if (!text) { - const contentNotSupportedText = getMessageText(messageOrStory); - return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined as any; + return {CONTENT_NOT_SUPPORTED}; } return ( diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 850c9a7b3..8ea67239d 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -121,8 +121,9 @@ const PrivateChatInfo: FC = ({ if (user && hasMedia) { e.stopPropagation(); openMediaViewer({ - avatarOwnerId: user.id, - mediaId: 0, + isAvatarView: true, + chatId: user.id, + mediaIndex: 0, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, }); } diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index ae1228e75..2bccdf00f 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -53,7 +53,7 @@ type StateProps = user?: ApiUser; userStatus?: ApiUserStatus; chat?: ApiChat; - mediaId?: number; + mediaIndex?: number; avatarOwnerId?: string; topic?: ApiTopic; messagesCount?: number; @@ -75,7 +75,7 @@ const ProfileInfo: FC = ({ userStatus, chat, isSynced, - mediaId, + mediaIndex, avatarOwnerId, topic, messagesCount, @@ -98,9 +98,9 @@ const ProfileInfo: FC = ({ const { id: userId } = user || {}; const { id: chatId } = chat || {}; const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY; - const prevMediaId = usePrevious(mediaId); + const prevMediaIndex = usePrevious(mediaIndex); const prevAvatarOwnerId = usePrevious(avatarOwnerId); - const mediaIdRef = useStateRef(mediaId); + const mediaIndexRef = useStateRef(mediaIndex); const [hasSlideAnimation, setHasSlideAnimation] = useState(true); // slideOptimized doesn't work well when animation is dynamically disabled const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none'; @@ -111,17 +111,17 @@ const ProfileInfo: FC = ({ // Set the current avatar photo to the last selected photo in Media Viewer after it is closed useEffect(() => { - if (prevAvatarOwnerId && prevMediaId !== undefined && mediaId === undefined) { + if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) { setHasSlideAnimation(false); - setCurrentPhotoIndex(prevMediaId); + setCurrentPhotoIndex(prevMediaIndex); } - }, [mediaId, prevMediaId, prevAvatarOwnerId]); + }, [mediaIndex, prevMediaIndex, prevAvatarOwnerId]); // Reset the current avatar photo to the one selected in Media Viewer if photos have changed useEffect(() => { setHasSlideAnimation(false); - setCurrentPhotoIndex(mediaIdRef.current || 0); - }, [mediaIdRef, photos]); + setCurrentPhotoIndex(mediaIndexRef.current || 0); + }, [mediaIndexRef, photos]); // Deleting the last profile photo may result in an error useEffect(() => { @@ -141,8 +141,9 @@ const ProfileInfo: FC = ({ const handleProfilePhotoClick = useLastCallback(() => { openMediaViewer({ - avatarOwnerId: userId || chatId, - mediaId: currentPhotoIndex, + isAvatarView: true, + chatId: userId || chatId, + mediaIndex: currentPhotoIndex, origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, }); }); @@ -386,7 +387,7 @@ export default memo(withGlobal( const isPrivate = isUserId(userId); const userStatus = selectUserStatus(global, userId); const chat = selectChat(global, userId); - const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer; + const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const isForum = chat?.isForum; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined; @@ -405,7 +406,7 @@ export default memo(withGlobal( userProfilePhoto: userFullInfo?.profilePhoto, userFallbackPhoto: userFullInfo?.fallbackPhoto, chatProfilePhoto: chatFullInfo?.profilePhoto, - mediaId, + mediaIndex, avatarOwnerId, emojiStatusSticker, ...(topic && { diff --git a/src/components/common/ReactionEmoji.tsx b/src/components/common/ReactionEmoji.tsx index 7799a66f2..a6a6f383e 100644 --- a/src/components/common/ReactionEmoji.tsx +++ b/src/components/common/ReactionEmoji.tsx @@ -54,7 +54,7 @@ const ReactionEmoji: FC = ({ const animationId = availableReaction?.selectAnimation?.id; const coords = useCoordsInSharedCanvas(ref, sharedCanvasRef); const mediaData = useMedia( - availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation) : undefined, + availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation, 'full') : undefined, !animationId, ); const handleClick = useLastCallback(() => { diff --git a/src/components/common/StickerSet.tsx b/src/components/common/StickerSet.tsx index 0e94d7cf9..ada88534a 100644 --- a/src/components/common/StickerSet.tsx +++ b/src/components/common/StickerSet.tsx @@ -177,6 +177,7 @@ const StickerSet: FC = ({ const handleDefaultTopicIconClick = useLastCallback(() => { onStickerSelect?.({ + mediaType: 'sticker', id: DEFAULT_TOPIC_ICON_STICKER_ID, isLottie: false, isVideo: false, @@ -188,6 +189,7 @@ const StickerSet: FC = ({ const handleDefaultStatusIconClick = useLastCallback(() => { onStickerSelect?.({ + mediaType: 'sticker', id: DEFAULT_STATUS_ICON_ID, isLottie: false, isVideo: false, diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index 9afddfa3e..41bdb72bc 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -5,7 +5,7 @@ import { getGlobal } from '../../global'; import type { ApiSticker } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; -import { getStickerPreviewHash } from '../../global/helpers'; +import { getStickerMediaHash } from '../../global/helpers'; import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import * as mediaLoader from '../../util/mediaLoader'; @@ -86,7 +86,7 @@ const StickerView: FC = ({ const isUnsupportedVideo = sticker.isVideo && (!IS_WEBM_SUPPORTED || isVideoBroken); const isVideo = sticker.isVideo && !isUnsupportedVideo; const isStatic = !isLottie && !isVideo; - const previewMediaHash = getStickerPreviewHash(sticker.id); + const previewMediaHash = getStickerMediaHash(sticker, 'preview'); const dpr = useDevicePixelRatio(); diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx index 5dd0e849b..0d0b91a4a 100644 --- a/src/components/common/WebLink.tsx +++ b/src/components/common/WebLink.tsx @@ -31,7 +31,7 @@ type OwnProps = { senderTitle?: string; isProtected?: boolean; observeIntersection?: ObserveFn; - onMessageClick: (messageId: number, chatId: string) => void; + onMessageClick: (message: ApiMessage) => void; }; type ApiWebPageWithFormatted = @@ -61,7 +61,7 @@ const WebLink: FC = ({ } const handleMessageClick = useLastCallback(() => { - onMessageClick(message.id, message.chatId); + onMessageClick(message); }); if (!linkData) { diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 04ab04da2..2d6888c04 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -89,16 +89,20 @@ const EmbeddedMessage: FC = ({ const ref = useRef(null); const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading); - const wrappedMedia = useMemo(() => { - const replyMedia = replyInfo?.type === 'message' && replyInfo.replyMedia; - if (!replyMedia) return undefined; - return { - content: replyMedia, - }; - }, [replyInfo]); + const containedMedia: MediaContainer | undefined = useMemo(() => { + const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content; + if (!media) { + return undefined; + } - const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting); - const mediaThumbnail = useThumbnail(message || wrappedMedia); + return { + content: media, + }; + }, [message, replyInfo]); + + const mediaHash = containedMedia && getMessageMediaHash(containedMedia, 'pictogram'); + const mediaBlobUrl = useMedia(mediaHash, !isIntersecting); + const mediaThumbnail = useThumbnail(containedMedia); const isRoundVideo = Boolean(message && getMessageRoundVideo(message)); const isSpoiler = Boolean(message && getMessageIsSpoiler(message)); const isQuote = Boolean(replyInfo?.type === 'message' && replyInfo.isQuote); @@ -131,7 +135,7 @@ const EmbeddedMessage: FC = ({ } if (!message) { - return customText || renderMediaContentType(wrappedMedia) || NBSP; + return customText || renderMediaContentType(containedMedia) || NBSP; } if (isActionMessage(message)) { diff --git a/src/components/common/embedded/EmojiIconBackground.tsx b/src/components/common/embedded/EmojiIconBackground.tsx index a9a07304f..e60d01d33 100644 --- a/src/components/common/embedded/EmojiIconBackground.tsx +++ b/src/components/common/embedded/EmojiIconBackground.tsx @@ -3,7 +3,7 @@ import React, { } from '../../../lib/teact/teact'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; -import { getStickerPreviewHash } from '../../../global/helpers'; +import { getStickerMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { preloadImage } from '../../../util/files'; import { REM } from '../helpers/mediaDimensions'; @@ -78,7 +78,7 @@ const EmojiIconBackground = ({ const lang = useOldLang(); const { customEmoji } = useCustomEmoji(emojiDocumentId); - const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined; + const previewMediaHash = customEmoji ? getStickerMediaHash(customEmoji, 'preview') : undefined; const previewUrl = useMedia(previewMediaHash); const customColor = useDynamicColorListener(containerRef); diff --git a/src/components/common/helpers/mediaDimensions.ts b/src/components/common/helpers/mediaDimensions.ts index f20b50eba..69a7c6ff1 100644 --- a/src/components/common/helpers/mediaDimensions.ts +++ b/src/components/common/helpers/mediaDimensions.ts @@ -1,5 +1,5 @@ import type { - ApiDimensions, ApiPhoto, ApiSticker, ApiVideo, + ApiDimensions, ApiMediaExtendedPreview, ApiPhoto, ApiSticker, ApiVideo, } from '../../../api/types'; import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config'; @@ -25,7 +25,7 @@ let cachedMaxWidthOwn: number | undefined; let cachedMaxWidth: number | undefined; let cachedMaxWidthNoAvatar: number | undefined; -function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean, isMobile?: boolean) { +function getMaxMessageWidthRem(fromOwnMessage?: boolean, noAvatars?: boolean, isMobile?: boolean) { const regularMaxWidth = fromOwnMessage ? MESSAGE_OWN_MAX_WIDTH_REM : MESSAGE_MAX_WIDTH_REM; if (!isMobile) { return regularMaxWidth; @@ -59,7 +59,7 @@ function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean, isM } export function getAvailableWidth( - fromOwnMessage: boolean, + fromOwnMessage?: boolean, asForwarded?: boolean, isWebPageMedia?: boolean, noAvatars?: boolean, @@ -94,7 +94,7 @@ export function calculateDimensionsForMessageMedia({ }: { width: number; height: number; - fromOwnMessage: boolean; + fromOwnMessage?: boolean; asForwarded?: boolean; isWebPageMedia?: boolean; isGif?: boolean; @@ -125,7 +125,7 @@ export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo: export function calculateInlineImageDimensions( photo: ApiPhoto, - fromOwnMessage: boolean, + fromOwnMessage?: boolean, asForwarded?: boolean, isWebPageMedia?: boolean, noAvatars?: boolean, @@ -146,7 +146,7 @@ export function calculateInlineImageDimensions( export function calculateVideoDimensions( video: ApiVideo, - fromOwnMessage: boolean, + fromOwnMessage?: boolean, asForwarded?: boolean, isWebPageMedia?: boolean, noAvatars?: boolean, @@ -166,6 +166,27 @@ export function calculateVideoDimensions( }); } +export function calculateExtendedPreviewDimensions( + preview: ApiMediaExtendedPreview, + fromOwnMessage?: boolean, + asForwarded?: boolean, + isWebPageMedia?: boolean, + noAvatars?: boolean, + isMobile?: boolean, +) { + const { width = DEFAULT_MEDIA_DIMENSIONS.width, height = DEFAULT_MEDIA_DIMENSIONS.height } = preview; + + return calculateDimensionsForMessageMedia({ + width, + height, + fromOwnMessage, + asForwarded, + isWebPageMedia, + noAvatars, + isMobile, + }); +} + export function getPictogramDimensions(): ApiDimensions { return { width: 2 * REM, diff --git a/src/components/common/profile/UserBirthday.tsx b/src/components/common/profile/UserBirthday.tsx index 934667ec4..a9abd5229 100644 --- a/src/components/common/profile/UserBirthday.tsx +++ b/src/components/common/profile/UserBirthday.tsx @@ -105,12 +105,12 @@ const UserBirthday = ({ if (!isToday || !numbersForAge) return; numbersForAge.forEach((sticker) => { - const hash = getStickerMediaHash(sticker.id); + const hash = getStickerMediaHash(sticker, 'preview'); mediaLoader.fetch(hash, ApiMediaFormat.BlobUrl); }); if (effectSticker) { - const effectHash = getStickerMediaHash(effectSticker.id); + const effectHash = getStickerMediaHash(effectSticker, 'preview'); mediaLoader.fetch(effectHash, ApiMediaFormat.BlobUrl); } }, [effectSticker, isToday, numbersForAge]); diff --git a/src/components/common/reactions/CustomEmojiEffect.tsx b/src/components/common/reactions/CustomEmojiEffect.tsx index f986332bd..14cabfa18 100644 --- a/src/components/common/reactions/CustomEmojiEffect.tsx +++ b/src/components/common/reactions/CustomEmojiEffect.tsx @@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../../lib/teact/teact'; import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types'; -import { getStickerPreviewHash } from '../../../global/helpers'; +import { getStickerHashById } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment'; @@ -31,7 +31,7 @@ const CustomEmojiEffect: FC = ({ particleSize, onEnded, }) => { - const stickerHash = getStickerPreviewHash(reaction.documentId); + const stickerHash = getStickerHashById(reaction.documentId); const previewMediaData = useMedia(!isLottie ? stickerHash : undefined); diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 9a67944e2..6e92fbe44 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -89,10 +89,10 @@ export default function useChatListEntry({ const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage); - const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage) - ? getMessageMediaThumbDataUri(lastMessage) - : undefined; - const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined); + const mediaHasPreview = lastMessage && !getMessageSticker(lastMessage); + + const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(lastMessage) : undefined; + const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(lastMessage, 'micro') : undefined); const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage)); const actionTargetUsers = useMemo(() => { diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index 5d2a40af5..2190f75c4 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -2,10 +2,12 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { ApiMessage } from '../../../api/types'; import type { StateProps } from './helpers/createMapStateToProps'; import { AudioOrigin, LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; +import { getIsDownloading } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; @@ -70,8 +72,8 @@ const AudioResults: FC = ({ }).filter(Boolean); }, [globalMessagesByChatId, foundIds]); - const handleMessageFocus = useCallback((messageId: number, chatId: string) => { - focusMessage({ chatId, messageId }); + const handleMessageFocus = useCallback((message: ApiMessage) => { + focusMessage({ chatId: message.chatId, messageId: message.id }); }, [focusMessage]); const handlePlayAudio = useCallback((messageId: number, chatId: string) => { @@ -111,7 +113,7 @@ const AudioResults: FC = ({ onPlay={handlePlayAudio} onDateClick={handleMessageFocus} canDownload={!chatsById[message.chatId]?.isProtected && !message.isProtected} - isDownloading={activeDownloads[message.chatId]?.ids?.includes(message.id)} + isDownloading={getIsDownloading(activeDownloads, message.content.audio!)} /> ); diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index 6a5fb6cfe..fca1f3446 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -9,7 +9,7 @@ import type { StateProps } from './helpers/createMapStateToProps'; import { LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; -import { getMessageDocument } from '../../../global/helpers'; +import { getIsDownloading, getMessageDocument } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; @@ -84,8 +84,8 @@ const FileResults: FC = ({ }).filter(Boolean) as ApiMessage[]; }, [globalMessagesByChatId, foundIds]); - const handleMessageFocus = useCallback((messageId: number, chatId: string) => { - focusMessage({ chatId, messageId }); + const handleMessageFocus = useCallback((message: ApiMessage) => { + focusMessage({ chatId: message.chatId, messageId: message.id }); }, [focusMessage]); function renderList() { @@ -111,13 +111,14 @@ const FileResults: FC = ({

)} = ({ }).filter(Boolean); }, [globalMessagesByChatId, foundIds]); - const handleMessageFocus = useCallback((messageId: number, chatId: string) => { - focusMessage({ chatId, messageId }); + const handleMessageFocus = useCallback((message: ApiMessage) => { + focusMessage({ chatId: message.chatId, messageId: message.id }); }, [focusMessage]); function renderList() { diff --git a/src/components/left/search/MediaResults.tsx b/src/components/left/search/MediaResults.tsx index 5f99c384e..05f08ce23 100644 --- a/src/components/left/search/MediaResults.tsx +++ b/src/components/left/search/MediaResults.tsx @@ -80,7 +80,7 @@ const MediaResults: FC = ({ const handleSelectMedia = useCallback((id: number, chatId: string) => { openMediaViewer({ chatId, - mediaId: id, + messageId: id, origin: MediaViewerOrigin.SearchResult, }); }, [openMediaViewer]); diff --git a/src/components/left/search/helpers/createMapStateToProps.ts b/src/components/left/search/helpers/createMapStateToProps.ts index 6eabcfa88..b0880e7c5 100644 --- a/src/components/left/search/helpers/createMapStateToProps.ts +++ b/src/components/left/search/helpers/createMapStateToProps.ts @@ -14,7 +14,7 @@ export type StateProps = { globalMessagesByChatId?: Record }>; foundIds?: string[]; searchChatId?: string; - activeDownloads: TabState['activeDownloads']['byChatId']; + activeDownloads: TabState['activeDownloads']; isChatProtected?: boolean; shouldWarnAboutSvg?: boolean; }; @@ -36,7 +36,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) { const { byChatId: globalMessagesByChatId } = global.messages; const foundIds = resultsByType?.[currentType]?.foundIds; - const activeDownloads = tabState.activeDownloads.byChatId; + const activeDownloads = tabState.activeDownloads; return { theme: selectTheme(global), diff --git a/src/components/main/DownloadManager.tsx b/src/components/main/DownloadManager.tsx index 69bc52db1..a250292d9 100644 --- a/src/components/main/DownloadManager.tsx +++ b/src/components/main/DownloadManager.tsx @@ -1,17 +1,12 @@ import type { FC } from '../../lib/teact/teact'; import { memo, useEffect } from '../../lib/teact/teact'; -import { getActions, getGlobal, withGlobal } from '../../global'; +import { getActions, withGlobal } from '../../global'; -import type { ApiMessage } from '../../api/types'; -import type { GlobalState, TabState } from '../../global/types'; +import type { TabState } from '../../global/types'; import { ApiMediaFormat } from '../../api/types'; -import { - getMessageContentFilename, getMessageMediaFormat, getMessageMediaHash, -} from '../../global/helpers'; import { selectTabState } from '../../global/selectors'; import download from '../../util/download'; -import { compact } from '../../util/iteratees'; import * as mediaLoader from '../../util/mediaLoader'; import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/windowEnvironment'; @@ -19,85 +14,64 @@ import useLastCallback from '../../hooks/useLastCallback'; import useRunDebounced from '../../hooks/useRunDebounced'; type StateProps = { - activeDownloads: TabState['activeDownloads']['byChatId']; - messages?: GlobalState['messages']['byChatId']; + activeDownloads: TabState['activeDownloads']; }; const GLOBAL_UPDATE_DEBOUNCE = 1000; -const processedMessages = new Set(); -const downloadedMessages = new Set(); +const processedHashes = new Set(); +const downloadedHashes = new Set(); const DownloadManager: FC = ({ activeDownloads, }) => { - const { cancelMessagesMediaDownload, showNotification } = getActions(); + const { cancelMediaHashDownloads, showNotification } = getActions(); const runDebounced = useRunDebounced(GLOBAL_UPDATE_DEBOUNCE, true); - const handleMessageDownloaded = useLastCallback((message: ApiMessage) => { - downloadedMessages.add(message); + const handleMediaDownloaded = useLastCallback((hash: string) => { + downloadedHashes.add(hash); runDebounced(() => { - if (downloadedMessages.size) { - cancelMessagesMediaDownload({ messages: Array.from(downloadedMessages) }); - downloadedMessages.clear(); + if (downloadedHashes.size) { + cancelMediaHashDownloads({ mediaHashes: Array.from(downloadedHashes) }); + downloadedHashes.clear(); } }); }); useEffect(() => { - // No need for expensive global updates on messages, so we avoid them - const messages = getGlobal().messages.byChatId; - const scheduledMessages = getGlobal().scheduledMessages.byChatId; - - const activeMessages = Object.entries(activeDownloads).map(([chatId, chatActiveDownloads]) => { - const chatMessages = chatActiveDownloads.ids?.map((id) => messages[chatId]?.byId[id]); - const chatScheduledMessages = chatActiveDownloads.scheduledIds?.map((id) => scheduledMessages[chatId]?.byId[id]); - - return compact([...chatMessages || [], ...chatScheduledMessages || []]); - }).flat(); - - if (!activeMessages.length) { - processedMessages.clear(); + if (!Object.keys(activeDownloads).length) { + processedHashes.clear(); return; } - activeMessages.forEach((message) => { - if (processedMessages.has(message)) { - return; - } - processedMessages.add(message); - const downloadHash = getMessageMediaHash(message, 'download'); - if (!downloadHash) { - handleMessageDownloaded(message); + Object.entries(activeDownloads).forEach(([mediaHash, metadata]) => { + if (processedHashes.has(mediaHash)) { return; } + processedHashes.add(mediaHash); - const mediaData = mediaLoader.getFromMemory(downloadHash); + const { size, filename, format: mediaFormat } = metadata; + + const mediaData = mediaLoader.getFromMemory(mediaHash); if (mediaData) { - download(mediaData, getMessageContentFilename(message)); - handleMessageDownloaded(message); + download(mediaData, filename); + handleMediaDownloaded(mediaHash); return; } - const { - document, video, audio, - } = message.content; - const mediaSize = (document || video || audio)?.size || 0; - if (mediaSize > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) { + if (size > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) { showNotification({ message: 'Downloading files bigger than 2GB is not supported in your browser.', }); - handleMessageDownloaded(message); + handleMediaDownloaded(mediaHash); return; } - const mediaFormat = getMessageMediaFormat(message, 'download'); - mediaLoader.fetch(downloadHash, mediaFormat, true).then((result) => { + mediaLoader.fetch(mediaHash, mediaFormat, true).then((result) => { if (mediaFormat === ApiMediaFormat.DownloadUrl) { const url = new URL(result, window.document.baseURI); - const filename = getMessageContentFilename(message); url.searchParams.set('filename', encodeURIComponent(filename)); const downloadWindow = window.open(url.toString()); downloadWindow?.addEventListener('beforeunload', () => { @@ -106,20 +80,20 @@ const DownloadManager: FC = ({ }); }); } else if (result) { - download(result, getMessageContentFilename(message)); + download(result, filename); } - handleMessageDownloaded(message); + handleMediaDownloaded(mediaHash); }); }); - }, [activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]); + }, [activeDownloads]); return undefined; }; export default memo(withGlobal( (global): StateProps => { - const activeDownloads = selectTabState(global).activeDownloads.byChatId; + const activeDownloads = selectTabState(global).activeDownloads; return { activeDownloads, diff --git a/src/components/main/premium/PremiumFeatureModal.tsx b/src/components/main/premium/PremiumFeatureModal.tsx index 92e7ea594..b538375ca 100644 --- a/src/components/main/premium/PremiumFeatureModal.tsx +++ b/src/components/main/premium/PremiumFeatureModal.tsx @@ -46,6 +46,7 @@ export const PREMIUM_FEATURE_TITLES: Record = { saved_tags: 'PremiumPreviewTags2', last_seen: 'PremiumPreviewLastSeen', message_privacy: 'PremiumPreviewMessagePrivacy', + effects: 'PremiumPreviewEffects', }; export const PREMIUM_FEATURE_DESCRIPTIONS: Record = { @@ -66,6 +67,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record = { saved_tags: 'PremiumPreviewTagsDescription2', last_seen: 'PremiumPreviewLastSeenDescription', message_privacy: 'PremiumPreviewMessagePrivacyDescription', + effects: 'PremiumPreviewEffectsDescription', }; const LIMITS_TITLES: Record = { diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index 6c442f7d8..39d76ce37 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -42,6 +42,7 @@ import styles from './PremiumMainModal.module.scss'; import PremiumAds from '../../../assets/premium/PremiumAds.svg'; import PremiumBadge from '../../../assets/premium/PremiumBadge.svg'; import PremiumChats from '../../../assets/premium/PremiumChats.svg'; +import PremiumEffects from '../../../assets/premium/PremiumEffects.svg'; import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg'; import PremiumFile from '../../../assets/premium/PremiumFile.svg'; import PremiumLastSeen from '../../../assets/premium/PremiumLastSeen.svg'; @@ -78,6 +79,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record = { saved_tags: PremiumTags, last_seen: PremiumLastSeen, message_privacy: PremiumMessagePrivacy, + effects: PremiumEffects, }; export type OwnProps = { diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index d06193739..1503a2dca 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -1,18 +1,19 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useRef, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { - ApiMessage, ApiPeer, ApiPhoto, ApiUser, + ApiChat, + ApiMessage, ApiPeer, ApiPhoto, } from '../../api/types'; -import { MediaViewerOrigin, type ThreadId } from '../../types'; +import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types'; import { ANIMATION_END_DELAY } from '../../config'; -import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers'; import { - selectChat, + getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, isUserId, +} from '../../global/helpers'; +import { selectChatMessage, selectChatMessages, selectChatScheduledMessages, @@ -21,17 +22,17 @@ import { selectIsChatWithSelf, selectListedIds, selectOutlyingListByMessageId, + selectPeer, selectPerformanceSettingsValue, selectScheduledMessage, selectTabState, - selectUser, - selectUserFullInfo, } from '../../global/selectors'; import { stopCurrentAudio } from '../../util/audioPlayer'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager'; import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions'; import { renderMessageText } from '../common/helpers/renderMessageText'; +import getViewableMedia, { getMediaViewerItem, type MediaViewerItem } from './helpers/getViewableMedia'; import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import useAppLayout from '../../hooks/useAppLayout'; @@ -44,7 +45,6 @@ import useOldLang from '../../hooks/useOldLang'; import { exitPictureInPictureIfNeeded, usePictureInPictureSignal } from '../../hooks/usePictureInPicture'; import usePrevious from '../../hooks/usePrevious'; import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; -import { useStateRef } from '../../hooks/useStateRef'; import { useMediaProps } from './hooks/useMediaProps'; import ReportModal from '../common/ReportModal'; @@ -60,16 +60,17 @@ import './MediaViewer.scss'; type StateProps = { chatId?: string; threadId?: ThreadId; - mediaId?: number; - senderId?: string; + messageId?: number; + message?: ApiMessage; + collectedMessageIds?: number[]; isChatWithSelf?: boolean; canUpdateMedia?: boolean; origin?: MediaViewerOrigin; + avatar?: ApiPhoto; avatarOwner?: ApiPeer; - avatarOwnerFallbackPhoto?: ApiPhoto; - message?: ApiMessage; chatMessages?: Record; - collectionIds?: number[]; + standaloneMedia?: MediaViewerMedia[]; + mediaIndex?: number; isHidden?: boolean; withAnimation?: boolean; shouldSkipHistoryAnimations?: boolean; @@ -80,26 +81,27 @@ type StateProps = { const ANIMATION_DURATION = 250; -const MediaViewer: FC = ({ +const MediaViewer = ({ chatId, threadId, - mediaId, - senderId, + messageId, + message, + collectedMessageIds, isChatWithSelf, canUpdateMedia, origin, + avatar, avatarOwner, - avatarOwnerFallbackPhoto, - message, chatMessages, - collectionIds, + standaloneMedia, + mediaIndex, withAnimation, isHidden, shouldSkipHistoryAnimations, withDynamicLoading, isLoadingMoreMedia, isSynced, -}) => { +}: StateProps) => { const { openMediaViewer, closeMediaViewer, @@ -109,11 +111,12 @@ const MediaViewer: FC = ({ searchChatMediaMessages, } = getActions(); - const isOpen = Boolean(avatarOwner || mediaId); + const isOpen = Boolean(avatarOwner || message || standaloneMedia); const { isMobile } = useAppLayout(); /* Animation */ const animationKey = useRef(); + const senderId = message?.senderId || avatarOwner?.id; const prevSenderId = usePrevious(senderId); const headerAnimation = withAnimation ? 'slideFade' : 'none'; const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations); @@ -121,40 +124,34 @@ const MediaViewer: FC = ({ /* Controls */ const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); + const currentItem = getMediaViewerItem({ + message, avatarOwner, standaloneMedia, mediaIndex, + }); + const { media, isSingle } = getViewableMedia(currentItem) || {}; + const { - webPagePhoto, - webPageVideo, isVideo, - actionPhoto, isPhoto, bestImageData, bestData, dimensions, isGif, isFromSharedMedia, - avatarPhoto, - fileName, } = useMediaProps({ - message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION, + media, isAvatar: Boolean(avatarOwner), origin, delay: isGhostAnimation && ANIMATION_DURATION, }); - const canReport = !!avatarPhoto && !isChatWithSelf; + const canReport = avatarOwner && !isChatWithSelf; const isVisible = !isHidden && isOpen; - /* Navigation */ - const singleMediaId = webPagePhoto || webPageVideo || actionPhoto || isGif ? mediaId : undefined; + const messageMediaIds = useMemo(() => { + return withDynamicLoading + ? collectedMessageIds + : getChatMediaMessageIds(chatMessages || {}, collectedMessageIds || [], isFromSharedMedia); + }, [chatMessages, collectedMessageIds, isFromSharedMedia, withDynamicLoading]); - const mediaIds = useMemo(() => { - if (singleMediaId) return [singleMediaId]; - if (avatarOwner) return avatarOwner.photos?.map((p, i) => i) || []; - if (withDynamicLoading) return collectionIds || []; - return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia); - }, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia, withDynamicLoading]); - - const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1; - - if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) { - animationKey.current = selectedMediaIndex; + if (isOpen && (!prevSenderId || prevSenderId !== senderId || animationKey.current === undefined)) { + animationKey.current = isSingle ? 0 : (messageId || mediaIndex); } const [getIsPictureInPicture] = usePictureInPictureSignal(); @@ -202,65 +199,48 @@ const MediaViewer: FC = ({ const prevMessage = usePrevious(message); const prevIsHidden = usePrevious(isHidden); const prevOrigin = usePrevious(origin); - const prevMediaId = usePrevious(mediaId); - const prevAvatarOwner = usePrevious(avatarOwner); + const prevItem = usePrevious(currentItem); const prevBestImageData = usePrevious(bestImageData); const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined; const hasFooter = Boolean(textParts); - const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId; + const shouldAnimateOpening = prevIsHidden && prevItem !== currentItem; useEffect(() => { - if (isGhostAnimation && isOpen && (!prevMessage || shouldAnimateOpening) && !prevAvatarOwner) { + if (isGhostAnimation && isOpen && (shouldAnimateOpening || !prevItem)) { dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); - animateOpening(hasFooter, origin!, bestImageData!, dimensions!, isVideo, message); + animateOpening(hasFooter, origin!, bestImageData!, dimensions!, isVideo, message, mediaIndex); } - if (isGhostAnimation && !isOpen && (prevMessage || prevAvatarOwner)) { + if (isGhostAnimation && !isOpen && prevItem) { dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY); - animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined); + animateClosing(prevOrigin!, prevBestImageData!, prevMessage, prevItem?.mediaIndex); } }, [ - isGhostAnimation, isOpen, shouldAnimateOpening, origin, prevOrigin, message, prevMessage, prevAvatarOwner, - bestImageData, prevBestImageData, dimensions, isVideo, hasFooter, + bestImageData, dimensions, hasFooter, isGhostAnimation, isOpen, isVideo, message, origin, + prevBestImageData, prevItem, prevMessage, prevOrigin, shouldAnimateOpening, mediaIndex, ]); const handleClose = useLastCallback(() => closeMediaViewer()); - const mediaIdRef = useStateRef(mediaId); const handleFooterClick = useLastCallback(() => { handleClose(); - const currentMediaId = mediaIdRef.current; - - if (!chatId || !currentMediaId) return; + if (!chatId || !messageId) return; if (isMobile) { setTimeout(() => { toggleChatInfo({ force: false }, { forceSyncOnIOs: true }); - focusMessage({ chatId, threadId, messageId: currentMediaId }); + focusMessage({ chatId, threadId, messageId }); }, ANIMATION_DURATION); } else { - focusMessage({ chatId, threadId, messageId: currentMediaId }); + focusMessage({ chatId, threadId, messageId }); } }); const handleForward = useLastCallback(() => { openForwardMenu({ fromChatId: chatId!, - messageIds: [mediaId!], - }); - }); - - const selectMedia = useLastCallback((id?: number) => { - openMediaViewer({ - chatId, - threadId, - mediaId: id, - avatarOwnerId: avatarOwner?.id, - origin: origin!, - withDynamicLoading, - }, { - forceOnHeavyAnimation: true, + messageIds: [messageId!], }); }); @@ -274,56 +254,106 @@ const MediaViewer: FC = ({ } }, [isGif, isVideo]); - const mediaIdsRef = useStateRef(mediaIds); - - const loadMoreMediaIfNeeded = useLastCallback((activeMediaId?: number) => { - if (!activeMediaId || !withDynamicLoading || isLoadingMoreMedia) return; - searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: activeMediaId }); + const loadMoreItemsIfNeeded = useLastCallback((item?: MediaViewerItem) => { + if (!item || !withDynamicLoading || isLoadingMoreMedia) return; + if (item.type !== 'message') return; + searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: item.message.id }); }); - const getMediaId = useLastCallback((fromId?: number, direction?: number): number | undefined => { - if (fromId === undefined) return undefined; - const mIds = mediaIdsRef.current; - const index = mIds.indexOf(fromId); - if ((direction === -1 && index > 0) || (direction === 1 && index < mIds.length - 1)) { - return mIds[index + direction]; + const getNextItem = useLastCallback((from: MediaViewerItem, direction: number): MediaViewerItem | undefined => { + if (direction === 0 || isSingle) return undefined; + + if (from.type === 'standalone') { + const { media: fromMedia, mediaIndex: fromMediaIndex } = from; + const nextIndex = fromMediaIndex + direction; + if (nextIndex >= 0 && nextIndex < fromMedia.length) { + return { type: 'standalone', media: fromMedia, mediaIndex: nextIndex }; + } + + return undefined; } - // Fallback - if (isVisible) loadMoreMediaIfNeeded(fromId); + + if (from.type === 'avatar') { + const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from; + const nextIndex = fromMediaIndex + direction; + if (nextIndex >= 0 && fromAvatarOwner.photos && nextIndex < fromAvatarOwner.photos.length) { + return { type: 'avatar', avatarOwner: fromAvatarOwner, mediaIndex: nextIndex }; + } + + return undefined; + } + + const { message: fromMessage, mediaIndex: fromMediaIndex } = from; + + const paidMedia = getMessagePaidMedia(fromMessage); + if (paidMedia) { + const nextIndex = fromMediaIndex! + direction; + + if (nextIndex >= 0 && nextIndex < paidMedia.extendedMedia.length) { + return { type: 'message', message: fromMessage, mediaIndex: nextIndex }; + } + } + + const index = messageMediaIds?.indexOf(fromMessage.id); + if (index === undefined) return undefined; + const nextIndex = index + direction; + const nextMessageId = messageMediaIds![nextIndex]; + const nextMessage = chatMessages?.[nextMessageId]; + if (nextMessage) { + return { type: 'message', message: nextMessage }; + } + return undefined; }); - const handleBeforeDelete = useLastCallback(() => { - if (mediaIds.length <= 1) { + const openMediaViewerItem = useLastCallback((item?: MediaViewerItem) => { + if (!item) { handleClose(); return; } - let index = mediaId ? mediaIds.indexOf(mediaId) : -1; - // Before deleting, select previous media or the first one - index = index > 0 ? index - 1 : 0; - selectMedia(mediaIds[index]); + + const itemChatId = item.type === 'avatar' + ? item.avatarOwner.id : item.type === 'message' + ? item.message.chatId : undefined; + const itemMessageId = item.type === 'message' ? item.message.id : undefined; + const itemStandaloneMedia = item.type === 'standalone' ? item.media : undefined; + + openMediaViewer({ + origin: origin!, + chatId: itemChatId, + messageId: itemMessageId, + standaloneMedia: itemStandaloneMedia, + mediaIndex: item.mediaIndex, + isAvatarView: item.type === 'avatar', + withDynamicLoading, + }, { + forceOnHeavyAnimation: true, + }); + }); + + const handleBeforeDelete = useLastCallback(() => { + const mediaCount = avatarOwner?.photos?.length || standaloneMedia?.length || messageMediaIds?.length || 0; + if (mediaCount <= 1 || !currentItem) { + handleClose(); + return; + } + // Before deleting, select previous media + const prevMedia = getNextItem(currentItem, -1); + if (prevMedia) { + openMediaViewerItem(prevMedia); + return; + } + + if (currentItem.type === 'avatar' || currentItem.type === 'standalone') { + // Keep current item, it'll update when indexes shift + return; + } + + handleClose(); }); const lang = useOldLang(); - function renderSenderInfo() { - return avatarOwner ? ( - - ) : ( - - ); - } - return ( = ({ )} - {renderSenderInfo()} + = ({ isOpen={isReportModalOpen} onClose={closeReportModal} subject="media" - photo={avatarPhoto} + photo={avatar} peerId={avatarOwner?.id} /> = ({ isVideo={isVideo} withAnimation={withAnimation} onClose={handleClose} - selectMedia={selectMedia} + selectItem={openMediaViewerItem} isHidden={isHidden} onFooterClick={handleFooterClick} /> @@ -402,122 +431,95 @@ export default memo(withGlobal( const { chatId, threadId, - mediaId, - avatarOwnerId, + messageId, origin, isHidden, withDynamicLoading, + standaloneMedia, + mediaIndex, + isAvatarView, } = mediaViewer; const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations'); const { currentUserId, isSynced } = global; - let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId); + const isChatWithSelf = Boolean(chatId) && selectIsChatWithSelf(global, chatId); - if (origin === MediaViewerOrigin.SearchResult) { - if (!(chatId && mediaId)) { - return { withAnimation, shouldSkipHistoryAnimations }; - } - - const message = selectChatMessage(global, chatId, mediaId); - if (!message) { - return { withAnimation, shouldSkipHistoryAnimations }; - } - - return { - chatId, - mediaId, - senderId: message.senderId, - isChatWithSelf, - origin, - message, - withAnimation, - isHidden, - shouldSkipHistoryAnimations, - }; - } - - if (avatarOwnerId) { - const user = selectUser(global, avatarOwnerId); - const chat = selectChat(global, avatarOwnerId); + if (isAvatarView) { + const peer = selectPeer(global, chatId!); let canUpdateMedia = false; - if (user) { - canUpdateMedia = avatarOwnerId === currentUserId; - } else if (chat) { - canUpdateMedia = isChatAdmin(chat); + if (peer) { + canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat); } - isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId); - return { - mediaId, - senderId: avatarOwnerId, - avatarOwner: user || chat, - avatarOwnerFallbackPhoto: user ? selectUserFullInfo(global, avatarOwnerId)?.fallbackPhoto : undefined, + avatar: peer?.photos?.[mediaIndex!], + avatarOwner: peer, isChatWithSelf, canUpdateMedia, withAnimation, origin, shouldSkipHistoryAnimations, isHidden, + standaloneMedia, + mediaIndex, }; } - if (!(chatId && threadId && mediaId)) { - return { withAnimation, shouldSkipHistoryAnimations }; - } - let message: ApiMessage | undefined; - if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { - message = selectScheduledMessage(global, chatId, mediaId); - } else { - message = selectChatMessage(global, chatId, mediaId); - } - - if (!message) { - return { withAnimation, shouldSkipHistoryAnimations }; + if (chatId && messageId) { + if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { + message = selectScheduledMessage(global, chatId, messageId); + } else { + message = selectChatMessage(global, chatId, messageId); + } } let chatMessages: Record | undefined; - if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { - chatMessages = selectChatScheduledMessages(global, chatId); - } else { - chatMessages = selectChatMessages(global, chatId); + if (chatId) { + if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) { + chatMessages = selectChatScheduledMessages(global, chatId); + } else { + chatMessages = selectChatMessages(global, chatId); + } } let isLoadingMoreMedia = false; const isOriginInline = origin === MediaViewerOrigin.Inline; const isOriginAlbum = origin === MediaViewerOrigin.Album; - let collectionIds: number[] | undefined; + let collectedMessageIds: number[] | undefined; - if (withDynamicLoading && (isOriginInline || isOriginAlbum)) { - const currentSearch = selectCurrentChatMediaSearch(global); - isLoadingMoreMedia = Boolean(currentSearch?.isLoading); - const { foundIds } = (currentSearch?.currentSegment) || {}; - collectionIds = foundIds; - } else if (origin === MediaViewerOrigin.SharedMedia) { - const currentSearch = selectCurrentSharedMediaSearch(global); - const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {}; - collectionIds = foundIds; - } else if (isOriginInline || isOriginAlbum) { - const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, message.id); - collectionIds = outlyingList || selectListedIds(global, chatId, threadId); + if (chatId && threadId && messageId) { + if (withDynamicLoading && (isOriginInline || isOriginAlbum)) { + const currentSearch = selectCurrentChatMediaSearch(global); + isLoadingMoreMedia = Boolean(currentSearch?.isLoading); + const { foundIds } = (currentSearch?.currentSegment) || {}; + collectedMessageIds = foundIds; + } else if (origin === MediaViewerOrigin.SharedMedia) { + const currentSearch = selectCurrentSharedMediaSearch(global); + const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {}; + collectedMessageIds = foundIds; + } else if (isOriginInline || isOriginAlbum) { + const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, messageId); + collectedMessageIds = outlyingList || selectListedIds(global, chatId, threadId); + } } return { chatId, threadId, - mediaId, - senderId: message.senderId, + messageId, isChatWithSelf, origin, message, chatMessages, - collectionIds, + collectedMessageIds, withAnimation, isHidden, shouldSkipHistoryAnimations, withDynamicLoading, + standaloneMedia, + mediaIndex, isLoadingMoreMedia, isSynced, }; diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index 4fe2e1ce6..f917e7d13 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -2,20 +2,27 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useMemo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { - ApiMessage, ApiPeer, ApiPhoto, -} from '../../api/types'; -import type { MessageListType } from '../../global/types'; +import type { ActiveDownloads, MessageListType } from '../../global/types'; +import type { MediaViewerOrigin } from '../../types'; import type { MenuItemProps } from '../ui/MenuItem'; +import type { MediaViewerItem } from './helpers/getViewableMedia'; -import { getMessageMediaFormat, getMessageMediaHash, isUserId } from '../../global/helpers'; import { + getIsDownloading, + getMediaFilename, + getMediaFormat, + getMediaHash, + isUserId, +} from '../../global/helpers'; +import { + selectActiveDownloads, selectAllowedMessageActions, selectCurrentMessageList, selectIsChatProtected, - selectIsDownloading, selectIsMessageProtected, + selectTabState, } from '../../global/selectors'; +import getViewableMedia from './helpers/getViewableMedia'; import useAppLayout from '../../hooks/useAppLayout'; import useFlag from '../../hooks/useFlag'; @@ -34,26 +41,22 @@ import ProgressSpinner from '../ui/ProgressSpinner'; import './MediaViewerActions.scss'; type StateProps = { - isDownloading?: boolean; + activeDownloads: ActiveDownloads; isProtected?: boolean; isChatProtected?: boolean; canDelete?: boolean; canUpdate?: boolean; messageListType?: MessageListType; - avatarOwnerId?: string; + origin?: MediaViewerOrigin; }; type OwnProps = { + item?: MediaViewerItem; mediaData?: string; isVideo: boolean; - message?: ApiMessage; canUpdateMedia?: boolean; - isSingleMedia?: boolean; - avatarPhoto?: ApiPhoto; - avatarOwner?: ApiPeer; - fileName?: string; canReport?: boolean; - selectMedia: (mediaId?: number) => void; + activeDownloads?: ActiveDownloads; onReport: NoneToVoidFunction; onBeforeDelete: NoneToVoidFunction; onCloseMediaViewer: NoneToVoidFunction; @@ -61,20 +64,17 @@ type OwnProps = { }; const MediaViewerActions: FC = ({ + item, mediaData, isVideo, - message, - avatarPhoto, - avatarOwnerId, - fileName, isChatProtected, - isDownloading, isProtected, canReport, canDelete, canUpdate, messageListType, - selectMedia, + activeDownloads, + origin, onReport, onCloseMediaViewer, onBeforeDelete, @@ -85,23 +85,32 @@ const MediaViewerActions: FC = ({ const { isMobile } = useAppLayout(); const { - downloadMessageMedia, - cancelMessageMediaDownload, + downloadMedia, + cancelMediaDownload, updateProfilePhoto, updateChatPhoto, + openMediaViewer, } = getActions(); + const isMessage = item?.type === 'message'; + + const { media } = getViewableMedia(item) || {}; + const fileName = media && getMediaFilename(media); + const isDownloading = media && getIsDownloading(activeDownloads, media); + const { loadProgress: downloadProgress } = useMediaWithLoadProgress( - message && getMessageMediaHash(message, 'download'), + media && getMediaHash(media, 'download'), !isDownloading, - message && getMessageMediaFormat(message, 'download'), + media && getMediaFormat(media, 'download'), ); const handleDownloadClick = useLastCallback(() => { + if (!media) return; + if (isDownloading) { - cancelMessageMediaDownload({ message: message! }); + cancelMediaDownload({ media }); } else { - downloadMessageMedia({ message: message! }); + downloadMedia({ media }); } }); @@ -118,13 +127,23 @@ const MediaViewerActions: FC = ({ }); const handleUpdate = useLastCallback(() => { - if (!avatarPhoto || !avatarOwnerId) return; - if (isUserId(avatarOwnerId)) { + if (item?.type !== 'avatar') return; + const { avatarOwner, mediaIndex } = item; + const avatarPhoto = avatarOwner.photos?.[mediaIndex]!; + if (isUserId(avatarOwner.id)) { updateProfilePhoto({ photo: avatarPhoto }); } else { - updateChatPhoto({ chatId: avatarOwnerId, photo: avatarPhoto }); + updateChatPhoto({ chatId: avatarOwner.id, photo: avatarPhoto }); } - selectMedia(0); + + openMediaViewer({ + origin: origin!, + chatId: avatarOwner.id, + mediaIndex: 0, + isAvatarView: true, + }, { + forceOnHeavyAnimation: true, + }); }); const lang = useOldLang(); @@ -145,29 +164,34 @@ const MediaViewerActions: FC = ({ }, []); function renderDeleteModals() { - return message - ? ( + if (item?.type === 'message') { + return ( - ) - : (avatarOwnerId && avatarPhoto) ? ( + ); + } + if (item?.type === 'avatar') { + return ( - ) : undefined; + ); + } + + return undefined; } function renderDownloadButton() { - if (isProtected) { + if (isProtected || item?.type === 'standalone') { return undefined; } @@ -201,7 +225,7 @@ const MediaViewerActions: FC = ({ if (isMobile) { const menuItems: MenuItemProps[] = []; - if (message?.isForwardingAllowed && !isChatProtected) { + if (isMessage && item.message.isForwardingAllowed && !item.message.content.action && !isChatProtected) { menuItems.push({ icon: 'forward', onClick: onForward, @@ -283,7 +307,7 @@ const MediaViewerActions: FC = ({ return (
- {message?.isForwardingAllowed && !isChatProtected && ( + {isMessage && item.message.isForwardingAllowed && !isChatProtected && ( + )} + {paidMedia.isBought && ( +
+ {isOutgoing ? formatCurrency(paidMedia.starsAmount, STARS_CURRENCY_CODE) : lang('Chat.PaidMedia.Purchased')} +
+ )} +
+ ); +}; + +export default memo(PaidMediaOverlay); diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index cb63c8e17..18e03beb9 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -1,7 +1,6 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { useEffect, useRef, useState } from '../../../lib/teact/teact'; -import type { ApiMessage } from '../../../api/types'; +import type { ApiMediaExtendedPreview, ApiPhoto } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ISettings } from '../../../types'; import type { IMediaDimensions } from './helpers/calculateAlbumLayout'; @@ -9,13 +8,10 @@ import type { IMediaDimensions } from './helpers/calculateAlbumLayout'; import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { + getMediaFormat, + getMediaThumbUri, getMediaTransferState, - getMessageMediaFormat, - getMessageMediaHash, - getMessageMediaThumbDataUri, - getMessagePhoto, - getMessageWebPagePhoto, - isOwnMessage, + getPhotoMediaHash, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import getCustomAppendixBg from './helpers/getCustomAppendixBg'; @@ -35,9 +31,12 @@ import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import MediaSpoiler from '../../common/MediaSpoiler'; import ProgressSpinner from '../../ui/ProgressSpinner'; -export type OwnProps = { +export type OwnProps = { id?: string; - message: ApiMessage; + photo: ApiPhoto | ApiMediaExtendedPreview; + isInWebPage?: boolean; + messageText?: string; + isOwn?: boolean; observeIntersection?: ObserveFn; noAvatars?: boolean; canAutoLoad?: boolean; @@ -53,13 +52,18 @@ export type OwnProps = { isDownloading?: boolean; isProtected?: boolean; theme: ISettings['theme']; - onClick?: (id: number) => void; - onCancelUpload?: (message: ApiMessage) => void; + className?: string; + clickArg?: T; + onClick?: (arg: T, e: React.MouseEvent) => void; + onCancelUpload?: (arg: T) => void; }; -const Photo: FC = ({ +// eslint-disable-next-line @typescript-eslint/comma-dangle +const Photo = ({ id, - message, + photo, + messageText, + isOwn, observeIntersection, noAvatars, canAutoLoad, @@ -75,53 +79,57 @@ const Photo: FC = ({ isDownloading, isProtected, theme, + isInWebPage, + clickArg, + className, onClick, onCancelUpload, -}) => { +}: OwnProps) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); + const isPaidPreview = photo.mediaType === 'extendedMediaPreview'; - const photo = (getMessagePhoto(message) || getMessageWebPagePhoto(message))!; - const localBlobUrl = photo.blobUrl; + const localBlobUrl = !isPaidPreview ? photo.blobUrl : undefined; const isIntersecting = useIsIntersecting(ref, observeIntersection); const { isMobile } = useAppLayout(); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); - const shouldLoad = isLoadAllowed && isIntersecting; + const shouldLoad = isLoadAllowed && isIntersecting && !isPaidPreview; const { mediaData, loadProgress, - } = useMediaWithLoadProgress(getMessageMediaHash(message, size), !shouldLoad); + } = useMediaWithLoadProgress(!isPaidPreview ? getPhotoMediaHash(photo, size) : undefined, !shouldLoad); const fullMediaData = localBlobUrl || mediaData; const withBlurredBackground = Boolean(forcedWidth); const [withThumb] = useState(!fullMediaData); const noThumb = Boolean(fullMediaData); - const thumbRef = useBlurredMediaThumbRef(message, noThumb); - const blurredBackgroundRef = useBlurredMediaThumbRef(message, !withBlurredBackground); + const thumbRef = useBlurredMediaThumbRef(photo, noThumb); + const blurredBackgroundRef = useBlurredMediaThumbRef(photo, !withBlurredBackground); const thumbClassNames = useMediaTransition(!noThumb); - const thumbDataUri = getMessageMediaThumbDataUri(message); + const thumbDataUri = getMediaThumbUri(photo); - const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(photo.isSpoiler); + const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || photo.isSpoiler); useEffect(() => { - if (photo.isSpoiler) { + if (isPaidPreview || photo.isSpoiler) { showSpoiler(); } else { hideSpoiler(); } - }, [photo.isSpoiler]); + }, [isPaidPreview, photo]); const { loadProgress: downloadProgress, } = useMediaWithLoadProgress( - getMessageMediaHash(message, 'download'), !isDownloading, getMessageMediaFormat(message, 'download'), + !isPaidPreview ? getPhotoMediaHash(photo, 'download') : undefined, + !isDownloading, + !isPaidPreview ? getMediaFormat(photo, 'download') : undefined, ); const { isUploading, isTransferring, transferProgress, } = getMediaTransferState( - message, uploadProgress || (isDownloading ? downloadProgress : loadProgress), shouldLoad && !fullMediaData, uploadProgress !== undefined, @@ -137,9 +145,9 @@ const Photo: FC = ({ transitionClassNames: downloadButtonClassNames, } = useShowTransition(!fullMediaData && !isLoadAllowed); - const handleClick = useLastCallback(() => { + const handleClick = useLastCallback((e: React.MouseEvent) => { if (isUploading) { - onCancelUpload?.(message); + onCancelUpload?.(clickArg!); return; } @@ -153,10 +161,9 @@ const Photo: FC = ({ return; } - onClick?.(message.id); + onClick?.(clickArg!, e); }); - const isOwn = isOwnMessage(message); useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => { if (!shouldAffectAppendix) { if (prevShouldAffectAppendix) { @@ -167,7 +174,7 @@ const Photo: FC = ({ const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; if (fullMediaData) { - getCustomAppendixBg(fullMediaData, isOwn, isSelected, theme).then((appendixBg) => { + getCustomAppendixBg(fullMediaData, Boolean(isOwn), isSelected, theme).then((appendixBg) => { requestMutation(() => { contentEl.style.setProperty('--appendix-bg', appendixBg); contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); @@ -178,14 +185,23 @@ const Photo: FC = ({ } }, [shouldAffectAppendix, fullMediaData, isOwn, isInSelectMode, isSelected, theme]); - const { width, height, isSmall } = dimensions || calculateMediaDimensions(message, asForwarded, noAvatars, isMobile); + const { width, height, isSmall } = dimensions || calculateMediaDimensions({ + media: photo, + isOwn, + asForwarded, + noAvatars, + isMobile, + messageText, + isInWebPage, + }); - const className = buildClassName( + const componentClassName = buildClassName( 'media-inner', !isUploading && !nonInteractive && 'interactive', isSmall && 'small-image', width === height && 'square-image', height < MIN_MEDIA_HEIGHT && 'fix-min-height', + className, ); const dimensionsStyle = dimensions ? ` width: ${width}px; left: ${dimensions.x}px; top: ${dimensions.y}px;` : ''; @@ -195,7 +211,7 @@ const Photo: FC = ({
diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index 9a40698be..b9b4edbb3 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -13,7 +13,7 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../../api/types'; import { - getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, hasMessageTtl, + getMediaFormat, getMessageMediaThumbDataUri, getVideoMediaHash, hasMessageTtl, } from '../../../global/helpers'; import { stopCurrentAudio } from '../../../util/audioPlayer'; import buildClassName from '../../../util/buildClassName'; @@ -76,20 +76,20 @@ const RoundVideo: FC = ({ const video = message.content.video!; - const { cancelMessageMediaDownload, openOneTimeMediaModal } = getActions(); + const { cancelMediaDownload, openOneTimeMediaModal } = getActions(); const isIntersecting = useIsIntersecting(ref, observeIntersection); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); const shouldLoad = Boolean(isLoadAllowed && isIntersecting); const { mediaData, loadProgress } = useMediaWithLoadProgress( - getMessageMediaHash(message, 'inline'), + getVideoMediaHash(video, 'inline'), !shouldLoad, - getMessageMediaFormat(message, 'inline'), + getMediaFormat(video, 'inline'), ); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( - getMessageMediaHash(message, 'download'), + getVideoMediaHash(video, 'download'), !isDownloading, ApiMediaFormat.BlobUrl, ); @@ -100,7 +100,7 @@ const RoundVideo: FC = ({ const shouldRenderSpoiler = hasTtl && !isInOneTimeModal; const hasThumb = Boolean(getMessageMediaThumbDataUri(message)); const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler; - const thumbRef = useBlurredMediaThumbRef(message, noThumb); + const thumbRef = useBlurredMediaThumbRef(video, noThumb); const thumbClassNames = useMediaTransition(!noThumb); const thumbDataUri = getMessageMediaThumbDataUri(message); const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading; @@ -186,7 +186,7 @@ const RoundVideo: FC = ({ } if (isDownloading) { - cancelMessageMediaDownload({ message }); + cancelMediaDownload({ media: video }); return; } diff --git a/src/components/middle/message/Sticker.tsx b/src/components/middle/message/Sticker.tsx index ac409ec68..5eff96219 100644 --- a/src/components/middle/message/Sticker.tsx +++ b/src/components/middle/message/Sticker.tsx @@ -6,7 +6,7 @@ import type { ApiMessage } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { ApiMediaFormat } from '../../../api/types'; -import { getMessageMediaHash } from '../../../global/helpers'; +import { getStickerMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { IS_WEBM_SUPPORTED } from '../../../util/windowEnvironment'; import { getStickerDimensions } from '../../common/helpers/mediaDimensions'; @@ -58,7 +58,7 @@ const Sticker: FC = ({ const isMirrored = !message.isOutgoing; const mediaHash = sticker.isPreloadedGlobally ? undefined : ( - getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')! + getStickerMediaHash(sticker, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')! ); const canLoad = useIsIntersecting(ref, observeIntersection); diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index c1972f9b0..1b4c9e7db 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -1,24 +1,20 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { useEffect, useRef, useState } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; -import type { ApiMessage } from '../../../api/types'; +import type { ApiMediaExtendedPreview, ApiVideo } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { IMediaDimensions } from './helpers/calculateAlbumLayout'; import { + getMediaFormat, + getMediaThumbUri, getMediaTransferState, - getMessageMediaFormat, - getMessageMediaHash, - getMessageMediaThumbDataUri, - getMessageVideo, - getMessageWebPageVideo, - isOwnMessage, + getVideoMediaHash, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { formatMediaDuration } from '../../../util/dates/dateFormat'; import * as mediaLoader from '../../../util/mediaLoader'; -import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions'; +import { calculateExtendedPreviewDimensions, calculateVideoDimensions } from '../../common/helpers/mediaDimensions'; import { MIN_MEDIA_HEIGHT } from './helpers/mediaDimensions'; import useUnsupportedMedia from '../../../hooks/media/useUnsupportedMedia'; @@ -37,10 +33,12 @@ import MediaSpoiler from '../../common/MediaSpoiler'; import OptimizedVideo from '../../ui/OptimizedVideo'; import ProgressSpinner from '../../ui/ProgressSpinner'; -export type OwnProps = { +export type OwnProps = { id?: string; - message: ApiMessage; - observeIntersectionForLoading: ObserveFn; + video: ApiVideo | ApiMediaExtendedPreview; + isOwn?: boolean; + isInWebPage?: boolean; + observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; noAvatars?: boolean; canAutoLoad?: boolean; @@ -51,13 +49,18 @@ export type OwnProps = { asForwarded?: boolean; isDownloading?: boolean; isProtected?: boolean; - onClick?: (id: number, isGif?: boolean) => void; - onCancelUpload?: (message: ApiMessage) => void; + className?: string; + clickArg?: T; + onClick?: (arg: T, e: React.MouseEvent) => void; + onCancelUpload?: (arg: T) => void; }; -const Video: FC = ({ +// eslint-disable-next-line @typescript-eslint/comma-dangle +const Video = ({ id, - message, + video, + isOwn, + isInWebPage, observeIntersectionForLoading, observeIntersectionForPlaying, noAvatars, @@ -69,26 +72,30 @@ const Video: FC = ({ asForwarded, isDownloading, isProtected, + className, + clickArg, onClick, onCancelUpload, -}) => { +}: OwnProps) => { + const { cancelMediaDownload } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const videoRef = useRef(null); - const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!; - const localBlobUrl = video.blobUrl; + const isPaidPreview = video.mediaType === 'extendedMediaPreview'; - const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(video.isSpoiler); + const localBlobUrl = !isPaidPreview ? video.blobUrl : undefined; + + const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || video.isSpoiler); useEffect(() => { - if (video.isSpoiler) { + if (isPaidPreview || video.isSpoiler) { showSpoiler(); } else { hideSpoiler(); } - }, [video.isSpoiler]); + }, [isPaidPreview, video]); const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading); const isIntersectingForPlaying = ( @@ -102,43 +109,44 @@ const Video: FC = ({ const { isMobile } = useAppLayout(); const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad); - const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading); + const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading && !isPaidPreview); const [isPlayAllowed, setIsPlayAllowed] = useState(Boolean(canAutoPlay && !isSpoilerShown)); - const fullMediaHash = getMessageMediaHash(message, 'inline'); + const fullMediaHash = !isPaidPreview ? getVideoMediaHash(video, 'inline') : undefined; const [isFullMediaPreloaded] = useState(Boolean(fullMediaHash && mediaLoader.getFromMemory(fullMediaHash))); const { mediaData, loadProgress } = useMediaWithLoadProgress( - fullMediaHash, !shouldLoad, getMessageMediaFormat(message, 'inline'), + fullMediaHash, + !shouldLoad, + !isPaidPreview ? getMediaFormat(video, 'inline') : undefined, ); const fullMediaData = localBlobUrl || mediaData; const [isPlayerReady, markPlayerReady] = useFlag(); - const thumbDataUri = getMessageMediaThumbDataUri(message); + const thumbDataUri = getMediaThumbUri(video); const hasThumb = Boolean(thumbDataUri); const withBlurredBackground = Boolean(forcedWidth); - const previewMediaHash = getMessageMediaHash(message, 'preview'); + const previewMediaHash = !isPaidPreview ? getVideoMediaHash(video, 'preview') : undefined; const [isPreviewPreloaded] = useState(Boolean(previewMediaHash && mediaLoader.getFromMemory(previewMediaHash))); const canLoadPreview = isIntersectingForLoading; const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview); const previewClassNames = useMediaTransition((hasThumb || previewBlobUrl) && !isPlayerReady); - const noThumb = !hasThumb || previewBlobUrl || isPlayerReady; - const thumbRef = useBlurredMediaThumbRef(message, noThumb); - const blurredBackgroundRef = useBlurredMediaThumbRef(message, !withBlurredBackground); + const noThumb = Boolean(!hasThumb || previewBlobUrl || isPlayerReady); + const thumbRef = useBlurredMediaThumbRef(video, noThumb); + const blurredBackgroundRef = useBlurredMediaThumbRef(video, !withBlurredBackground); const thumbClassNames = useMediaTransition(!noThumb); const isInline = fullMediaData && wasIntersectedRef.current; const isUnsupported = useUnsupportedMedia(videoRef, true, !isInline); const { loadProgress: downloadProgress } = useMediaWithLoadProgress( - getMessageMediaHash(message, 'download'), + !isPaidPreview ? getVideoMediaHash(video, 'download') : undefined, !isDownloading, - getMessageMediaFormat(message, 'download'), + !isPaidPreview ? getMediaFormat(video, 'download') : undefined, ); const { isUploading, isTransferring, transferProgress } = getMediaTransferState( - message, uploadProgress || (isDownloading ? downloadProgress : loadProgress), (shouldLoad && !isPlayerReady && !isFullMediaPreloaded) || isDownloading, uploadProgress !== undefined, @@ -160,20 +168,22 @@ const Video: FC = ({ const duration = (Number.isFinite(videoRef.current?.duration) ? videoRef.current?.duration : video.duration) || 0; - const isOwn = isOwnMessage(message); - const isWebPageVideo = Boolean(getMessageWebPageVideo(message)); const { width, height, - } = dimensions || calculateVideoDimensions(video, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile); + } = dimensions || ( + isPaidPreview + ? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile) + : calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile) + ); - const handleClick = useLastCallback(() => { + const handleClick = useLastCallback((e: React.MouseEvent) => { if (isUploading) { - onCancelUpload?.(message); + onCancelUpload?.(clickArg!); return; } - if (isDownloading) { - getActions().cancelMessageMediaDownload({ message }); + if (!isPaidPreview && isDownloading) { + cancelMediaDownload({ media: video }); return; } @@ -191,13 +201,14 @@ const Video: FC = ({ return; } - onClick?.(message.id, video?.isGif); + onClick?.(clickArg!, e); }); - const className = buildClassName( + const componentClassName = buildClassName( 'media-inner dark', !isUploading && 'interactive', height < MIN_MEDIA_HEIGHT && 'fix-min-height', + className, ); const dimensionsStyle = dimensions ? ` width: ${width}px; left: ${dimensions.x}px; top: ${dimensions.y}px;` : ''; @@ -207,7 +218,7 @@ const Video: FC = ({
@@ -266,7 +277,7 @@ const Video: FC = ({ ) : (
- {video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))} + {!isPaidPreview && video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))} {isUnsupported && }
)} diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index a3bdd7aa6..eabc7be7c 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -137,7 +137,14 @@ const WebPage: FC = ({ const isArticle = Boolean(truncatedDescription || title || siteName); let isSquarePhoto = Boolean(stickers); if (isArticle && webPage?.photo && !webPage.video) { - const { width, height } = calculateMediaDimensions(message, undefined, undefined, isMobile); + const { width, height } = calculateMediaDimensions({ + media: webPage.photo, + isOwn: message.isOutgoing, + isInWebPage: true, + asForwarded, + noAvatars, + isMobile, + }); isSquarePhoto = width === height; } const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto; @@ -188,7 +195,9 @@ const WebPage: FC = ({ )} {photo && !video && ( = ({ )} {!inPreview && video && (