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 && (