diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 7fb7e7b42..9df28b843 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -24,7 +24,6 @@ import type { import { numberToHexColor } from '../../../util/colors'; import { pick } from '../../../util/iteratees'; -import { generateRandomInt } from '../gramjsBuilders'; import { addDocumentToLocalDb } from '../helpers/localDb'; import { serializeBytes } from '../helpers/misc'; import { buildApiMessageEntity, buildApiPhoto } from './common'; @@ -255,16 +254,6 @@ export function buildBotInlineMessage( currency: sendMessage.currency, amount: sendMessage.totalAmount.toJSNumber(), }; - } else { - const mediaSize = sendMessage.forceSmallMedia ? 'small' : sendMessage.forceLargeMedia ? 'large' : undefined; - - content.webPage = { - mediaType: 'webpage', - id: generateRandomInt(), - mediaSize, - url: sendMessage.url, - displayUrl: sendMessage.url, - }; } return { diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 23055b0ca..11b0e83e3 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -13,6 +13,7 @@ import type { ApiMediaInvoice, ApiMediaTodo, ApiMessageStoryData, + ApiMessageWebPage, ApiPaidMedia, ApiPhoto, ApiPoll, @@ -35,7 +36,7 @@ import { addTimestampEntities } from '../../../util/dates/timestamp'; import { generateWaveform } from '../../../util/generateWaveform'; import { pick } from '../../../util/iteratees'; import { - addMediaToLocalDb, addStoryToLocalDb, type MediaRepairContext, + addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext, } from '../helpers/localDb'; import { serializeBytes } from '../helpers/misc'; import { @@ -158,7 +159,7 @@ export function buildMessageMediaContent( const todo = buildTodoFromMedia(media); if (todo) return { todo }; - const webPage = buildWebPage(media); + const webPage = buildMessageWebPageFromMedia(media); if (webPage) return { webPage }; const invoice = buildInvoiceFromMedia(media); @@ -798,85 +799,126 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu }; } -export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined { - if ( - !(media instanceof GramJs.MessageMediaWebPage) - || !(media.webpage instanceof GramJs.WebPage) - ) { +export function buildMessageWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiMessageWebPage | undefined { + if (!(media instanceof GramJs.MessageMediaWebPage) || media.webpage instanceof GramJs.WebPageNotModified) { return undefined; } - const { - id, photo, document, attributes, - } = media.webpage; - - let video; - let audio; - if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) { - video = buildVideoFromDocument(document); - } - if (document instanceof GramJs.Document && document.mimeType.startsWith('audio/')) { - audio = buildAudioFromDocument(document); - } - let story: ApiWebPageStoryData | undefined; - let gift: ApiStarGiftUnique | undefined; - let stickers: ApiWebPageStickerData | undefined; - const attributeStory = attributes - ?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); - const attributeGift = attributes - ?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift); - if (attributeStory) { - const peerId = getApiChatIdFromMtpPeer(attributeStory.peer); - story = { - id: attributeStory.id, - peerId, - }; - - if (attributeStory.story instanceof GramJs.StoryItem) { - addStoryToLocalDb(attributeStory.story, peerId); - } - } - if (attributeGift) { - const starGift = buildApiStarGift(attributeGift.gift); - gift = starGift.type === 'starGiftUnique' ? starGift : undefined; - } - const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => ( - a instanceof GramJs.WebPageAttributeStickerSet - )); - if (attributeStickers) { - stickers = { - documents: processStickerResult(attributeStickers.stickers), - isEmoji: attributeStickers.emojis, - isWithTextColor: attributeStickers.textColor, - }; - } - - const mediaSize = media.forceSmallMedia ? 'small' : media.forceLargeMedia ? 'large' : undefined; + webpage, forceLargeMedia, forceSmallMedia, safe, + } = media; return { - mediaType: 'webpage', - id: Number(id), - ...pick(media.webpage, [ - 'url', - 'displayUrl', - 'type', - 'siteName', - 'title', - 'description', - 'duration', - 'hasLargeMedia', - ]), - photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined, - document: !video && !audio && document ? buildApiDocument(document) : undefined, - video, - audio, - story, - gift, - stickers, - mediaSize, + id: webpage.id.toString(), + isSafe: safe, + mediaSize: forceSmallMedia ? 'small' : forceLargeMedia ? 'large' : undefined, }; } +export function buildWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiWebPage | undefined { + if (!(media instanceof GramJs.MessageMediaWebPage)) { + return undefined; + } + const { + webpage, + } = media; + + return buildWebPage(webpage); +} + +export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefined { + addWebPageMediaToLocalDb(webPage); + + if (webPage instanceof GramJs.WebPageEmpty) { + return { + mediaType: 'webpage', + webpageType: 'empty', + id: webPage.id.toString(), + url: webPage.url, + }; + } + + if (webPage instanceof GramJs.WebPagePending) { + return { + mediaType: 'webpage', + webpageType: 'pending', + id: webPage.id.toString(), + url: webPage.url, + }; + } + + if (webPage instanceof GramJs.WebPage) { + const { + id, photo, document, attributes, + } = webPage; + + let video; + let audio; + if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) { + video = buildVideoFromDocument(document); + } + if (document instanceof GramJs.Document && document.mimeType.startsWith('audio/')) { + audio = buildAudioFromDocument(document); + } + let story: ApiWebPageStoryData | undefined; + let gift: ApiStarGiftUnique | undefined; + let stickers: ApiWebPageStickerData | undefined; + const attributeStory = attributes + ?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory); + const attributeGift = attributes + ?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift); + if (attributeStory) { + const peerId = getApiChatIdFromMtpPeer(attributeStory.peer); + story = { + id: attributeStory.id, + peerId, + }; + + if (attributeStory.story instanceof GramJs.StoryItem) { + addStoryToLocalDb(attributeStory.story, peerId); + } + } + if (attributeGift) { + const starGift = buildApiStarGift(attributeGift.gift); + gift = starGift.type === 'starGiftUnique' ? starGift : undefined; + } + const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => ( + a instanceof GramJs.WebPageAttributeStickerSet + )); + if (attributeStickers) { + stickers = { + documents: processStickerResult(attributeStickers.stickers), + isEmoji: attributeStickers.emojis, + isWithTextColor: attributeStickers.textColor, + }; + } + + return { + mediaType: 'webpage', + webpageType: 'full', + id: id.toString(), + ...pick(webPage, [ + 'url', + 'displayUrl', + 'type', + 'siteName', + 'title', + 'description', + 'duration', + 'hasLargeMedia', + ]), + photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined, + document: !video && !audio && document ? buildApiDocument(document) : undefined, + video, + audio, + story, + gift, + stickers, + }; + } + + return undefined; +} + function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined { if (!(media instanceof GramJs.MessageMediaPaidMedia)) { return undefined; diff --git a/src/api/gramjs/helpers/localDb.ts b/src/api/gramjs/helpers/localDb.ts index 1e40e9c1b..cadf632f6 100644 --- a/src/api/gramjs/helpers/localDb.ts +++ b/src/api/gramjs/helpers/localDb.ts @@ -25,25 +25,25 @@ export function addMessageToLocalDb(message: GramJs.TypeMessage | GramJs.TypeSpo } } +export function addWebPageMediaToLocalDb(webPage: GramJs.TypeWebPage, context?: MediaRepairContext) { + if (webPage instanceof GramJs.WebPage) { + if (webPage.document) { + const document = addMessageRepairInfo(webPage.document, context); + addDocumentToLocalDb(document); + } + if (webPage.photo) { + const photo = addMessageRepairInfo(webPage.photo, context); + addPhotoToLocalDb(photo); + } + } +} + export function addMediaToLocalDb(media: GramJs.TypeMessageMedia, context?: MediaRepairContext) { if (media instanceof GramJs.MessageMediaDocument && media.document) { const document = addMessageRepairInfo(media.document, context); addDocumentToLocalDb(document); } - if (media instanceof GramJs.MessageMediaWebPage - && media.webpage instanceof GramJs.WebPage - ) { - if (media.webpage.document) { - const document = addMessageRepairInfo(media.webpage.document, context); - addDocumentToLocalDb(document); - } - if (media.webpage.photo) { - const photo = addMessageRepairInfo(media.webpage.photo, context); - addPhotoToLocalDb(photo); - } - } - if (media instanceof GramJs.MessageMediaGame) { if (media.game.document) { const document = addMessageRepairInfo(media.game.document, context); diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 942f814f5..8caebfd0a 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -29,6 +29,7 @@ import type { ApiTodoItem, ApiUser, ApiUserStatus, + ApiWebPage, MediaContent, } from '../../types'; import { @@ -60,7 +61,8 @@ import { } from '../apiBuilders/chats'; import { buildApiFormattedText } from '../apiBuilders/common'; import { - buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia, buildWebPage, + buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia, + buildWebPageFromMedia, } from '../apiBuilders/messageContent'; import { buildApiFactCheck, @@ -1709,7 +1711,9 @@ export async function fetchWebPagePreview({ entities: textWithEntities.entities, })); - return preview && buildWebPage(preview.media); + if (!preview) return undefined; + + return buildWebPageFromMedia(preview.media); } export async function sendPollVote({ @@ -2363,6 +2367,7 @@ function handleLocalMessageUpdate( let newContent: MediaContent | undefined; let poll: ApiPoll | undefined; + let webPage: ApiWebPage | undefined; if (messageUpdate instanceof GramJs.UpdateShortSentMessage) { if (localMessage.content.text && messageUpdate.entities) { newContent = { @@ -2377,6 +2382,7 @@ function handleLocalMessageUpdate( }), }; poll = buildPollFromMedia(messageUpdate.media); + webPage = buildWebPageFromMedia(messageUpdate.media); } const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate); @@ -2417,6 +2423,7 @@ function handleLocalMessageUpdate( localId: localMessage.id, message: updatedMessage, poll, + webPage, }); } diff --git a/src/api/gramjs/updates/entityProcessor.ts b/src/api/gramjs/updates/entityProcessor.ts index fc67ca380..5bbb45f3f 100644 --- a/src/api/gramjs/updates/entityProcessor.ts +++ b/src/api/gramjs/updates/entityProcessor.ts @@ -2,11 +2,12 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiChat, ApiPoll, ApiThreadInfo, ApiUser, + ApiWebPage, } from '../../types'; import { buildCollectionByKey } from '../../../util/iteratees'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; -import { buildPollFromMedia } from '../apiBuilders/messageContent'; +import { buildPollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent'; import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages'; import { buildApiUser } from '../apiBuilders/users'; import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb'; @@ -24,6 +25,7 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons let chatById: Record | undefined; const threadInfos: ApiThreadInfo[] | undefined = []; const polls: ApiPoll[] | undefined = []; + const webPages: ApiWebPage[] | undefined = []; if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) { const users = response.users.map((user: GramJs.TypeUser) => { @@ -54,9 +56,16 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons threadInfos.push(threadInfo); } - const poll = 'media' in message && message.media && buildPollFromMedia(message.media); - if (poll) { - polls.push(poll); + if ('media' in message && message.media) { + const poll = buildPollFromMedia(message.media); + if (poll) { + polls.push(poll); + } + + const webPage = buildWebPageFromMedia(message.media); + if (webPage) { + webPages.push(webPage); + } } }); } @@ -69,6 +78,7 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons chats: chatById, threadInfos: threadInfos?.length ? threadInfos : undefined, polls: polls?.length ? polls : undefined, + webPages: webPages?.length ? webPages : undefined, }); } diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index 24130350a..03048b304 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -5,6 +5,7 @@ import type { GroupCallConnectionData } from '../../../lib/secret-sauce'; import type { ApiMessage, ApiPoll, ApiStory, ApiStorySkipped, ApiUpdateConnectionStateType, + ApiWebPage, } from '../../types'; import { DEBUG, GENERAL_TOPIC_ID } from '../../../config'; @@ -36,6 +37,8 @@ import { buildPoll, buildPollFromMedia, buildPollResults, + buildWebPage, + buildWebPageFromMedia, } from '../apiBuilders/messageContent'; import { buildApiMessage, @@ -124,6 +127,7 @@ export function updater(update: Update) { ) { let message: ApiMessage | undefined; let poll: ApiPoll | undefined; + let webPage: ApiWebPage | undefined; let shouldForceReply: boolean | undefined; if (update instanceof GramJs.UpdateShortChatMessage) { @@ -148,6 +152,7 @@ export function updater(update: Update) { if (mtpMessage instanceof GramJs.Message) { poll = mtpMessage.media && buildPollFromMedia(mtpMessage.media); + webPage = mtpMessage.media && buildWebPageFromMedia(mtpMessage.media); } shouldForceReply = 'replyMarkup' in update.message @@ -162,6 +167,7 @@ export function updater(update: Update) { chatId: message.chatId, message, poll, + webPage, isFromNew: true, }); } else { @@ -172,6 +178,7 @@ export function updater(update: Update) { message, shouldForceReply, poll, + webPage, isFromNew: true, }); } @@ -275,10 +282,17 @@ export function updater(update: Update) { const message = buildApiMessage(update.message); if (!message) return; + const poll = update.message instanceof GramJs.Message && update.message.media + ? buildPollFromMedia(update.message.media) : undefined; + const webPage = update.message instanceof GramJs.Message && update.message.media + ? buildWebPageFromMedia(update.message.media) : undefined; + sendApiUpdate({ '@type': 'updateQuickReplyMessage', id: message.id, message, + poll, + webPage, }); } else if (update instanceof GramJs.UpdateDeleteQuickReplyMessages) { sendApiUpdate({ @@ -326,12 +340,16 @@ export function updater(update: Update) { const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media ? buildPollFromMedia(mtpMessage.media) : undefined; + const webPage = mtpMessage instanceof GramJs.Message && mtpMessage.media + ? buildWebPageFromMedia(mtpMessage.media) : undefined; + sendApiUpdate({ '@type': 'updateMessage', id: message.id, chatId: message.chatId, message, poll, + webPage, }); } else if (update instanceof GramJs.UpdateMessageReactions) { sendApiUpdate({ @@ -934,6 +952,14 @@ export function updater(update: Update) { '@type': 'updateWebViewResultSent', queryId: queryId.toString(), }); + } else if (update instanceof GramJs.UpdateWebPage || update instanceof GramJs.UpdateChannelWebPage) { + const webPage = buildWebPage(update.webpage); + if (webPage) { + sendApiUpdate({ + '@type': 'updateWebPage', + webPage, + }); + } } else if (update instanceof GramJs.UpdateBotMenuButton) { const { botId, @@ -1075,6 +1101,8 @@ export function updater(update: Update) { }); } else if (update instanceof LocalUpdatePts || update instanceof LocalUpdateChannelPts) { // Do nothing, handled on the manager side + } else if (update instanceof GramJs.UpdateMessageID || update instanceof GramJs.UpdateShortSentMessage) { + // Do nothing, handled when sending the message } else if (DEBUG) { const params = typeof update === 'object' && 'className' in update ? update.className : update; log('UNEXPECTED UPDATE', params); diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index ddbbd0776..8e1ce1cfe 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -361,9 +361,26 @@ export type ApiNewMediaTodo = { todo: ApiTodoList; }; -export interface ApiWebPage { +export interface ApiWebPagePending { mediaType: 'webpage'; - id: number; + webpageType: 'pending'; + id: string; + url?: string; + isSafe?: true; +} + +export interface ApiWebPageEmpty { + mediaType: 'webpage'; + webpageType: 'empty'; + id: string; + url?: string; + isSafe?: true; +} + +export interface ApiWebPageFull { + mediaType: 'webpage'; + webpageType: 'full'; + id: string; url: string; displayUrl: string; type?: string; @@ -378,10 +395,20 @@ export interface ApiWebPage { story?: ApiWebPageStoryData; gift?: ApiStarGiftUnique; stickers?: ApiWebPageStickerData; - mediaSize?: WebPageMediaSize; hasLargeMedia?: boolean; } +export type ApiWebPage = ApiWebPagePending | ApiWebPageEmpty | ApiWebPageFull; + +/** + * Wrapper with message-specific fields + */ +export interface ApiMessageWebPage { + id: string; + isSafe?: true; + mediaSize?: WebPageMediaSize; +} + export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo; export interface ApiMessageReplyInfo { @@ -561,7 +588,7 @@ export type MediaContent = { pollId?: string; todo?: ApiMediaTodo; action?: ApiMessageAction; - webPage?: ApiWebPage; + webPage?: ApiMessageWebPage; audio?: ApiAudio; voice?: ApiVoice; invoice?: ApiMediaInvoice; @@ -580,8 +607,17 @@ export type MediaContainer = { export type StatefulMediaContent = { poll?: ApiPoll; story?: ApiStory; + webPage?: ApiWebPage; }; +export type SizeTarget = + 'micro' + | 'pictogram' + | 'inline' + | 'preview' + | 'full' + | 'download'; + export type BoughtPaidMedia = Pick; export interface ApiMessage { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 9f23cca97..e66302c0f 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -33,6 +33,7 @@ import type { ApiReactions, ApiStickerSet, ApiThreadInfo, + ApiWebPage, BoughtPaidMedia, } from './messages'; import type { @@ -215,6 +216,7 @@ export type ApiUpdateNewScheduledMessage = { message: ApiMessage; wasDrafted?: boolean; poll?: ApiPoll; + webPage?: ApiWebPage; }; export type ApiUpdateNewMessage = { @@ -225,6 +227,7 @@ export type ApiUpdateNewMessage = { shouldForceReply?: boolean; wasDrafted?: boolean; poll?: ApiPoll; + webPage?: ApiWebPage; }; export type ApiUpdateMessage = { @@ -233,6 +236,7 @@ export type ApiUpdateMessage = { id: number; message: Partial; poll?: ApiPoll; + webPage?: ApiWebPage; shouldForceReply?: boolean; isFromNew?: true; }; @@ -243,6 +247,7 @@ export type ApiUpdateScheduledMessage = { id: number; message: Partial; poll?: ApiPoll; + webPage?: ApiWebPage; isFromNew?: true; }; @@ -251,6 +256,7 @@ export type ApiUpdateQuickReplyMessage = { id: number; message: Partial; poll?: ApiPoll; + webPage?: ApiWebPage; }; export type ApiUpdateDeleteQuickReplyMessages = { @@ -287,6 +293,7 @@ export type ApiUpdateScheduledMessageSendSucceeded = { localId: number; message: ApiMessage; poll?: ApiPoll; + webPage?: ApiWebPage; }; export type ApiUpdateMessageSendSucceeded = { @@ -295,6 +302,7 @@ export type ApiUpdateMessageSendSucceeded = { localId: number; message: ApiMessage; poll?: ApiPoll; + webPage?: ApiWebPage; }; export type ApiUpdateVideoProcessingPending = { @@ -815,6 +823,7 @@ export type ApiUpdateEntities = { chats?: Record; threadInfos?: ApiThreadInfo[]; polls?: ApiPoll[]; + webPages?: ApiWebPage[]; }; export type ApiUpdatePaidReactionPrivacy = { @@ -840,6 +849,11 @@ export type ApiUpdateBotCommands = { commands?: ApiBotCommand[]; }; +export type ApiUpdateWebPage = { + '@type': 'updateWebPage'; + webPage: ApiWebPage; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -857,7 +871,7 @@ export type ApiUpdate = ( ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | - ApiUpdateFailedMessageTranslations | + ApiUpdateFailedMessageTranslations | ApiUpdateWebPage | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index cc722e643..ae658ca12 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1097,6 +1097,7 @@ "AttachSticker" = "Sticker"; "AttachMusic" = "Music"; "AttachContact" = "Contact"; +"AttachStory" = "Story"; "MessageLocation" = "Location"; "MessageLiveLocation" = "Live Location"; "ServiceNotifications" = "service notifications"; @@ -2140,7 +2141,6 @@ "ToDoListErrorChooseTasks" = "Please enter at least one task."; "GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**"; "PremiumPreviewTodo" = "Checklists"; -"MenuTon" = "My TON"; "DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram."; "ButtonTopUpViaFragment" = "Top-Up Via Fragment"; "TonModalHint" = "You can top-up your TON using Fragment."; @@ -2173,6 +2173,13 @@ "PriceChanged" = "Price Changed"; "PayNewPrice" = "Pay New Price"; "PriceChangedText" = "The price has already changed from **{originalAmount}** to **{newAmount}**. Do you want to pay the new price?"; +"LinkPreview" = "Link Preview"; +"ContextMoveTextUp" = "Move Caption Up"; +"ContextMoveTextDown" = "Move Caption Down"; +"ContextLinkLargerMedia" = "Larger Media"; +"ContextLinkSmallerMedia" = "Smaller Media"; +"ContextLinkRemovePreview" = "Remove Preview"; +"AccLinkRemovePreview" = "Remove Preview"; "GlobalSearch" = "Global Search"; "DescriptionPublicPostsSearch" = "Type a keyword to search for posts from public channels."; "ButtonSearchPublicPosts" = "Search {query}"; @@ -2189,5 +2196,3 @@ "PublicPostsSubscribeToPremium" = "Subscribe to Premium"; "NotificationPaidExtraSearch" = "{stars} spent on extra search."; "PostsSearchTransaction" = "Posts Search"; - - diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index f94670dd4..69114dbc5 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -1,11 +1,12 @@ -import type { ElementRef, FC } from '../../lib/teact/teact'; +import type { ElementRef } from '../../lib/teact/teact'; import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; -import { getActions } from '../../global'; +import { getActions, withGlobal } from '../../global'; import type { ApiAudio, ApiMessage, ApiVideo, ApiVoice, + ApiWebPage, } from '../../api/types'; import type { BufferedRange } from '../../hooks/useBuffering'; import type { OldLangFn } from '../../hooks/useOldLang'; @@ -14,15 +15,16 @@ import { ApiMediaFormat } from '../../api/types'; import { AudioOrigin } from '../../types'; import { - getMediaDuration, getMediaFormat, getMediaHash, getMediaTransferState, - getMessageWebPageAudio, + getWebPageAudio, hasMessageTtl, isMessageLocal, isOwnMessage, } from '../../global/helpers'; +import { selectWebPageFromMessage } from '../../global/selectors'; +import { selectMessageMediaDuration } from '../../global/selectors/media'; import { makeTrackId } from '../../util/audioPlayer'; import buildClassName from '../../util/buildClassName'; import { captureEvents } from '../../util/captureEvents'; @@ -77,13 +79,18 @@ type OwnProps = { onDateClick?: (arg: ApiMessage) => void; }; +type StateProps = { + mediaDuration?: number; + webPage?: ApiWebPage; +}; + export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)'); export const WITH_AVATAR_TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 410px)'); const AVG_VOICE_DURATION = 10; // This is needed for browsers requiring user interaction before playing. const PRELOAD = true; -const Audio: FC = ({ +const Audio = ({ theme, message, senderTitle, @@ -102,13 +109,15 @@ const Audio: FC = ({ canDownload, canTranscribe, autoPlay, + webPage, + mediaDuration, onHideTranscription, onPlay, onPause, onReadMedia, onCancelUpload, onDateClick, -}) => { +}: OwnProps & StateProps) => { const { cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal, } = getActions(); @@ -118,7 +127,7 @@ const Audio: FC = ({ audio: contentAudio, voice, video, }, isMediaUnread, } = message; - const audio = contentAudio || getMessageWebPageAudio(message); + const audio = contentAudio || getWebPageAudio(webPage); const media = (voice || video || audio)!; const mediaSource = (voice || video); const isVoice = Boolean(voice || video); @@ -166,7 +175,7 @@ const Audio: FC = ({ isPlaying, playProgress, playPause, setCurrentTime, duration, } = useAudioPlayer( makeTrackId(message), - getMediaDuration(message)!, + mediaDuration!, trackType, mediaData, bufferingHandlers, @@ -716,4 +725,16 @@ function renderSeekline( ); } -export default memo(Audio); +export default memo(withGlobal( + (global, { + message, + }): StateProps => { + const webPage = selectWebPageFromMessage(global, message); + const mediaDuration = selectMessageMediaDuration(global, message); + + return { + webPage, + mediaDuration, + }; + }, +)(Audio)); diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index c9e11a20f..fca274611 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -107,6 +107,7 @@ import { selectTopicFromMessage, selectUser, selectUserFullInfo, + selectWebPage, } from '../../global/selectors'; import { selectCurrentLimit } from '../../global/selectors/limits'; import { selectSharedSettings } from '../../global/selectors/sharedState'; @@ -158,6 +159,7 @@ import useDraft from '../middle/composer/hooks/useDraft'; import useEditing from '../middle/composer/hooks/useEditing'; import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip'; import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip'; +import useLoadLinkPreview from '../middle/composer/hooks/useLoadLinkPreview'; import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip'; import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation'; import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip'; @@ -826,6 +828,12 @@ const Composer: FC = ({ isDisabled: isInStoryViewer || Boolean(requestedDraft) || (!hasSuggestedPost && isMonoforum), }); + useLoadLinkPreview({ + chatId, + threadId, + getHtml, + }); + const resetComposer = useLastCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { setHtml(''); @@ -2028,8 +2036,7 @@ const Composer: FC = ({ @@ -2523,6 +2530,8 @@ export default memo(withGlobal( const isAppConfigLoaded = global.isAppConfigLoaded; const insertingPeerIdMention = tabState.insertingPeerIdMention; + const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined; + return { availableReactions: global.reactions.availableReactions, topReactions: type === 'story' ? global.reactions.topReactions : undefined, @@ -2594,7 +2603,7 @@ export default memo(withGlobal( quickReplies: global.quickReplies.byId, canSendQuickReplies, noWebPage, - webPagePreview: selectTabState(global).webPagePreview, + webPagePreview, isContactRequirePremium: userFullInfo?.isContactRequirePremium, effect, effectReactions, diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index 57c662b79..3e98e002b 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -7,14 +7,14 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import { getMessageHtmlId, getMessageIsSpoiler, - getMessageMediaHash, - getMessageMediaThumbDataUri, getMessageVideo, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration } from '../../util/dates/dateFormat'; import stopEvent from '../../util/stopEvent'; +import useMessageMediaHash from '../../hooks/media/useMessageMediaHash'; +import useThumbnail from '../../hooks/media/useThumbnail'; import useFlag from '../../hooks/useFlag'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import useLastCallback from '../../hooks/useLastCallback'; @@ -43,8 +43,9 @@ const Media: FC = ({ const ref = useRef(); const isIntersecting = useIsIntersecting(ref, observeIntersection); - const thumbDataUri = getMessageMediaThumbDataUri(message); - const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting); + const thumbDataUri = useThumbnail(message); + const mediaHash = useMessageMediaHash(message, 'pictogram'); + const mediaBlobUrl = useMedia(mediaHash, !isIntersecting); const transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl); const video = getMessageVideo(message); diff --git a/src/components/common/PreviewMedia.tsx b/src/components/common/PreviewMedia.tsx index ef3a334cf..3fdd2f9c2 100644 --- a/src/components/common/PreviewMedia.tsx +++ b/src/components/common/PreviewMedia.tsx @@ -4,13 +4,12 @@ import { memo, useRef } from '../../lib/teact/teact'; import type { ApiBotPreviewMedia } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; -import { - getMessageMediaHash, getMessageMediaThumbDataUri, -} from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration } from '../../util/dates/dateFormat'; import stopEvent from '../../util/stopEvent'; +import useMessageMediaHash from '../../hooks/media/useMessageMediaHash'; +import useThumbnail from '../../hooks/media/useThumbnail'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import useLastCallback from '../../hooks/useLastCallback'; import useMedia from '../../hooks/useMedia'; @@ -38,9 +37,10 @@ const PreviewMedia: FC = ({ const ref = useRef(); const isIntersecting = useIsIntersecting(ref, observeIntersection); - const thumbDataUri = getMessageMediaThumbDataUri(media); + const thumbDataUri = useThumbnail(media); - const mediaBlobUrl = useMedia(getMessageMediaHash(media, 'preview'), !isIntersecting); + const mediaHash = useMessageMediaHash(media, 'preview'); + const mediaBlobUrl = useMedia(mediaHash, !isIntersecting); const transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl); const video = media.content.video; diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index b6c9b1fa0..8d6c77b8d 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -11,6 +11,7 @@ import { IS_ANDROID, IS_IOS, IS_WEBM_SUPPORTED } from '../../util/browser/window import buildClassName from '../../util/buildClassName'; import * as mediaLoader from '../../util/mediaLoader'; +import useThumbnail from '../../hooks/media/useThumbnail'; import useColorFilter from '../../hooks/stickers/useColorFilter'; import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas'; import useFlag from '../../hooks/useFlag'; @@ -18,7 +19,6 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import useMedia from '../../hooks/useMedia'; import useMediaTransition from '../../hooks/useMediaTransition'; import useMountAfterHeavyAnimation from '../../hooks/useMountAfterHeavyAnimation'; -import useThumbnail from '../../hooks/useThumbnail'; import useUniqueId from '../../hooks/useUniqueId'; import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; @@ -120,7 +120,7 @@ const StickerView: FC = ({ fullMediaHash === previewMediaHash && (cachedPreview || previewMediaData) )); const fullMediaData = useMedia(fullMediaHash || `sticker${id}`, !shouldLoad || shouldSkipLoadingFullMedia); - const shouldRenderFullMedia = isReadyToMountFullMedia && fullMediaData && !isVideoBroken; + const shouldRenderFullMedia = isReadyToMountFullMedia && Boolean(fullMediaData) && !isVideoBroken; const [isPlayerReady, markPlayerReady] = useFlag(); const isFullMediaReady = shouldRenderFullMedia && (isStatic || isPlayerReady); @@ -129,10 +129,12 @@ const StickerView: FC = ({ const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb; const noCrossTransition = Boolean(isLottie && withPreview); - const thumbRef = useMediaTransition(thumbData && !isFullMediaReady, { + const { ref: thumbRef } = useMediaTransition({ + hasMediaData: Boolean(thumbData && !isFullMediaReady), noCloseTransition: noCrossTransition, }); - const fullMediaRef = useMediaTransition(isFullMediaReady, { + const { ref: fullMediaRef } = useMediaTransition({ + hasMediaData: isFullMediaReady, noOpenTransition: noCrossTransition, }); diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx index 69772bd25..ba9b47c50 100644 --- a/src/components/common/WebLink.tsx +++ b/src/components/common/WebLink.tsx @@ -1,5 +1,5 @@ -import type { FC } from '../../lib/teact/teact'; import { memo } from '../../lib/teact/teact'; +import { withGlobal } from '../../global'; import type { ApiMessage, ApiWebPage } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; @@ -7,8 +7,8 @@ import type { TextPart } from '../../types'; import { getFirstLinkInMessage, getMessageText, - getMessageWebPage, } from '../../global/helpers'; +import { selectWebPageFromMessage } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatPastTimeShort } from '../../util/dates/dateFormat'; import trimText from '../../util/trimText'; @@ -26,6 +26,10 @@ import './WebLink.scss'; const MAX_TEXT_LENGTH = 170; // symbols +type ApiWebPageWithFormatted = + ApiWebPage + & { formattedDescription?: TextPart[] }; + type OwnProps = { message: ApiMessage; senderTitle?: string; @@ -34,16 +38,16 @@ type OwnProps = { onMessageClick: (message: ApiMessage) => void; }; -type ApiWebPageWithFormatted = - ApiWebPage - & { formattedDescription?: TextPart[] }; +type StateProps = { + webPage?: ApiWebPage; +}; -const WebLink: FC = ({ - message, senderTitle, isProtected, observeIntersection, onMessageClick, -}) => { +const WebLink = ({ + message, webPage, senderTitle, isProtected, observeIntersection, onMessageClick, +}: OwnProps & StateProps) => { const lang = useOldLang(); - let linkData: ApiWebPageWithFormatted | undefined = getMessageWebPage(message); + let linkData: ApiWebPageWithFormatted | undefined = webPage; if (!linkData) { const link = getFirstLinkInMessage(message); @@ -64,7 +68,7 @@ const WebLink: FC = ({ onMessageClick(message); }); - if (!linkData) { + if (linkData?.webpageType !== 'full') { return undefined; } @@ -129,4 +133,14 @@ const WebLink: FC = ({ ); }; -export default memo(WebLink); +export default memo(withGlobal( + (global, { + message, + }): StateProps => { + const webPage = selectWebPageFromMessage(global, message); + + return { + webPage, + }; + }, +)(WebLink)); diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 4c20ad22e..530901893 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -14,7 +14,6 @@ import type { IconName } from '../../../types/icons'; import { CONTENT_NOT_SUPPORTED, TON_CURRENCY_CODE } from '../../../config'; import { getMessageIsSpoiler, - getMessageMediaHash, getMessageRoundVideo, isChatChannel, isChatGroup, @@ -31,12 +30,13 @@ import { getPictogramDimensions } from '../helpers/mediaDimensions'; import renderText from '../helpers/renderText'; import { renderTextWithEntities } from '../helpers/renderTextWithEntities'; +import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash'; +import useThumbnail from '../../../hooks/media/useThumbnail'; import { useFastClick } from '../../../hooks/useFastClick'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import useLang from '../../../hooks/useLang'; import useMedia from '../../../hooks/useMedia'; import useOldLang from '../../../hooks/useOldLang'; -import useThumbnail from '../../../hooks/useThumbnail'; import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation'; import RippleEffect from '../../ui/RippleEffect'; @@ -111,7 +111,7 @@ const EmbeddedMessage: FC = ({ const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined; const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length); - const mediaHash = containedMedia && getMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram'); + const mediaHash = useMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram'); const mediaBlobUrl = useMedia(mediaHash, !isIntersecting); const mediaThumbnail = useThumbnail(containedMedia); diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index a0b4f6358..dd99c464b 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -12,8 +12,6 @@ import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config'; import { requestMutation } from '../../../../lib/fasterdom/fasterdom'; import { getMessageIsSpoiler, - getMessageMediaHash, - getMessageMediaThumbDataUri, getMessageRoundVideo, getMessageSticker, getMessageVideo, @@ -24,6 +22,8 @@ import renderText from '../../../common/helpers/renderText'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { ChatAnimationTypes } from './useChatAnimationType'; +import useMessageMediaHash from '../../../../hooks/media/useMessageMediaHash'; +import useThumbnail from '../../../../hooks/media/useThumbnail'; import useEnsureStory from '../../../../hooks/useEnsureStory'; import useMedia from '../../../../hooks/useMedia'; import useOldLang from '../../../../hooks/useOldLang'; @@ -82,8 +82,11 @@ export default function useChatListEntry({ const mediaContent = statefulMediaContent?.story || lastMessage; const mediaHasPreview = mediaContent && !getMessageSticker(mediaContent); - const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(mediaContent) : undefined; - const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(mediaContent, 'micro') : undefined); + const thumbDataUri = useThumbnail(mediaContent); + + const mediaThumbnail = mediaHasPreview ? thumbDataUri : undefined; + const mediaHash = useMessageMediaHash(mediaContent, 'micro'); + const mediaBlobUrl = useMedia(mediaHasPreview ? mediaHash : undefined); const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage)); const renderLastMessageOrTyping = useCallback(() => { diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index 9b630ca3b..abd3bde83 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -1,13 +1,14 @@ import type { FC } from '../../../lib/teact/teact'; import { memo, useCallback, useMemo } from '../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../global'; +import { getActions, getGlobal, 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, getMessageDownloadableMedia } from '../../../global/helpers'; +import { getIsDownloading } from '../../../global/helpers'; +import { selectMessageDownloadableMedia } from '../../../global/selectors/media'; import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat'; import { parseSearchResultKey } from '../../../util/keys/searchResultKey'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; @@ -82,11 +83,12 @@ const AudioResults: FC = ({ function renderList() { return foundMessages.map((message, index) => { + const global = getGlobal(); const isFirst = index === 0; const shouldDrawDateDivider = isFirst || toYearMonth(message.date) !== toYearMonth(foundMessages[index - 1].date); - const media = getMessageDownloadableMedia(message)!; + const media = selectMessageDownloadableMedia(global, message)!; return ( <> {shouldDrawDateDivider && ( diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index 0be2fd9df..6409526a8 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -10,8 +10,6 @@ import type { OldLangFn } from '../../../hooks/useOldLang'; import { getMessageIsSpoiler, - getMessageMediaHash, - getMessageMediaThumbDataUri, getMessageRoundVideo, getMessageSticker, getMessageVideo, @@ -22,6 +20,8 @@ import buildClassName from '../../../util/buildClassName'; import { formatPastTimeShort } from '../../../util/dates/dateFormat'; import { renderMessageSummary } from '../../common/helpers/renderMessageText'; +import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash'; +import useThumbnail from '../../../hooks/media/useThumbnail'; import useAppLayout from '../../../hooks/useAppLayout'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; @@ -58,8 +58,10 @@ const ChatMessage: FC = ({ const { focusMessage } = getActions(); const { isMobile } = useAppLayout(); - const mediaThumbnail = !getMessageSticker(message) ? getMessageMediaThumbDataUri(message) : undefined; - const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro')); + const thumbDataUri = useThumbnail(message); + const mediaThumbnail = !getMessageSticker(message) ? thumbDataUri : undefined; + const mediaHash = useMessageMediaHash(message, 'micro'); + const mediaBlobUrl = useMedia(mediaHash); const isRoundVideo = Boolean(getMessageRoundVideo(message)); const handleClick = useLastCallback(() => { diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 96736b5aa..54a4872e1 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -41,7 +41,8 @@ import { disableDirectTextInput, enableDirectTextInput } from '../../util/direct import { isUserId } from '../../util/entities/ids'; import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions'; import { renderMessageText } from '../common/helpers/renderMessageText'; -import getViewableMedia, { getMediaViewerItem, type MediaViewerItem } from './helpers/getViewableMedia'; +import { getMediaViewerItem, type MediaViewerItem, type ViewableMedia } from './helpers/getViewableMedia'; +import selectViewableMedia from './helpers/getViewableMedia'; import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import useAppLayout from '../../hooks/useAppLayout'; @@ -88,6 +89,8 @@ type StateProps = { withDynamicLoading?: boolean; isLoadingMoreMedia?: boolean; isSynced?: boolean; + currentItem?: MediaViewerItem; + viewableMedia?: ViewableMedia; }; const ANIMATION_DURATION = 250; @@ -115,6 +118,8 @@ const MediaViewer = ({ withDynamicLoading, isLoadingMoreMedia, isSynced, + currentItem, + viewableMedia, }: StateProps) => { const { openMediaViewer, @@ -131,6 +136,8 @@ const MediaViewer = ({ const isOpen = Boolean(avatarOwner || message || standaloneMedia || sponsoredMessage); const { isMobile } = useAppLayout(); + const { media, isSingle } = viewableMedia || {}; + /* Animation */ const animationKey = useRef(); const senderId = message?.senderId || avatarOwner?.id || message?.chatId; @@ -141,11 +148,6 @@ const MediaViewer = ({ /* Controls */ const [isReportAvatarModalOpen, openReportAvatarModal, closeReportAvatarModal] = useFlag(); - const currentItem = getMediaViewerItem({ - message, avatarOwner, standaloneMedia, profilePhotos, mediaIndex, sponsoredMessage, - }); - const { media, isSingle } = getViewableMedia(currentItem) || {}; - const { isVideo, isPhoto, @@ -504,18 +506,24 @@ export default memo(withGlobal( const isChatWithSelf = Boolean(chatId) && selectIsChatWithSelf(global, chatId); if (isAvatarView) { - const peer = selectPeer(global, chatId!); + const avatarOwner = selectPeer(global, chatId!); let canUpdateMedia = false; - if (peer) { - canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat); + if (avatarOwner) { + canUpdateMedia = isUserId(avatarOwner.id) + ? avatarOwner.id === currentUserId : isChatAdmin(avatarOwner as ApiChat); } const profilePhotos = selectPeerPhotos(global, chatId!); + const currentItem = getMediaViewerItem({ + avatarOwner, standaloneMedia, profilePhotos, mediaIndex, + }); + const viewableMedia = selectViewableMedia(global, currentItem); + return { profilePhotos, avatar: profilePhotos?.photos[mediaIndex!], - avatarOwner: peer, + avatarOwner, isLoadingMoreMedia: profilePhotos?.isLoading, isChatWithSelf, canUpdateMedia, @@ -526,6 +534,8 @@ export default memo(withGlobal( standaloneMedia, mediaIndex, isSynced, + currentItem, + viewableMedia, }; } @@ -576,6 +586,11 @@ export default memo(withGlobal( } } + const currentItem = getMediaViewerItem({ + message, standaloneMedia, mediaIndex, sponsoredMessage, + }); + const viewableMedia = selectViewableMedia(global, currentItem); + return { chatId, threadId, @@ -594,6 +609,8 @@ export default memo(withGlobal( mediaIndex, isLoadingMoreMedia, isSynced, + currentItem, + viewableMedia, }; }, )(MediaViewer)); diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index ff1f0fa1e..57f4fa75e 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -6,7 +6,7 @@ import type { ApiChat } from '../../api/types'; import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types'; import type { IconName } from '../../types/icons'; import type { MenuItemProps } from '../ui/MenuItem'; -import type { MediaViewerItem } from './helpers/getViewableMedia'; +import type { MediaViewerItem, ViewableMedia } from './helpers/getViewableMedia'; import { getIsDownloading, @@ -23,7 +23,7 @@ import { selectTabState, } from '../../global/selectors'; import { isUserId } from '../../util/entities/ids'; -import getViewableMedia from './helpers/getViewableMedia'; +import selectViewableMedia from './helpers/getViewableMedia'; import useAppLayout from '../../hooks/useAppLayout'; import useFlag from '../../hooks/useFlag'; @@ -41,17 +41,6 @@ import ProgressSpinner from '../ui/ProgressSpinner'; import './MediaViewerActions.scss'; -type StateProps = { - activeDownloads: ActiveDownloads; - isProtected?: boolean; - isChatProtected?: boolean; - canDelete?: boolean; - chat?: ApiChat; - canUpdate?: boolean; - messageListType?: MessageListType; - origin?: MediaViewerOrigin; -}; - type OwnProps = { item?: MediaViewerItem; mediaData?: string; @@ -65,6 +54,18 @@ type OwnProps = { onForward: NoneToVoidFunction; }; +type StateProps = { + activeDownloads: ActiveDownloads; + isProtected?: boolean; + isChatProtected?: boolean; + canDelete?: boolean; + chat?: ApiChat; + canUpdate?: boolean; + messageListType?: MessageListType; + origin?: MediaViewerOrigin; + viewableMedia?: ViewableMedia; +}; + const MediaViewerActions: FC = ({ item, mediaData, @@ -78,6 +79,7 @@ const MediaViewerActions: FC = ({ messageListType, activeDownloads, origin, + viewableMedia, onReportAvatar: onReport, onCloseMediaViewer, onBeforeDelete, @@ -98,7 +100,7 @@ const MediaViewerActions: FC = ({ const isMessage = item?.type === 'message'; - const { media } = getViewableMedia(item) || {}; + const { media } = viewableMedia || {}; const fileName = media && getMediaFilename(media); const isDownloading = media && getIsDownloading(activeDownloads, media); @@ -411,6 +413,7 @@ export default memo(withGlobal( const canDelete = canDeleteMessage || canDeleteAvatar; const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar; const messageListType = currentMessageList?.type; + const viewableMedia = selectViewableMedia(global, item); return { activeDownloads, @@ -421,6 +424,7 @@ export default memo(withGlobal( canUpdate, messageListType, origin, + viewableMedia, }; }, )(MediaViewerActions)); diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index a962d70e0..e1f29d81c 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -6,19 +6,20 @@ import type { ApiDimensions, ApiMessage, ApiSponsoredMessage, } from '../../api/types'; import type { MediaViewerOrigin, ThreadId } from '../../types'; -import type { MediaViewerItem } from './helpers/getViewableMedia'; +import type { MediaViewerItem, ViewableMedia } from './helpers/getViewableMedia'; import { MEDIA_TIMESTAMP_SAVE_MINIMUM_DURATION } from '../../config'; import { - selectIsMessageProtected, selectMessageTimestampableDuration, selectTabState, + selectIsMessageProtected, selectTabState, } from '../../global/selectors'; +import { selectMessageTimestampableDuration } from '../../global/selectors/media'; import { ARE_WEBCODECS_SUPPORTED } from '../../util/browser/globalEnvironment'; import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import buildClassName from '../../util/buildClassName'; import stopEvent from '../../util/stopEvent'; import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions'; import { renderMessageText } from '../common/helpers/renderMessageText'; -import getViewableMedia from './helpers/getViewableMedia'; +import selectViewableMedia from './helpers/getViewableMedia'; import useAppLayout from '../../hooks/useAppLayout'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; @@ -46,6 +47,7 @@ type OwnProps = { }; type StateProps = { + viewableMedia?: ViewableMedia; textMessage?: ApiMessage | ApiSponsoredMessage; origin?: MediaViewerOrigin; isProtected?: boolean; @@ -64,6 +66,7 @@ const PLAYBACK_SAVE_INTERVAL = 1000; const MediaViewerContent = ({ item, + viewableMedia, isActive, textMessage, origin, @@ -87,7 +90,7 @@ const MediaViewerContent = ({ const isAvatar = item.type === 'avatar'; const isSponsoredMessage = item.type === 'sponsoredMessage'; - const { media } = getViewableMedia(item) || {}; + const { media } = viewableMedia || {}; const { isVideo, @@ -254,6 +257,7 @@ export default memo(withGlobal( const message = item.type === 'message' ? item.message : undefined; const sponsoredMessage = item.type === 'sponsoredMessage' ? item.message : undefined; const textMessage = message || sponsoredMessage; + const viewableMedia = selectViewableMedia(global, item); const maxTimestamp = message && selectMessageTimestampableDuration(global, message, true); @@ -268,6 +272,7 @@ export default memo(withGlobal( threadId, timestamp, maxTimestamp, + viewableMedia, }; }, )(MediaViewerContent)); diff --git a/src/components/mediaViewer/helpers/getViewableMedia.ts b/src/components/mediaViewer/helpers/getViewableMedia.ts index 447806a49..79765ab51 100644 --- a/src/components/mediaViewer/helpers/getViewableMedia.ts +++ b/src/components/mediaViewer/helpers/getViewableMedia.ts @@ -1,9 +1,11 @@ import type { ApiMessage, ApiPeer, ApiPeerPhotos, ApiSponsoredMessage, } from '../../../api/types'; +import type { GlobalState } from '../../../global/types'; import type { MediaViewerMedia } from '../../../types'; import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers'; +import { selectWebPageFromMessage } from '../../../global/selectors'; export type MediaViewerItem = { type: 'message'; @@ -24,7 +26,7 @@ export type MediaViewerItem = { mediaIndex?: number; }; -type ViewableMedia = { +export type ViewableMedia = { media: MediaViewerMedia; isSingle?: boolean; }; @@ -75,7 +77,7 @@ export function getMediaViewerItem({ return undefined; } -export default function getViewableMedia(params?: MediaViewerItem): ViewableMedia | undefined { +export default function selectViewableMedia(global: GlobalState, params?: MediaViewerItem): ViewableMedia | undefined { if (!params) return undefined; if (params.type === 'standalone') { @@ -96,7 +98,7 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi } const { - action, document, photo, video, webPage, paidMedia, + action, document, photo, video, paidMedia, } = getMessageContent(params.message); if (action?.type === 'chatEditPhoto' || action?.type === 'suggestProfilePhoto') { @@ -112,7 +114,8 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi }; } - if (webPage) { + const webPage = selectWebPageFromMessage(global, params.message); + if (webPage?.webpageType === 'full') { const { photo: webPagePhoto, video: webPageVideo, document: webPageDocument } = webPage; const isDocumentMedia = webPageDocument && (isDocumentPhoto(webPageDocument) || isDocumentVideo(webPageDocument)); const mediaDocument = isDocumentMedia ? webPageDocument : undefined; diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index 5e7ad7620..ef95473de 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../../lib/teact/teact'; import { memo, useEffect, useMemo, @@ -18,8 +17,6 @@ import { getMessageAudio, getMessageDocument, getMessagePhoto, getMessageVideo, getMessageVoice, - getMessageWebPagePhoto, - getMessageWebPageVideo, } from '../../../global/helpers'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; @@ -57,18 +54,18 @@ export type OwnProps = { peerType?: ApiAttachMenuPeerType; shouldCollectDebugLogs?: boolean; theme: ThemeKey; + canEditMedia?: boolean; + editingMessage?: ApiMessage; + messageListType?: MessageListType; + paidMessagesStars?: number; onFileSelect: (files: File[]) => void; onPollCreate: NoneToVoidFunction; onTodoListCreate: NoneToVoidFunction; onMenuOpen: NoneToVoidFunction; onMenuClose: NoneToVoidFunction; - canEditMedia?: boolean; - editingMessage?: ApiMessage; - messageListType?: MessageListType; - paidMessagesStars?: number; }; -const AttachMenu: FC = ({ +const AttachMenu = ({ chatId, threadId, isButtonVisible, @@ -84,16 +81,16 @@ const AttachMenu: FC = ({ isScheduled, theme, shouldCollectDebugLogs, + canEditMedia, + editingMessage, + messageListType, + paidMessagesStars, onFileSelect, onMenuOpen, onMenuClose, onPollCreate, onTodoListCreate, - canEditMedia, - editingMessage, - messageListType, - paidMessagesStars, -}) => { +}: OwnProps) => { const { updateAttachmentSettings, } = getActions(); @@ -107,8 +104,8 @@ const AttachMenu: FC = ({ const isMenuOpen = isAttachMenuOpen || isAttachmentBotMenuOpen; const isPhotoOrVideo = editingMessage && editingMessage?.groupedId - && Boolean(getMessagePhoto(editingMessage) || getMessageWebPagePhoto(editingMessage) - || Boolean(getMessageVideo(editingMessage) || getMessageWebPageVideo(editingMessage))); + && Boolean(getMessagePhoto(editingMessage) + || Boolean(getMessageVideo(editingMessage))); const isFile = editingMessage && editingMessage?.groupedId && Boolean(getMessageAudio(editingMessage) || getMessageVoice(editingMessage) || getMessageDocument(editingMessage)); diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index fafec7638..32c031b70 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -554,12 +554,12 @@ const AttachmentModal: FC = ({ canInvertMedia && (!isInvertedMedia ? ( setIsInvertedMedia(true)}> - {oldLang('PreviewSender.MoveTextUp')} + {lang('ContextMoveTextUp')} ) : ( setIsInvertedMedia(undefined)}> - {oldLang(('PreviewSender.MoveTextDown'))} + {lang('ContextMoveTextDown')} )) } diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index 8c30d6292..867bfae26 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -22,10 +22,10 @@ import { selectForwardedSender, selectIsChatWithSelf, selectIsCurrentUserPremium, - selectIsMediaNsfw, selectSender, selectTabState, } from '../../../global/selectors'; +import { selectIsMediaNsfw } from '../../../global/selectors/media'; import buildClassName from '../../../util/buildClassName'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import { unique } from '../../../util/iteratees'; diff --git a/src/components/middle/composer/DropArea.tsx b/src/components/middle/composer/DropArea.tsx index 96c0de8e4..a9f84da26 100644 --- a/src/components/middle/composer/DropArea.tsx +++ b/src/components/middle/composer/DropArea.tsx @@ -1,5 +1,3 @@ -import type { FC } from '../../../lib/teact/teact'; -import type React from '../../../lib/teact/teact'; import { memo, useEffect, useRef } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; @@ -24,9 +22,9 @@ import './DropArea.scss'; export type OwnProps = { isOpen: boolean; withQuick?: boolean; + editingMessage?: ApiMessage | undefined; onHide: NoneToVoidFunction; onFileSelect: (files: File[]) => void; - editingMessage?: ApiMessage | undefined; }; export enum DropAreaState { @@ -37,9 +35,9 @@ export enum DropAreaState { const DROP_LEAVE_TIMEOUT_MS = 150; -const DropArea: FC = ({ - isOpen, withQuick, onHide, onFileSelect, editingMessage, -}) => { +const DropArea = ({ + isOpen, withQuick, editingMessage, onHide, onFileSelect, +}: OwnProps) => { const lang = useLang(); const { showNotification, updateAttachmentSettings } = getActions(); const hideTimeoutRef = useRef(); diff --git a/src/components/middle/composer/WebPagePreview.module.scss b/src/components/middle/composer/WebPagePreview.module.scss new file mode 100644 index 000000000..5f7c86843 --- /dev/null +++ b/src/components/middle/composer/WebPagePreview.module.scss @@ -0,0 +1,141 @@ +.root { + --accent-color: var(--color-primary); + + position: relative; + height: 0; + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ + transition: height 150ms ease-out, opacity 150ms ease-out; + + &:global(.open) { + height: 3.125rem; + } + + :global { + body.no-page-transitions & { + transition: opacity 150ms ease-out; + } + + .select-mode-active + .middle-column-footer & { + display: none; + } + + .ComposerEmbeddedMessage + & { + body.no-message-composer-animations & { + transition: opacity 150ms ease-out; + } + } + } +} + +.inner { + display: flex; + align-items: center; + padding-top: 0.5rem; +} + +.contextMenu { + position: absolute; + + :global(.bubble) { + width: auto; + } +} + +.clear { + flex-shrink: 0; + align-self: center; + + width: auto !important; + height: 1.5rem; + margin: 0.5625rem 1rem 0.5625rem 0.75rem; + padding: 0; + + color: var(--accent-color); + + background: none !important; + + @media (max-width: 600px) { + margin: 0.5625rem 0.75rem 0.5625rem 0.5rem; + } +} + +.left-icon { + display: grid; + flex-shrink: 0; + place-content: center; + + height: 2.625rem; + padding: 0.5625rem 0.75rem 0.5625rem 1rem; + + font-size: 1.5rem; + color: var(--accent-color); + + background: none !important; + + @media (max-width: 600px) { + width: 2.875rem; + } +} + +.preview { + overflow: hidden; + display: flex; + flex-grow: 1; + + max-width: calc(100% - 3.375rem); + padding-block: 0.1875rem; + padding-inline: 0.5rem 0.375rem; +} + +.previewText { + display: flex; + flex-direction: column; + + min-width: 0; + + font-size: calc(var(--message-text-size, 1rem) - 0.0625rem); + line-height: 1.125rem; +} + +.previewImageContainer { + position: relative; + + overflow: hidden; + flex-shrink: 0; + + width: 2rem; + height: 2rem; + margin-block: 0.125rem; + margin-inline: 0 0.375rem; + border-radius: 0.25rem; +} + +.previewImage { + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + object-fit: cover; +} + +.siteName, +.siteDescription { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.siteDescription { + color: var(--color-text); +} + +.interactive { + cursor: var(--custom-cursor, pointer); + + &:active { + background-color: var(--background-active-color); + } +} diff --git a/src/components/middle/composer/WebPagePreview.scss b/src/components/middle/composer/WebPagePreview.scss deleted file mode 100644 index ac1784c3d..000000000 --- a/src/components/middle/composer/WebPagePreview.scss +++ /dev/null @@ -1,105 +0,0 @@ -.WebPagePreview { - --accent-color: var(--color-primary); - - position: relative; - height: 3.125rem; - /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ - transition: height 150ms ease-out, opacity 150ms ease-out; - - body.no-page-transitions & { - transition: opacity 150ms ease-out; - } - - .select-mode-active + .middle-column-footer & { - display: none; - } - - &:not(.open) { - height: 0 !important; - } - - // TODO Remove duplication with `.ComposerEmbeddedMessage` - &_inner { - display: flex; - align-items: center; - padding-top: 0.5rem; - } - - .ComposerEmbeddedMessage + & { - body.no-message-composer-animations & { - transition: opacity 150ms ease-out; - } - } - - .web-page-preview-context-menu { - position: absolute; - - .bubble { - width: auto; - } - } - - & &-left-icon { - display: grid; - flex-shrink: 0; - place-content: center; - - height: 2.625rem; - padding: 0.5625rem 0.75rem 0.5625rem 1rem; - - font-size: 1.5rem; - color: var(--accent-color); - - background: none !important; - - @media (max-width: 600px) { - width: 2.875rem; - } - } - - & &-clear { - flex-shrink: 0; - align-self: center; - - width: auto; - height: 1.5rem; - margin: 0.5625rem 1rem 0.5625rem 0.75rem; - padding: 0; - - color: var(--accent-color); - - background: none !important; - - @media (max-width: 600px) { - margin: 0.5625rem 0.75rem 0.5625rem 0.5rem; - } - } - - .WebPage { - overflow: hidden; - flex-grow: 1; - max-width: calc(100% - 3.375rem); - - &.with-video .media-inner { - display: none; - } - - .site-title, - .site-name, - .site-description { - overflow: hidden; - flex: 1; - - max-width: 100%; - max-height: 1rem; - - text-overflow: ellipsis; - white-space: nowrap; - } - - .site-title { - margin-top: 0.125rem; - margin-bottom: 0.1875rem; - } - } -} diff --git a/src/components/middle/composer/WebPagePreview.tsx b/src/components/middle/composer/WebPagePreview.tsx index 9c819c8f8..8ee4e9853 100644 --- a/src/components/middle/composer/WebPagePreview.tsx +++ b/src/components/middle/composer/WebPagePreview.tsx @@ -1,122 +1,104 @@ -import type { FC } from '../../../lib/teact/teact'; -import type React from '../../../lib/teact/teact'; import { memo, useEffect, useRef } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { - ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage, + ApiWebPage, + ApiWebPageFull, + ApiWebPagePending, } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; -import type { ThemeKey, ThreadId, WebPageMediaSize } from '../../../types'; -import type { Signal } from '../../../util/signals'; -import { ApiMessageEntityTypes } from '../../../api/types'; +import type { ThreadId, WebPageMediaSize } from '../../../types'; -import { RE_LINK_TEMPLATE } from '../../../config'; -import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors'; +import { + getMediaHash, + getWebPageAudio, + getWebPageDocument, + getWebPagePhoto, + getWebPageVideo, +} from '../../../global/helpers'; +import { selectNoWebPage, selectTabState, selectWebPage } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText'; -import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers'; +import useThumbnail from '../../../hooks/media/useThumbnail'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; -import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; -import useDerivedSignal from '../../../hooks/useDerivedSignal'; import useDerivedState from '../../../hooks/useDerivedState'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import useOldLang from '../../../hooks/useOldLang'; -import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; -import useSyncEffect from '../../../hooks/useSyncEffect'; +import useMedia from '../../../hooks/useMedia'; +import useShowTransition from '../../../hooks/useShowTransition'; import Icon from '../../common/icons/Icon'; +import PeerColorWrapper from '../../common/PeerColorWrapper'; import Button from '../../ui/Button'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; -import WebPage from '../message/WebPage'; -import './WebPagePreview.scss'; +import styles from './WebPagePreview.module.scss'; type OwnProps = { chatId: string; threadId: ThreadId; - getHtml: Signal; isEditing: boolean; isDisabled?: boolean; }; type StateProps = { - webPagePreview?: ApiWebPage; + webPagePreview?: ApiWebPageFull | ApiWebPagePending; noWebPage?: boolean; - theme: ThemeKey; attachmentSettings: GlobalState['attachmentSettings']; }; -const DEBOUNCE_MS = 300; -const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); - -const WebPagePreview: FC = ({ +const WebPagePreview = ({ chatId, threadId, - getHtml, isDisabled, webPagePreview, noWebPage, - theme, attachmentSettings, isEditing, -}) => { +}: OwnProps & StateProps) => { const { - loadWebPagePreview, - clearWebPagePreview, toggleMessageWebPage, updateAttachmentSettings, } = getActions(); - const lang = useOldLang(); - - const formattedTextWithLinkRef = useRef(); + const lang = useLang(); const ref = useRef(); const isInvertedMedia = attachmentSettings.isInvertedMedia; const isSmallerMedia = attachmentSettings.webPageMediaSize === 'small'; - const detectLinkDebounced = useDebouncedResolver(() => { - const formattedText = parseHtmlAsFormattedText(getHtml()); - const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => ( - entity.type === ApiMessageEntityTypes.TextUrl - )); - - formattedTextWithLinkRef.current = formattedText; - - return linkEntity?.url || formattedText.text.match(RE_LINK)?.[0]; - }, [getHtml], DEBOUNCE_MS, true); - - const getLink = useDerivedSignal(detectLinkDebounced, [detectLinkDebounced, getHtml], true); - - useEffect(() => { - const link = getLink(); - const formattedText = formattedTextWithLinkRef.current; - - if (link) { - loadWebPagePreview({ text: formattedText! }); - } else { - clearWebPagePreview(); - toggleMessageWebPage({ chatId, threadId }); - } - }, [getLink, chatId, threadId]); - - useSyncEffect(() => { - clearWebPagePreview(); - toggleMessageWebPage({ chatId, threadId }); - }, [chatId, clearWebPagePreview, threadId, toggleMessageWebPage]); - const isShown = useDerivedState(() => { - return Boolean(webPagePreview && getHtml() && !noWebPage && !isDisabled); - }, [isDisabled, getHtml, noWebPage, webPagePreview]); - const { shouldRender, transitionClassNames } = useShowTransitionDeprecated(isShown); + return Boolean(webPagePreview && !noWebPage && !isDisabled); + }, [isDisabled, noWebPage, webPagePreview]); + const { shouldRender } = useShowTransition({ isOpen: isShown, ref, withShouldRender: true }); - const hasMediaSizeOptions = webPagePreview?.hasLargeMedia; + const hasMediaSizeOptions = webPagePreview?.webpageType === 'full' && webPagePreview.hasLargeMedia; - const renderingWebPage = useCurrentOrPrev(webPagePreview, true); + const prevWebPageRef = useRef(webPagePreview); + + if (webPagePreview && webPagePreview !== prevWebPageRef.current) { + prevWebPageRef.current = webPagePreview; + } + + const renderingWebPage = webPagePreview || prevWebPageRef.current; + + const isFullWebPage = renderingWebPage?.webpageType === 'full'; + + const thumbnailUrl = useThumbnail(isFullWebPage ? { content: renderingWebPage } : undefined); + const previewMedia = getWebPagePhoto(renderingWebPage) || getWebPageVideo(renderingWebPage) + || getWebPageAudio(renderingWebPage) || getWebPageDocument(renderingWebPage); + const previewMediaHash = previewMedia && getMediaHash(previewMedia, 'pictogram'); + const previewMediaUrl = useMedia(previewMediaHash); + + const { shouldRender: shouldRenderPreviewMedia, ref: previewMediaRef } = useShowTransition({ + isOpen: Boolean(previewMediaUrl), + withShouldRender: true, + noCloseTransition: true, + }); + + const hasPreviewMedia = Boolean(previewMediaUrl || shouldRenderPreviewMedia); const handleClearWebpagePreview = useLastCallback(() => { toggleMessageWebPage({ chatId, threadId, noWebPage: true }); @@ -124,13 +106,13 @@ const WebPagePreview: FC = ({ const { isContextMenuOpen, contextMenuAnchor, handleContextMenu, - handleContextMenuClose, handleContextMenuHide, + handleContextMenuClose, handleContextMenuHide, handleBeforeContextMenu, } = useContextMenuHandlers(ref, isEditing, true); const getTriggerElement = useLastCallback(() => ref.current); const getRootElement = useLastCallback(() => ref.current!); const getMenuElement = useLastCallback( - () => ref.current!.querySelector('.web-page-preview-context-menu .bubble'), + () => ref.current!.querySelector(`.${styles.contextMenu} .bubble`), ); const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => { @@ -156,14 +138,6 @@ const WebPagePreview: FC = ({ return undefined; } - // TODO Refactor so `WebPage` can be used without message - const { photo, ...webPageWithoutPhoto } = renderingWebPage; - const messageStub = { - content: { - webPage: webPageWithoutPhoto, - }, - } as ApiMessage; - function renderContextMenu() { return ( = ({ getTriggerElement={getTriggerElement} getRootElement={getRootElement} getMenuElement={getMenuElement} - className="web-page-preview-context-menu" + className={styles.contextMenu} onClose={handleContextMenuClose} onCloseAnimationEnd={handleContextMenuHide} autoClose @@ -180,36 +154,31 @@ const WebPagePreview: FC = ({ <> { isInvertedMedia ? ( - updateIsInvertedMedia(undefined)}> - {lang('PreviewSender.MoveTextUp')} + {lang('ContextMoveTextUp')} ) : ( - updateIsInvertedMedia(true)}> - {lang(('PreviewSender.MoveTextDown'))} + {lang('ContextMoveTextDown')} ) } {hasMediaSizeOptions && ( isSmallerMedia ? ( - updateIsLargerMedia('large')}> - {lang('ChatInput.EditLink.LargerMedia')} + {lang('ContextLinkLargerMedia')} ) : ( - updateIsLargerMedia('small')}> - {lang(('ChatInput.EditLink.SmallerMedia'))} + {lang('ContextLinkSmallerMedia')} ) )} - {lang('ChatInput.EditLink.RemovePreview')} + {lang('ContextLinkRemovePreview')} @@ -217,24 +186,55 @@ const WebPagePreview: FC = ({ } return ( -
-
-
+
+
+
- + {renderingWebPage && renderingWebPage.webpageType !== 'empty' && ( + + {hasPreviewMedia && ( +
+ {thumbnailUrl && ( + + )} + {shouldRenderPreviewMedia && ( + + )} +
+ )} +
+ + {isFullWebPage + ? (renderingWebPage.siteName || renderingWebPage.url) + : lang('Loading')} + + + {isFullWebPage + ? (renderingWebPage.description || lang(getMediaTypeKey(renderingWebPage))) + : renderingWebPage.url} + +
+
+ )}