WebPage: Support server update (#6118)

This commit is contained in:
zubiden 2025-08-15 18:25:34 +02:00 committed by Alexander Zinchuk
parent baf36a1e15
commit 3d60271505
70 changed files with 1370 additions and 810 deletions

View File

@ -24,7 +24,6 @@ import type {
import { numberToHexColor } from '../../../util/colors'; import { numberToHexColor } from '../../../util/colors';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import { generateRandomInt } from '../gramjsBuilders';
import { addDocumentToLocalDb } from '../helpers/localDb'; import { addDocumentToLocalDb } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc'; import { serializeBytes } from '../helpers/misc';
import { buildApiMessageEntity, buildApiPhoto } from './common'; import { buildApiMessageEntity, buildApiPhoto } from './common';
@ -255,16 +254,6 @@ export function buildBotInlineMessage(
currency: sendMessage.currency, currency: sendMessage.currency,
amount: sendMessage.totalAmount.toJSNumber(), 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 { return {

View File

@ -13,6 +13,7 @@ import type {
ApiMediaInvoice, ApiMediaInvoice,
ApiMediaTodo, ApiMediaTodo,
ApiMessageStoryData, ApiMessageStoryData,
ApiMessageWebPage,
ApiPaidMedia, ApiPaidMedia,
ApiPhoto, ApiPhoto,
ApiPoll, ApiPoll,
@ -35,7 +36,7 @@ import { addTimestampEntities } from '../../../util/dates/timestamp';
import { generateWaveform } from '../../../util/generateWaveform'; import { generateWaveform } from '../../../util/generateWaveform';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import { import {
addMediaToLocalDb, addStoryToLocalDb, type MediaRepairContext, addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
} from '../helpers/localDb'; } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc'; import { serializeBytes } from '../helpers/misc';
import { import {
@ -158,7 +159,7 @@ export function buildMessageMediaContent(
const todo = buildTodoFromMedia(media); const todo = buildTodoFromMedia(media);
if (todo) return { todo }; if (todo) return { todo };
const webPage = buildWebPage(media); const webPage = buildMessageWebPageFromMedia(media);
if (webPage) return { webPage }; if (webPage) return { webPage };
const invoice = buildInvoiceFromMedia(media); const invoice = buildInvoiceFromMedia(media);
@ -798,85 +799,126 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu
}; };
} }
export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined { export function buildMessageWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiMessageWebPage | undefined {
if ( if (!(media instanceof GramJs.MessageMediaWebPage) || media.webpage instanceof GramJs.WebPageNotModified) {
!(media instanceof GramJs.MessageMediaWebPage)
|| !(media.webpage instanceof GramJs.WebPage)
) {
return undefined; return undefined;
} }
const { const {
id, photo, document, attributes, webpage, forceLargeMedia, forceSmallMedia, safe,
} = media.webpage; } = media;
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;
return { return {
mediaType: 'webpage', id: webpage.id.toString(),
id: Number(id), isSafe: safe,
...pick(media.webpage, [ mediaSize: forceSmallMedia ? 'small' : forceLargeMedia ? 'large' : undefined,
'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,
}; };
} }
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 { function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined {
if (!(media instanceof GramJs.MessageMediaPaidMedia)) { if (!(media instanceof GramJs.MessageMediaPaidMedia)) {
return undefined; return undefined;

View File

@ -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) { export function addMediaToLocalDb(media: GramJs.TypeMessageMedia, context?: MediaRepairContext) {
if (media instanceof GramJs.MessageMediaDocument && media.document) { if (media instanceof GramJs.MessageMediaDocument && media.document) {
const document = addMessageRepairInfo(media.document, context); const document = addMessageRepairInfo(media.document, context);
addDocumentToLocalDb(document); 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 instanceof GramJs.MessageMediaGame) {
if (media.game.document) { if (media.game.document) {
const document = addMessageRepairInfo(media.game.document, context); const document = addMessageRepairInfo(media.game.document, context);

View File

@ -29,6 +29,7 @@ import type {
ApiTodoItem, ApiTodoItem,
ApiUser, ApiUser,
ApiUserStatus, ApiUserStatus,
ApiWebPage,
MediaContent, MediaContent,
} from '../../types'; } from '../../types';
import { import {
@ -60,7 +61,8 @@ import {
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiFormattedText } from '../apiBuilders/common'; import { buildApiFormattedText } from '../apiBuilders/common';
import { import {
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia, buildWebPage, buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
buildWebPageFromMedia,
} from '../apiBuilders/messageContent'; } from '../apiBuilders/messageContent';
import { import {
buildApiFactCheck, buildApiFactCheck,
@ -1709,7 +1711,9 @@ export async function fetchWebPagePreview({
entities: textWithEntities.entities, entities: textWithEntities.entities,
})); }));
return preview && buildWebPage(preview.media); if (!preview) return undefined;
return buildWebPageFromMedia(preview.media);
} }
export async function sendPollVote({ export async function sendPollVote({
@ -2363,6 +2367,7 @@ function handleLocalMessageUpdate(
let newContent: MediaContent | undefined; let newContent: MediaContent | undefined;
let poll: ApiPoll | undefined; let poll: ApiPoll | undefined;
let webPage: ApiWebPage | undefined;
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) { if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
if (localMessage.content.text && messageUpdate.entities) { if (localMessage.content.text && messageUpdate.entities) {
newContent = { newContent = {
@ -2377,6 +2382,7 @@ function handleLocalMessageUpdate(
}), }),
}; };
poll = buildPollFromMedia(messageUpdate.media); poll = buildPollFromMedia(messageUpdate.media);
webPage = buildWebPageFromMedia(messageUpdate.media);
} }
const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate); const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate);
@ -2417,6 +2423,7 @@ function handleLocalMessageUpdate(
localId: localMessage.id, localId: localMessage.id,
message: updatedMessage, message: updatedMessage,
poll, poll,
webPage,
}); });
} }

View File

@ -2,11 +2,12 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiChat, ApiPoll, ApiThreadInfo, ApiUser, ApiChat, ApiPoll, ApiThreadInfo, ApiUser,
ApiWebPage,
} from '../../types'; } from '../../types';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildPollFromMedia } from '../apiBuilders/messageContent'; import { buildPollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent';
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages'; import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb'; import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb';
@ -24,6 +25,7 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
let chatById: Record<string, ApiChat> | undefined; let chatById: Record<string, ApiChat> | undefined;
const threadInfos: ApiThreadInfo[] | undefined = []; const threadInfos: ApiThreadInfo[] | undefined = [];
const polls: ApiPoll[] | undefined = []; const polls: ApiPoll[] | undefined = [];
const webPages: ApiWebPage[] | undefined = [];
if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) { if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) {
const users = response.users.map((user: GramJs.TypeUser) => { const users = response.users.map((user: GramJs.TypeUser) => {
@ -54,9 +56,16 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
threadInfos.push(threadInfo); threadInfos.push(threadInfo);
} }
const poll = 'media' in message && message.media && buildPollFromMedia(message.media); if ('media' in message && message.media) {
if (poll) { const poll = buildPollFromMedia(message.media);
polls.push(poll); 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, chats: chatById,
threadInfos: threadInfos?.length ? threadInfos : undefined, threadInfos: threadInfos?.length ? threadInfos : undefined,
polls: polls?.length ? polls : undefined, polls: polls?.length ? polls : undefined,
webPages: webPages?.length ? webPages : undefined,
}); });
} }

View File

@ -5,6 +5,7 @@ import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
import type { import type {
ApiMessage, ApiPoll, ApiStory, ApiStorySkipped, ApiMessage, ApiPoll, ApiStory, ApiStorySkipped,
ApiUpdateConnectionStateType, ApiUpdateConnectionStateType,
ApiWebPage,
} from '../../types'; } from '../../types';
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config'; import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
@ -36,6 +37,8 @@ import {
buildPoll, buildPoll,
buildPollFromMedia, buildPollFromMedia,
buildPollResults, buildPollResults,
buildWebPage,
buildWebPageFromMedia,
} from '../apiBuilders/messageContent'; } from '../apiBuilders/messageContent';
import { import {
buildApiMessage, buildApiMessage,
@ -124,6 +127,7 @@ export function updater(update: Update) {
) { ) {
let message: ApiMessage | undefined; let message: ApiMessage | undefined;
let poll: ApiPoll | undefined; let poll: ApiPoll | undefined;
let webPage: ApiWebPage | undefined;
let shouldForceReply: boolean | undefined; let shouldForceReply: boolean | undefined;
if (update instanceof GramJs.UpdateShortChatMessage) { if (update instanceof GramJs.UpdateShortChatMessage) {
@ -148,6 +152,7 @@ export function updater(update: Update) {
if (mtpMessage instanceof GramJs.Message) { if (mtpMessage instanceof GramJs.Message) {
poll = mtpMessage.media && buildPollFromMedia(mtpMessage.media); poll = mtpMessage.media && buildPollFromMedia(mtpMessage.media);
webPage = mtpMessage.media && buildWebPageFromMedia(mtpMessage.media);
} }
shouldForceReply = 'replyMarkup' in update.message shouldForceReply = 'replyMarkup' in update.message
@ -162,6 +167,7 @@ export function updater(update: Update) {
chatId: message.chatId, chatId: message.chatId,
message, message,
poll, poll,
webPage,
isFromNew: true, isFromNew: true,
}); });
} else { } else {
@ -172,6 +178,7 @@ export function updater(update: Update) {
message, message,
shouldForceReply, shouldForceReply,
poll, poll,
webPage,
isFromNew: true, isFromNew: true,
}); });
} }
@ -275,10 +282,17 @@ export function updater(update: Update) {
const message = buildApiMessage(update.message); const message = buildApiMessage(update.message);
if (!message) return; 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({ sendApiUpdate({
'@type': 'updateQuickReplyMessage', '@type': 'updateQuickReplyMessage',
id: message.id, id: message.id,
message, message,
poll,
webPage,
}); });
} else if (update instanceof GramJs.UpdateDeleteQuickReplyMessages) { } else if (update instanceof GramJs.UpdateDeleteQuickReplyMessages) {
sendApiUpdate({ sendApiUpdate({
@ -326,12 +340,16 @@ export function updater(update: Update) {
const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media
? buildPollFromMedia(mtpMessage.media) : undefined; ? buildPollFromMedia(mtpMessage.media) : undefined;
const webPage = mtpMessage instanceof GramJs.Message && mtpMessage.media
? buildWebPageFromMedia(mtpMessage.media) : undefined;
sendApiUpdate({ sendApiUpdate({
'@type': 'updateMessage', '@type': 'updateMessage',
id: message.id, id: message.id,
chatId: message.chatId, chatId: message.chatId,
message, message,
poll, poll,
webPage,
}); });
} else if (update instanceof GramJs.UpdateMessageReactions) { } else if (update instanceof GramJs.UpdateMessageReactions) {
sendApiUpdate({ sendApiUpdate({
@ -934,6 +952,14 @@ export function updater(update: Update) {
'@type': 'updateWebViewResultSent', '@type': 'updateWebViewResultSent',
queryId: queryId.toString(), 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) { } else if (update instanceof GramJs.UpdateBotMenuButton) {
const { const {
botId, botId,
@ -1075,6 +1101,8 @@ export function updater(update: Update) {
}); });
} else if (update instanceof LocalUpdatePts || update instanceof LocalUpdateChannelPts) { } else if (update instanceof LocalUpdatePts || update instanceof LocalUpdateChannelPts) {
// Do nothing, handled on the manager side // 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) { } else if (DEBUG) {
const params = typeof update === 'object' && 'className' in update ? update.className : update; const params = typeof update === 'object' && 'className' in update ? update.className : update;
log('UNEXPECTED UPDATE', params); log('UNEXPECTED UPDATE', params);

View File

@ -361,9 +361,26 @@ export type ApiNewMediaTodo = {
todo: ApiTodoList; todo: ApiTodoList;
}; };
export interface ApiWebPage { export interface ApiWebPagePending {
mediaType: 'webpage'; 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; url: string;
displayUrl: string; displayUrl: string;
type?: string; type?: string;
@ -378,10 +395,20 @@ export interface ApiWebPage {
story?: ApiWebPageStoryData; story?: ApiWebPageStoryData;
gift?: ApiStarGiftUnique; gift?: ApiStarGiftUnique;
stickers?: ApiWebPageStickerData; stickers?: ApiWebPageStickerData;
mediaSize?: WebPageMediaSize;
hasLargeMedia?: boolean; 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 type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo;
export interface ApiMessageReplyInfo { export interface ApiMessageReplyInfo {
@ -561,7 +588,7 @@ export type MediaContent = {
pollId?: string; pollId?: string;
todo?: ApiMediaTodo; todo?: ApiMediaTodo;
action?: ApiMessageAction; action?: ApiMessageAction;
webPage?: ApiWebPage; webPage?: ApiMessageWebPage;
audio?: ApiAudio; audio?: ApiAudio;
voice?: ApiVoice; voice?: ApiVoice;
invoice?: ApiMediaInvoice; invoice?: ApiMediaInvoice;
@ -580,8 +607,17 @@ export type MediaContainer = {
export type StatefulMediaContent = { export type StatefulMediaContent = {
poll?: ApiPoll; poll?: ApiPoll;
story?: ApiStory; story?: ApiStory;
webPage?: ApiWebPage;
}; };
export type SizeTarget =
'micro'
| 'pictogram'
| 'inline'
| 'preview'
| 'full'
| 'download';
export type BoughtPaidMedia = Pick<MediaContent, 'photo' | 'video'>; export type BoughtPaidMedia = Pick<MediaContent, 'photo' | 'video'>;
export interface ApiMessage { export interface ApiMessage {

View File

@ -33,6 +33,7 @@ import type {
ApiReactions, ApiReactions,
ApiStickerSet, ApiStickerSet,
ApiThreadInfo, ApiThreadInfo,
ApiWebPage,
BoughtPaidMedia, BoughtPaidMedia,
} from './messages'; } from './messages';
import type { import type {
@ -215,6 +216,7 @@ export type ApiUpdateNewScheduledMessage = {
message: ApiMessage; message: ApiMessage;
wasDrafted?: boolean; wasDrafted?: boolean;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
}; };
export type ApiUpdateNewMessage = { export type ApiUpdateNewMessage = {
@ -225,6 +227,7 @@ export type ApiUpdateNewMessage = {
shouldForceReply?: boolean; shouldForceReply?: boolean;
wasDrafted?: boolean; wasDrafted?: boolean;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
}; };
export type ApiUpdateMessage = { export type ApiUpdateMessage = {
@ -233,6 +236,7 @@ export type ApiUpdateMessage = {
id: number; id: number;
message: Partial<ApiMessage>; message: Partial<ApiMessage>;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
shouldForceReply?: boolean; shouldForceReply?: boolean;
isFromNew?: true; isFromNew?: true;
}; };
@ -243,6 +247,7 @@ export type ApiUpdateScheduledMessage = {
id: number; id: number;
message: Partial<ApiMessage>; message: Partial<ApiMessage>;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
isFromNew?: true; isFromNew?: true;
}; };
@ -251,6 +256,7 @@ export type ApiUpdateQuickReplyMessage = {
id: number; id: number;
message: Partial<ApiMessage>; message: Partial<ApiMessage>;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
}; };
export type ApiUpdateDeleteQuickReplyMessages = { export type ApiUpdateDeleteQuickReplyMessages = {
@ -287,6 +293,7 @@ export type ApiUpdateScheduledMessageSendSucceeded = {
localId: number; localId: number;
message: ApiMessage; message: ApiMessage;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
}; };
export type ApiUpdateMessageSendSucceeded = { export type ApiUpdateMessageSendSucceeded = {
@ -295,6 +302,7 @@ export type ApiUpdateMessageSendSucceeded = {
localId: number; localId: number;
message: ApiMessage; message: ApiMessage;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
}; };
export type ApiUpdateVideoProcessingPending = { export type ApiUpdateVideoProcessingPending = {
@ -815,6 +823,7 @@ export type ApiUpdateEntities = {
chats?: Record<string, ApiChat>; chats?: Record<string, ApiChat>;
threadInfos?: ApiThreadInfo[]; threadInfos?: ApiThreadInfo[];
polls?: ApiPoll[]; polls?: ApiPoll[];
webPages?: ApiWebPage[];
}; };
export type ApiUpdatePaidReactionPrivacy = { export type ApiUpdatePaidReactionPrivacy = {
@ -840,6 +849,11 @@ export type ApiUpdateBotCommands = {
commands?: ApiBotCommand[]; commands?: ApiBotCommand[];
}; };
export type ApiUpdateWebPage = {
'@type': 'updateWebPage';
webPage: ApiWebPage;
};
export type ApiUpdate = ( export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
@ -857,7 +871,7 @@ export type ApiUpdate = (
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop | ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateFailedMessageTranslations | ApiUpdateFailedMessageTranslations | ApiUpdateWebPage |
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |

View File

@ -1097,6 +1097,7 @@
"AttachSticker" = "Sticker"; "AttachSticker" = "Sticker";
"AttachMusic" = "Music"; "AttachMusic" = "Music";
"AttachContact" = "Contact"; "AttachContact" = "Contact";
"AttachStory" = "Story";
"MessageLocation" = "Location"; "MessageLocation" = "Location";
"MessageLiveLocation" = "Live Location"; "MessageLiveLocation" = "Live Location";
"ServiceNotifications" = "service notifications"; "ServiceNotifications" = "service notifications";
@ -2140,7 +2141,6 @@
"ToDoListErrorChooseTasks" = "Please enter at least one task."; "ToDoListErrorChooseTasks" = "Please enter at least one task.";
"GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**"; "GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**";
"PremiumPreviewTodo" = "Checklists"; "PremiumPreviewTodo" = "Checklists";
"MenuTon" = "My TON";
"DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram."; "DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram.";
"ButtonTopUpViaFragment" = "Top-Up Via Fragment"; "ButtonTopUpViaFragment" = "Top-Up Via Fragment";
"TonModalHint" = "You can top-up your TON using Fragment."; "TonModalHint" = "You can top-up your TON using Fragment.";
@ -2173,6 +2173,13 @@
"PriceChanged" = "Price Changed"; "PriceChanged" = "Price Changed";
"PayNewPrice" = "Pay New Price"; "PayNewPrice" = "Pay New Price";
"PriceChangedText" = "The price has already changed from **{originalAmount}** to **{newAmount}**. Do you want to pay the 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"; "GlobalSearch" = "Global Search";
"DescriptionPublicPostsSearch" = "Type a keyword to search for posts from public channels."; "DescriptionPublicPostsSearch" = "Type a keyword to search for posts from public channels.";
"ButtonSearchPublicPosts" = "Search {query}"; "ButtonSearchPublicPosts" = "Search {query}";
@ -2189,5 +2196,3 @@
"PublicPostsSubscribeToPremium" = "Subscribe to Premium"; "PublicPostsSubscribeToPremium" = "Subscribe to Premium";
"NotificationPaidExtraSearch" = "{stars} spent on extra search."; "NotificationPaidExtraSearch" = "{stars} spent on extra search.";
"PostsSearchTransaction" = "Posts Search"; "PostsSearchTransaction" = "Posts Search";

View File

@ -1,11 +1,12 @@
import type { ElementRef, FC } from '../../lib/teact/teact'; import type { ElementRef } from '../../lib/teact/teact';
import { import {
memo, useEffect, useLayoutEffect, useMemo, useRef, useState, memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiAudio, ApiMessage, ApiVideo, ApiVoice, ApiAudio, ApiMessage, ApiVideo, ApiVoice,
ApiWebPage,
} from '../../api/types'; } from '../../api/types';
import type { BufferedRange } from '../../hooks/useBuffering'; import type { BufferedRange } from '../../hooks/useBuffering';
import type { OldLangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
@ -14,15 +15,16 @@ import { ApiMediaFormat } from '../../api/types';
import { AudioOrigin } from '../../types'; import { AudioOrigin } from '../../types';
import { import {
getMediaDuration,
getMediaFormat, getMediaFormat,
getMediaHash, getMediaHash,
getMediaTransferState, getMediaTransferState,
getMessageWebPageAudio, getWebPageAudio,
hasMessageTtl, hasMessageTtl,
isMessageLocal, isMessageLocal,
isOwnMessage, isOwnMessage,
} from '../../global/helpers'; } from '../../global/helpers';
import { selectWebPageFromMessage } from '../../global/selectors';
import { selectMessageMediaDuration } from '../../global/selectors/media';
import { makeTrackId } from '../../util/audioPlayer'; import { makeTrackId } from '../../util/audioPlayer';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { captureEvents } from '../../util/captureEvents'; import { captureEvents } from '../../util/captureEvents';
@ -77,13 +79,18 @@ type OwnProps = {
onDateClick?: (arg: ApiMessage) => void; onDateClick?: (arg: ApiMessage) => void;
}; };
type StateProps = {
mediaDuration?: number;
webPage?: ApiWebPage;
};
export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)'); export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)');
export const WITH_AVATAR_TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 410px)'); export const WITH_AVATAR_TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 410px)');
const AVG_VOICE_DURATION = 10; const AVG_VOICE_DURATION = 10;
// This is needed for browsers requiring user interaction before playing. // This is needed for browsers requiring user interaction before playing.
const PRELOAD = true; const PRELOAD = true;
const Audio: FC<OwnProps> = ({ const Audio = ({
theme, theme,
message, message,
senderTitle, senderTitle,
@ -102,13 +109,15 @@ const Audio: FC<OwnProps> = ({
canDownload, canDownload,
canTranscribe, canTranscribe,
autoPlay, autoPlay,
webPage,
mediaDuration,
onHideTranscription, onHideTranscription,
onPlay, onPlay,
onPause, onPause,
onReadMedia, onReadMedia,
onCancelUpload, onCancelUpload,
onDateClick, onDateClick,
}) => { }: OwnProps & StateProps) => {
const { const {
cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal, cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal,
} = getActions(); } = getActions();
@ -118,7 +127,7 @@ const Audio: FC<OwnProps> = ({
audio: contentAudio, voice, video, audio: contentAudio, voice, video,
}, isMediaUnread, }, isMediaUnread,
} = message; } = message;
const audio = contentAudio || getMessageWebPageAudio(message); const audio = contentAudio || getWebPageAudio(webPage);
const media = (voice || video || audio)!; const media = (voice || video || audio)!;
const mediaSource = (voice || video); const mediaSource = (voice || video);
const isVoice = Boolean(voice || video); const isVoice = Boolean(voice || video);
@ -166,7 +175,7 @@ const Audio: FC<OwnProps> = ({
isPlaying, playProgress, playPause, setCurrentTime, duration, isPlaying, playProgress, playPause, setCurrentTime, duration,
} = useAudioPlayer( } = useAudioPlayer(
makeTrackId(message), makeTrackId(message),
getMediaDuration(message)!, mediaDuration!,
trackType, trackType,
mediaData, mediaData,
bufferingHandlers, bufferingHandlers,
@ -716,4 +725,16 @@ function renderSeekline(
); );
} }
export default memo(Audio); export default memo(withGlobal<OwnProps>(
(global, {
message,
}): StateProps => {
const webPage = selectWebPageFromMessage(global, message);
const mediaDuration = selectMessageMediaDuration(global, message);
return {
webPage,
mediaDuration,
};
},
)(Audio));

View File

@ -107,6 +107,7 @@ import {
selectTopicFromMessage, selectTopicFromMessage,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
selectWebPage,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectCurrentLimit } from '../../global/selectors/limits'; import { selectCurrentLimit } from '../../global/selectors/limits';
import { selectSharedSettings } from '../../global/selectors/sharedState'; 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 useEditing from '../middle/composer/hooks/useEditing';
import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip'; import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip';
import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip'; import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip';
import useLoadLinkPreview from '../middle/composer/hooks/useLoadLinkPreview';
import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip'; import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip';
import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation'; import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation';
import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip'; import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip';
@ -826,6 +828,12 @@ const Composer: FC<OwnProps & StateProps> = ({
isDisabled: isInStoryViewer || Boolean(requestedDraft) || (!hasSuggestedPost && isMonoforum), isDisabled: isInStoryViewer || Boolean(requestedDraft) || (!hasSuggestedPost && isMonoforum),
}); });
useLoadLinkPreview({
chatId,
threadId,
getHtml,
});
const resetComposer = useLastCallback((shouldPreserveInput = false) => { const resetComposer = useLastCallback((shouldPreserveInput = false) => {
if (!shouldPreserveInput) { if (!shouldPreserveInput) {
setHtml(''); setHtml('');
@ -2028,8 +2036,7 @@ const Composer: FC<OwnProps & StateProps> = ({
<WebPagePreview <WebPagePreview
chatId={chatId} chatId={chatId}
threadId={threadId} threadId={threadId}
getHtml={getHtml} isDisabled={!canAttachEmbedLinks || hasAttachments || !hasText}
isDisabled={!canAttachEmbedLinks || hasAttachments}
isEditing={Boolean(editingMessage)} isEditing={Boolean(editingMessage)}
/> />
</> </>
@ -2523,6 +2530,8 @@ export default memo(withGlobal<OwnProps>(
const isAppConfigLoaded = global.isAppConfigLoaded; const isAppConfigLoaded = global.isAppConfigLoaded;
const insertingPeerIdMention = tabState.insertingPeerIdMention; const insertingPeerIdMention = tabState.insertingPeerIdMention;
const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined;
return { return {
availableReactions: global.reactions.availableReactions, availableReactions: global.reactions.availableReactions,
topReactions: type === 'story' ? global.reactions.topReactions : undefined, topReactions: type === 'story' ? global.reactions.topReactions : undefined,
@ -2594,7 +2603,7 @@ export default memo(withGlobal<OwnProps>(
quickReplies: global.quickReplies.byId, quickReplies: global.quickReplies.byId,
canSendQuickReplies, canSendQuickReplies,
noWebPage, noWebPage,
webPagePreview: selectTabState(global).webPagePreview, webPagePreview,
isContactRequirePremium: userFullInfo?.isContactRequirePremium, isContactRequirePremium: userFullInfo?.isContactRequirePremium,
effect, effect,
effectReactions, effectReactions,

View File

@ -7,14 +7,14 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { import {
getMessageHtmlId, getMessageHtmlId,
getMessageIsSpoiler, getMessageIsSpoiler,
getMessageMediaHash,
getMessageMediaThumbDataUri,
getMessageVideo, getMessageVideo,
} from '../../global/helpers'; } from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dates/dateFormat'; import { formatMediaDuration } from '../../util/dates/dateFormat';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import useMessageMediaHash from '../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../hooks/media/useThumbnail';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -43,8 +43,9 @@ const Media: FC<OwnProps> = ({
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
const thumbDataUri = getMessageMediaThumbDataUri(message); const thumbDataUri = useThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting); const mediaHash = useMessageMediaHash(message, 'pictogram');
const mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
const transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl); const transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl);
const video = getMessageVideo(message); const video = getMessageVideo(message);

View File

@ -4,13 +4,12 @@ import { memo, useRef } from '../../lib/teact/teact';
import type { ApiBotPreviewMedia } from '../../api/types'; import type { ApiBotPreviewMedia } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import {
getMessageMediaHash, getMessageMediaThumbDataUri,
} from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dates/dateFormat'; import { formatMediaDuration } from '../../util/dates/dateFormat';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import useMessageMediaHash from '../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../hooks/media/useThumbnail';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
@ -38,9 +37,10 @@ const PreviewMedia: FC<OwnProps> = ({
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const isIntersecting = useIsIntersecting(ref, observeIntersection); 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 transitionClassNames = useMediaTransitionDeprecated(mediaBlobUrl);
const video = media.content.video; const video = media.content.video;

View File

@ -11,6 +11,7 @@ import { IS_ANDROID, IS_IOS, IS_WEBM_SUPPORTED } from '../../util/browser/window
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import * as mediaLoader from '../../util/mediaLoader'; import * as mediaLoader from '../../util/mediaLoader';
import useThumbnail from '../../hooks/media/useThumbnail';
import useColorFilter from '../../hooks/stickers/useColorFilter'; import useColorFilter from '../../hooks/stickers/useColorFilter';
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas'; import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
@ -18,7 +19,6 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useMediaTransition from '../../hooks/useMediaTransition'; import useMediaTransition from '../../hooks/useMediaTransition';
import useMountAfterHeavyAnimation from '../../hooks/useMountAfterHeavyAnimation'; import useMountAfterHeavyAnimation from '../../hooks/useMountAfterHeavyAnimation';
import useThumbnail from '../../hooks/useThumbnail';
import useUniqueId from '../../hooks/useUniqueId'; import useUniqueId from '../../hooks/useUniqueId';
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio';
@ -120,7 +120,7 @@ const StickerView: FC<OwnProps> = ({
fullMediaHash === previewMediaHash && (cachedPreview || previewMediaData) fullMediaHash === previewMediaHash && (cachedPreview || previewMediaData)
)); ));
const fullMediaData = useMedia(fullMediaHash || `sticker${id}`, !shouldLoad || shouldSkipLoadingFullMedia); const fullMediaData = useMedia(fullMediaHash || `sticker${id}`, !shouldLoad || shouldSkipLoadingFullMedia);
const shouldRenderFullMedia = isReadyToMountFullMedia && fullMediaData && !isVideoBroken; const shouldRenderFullMedia = isReadyToMountFullMedia && Boolean(fullMediaData) && !isVideoBroken;
const [isPlayerReady, markPlayerReady] = useFlag(); const [isPlayerReady, markPlayerReady] = useFlag();
const isFullMediaReady = shouldRenderFullMedia && (isStatic || isPlayerReady); const isFullMediaReady = shouldRenderFullMedia && (isStatic || isPlayerReady);
@ -129,10 +129,12 @@ const StickerView: FC<OwnProps> = ({
const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb; const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb;
const noCrossTransition = Boolean(isLottie && withPreview); const noCrossTransition = Boolean(isLottie && withPreview);
const thumbRef = useMediaTransition<HTMLImageElement>(thumbData && !isFullMediaReady, { const { ref: thumbRef } = useMediaTransition<HTMLImageElement>({
hasMediaData: Boolean(thumbData && !isFullMediaReady),
noCloseTransition: noCrossTransition, noCloseTransition: noCrossTransition,
}); });
const fullMediaRef = useMediaTransition<HTMLElement>(isFullMediaReady, { const { ref: fullMediaRef } = useMediaTransition<HTMLElement>({
hasMediaData: isFullMediaReady,
noOpenTransition: noCrossTransition, noOpenTransition: noCrossTransition,
}); });

View File

@ -1,5 +1,5 @@
import type { FC } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact'; import { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiMessage, ApiWebPage } from '../../api/types'; import type { ApiMessage, ApiWebPage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
@ -7,8 +7,8 @@ import type { TextPart } from '../../types';
import { import {
getFirstLinkInMessage, getMessageText, getFirstLinkInMessage, getMessageText,
getMessageWebPage,
} from '../../global/helpers'; } from '../../global/helpers';
import { selectWebPageFromMessage } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatPastTimeShort } from '../../util/dates/dateFormat'; import { formatPastTimeShort } from '../../util/dates/dateFormat';
import trimText from '../../util/trimText'; import trimText from '../../util/trimText';
@ -26,6 +26,10 @@ import './WebLink.scss';
const MAX_TEXT_LENGTH = 170; // symbols const MAX_TEXT_LENGTH = 170; // symbols
type ApiWebPageWithFormatted =
ApiWebPage
& { formattedDescription?: TextPart[] };
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
senderTitle?: string; senderTitle?: string;
@ -34,16 +38,16 @@ type OwnProps = {
onMessageClick: (message: ApiMessage) => void; onMessageClick: (message: ApiMessage) => void;
}; };
type ApiWebPageWithFormatted = type StateProps = {
ApiWebPage webPage?: ApiWebPage;
& { formattedDescription?: TextPart[] }; };
const WebLink: FC<OwnProps> = ({ const WebLink = ({
message, senderTitle, isProtected, observeIntersection, onMessageClick, message, webPage, senderTitle, isProtected, observeIntersection, onMessageClick,
}) => { }: OwnProps & StateProps) => {
const lang = useOldLang(); const lang = useOldLang();
let linkData: ApiWebPageWithFormatted | undefined = getMessageWebPage(message); let linkData: ApiWebPageWithFormatted | undefined = webPage;
if (!linkData) { if (!linkData) {
const link = getFirstLinkInMessage(message); const link = getFirstLinkInMessage(message);
@ -64,7 +68,7 @@ const WebLink: FC<OwnProps> = ({
onMessageClick(message); onMessageClick(message);
}); });
if (!linkData) { if (linkData?.webpageType !== 'full') {
return undefined; return undefined;
} }
@ -129,4 +133,14 @@ const WebLink: FC<OwnProps> = ({
); );
}; };
export default memo(WebLink); export default memo(withGlobal<OwnProps>(
(global, {
message,
}): StateProps => {
const webPage = selectWebPageFromMessage(global, message);
return {
webPage,
};
},
)(WebLink));

View File

@ -14,7 +14,6 @@ import type { IconName } from '../../../types/icons';
import { CONTENT_NOT_SUPPORTED, TON_CURRENCY_CODE } from '../../../config'; import { CONTENT_NOT_SUPPORTED, TON_CURRENCY_CODE } from '../../../config';
import { import {
getMessageIsSpoiler, getMessageIsSpoiler,
getMessageMediaHash,
getMessageRoundVideo, getMessageRoundVideo,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
@ -31,12 +30,13 @@ import { getPictogramDimensions } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText'; import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../hooks/media/useThumbnail';
import { useFastClick } from '../../../hooks/useFastClick'; import { useFastClick } from '../../../hooks/useFastClick';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useThumbnail from '../../../hooks/useThumbnail';
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation'; import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
import RippleEffect from '../../ui/RippleEffect'; import RippleEffect from '../../ui/RippleEffect';
@ -111,7 +111,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined; const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined;
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length); 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 mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
const mediaThumbnail = useThumbnail(containedMedia); const mediaThumbnail = useThumbnail(containedMedia);

View File

@ -12,8 +12,6 @@ import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
import { requestMutation } from '../../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
import { import {
getMessageIsSpoiler, getMessageIsSpoiler,
getMessageMediaHash,
getMessageMediaThumbDataUri,
getMessageRoundVideo, getMessageRoundVideo,
getMessageSticker, getMessageSticker,
getMessageVideo, getMessageVideo,
@ -24,6 +22,8 @@ import renderText from '../../../common/helpers/renderText';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import { ChatAnimationTypes } from './useChatAnimationType'; import { ChatAnimationTypes } from './useChatAnimationType';
import useMessageMediaHash from '../../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../../hooks/media/useThumbnail';
import useEnsureStory from '../../../../hooks/useEnsureStory'; import useEnsureStory from '../../../../hooks/useEnsureStory';
import useMedia from '../../../../hooks/useMedia'; import useMedia from '../../../../hooks/useMedia';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
@ -82,8 +82,11 @@ export default function useChatListEntry({
const mediaContent = statefulMediaContent?.story || lastMessage; const mediaContent = statefulMediaContent?.story || lastMessage;
const mediaHasPreview = mediaContent && !getMessageSticker(mediaContent); const mediaHasPreview = mediaContent && !getMessageSticker(mediaContent);
const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(mediaContent) : undefined; const thumbDataUri = useThumbnail(mediaContent);
const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(mediaContent, 'micro') : undefined);
const mediaThumbnail = mediaHasPreview ? thumbDataUri : undefined;
const mediaHash = useMessageMediaHash(mediaContent, 'micro');
const mediaBlobUrl = useMedia(mediaHasPreview ? mediaHash : undefined);
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage)); const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
const renderLastMessageOrTyping = useCallback(() => { const renderLastMessageOrTyping = useCallback(() => {

View File

@ -1,13 +1,14 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { memo, useCallback, useMemo } 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 { ApiMessage } from '../../../api/types';
import type { StateProps } from './helpers/createMapStateToProps'; import type { StateProps } from './helpers/createMapStateToProps';
import { AudioOrigin, LoadMoreDirection } from '../../../types'; import { AudioOrigin, LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config'; 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 { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey'; import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -82,11 +83,12 @@ const AudioResults: FC<OwnProps & StateProps> = ({
function renderList() { function renderList() {
return foundMessages.map((message, index) => { return foundMessages.map((message, index) => {
const global = getGlobal();
const isFirst = index === 0; const isFirst = index === 0;
const shouldDrawDateDivider = isFirst const shouldDrawDateDivider = isFirst
|| toYearMonth(message.date) !== toYearMonth(foundMessages[index - 1].date); || toYearMonth(message.date) !== toYearMonth(foundMessages[index - 1].date);
const media = getMessageDownloadableMedia(message)!; const media = selectMessageDownloadableMedia(global, message)!;
return ( return (
<> <>
{shouldDrawDateDivider && ( {shouldDrawDateDivider && (

View File

@ -10,8 +10,6 @@ import type { OldLangFn } from '../../../hooks/useOldLang';
import { import {
getMessageIsSpoiler, getMessageIsSpoiler,
getMessageMediaHash,
getMessageMediaThumbDataUri,
getMessageRoundVideo, getMessageRoundVideo,
getMessageSticker, getMessageSticker,
getMessageVideo, getMessageVideo,
@ -22,6 +20,8 @@ import buildClassName from '../../../util/buildClassName';
import { formatPastTimeShort } from '../../../util/dates/dateFormat'; import { formatPastTimeShort } from '../../../util/dates/dateFormat';
import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../hooks/media/useThumbnail';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
@ -58,8 +58,10 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
const { focusMessage } = getActions(); const { focusMessage } = getActions();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const mediaThumbnail = !getMessageSticker(message) ? getMessageMediaThumbDataUri(message) : undefined; const thumbDataUri = useThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro')); const mediaThumbnail = !getMessageSticker(message) ? thumbDataUri : undefined;
const mediaHash = useMessageMediaHash(message, 'micro');
const mediaBlobUrl = useMedia(mediaHash);
const isRoundVideo = Boolean(getMessageRoundVideo(message)); const isRoundVideo = Boolean(getMessageRoundVideo(message));
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {

View File

@ -41,7 +41,8 @@ import { disableDirectTextInput, enableDirectTextInput } from '../../util/direct
import { isUserId } from '../../util/entities/ids'; import { isUserId } from '../../util/entities/ids';
import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions'; import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
import { renderMessageText } from '../common/helpers/renderMessageText'; 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 { animateClosing, animateOpening } from './helpers/ghostAnimation';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
@ -88,6 +89,8 @@ type StateProps = {
withDynamicLoading?: boolean; withDynamicLoading?: boolean;
isLoadingMoreMedia?: boolean; isLoadingMoreMedia?: boolean;
isSynced?: boolean; isSynced?: boolean;
currentItem?: MediaViewerItem;
viewableMedia?: ViewableMedia;
}; };
const ANIMATION_DURATION = 250; const ANIMATION_DURATION = 250;
@ -115,6 +118,8 @@ const MediaViewer = ({
withDynamicLoading, withDynamicLoading,
isLoadingMoreMedia, isLoadingMoreMedia,
isSynced, isSynced,
currentItem,
viewableMedia,
}: StateProps) => { }: StateProps) => {
const { const {
openMediaViewer, openMediaViewer,
@ -131,6 +136,8 @@ const MediaViewer = ({
const isOpen = Boolean(avatarOwner || message || standaloneMedia || sponsoredMessage); const isOpen = Boolean(avatarOwner || message || standaloneMedia || sponsoredMessage);
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const { media, isSingle } = viewableMedia || {};
/* Animation */ /* Animation */
const animationKey = useRef<number>(); const animationKey = useRef<number>();
const senderId = message?.senderId || avatarOwner?.id || message?.chatId; const senderId = message?.senderId || avatarOwner?.id || message?.chatId;
@ -141,11 +148,6 @@ const MediaViewer = ({
/* Controls */ /* Controls */
const [isReportAvatarModalOpen, openReportAvatarModal, closeReportAvatarModal] = useFlag(); const [isReportAvatarModalOpen, openReportAvatarModal, closeReportAvatarModal] = useFlag();
const currentItem = getMediaViewerItem({
message, avatarOwner, standaloneMedia, profilePhotos, mediaIndex, sponsoredMessage,
});
const { media, isSingle } = getViewableMedia(currentItem) || {};
const { const {
isVideo, isVideo,
isPhoto, isPhoto,
@ -504,18 +506,24 @@ export default memo(withGlobal(
const isChatWithSelf = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const isChatWithSelf = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
if (isAvatarView) { if (isAvatarView) {
const peer = selectPeer(global, chatId!); const avatarOwner = selectPeer(global, chatId!);
let canUpdateMedia = false; let canUpdateMedia = false;
if (peer) { if (avatarOwner) {
canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat); canUpdateMedia = isUserId(avatarOwner.id)
? avatarOwner.id === currentUserId : isChatAdmin(avatarOwner as ApiChat);
} }
const profilePhotos = selectPeerPhotos(global, chatId!); const profilePhotos = selectPeerPhotos(global, chatId!);
const currentItem = getMediaViewerItem({
avatarOwner, standaloneMedia, profilePhotos, mediaIndex,
});
const viewableMedia = selectViewableMedia(global, currentItem);
return { return {
profilePhotos, profilePhotos,
avatar: profilePhotos?.photos[mediaIndex!], avatar: profilePhotos?.photos[mediaIndex!],
avatarOwner: peer, avatarOwner,
isLoadingMoreMedia: profilePhotos?.isLoading, isLoadingMoreMedia: profilePhotos?.isLoading,
isChatWithSelf, isChatWithSelf,
canUpdateMedia, canUpdateMedia,
@ -526,6 +534,8 @@ export default memo(withGlobal(
standaloneMedia, standaloneMedia,
mediaIndex, mediaIndex,
isSynced, isSynced,
currentItem,
viewableMedia,
}; };
} }
@ -576,6 +586,11 @@ export default memo(withGlobal(
} }
} }
const currentItem = getMediaViewerItem({
message, standaloneMedia, mediaIndex, sponsoredMessage,
});
const viewableMedia = selectViewableMedia(global, currentItem);
return { return {
chatId, chatId,
threadId, threadId,
@ -594,6 +609,8 @@ export default memo(withGlobal(
mediaIndex, mediaIndex,
isLoadingMoreMedia, isLoadingMoreMedia,
isSynced, isSynced,
currentItem,
viewableMedia,
}; };
}, },
)(MediaViewer)); )(MediaViewer));

View File

@ -6,7 +6,7 @@ import type { ApiChat } from '../../api/types';
import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types'; import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import type { MenuItemProps } from '../ui/MenuItem'; import type { MenuItemProps } from '../ui/MenuItem';
import type { MediaViewerItem } from './helpers/getViewableMedia'; import type { MediaViewerItem, ViewableMedia } from './helpers/getViewableMedia';
import { import {
getIsDownloading, getIsDownloading,
@ -23,7 +23,7 @@ import {
selectTabState, selectTabState,
} from '../../global/selectors'; } from '../../global/selectors';
import { isUserId } from '../../util/entities/ids'; import { isUserId } from '../../util/entities/ids';
import getViewableMedia from './helpers/getViewableMedia'; import selectViewableMedia from './helpers/getViewableMedia';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
@ -41,17 +41,6 @@ import ProgressSpinner from '../ui/ProgressSpinner';
import './MediaViewerActions.scss'; import './MediaViewerActions.scss';
type StateProps = {
activeDownloads: ActiveDownloads;
isProtected?: boolean;
isChatProtected?: boolean;
canDelete?: boolean;
chat?: ApiChat;
canUpdate?: boolean;
messageListType?: MessageListType;
origin?: MediaViewerOrigin;
};
type OwnProps = { type OwnProps = {
item?: MediaViewerItem; item?: MediaViewerItem;
mediaData?: string; mediaData?: string;
@ -65,6 +54,18 @@ type OwnProps = {
onForward: NoneToVoidFunction; 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<OwnProps & StateProps> = ({ const MediaViewerActions: FC<OwnProps & StateProps> = ({
item, item,
mediaData, mediaData,
@ -78,6 +79,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
messageListType, messageListType,
activeDownloads, activeDownloads,
origin, origin,
viewableMedia,
onReportAvatar: onReport, onReportAvatar: onReport,
onCloseMediaViewer, onCloseMediaViewer,
onBeforeDelete, onBeforeDelete,
@ -98,7 +100,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
const isMessage = item?.type === 'message'; const isMessage = item?.type === 'message';
const { media } = getViewableMedia(item) || {}; const { media } = viewableMedia || {};
const fileName = media && getMediaFilename(media); const fileName = media && getMediaFilename(media);
const isDownloading = media && getIsDownloading(activeDownloads, media); const isDownloading = media && getIsDownloading(activeDownloads, media);
@ -411,6 +413,7 @@ export default memo(withGlobal<OwnProps>(
const canDelete = canDeleteMessage || canDeleteAvatar; const canDelete = canDeleteMessage || canDeleteAvatar;
const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar; const canUpdate = canUpdateMedia && Boolean(avatarPhoto) && !isCurrentAvatar;
const messageListType = currentMessageList?.type; const messageListType = currentMessageList?.type;
const viewableMedia = selectViewableMedia(global, item);
return { return {
activeDownloads, activeDownloads,
@ -421,6 +424,7 @@ export default memo(withGlobal<OwnProps>(
canUpdate, canUpdate,
messageListType, messageListType,
origin, origin,
viewableMedia,
}; };
}, },
)(MediaViewerActions)); )(MediaViewerActions));

View File

@ -6,19 +6,20 @@ import type {
ApiDimensions, ApiMessage, ApiSponsoredMessage, ApiDimensions, ApiMessage, ApiSponsoredMessage,
} from '../../api/types'; } from '../../api/types';
import type { MediaViewerOrigin, ThreadId } from '../../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 { MEDIA_TIMESTAMP_SAVE_MINIMUM_DURATION } from '../../config';
import { import {
selectIsMessageProtected, selectMessageTimestampableDuration, selectTabState, selectIsMessageProtected, selectTabState,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectMessageTimestampableDuration } from '../../global/selectors/media';
import { ARE_WEBCODECS_SUPPORTED } from '../../util/browser/globalEnvironment'; import { ARE_WEBCODECS_SUPPORTED } from '../../util/browser/globalEnvironment';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions'; import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
import { renderMessageText } from '../common/helpers/renderMessageText'; import { renderMessageText } from '../common/helpers/renderMessageText';
import getViewableMedia from './helpers/getViewableMedia'; import selectViewableMedia from './helpers/getViewableMedia';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
@ -46,6 +47,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
viewableMedia?: ViewableMedia;
textMessage?: ApiMessage | ApiSponsoredMessage; textMessage?: ApiMessage | ApiSponsoredMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isProtected?: boolean; isProtected?: boolean;
@ -64,6 +66,7 @@ const PLAYBACK_SAVE_INTERVAL = 1000;
const MediaViewerContent = ({ const MediaViewerContent = ({
item, item,
viewableMedia,
isActive, isActive,
textMessage, textMessage,
origin, origin,
@ -87,7 +90,7 @@ const MediaViewerContent = ({
const isAvatar = item.type === 'avatar'; const isAvatar = item.type === 'avatar';
const isSponsoredMessage = item.type === 'sponsoredMessage'; const isSponsoredMessage = item.type === 'sponsoredMessage';
const { media } = getViewableMedia(item) || {}; const { media } = viewableMedia || {};
const { const {
isVideo, isVideo,
@ -254,6 +257,7 @@ export default memo(withGlobal<OwnProps>(
const message = item.type === 'message' ? item.message : undefined; const message = item.type === 'message' ? item.message : undefined;
const sponsoredMessage = item.type === 'sponsoredMessage' ? item.message : undefined; const sponsoredMessage = item.type === 'sponsoredMessage' ? item.message : undefined;
const textMessage = message || sponsoredMessage; const textMessage = message || sponsoredMessage;
const viewableMedia = selectViewableMedia(global, item);
const maxTimestamp = message && selectMessageTimestampableDuration(global, message, true); const maxTimestamp = message && selectMessageTimestampableDuration(global, message, true);
@ -268,6 +272,7 @@ export default memo(withGlobal<OwnProps>(
threadId, threadId,
timestamp, timestamp,
maxTimestamp, maxTimestamp,
viewableMedia,
}; };
}, },
)(MediaViewerContent)); )(MediaViewerContent));

View File

@ -1,9 +1,11 @@
import type { import type {
ApiMessage, ApiPeer, ApiPeerPhotos, ApiSponsoredMessage, ApiMessage, ApiPeer, ApiPeerPhotos, ApiSponsoredMessage,
} from '../../../api/types'; } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { MediaViewerMedia } from '../../../types'; import type { MediaViewerMedia } from '../../../types';
import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers'; import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers';
import { selectWebPageFromMessage } from '../../../global/selectors';
export type MediaViewerItem = { export type MediaViewerItem = {
type: 'message'; type: 'message';
@ -24,7 +26,7 @@ export type MediaViewerItem = {
mediaIndex?: number; mediaIndex?: number;
}; };
type ViewableMedia = { export type ViewableMedia = {
media: MediaViewerMedia; media: MediaViewerMedia;
isSingle?: boolean; isSingle?: boolean;
}; };
@ -75,7 +77,7 @@ export function getMediaViewerItem({
return undefined; 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) return undefined;
if (params.type === 'standalone') { if (params.type === 'standalone') {
@ -96,7 +98,7 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi
} }
const { const {
action, document, photo, video, webPage, paidMedia, action, document, photo, video, paidMedia,
} = getMessageContent(params.message); } = getMessageContent(params.message);
if (action?.type === 'chatEditPhoto' || action?.type === 'suggestProfilePhoto') { 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 { photo: webPagePhoto, video: webPageVideo, document: webPageDocument } = webPage;
const isDocumentMedia = webPageDocument && (isDocumentPhoto(webPageDocument) || isDocumentVideo(webPageDocument)); const isDocumentMedia = webPageDocument && (isDocumentPhoto(webPageDocument) || isDocumentVideo(webPageDocument));
const mediaDocument = isDocumentMedia ? webPageDocument : undefined; const mediaDocument = isDocumentMedia ? webPageDocument : undefined;

View File

@ -1,4 +1,3 @@
import type { FC } from '../../../lib/teact/teact';
import { import {
memo, useEffect, memo, useEffect,
useMemo, useMemo,
@ -18,8 +17,6 @@ import {
getMessageAudio, getMessageDocument, getMessageAudio, getMessageDocument,
getMessagePhoto, getMessagePhoto,
getMessageVideo, getMessageVoice, getMessageVideo, getMessageVoice,
getMessageWebPagePhoto,
getMessageWebPageVideo,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -57,18 +54,18 @@ export type OwnProps = {
peerType?: ApiAttachMenuPeerType; peerType?: ApiAttachMenuPeerType;
shouldCollectDebugLogs?: boolean; shouldCollectDebugLogs?: boolean;
theme: ThemeKey; theme: ThemeKey;
canEditMedia?: boolean;
editingMessage?: ApiMessage;
messageListType?: MessageListType;
paidMessagesStars?: number;
onFileSelect: (files: File[]) => void; onFileSelect: (files: File[]) => void;
onPollCreate: NoneToVoidFunction; onPollCreate: NoneToVoidFunction;
onTodoListCreate: NoneToVoidFunction; onTodoListCreate: NoneToVoidFunction;
onMenuOpen: NoneToVoidFunction; onMenuOpen: NoneToVoidFunction;
onMenuClose: NoneToVoidFunction; onMenuClose: NoneToVoidFunction;
canEditMedia?: boolean;
editingMessage?: ApiMessage;
messageListType?: MessageListType;
paidMessagesStars?: number;
}; };
const AttachMenu: FC<OwnProps> = ({ const AttachMenu = ({
chatId, chatId,
threadId, threadId,
isButtonVisible, isButtonVisible,
@ -84,16 +81,16 @@ const AttachMenu: FC<OwnProps> = ({
isScheduled, isScheduled,
theme, theme,
shouldCollectDebugLogs, shouldCollectDebugLogs,
canEditMedia,
editingMessage,
messageListType,
paidMessagesStars,
onFileSelect, onFileSelect,
onMenuOpen, onMenuOpen,
onMenuClose, onMenuClose,
onPollCreate, onPollCreate,
onTodoListCreate, onTodoListCreate,
canEditMedia, }: OwnProps) => {
editingMessage,
messageListType,
paidMessagesStars,
}) => {
const { const {
updateAttachmentSettings, updateAttachmentSettings,
} = getActions(); } = getActions();
@ -107,8 +104,8 @@ const AttachMenu: FC<OwnProps> = ({
const isMenuOpen = isAttachMenuOpen || isAttachmentBotMenuOpen; const isMenuOpen = isAttachMenuOpen || isAttachmentBotMenuOpen;
const isPhotoOrVideo = editingMessage && editingMessage?.groupedId const isPhotoOrVideo = editingMessage && editingMessage?.groupedId
&& Boolean(getMessagePhoto(editingMessage) || getMessageWebPagePhoto(editingMessage) && Boolean(getMessagePhoto(editingMessage)
|| Boolean(getMessageVideo(editingMessage) || getMessageWebPageVideo(editingMessage))); || Boolean(getMessageVideo(editingMessage)));
const isFile = editingMessage && editingMessage?.groupedId && Boolean(getMessageAudio(editingMessage) const isFile = editingMessage && editingMessage?.groupedId && Boolean(getMessageAudio(editingMessage)
|| getMessageVoice(editingMessage) || getMessageDocument(editingMessage)); || getMessageVoice(editingMessage) || getMessageDocument(editingMessage));

View File

@ -554,12 +554,12 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
canInvertMedia && (!isInvertedMedia ? ( canInvertMedia && (!isInvertedMedia ? (
<MenuItem icon="move-caption-up" onClick={() => setIsInvertedMedia(true)}> <MenuItem icon="move-caption-up" onClick={() => setIsInvertedMedia(true)}>
{oldLang('PreviewSender.MoveTextUp')} {lang('ContextMoveTextUp')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem icon="move-caption-down" onClick={() => setIsInvertedMedia(undefined)}> <MenuItem icon="move-caption-down" onClick={() => setIsInvertedMedia(undefined)}>
{oldLang(('PreviewSender.MoveTextDown'))} {lang('ContextMoveTextDown')}
</MenuItem> </MenuItem>
)) ))
} }

View File

@ -22,10 +22,10 @@ import {
selectForwardedSender, selectForwardedSender,
selectIsChatWithSelf, selectIsChatWithSelf,
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectIsMediaNsfw,
selectSender, selectSender,
selectTabState, selectTabState,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { selectIsMediaNsfw } from '../../../global/selectors/media';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';

View File

@ -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 { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
@ -24,9 +22,9 @@ import './DropArea.scss';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
withQuick?: boolean; withQuick?: boolean;
editingMessage?: ApiMessage | undefined;
onHide: NoneToVoidFunction; onHide: NoneToVoidFunction;
onFileSelect: (files: File[]) => void; onFileSelect: (files: File[]) => void;
editingMessage?: ApiMessage | undefined;
}; };
export enum DropAreaState { export enum DropAreaState {
@ -37,9 +35,9 @@ export enum DropAreaState {
const DROP_LEAVE_TIMEOUT_MS = 150; const DROP_LEAVE_TIMEOUT_MS = 150;
const DropArea: FC<OwnProps> = ({ const DropArea = ({
isOpen, withQuick, onHide, onFileSelect, editingMessage, isOpen, withQuick, editingMessage, onHide, onFileSelect,
}) => { }: OwnProps) => {
const lang = useLang(); const lang = useLang();
const { showNotification, updateAttachmentSettings } = getActions(); const { showNotification, updateAttachmentSettings } = getActions();
const hideTimeoutRef = useRef<number>(); const hideTimeoutRef = useRef<number>();

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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 { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage, ApiWebPage,
ApiWebPageFull,
ApiWebPagePending,
} from '../../../api/types'; } from '../../../api/types';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { ThemeKey, ThreadId, WebPageMediaSize } from '../../../types'; import type { ThreadId, WebPageMediaSize } from '../../../types';
import type { Signal } from '../../../util/signals';
import { ApiMessageEntityTypes } from '../../../api/types';
import { RE_LINK_TEMPLATE } from '../../../config'; import {
import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors'; getMediaHash,
getWebPageAudio,
getWebPageDocument,
getWebPagePhoto,
getWebPageVideo,
} from '../../../global/helpers';
import { selectNoWebPage, selectTabState, selectWebPage } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; 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 useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDerivedSignal from '../../../hooks/useDerivedSignal';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useMedia from '../../../hooks/useMedia';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; import useShowTransition from '../../../hooks/useShowTransition';
import useSyncEffect from '../../../hooks/useSyncEffect';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
import WebPage from '../message/WebPage';
import './WebPagePreview.scss'; import styles from './WebPagePreview.module.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId: ThreadId; threadId: ThreadId;
getHtml: Signal<string>;
isEditing: boolean; isEditing: boolean;
isDisabled?: boolean; isDisabled?: boolean;
}; };
type StateProps = { type StateProps = {
webPagePreview?: ApiWebPage; webPagePreview?: ApiWebPageFull | ApiWebPagePending;
noWebPage?: boolean; noWebPage?: boolean;
theme: ThemeKey;
attachmentSettings: GlobalState['attachmentSettings']; attachmentSettings: GlobalState['attachmentSettings'];
}; };
const DEBOUNCE_MS = 300; const WebPagePreview = ({
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
const WebPagePreview: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
getHtml,
isDisabled, isDisabled,
webPagePreview, webPagePreview,
noWebPage, noWebPage,
theme,
attachmentSettings, attachmentSettings,
isEditing, isEditing,
}) => { }: OwnProps & StateProps) => {
const { const {
loadWebPagePreview,
clearWebPagePreview,
toggleMessageWebPage, toggleMessageWebPage,
updateAttachmentSettings, updateAttachmentSettings,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useLang();
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const isInvertedMedia = attachmentSettings.isInvertedMedia; const isInvertedMedia = attachmentSettings.isInvertedMedia;
const isSmallerMedia = attachmentSettings.webPageMediaSize === 'small'; 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(() => { const isShown = useDerivedState(() => {
return Boolean(webPagePreview && getHtml() && !noWebPage && !isDisabled); return Boolean(webPagePreview && !noWebPage && !isDisabled);
}, [isDisabled, getHtml, noWebPage, webPagePreview]); }, [isDisabled, noWebPage, webPagePreview]);
const { shouldRender, transitionClassNames } = useShowTransitionDeprecated(isShown); 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<ApiWebPage | undefined>(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<HTMLImageElement>({
isOpen: Boolean(previewMediaUrl),
withShouldRender: true,
noCloseTransition: true,
});
const hasPreviewMedia = Boolean(previewMediaUrl || shouldRenderPreviewMedia);
const handleClearWebpagePreview = useLastCallback(() => { const handleClearWebpagePreview = useLastCallback(() => {
toggleMessageWebPage({ chatId, threadId, noWebPage: true }); toggleMessageWebPage({ chatId, threadId, noWebPage: true });
@ -124,13 +106,13 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
const { const {
isContextMenuOpen, contextMenuAnchor, handleContextMenu, isContextMenuOpen, contextMenuAnchor, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide, handleBeforeContextMenu,
} = useContextMenuHandlers(ref, isEditing, true); } = useContextMenuHandlers(ref, isEditing, true);
const getTriggerElement = useLastCallback(() => ref.current); const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!); const getRootElement = useLastCallback(() => ref.current!);
const getMenuElement = useLastCallback( 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 => { const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => {
@ -156,14 +138,6 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
// TODO Refactor so `WebPage` can be used without message
const { photo, ...webPageWithoutPhoto } = renderingWebPage;
const messageStub = {
content: {
webPage: webPageWithoutPhoto,
},
} as ApiMessage;
function renderContextMenu() { function renderContextMenu() {
return ( return (
<Menu <Menu
@ -172,7 +146,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
getTriggerElement={getTriggerElement} getTriggerElement={getTriggerElement}
getRootElement={getRootElement} getRootElement={getRootElement}
getMenuElement={getMenuElement} getMenuElement={getMenuElement}
className="web-page-preview-context-menu" className={styles.contextMenu}
onClose={handleContextMenuClose} onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide} onCloseAnimationEnd={handleContextMenuHide}
autoClose autoClose
@ -180,36 +154,31 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
<> <>
{ {
isInvertedMedia ? ( isInvertedMedia ? (
<MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}> <MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}>
{lang('PreviewSender.MoveTextUp')} {lang('ContextMoveTextUp')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem icon="move-caption-down" onClick={() => updateIsInvertedMedia(true)}> <MenuItem icon="move-caption-down" onClick={() => updateIsInvertedMedia(true)}>
{lang(('PreviewSender.MoveTextDown'))} {lang('ContextMoveTextDown')}
</MenuItem> </MenuItem>
) )
} }
{hasMediaSizeOptions && ( {hasMediaSizeOptions && (
isSmallerMedia ? ( isSmallerMedia ? (
<MenuItem icon="expand" onClick={() => updateIsLargerMedia('large')}> <MenuItem icon="expand" onClick={() => updateIsLargerMedia('large')}>
{lang('ChatInput.EditLink.LargerMedia')} {lang('ContextLinkLargerMedia')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem icon="collapse" onClick={() => updateIsLargerMedia('small')}> <MenuItem icon="collapse" onClick={() => updateIsLargerMedia('small')}>
{lang(('ChatInput.EditLink.SmallerMedia'))} {lang('ContextLinkSmallerMedia')}
</MenuItem> </MenuItem>
) )
)} )}
<MenuItem <MenuItem
icon="delete" icon="delete"
onClick={handleClearWebpagePreview} onClick={handleClearWebpagePreview}
> >
{lang('ChatInput.EditLink.RemovePreview')} {lang('ContextLinkRemovePreview')}
</MenuItem> </MenuItem>
</> </>
</Menu> </Menu>
@ -217,24 +186,55 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
} }
return ( return (
<div className={buildClassName('WebPagePreview', transitionClassNames)} ref={ref}> <div
<div className="WebPagePreview_inner"> className={buildClassName(
<div className="WebPagePreview-left-icon" onClick={handlePreviewClick}> styles.root,
!isEditing && styles.interactive,
)}
ref={ref}
>
<div className={styles.inner}>
<div className={styles.leftIcon} onClick={handlePreviewClick}>
<Icon name="link" /> <Icon name="link" />
</div> </div>
<WebPage {renderingWebPage && renderingWebPage.webpageType !== 'empty' && (
message={messageStub} <PeerColorWrapper
inPreview noUserColors
theme={theme} className={styles.preview}
onContainerClick={handlePreviewClick} onContextMenu={handleContextMenu}
isEditing={isEditing} onMouseDown={handleBeforeContextMenu}
/> onClick={handlePreviewClick}
>
{hasPreviewMedia && (
<div className={styles.previewImageContainer}>
{thumbnailUrl && (
<img src={thumbnailUrl} alt="" className={styles.previewImage} />
)}
{shouldRenderPreviewMedia && (
<img ref={previewMediaRef} src={previewMediaUrl} alt="" className={styles.previewImage} />
)}
</div>
)}
<div className={styles.previewText}>
<span className={styles.siteName}>
{isFullWebPage
? (renderingWebPage.siteName || renderingWebPage.url)
: lang('Loading')}
</span>
<span className={styles.siteDescription}>
{isFullWebPage
? (renderingWebPage.description || lang(getMediaTypeKey(renderingWebPage)))
: renderingWebPage.url}
</span>
</div>
</PeerColorWrapper>
)}
<Button <Button
className="WebPagePreview-clear" className={styles.clear}
round round
faded faded
color="translucent" color="translucent"
ariaLabel="Clear Webpage Preview" ariaLabel={lang('AccLinkRemovePreview')}
onClick={handleClearWebpagePreview} onClick={handleClearWebpagePreview}
> >
<Icon name="close" /> <Icon name="close" />
@ -245,15 +245,27 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
); );
}; };
function getMediaTypeKey(webPage: ApiWebPageFull) {
if (webPage.photo) return 'AttachPhoto';
if (webPage.video) return 'AttachVideo';
if (webPage.audio) return 'AttachMusic';
if (webPage.document) return 'AttachDocument';
if (webPage.story) return 'AttachStory';
return 'LinkPreview';
}
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId }): StateProps => { (global, { chatId, threadId }): StateProps => {
const tabState = selectTabState(global);
const noWebPage = selectNoWebPage(global, chatId, threadId); const noWebPage = selectNoWebPage(global, chatId, threadId);
const { const {
attachmentSettings, attachmentSettings,
} = global; } = global;
const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined;
return { return {
theme: selectTheme(global), webPagePreview: webPagePreview?.webpageType === 'empty' ? undefined : webPagePreview,
webPagePreview: selectTabState(global).webPagePreview,
noWebPage, noWebPage,
attachmentSettings, attachmentSettings,
}; };

View File

@ -0,0 +1,68 @@
import { useEffect, useRef } from '@teact';
import { getActions } from '../../../../global';
import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals';
import {
type ApiFormattedText,
type ApiMessageEntityTextUrl,
ApiMessageEntityTypes,
} from '../../../../api/types';
import { RE_LINK_TEMPLATE } from '../../../../config';
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
import useDerivedSignal from '../../../../hooks/useDerivedSignal';
import useSyncEffect from '../../../../hooks/useSyncEffect';
const DEBOUNCE_MS = 300;
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
export default function useLoadLinkPreview({
getHtml,
chatId,
threadId,
}: {
chatId: string;
threadId: ThreadId;
getHtml: Signal<string>;
}) {
const {
loadWebPagePreview,
clearWebPagePreview,
toggleMessageWebPage,
} = getActions();
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
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]);
}

View File

@ -15,6 +15,7 @@ import type {
ApiStickerSetInfo, ApiStickerSetInfo,
ApiThreadInfo, ApiThreadInfo,
ApiTypeStory, ApiTypeStory,
ApiWebPage,
} from '../../../api/types'; } from '../../../api/types';
import type { import type {
ActiveDownloads, ActiveDownloads,
@ -33,7 +34,6 @@ import {
areReactionsEmpty, areReactionsEmpty,
getCanPostInChat, getCanPostInChat,
getIsDownloading, getIsDownloading,
getMessageDownloadableMedia,
getMessageVideo, getMessageVideo,
getUserFullName, getUserFullName,
hasMessageTtl, hasMessageTtl,
@ -75,6 +75,7 @@ import {
selectUser, selectUser,
selectUserStatus, selectUserStatus,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { selectMessageDownloadableMedia } from '../../../global/selectors/media';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { isUserId } from '../../../util/entities/ids'; import { isUserId } from '../../../util/entities/ids';
@ -110,6 +111,7 @@ export type OwnProps = {
type StateProps = { type StateProps = {
threadId?: ThreadId; threadId?: ThreadId;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
story?: ApiTypeStory; story?: ApiTypeStory;
chat?: ApiChat; chat?: ApiChat;
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
@ -180,6 +182,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
customEmojiSets, customEmojiSets,
album, album,
poll, poll,
webPage,
story, story,
anchor, anchor,
targetHref, targetHref,
@ -345,15 +348,16 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}, [message.reactions?.recentReactions, message.seenByDates]); }, [message.reactions?.recentReactions, message.seenByDates]);
const isDownloading = useMemo(() => { const isDownloading = useMemo(() => {
const global = getGlobal();
if (album) { if (album) {
return album.messages.some((msg) => { return album.messages.some((msg) => {
const downloadableMedia = getMessageDownloadableMedia(msg); const downloadableMedia = selectMessageDownloadableMedia(global, msg);
if (!downloadableMedia) return false; if (!downloadableMedia) return false;
return getIsDownloading(activeDownloads, downloadableMedia); return getIsDownloading(activeDownloads, downloadableMedia);
}); });
} }
const downloadableMedia = getMessageDownloadableMedia(message); const downloadableMedia = selectMessageDownloadableMedia(global, message);
if (!downloadableMedia) return false; if (!downloadableMedia) return false;
return getIsDownloading(activeDownloads, downloadableMedia); return getIsDownloading(activeDownloads, downloadableMedia);
}, [activeDownloads, album, message]); }, [activeDownloads, album, message]);
@ -578,8 +582,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleDownloadClick = useLastCallback(() => { const handleDownloadClick = useLastCallback(() => {
const global = getGlobal();
(album?.messages || [message]).forEach((msg) => { (album?.messages || [message]).forEach((msg) => {
const downloadableMedia = getMessageDownloadableMedia(msg); const downloadableMedia = selectMessageDownloadableMedia(global, msg);
if (!downloadableMedia) return; if (!downloadableMedia) return;
if (isDownloading) { if (isDownloading) {
@ -728,6 +733,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
isInSavedMessages={isInSavedMessages} isInSavedMessages={isInSavedMessages}
noReplies={noReplies} noReplies={noReplies}
poll={poll} poll={poll}
webPage={webPage}
story={story} story={story}
onOpenThread={handleOpenThread} onOpenThread={handleOpenThread}
onReply={handleReply} onReply={handleReply}

View File

@ -27,6 +27,7 @@ import type {
ApiTopic, ApiTopic,
ApiTypeStory, ApiTypeStory,
ApiUser, ApiUser,
ApiWebPage,
} from '../../../api/types'; } from '../../../api/types';
import type { ActionPayloads } from '../../../global/types'; import type { ActionPayloads } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
@ -52,10 +53,10 @@ import {
getMainUsername, getMainUsername,
getMessageContent, getMessageContent,
getMessageCustomShape, getMessageCustomShape,
getMessageDownloadableMedia,
getMessageHtmlId, getMessageHtmlId,
getMessageSingleCustomEmoji, getMessageSingleCustomEmoji,
getMessageSingleRegularEmoji, getMessageSingleRegularEmoji,
getMessageWebPage,
hasMessageText, hasMessageText,
hasMessageTtl, hasMessageTtl,
isAnonymousForwardsChat, isAnonymousForwardsChat,
@ -85,6 +86,7 @@ import {
selectCurrentMiddleSearch, selectCurrentMiddleSearch,
selectDefaultReaction, selectDefaultReaction,
selectForwardedSender, selectForwardedSender,
selectFullWebPageFromMessage,
selectIsChatProtected, selectIsChatProtected,
selectIsChatRestricted, selectIsChatRestricted,
selectIsChatWithSelf, selectIsChatWithSelf,
@ -92,13 +94,10 @@ import {
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectIsDocumentGroupSelected, selectIsDocumentGroupSelected,
selectIsInSelectMode, selectIsInSelectMode,
selectIsMediaNsfw,
selectIsMessageFocused, selectIsMessageFocused,
selectIsMessageProtected, selectIsMessageProtected,
selectIsMessageSelected, selectIsMessageSelected,
selectMessageIdsByGroupId, selectMessageIdsByGroupId,
selectMessageLastPlaybackTimestamp,
selectMessageTimestampableDuration,
selectOutgoingStatus, selectOutgoingStatus,
selectPeer, selectPeer,
selectPeerStory, selectPeerStory,
@ -118,6 +117,12 @@ import {
selectUploadProgress, selectUploadProgress,
selectUser, selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
import {
selectIsMediaNsfw,
selectMessageDownloadableMedia,
selectMessageLastPlaybackTimestamp,
selectMessageTimestampableDuration,
} from '../../../global/selectors/media';
import { selectSharedSettings } from '../../../global/selectors/sharedState'; import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment'; import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -312,6 +317,7 @@ type StateProps = {
viaBusinessBot?: ApiUser; viaBusinessBot?: ApiUser;
effect?: ApiAvailableEffect; effect?: ApiAvailableEffect;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
maxTimestamp?: number; maxTimestamp?: number;
lastPlaybackTimestamp?: number; lastPlaybackTimestamp?: number;
paidMessageStars?: number; paidMessageStars?: number;
@ -447,6 +453,7 @@ const Message: FC<OwnProps & StateProps> = ({
isChatWithUser, isChatWithUser,
isAccountFrozen, isAccountFrozen,
minFutureTime, minFutureTime,
webPage,
onIntersectPinnedMessage, onIntersectPinnedMessage,
}) => { }) => {
const { const {
@ -539,7 +546,7 @@ const Message: FC<OwnProps & StateProps> = ({
const { const {
photo = paidMediaPhoto, video = paidMediaVideo, audio, photo = paidMediaPhoto, video = paidMediaVideo, audio,
voice, document, sticker, contact, voice, document, sticker, contact,
webPage, invoice, location, invoice, location,
action, game, storyData, giveaway, action, game, storyData, giveaway,
giveawayResults, todo, giveawayResults, todo,
} = getMessageContent(message); } = getMessageContent(message);
@ -669,6 +676,7 @@ const Message: FC<OwnProps & StateProps> = ({
lang: oldLang, lang: oldLang,
selectMessage, selectMessage,
message, message,
webPage,
chatId, chatId,
threadId, threadId,
isInDocumentGroup, isInDocumentGroup,
@ -799,6 +807,7 @@ const Message: FC<OwnProps & StateProps> = ({
const contentClassName = buildContentClassName(message, album, { const contentClassName = buildContentClassName(message, album, {
poll, poll,
webPage,
hasSubheader, hasSubheader,
isCustomShape, isCustomShape,
isLastInGroup, isLastInGroup,
@ -1397,8 +1406,12 @@ const Message: FC<OwnProps & StateProps> = ({
} }
function renderWebPage() { function renderWebPage() {
return webPage && ( const messageWebPage = getMessageWebPage(message);
if (!messageWebPage || !webPage) return undefined;
return (
<WebPage <WebPage
messageWebPage={messageWebPage}
webPage={webPage}
message={message} message={message}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
@ -1865,6 +1878,8 @@ export default memo(withGlobal<OwnProps>(
paidMessageStars, paidMessageStars,
} = message; } = message;
const webPage = selectFullWebPageFromMessage(global, message);
const { shouldWarnAboutSvg } = selectSharedSettings(global); const { shouldWarnAboutSvg } = selectSharedSettings(global);
const isChatWithUser = isUserId(chatId); const isChatWithUser = isUserId(chatId);
@ -1875,7 +1890,7 @@ export default memo(withGlobal<OwnProps>(
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const isGroup = chat && isChatGroup(chat); const isGroup = chat && isChatGroup(chat);
const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined;
const webPageStoryData = message.content.webPage?.story; const webPageStoryData = webPage?.story;
const webPageStory = webPageStoryData const webPageStory = webPageStoryData
? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id) ? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id)
: undefined; : undefined;
@ -1941,7 +1956,7 @@ export default memo(withGlobal<OwnProps>(
const canReply = messageListType === 'thread' && selectCanReplyToMessage(global, message, threadId); const canReply = messageListType === 'thread' && selectCanReplyToMessage(global, message, threadId);
const activeDownloads = selectActiveDownloads(global); const activeDownloads = selectActiveDownloads(global);
const downloadableMedia = getMessageDownloadableMedia(message); const downloadableMedia = selectMessageDownloadableMedia(global, message);
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia); const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id); const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id);
@ -2091,6 +2106,7 @@ export default memo(withGlobal<OwnProps>(
isAccountFrozen, isAccountFrozen,
isMediaNsfw, isMediaNsfw,
isReplyMediaNsfw, isReplyMediaNsfw,
webPage,
}; };
}, },
)(Message)); )(Message));

View File

@ -16,6 +16,7 @@ import type {
ApiThreadInfo, ApiThreadInfo,
ApiTypeStory, ApiTypeStory,
ApiUser, ApiUser,
ApiWebPage,
} from '../../../api/types'; } from '../../../api/types';
import type { IAnchorPosition } from '../../../types'; import type { IAnchorPosition } from '../../../types';
@ -57,6 +58,7 @@ type OwnProps = {
targetHref?: string; targetHref?: string;
message: ApiMessage; message: ApiMessage;
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
story?: ApiTypeStory; story?: ApiTypeStory;
canSendNow?: boolean; canSendNow?: boolean;
enabledReactions?: ApiChatReactions; enabledReactions?: ApiChatReactions;
@ -148,6 +150,7 @@ const MessageContextMenu: FC<OwnProps> = ({
isOpen, isOpen,
message, message,
poll, poll,
webPage,
story, story,
isPrivate, isPrivate,
isCurrentUserPremium, isCurrentUserPremium,
@ -301,7 +304,7 @@ const MessageContextMenu: FC<OwnProps> = ({
const copyOptions = getMessageCopyOptions( const copyOptions = getMessageCopyOptions(
message, message,
groupStatefulContent({ poll, story }), groupStatefulContent({ poll, webPage, story }),
targetHref, targetHref,
canCopy, canCopy,
handleAfterCopy, handleAfterCopy,

View File

@ -23,6 +23,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps'; import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
import usePrevious from '../../../hooks/usePrevious';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
@ -105,13 +106,19 @@ const Photo = <T,>({
const { const {
mediaData, loadProgress, mediaData, loadProgress,
} = useMediaWithLoadProgress(!isPaidPreview ? getPhotoMediaHash(photo, size) : undefined, !shouldLoad); } = useMediaWithLoadProgress(!isPaidPreview ? getPhotoMediaHash(photo, size) : undefined, !shouldLoad);
const prevMediaData = usePrevious(mediaData);
const fullMediaData = localBlobUrl || mediaData; const fullMediaData = localBlobUrl || mediaData;
const { ref: fullMediaRef, shouldRender: shouldRenderFullMedia } = useMediaTransition<HTMLImageElement>({
hasMediaData: Boolean(fullMediaData),
withShouldRender: true,
});
const withBlurredBackground = Boolean(forcedWidth); const withBlurredBackground = Boolean(forcedWidth);
const [withThumb] = useState(!fullMediaData); const [withThumb] = useState(!fullMediaData);
const noThumb = Boolean(fullMediaData); const noThumb = Boolean(fullMediaData);
const thumbRef = useBlurredMediaThumbRef(photo, noThumb); const thumbRef = useBlurredMediaThumbRef(photo, noThumb);
useMediaTransition(!noThumb, { ref: thumbRef }); useMediaTransition({ ref: thumbRef, hasMediaData: !noThumb });
const blurredBackgroundRef = useBlurredMediaThumbRef(photo, !withBlurredBackground); const blurredBackgroundRef = useBlurredMediaThumbRef(photo, !withBlurredBackground);
const thumbDataUri = getMediaThumbUri(photo); const thumbDataUri = getMediaThumbUri(photo);
@ -255,9 +262,10 @@ const Photo = <T,>({
{withBlurredBackground && ( {withBlurredBackground && (
<canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" /> <canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />
)} )}
{fullMediaData && ( {shouldRenderFullMedia && (
<img <img
src={fullMediaData} ref={fullMediaRef}
src={fullMediaData || prevMediaData}
className={buildClassName('full-media', withBlurredBackground && 'with-blurred-bg')} className={buildClassName('full-media', withBlurredBackground && 'with-blurred-bg')}
alt="" alt=""
style={forcedWidth ? `width: ${forcedWidth}px` : undefined} style={forcedWidth ? `width: ${forcedWidth}px` : undefined}

View File

@ -12,7 +12,6 @@ import { ApiMediaFormat } from '../../../api/types';
import { import {
getMediaFormat, getMediaFormat,
getMessageMediaThumbDataUri,
getVideoMediaHash, getVideoMediaHash,
hasMessageTtl, hasMessageTtl,
} from '../../../global/helpers'; } from '../../../global/helpers';
@ -22,6 +21,7 @@ import { formatMediaDuration } from '../../../util/dates/dateFormat';
import safePlay from '../../../util/safePlay'; import safePlay from '../../../util/safePlay';
import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
import useThumbnail from '../../../hooks/media/useThumbnail';
import { useThrottledSignal } from '../../../hooks/useAsyncResolvers'; import { useThrottledSignal } from '../../../hooks/useAsyncResolvers';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
@ -109,11 +109,12 @@ const RoundVideo: FC<OwnProps> = ({
const hasTtl = hasMessageTtl(message); const hasTtl = hasMessageTtl(message);
const isInOneTimeModal = origin === 'oneTimeModal'; const isInOneTimeModal = origin === 'oneTimeModal';
const shouldRenderSpoiler = hasTtl && !isInOneTimeModal; const shouldRenderSpoiler = hasTtl && !isInOneTimeModal;
const hasThumb = Boolean(getMessageMediaThumbDataUri(message)); const thumbDataUri = useThumbnail(message);
const hasThumb = Boolean(thumbDataUri);
const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler; const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler;
const thumbRef = useBlurredMediaThumbRef(video, noThumb); const thumbRef = useBlurredMediaThumbRef(video, noThumb);
useMediaTransition(!noThumb, { ref: thumbRef }); useMediaTransition({ hasMediaData: !noThumb, ref: thumbRef });
const thumbDataUri = getMessageMediaThumbDataUri(message);
const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading; const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading;
const wasLoadDisabled = usePreviousDeprecated(isLoadAllowed) === false; const wasLoadDisabled = usePreviousDeprecated(isLoadAllowed) === false;

View File

@ -12,13 +12,13 @@ import { MediaViewerOrigin } from '../../../types';
import { import {
getIsDownloading, getIsDownloading,
getMessageContent, getMessageContent,
getMessageDownloadableMedia,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { import {
selectActiveDownloads, selectCanAutoLoadMedia, selectCanAutoPlayMedia, selectActiveDownloads, selectCanAutoLoadMedia, selectCanAutoPlayMedia,
selectSponsoredMessage, selectSponsoredMessage,
selectTheme, selectTheme,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { selectMessageDownloadableMedia } from '../../../global/selectors/media';
import { IS_ANDROID } from '../../../util/browser/windowEnvironment'; import { IS_ANDROID } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
@ -341,7 +341,7 @@ export default memo(withGlobal<OwnProps>(
const message = selectSponsoredMessage(global, chatId); const message = selectSponsoredMessage(global, chatId);
const activeDownloads = selectActiveDownloads(global); const activeDownloads = selectActiveDownloads(global);
const downloadableMedia = message ? getMessageDownloadableMedia(message) : undefined; const downloadableMedia = message ? selectMessageDownloadableMedia(global, message) : undefined;
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia); const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
return { return {

View File

@ -153,11 +153,13 @@ const Video = <T,>({
const canLoadPreview = isIntersectingForLoading; const canLoadPreview = isIntersectingForLoading;
const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview); const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview);
const shouldHidePreview = isPlayerReady && !isUnsupported; const shouldHidePreview = isPlayerReady && !isUnsupported;
const previewRef = useMediaTransition<HTMLImageElement>((hasThumb || previewBlobUrl) && !shouldHidePreview); const { ref: previewRef } = useMediaTransition<HTMLImageElement>({
hasMediaData: Boolean((hasThumb || previewBlobUrl) && !shouldHidePreview),
});
const noThumb = Boolean(!hasThumb || previewBlobUrl || isPlayerReady); const noThumb = Boolean(!hasThumb || previewBlobUrl || isPlayerReady);
const thumbRef = useBlurredMediaThumbRef(video, noThumb); const thumbRef = useBlurredMediaThumbRef(video, noThumb);
useMediaTransition(!noThumb, { ref: thumbRef }); useMediaTransition({ ref: thumbRef, hasMediaData: !noThumb });
const blurredBackgroundRef = useBlurredMediaThumbRef(video, !withBlurredBackground); const blurredBackgroundRef = useBlurredMediaThumbRef(video, !withBlurredBackground);
const { loadProgress: downloadProgress } = useMediaWithLoadProgress( const { loadProgress: downloadProgress } = useMediaWithLoadProgress(

View File

@ -45,34 +45,6 @@
color: var(--accent-color); color: var(--accent-color);
} }
&.in-preview {
height: 2.625rem;
margin: 0;
padding-inline-start: 0.5rem;
padding-inline-end: 0.375rem;
border-radius: 0.25rem;
background-color: var(--color-primary-tint);
transition: background-color 0.2s ease-in;
&.with-gift {
line-height: 1;
}
&.interactive {
cursor: var(--custom-cursor, pointer);
&:active {
background-color: var(--background-active-color);
}
}
.site-title, .site-name {
font-weight: var(--font-weight-normal);
}
}
.WebPage--content { .WebPage--content {
position: relative; position: relative;
@ -221,16 +193,12 @@
} }
@media (min-width: 1921px) { @media (min-width: 1921px) {
@supports (aspect-ratio: 1) { max-width: none;
&:not(.in-preview) {
max-width: none;
.thumbnail, .thumbnail,
.full-media { .full-media {
width: 100% !important; width: 100% !important;
height: auto !important; height: auto !important;
}
}
} }
} }
} }

View File

@ -3,21 +3,19 @@ import type React from '../../../lib/teact/teact';
import { memo, useMemo, useRef } from '../../../lib/teact/teact'; import { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiMessage, ApiTypeStory } from '../../../api/types'; import type { ApiMessage, ApiMessageWebPage, ApiTypeStory, ApiWebPage, ApiWebPageFull } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { AudioOrigin, type ThemeKey } from '../../../types'; import { AudioOrigin, type ThemeKey, type WebPageMediaSize } from '../../../types';
import { getMessageWebPage } from '../../../global/helpers'; import { getPhotoFullDimensions } from '../../../global/helpers';
import { selectCanPlayAnimatedEmojis } from '../../../global/selectors'; import { selectCanPlayAnimatedEmojis } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { tryParseDeepLink } from '../../../util/deepLinkParser'; import { tryParseDeepLink } from '../../../util/deepLinkParser';
import trimText from '../../../util/trimText'; import trimText from '../../../util/trimText';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { calculateMediaDimensions } from './helpers/mediaDimensions';
import { getWebpageButtonLangKey } from './helpers/webpageType'; import { getWebpageButtonLangKey } from './helpers/webpageType';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener'; import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useAppLayout from '../../../hooks/useAppLayout';
import useEnsureStory from '../../../hooks/useEnsureStory'; import useEnsureStory from '../../../hooks/useEnsureStory';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -44,11 +42,12 @@ const STICKER_SIZE = 80;
const EMOJI_SIZE = 38; const EMOJI_SIZE = 38;
type OwnProps = { type OwnProps = {
message: ApiMessage; messageWebPage: ApiMessageWebPage;
webPage: ApiWebPage;
message?: ApiMessage;
noAvatars?: boolean; noAvatars?: boolean;
canAutoLoad?: boolean; canAutoLoad?: boolean;
canAutoPlay?: boolean; canAutoPlay?: boolean;
inPreview?: boolean;
asForwarded?: boolean; asForwarded?: boolean;
isDownloading?: boolean; isDownloading?: boolean;
isProtected?: boolean; isProtected?: boolean;
@ -59,7 +58,6 @@ type OwnProps = {
shouldWarnAboutSvg?: boolean; shouldWarnAboutSvg?: boolean;
autoLoadFileMaxSizeMb?: number; autoLoadFileMaxSizeMb?: number;
lastPlaybackTimestamp?: number; lastPlaybackTimestamp?: number;
isEditing?: boolean;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
onAudioPlay?: NoneToVoidFunction; onAudioPlay?: NoneToVoidFunction;
@ -73,11 +71,12 @@ type StateProps = {
}; };
const WebPage: FC<OwnProps & StateProps> = ({ const WebPage: FC<OwnProps & StateProps> = ({
messageWebPage,
webPage,
message, message,
noAvatars, noAvatars,
canAutoLoad, canAutoLoad,
canAutoPlay, canAutoPlay,
inPreview,
asForwarded, asForwarded,
isDownloading = false, isDownloading = false,
isProtected, isProtected,
@ -88,7 +87,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
shouldWarnAboutSvg, shouldWarnAboutSvg,
autoLoadFileMaxSizeMb, autoLoadFileMaxSizeMb,
lastPlaybackTimestamp, lastPlaybackTimestamp,
isEditing,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
onMediaClick, onMediaClick,
@ -98,8 +96,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelMediaTransfer, onCancelMediaTransfer,
}) => { }) => {
const { openUrl, openTelegramLink } = getActions(); const { openUrl, openTelegramLink } = getActions();
const webPage = getMessageWebPage(message);
const { isMobile } = useAppLayout();
const stickersRef = useRef<HTMLDivElement>(); const stickersRef = useRef<HTMLDivElement>();
const oldLang = useOldLang(); const oldLang = useOldLang();
@ -113,15 +109,9 @@ const WebPage: FC<OwnProps & StateProps> = ({
onContainerClick?.(e); onContainerClick?.(e);
}); });
const handleOpenTelegramLink = useLastCallback(() => { const fullWebPage = webPage?.webpageType === 'full' ? webPage : undefined;
if (!webPage) return;
openTelegramLink({ const { story: storyData, stickers } = fullWebPage || {};
url: webPage.url,
});
});
const { story: storyData, stickers } = webPage || {};
useEnsureStory(storyData?.peerId, storyData?.id, story); useEnsureStory(storyData?.peerId, storyData?.id, story);
@ -134,9 +124,13 @@ const WebPage: FC<OwnProps & StateProps> = ({
return parsedLink.timestamp; return parsedLink.timestamp;
}, [webPage?.url]); }, [webPage?.url]);
if (!webPage) { if (webPage?.webpageType !== 'full') return undefined;
return undefined;
} const handleOpenTelegramLink = useLastCallback(() => {
openTelegramLink({
url: webPage.url,
});
});
const { const {
siteName, siteName,
@ -149,38 +143,28 @@ const WebPage: FC<OwnProps & StateProps> = ({
audio, audio,
type, type,
document, document,
mediaSize,
} = webPage; } = webPage;
const { mediaSize } = messageWebPage;
const isStory = type === WEBPAGE_STORY_TYPE; const isStory = type === WEBPAGE_STORY_TYPE;
const isGift = type === WEBPAGE_GIFT_TYPE; const isGift = type === WEBPAGE_GIFT_TYPE;
const isExpiredStory = story && 'isDeleted' in story; const isExpiredStory = story && 'isDeleted' in story;
const resultType = stickers?.isEmoji ? 'telegram_emojiset' : type; const resultType = stickers?.isEmoji ? 'telegram_emojiset' : type;
const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonLangKey(resultType) : undefined; const quickButtonLangKey = !isExpiredStory ? getWebpageButtonLangKey(resultType) : undefined;
const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey); const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey);
const truncatedDescription = trimText(description, MAX_TEXT_LENGTH); const truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName); const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = Boolean(stickers); let isSquarePhoto = Boolean(stickers);
if (isArticle && webPage?.photo && !webPage.video && !webPage.document) { if (isArticle && webPage?.photo && !webPage.video && !webPage.document) {
const { width, height } = calculateMediaDimensions({ isSquarePhoto = getIsSmallPhoto(webPage, mediaSize);
media: webPage.photo,
isOwn: message.isOutgoing,
isInWebPage: true,
asForwarded,
noAvatars,
isMobile,
});
isSquarePhoto = (width === height || mediaSize === 'small') && mediaSize !== 'large';
} }
const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto; const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto;
const className = buildClassName( const className = buildClassName(
'WebPage', 'WebPage',
inPreview && 'in-preview',
!isEditing && inPreview && 'interactive',
isSquarePhoto && 'with-square-photo', isSquarePhoto && 'with-square-photo',
!photo && !video && !inPreview && 'without-media', !photo && !video && 'without-media',
video && 'with-video', video && 'with-video',
!isArticle && 'no-article', !isArticle && 'no-article',
document && 'with-document', document && 'with-document',
@ -225,7 +209,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
{isStory && ( {isStory && (
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview /> <BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
)} )}
{isGift && !inPreview && ( {isGift && (
<WebPageUniqueGift <WebPageUniqueGift
gift={webPage.gift!} gift={webPage.gift!}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
@ -235,11 +219,11 @@ const WebPage: FC<OwnProps & StateProps> = ({
)} )}
{isArticle && ( {isArticle && (
<div <div
className={buildClassName('WebPage-text', !inPreview && 'WebPage-text_interactive')} className={buildClassName('WebPage-text', 'WebPage-text_interactive')}
onClick={!inPreview ? () => openUrl({ url, shouldSkipModal: true }) : undefined} onClick={() => openUrl({ url, shouldSkipModal: messageWebPage.isSafe })}
> >
<SafeLink className="site-name" url={url} text={siteName || displayUrl} /> <SafeLink className="site-name" url={url} text={siteName || displayUrl} />
{(!inPreview || isGift) && title && ( {title && (
<p className="site-title">{renderText(title)}</p> <p className="site-title">{renderText(title)}</p>
)} )}
{truncatedDescription && !isGift && ( {truncatedDescription && !isGift && (
@ -250,7 +234,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
{photo && !isGift && !video && !document && ( {photo && !isGift && !video && !document && (
<Photo <Photo
photo={photo} photo={photo}
isOwn={message.isOutgoing} isOwn={message?.isOutgoing}
isInWebPage isInWebPage
observeIntersection={observeIntersectionForLoading} observeIntersection={observeIntersectionForLoading}
noAvatars={noAvatars} noAvatars={noAvatars}
@ -265,10 +249,10 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer} onCancelUpload={onCancelMediaTransfer}
/> />
)} )}
{!inPreview && video && ( {video && (
<Video <Video
video={video} video={video}
isOwn={message.isOutgoing} isOwn={message?.isOutgoing}
isInWebPage isInWebPage
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
noAvatars={noAvatars} noAvatars={noAvatars}
@ -282,10 +266,10 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer} onCancelUpload={onCancelMediaTransfer}
/> />
)} )}
{!inPreview && audio && ( {audio && (
<Audio <Audio
theme={theme} theme={theme}
message={message} message={message!}
origin={AudioOrigin.Inline} origin={AudioOrigin.Inline}
noAvatars={noAvatars} noAvatars={noAvatars}
isDownloading={isDownloading} isDownloading={isDownloading}
@ -293,7 +277,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer} onCancelUpload={onCancelMediaTransfer}
/> />
)} )}
{!inPreview && document && ( {document && (
<Document <Document
document={document} document={document}
message={message} message={message}
@ -305,7 +289,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
shouldWarnAboutSvg={shouldWarnAboutSvg} shouldWarnAboutSvg={shouldWarnAboutSvg}
/> />
)} )}
{!inPreview && stickers && ( {stickers && (
<div <div
ref={stickersRef} ref={stickersRef}
className={buildClassName( className={buildClassName(
@ -327,18 +311,23 @@ const WebPage: FC<OwnProps & StateProps> = ({
))} ))}
</div> </div>
)} )}
{inPreview && displayUrl && !isArticle && (
<div className="WebPage-text">
<p className="site-name">{displayUrl}</p>
<p className="site-description">{oldLang('Chat.Empty.LinkPreview')}</p>
</div>
)}
</div> </div>
{quickButtonTitle && renderQuickButton(quickButtonTitle)} {quickButtonTitle && renderQuickButton(quickButtonTitle)}
</PeerColorWrapper> </PeerColorWrapper>
); );
}; };
function getIsSmallPhoto(webPage: ApiWebPageFull, mediaSize?: WebPageMediaSize) {
if (!webPage?.photo) return false;
if (mediaSize === 'small') return true;
if (mediaSize === 'large') return false;
const { width, height } = getPhotoFullDimensions(webPage.photo) || {};
if (!width || !height) return false;
return width === height && !webPage.hasLargeMedia;
}
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
return { return {

View File

@ -1,4 +1,4 @@
import type { ApiMessage, ApiPoll } from '../../../../api/types'; import type { ApiMessage, ApiPoll, ApiWebPage } from '../../../../api/types';
import type { IAlbum } from '../../../../types'; import type { IAlbum } from '../../../../types';
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config'; import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
@ -10,6 +10,7 @@ export function buildContentClassName(
album?: IAlbum, album?: IAlbum,
{ {
poll, poll,
webPage,
hasSubheader, hasSubheader,
isCustomShape, isCustomShape,
isLastInGroup, isLastInGroup,
@ -26,6 +27,7 @@ export function buildContentClassName(
hasOutsideReactions, hasOutsideReactions,
}: { }: {
poll?: ApiPoll; poll?: ApiPoll;
webPage?: ApiWebPage;
hasSubheader?: boolean; hasSubheader?: boolean;
isCustomShape?: boolean | number; isCustomShape?: boolean | number;
isLastInGroup?: boolean; isLastInGroup?: boolean;
@ -48,7 +50,7 @@ export function buildContentClassName(
const content = getMessageContent(message); const content = getMessageContent(message);
const { const {
photo = paidMediaPhoto, video = paidMediaVideo, photo = paidMediaPhoto, video = paidMediaVideo,
audio, voice, document, webPage, contact, location, invoice, storyData, audio, voice, document, contact, location, invoice, storyData,
giveaway, giveawayResults, giveaway, giveawayResults,
} = content; } = content;
const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text; const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text;
@ -128,7 +130,7 @@ export function buildContentClassName(
classNames.push('poll'); classNames.push('poll');
} else if (giveaway || giveawayResults) { } else if (giveaway || giveawayResults) {
classNames.push('giveaway'); classNames.push('giveaway');
} else if (webPage) { } else if (webPage?.webpageType === 'full') {
classNames.push('web-page'); classNames.push('web-page');
if (webPage.photo || webPage.video) { if (webPage.photo || webPage.video) {

View File

@ -7,9 +7,9 @@ import {
getMessageHtmlId, getMessageHtmlId,
getMessagePhoto, getMessagePhoto,
getMessageText, getMessageText,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getPhotoMediaHash, getPhotoMediaHash,
getWebPagePhoto,
getWebPageVideo,
hasMediaLocalBlobUrl, hasMediaLocalBlobUrl,
} from '../../../../global/helpers'; } from '../../../../global/helpers';
import { getMessageTextWithSpoilers } from '../../../../global/helpers/messageSummary'; import { getMessageTextWithSpoilers } from '../../../../global/helpers/messageSummary';
@ -40,10 +40,11 @@ export function getMessageCopyOptions(
onCopyMessages?: (messageIds: number[]) => void, onCopyMessages?: (messageIds: number[]) => void,
onCopyNumber?: () => void, onCopyNumber?: () => void,
): ICopyOptions { ): ICopyOptions {
const { webPage } = statefulContent || {};
const options: ICopyOptions = []; const options: ICopyOptions = [];
const text = getMessageText(message); const text = getMessageText(message);
const photo = getMessagePhoto(message) const photo = getMessagePhoto(message)
|| (!getMessageWebPageVideo(message) ? getMessageWebPagePhoto(message) : undefined); || (!getWebPageVideo(webPage) ? getWebPagePhoto(webPage) : undefined);
const contact = getMessageContact(message); const contact = getMessageContact(message);
const mediaHash = photo ? getPhotoMediaHash(photo, 'full') : undefined; const mediaHash = photo ? getPhotoMediaHash(photo, 'full') : undefined;
const canImageBeCopied = canCopy && photo && (mediaHash || hasMediaLocalBlobUrl(photo)) const canImageBeCopied = canCopy && photo && (mediaHash || hasMediaLocalBlobUrl(photo))

View File

@ -2,13 +2,14 @@ import { getActions } from '../../../../global';
import type { import type {
ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
ApiWebPage,
} from '../../../../api/types'; } from '../../../../api/types';
import type { OldLangFn } from '../../../../hooks/useOldLang'; import type { OldLangFn } from '../../../../hooks/useOldLang';
import type { IAlbum, ThreadId } from '../../../../types'; import type { IAlbum, ThreadId } from '../../../../types';
import { MAIN_THREAD_ID } from '../../../../api/types'; import { MAIN_THREAD_ID } from '../../../../api/types';
import { MediaViewerOrigin } from '../../../../types'; import { MediaViewerOrigin } from '../../../../types';
import { getMessagePhoto, getMessageWebPagePhoto } from '../../../../global/helpers'; import { getMessagePhoto, getWebPagePhoto, getWebPageVideo } from '../../../../global/helpers';
import { getMessageReplyInfo } from '../../../../global/helpers/replies'; import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import { tryParseDeepLink } from '../../../../util/deepLinkParser'; import { tryParseDeepLink } from '../../../../util/deepLinkParser';
@ -18,6 +19,7 @@ export default function useInnerHandlers({
lang, lang,
selectMessage, selectMessage,
message, message,
webPage,
chatId, chatId,
threadId, threadId,
isInDocumentGroup, isInDocumentGroup,
@ -37,6 +39,7 @@ export default function useInnerHandlers({
lang: OldLangFn; lang: OldLangFn;
selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void; selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void;
message: ApiMessage; message: ApiMessage;
webPage?: ApiWebPage;
chatId: string; chatId: string;
threadId: ThreadId; threadId: ThreadId;
isInDocumentGroup: boolean; isInDocumentGroup: boolean;
@ -61,7 +64,7 @@ export default function useInnerHandlers({
} = getActions(); } = getActions();
const { const {
id: messageId, forwardInfo, groupedId, content: { paidMedia, video, webPage }, id: messageId, forwardInfo, groupedId, content: { paidMedia, video },
} = message; } = message;
const { const {
@ -135,7 +138,7 @@ export default function useInnerHandlers({
const parsedLink = webPage?.url && tryParseDeepLink(webPage.url); const parsedLink = webPage?.url && tryParseDeepLink(webPage.url);
const videoContent = video || webPage?.video; const videoContent = video || getWebPageVideo(webPage);
const webpageTimestamp = parsedLink && 'timestamp' in parsedLink ? parsedLink.timestamp : undefined; const webpageTimestamp = parsedLink && 'timestamp' in parsedLink ? parsedLink.timestamp : undefined;
openMediaViewer({ openMediaViewer({
@ -158,7 +161,7 @@ export default function useInnerHandlers({
}); });
const handleMediaClick = useLastCallback((): void => { const handleMediaClick = useLastCallback((): void => {
const photo = getMessagePhoto(message) || getMessageWebPagePhoto(message); const photo = getMessagePhoto(message) || getWebPagePhoto(webPage);
if (photo) { if (photo) {
handlePhotoMediaClick(); handlePhotoMediaClick();
} }

View File

@ -10,12 +10,13 @@ import type { IconName } from '../../../types/icons';
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config'; import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config';
import { import {
getMediaDuration, getMessageContent, getMessageMediaHash, isMessageLocal, getMessageContent, isMessageLocal,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { getPeerTitle } from '../../../global/helpers/peers'; import { getPeerTitle } from '../../../global/helpers/peers';
import { import {
selectChat, selectChatMessage, selectSender, selectTabState, selectChat, selectChatMessage, selectSender, selectTabState,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { selectMessageMediaDuration } from '../../../global/selectors/media';
import { makeTrackId } from '../../../util/audioPlayer'; import { makeTrackId } from '../../../util/audioPlayer';
import { IS_IOS, IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import { IS_IOS, IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -23,6 +24,7 @@ import * as mediaLoader from '../../../util/mediaLoader';
import { clearMediaSession } from '../../../util/mediaSession'; import { clearMediaSession } from '../../../util/mediaSession';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useAudioPlayer from '../../../hooks/useAudioPlayer'; import useAudioPlayer from '../../../hooks/useAudioPlayer';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
@ -54,6 +56,7 @@ type StateProps = {
message?: ApiMessage; message?: ApiMessage;
sender?: ApiPeer; sender?: ApiPeer;
chat?: ApiChat; chat?: ApiChat;
mediaDuration?: number;
volume: number; volume: number;
playbackRate: number; playbackRate: number;
isPlaybackRateActive?: boolean; isPlaybackRateActive?: boolean;
@ -75,6 +78,7 @@ const DEFAULT_FAST_PLAYBACK_RATE = 2;
const AudioPlayer: FC<OwnProps & StateProps> = ({ const AudioPlayer: FC<OwnProps & StateProps> = ({
message, message,
mediaDuration,
className, className,
noUi, noUi,
sender, sender,
@ -105,7 +109,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION; const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION;
const senderName = sender ? getPeerTitle(lang, sender) : undefined; const senderName = sender ? getPeerTitle(lang, sender) : undefined;
const mediaHash = renderingMessage && getMessageMediaHash(renderingMessage, 'inline'); const mediaHash = useMessageMediaHash(renderingMessage, 'inline');
const mediaData = mediaHash && mediaLoader.getFromMemory(mediaHash); const mediaData = mediaHash && mediaLoader.getFromMemory(mediaHash);
const mediaMetadata = useMessageMediaMetadata(renderingMessage, sender, chat); const mediaMetadata = useMessageMediaMetadata(renderingMessage, sender, chat);
@ -123,7 +127,7 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
setCurrentTime, setCurrentTime,
} = useAudioPlayer( } = useAudioPlayer(
message && makeTrackId(message), message && makeTrackId(message),
message ? getMediaDuration(message)! : 0, mediaDuration || 0,
isVoice ? 'voice' : 'audio', isVoice ? 'voice' : 'audio',
mediaData, mediaData,
undefined, undefined,
@ -421,6 +425,8 @@ export default withGlobal<OwnProps>(
volume, playbackRate, isMuted, isPlaybackRateActive, timestamp, volume, playbackRate, isMuted, isPlaybackRateActive, timestamp,
} = selectTabState(global).audioPlayer; } = selectTabState(global).audioPlayer;
const mediaDuration = message ? selectMessageMediaDuration(global, message) : undefined;
return { return {
message, message,
sender, sender,
@ -430,6 +436,7 @@ export default withGlobal<OwnProps>(
isPlaybackRateActive, isPlaybackRateActive,
isMuted, isMuted,
timestamp, timestamp,
mediaDuration,
}; };
}, },
)(AudioPlayer); )(AudioPlayer);

View File

@ -10,7 +10,6 @@ import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
getIsSavedDialog, getIsSavedDialog,
getMessageIsSpoiler, getMessageIsSpoiler,
getMessageMediaHash,
getMessageSingleInlineButton, getMessageSingleInlineButton,
getMessageVideo, getMessageVideo,
} from '../../../global/helpers'; } from '../../../global/helpers';
@ -30,6 +29,8 @@ import { getPictogramDimensions, REM } from '../../common/helpers/mediaDimension
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText'; import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../hooks/media/useThumbnail';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDerivedState from '../../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureMessage from '../../../hooks/useEnsureMessage';
@ -39,7 +40,6 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useThumbnail from '../../../hooks/useThumbnail';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane'; import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
@ -119,7 +119,7 @@ const HeaderPinnedMessage = ({
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length); const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
const mediaThumbnail = useThumbnail(pinnedMessage); const mediaThumbnail = useThumbnail(pinnedMessage);
const mediaHash = pinnedMessage && getMessageMediaHash(pinnedMessage, isVideoThumbnail ? 'full' : 'pictogram'); const mediaHash = useMessageMediaHash(pinnedMessage, isVideoThumbnail ? 'full' : 'pictogram');
const mediaBlobUrl = useMedia(mediaHash); const mediaBlobUrl = useMedia(mediaHash);
const isSpoiler = pinnedMessage && getMessageIsSpoiler(pinnedMessage); const isSpoiler = pinnedMessage && getMessageIsSpoiler(pinnedMessage);

View File

@ -94,7 +94,9 @@ const Checkout: FC<OwnProps> = ({
const photoUrl = useMedia(getWebDocumentHash(photo)); const photoUrl = useMedia(getWebDocumentHash(photo));
const ref = useMediaTransition<HTMLImageElement>(photoUrl); const { ref } = useMediaTransition<HTMLImageElement>({
hasMediaData: Boolean(photoUrl),
});
const handleTipsClick = useCallback((tips: number) => { const handleTipsClick = useCallback((tips: number) => {
dispatch!({ type: 'setTipAmount', payload: maxTipAmount ? Math.min(tips, maxTipAmount) : tips }); dispatch!({ type: 'setTipAmount', payload: maxTipAmount ? Math.min(tips, maxTipAmount) : tips });

View File

@ -3,7 +3,7 @@ import {
memo, useCallback, memo, useCallback,
useEffect, useMemo, useRef, useState, useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiBotPreviewMedia, ApiBotPreviewMedia,
@ -34,7 +34,6 @@ import {
getIsDownloading, getIsDownloading,
getIsSavedDialog, getIsSavedDialog,
getMessageDocument, getMessageDocument,
getMessageDownloadableMedia,
isChatAdmin, isChatAdmin,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
@ -62,6 +61,7 @@ import {
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectPremiumLimit } from '../../global/selectors/limits'; import { selectPremiumLimit } from '../../global/selectors/limits';
import { selectMessageDownloadableMedia } from '../../global/selectors/media';
import { selectSharedSettings } from '../../global/selectors/sharedState'; import { selectSharedSettings } from '../../global/selectors/sharedState';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -714,21 +714,23 @@ const Profile: FC<OwnProps & StateProps> = ({
)) ))
) : resultType === 'voice' ? ( ) : resultType === 'voice' ? (
(viewportIds as number[]).map((id) => { (viewportIds as number[]).map((id) => {
const global = getGlobal();
const message = messagesById[id]; const message = messagesById[id];
if (!message) return undefined; if (!message) return undefined;
const media = messagesById[id] && getMessageDownloadableMedia(message)!;
const media = selectMessageDownloadableMedia(global, message)!;
return messagesById[id] && ( return messagesById[id] && (
<Audio <Audio
key={id} key={id}
theme={theme} theme={theme}
message={messagesById[id]} message={message}
senderTitle={getSenderName(oldLang, messagesById[id], chatsById, usersById)} senderTitle={getSenderName(oldLang, message, chatsById, usersById)}
origin={AudioOrigin.SharedMedia} origin={AudioOrigin.SharedMedia}
date={messagesById[id].date} date={message.date}
className="scroll-item" className="scroll-item"
onPlay={handlePlayAudio} onPlay={handlePlayAudio}
onDateClick={handleMessageFocus} onDateClick={handleMessageFocus}
canDownload={!isChatProtected && !messagesById[id].isProtected} canDownload={!isChatProtected && !message.isProtected}
isDownloading={getIsDownloading(activeDownloads, media)} isDownloading={getIsDownloading(activeDownloads, media)}
/> />
); );

View File

@ -6,8 +6,6 @@ import type { ApiMessage, StatisticsMessageInteractionCounter } from '../../../a
import type { OldLangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import { import {
getMessageMediaHash,
getMessageMediaThumbDataUri,
getMessageRoundVideo, getMessageRoundVideo,
getMessageVideo, getMessageVideo,
} from '../../../global/helpers'; } from '../../../global/helpers';
@ -15,6 +13,8 @@ import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat'; import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../hooks/media/useThumbnail';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
@ -32,8 +32,9 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ postStatistic, message }) => {
const lang = useOldLang(); const lang = useOldLang();
const { toggleMessageStatistics } = getActions(); const { toggleMessageStatistics } = getActions();
const mediaThumbnail = getMessageMediaThumbDataUri(message); const thumbDataUri = useThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro')); const mediaHash = useMessageMediaHash(message, 'micro');
const mediaBlobUrl = useMedia(mediaHash);
const isRoundVideo = Boolean(getMessageRoundVideo(message)); const isRoundVideo = Boolean(getMessageRoundVideo(message));
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
@ -44,13 +45,13 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ postStatistic, message }) => {
<div <div
className={buildClassName( className={buildClassName(
styles.root, styles.root,
Boolean(mediaBlobUrl || mediaThumbnail) && styles.withImage, Boolean(mediaBlobUrl || thumbDataUri) && styles.withImage,
)} )}
onClick={handleClick} onClick={handleClick}
> >
<div className={styles.title}> <div className={styles.title}>
<div className={styles.summary}> <div className={styles.summary}>
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)} {renderSummary(lang, message, mediaBlobUrl || thumbDataUri, isRoundVideo)}
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>
{lang('ChannelStats.ViewsCount', postStatistic.viewsCount, 'i')} {lang('ChannelStats.ViewsCount', postStatistic.viewsCount, 'i')}

View File

@ -1272,19 +1272,23 @@ addActionHandler('loadWebPagePreview', async (global, actions, payload): Promise
global = getGlobal(); global = getGlobal();
global = updateTabState(global, { global = updateTabState(global, {
webPagePreview, webPagePreviewId: webPagePreview?.id,
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
if (!webPagePreview) return;
actions.apiUpdate({
'@type': 'updateWebPage',
webPage: webPagePreview,
});
}); });
addActionHandler('clearWebPagePreview', (global, actions, payload): ActionReturnType => { addActionHandler('clearWebPagePreview', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
if (!selectTabState(global, tabId).webPagePreview) {
return undefined;
}
return updateTabState(global, { return updateTabState(global, {
webPagePreview: undefined, webPagePreviewId: undefined,
}, tabId); }, tabId);
}); });

View File

@ -43,6 +43,7 @@ import {
deleteTopic, deleteTopic,
removeChatFromChatLists, removeChatFromChatLists,
replaceThreadParam, replaceThreadParam,
replaceWebPage,
updateChat, updateChat,
updateChatLastMessageId, updateChatLastMessageId,
updateChatMediaLoadingState, updateChatMediaLoadingState,
@ -101,7 +102,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'newMessage': { case 'newMessage': {
const { const {
chatId, id, message, shouldForceReply, wasDrafted, poll, chatId, id, message, shouldForceReply, wasDrafted, poll, webPage,
} = update; } = update;
global = updateWithLocalMedia(global, chatId, id, message); global = updateWithLocalMedia(global, chatId, id, message);
global = updateListedAndViewportIds(global, actions, message as ApiMessage); global = updateListedAndViewportIds(global, actions, message as ApiMessage);
@ -169,6 +170,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
if (webPage) {
global = replaceWebPage(global, webPage.id, webPage);
}
if (message.reportDeliveryUntilDate && message.reportDeliveryUntilDate > getServerTime()) { if (message.reportDeliveryUntilDate && message.reportDeliveryUntilDate > getServerTime()) {
actions.reportMessageDelivery({ chatId, messageId: id }); actions.reportMessageDelivery({ chatId, messageId: id });
} }
@ -228,7 +233,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'newScheduledMessage': { case 'newScheduledMessage': {
const { const {
chatId, id, message, poll, chatId, id, message, poll, webPage,
} = update; } = update;
global = updateWithLocalMedia(global, chatId, id, message, true); global = updateWithLocalMedia(global, chatId, id, message, true);
@ -246,6 +251,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
if (webPage) {
global = replaceWebPage(global, webPage.id, webPage);
}
global = updatePeerFullInfo(global, chatId, { global = updatePeerFullInfo(global, chatId, {
hasScheduledMessages: true, hasScheduledMessages: true,
}); });
@ -257,7 +266,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateScheduledMessage': { case 'updateScheduledMessage': {
const { const {
chatId, id, message, poll, isFromNew, chatId, id, message, poll, webPage, isFromNew,
} = update; } = update;
const currentMessage = selectScheduledMessage(global, chatId, id); const currentMessage = selectScheduledMessage(global, chatId, id);
@ -269,6 +278,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
chatId: update.chatId, chatId: update.chatId,
message: update.message as ApiMessage, message: update.message as ApiMessage,
poll: update.poll, poll: update.poll,
webPage: update.webPage,
}); });
} }
return; return;
@ -287,6 +297,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
if (webPage) {
global = replaceWebPage(global, webPage.id, webPage);
}
setGlobal(global); setGlobal(global);
break; break;
@ -294,7 +308,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateMessage': { case 'updateMessage': {
const { const {
chatId, id, message, poll, isFromNew, shouldForceReply, chatId, id, message, poll, webPage, isFromNew, shouldForceReply,
} = update; } = update;
const currentMessage = selectChatMessage(global, chatId, id); const currentMessage = selectChatMessage(global, chatId, id);
@ -307,6 +321,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
chatId: update.chatId, chatId: update.chatId,
message: update.message, message: update.message,
poll: update.poll, poll: update.poll,
webPage: update.webPage,
shouldForceReply, shouldForceReply,
}); });
} }
@ -333,13 +348,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
if (webPage) {
global = replaceWebPage(global, webPage.id, webPage);
}
setGlobal(global); setGlobal(global);
break; break;
} }
case 'updateQuickReplyMessage': { case 'updateQuickReplyMessage': {
const { id, message, poll } = update; const { id, message, poll, webPage } = update;
global = updateQuickReplyMessage(global, id, message); global = updateQuickReplyMessage(global, id, message);
@ -347,6 +366,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
} }
if (webPage) {
global = replaceWebPage(global, webPage.id, webPage);
}
setGlobal(global); setGlobal(global);
break; break;

View File

@ -12,8 +12,10 @@ import {
addUsers, addUsers,
removeBlockedUser, removeBlockedUser,
removePeerStory, removePeerStory,
replaceWebPage,
setConfirmPaymentUrl, setConfirmPaymentUrl,
setPaymentStep, setPaymentStep,
updateFullWebPage,
updateLastReadStoryForPeer, updateLastReadStoryForPeer,
updatePeerStory, updatePeerStory,
updatePeersWithStories, updatePeersWithStories,
@ -33,7 +35,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'updateEntities': { case 'updateEntities': {
const { const {
users, chats, threadInfos, polls, users, chats, threadInfos, polls, webPages,
} = update; } = update;
if (users) global = addUsers(global, users); if (users) global = addUsers(global, users);
if (chats) global = addChats(global, chats); if (chats) global = addChats(global, chats);
@ -43,6 +45,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updatePoll(global, poll.id, poll); global = updatePoll(global, poll.id, poll);
}); });
} }
if (webPages) {
webPages.forEach((webPage) => {
if (webPage.webpageType === 'full') {
global = updateFullWebPage(global, webPage.id, webPage);
} else {
global = replaceWebPage(global, webPage.id, webPage);
}
});
}
setGlobal(global); setGlobal(global);
break; break;
} }
@ -153,6 +164,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}); });
break; break;
case 'updateWebPage': {
const { webPage } = update;
if (webPage.webpageType === 'full') {
global = updateFullWebPage(global, webPage.id, webPage);
} else {
global = replaceWebPage(global, webPage.id, webPage);
}
setGlobal(global);
break;
}
case 'updateStory': case 'updateStory':
global = addStoriesForPeer(global, update.peerId, { [update.story.id]: update.story }); global = addStoriesForPeer(global, update.peerId, { [update.story.id]: update.story });
global = updatePeersWithStories(global, { [update.peerId]: selectPeerStories(global, update.peerId)! }); global = updatePeersWithStories(global, { [update.peerId]: selectPeerStories(global, update.peerId)! });

View File

@ -4,11 +4,11 @@ import { AudioOrigin, MediaViewerOrigin } from '../../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { omit } from '../../../util/iteratees'; import { omit } from '../../../util/iteratees';
import { getTimestampableMedia } from '../../helpers';
import { getMessageReplyInfo } from '../../helpers/replies'; import { getMessageReplyInfo } from '../../helpers/replies';
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { selectChatMessage, selectReplyMessage, selectTabState } from '../../selectors'; import { selectChatMessage, selectReplyMessage, selectTabState } from '../../selectors';
import { selectTimestampableMedia } from '../../selectors/media';
addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => { addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => {
const { const {
@ -65,7 +65,7 @@ addActionHandler('openMediaFromTimestamp', (global, actions, payload): ActionRet
const replyInfo = getMessageReplyInfo(message); const replyInfo = getMessageReplyInfo(message);
const replyMessage = selectReplyMessage(global, message); const replyMessage = selectReplyMessage(global, message);
const messageMedia = getTimestampableMedia(message); const messageMedia = selectTimestampableMedia(global, message);
const maxMessageDuration = messageMedia?.duration; const maxMessageDuration = messageMedia?.duration;
if (maxMessageDuration) { if (maxMessageDuration) {
if (maxMessageDuration <= timestamp) return; if (maxMessageDuration <= timestamp) return;
@ -93,7 +93,7 @@ addActionHandler('openMediaFromTimestamp', (global, actions, payload): ActionRet
return; return;
} }
const replyMessageMedia = replyMessage ? getTimestampableMedia(replyMessage) : undefined; const replyMessageMedia = replyMessage ? selectTimestampableMedia(global, replyMessage) : undefined;
const maxReplyMessageDuration = replyMessageMedia?.duration; const maxReplyMessageDuration = replyMessageMedia?.duration;
if (!maxReplyMessageDuration || maxReplyMessageDuration <= timestamp) return; if (!maxReplyMessageDuration || maxReplyMessageDuration <= timestamp) return;

View File

@ -27,7 +27,6 @@ import {
getMediaFilename, getMediaFilename,
getMediaFormat, getMediaFormat,
getMediaHash, getMediaHash,
getMessageDownloadableMedia,
getMessageStatefulContent, getMessageStatefulContent,
isChatChannel, isChatChannel,
} from '../../helpers'; } from '../../helpers';
@ -72,6 +71,7 @@ import {
selectThreadInfo, selectThreadInfo,
selectViewportIds, selectViewportIds,
} from '../../selectors'; } from '../../selectors';
import { selectMessageDownloadableMedia } from '../../selectors/media';
import { getPeerStarsForMessage } from '../api/messages'; import { getPeerStarsForMessage } from '../api/messages';
import { getIsMobile } from '../../../hooks/useAppLayout'; import { getIsMobile } from '../../../hooks/useAppLayout';
@ -679,7 +679,7 @@ addActionHandler('downloadSelectedMessages', (global, actions, payload): ActionR
const messages = messageIds.map((id) => chatMessages[id]) const messages = messageIds.map((id) => chatMessages[id])
.filter((message) => selectAllowedMessageActionsSlow(global, message, threadId).canDownload); .filter((message) => selectAllowedMessageActionsSlow(global, message, threadId).canDownload);
messages.forEach((message) => { messages.forEach((message) => {
const media = getMessageDownloadableMedia(message); const media = selectMessageDownloadableMedia(global, message);
if (!media) return; if (!media) return;
actions.downloadMedia({ media, originMessage: message, tabId }); actions.downloadMedia({ media, originMessage: message, tabId });
}); });

View File

@ -40,6 +40,7 @@ import {
selectChatLastMessageId, selectChatLastMessageId,
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
selectFullWebPageFromMessage,
selectTopics, selectTopics,
selectViewportIds, selectViewportIds,
selectVisibleUsers, selectVisibleUsers,
@ -344,6 +345,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.settings.themes) { if (!cached.settings.themes) {
cached.settings.themes = initialState.settings.themes; cached.settings.themes = initialState.settings.themes;
} }
if (!cached.messages.webPageById) {
cached.messages.webPageById = initialState.messages.webPageById;
}
} }
function updateCache(force?: boolean) { function updateCache(force?: boolean) {
@ -483,7 +488,10 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
const chatStoriesUserIds = currentChatIds const chatStoriesUserIds = currentChatIds
.flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {})) .flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {}))
.map((message) => message.content.storyData?.peerId || message.content.webPage?.story?.peerId) .map((message) => {
const webPage = selectFullWebPageFromMessage(global, message);
return message.content.storyData?.peerId || webPage?.story?.peerId;
})
.filter((id): id is string => Boolean(id) && isUserId(id)); .filter((id): id is string => Boolean(id) && isUserId(id));
const attachBotIds = Object.keys(global.attachMenu?.bots || {}); const attachBotIds = Object.keys(global.attachMenu?.bots || {});
@ -530,8 +538,9 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
const message = messages[id]; const message = messages[id];
if (!message) return undefined; if (!message) return undefined;
const content = message.content; const content = message.content;
const webPage = selectFullWebPageFromMessage(global, message);
const replyPeer = message.replyInfo?.type === 'message' && message.replyInfo.replyToPeerId; const replyPeer = message.replyInfo?.type === 'message' && message.replyInfo.replyToPeerId;
return content.storyData?.peerId || content.webPage?.story?.peerId || replyPeer; return content.storyData?.peerId || webPage?.story?.peerId || replyPeer;
}); });
})); }));
@ -610,6 +619,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
}, {} as Record<string, Set<ThreadId>>); }, {} as Record<string, Set<ThreadId>>);
const pollIdsToSave: string[] = []; const pollIdsToSave: string[] = [];
const webPageIdsToSave: string[] = [];
chatIdsToSave.forEach((chatId) => { chatIdsToSave.forEach((chatId) => {
const current = global.messages.byChatId[chatId]; const current = global.messages.byChatId[chatId];
@ -658,6 +668,10 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
pollIdsToSave.push(message.content.pollId); pollIdsToSave.push(message.content.pollId);
} }
if (message.content.webPage) {
webPageIdsToSave.push(message.content.webPage.id);
}
return acc; return acc;
}, {} as Record<number, ApiMessage>); }, {} as Record<number, ApiMessage>);
@ -670,6 +684,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
return { return {
byChatId, byChatId,
pollById: pickTruthy(global.messages.pollById, pollIdsToSave), pollById: pickTruthy(global.messages.pollById, pollIdsToSave),
webPageById: pickTruthy(global.messages.webPageById, webPageIdsToSave),
sponsoredByChatId: {}, sponsoredByChatId: {},
playbackByChatId: {}, playbackByChatId: {},
}; };

View File

@ -13,7 +13,10 @@ import type {
ApiVideo, ApiVideo,
ApiVoice, ApiVoice,
ApiWebDocument, ApiWebDocument,
ApiWebPage,
MediaContainer, MediaContainer,
SizeTarget,
StatefulMediaContent,
} from '../../api/types'; } from '../../api/types';
import type { ActiveDownloads } from '../../types'; import type { ActiveDownloads } from '../../types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
@ -31,14 +34,6 @@ import { getAttachmentMediaType, matchLinkInMessageText } from './messages';
export type MediaWithThumbs = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiMediaExtendedPreview; export type MediaWithThumbs = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiMediaExtendedPreview;
export type DownloadableMedia = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiAudio | ApiVoice | ApiWebDocument; export type DownloadableMedia = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiAudio | ApiVoice | ApiWebDocument;
type Target =
'micro'
| 'pictogram'
| 'inline'
| 'preview'
| 'full'
| 'download';
export function getMessageContent(message: MediaContainer) { export function getMessageContent(message: MediaContainer) {
return message.content; return message.content;
} }
@ -104,10 +99,6 @@ export function getMessageDocument(message: MediaContainer) {
return message.content.document; return message.content.document;
} }
export function getMessageWebPageDocument(message: MediaContainer) {
return getMessageWebPage(message)?.document;
}
export function isDocumentPhoto(document: ApiDocument) { export function isDocumentPhoto(document: ApiDocument) {
return document.innerMediaType === 'photo'; return document.innerMediaType === 'photo';
} }
@ -149,21 +140,25 @@ export function getMessagePaidMedia(message: MediaContainer) {
return message.content.paidMedia; return message.content.paidMedia;
} }
export function getMessageWebPagePhoto(message: MediaContainer) {
return getMessageWebPage(message)?.photo;
}
export function getMessageDocumentPhoto(message: MediaContainer) { export function getMessageDocumentPhoto(message: MediaContainer) {
const document = getMessageDocument(message); const document = getMessageDocument(message);
return document && isDocumentPhoto(document) ? document : undefined; return document && isDocumentPhoto(document) ? document : undefined;
} }
export function getMessageWebPageVideo(message: MediaContainer) { export function getWebPagePhoto(webPage?: ApiWebPage) {
return getMessageWebPage(message)?.video; return webPage?.webpageType === 'full' ? webPage.photo : undefined;
} }
export function getMessageWebPageAudio(message: MediaContainer) { export function getWebPageVideo(webPage?: ApiWebPage) {
return getMessageWebPage(message)?.audio; return webPage?.webpageType === 'full' ? webPage.video : undefined;
}
export function getWebPageAudio(webPage?: ApiWebPage) {
return webPage?.webpageType === 'full' ? webPage.audio : undefined;
}
export function getWebPageDocument(webPage?: ApiWebPage) {
return webPage?.webpageType === 'full' ? webPage.document : undefined;
} }
export function getMessageDocumentVideo(message: MediaContainer) { export function getMessageDocumentVideo(message: MediaContainer) {
@ -171,40 +166,6 @@ export function getMessageDocumentVideo(message: MediaContainer) {
return document && isDocumentVideo(document) ? document : undefined; return document && isDocumentVideo(document) ? document : undefined;
} }
export function getMessageDownloadableMedia(message: MediaContainer): DownloadableMedia | undefined {
return (
getMessagePhoto(message)
|| getMessageVideo(message)
|| getMessageDocument(message)
|| getMessageSticker(message)
|| getMessageAudio(message)
|| getMessageVoice(message)
|| getMessageWebPagePhoto(message)
|| getMessageWebPageVideo(message)
|| getMessageWebPageAudio(message)
);
}
function getMessageMediaThumbnail(message: MediaContainer) {
const media = getMessagePhoto(message)
|| getMessageVideo(message)
|| getMessageDocument(message)
|| getMessageSticker(message)
|| getMessageWebPagePhoto(message)
|| getMessageWebPageVideo(message)
|| getMessageInvoice(message)?.extendedMedia;
if (!media) {
return undefined;
}
return media.thumbnail;
}
export function getMessageMediaThumbDataUri(message: MediaContainer) {
return getMessageMediaThumbnail(message)?.dataUri;
}
export function getMediaThumbUri(media: MediaWithThumbs) { export function getMediaThumbUri(media: MediaWithThumbs) {
return media.thumbnail?.dataUri; return media.thumbnail?.dataUri;
} }
@ -232,48 +193,7 @@ export function buildStaticMapHash(
return `staticMap:${accessHash}?lat=${lat}&long=${long}&w=${width}&h=${height}&zoom=${zoom}&scale=${scale}&accuracyRadius=${accuracyRadius}`; return `staticMap:${accessHash}?lat=${lat}&long=${long}&w=${width}&h=${height}&zoom=${zoom}&scale=${scale}&accuracyRadius=${accuracyRadius}`;
} }
export function getMessageMediaHash( export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: SizeTarget, isAction?: boolean) {
message: MediaContainer,
target: Target,
) {
const {
video, sticker, audio, voice, document,
} = message.content;
const messagePhoto = getMessagePhoto(message) || getMessageWebPagePhoto(message);
const actionPhoto = getMessageActionPhoto(message);
const messageVideo = video || getMessageWebPageVideo(message);
const messageDocument = document || getMessageWebPageDocument(message);
const messageAudio = audio || getMessageWebPageAudio(message);
if (messageVideo) {
return getVideoMediaHash(messageVideo, target);
}
if (messagePhoto || actionPhoto) {
return getPhotoMediaHash(messagePhoto || actionPhoto!, target, Boolean(actionPhoto));
}
if (messageDocument) {
return getDocumentMediaHash(messageDocument, target);
}
if (sticker) {
return getStickerMediaHash(sticker, target);
}
if (messageAudio) {
return getAudioMediaHash(messageAudio, target);
}
if (voice) {
return getVoiceMediaHash(voice, target);
}
return undefined;
}
export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: Target, isAction?: boolean) {
const base = `photo${photo.id}`; const base = `photo${photo.id}`;
const isVideo = photo.mediaType === 'photo' && photo.isVideo; const isVideo = photo.mediaType === 'photo' && photo.isVideo;
@ -302,7 +222,7 @@ export function getVideoProfilePhotoMediaHash(photo: ApiPhoto) {
return `photo${photo.id}?size=u`; return `photo${photo.id}?size=u`;
} }
export function getVideoMediaHash(video: ApiVideo | ApiDocument, target: Target) { export function getVideoMediaHash(video: ApiVideo | ApiDocument, target: SizeTarget) {
const base = `document${video.id}`; const base = `document${video.id}`;
switch (target) { switch (target) {
@ -325,7 +245,7 @@ export function getVideoPreviewMediaHash(video: ApiVideo) {
return video.hasVideoPreview ? `document${video.id}?size=v` : undefined; return video.hasVideoPreview ? `document${video.id}?size=v` : undefined;
} }
export function getDocumentMediaHash(document: ApiDocument, target: Target) { export function getDocumentMediaHash(document: ApiDocument, target: SizeTarget) {
const base = `document${document.id}`; const base = `document${document.id}`;
switch (target) { switch (target) {
@ -345,7 +265,7 @@ export function getDocumentMediaHash(document: ApiDocument, target: Target) {
} }
} }
export function getAudioMediaHash(audio: ApiAudio, target: Target) { export function getAudioMediaHash(audio: ApiAudio, target: SizeTarget) {
const base = `document${audio.id}`; const base = `document${audio.id}`;
switch (target) { switch (target) {
@ -361,7 +281,7 @@ export function getAudioMediaHash(audio: ApiAudio, target: Target) {
} }
} }
export function getVoiceMediaHash(voice: ApiVoice, target: Target) { export function getVoiceMediaHash(voice: ApiVoice, target: SizeTarget) {
const base = `document${voice.id}`; const base = `document${voice.id}`;
switch (target) { switch (target) {
@ -381,7 +301,7 @@ export function getWebDocumentHash(webDocument?: ApiWebDocument) {
return `webDocument:${webDocument.url}`; return `webDocument:${webDocument.url}`;
} }
export function getStickerMediaHash(sticker: ApiSticker, target: Target) { export function getStickerMediaHash(sticker: ApiSticker, target: SizeTarget) {
const base = `document${sticker.id}`; const base = `document${sticker.id}`;
switch (target) { switch (target) {
@ -401,7 +321,7 @@ export function getStickerMediaHash(sticker: ApiSticker, target: Target) {
} }
} }
export function getMediaHash(media: DownloadableMedia, target: Target) { export function getMediaHash(media: DownloadableMedia, target: SizeTarget) {
switch (media.mediaType) { switch (media.mediaType) {
case 'photo': case 'photo':
return getPhotoMediaHash(media, target); return getPhotoMediaHash(media, target);
@ -458,7 +378,7 @@ export function getAudioHasCover(media: ApiAudio) {
} }
export function getMediaFormat( export function getMediaFormat(
media: DownloadableMedia, target: Target, media: DownloadableMedia, target: SizeTarget,
): ApiMediaFormat { ): ApiMediaFormat {
const isDocument = media.mediaType === 'document'; const isDocument = media.mediaType === 'document';
const hasInnerVideo = isDocument && media.innerMediaType === 'video'; const hasInnerVideo = isDocument && media.innerMediaType === 'video';
@ -609,31 +529,6 @@ export function getMessageContentIds(
}, [] as Array<number>); }, [] as Array<number>);
} }
export function getMediaDuration(message: ApiMessage) {
const { audio, voice, video } = getMessageContent(message);
const media = audio || voice || video || getMessageWebPageVideo(message) || getMessageWebPageAudio(message);
if (!media) {
return undefined;
}
return media.duration;
}
export function canReplaceMessageMedia(message: ApiMessage, attachment: ApiAttachment) {
const isPhotoOrVideo = Boolean(getMessagePhoto(message)
|| getMessageWebPagePhoto(message) || Boolean(getMessageVideo(message)
|| getMessageWebPageVideo(message)));
const isFile = Boolean(getMessageAudio(message)
|| getMessageVoice(message) || getMessageDocument(message));
const fileType = getAttachmentMediaType(attachment);
return (
(isPhotoOrVideo && (fileType === 'photo' || fileType === 'video'))
|| (isFile && (fileType === 'audio' || fileType === 'file'))
);
}
export function isMediaLoadableInViewer(newMessage: ApiMessage) { export function isMediaLoadableInViewer(newMessage: ApiMessage) {
if (!newMessage.content) return false; if (!newMessage.content) return false;
if (newMessage.content.photo) return true; if (newMessage.content.photo) return true;
@ -672,9 +567,60 @@ export function getIsDownloading(activeDownloads: ActiveDownloads, media: Downlo
return Boolean(activeDownloads[hash]); return Boolean(activeDownloads[hash]);
} }
export function getTimestampableMedia(message: MediaContainer) { export function getMessageMediaHash(
const video = getMessageVideo(message) || getMessageWebPageVideo(message); message: MediaContainer,
return (video && !video.isRound && !video.isGif ? video : undefined) statefulMedia: StatefulMediaContent,
|| getMessageAudio(message) target: SizeTarget,
|| getMessageVoice(message); ) {
const {
video, sticker, audio, voice, document,
} = message.content;
const { webPage } = statefulMedia;
const messagePhoto = getMessagePhoto(message) || getWebPagePhoto(webPage);
const actionPhoto = getMessageActionPhoto(message);
const messageVideo = video || getWebPageVideo(webPage);
const messageDocument = document || getWebPageDocument(webPage);
const messageAudio = audio || getWebPageAudio(webPage);
if (messageVideo) {
return getVideoMediaHash(messageVideo, target);
}
if (messagePhoto || actionPhoto) {
return getPhotoMediaHash(messagePhoto || actionPhoto!, target, Boolean(actionPhoto));
}
if (messageDocument) {
return getDocumentMediaHash(messageDocument, target);
}
if (sticker) {
return getStickerMediaHash(sticker, target);
}
if (messageAudio) {
return getAudioMediaHash(messageAudio, target);
}
if (voice) {
return getVoiceMediaHash(voice, target);
}
return undefined;
}
export function canReplaceMessageMedia(
message: MediaContainer, attachment: ApiAttachment,
) {
const isPhotoOrVideo = Boolean(getMessagePhoto(message) || getMessageVideo(message));
const isFile = Boolean(getMessageAudio(message)
|| getMessageVoice(message) || getMessageDocument(message));
const fileType = getAttachmentMediaType(attachment);
return (
(isPhotoOrVideo && (fileType === 'photo' || fileType === 'video'))
|| (isFile && (fileType === 'audio' || fileType === 'file'))
);
} }

View File

@ -9,7 +9,7 @@ import type {
ApiTypeStory, ApiTypeStory,
} from '../../api/types'; } from '../../api/types';
import type { import type {
ApiPoll, MediaContainer, StatefulMediaContent, ApiPoll, ApiWebPage, MediaContainer, StatefulMediaContent,
} from '../../api/types/messages'; } from '../../api/types/messages';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization'; import type { LangFn } from '../../util/localization';
@ -34,6 +34,7 @@ import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { isLocalMessageId } from '../../util/keys/messageKey'; import { isLocalMessageId } from '../../util/keys/messageKey';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
import { getGlobal } from '../index'; import { getGlobal } from '../index';
import { selectPollFromMessage, selectWebPageFromMessage } from '../selectors';
import { getMainUsername } from './users'; import { getMainUsername } from './users';
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
@ -68,24 +69,28 @@ export function hasMessageText(message: MediaContainer) {
} }
export function getMessageStatefulContent(global: GlobalState, message: ApiMessage): StatefulMediaContent { export function getMessageStatefulContent(global: GlobalState, message: ApiMessage): StatefulMediaContent {
const poll = message.content.pollId ? global.messages.pollById[message.content.pollId] : undefined; const poll = selectPollFromMessage(global, message);
const webPage = selectWebPageFromMessage(global, message);
const { peerId: storyPeerId, id: storyId } = message.content.storyData || {}; const { peerId: storyPeerId, id: storyId } = message.content.storyData || {};
const story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined; const story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined;
return groupStatefulContent({ poll, story }); return groupStatefulContent({ poll, story, webPage });
} }
export function groupStatefulContent({ export function groupStatefulContent({
poll, poll,
story, story,
webPage,
}: { }: {
poll?: ApiPoll; poll?: ApiPoll;
story?: ApiTypeStory; story?: ApiTypeStory;
webPage?: ApiWebPage;
}) { }) {
return { return {
poll, poll,
story: story && 'content' in story ? story : undefined, story: story && 'content' in story ? story : undefined,
webPage,
}; };
} }
@ -104,8 +109,8 @@ export function getMessageCustomShape(message: ApiMessage): boolean {
return true; return true;
} }
if (!text || photo || video || audio || voice || document || pollId || webPage || contact || action || game || invoice if (!text || photo || video || audio || voice || document || pollId || webPage || contact || action || game
|| location || storyData) { || invoice || location || storyData) {
return false; return false;
} }

View File

@ -157,6 +157,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
byChatId: {}, byChatId: {},
sponsoredByChatId: {}, sponsoredByChatId: {},
pollById: {}, pollById: {},
webPageById: {},
playbackByChatId: {}, playbackByChatId: {},
}, },

View File

@ -1,5 +1,7 @@
import type { import type {
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo, ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
ApiWebPage,
ApiWebPageFull,
} from '../../api/types'; } from '../../api/types';
import type { import type {
FocusDirection, FocusDirection,
@ -47,6 +49,7 @@ import {
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadInfo, selectThreadInfo,
selectViewportIds, selectViewportIds,
selectWebPage,
} from '../selectors'; } from '../selectors';
import { removeIdFromSearchResults } from './middleSearch'; import { removeIdFromSearchResults } from './middleSearch';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
@ -977,6 +980,40 @@ export function deleteQuickReply<T extends GlobalState>(
}; };
} }
export function updateFullWebPage<T extends GlobalState>(
global: T,
webPageId: string,
update: Partial<ApiWebPageFull>,
) {
const webpage = selectWebPage(global, webPageId);
const updatedWebpage = webpage?.webpageType === 'full'
? { ...webpage, ...update }
: { webpageType: 'full', mediaType: 'webpage', ...update };
if (!updatedWebpage.id) {
return global;
}
return replaceWebPage(global, webPageId, updatedWebpage as ApiWebPageFull);
}
export function replaceWebPage<T extends GlobalState>(
global: T,
webPageId: string,
webPage: ApiWebPage,
) {
return {
...global,
messages: {
...global.messages,
webPageById: {
...global.messages.webPageById,
[webPageId]: webPage,
},
},
};
}
export function updatePoll<T extends GlobalState>( export function updatePoll<T extends GlobalState>(
global: T, global: T,
pollId: string, pollId: string,

View File

@ -0,0 +1,126 @@
import type {
ApiMessage,
MediaContainer,
SizeTarget,
} from '../../api/types';
import type {
GlobalState,
} from '../types';
import { NSFW_RESTRICTION_REASON } from '../../config';
import {
getMessageAudio,
getMessageContent,
getMessageDocument,
getMessageInvoice,
getMessageMediaHash,
getMessagePhoto,
getMessageSticker,
getMessageVideo,
getMessageVoice,
getWebPageAudio,
getWebPagePhoto,
getWebPageVideo,
} from '../helpers';
import { selectChat } from './chats';
import {
selectActiveRestrictionReasons,
selectReplyMessage,
selectWebPageFromMessage,
} from './messages';
import { selectSettingsKeys } from './settings';
export function selectIsMediaNsfw<T extends GlobalState>(global: T, message: ApiMessage) {
const { isSensitiveEnabled } = selectSettingsKeys(global);
const chat = selectChat(global, message.chatId);
if (isSensitiveEnabled) return false;
const chatActiveRestrictions = selectActiveRestrictionReasons(global, chat?.restrictionReasons);
const messageActiveRestrictions = selectActiveRestrictionReasons(global, message.restrictionReasons);
return chatActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON)
|| messageActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON);
}
export function selectMessageDownloadableMedia<T extends GlobalState>(global: T, message: MediaContainer) {
const webPage = selectWebPageFromMessage(global, message);
return (
getMessagePhoto(message)
|| getMessageVideo(message)
|| getMessageDocument(message)
|| getMessageSticker(message)
|| getMessageAudio(message)
|| getMessageVoice(message)
|| getWebPagePhoto(webPage)
|| getWebPageVideo(webPage)
|| getWebPageAudio(webPage)
);
}
export function selectMessageMediaThumbnail<T extends GlobalState>(global: T, message: MediaContainer) {
const webPage = selectWebPageFromMessage(global, message);
const media = getMessagePhoto(message)
|| getMessageVideo(message)
|| getMessageDocument(message)
|| getMessageSticker(message)
|| getWebPagePhoto(webPage)
|| getWebPageVideo(webPage)
|| getMessageInvoice(message)?.extendedMedia;
if (!media) {
return undefined;
}
return media.thumbnail;
}
export function selectMessageMediaThumbDataUri<T extends GlobalState>(global: T, message: MediaContainer) {
const thumbnail = selectMessageMediaThumbnail(global, message);
return thumbnail?.dataUri;
}
export function selectMessageMediaHash<T extends GlobalState>(
global: T,
message: MediaContainer,
target: SizeTarget,
) {
const webPage = selectWebPageFromMessage(global, message);
return getMessageMediaHash(message, { webPage }, target);
}
export function selectMessageMediaDuration<T extends GlobalState>(global: T, message: MediaContainer) {
const { audio, voice, video } = getMessageContent(message);
const webPage = selectWebPageFromMessage(global, message);
const media = audio || voice || video || getWebPageVideo(webPage) || getWebPageAudio(webPage);
if (!media) {
return undefined;
}
return media.duration;
}
export function selectTimestampableMedia<T extends GlobalState>(global: T, message: MediaContainer) {
const webPage = selectWebPageFromMessage(global, message);
const video = getMessageVideo(message) || getWebPageVideo(webPage);
return (video && !video.isRound && !video.isGif ? video : undefined)
|| getMessageAudio(message)
|| getMessageVoice(message);
}
export function selectMessageTimestampableDuration<T extends GlobalState>(
global: T, message: ApiMessage, noReplies?: boolean,
) {
const replyMessage = !noReplies ? selectReplyMessage(global, message) : undefined;
const timestampableMedia = selectTimestampableMedia(global, message);
const replyTimestampableMedia = replyMessage && selectTimestampableMedia(global, replyMessage);
return timestampableMedia?.duration || replyTimestampableMedia?.duration;
}
export function selectMessageLastPlaybackTimestamp<T extends GlobalState>(
global: T, chatId: string, messageId: number,
) {
return global.messages.playbackByChatId[chatId]?.byId[messageId];
}

View File

@ -7,6 +7,7 @@ import type {
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiRestrictionReason, ApiSponsoredMessage, ApiPeer, ApiRestrictionReason, ApiSponsoredMessage,
ApiStickerSetInfo, ApiStickerSetInfo,
MediaContainer,
} from '../../api/types'; } from '../../api/types';
import type { import type {
ChatTranslatedMessages, ChatTranslatedMessages,
@ -22,7 +23,7 @@ import type {
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import { import {
ANONYMOUS_USER_ID, API_GENERAL_ID_LIMIT, GENERAL_TOPIC_ID, NSFW_RESTRICTION_REASON, SERVICE_NOTIFICATIONS_USER_ID, ANONYMOUS_USER_ID, API_GENERAL_ID_LIMIT, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID,
SVG_EXTENSIONS, WEB_APP_PLATFORM, SVG_EXTENSIONS, WEB_APP_PLATFORM,
} from '../../config'; } from '../../config';
import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment'; import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment';
@ -46,10 +47,9 @@ import {
getMessagePhoto, getMessagePhoto,
getMessageVideo, getMessageVideo,
getMessageVoice, getMessageVoice,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getSendingState, getSendingState,
getTimestampableMedia, getWebPagePhoto,
getWebPageVideo,
hasMessageTtl, hasMessageTtl,
isActionMessage, isActionMessage,
isChatBasicGroup, isChatBasicGroup,
@ -78,7 +78,6 @@ import {
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
} from './chats'; } from './chats';
import { selectPeer, selectPeerPaidMessagesStars } from './peers'; import { selectPeer, selectPeerPaidMessagesStars } from './peers';
import { selectSettingsKeys } from './settings';
import { selectPeerStory } from './stories'; import { selectPeerStory } from './stories';
import { selectIsStickerFavorite } from './symbols'; import { selectIsStickerFavorite } from './symbols';
import { selectTabState } from './tabs'; import { selectTabState } from './tabs';
@ -485,11 +484,31 @@ export function selectPoll<T extends GlobalState>(global: T, pollId: string) {
return global.messages.pollById[pollId]; return global.messages.pollById[pollId];
} }
export function selectPollFromMessage<T extends GlobalState>(global: T, message: ApiMessage) { export function selectPollFromMessage<T extends GlobalState>(global: T, message: MediaContainer) {
if (!message.content.pollId) return undefined; if (!message.content.pollId) return undefined;
return selectPoll(global, message.content.pollId); return selectPoll(global, message.content.pollId);
} }
export function selectWebPage<T extends GlobalState>(global: T, webPageId: string) {
return global.messages.webPageById[webPageId];
}
export function selectWebPageFromMessage<T extends GlobalState>(global: T, message: MediaContainer) {
if (!message.content.webPage) return undefined;
return selectWebPage(global, message.content.webPage.id);
}
export function selectFullWebPage<T extends GlobalState>(global: T, webPageId: string) {
const webPage = selectWebPage(global, webPageId);
if (!webPage || webPage.webpageType !== 'full') return undefined;
return webPage;
}
export function selectFullWebPageFromMessage<T extends GlobalState>(global: T, message: MediaContainer) {
if (!message.content.webPage) return undefined;
return selectFullWebPage(global, message.content.webPage.id);
}
export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) { export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
const { chatId } = message; const { chatId } = message;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -614,10 +633,13 @@ export function selectCanForwardMessage<T extends GlobalState>(global: T, messag
const isAction = isActionMessage(message); const isAction = isActionMessage(message);
const hasTtl = hasMessageTtl(message); const hasTtl = hasMessageTtl(message);
const { content } = message; const { content } = message;
const webPage = selectFullWebPageFromMessage(global, message);
const story = content.storyData const story = content.storyData
? selectPeerStory(global, content.storyData.peerId, content.storyData.id) ? selectPeerStory(global, content.storyData.peerId, content.storyData.id)
: (content.webPage?.story : (webPage?.story
? selectPeerStory(global, content.webPage.story.peerId, content.webPage.story.id) ? selectPeerStory(global, webPage.story.peerId, webPage.story.id)
: undefined : undefined
); );
const isChatProtected = selectIsChatProtected(global, message.chatId); const isChatProtected = selectIsChatProtected(global, message.chatId);
@ -657,6 +679,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const isDocumentSticker = isMessageDocumentSticker(message); const isDocumentSticker = isMessageDocumentSticker(message);
const isBoostMessage = message.content.action?.type === 'boostApply'; const isBoostMessage = message.content.action?.type === 'boostApply';
const isMonoforum = chat.isMonoforum; const isMonoforum = chat.isMonoforum;
const webPage = selectFullWebPageFromMessage(global, message);
const hasChatPinPermission = (chat.isCreator const hasChatPinPermission = (chat.isCreator
|| (!isChannel && !isUserRightBanned(chat, 'pinMessages')) || (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
@ -736,7 +759,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const canCopyLink = !isLocal && !isAction && (isChannel || isSuperGroup) && !isMonoforum; const canCopyLink = !isLocal && !isAction && (isChannel || isSuperGroup) && !isMonoforum;
const canSelect = !isLocal && !isAction; const canSelect = !isLocal && !isAction;
const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo const canDownload = Boolean(webPage?.document || webPage?.video || webPage?.photo
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker) || content.audio || content.voice || content.photo || content.video || content.document || content.sticker)
&& !hasTtl; && !hasTtl;
@ -1143,8 +1166,9 @@ export function selectCanAutoLoadMedia<T extends GlobalState>(
const sender = 'id' in message ? selectSender(global, message) : undefined; const sender = 'id' in message ? selectSender(global, message) : undefined;
const isPhoto = Boolean(getMessagePhoto(message) || getMessageWebPagePhoto(message)); const webPage = selectWebPageFromMessage(global, message);
const isVideo = Boolean(getMessageVideo(message) || getMessageWebPageVideo(message)); const isPhoto = Boolean(getMessagePhoto(message) || getWebPagePhoto(webPage));
const isVideo = Boolean(getMessageVideo(message) || getWebPageVideo(webPage));
const isFile = Boolean(getMessageAudio(message) || getMessageVoice(message) || getMessageDocument(message)); const isFile = Boolean(getMessageAudio(message) || getMessageVoice(message) || getMessageDocument(message));
const { const {
@ -1551,23 +1575,6 @@ export function selectReplyMessage<T extends GlobalState>(global: T, message: Ap
return replyMessage; return replyMessage;
} }
export function selectMessageTimestampableDuration<T extends GlobalState>(
global: T, message: ApiMessage, noReplies?: boolean,
) {
const replyMessage = !noReplies ? selectReplyMessage(global, message) : undefined;
const timestampableMedia = getTimestampableMedia(message);
const replyTimestampableMedia = replyMessage && getTimestampableMedia(replyMessage);
return timestampableMedia?.duration || replyTimestampableMedia?.duration;
}
export function selectMessageLastPlaybackTimestamp<T extends GlobalState>(
global: T, chatId: string, messageId: number,
) {
return global.messages.playbackByChatId[chatId]?.byId[messageId];
}
export function selectActiveRestrictionReasons<T extends GlobalState>( export function selectActiveRestrictionReasons<T extends GlobalState>(
global: T, restrictionReasons?: ApiRestrictionReason[], global: T, restrictionReasons?: ApiRestrictionReason[],
): ApiRestrictionReason[] { ): ApiRestrictionReason[] {
@ -1583,15 +1590,3 @@ export function selectActiveRestrictionReasons<T extends GlobalState>(
return !shouldIgnore; return !shouldIgnore;
}); });
} }
export function selectIsMediaNsfw<T extends GlobalState>(global: T, message: ApiMessage) {
const { isSensitiveEnabled } = selectSettingsKeys(global);
const chat = selectChat(global, message.chatId);
if (isSensitiveEnabled) return false;
const chatActiveRestrictions = selectActiveRestrictionReasons(global, chat?.restrictionReasons);
const messageActiveRestrictions = selectActiveRestrictionReasons(global, message.restrictionReasons);
return chatActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON)
|| messageActiveRestrictions.some((reason) => reason.reason === NSFW_RESTRICTION_REASON);
}

View File

@ -5,8 +5,9 @@ import { NewChatMembersProgress, RightColumnContent } from '../../types';
import { IS_SNAP_EFFECT_SUPPORTED } from '../../util/browser/windowEnvironment'; import { IS_SNAP_EFFECT_SUPPORTED } from '../../util/browser/windowEnvironment';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia'; import { getMessageVideo, getWebPageVideo } from '../helpers/messageMedia';
import { selectCurrentManagement } from './management'; import { selectCurrentManagement } from './management';
import { selectWebPageFromMessage } from './messages';
import { selectSharedSettings } from './sharedState'; import { selectSharedSettings } from './sharedState';
import { selectIsStatisticsShown } from './statistics'; import { selectIsStatisticsShown } from './statistics';
import { selectTabState } from './tabs'; import { selectTabState } from './tabs';
@ -117,7 +118,8 @@ export function selectPerformanceSettingsValue<T extends GlobalState>(
} }
export function selectCanAutoPlayMedia<T extends GlobalState>(global: T, message: ApiMessage | ApiSponsoredMessage) { export function selectCanAutoPlayMedia<T extends GlobalState>(global: T, message: ApiMessage | ApiSponsoredMessage) {
const video = getMessageVideo(message) || getMessageWebPageVideo(message); const webPage = selectWebPageFromMessage(global, message);
const video = getMessageVideo(message) || getWebPageVideo(webPage);
if (!video) { if (!video) {
return undefined; return undefined;
} }

View File

@ -47,6 +47,7 @@ import type {
ApiUserStatus, ApiUserStatus,
ApiVideo, ApiVideo,
ApiWallpaper, ApiWallpaper,
ApiWebPage,
ApiWebSession, ApiWebSession,
} from '../../api/types'; } from '../../api/types';
import type { import type {
@ -238,6 +239,7 @@ export type GlobalState = {
}>; }>;
sponsoredByChatId: Record<string, ApiSponsoredMessage>; sponsoredByChatId: Record<string, ApiSponsoredMessage>;
pollById: Record<string, ApiPoll>; pollById: Record<string, ApiPoll>;
webPageById: Record<string, ApiWebPage>;
}; };
stories: { stories: {

View File

@ -51,7 +51,6 @@ import type {
ApiTypeStoryView, ApiTypeStoryView,
ApiUser, ApiUser,
ApiVideo, ApiVideo,
ApiWebPage,
} from '../../api/types'; } from '../../api/types';
import type { ApiEmojiStatusCollectible } from '../../api/types/users'; import type { ApiEmojiStatusCollectible } from '../../api/types/users';
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
@ -372,7 +371,7 @@ export type TabState = {
isMuted: boolean; isMuted: boolean;
}; };
webPagePreview?: ApiWebPage; webPagePreviewId?: string;
loadingThread?: { loadingThread?: {
loadingChatId: string; loadingChatId: string;

View File

@ -0,0 +1,19 @@
import type { MediaContainer, SizeTarget } from '../../api/types';
import type { GlobalState } from '../../global/types';
import { getMessageMediaHash } from '../../global/helpers/messageMedia';
import useSelector from '../data/useSelector';
function selectWebPagesById(global: GlobalState) {
return global.messages.webPageById;
}
export default function useMessageMediaHash(message: MediaContainer | undefined, target: SizeTarget) {
const webPagesById = useSelector(selectWebPagesById);
if (!message) return undefined;
const webPageId = message.content.webPage?.id;
const webPage = webPageId ? webPagesById[webPageId] : undefined;
return getMessageMediaHash(message, { webPage }, target);
}

View File

@ -0,0 +1,25 @@
import { useMemo } from '../../lib/teact/teact';
import { getGlobal } from '../../global';
import type { ApiThumbnail, MediaContainer } from '../../api/types';
import { selectTheme } from '../../global/selectors';
import { selectMessageMediaThumbDataUri } from '../../global/selectors/media';
export default function useThumbnail(media?: MediaContainer | ApiThumbnail) {
const isMediaContainer = media && 'content' in media;
const global = getGlobal();
const thumbDataUri = isMediaContainer ? selectMessageMediaThumbDataUri(global, media) : media?.dataUri;
// TODO Find a way to update thumbnail on theme change
const theme = selectTheme(global);
const dataUri = useMemo(() => {
const uri = thumbDataUri;
if (!uri || theme !== 'dark') return uri;
return uri.replace('<svg', '<svg fill="white"');
}, [thumbDataUri, theme]);
return dataUri;
}

View File

@ -1,17 +1,33 @@
import useShowTransition, { type HookParams } from './useShowTransition'; import useShowTransition, {
type HookParams,
type HookParamsWithShouldRender,
type HookResult,
type HookResultWithShouldRender,
} from './useShowTransition';
type HookParamsWithMediaData<RefType extends HTMLElement> = Omit<HookParams<RefType>, 'isOpen'> & {
hasMediaData: boolean;
};
type HookParamsWithMediaDataAndShouldRender<RefType extends HTMLElement>
= Omit<HookParamsWithShouldRender<RefType>, 'isOpen'> & {
hasMediaData: boolean;
};
export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>( export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>(
mediaData?: unknown, params: HookParamsWithMediaData<RefType>
options?: Partial<HookParams<RefType>>, ): HookResult<RefType>;
) { export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>(
const isMediaReady = Boolean(mediaData); params: HookParamsWithMediaDataAndShouldRender<RefType>
): HookResultWithShouldRender<RefType>;
const { ref } = useShowTransition<RefType>({ export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>(
isOpen: isMediaReady, params: HookParamsWithMediaData<RefType> | HookParamsWithMediaDataAndShouldRender<RefType>,
noMountTransition: isMediaReady, ): HookResult<RefType> | HookResultWithShouldRender<RefType> {
const result = useShowTransition<RefType>({
isOpen: params.hasMediaData,
noMountTransition: params.hasMediaData,
className: 'slow', className: 'slow',
...options, ...params,
}); } as HookParams<RefType>);
return ref; return result;
} }

View File

@ -31,16 +31,16 @@ export type HookParams<RefType extends HTMLElement> = BaseHookParams<RefType> &
withShouldRender?: never; withShouldRender?: never;
}; };
type HookParamsWithShouldRender<RefType extends HTMLElement> = BaseHookParams<RefType> & { export type HookParamsWithShouldRender<RefType extends HTMLElement> = BaseHookParams<RefType> & {
withShouldRender: true; withShouldRender: true;
}; };
type HookResult<RefType extends HTMLElement> = { export type HookResult<RefType extends HTMLElement> = {
ref: ElementRef<RefType>; ref: ElementRef<RefType>;
getIsClosing: Signal<boolean>; getIsClosing: Signal<boolean>;
}; };
type HookResultWithShouldRender<RefType extends HTMLElement> = HookResult<RefType> & { export type HookResultWithShouldRender<RefType extends HTMLElement> = HookResult<RefType> & {
shouldRender: boolean; shouldRender: boolean;
}; };
@ -50,6 +50,9 @@ type State =
| 'open' | 'open'
| 'closing'; | 'closing';
/**
* Use for showing and hiding small elements with transitions. For large elements, use {@link useMediaTransition}.
*/
export default function useShowTransition<RefType extends HTMLElement = HTMLDivElement>( export default function useShowTransition<RefType extends HTMLElement = HTMLDivElement>(
params: HookParams<RefType> params: HookParams<RefType>
): HookResult<RefType>; ): HookResult<RefType>;

View File

@ -1,24 +0,0 @@
import { useMemo } from '../lib/teact/teact';
import { getGlobal } from '../global';
import type { ApiThumbnail, MediaContainer } from '../api/types';
import { getMessageMediaThumbDataUri } from '../global/helpers';
import { selectTheme } from '../global/selectors';
export default function useThumbnail(media?: MediaContainer | ApiThumbnail) {
const isMediaContainer = media && 'content' in media;
const thumbDataUri = isMediaContainer ? getMessageMediaThumbDataUri(media) : media?.dataUri;
// TODO Find a way to update thumbnail on theme change
const theme = selectTheme(getGlobal());
const dataUri = useMemo(() => {
const uri = thumbDataUri;
if (!uri || theme !== 'dark') return uri;
return uri.replace('<svg', '<svg fill="white"');
}, [thumbDataUri, theme]);
return dataUri;
}

View File

@ -947,6 +947,7 @@ export interface LangPair {
'AttachSticker': undefined; 'AttachSticker': undefined;
'AttachMusic': undefined; 'AttachMusic': undefined;
'AttachContact': undefined; 'AttachContact': undefined;
'AttachStory': undefined;
'MessageLocation': undefined; 'MessageLocation': undefined;
'MessageLiveLocation': undefined; 'MessageLiveLocation': undefined;
'ServiceNotifications': undefined; 'ServiceNotifications': undefined;
@ -1622,6 +1623,13 @@ export interface LangPair {
'LabelPayInTON': undefined; 'LabelPayInTON': undefined;
'PriceChanged': undefined; 'PriceChanged': undefined;
'PayNewPrice': undefined; 'PayNewPrice': undefined;
'LinkPreview': undefined;
'ContextMoveTextUp': undefined;
'ContextMoveTextDown': undefined;
'ContextLinkLargerMedia': undefined;
'ContextLinkSmallerMedia': undefined;
'ContextLinkRemovePreview': undefined;
'AccLinkRemovePreview': undefined;
'GlobalSearch': undefined; 'GlobalSearch': undefined;
'DescriptionPublicPostsSearch': undefined; 'DescriptionPublicPostsSearch': undefined;
'PublicPosts': undefined; 'PublicPosts': undefined;

View File

@ -220,8 +220,8 @@ export default function createConfig(
APP_VERSION: JSON.stringify(appVersion), APP_VERSION: JSON.stringify(appVersion),
APP_REVISION: DefinePlugin.runtimeValue(() => { APP_REVISION: DefinePlugin.runtimeValue(() => {
const { branch, commit } = getGitMetadata(); const { branch, commit } = getGitMetadata();
const shouldDisplayCommit = APP_ENV === 'staging' || !branch || branch === 'HEAD'; const shouldDisplayOnlyCommit = APP_ENV === 'staging' || !branch || branch === 'HEAD';
return JSON.stringify(shouldDisplayCommit ? commit : branch); return JSON.stringify(shouldDisplayOnlyCommit ? commit : `${branch}#${commit}`);
}, mode === 'development' ? true : []), }, mode === 'development' ? true : []),
}), }),
new ProvidePlugin({ new ProvidePlugin({