diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index d2ea56634..2501743be 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1,7 +1,4 @@ import { Api as GramJs } from '../../../lib/gramjs'; -import { - ApiMessageEntityTypes, -} from '../../types'; import type { ApiMessage, ApiMessageForwardInfo, @@ -36,6 +33,10 @@ import type { PhoneCallAction, ApiWebDocument, ApiMessageEntityDefault, + ApiMessageExtendedMediaPreview, +} from '../../types'; +import { + ApiMessageEntityTypes, } from '../../types'; import { @@ -161,16 +162,21 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM content.action = action; } + const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice + && Boolean(mtpMessage.media.extendedMedia); + const { replyToMsgId, replyToTopId, replyToPeerId } = mtpMessage.replyTo || {}; const isEdited = mtpMessage.editDate && !mtpMessage.editHide; const { inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, - } = buildReplyButtons(mtpMessage) || {}; + } = buildReplyButtons(mtpMessage, isInvoiceMedia) || {}; const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf); const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage; const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId); const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker); const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide; + const isProtected = mtpMessage.noforwards || isInvoiceMedia; + const isForwardingAllowed = !mtpMessage.noforwards; const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text); return { @@ -205,7 +211,8 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM ...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }), ...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }), ...(postAuthor && { adminTitle: postAuthor }), - ...(mtpMessage.noforwards && { isProtected: true }), + isProtected, + isForwardingAllowed, }; } @@ -336,6 +343,10 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes return undefined; } + if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) { + return buildMessageMediaContent(media.extendedMedia.media); + } + const sticker = buildSticker(media); if (sticker) return { sticker }; @@ -748,9 +759,12 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { const { - description: text, title, photo, test, totalAmount, currency, receiptMsgId, + description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia, } = media; + const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview + ? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined; + return { title, text, @@ -759,6 +773,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { amount: Number(totalAmount), currency, isTest: test, + extendedMedia: preview, }; } @@ -1007,7 +1022,7 @@ function buildAction( }; } -function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefined { +function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: boolean): ApiReplyKeyboard | undefined { const { replyMarkup, media } = message; // TODO Move to the proper button inside preview @@ -1033,7 +1048,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi } const markup = replyMarkup.rows.map(({ buttons }) => { - return buttons.map((button): ApiKeyboardButton => { + return buttons.map((button): ApiKeyboardButton | undefined => { const { text } = button; if (button instanceof GramJs.KeyboardButton) { @@ -1096,6 +1111,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi receiptMessageId: media.receiptMsgId, }; } + if (shouldSkipBuyButton) return undefined; return { type: 'buy', text, @@ -1155,9 +1171,11 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi type: 'unsupported', text, }; - }); + }).filter(Boolean); }); + if (markup.every((row) => !row.length)) return undefined; + return { [replyMarkup instanceof GramJs.ReplyKeyboardMarkup ? 'keyboardButtons' : 'inlineButtons']: markup, ...(replyMarkup instanceof GramJs.ReplyKeyboardMarkup && { @@ -1368,6 +1386,21 @@ function buildUploadingMedia( }; } +export function buildApiMessageExtendedMediaPreview( + preview: GramJs.MessageExtendedMediaPreview, +): ApiMessageExtendedMediaPreview { + const { + w, h, thumb, videoDuration, + } = preview; + + return { + width: w, + height: h, + duration: videoDuration, + thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined, + }; +} + export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined { if (!document) return undefined; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 7d79421a1..22c0d7959 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -353,7 +353,7 @@ export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServic media instanceof GramJs.MessageMediaGame && (media.game.document instanceof GramJs.Document || media.game.photo instanceof GramJs.Photo) ) || ( - media instanceof GramJs.MessageMediaInvoice && media.photo + media instanceof GramJs.MessageMediaInvoice && (media.photo || media.extendedMedia) ) ); } diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index 8c01d2bda..9e79fba17 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -26,37 +26,48 @@ export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) { export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) { const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`; - localDb.messages[messageFullId] = message; - if (message instanceof GramJs.Message) { - if (message.media instanceof GramJs.MessageMediaDocument - && message.media.document instanceof GramJs.Document + 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 instanceof GramJs.MessageMediaDocument + && mockMessage.media.document instanceof GramJs.Document ) { - localDb.documents[String(message.media.document.id)] = message.media.document; + localDb.documents[String(mockMessage.media.document.id)] = mockMessage.media.document; } - if (message.media instanceof GramJs.MessageMediaWebPage - && message.media.webpage instanceof GramJs.WebPage - && message.media.webpage.document instanceof GramJs.Document + if (mockMessage.media instanceof GramJs.MessageMediaWebPage + && mockMessage.media.webpage instanceof GramJs.WebPage + && mockMessage.media.webpage.document instanceof GramJs.Document ) { - localDb.documents[String(message.media.webpage.document.id)] = message.media.webpage.document; + localDb.documents[String(mockMessage.media.webpage.document.id)] = mockMessage.media.webpage.document; } - if (message.media instanceof GramJs.MessageMediaGame) { - if (message.media.game.document instanceof GramJs.Document) { - localDb.documents[String(message.media.game.document.id)] = message.media.game.document; + if (mockMessage.media instanceof GramJs.MessageMediaGame) { + if (mockMessage.media.game.document instanceof GramJs.Document) { + localDb.documents[String(mockMessage.media.game.document.id)] = mockMessage.media.game.document; } - addPhotoToLocalDb(message.media.game.photo); + addPhotoToLocalDb(mockMessage.media.game.photo); } - if (message.media instanceof GramJs.MessageMediaInvoice - && message.media.photo) { - localDb.webDocuments[String(message.media.photo.url)] = message.media.photo; + if (mockMessage.media instanceof GramJs.MessageMediaInvoice + && mockMessage.media.photo) { + localDb.webDocuments[String(mockMessage.media.photo.url)] = mockMessage.media.photo; } } - if (message instanceof GramJs.MessageService && 'photo' in message.action) { - addPhotoToLocalDb(message.action.photo); + if (mockMessage instanceof GramJs.MessageService && 'photo' in mockMessage.action) { + addPhotoToLocalDb(mockMessage.action.photo); } } @@ -90,6 +101,24 @@ export function addEntitiesWithPhotosToLocalDb(entities: (GramJs.TypeUser | Gram }); } +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/methods/index.ts b/src/api/gramjs/methods/index.ts index be5429a16..2a40cfbf9 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -29,7 +29,7 @@ export { fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, - closePoll, + closePoll, fetchExtendedMedia, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index d2118ea93..c3cc4d935 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1122,6 +1122,18 @@ export async function loadPollOptionResults({ }; } +export async function fetchExtendedMedia({ + chat, ids, +} : { + chat: ApiChat; + ids: number[]; +}) { + await invokeRequest(new GramJs.messages.GetExtendedMedia({ + peer: buildInputPeer(chat.id, chat.accessHash), + id: ids, + })); +} + export async function forwardMessages({ fromChat, toChat, diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 423dd39ab..ad81c2ba2 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -1,6 +1,8 @@ import type { GroupCallConnectionData } from '../../lib/secret-sauce'; import { Api as GramJs, connection } from '../../lib/gramjs'; -import type { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types'; +import type { + ApiMessage, ApiMessageExtendedMediaPreview, ApiUpdateConnectionStateType, OnApiUpdate, +} from '../types'; import { pick } from '../../util/iteratees'; import { @@ -14,6 +16,7 @@ import { buildApiMessageFromNotification, buildMessageDraft, buildMessageReactions, + buildApiMessageExtendedMediaPreview, } from './apiBuilders/messages'; import { buildChatMember, @@ -40,6 +43,7 @@ import { resolveMessageApiChatId, serializeBytes, log, + swapLocalInvoiceMedia, } from './helpers'; import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc'; import { buildApiPhoto } from './apiBuilders/common'; @@ -309,6 +313,30 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { chatId: getApiChatIdFromMtpPeer(update.peer), reactions: buildMessageReactions(update.reactions), }); + } else if (update instanceof GramJs.UpdateMessageExtendedMedia) { + let media: ApiMessage['content'] | 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); + + swapLocalInvoiceMedia(chatId, update.msgId, update.extendedMedia); + + onUpdate({ + '@type': 'updateMessageExtendedMedia', + id: update.msgId, + chatId, + media, + preview, + }); } else if (update instanceof GramJs.UpdateDeleteMessages) { onUpdate({ '@type': 'deleteMessages', diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index d030fca1a..f710133b5 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -187,10 +187,18 @@ export interface ApiInvoice { isTest?: boolean; isRecurring?: boolean; recurringTermsUrl?: string; + extendedMedia?: ApiMessageExtendedMediaPreview; maxTipAmount?: number; suggestedTipAmounts?: number[]; } +export interface ApiMessageExtendedMediaPreview { + width?: number; + height?: number; + thumbnail?: ApiThumbnail; + duration?: number; +} + export interface ApiPaymentCredentials { id: string; title: string; @@ -406,6 +414,7 @@ export interface ApiMessage { isSilent?: boolean; seenByUserIds?: string[]; isProtected?: boolean; + isForwardingAllowed?: boolean; transcriptionId?: string; isTranscriptionError?: boolean; emojiOnlyCount?: number; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 18f477dae..f16f870c5 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -13,7 +13,14 @@ import type { ApiChatFolder, } from './chats'; import type { - ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiReactions, ApiStickerSet, ApiThreadInfo, + ApiFormattedText, + ApiMessage, + ApiMessageExtendedMediaPreview, + ApiPhoto, + ApiPoll, + ApiReactions, + ApiStickerSet, + ApiThreadInfo, } from './messages'; import type { ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus, @@ -309,6 +316,14 @@ export type ApiUpdateMessageReactions = { reactions: ApiReactions; }; +export type ApiUpdateMessageExtendedMedia = { + '@type': 'updateMessageExtendedMedia'; + id: number; + chatId: string; + media?: ApiMessage['content']; + preview?: ApiMessageExtendedMediaPreview; +}; + export type ApiDeleteContact = { '@type': 'deleteContact'; id: string; @@ -556,7 +571,8 @@ export type ApiUpdate = ( ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | - ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus + ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | + ApiUpdateMessageExtendedMedia ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/assets/turbulence.png b/src/assets/turbulence.png new file mode 100644 index 000000000..e8a9a7b0d Binary files /dev/null and b/src/assets/turbulence.png differ diff --git a/src/assets/turbulence_2x.png b/src/assets/turbulence_2x.png new file mode 100644 index 000000000..200ba282b Binary files /dev/null and b/src/assets/turbulence_2x.png differ diff --git a/src/components/common/helpers/mediaDimensions.ts b/src/components/common/helpers/mediaDimensions.ts index 4b9468c8e..7bce7a030 100644 --- a/src/components/common/helpers/mediaDimensions.ts +++ b/src/components/common/helpers/mediaDimensions.ts @@ -81,7 +81,7 @@ function getAvailableHeight(isGif?: boolean, aspectRatio?: number) { return 27 * REM; } -function calculateDimensionsForMessageMedia({ +export function calculateDimensionsForMessageMedia({ width, height, fromOwnMessage, diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 41f588fd7..c3262894b 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -320,7 +320,6 @@ const MediaViewer: FC = ({ onForward={handleForward} zoomLevelChange={zoomLevelChange} setZoomLevelChange={setZoomLevelChange} - isAvatar={Boolean(avatarOwner)} /> = ({ isVideo, message, fileName, - isAvatar, + isChatProtected, isDownloading, isProtected, canReport, + zoomLevelChange, + canDelete, + messageListType, onReport, onCloseMediaViewer, - zoomLevelChange, - setZoomLevelChange, - canDelete, onForward, - messageListType, + setZoomLevelChange, }) => { const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false); @@ -148,7 +153,7 @@ const MediaViewerActions: FC = ({ if (IS_SINGLE_COLUMN_LAYOUT) { const menuItems: MenuItemProps[] = []; - if (!isAvatar && !isProtected) { + if (!message?.isForwardingAllowed && !isChatProtected) { menuItems.push({ icon: 'forward', onClick: onForward, @@ -227,7 +232,7 @@ const MediaViewerActions: FC = ({ return (
- {!isAvatar && !isProtected && ( + {message?.isForwardingAllowed && !isChatProtected && (