WebPage: Support server update (#6118)
This commit is contained in:
parent
baf36a1e15
commit
3d60271505
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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>
|
||||
))
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>();
|
||||
|
||||
141
src/components/middle/composer/WebPagePreview.module.scss
Normal file
141
src/components/middle/composer/WebPagePreview.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
68
src/components/middle/composer/hooks/useLoadLinkPreview.ts
Normal file
68
src/components/middle/composer/hooks/useLoadLinkPreview.ts
Normal 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]);
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)! });
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
@ -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: {},
|
||||
};
|
||||
|
||||
@ -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'))
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -157,6 +157,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
byChatId: {},
|
||||
sponsoredByChatId: {},
|
||||
pollById: {},
|
||||
webPageById: {},
|
||||
playbackByChatId: {},
|
||||
},
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
126
src/global/selectors/media.ts
Normal file
126
src/global/selectors/media.ts
Normal 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];
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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;
|
||||
|
||||
19
src/hooks/media/useMessageMediaHash.ts
Normal file
19
src/hooks/media/useMessageMediaHash.ts
Normal 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);
|
||||
}
|
||||
25
src/hooks/media/useThumbnail.ts
Normal file
25
src/hooks/media/useThumbnail.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
8
src/types/language.d.ts
vendored
8
src/types/language.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user