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 { pick } from '../../../util/iteratees';
import { generateRandomInt } from '../gramjsBuilders';
import { addDocumentToLocalDb } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc';
import { buildApiMessageEntity, buildApiPhoto } from './common';
@ -255,16 +254,6 @@ export function buildBotInlineMessage(
currency: sendMessage.currency,
amount: sendMessage.totalAmount.toJSNumber(),
};
} else {
const mediaSize = sendMessage.forceSmallMedia ? 'small' : sendMessage.forceLargeMedia ? 'large' : undefined;
content.webPage = {
mediaType: 'webpage',
id: generateRandomInt(),
mediaSize,
url: sendMessage.url,
displayUrl: sendMessage.url,
};
}
return {

View File

@ -13,6 +13,7 @@ import type {
ApiMediaInvoice,
ApiMediaTodo,
ApiMessageStoryData,
ApiMessageWebPage,
ApiPaidMedia,
ApiPhoto,
ApiPoll,
@ -35,7 +36,7 @@ import { addTimestampEntities } from '../../../util/dates/timestamp';
import { generateWaveform } from '../../../util/generateWaveform';
import { pick } from '../../../util/iteratees';
import {
addMediaToLocalDb, addStoryToLocalDb, type MediaRepairContext,
addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
} from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc';
import {
@ -158,7 +159,7 @@ export function buildMessageMediaContent(
const todo = buildTodoFromMedia(media);
if (todo) return { todo };
const webPage = buildWebPage(media);
const webPage = buildMessageWebPageFromMedia(media);
if (webPage) return { webPage };
const invoice = buildInvoiceFromMedia(media);
@ -798,85 +799,126 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu
};
}
export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undefined {
if (
!(media instanceof GramJs.MessageMediaWebPage)
|| !(media.webpage instanceof GramJs.WebPage)
) {
export function buildMessageWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiMessageWebPage | undefined {
if (!(media instanceof GramJs.MessageMediaWebPage) || media.webpage instanceof GramJs.WebPageNotModified) {
return undefined;
}
const {
id, photo, document, attributes,
} = media.webpage;
let video;
let audio;
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
video = buildVideoFromDocument(document);
}
if (document instanceof GramJs.Document && document.mimeType.startsWith('audio/')) {
audio = buildAudioFromDocument(document);
}
let story: ApiWebPageStoryData | undefined;
let gift: ApiStarGiftUnique | undefined;
let stickers: ApiWebPageStickerData | undefined;
const attributeStory = attributes
?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
const attributeGift = attributes
?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift);
if (attributeStory) {
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = {
id: attributeStory.id,
peerId,
};
if (attributeStory.story instanceof GramJs.StoryItem) {
addStoryToLocalDb(attributeStory.story, peerId);
}
}
if (attributeGift) {
const starGift = buildApiStarGift(attributeGift.gift);
gift = starGift.type === 'starGiftUnique' ? starGift : undefined;
}
const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
a instanceof GramJs.WebPageAttributeStickerSet
));
if (attributeStickers) {
stickers = {
documents: processStickerResult(attributeStickers.stickers),
isEmoji: attributeStickers.emojis,
isWithTextColor: attributeStickers.textColor,
};
}
const mediaSize = media.forceSmallMedia ? 'small' : media.forceLargeMedia ? 'large' : undefined;
webpage, forceLargeMedia, forceSmallMedia, safe,
} = media;
return {
mediaType: 'webpage',
id: Number(id),
...pick(media.webpage, [
'url',
'displayUrl',
'type',
'siteName',
'title',
'description',
'duration',
'hasLargeMedia',
]),
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
document: !video && !audio && document ? buildApiDocument(document) : undefined,
video,
audio,
story,
gift,
stickers,
mediaSize,
id: webpage.id.toString(),
isSafe: safe,
mediaSize: forceSmallMedia ? 'small' : forceLargeMedia ? 'large' : undefined,
};
}
export function buildWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiWebPage | undefined {
if (!(media instanceof GramJs.MessageMediaWebPage)) {
return undefined;
}
const {
webpage,
} = media;
return buildWebPage(webpage);
}
export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefined {
addWebPageMediaToLocalDb(webPage);
if (webPage instanceof GramJs.WebPageEmpty) {
return {
mediaType: 'webpage',
webpageType: 'empty',
id: webPage.id.toString(),
url: webPage.url,
};
}
if (webPage instanceof GramJs.WebPagePending) {
return {
mediaType: 'webpage',
webpageType: 'pending',
id: webPage.id.toString(),
url: webPage.url,
};
}
if (webPage instanceof GramJs.WebPage) {
const {
id, photo, document, attributes,
} = webPage;
let video;
let audio;
if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
video = buildVideoFromDocument(document);
}
if (document instanceof GramJs.Document && document.mimeType.startsWith('audio/')) {
audio = buildAudioFromDocument(document);
}
let story: ApiWebPageStoryData | undefined;
let gift: ApiStarGiftUnique | undefined;
let stickers: ApiWebPageStickerData | undefined;
const attributeStory = attributes
?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
const attributeGift = attributes
?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift);
if (attributeStory) {
const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
story = {
id: attributeStory.id,
peerId,
};
if (attributeStory.story instanceof GramJs.StoryItem) {
addStoryToLocalDb(attributeStory.story, peerId);
}
}
if (attributeGift) {
const starGift = buildApiStarGift(attributeGift.gift);
gift = starGift.type === 'starGiftUnique' ? starGift : undefined;
}
const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
a instanceof GramJs.WebPageAttributeStickerSet
));
if (attributeStickers) {
stickers = {
documents: processStickerResult(attributeStickers.stickers),
isEmoji: attributeStickers.emojis,
isWithTextColor: attributeStickers.textColor,
};
}
return {
mediaType: 'webpage',
webpageType: 'full',
id: id.toString(),
...pick(webPage, [
'url',
'displayUrl',
'type',
'siteName',
'title',
'description',
'duration',
'hasLargeMedia',
]),
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
document: !video && !audio && document ? buildApiDocument(document) : undefined,
video,
audio,
story,
gift,
stickers,
};
}
return undefined;
}
function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined {
if (!(media instanceof GramJs.MessageMediaPaidMedia)) {
return undefined;

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) {
if (media instanceof GramJs.MessageMediaDocument && media.document) {
const document = addMessageRepairInfo(media.document, context);
addDocumentToLocalDb(document);
}
if (media instanceof GramJs.MessageMediaWebPage
&& media.webpage instanceof GramJs.WebPage
) {
if (media.webpage.document) {
const document = addMessageRepairInfo(media.webpage.document, context);
addDocumentToLocalDb(document);
}
if (media.webpage.photo) {
const photo = addMessageRepairInfo(media.webpage.photo, context);
addPhotoToLocalDb(photo);
}
}
if (media instanceof GramJs.MessageMediaGame) {
if (media.game.document) {
const document = addMessageRepairInfo(media.game.document, context);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import type { ElementRef, FC } from '../../lib/teact/teact';
import type { ElementRef } from '../../lib/teact/teact';
import {
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact';
import { getActions } from '../../global';
import { getActions, withGlobal } from '../../global';
import type {
ApiAudio, ApiMessage, ApiVideo, ApiVoice,
ApiWebPage,
} from '../../api/types';
import type { BufferedRange } from '../../hooks/useBuffering';
import type { OldLangFn } from '../../hooks/useOldLang';
@ -14,15 +15,16 @@ import { ApiMediaFormat } from '../../api/types';
import { AudioOrigin } from '../../types';
import {
getMediaDuration,
getMediaFormat,
getMediaHash,
getMediaTransferState,
getMessageWebPageAudio,
getWebPageAudio,
hasMessageTtl,
isMessageLocal,
isOwnMessage,
} from '../../global/helpers';
import { selectWebPageFromMessage } from '../../global/selectors';
import { selectMessageMediaDuration } from '../../global/selectors/media';
import { makeTrackId } from '../../util/audioPlayer';
import buildClassName from '../../util/buildClassName';
import { captureEvents } from '../../util/captureEvents';
@ -77,13 +79,18 @@ type OwnProps = {
onDateClick?: (arg: ApiMessage) => void;
};
type StateProps = {
mediaDuration?: number;
webPage?: ApiWebPage;
};
export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)');
export const WITH_AVATAR_TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 410px)');
const AVG_VOICE_DURATION = 10;
// This is needed for browsers requiring user interaction before playing.
const PRELOAD = true;
const Audio: FC<OwnProps> = ({
const Audio = ({
theme,
message,
senderTitle,
@ -102,13 +109,15 @@ const Audio: FC<OwnProps> = ({
canDownload,
canTranscribe,
autoPlay,
webPage,
mediaDuration,
onHideTranscription,
onPlay,
onPause,
onReadMedia,
onCancelUpload,
onDateClick,
}) => {
}: OwnProps & StateProps) => {
const {
cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal,
} = getActions();
@ -118,7 +127,7 @@ const Audio: FC<OwnProps> = ({
audio: contentAudio, voice, video,
}, isMediaUnread,
} = message;
const audio = contentAudio || getMessageWebPageAudio(message);
const audio = contentAudio || getWebPageAudio(webPage);
const media = (voice || video || audio)!;
const mediaSource = (voice || video);
const isVoice = Boolean(voice || video);
@ -166,7 +175,7 @@ const Audio: FC<OwnProps> = ({
isPlaying, playProgress, playPause, setCurrentTime, duration,
} = useAudioPlayer(
makeTrackId(message),
getMediaDuration(message)!,
mediaDuration!,
trackType,
mediaData,
bufferingHandlers,
@ -716,4 +725,16 @@ function renderSeekline(
);
}
export default memo(Audio);
export default memo(withGlobal<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,
selectUser,
selectUserFullInfo,
selectWebPage,
} from '../../global/selectors';
import { selectCurrentLimit } from '../../global/selectors/limits';
import { selectSharedSettings } from '../../global/selectors/sharedState';
@ -158,6 +159,7 @@ import useDraft from '../middle/composer/hooks/useDraft';
import useEditing from '../middle/composer/hooks/useEditing';
import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip';
import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip';
import useLoadLinkPreview from '../middle/composer/hooks/useLoadLinkPreview';
import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip';
import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation';
import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip';
@ -826,6 +828,12 @@ const Composer: FC<OwnProps & StateProps> = ({
isDisabled: isInStoryViewer || Boolean(requestedDraft) || (!hasSuggestedPost && isMonoforum),
});
useLoadLinkPreview({
chatId,
threadId,
getHtml,
});
const resetComposer = useLastCallback((shouldPreserveInput = false) => {
if (!shouldPreserveInput) {
setHtml('');
@ -2028,8 +2036,7 @@ const Composer: FC<OwnProps & StateProps> = ({
<WebPagePreview
chatId={chatId}
threadId={threadId}
getHtml={getHtml}
isDisabled={!canAttachEmbedLinks || hasAttachments}
isDisabled={!canAttachEmbedLinks || hasAttachments || !hasText}
isEditing={Boolean(editingMessage)}
/>
</>
@ -2523,6 +2530,8 @@ export default memo(withGlobal<OwnProps>(
const isAppConfigLoaded = global.isAppConfigLoaded;
const insertingPeerIdMention = tabState.insertingPeerIdMention;
const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined;
return {
availableReactions: global.reactions.availableReactions,
topReactions: type === 'story' ? global.reactions.topReactions : undefined,
@ -2594,7 +2603,7 @@ export default memo(withGlobal<OwnProps>(
quickReplies: global.quickReplies.byId,
canSendQuickReplies,
noWebPage,
webPagePreview: selectTabState(global).webPagePreview,
webPagePreview,
isContactRequirePremium: userFullInfo?.isContactRequirePremium,
effect,
effectReactions,

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type { FC } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiMessage, ApiWebPage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
@ -7,8 +7,8 @@ import type { TextPart } from '../../types';
import {
getFirstLinkInMessage, getMessageText,
getMessageWebPage,
} from '../../global/helpers';
import { selectWebPageFromMessage } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { formatPastTimeShort } from '../../util/dates/dateFormat';
import trimText from '../../util/trimText';
@ -26,6 +26,10 @@ import './WebLink.scss';
const MAX_TEXT_LENGTH = 170; // symbols
type ApiWebPageWithFormatted =
ApiWebPage
& { formattedDescription?: TextPart[] };
type OwnProps = {
message: ApiMessage;
senderTitle?: string;
@ -34,16 +38,16 @@ type OwnProps = {
onMessageClick: (message: ApiMessage) => void;
};
type ApiWebPageWithFormatted =
ApiWebPage
& { formattedDescription?: TextPart[] };
type StateProps = {
webPage?: ApiWebPage;
};
const WebLink: FC<OwnProps> = ({
message, senderTitle, isProtected, observeIntersection, onMessageClick,
}) => {
const WebLink = ({
message, webPage, senderTitle, isProtected, observeIntersection, onMessageClick,
}: OwnProps & StateProps) => {
const lang = useOldLang();
let linkData: ApiWebPageWithFormatted | undefined = getMessageWebPage(message);
let linkData: ApiWebPageWithFormatted | undefined = webPage;
if (!linkData) {
const link = getFirstLinkInMessage(message);
@ -64,7 +68,7 @@ const WebLink: FC<OwnProps> = ({
onMessageClick(message);
});
if (!linkData) {
if (linkData?.webpageType !== 'full') {
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 {
getMessageIsSpoiler,
getMessageMediaHash,
getMessageRoundVideo,
isChatChannel,
isChatGroup,
@ -31,12 +30,13 @@ import { getPictogramDimensions } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../hooks/media/useThumbnail';
import { useFastClick } from '../../../hooks/useFastClick';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import useThumbnail from '../../../hooks/useThumbnail';
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
import RippleEffect from '../../ui/RippleEffect';
@ -111,7 +111,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined;
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
const mediaHash = containedMedia && getMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram');
const mediaHash = useMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram');
const mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
const mediaThumbnail = useThumbnail(containedMedia);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { getActions } from '../../../global';
@ -24,9 +22,9 @@ import './DropArea.scss';
export type OwnProps = {
isOpen: boolean;
withQuick?: boolean;
editingMessage?: ApiMessage | undefined;
onHide: NoneToVoidFunction;
onFileSelect: (files: File[]) => void;
editingMessage?: ApiMessage | undefined;
};
export enum DropAreaState {
@ -37,9 +35,9 @@ export enum DropAreaState {
const DROP_LEAVE_TIMEOUT_MS = 150;
const DropArea: FC<OwnProps> = ({
isOpen, withQuick, onHide, onFileSelect, editingMessage,
}) => {
const DropArea = ({
isOpen, withQuick, editingMessage, onHide, onFileSelect,
}: OwnProps) => {
const lang = useLang();
const { showNotification, updateAttachmentSettings } = getActions();
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 { getActions, withGlobal } from '../../../global';
import type {
ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage,
ApiWebPage,
ApiWebPageFull,
ApiWebPagePending,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
import type { ThemeKey, ThreadId, WebPageMediaSize } from '../../../types';
import type { Signal } from '../../../util/signals';
import { ApiMessageEntityTypes } from '../../../api/types';
import type { ThreadId, WebPageMediaSize } from '../../../types';
import { RE_LINK_TEMPLATE } from '../../../config';
import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors';
import {
getMediaHash,
getWebPageAudio,
getWebPageDocument,
getWebPagePhoto,
getWebPageVideo,
} from '../../../global/helpers';
import { selectNoWebPage, selectTabState, selectWebPage } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers';
import useThumbnail from '../../../hooks/media/useThumbnail';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDerivedSignal from '../../../hooks/useDerivedSignal';
import useDerivedState from '../../../hooks/useDerivedState';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
import useSyncEffect from '../../../hooks/useSyncEffect';
import useMedia from '../../../hooks/useMedia';
import useShowTransition from '../../../hooks/useShowTransition';
import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import Button from '../../ui/Button';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import WebPage from '../message/WebPage';
import './WebPagePreview.scss';
import styles from './WebPagePreview.module.scss';
type OwnProps = {
chatId: string;
threadId: ThreadId;
getHtml: Signal<string>;
isEditing: boolean;
isDisabled?: boolean;
};
type StateProps = {
webPagePreview?: ApiWebPage;
webPagePreview?: ApiWebPageFull | ApiWebPagePending;
noWebPage?: boolean;
theme: ThemeKey;
attachmentSettings: GlobalState['attachmentSettings'];
};
const DEBOUNCE_MS = 300;
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
const WebPagePreview: FC<OwnProps & StateProps> = ({
const WebPagePreview = ({
chatId,
threadId,
getHtml,
isDisabled,
webPagePreview,
noWebPage,
theme,
attachmentSettings,
isEditing,
}) => {
}: OwnProps & StateProps) => {
const {
loadWebPagePreview,
clearWebPagePreview,
toggleMessageWebPage,
updateAttachmentSettings,
} = getActions();
const lang = useOldLang();
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
const lang = useLang();
const ref = useRef<HTMLDivElement>();
const isInvertedMedia = attachmentSettings.isInvertedMedia;
const isSmallerMedia = attachmentSettings.webPageMediaSize === 'small';
const detectLinkDebounced = useDebouncedResolver(() => {
const formattedText = parseHtmlAsFormattedText(getHtml());
const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => (
entity.type === ApiMessageEntityTypes.TextUrl
));
formattedTextWithLinkRef.current = formattedText;
return linkEntity?.url || formattedText.text.match(RE_LINK)?.[0];
}, [getHtml], DEBOUNCE_MS, true);
const getLink = useDerivedSignal(detectLinkDebounced, [detectLinkDebounced, getHtml], true);
useEffect(() => {
const link = getLink();
const formattedText = formattedTextWithLinkRef.current;
if (link) {
loadWebPagePreview({ text: formattedText! });
} else {
clearWebPagePreview();
toggleMessageWebPage({ chatId, threadId });
}
}, [getLink, chatId, threadId]);
useSyncEffect(() => {
clearWebPagePreview();
toggleMessageWebPage({ chatId, threadId });
}, [chatId, clearWebPagePreview, threadId, toggleMessageWebPage]);
const isShown = useDerivedState(() => {
return Boolean(webPagePreview && getHtml() && !noWebPage && !isDisabled);
}, [isDisabled, getHtml, noWebPage, webPagePreview]);
const { shouldRender, transitionClassNames } = useShowTransitionDeprecated(isShown);
return Boolean(webPagePreview && !noWebPage && !isDisabled);
}, [isDisabled, noWebPage, webPagePreview]);
const { shouldRender } = useShowTransition({ isOpen: isShown, ref, withShouldRender: true });
const hasMediaSizeOptions = webPagePreview?.hasLargeMedia;
const hasMediaSizeOptions = webPagePreview?.webpageType === 'full' && webPagePreview.hasLargeMedia;
const renderingWebPage = useCurrentOrPrev(webPagePreview, true);
const prevWebPageRef = useRef<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(() => {
toggleMessageWebPage({ chatId, threadId, noWebPage: true });
@ -124,13 +106,13 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
const {
isContextMenuOpen, contextMenuAnchor, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
handleContextMenuClose, handleContextMenuHide, handleBeforeContextMenu,
} = useContextMenuHandlers(ref, isEditing, true);
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!);
const getMenuElement = useLastCallback(
() => ref.current!.querySelector('.web-page-preview-context-menu .bubble'),
() => ref.current!.querySelector(`.${styles.contextMenu} .bubble`),
);
const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => {
@ -156,14 +138,6 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
return undefined;
}
// TODO Refactor so `WebPage` can be used without message
const { photo, ...webPageWithoutPhoto } = renderingWebPage;
const messageStub = {
content: {
webPage: webPageWithoutPhoto,
},
} as ApiMessage;
function renderContextMenu() {
return (
<Menu
@ -172,7 +146,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
className="web-page-preview-context-menu"
className={styles.contextMenu}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
autoClose
@ -180,36 +154,31 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
<>
{
isInvertedMedia ? (
<MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}>
{lang('PreviewSender.MoveTextUp')}
{lang('ContextMoveTextUp')}
</MenuItem>
) : (
<MenuItem icon="move-caption-down" onClick={() => updateIsInvertedMedia(true)}>
{lang(('PreviewSender.MoveTextDown'))}
{lang('ContextMoveTextDown')}
</MenuItem>
)
}
{hasMediaSizeOptions && (
isSmallerMedia ? (
<MenuItem icon="expand" onClick={() => updateIsLargerMedia('large')}>
{lang('ChatInput.EditLink.LargerMedia')}
{lang('ContextLinkLargerMedia')}
</MenuItem>
) : (
<MenuItem icon="collapse" onClick={() => updateIsLargerMedia('small')}>
{lang(('ChatInput.EditLink.SmallerMedia'))}
{lang('ContextLinkSmallerMedia')}
</MenuItem>
)
)}
<MenuItem
icon="delete"
onClick={handleClearWebpagePreview}
>
{lang('ChatInput.EditLink.RemovePreview')}
{lang('ContextLinkRemovePreview')}
</MenuItem>
</>
</Menu>
@ -217,24 +186,55 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
}
return (
<div className={buildClassName('WebPagePreview', transitionClassNames)} ref={ref}>
<div className="WebPagePreview_inner">
<div className="WebPagePreview-left-icon" onClick={handlePreviewClick}>
<div
className={buildClassName(
styles.root,
!isEditing && styles.interactive,
)}
ref={ref}
>
<div className={styles.inner}>
<div className={styles.leftIcon} onClick={handlePreviewClick}>
<Icon name="link" />
</div>
<WebPage
message={messageStub}
inPreview
theme={theme}
onContainerClick={handlePreviewClick}
isEditing={isEditing}
/>
{renderingWebPage && renderingWebPage.webpageType !== 'empty' && (
<PeerColorWrapper
noUserColors
className={styles.preview}
onContextMenu={handleContextMenu}
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
className="WebPagePreview-clear"
className={styles.clear}
round
faded
color="translucent"
ariaLabel="Clear Webpage Preview"
ariaLabel={lang('AccLinkRemovePreview')}
onClick={handleClearWebpagePreview}
>
<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>(
(global, { chatId, threadId }): StateProps => {
const tabState = selectTabState(global);
const noWebPage = selectNoWebPage(global, chatId, threadId);
const {
attachmentSettings,
} = global;
const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined;
return {
theme: selectTheme(global),
webPagePreview: selectTabState(global).webPagePreview,
webPagePreview: webPagePreview?.webpageType === 'empty' ? undefined : webPagePreview,
noWebPage,
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,
ApiThreadInfo,
ApiTypeStory,
ApiWebPage,
} from '../../../api/types';
import type {
ActiveDownloads,
@ -33,7 +34,6 @@ import {
areReactionsEmpty,
getCanPostInChat,
getIsDownloading,
getMessageDownloadableMedia,
getMessageVideo,
getUserFullName,
hasMessageTtl,
@ -75,6 +75,7 @@ import {
selectUser,
selectUserStatus,
} from '../../../global/selectors';
import { selectMessageDownloadableMedia } from '../../../global/selectors/media';
import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard';
import { isUserId } from '../../../util/entities/ids';
@ -110,6 +111,7 @@ export type OwnProps = {
type StateProps = {
threadId?: ThreadId;
poll?: ApiPoll;
webPage?: ApiWebPage;
story?: ApiTypeStory;
chat?: ApiChat;
availableReactions?: ApiAvailableReaction[];
@ -180,6 +182,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
customEmojiSets,
album,
poll,
webPage,
story,
anchor,
targetHref,
@ -345,15 +348,16 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}, [message.reactions?.recentReactions, message.seenByDates]);
const isDownloading = useMemo(() => {
const global = getGlobal();
if (album) {
return album.messages.some((msg) => {
const downloadableMedia = getMessageDownloadableMedia(msg);
const downloadableMedia = selectMessageDownloadableMedia(global, msg);
if (!downloadableMedia) return false;
return getIsDownloading(activeDownloads, downloadableMedia);
});
}
const downloadableMedia = getMessageDownloadableMedia(message);
const downloadableMedia = selectMessageDownloadableMedia(global, message);
if (!downloadableMedia) return false;
return getIsDownloading(activeDownloads, downloadableMedia);
}, [activeDownloads, album, message]);
@ -578,8 +582,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
});
const handleDownloadClick = useLastCallback(() => {
const global = getGlobal();
(album?.messages || [message]).forEach((msg) => {
const downloadableMedia = getMessageDownloadableMedia(msg);
const downloadableMedia = selectMessageDownloadableMedia(global, msg);
if (!downloadableMedia) return;
if (isDownloading) {
@ -728,6 +733,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
isInSavedMessages={isInSavedMessages}
noReplies={noReplies}
poll={poll}
webPage={webPage}
story={story}
onOpenThread={handleOpenThread}
onReply={handleReply}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -153,11 +153,13 @@ const Video = <T,>({
const canLoadPreview = isIntersectingForLoading;
const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview);
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 thumbRef = useBlurredMediaThumbRef(video, noThumb);
useMediaTransition(!noThumb, { ref: thumbRef });
useMediaTransition({ ref: thumbRef, hasMediaData: !noThumb });
const blurredBackgroundRef = useBlurredMediaThumbRef(video, !withBlurredBackground);
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(

View File

@ -45,34 +45,6 @@
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 {
position: relative;
@ -221,16 +193,12 @@
}
@media (min-width: 1921px) {
@supports (aspect-ratio: 1) {
&:not(.in-preview) {
max-width: none;
max-width: none;
.thumbnail,
.full-media {
width: 100% !important;
height: auto !important;
}
}
.thumbnail,
.full-media {
width: 100% !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 { 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 { 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 buildClassName from '../../../util/buildClassName';
import { tryParseDeepLink } from '../../../util/deepLinkParser';
import trimText from '../../../util/trimText';
import renderText from '../../common/helpers/renderText';
import { calculateMediaDimensions } from './helpers/mediaDimensions';
import { getWebpageButtonLangKey } from './helpers/webpageType';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useAppLayout from '../../../hooks/useAppLayout';
import useEnsureStory from '../../../hooks/useEnsureStory';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
@ -44,11 +42,12 @@ const STICKER_SIZE = 80;
const EMOJI_SIZE = 38;
type OwnProps = {
message: ApiMessage;
messageWebPage: ApiMessageWebPage;
webPage: ApiWebPage;
message?: ApiMessage;
noAvatars?: boolean;
canAutoLoad?: boolean;
canAutoPlay?: boolean;
inPreview?: boolean;
asForwarded?: boolean;
isDownloading?: boolean;
isProtected?: boolean;
@ -59,7 +58,6 @@ type OwnProps = {
shouldWarnAboutSvg?: boolean;
autoLoadFileMaxSizeMb?: number;
lastPlaybackTimestamp?: number;
isEditing?: boolean;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onAudioPlay?: NoneToVoidFunction;
@ -73,11 +71,12 @@ type StateProps = {
};
const WebPage: FC<OwnProps & StateProps> = ({
messageWebPage,
webPage,
message,
noAvatars,
canAutoLoad,
canAutoPlay,
inPreview,
asForwarded,
isDownloading = false,
isProtected,
@ -88,7 +87,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
shouldWarnAboutSvg,
autoLoadFileMaxSizeMb,
lastPlaybackTimestamp,
isEditing,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onMediaClick,
@ -98,8 +96,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelMediaTransfer,
}) => {
const { openUrl, openTelegramLink } = getActions();
const webPage = getMessageWebPage(message);
const { isMobile } = useAppLayout();
const stickersRef = useRef<HTMLDivElement>();
const oldLang = useOldLang();
@ -113,15 +109,9 @@ const WebPage: FC<OwnProps & StateProps> = ({
onContainerClick?.(e);
});
const handleOpenTelegramLink = useLastCallback(() => {
if (!webPage) return;
const fullWebPage = webPage?.webpageType === 'full' ? webPage : undefined;
openTelegramLink({
url: webPage.url,
});
});
const { story: storyData, stickers } = webPage || {};
const { story: storyData, stickers } = fullWebPage || {};
useEnsureStory(storyData?.peerId, storyData?.id, story);
@ -134,9 +124,13 @@ const WebPage: FC<OwnProps & StateProps> = ({
return parsedLink.timestamp;
}, [webPage?.url]);
if (!webPage) {
return undefined;
}
if (webPage?.webpageType !== 'full') return undefined;
const handleOpenTelegramLink = useLastCallback(() => {
openTelegramLink({
url: webPage.url,
});
});
const {
siteName,
@ -149,38 +143,28 @@ const WebPage: FC<OwnProps & StateProps> = ({
audio,
type,
document,
mediaSize,
} = webPage;
const { mediaSize } = messageWebPage;
const isStory = type === WEBPAGE_STORY_TYPE;
const isGift = type === WEBPAGE_GIFT_TYPE;
const isExpiredStory = story && 'isDeleted' in story;
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 truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = Boolean(stickers);
if (isArticle && webPage?.photo && !webPage.video && !webPage.document) {
const { width, height } = calculateMediaDimensions({
media: webPage.photo,
isOwn: message.isOutgoing,
isInWebPage: true,
asForwarded,
noAvatars,
isMobile,
});
isSquarePhoto = (width === height || mediaSize === 'small') && mediaSize !== 'large';
isSquarePhoto = getIsSmallPhoto(webPage, mediaSize);
}
const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto;
const className = buildClassName(
'WebPage',
inPreview && 'in-preview',
!isEditing && inPreview && 'interactive',
isSquarePhoto && 'with-square-photo',
!photo && !video && !inPreview && 'without-media',
!photo && !video && 'without-media',
video && 'with-video',
!isArticle && 'no-article',
document && 'with-document',
@ -225,7 +209,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
{isStory && (
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
)}
{isGift && !inPreview && (
{isGift && (
<WebPageUniqueGift
gift={webPage.gift!}
observeIntersectionForLoading={observeIntersectionForLoading}
@ -235,11 +219,11 @@ const WebPage: FC<OwnProps & StateProps> = ({
)}
{isArticle && (
<div
className={buildClassName('WebPage-text', !inPreview && 'WebPage-text_interactive')}
onClick={!inPreview ? () => openUrl({ url, shouldSkipModal: true }) : undefined}
className={buildClassName('WebPage-text', 'WebPage-text_interactive')}
onClick={() => openUrl({ url, shouldSkipModal: messageWebPage.isSafe })}
>
<SafeLink className="site-name" url={url} text={siteName || displayUrl} />
{(!inPreview || isGift) && title && (
{title && (
<p className="site-title">{renderText(title)}</p>
)}
{truncatedDescription && !isGift && (
@ -250,7 +234,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
{photo && !isGift && !video && !document && (
<Photo
photo={photo}
isOwn={message.isOutgoing}
isOwn={message?.isOutgoing}
isInWebPage
observeIntersection={observeIntersectionForLoading}
noAvatars={noAvatars}
@ -265,10 +249,10 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer}
/>
)}
{!inPreview && video && (
{video && (
<Video
video={video}
isOwn={message.isOutgoing}
isOwn={message?.isOutgoing}
isInWebPage
observeIntersectionForLoading={observeIntersectionForLoading}
noAvatars={noAvatars}
@ -282,10 +266,10 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer}
/>
)}
{!inPreview && audio && (
{audio && (
<Audio
theme={theme}
message={message}
message={message!}
origin={AudioOrigin.Inline}
noAvatars={noAvatars}
isDownloading={isDownloading}
@ -293,7 +277,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
onCancelUpload={onCancelMediaTransfer}
/>
)}
{!inPreview && document && (
{document && (
<Document
document={document}
message={message}
@ -305,7 +289,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
shouldWarnAboutSvg={shouldWarnAboutSvg}
/>
)}
{!inPreview && stickers && (
{stickers && (
<div
ref={stickersRef}
className={buildClassName(
@ -327,18 +311,23 @@ const WebPage: FC<OwnProps & StateProps> = ({
))}
</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>
{quickButtonTitle && renderQuickButton(quickButtonTitle)}
</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>(
(global): StateProps => {
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 { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
@ -10,6 +10,7 @@ export function buildContentClassName(
album?: IAlbum,
{
poll,
webPage,
hasSubheader,
isCustomShape,
isLastInGroup,
@ -26,6 +27,7 @@ export function buildContentClassName(
hasOutsideReactions,
}: {
poll?: ApiPoll;
webPage?: ApiWebPage;
hasSubheader?: boolean;
isCustomShape?: boolean | number;
isLastInGroup?: boolean;
@ -48,7 +50,7 @@ export function buildContentClassName(
const content = getMessageContent(message);
const {
photo = paidMediaPhoto, video = paidMediaVideo,
audio, voice, document, webPage, contact, location, invoice, storyData,
audio, voice, document, contact, location, invoice, storyData,
giveaway, giveawayResults,
} = content;
const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text;
@ -128,7 +130,7 @@ export function buildContentClassName(
classNames.push('poll');
} else if (giveaway || giveawayResults) {
classNames.push('giveaway');
} else if (webPage) {
} else if (webPage?.webpageType === 'full') {
classNames.push('web-page');
if (webPage.photo || webPage.video) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,10 @@ import {
addUsers,
removeBlockedUser,
removePeerStory,
replaceWebPage,
setConfirmPaymentUrl,
setPaymentStep,
updateFullWebPage,
updateLastReadStoryForPeer,
updatePeerStory,
updatePeersWithStories,
@ -33,7 +35,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'updateEntities': {
const {
users, chats, threadInfos, polls,
users, chats, threadInfos, polls, webPages,
} = update;
if (users) global = addUsers(global, users);
if (chats) global = addChats(global, chats);
@ -43,6 +45,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
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);
break;
}
@ -153,6 +164,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
});
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':
global = addStoriesForPeer(global, update.peerId, { [update.story.id]: update.story });
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 { omit } from '../../../util/iteratees';
import { getTimestampableMedia } from '../../helpers';
import { getMessageReplyInfo } from '../../helpers/replies';
import { addActionHandler } from '../../index';
import { updateTabState } from '../../reducers/tabs';
import { selectChatMessage, selectReplyMessage, selectTabState } from '../../selectors';
import { selectTimestampableMedia } from '../../selectors/media';
addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => {
const {
@ -65,7 +65,7 @@ addActionHandler('openMediaFromTimestamp', (global, actions, payload): ActionRet
const replyInfo = getMessageReplyInfo(message);
const replyMessage = selectReplyMessage(global, message);
const messageMedia = getTimestampableMedia(message);
const messageMedia = selectTimestampableMedia(global, message);
const maxMessageDuration = messageMedia?.duration;
if (maxMessageDuration) {
if (maxMessageDuration <= timestamp) return;
@ -93,7 +93,7 @@ addActionHandler('openMediaFromTimestamp', (global, actions, payload): ActionRet
return;
}
const replyMessageMedia = replyMessage ? getTimestampableMedia(replyMessage) : undefined;
const replyMessageMedia = replyMessage ? selectTimestampableMedia(global, replyMessage) : undefined;
const maxReplyMessageDuration = replyMessageMedia?.duration;
if (!maxReplyMessageDuration || maxReplyMessageDuration <= timestamp) return;

View File

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

View File

@ -40,6 +40,7 @@ import {
selectChatLastMessageId,
selectChatMessages,
selectCurrentMessageList,
selectFullWebPageFromMessage,
selectTopics,
selectViewportIds,
selectVisibleUsers,
@ -344,6 +345,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.settings.themes) {
cached.settings.themes = initialState.settings.themes;
}
if (!cached.messages.webPageById) {
cached.messages.webPageById = initialState.messages.webPageById;
}
}
function updateCache(force?: boolean) {
@ -483,7 +488,10 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
const chatStoriesUserIds = currentChatIds
.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));
const attachBotIds = Object.keys(global.attachMenu?.bots || {});
@ -530,8 +538,9 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
const message = messages[id];
if (!message) return undefined;
const content = message.content;
const webPage = selectFullWebPageFromMessage(global, message);
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>>);
const pollIdsToSave: string[] = [];
const webPageIdsToSave: string[] = [];
chatIdsToSave.forEach((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);
}
if (message.content.webPage) {
webPageIdsToSave.push(message.content.webPage.id);
}
return acc;
}, {} as Record<number, ApiMessage>);
@ -670,6 +684,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
return {
byChatId,
pollById: pickTruthy(global.messages.pollById, pollIdsToSave),
webPageById: pickTruthy(global.messages.webPageById, webPageIdsToSave),
sponsoredByChatId: {},
playbackByChatId: {},
};

View File

@ -13,7 +13,10 @@ import type {
ApiVideo,
ApiVoice,
ApiWebDocument,
ApiWebPage,
MediaContainer,
SizeTarget,
StatefulMediaContent,
} from '../../api/types';
import type { ActiveDownloads } from '../../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 DownloadableMedia = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiAudio | ApiVoice | ApiWebDocument;
type Target =
'micro'
| 'pictogram'
| 'inline'
| 'preview'
| 'full'
| 'download';
export function getMessageContent(message: MediaContainer) {
return message.content;
}
@ -104,10 +99,6 @@ export function getMessageDocument(message: MediaContainer) {
return message.content.document;
}
export function getMessageWebPageDocument(message: MediaContainer) {
return getMessageWebPage(message)?.document;
}
export function isDocumentPhoto(document: ApiDocument) {
return document.innerMediaType === 'photo';
}
@ -149,21 +140,25 @@ export function getMessagePaidMedia(message: MediaContainer) {
return message.content.paidMedia;
}
export function getMessageWebPagePhoto(message: MediaContainer) {
return getMessageWebPage(message)?.photo;
}
export function getMessageDocumentPhoto(message: MediaContainer) {
const document = getMessageDocument(message);
return document && isDocumentPhoto(document) ? document : undefined;
}
export function getMessageWebPageVideo(message: MediaContainer) {
return getMessageWebPage(message)?.video;
export function getWebPagePhoto(webPage?: ApiWebPage) {
return webPage?.webpageType === 'full' ? webPage.photo : undefined;
}
export function getMessageWebPageAudio(message: MediaContainer) {
return getMessageWebPage(message)?.audio;
export function getWebPageVideo(webPage?: ApiWebPage) {
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) {
@ -171,40 +166,6 @@ export function getMessageDocumentVideo(message: MediaContainer) {
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) {
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}`;
}
export function getMessageMediaHash(
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) {
export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: SizeTarget, isAction?: boolean) {
const base = `photo${photo.id}`;
const isVideo = photo.mediaType === 'photo' && photo.isVideo;
@ -302,7 +222,7 @@ export function getVideoProfilePhotoMediaHash(photo: ApiPhoto) {
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}`;
switch (target) {
@ -325,7 +245,7 @@ export function getVideoPreviewMediaHash(video: ApiVideo) {
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}`;
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}`;
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}`;
switch (target) {
@ -381,7 +301,7 @@ export function getWebDocumentHash(webDocument?: ApiWebDocument) {
return `webDocument:${webDocument.url}`;
}
export function getStickerMediaHash(sticker: ApiSticker, target: Target) {
export function getStickerMediaHash(sticker: ApiSticker, target: SizeTarget) {
const base = `document${sticker.id}`;
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) {
case 'photo':
return getPhotoMediaHash(media, target);
@ -458,7 +378,7 @@ export function getAudioHasCover(media: ApiAudio) {
}
export function getMediaFormat(
media: DownloadableMedia, target: Target,
media: DownloadableMedia, target: SizeTarget,
): ApiMediaFormat {
const isDocument = media.mediaType === 'document';
const hasInnerVideo = isDocument && media.innerMediaType === 'video';
@ -609,31 +529,6 @@ export function getMessageContentIds(
}, [] 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) {
if (!newMessage.content) return false;
if (newMessage.content.photo) return true;
@ -672,9 +567,60 @@ export function getIsDownloading(activeDownloads: ActiveDownloads, media: Downlo
return Boolean(activeDownloads[hash]);
}
export function getTimestampableMedia(message: MediaContainer) {
const video = getMessageVideo(message) || getMessageWebPageVideo(message);
return (video && !video.isRound && !video.isGif ? video : undefined)
|| getMessageAudio(message)
|| getMessageVoice(message);
export function getMessageMediaHash(
message: MediaContainer,
statefulMedia: StatefulMediaContent,
target: SizeTarget,
) {
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,
} from '../../api/types';
import type {
ApiPoll, MediaContainer, StatefulMediaContent,
ApiPoll, ApiWebPage, MediaContainer, StatefulMediaContent,
} from '../../api/types/messages';
import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization';
@ -34,6 +34,7 @@ import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { isLocalMessageId } from '../../util/keys/messageKey';
import { getServerTime } from '../../util/serverTime';
import { getGlobal } from '../index';
import { selectPollFromMessage, selectWebPageFromMessage } from '../selectors';
import { getMainUsername } from './users';
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 {
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 story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined;
return groupStatefulContent({ poll, story });
return groupStatefulContent({ poll, story, webPage });
}
export function groupStatefulContent({
poll,
story,
webPage,
}: {
poll?: ApiPoll;
story?: ApiTypeStory;
webPage?: ApiWebPage;
}) {
return {
poll,
story: story && 'content' in story ? story : undefined,
webPage,
};
}
@ -104,8 +109,8 @@ export function getMessageCustomShape(message: ApiMessage): boolean {
return true;
}
if (!text || photo || video || audio || voice || document || pollId || webPage || contact || action || game || invoice
|| location || storyData) {
if (!text || photo || video || audio || voice || document || pollId || webPage || contact || action || game
|| invoice || location || storyData) {
return false;
}

View File

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

View File

@ -1,5 +1,7 @@
import type {
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
ApiWebPage,
ApiWebPageFull,
} from '../../api/types';
import type {
FocusDirection,
@ -47,6 +49,7 @@ import {
selectThreadIdFromMessage,
selectThreadInfo,
selectViewportIds,
selectWebPage,
} from '../selectors';
import { removeIdFromSearchResults } from './middleSearch';
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>(
global: T,
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,
ApiPeer, ApiRestrictionReason, ApiSponsoredMessage,
ApiStickerSetInfo,
MediaContainer,
} from '../../api/types';
import type {
ChatTranslatedMessages,
@ -22,7 +23,7 @@ import type {
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
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,
} from '../../config';
import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment';
@ -46,10 +47,9 @@ import {
getMessagePhoto,
getMessageVideo,
getMessageVoice,
getMessageWebPagePhoto,
getMessageWebPageVideo,
getSendingState,
getTimestampableMedia,
getWebPagePhoto,
getWebPageVideo,
hasMessageTtl,
isActionMessage,
isChatBasicGroup,
@ -78,7 +78,6 @@ import {
selectRequestedChatTranslationLanguage,
} from './chats';
import { selectPeer, selectPeerPaidMessagesStars } from './peers';
import { selectSettingsKeys } from './settings';
import { selectPeerStory } from './stories';
import { selectIsStickerFavorite } from './symbols';
import { selectTabState } from './tabs';
@ -485,11 +484,31 @@ export function selectPoll<T extends GlobalState>(global: T, pollId: string) {
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;
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) {
const { chatId } = message;
const chat = selectChat(global, chatId);
@ -614,10 +633,13 @@ export function selectCanForwardMessage<T extends GlobalState>(global: T, messag
const isAction = isActionMessage(message);
const hasTtl = hasMessageTtl(message);
const { content } = message;
const webPage = selectFullWebPageFromMessage(global, message);
const story = content.storyData
? selectPeerStory(global, content.storyData.peerId, content.storyData.id)
: (content.webPage?.story
? selectPeerStory(global, content.webPage.story.peerId, content.webPage.story.id)
: (webPage?.story
? selectPeerStory(global, webPage.story.peerId, webPage.story.id)
: undefined
);
const isChatProtected = selectIsChatProtected(global, message.chatId);
@ -657,6 +679,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const isDocumentSticker = isMessageDocumentSticker(message);
const isBoostMessage = message.content.action?.type === 'boostApply';
const isMonoforum = chat.isMonoforum;
const webPage = selectFullWebPageFromMessage(global, message);
const hasChatPinPermission = (chat.isCreator
|| (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
@ -736,7 +759,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const canCopyLink = !isLocal && !isAction && (isChannel || isSuperGroup) && !isMonoforum;
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)
&& !hasTtl;
@ -1143,8 +1166,9 @@ export function selectCanAutoLoadMedia<T extends GlobalState>(
const sender = 'id' in message ? selectSender(global, message) : undefined;
const isPhoto = Boolean(getMessagePhoto(message) || getMessageWebPagePhoto(message));
const isVideo = Boolean(getMessageVideo(message) || getMessageWebPageVideo(message));
const webPage = selectWebPageFromMessage(global, 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 {
@ -1551,23 +1575,6 @@ export function selectReplyMessage<T extends GlobalState>(global: T, message: Ap
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>(
global: T, restrictionReasons?: ApiRestrictionReason[],
): ApiRestrictionReason[] {
@ -1583,15 +1590,3 @@ export function selectActiveRestrictionReasons<T extends GlobalState>(
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 { getCurrentTabId } from '../../util/establishMultitabRole';
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia';
import { getMessageVideo, getWebPageVideo } from '../helpers/messageMedia';
import { selectCurrentManagement } from './management';
import { selectWebPageFromMessage } from './messages';
import { selectSharedSettings } from './sharedState';
import { selectIsStatisticsShown } from './statistics';
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) {
const video = getMessageVideo(message) || getMessageWebPageVideo(message);
const webPage = selectWebPageFromMessage(global, message);
const video = getMessageVideo(message) || getWebPageVideo(webPage);
if (!video) {
return undefined;
}

View File

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

View File

@ -51,7 +51,6 @@ import type {
ApiTypeStoryView,
ApiUser,
ApiVideo,
ApiWebPage,
} from '../../api/types';
import type { ApiEmojiStatusCollectible } from '../../api/types/users';
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
@ -372,7 +371,7 @@ export type TabState = {
isMuted: boolean;
};
webPagePreview?: ApiWebPage;
webPagePreviewId?: string;
loadingThread?: {
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>(
mediaData?: unknown,
options?: Partial<HookParams<RefType>>,
) {
const isMediaReady = Boolean(mediaData);
const { ref } = useShowTransition<RefType>({
isOpen: isMediaReady,
noMountTransition: isMediaReady,
params: HookParamsWithMediaData<RefType>
): HookResult<RefType>;
export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>(
params: HookParamsWithMediaDataAndShouldRender<RefType>
): HookResultWithShouldRender<RefType>;
export default function useMediaTransition<RefType extends HTMLElement = HTMLDivElement>(
params: HookParamsWithMediaData<RefType> | HookParamsWithMediaDataAndShouldRender<RefType>,
): HookResult<RefType> | HookResultWithShouldRender<RefType> {
const result = useShowTransition<RefType>({
isOpen: params.hasMediaData,
noMountTransition: params.hasMediaData,
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;
};
type HookParamsWithShouldRender<RefType extends HTMLElement> = BaseHookParams<RefType> & {
export type HookParamsWithShouldRender<RefType extends HTMLElement> = BaseHookParams<RefType> & {
withShouldRender: true;
};
type HookResult<RefType extends HTMLElement> = {
export type HookResult<RefType extends HTMLElement> = {
ref: ElementRef<RefType>;
getIsClosing: Signal<boolean>;
};
type HookResultWithShouldRender<RefType extends HTMLElement> = HookResult<RefType> & {
export type HookResultWithShouldRender<RefType extends HTMLElement> = HookResult<RefType> & {
shouldRender: boolean;
};
@ -50,6 +50,9 @@ type State =
| 'open'
| 'closing';
/**
* Use for showing and hiding small elements with transitions. For large elements, use {@link useMediaTransition}.
*/
export default function useShowTransition<RefType extends HTMLElement = HTMLDivElement>(
params: HookParams<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;
'AttachMusic': undefined;
'AttachContact': undefined;
'AttachStory': undefined;
'MessageLocation': undefined;
'MessageLiveLocation': undefined;
'ServiceNotifications': undefined;
@ -1622,6 +1623,13 @@ export interface LangPair {
'LabelPayInTON': undefined;
'PriceChanged': undefined;
'PayNewPrice': undefined;
'LinkPreview': undefined;
'ContextMoveTextUp': undefined;
'ContextMoveTextDown': undefined;
'ContextLinkLargerMedia': undefined;
'ContextLinkSmallerMedia': undefined;
'ContextLinkRemovePreview': undefined;
'AccLinkRemovePreview': undefined;
'GlobalSearch': undefined;
'DescriptionPublicPostsSearch': undefined;
'PublicPosts': undefined;

View File

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