Introduce Paid Media (#4729)
This commit is contained in:
parent
aa2cda81c2
commit
aad2ed366d
@ -17,7 +17,7 @@ import type {
|
||||
} from '../../types';
|
||||
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import localDb from '../localDb';
|
||||
import { addDocumentToLocalDb } from '../helpers';
|
||||
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent';
|
||||
@ -100,7 +100,7 @@ function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIco
|
||||
|
||||
if (!document) return undefined;
|
||||
|
||||
localDb.documents[String(icon.icon.id)] = icon.icon;
|
||||
addDocumentToLocalDb(icon.icon);
|
||||
|
||||
return {
|
||||
name: icon.name,
|
||||
|
||||
@ -85,10 +85,12 @@ export function buildApiPhoto(photo: GramJs.Photo, isSpoiler?: boolean): ApiPhot
|
||||
.map(buildApiPhotoSize);
|
||||
|
||||
return {
|
||||
mediaType: 'photo',
|
||||
id: String(photo.id),
|
||||
thumbnail: buildApiThumbnailFromStripped(photo.sizes),
|
||||
sizes,
|
||||
isSpoiler,
|
||||
date: photo.date,
|
||||
...(photo.videoSizes && { videoSizes: compact(photo.videoSizes.map(buildApiVideoSize)), isVideo: true }),
|
||||
};
|
||||
}
|
||||
@ -115,7 +117,7 @@ export function buildApiPhotoSize(photoSize: GramJs.PhotoSize): ApiPhotoSize {
|
||||
return {
|
||||
width: w,
|
||||
height: h,
|
||||
type: type as ('m' | 'x' | 'y'),
|
||||
type: type as ('s' | 'm' | 'x' | 'y' | 'w'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -10,8 +10,9 @@ import type {
|
||||
ApiGiveawayResults,
|
||||
ApiInvoice,
|
||||
ApiLocation,
|
||||
ApiMessageExtendedMediaPreview,
|
||||
ApiMediaExtendedPreview,
|
||||
ApiMessageStoryData,
|
||||
ApiPaidMedia,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiSticker,
|
||||
@ -27,7 +28,7 @@ import type { UniversalMessage } from './messages';
|
||||
|
||||
import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_WEBM_TYPE } from '../../../config';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { addStoryToLocalDb, serializeBytes } from '../helpers';
|
||||
import { addMediaToLocalDb, addStoryToLocalDb, serializeBytes } from '../helpers';
|
||||
import {
|
||||
buildApiFormattedText,
|
||||
buildApiMessageEntity,
|
||||
@ -74,6 +75,8 @@ export function buildMessageTextContent(
|
||||
}
|
||||
|
||||
export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaContent | undefined {
|
||||
addMediaToLocalDb(media);
|
||||
|
||||
const ttlSeconds = 'ttlSeconds' in media ? media.ttlSeconds : undefined;
|
||||
|
||||
const isExpiredVoice = isExpiredVoiceMessage(media);
|
||||
@ -98,7 +101,7 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
if (media instanceof GramJs.MessageMediaInvoice && media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
return buildMessageMediaContent(media.extendedMedia.media);
|
||||
}
|
||||
|
||||
@ -145,6 +148,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC
|
||||
const giveawayResults = buildGiweawayResultsFromMedia(media);
|
||||
if (giveawayResults) return { giveawayResults };
|
||||
|
||||
const paidMedia = buildPaidMedia(media);
|
||||
if (paidMedia) return { paidMedia };
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -202,6 +208,7 @@ export function buildVideoFromDocument(document: GramJs.Document, isSpoiler?: bo
|
||||
} = videoAttr;
|
||||
|
||||
return {
|
||||
mediaType: 'video',
|
||||
id: String(id),
|
||||
mimeType,
|
||||
duration,
|
||||
@ -241,6 +248,7 @@ export function buildAudioFromDocument(document: GramJs.Document): ApiAudio | un
|
||||
} = audioAttributes;
|
||||
|
||||
return {
|
||||
mediaType: 'audio',
|
||||
id: String(id),
|
||||
mimeType,
|
||||
duration,
|
||||
@ -298,6 +306,7 @@ function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
|
||||
.map((thumb) => buildApiPhotoSize(thumb));
|
||||
|
||||
return {
|
||||
mediaType: 'audio',
|
||||
id: String(media.document.id),
|
||||
fileName: getFilenameFromDocument(media.document, 'audio'),
|
||||
thumbnailSizes,
|
||||
@ -342,7 +351,9 @@ function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
|
||||
const { duration, waveform } = audioAttribute;
|
||||
|
||||
return {
|
||||
mediaType: 'voice',
|
||||
id: String(media.document.id),
|
||||
size: media.document.size.toJSNumber(),
|
||||
duration,
|
||||
waveform: waveform ? Array.from(waveform) : undefined,
|
||||
};
|
||||
@ -374,7 +385,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
||||
}
|
||||
}
|
||||
|
||||
let mediaType: ApiDocument['mediaType'] | undefined;
|
||||
let innerMediaType: ApiDocument['innerMediaType'] | undefined;
|
||||
let mediaSize: ApiDocument['mediaSize'] | undefined;
|
||||
if (photoSize) {
|
||||
mediaSize = {
|
||||
@ -383,7 +394,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
||||
};
|
||||
|
||||
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType)) {
|
||||
mediaType = 'photo';
|
||||
innerMediaType = 'photo';
|
||||
|
||||
const imageAttribute = attributes
|
||||
.find((a: any): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize);
|
||||
@ -396,7 +407,7 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
||||
};
|
||||
}
|
||||
} else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
|
||||
mediaType = 'video';
|
||||
innerMediaType = 'video';
|
||||
const videoAttribute = attributes
|
||||
.find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);
|
||||
|
||||
@ -411,13 +422,14 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: 'document',
|
||||
id: String(id),
|
||||
size: size.toJSNumber(),
|
||||
mimeType,
|
||||
timestamp: date,
|
||||
fileName: getFilenameFromDocument(document),
|
||||
thumbnail,
|
||||
mediaType,
|
||||
innerMediaType,
|
||||
mediaSize,
|
||||
};
|
||||
}
|
||||
@ -432,7 +444,11 @@ function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined {
|
||||
} = media;
|
||||
|
||||
return {
|
||||
firstName, lastName, phoneNumber, userId: buildApiPeerId(userId, 'user'),
|
||||
mediaType: 'contact',
|
||||
firstName,
|
||||
lastName,
|
||||
phoneNumber,
|
||||
userId: buildApiPeerId(userId, 'user'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -470,7 +486,7 @@ function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | u
|
||||
|
||||
function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined {
|
||||
const point = buildGeoPoint(media.geo);
|
||||
return point && { type: 'geo', geo: point };
|
||||
return point && { mediaType: 'geo', geo: point };
|
||||
}
|
||||
|
||||
function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined {
|
||||
@ -479,7 +495,7 @@ function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined {
|
||||
} = media;
|
||||
const point = buildGeoPoint(geo);
|
||||
return point && {
|
||||
type: 'venue',
|
||||
mediaType: 'venue',
|
||||
geo: point,
|
||||
title,
|
||||
provider,
|
||||
@ -493,7 +509,7 @@ function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefine
|
||||
const { geo, period, heading } = media;
|
||||
const point = buildGeoPoint(geo);
|
||||
return point && {
|
||||
type: 'geoLive',
|
||||
mediaType: 'geoLive',
|
||||
geo: point,
|
||||
period,
|
||||
heading,
|
||||
@ -530,6 +546,7 @@ function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined {
|
||||
const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined;
|
||||
|
||||
return {
|
||||
mediaType: 'game',
|
||||
id: id.toString(),
|
||||
accessHash: accessHash.toString(),
|
||||
shortName,
|
||||
@ -556,6 +573,7 @@ function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefi
|
||||
const channelIds = channels.map((channel) => buildApiPeerId(channel, 'channel'));
|
||||
|
||||
return {
|
||||
mediaType: 'giveaway',
|
||||
channelIds,
|
||||
months,
|
||||
quantity,
|
||||
@ -583,6 +601,7 @@ function buildGiveawayResults(media: GramJs.MessageMediaGiveawayResults): ApiGiv
|
||||
const winnerIds = winners.map((winner) => buildApiPeerId(winner, 'user'));
|
||||
|
||||
return {
|
||||
mediaType: 'giveawayResults',
|
||||
months,
|
||||
untilDate,
|
||||
isOnlyForNewSubscribers: onlyNewSubscribers,
|
||||
@ -604,7 +623,12 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag
|
||||
|
||||
const peerId = getApiChatIdFromMtpPeer(media.peer);
|
||||
|
||||
return { id: media.id, peerId, ...(media.viaMention && { isMention: true }) };
|
||||
return {
|
||||
mediaType: 'storyData',
|
||||
id: media.id,
|
||||
peerId,
|
||||
...(media.viaMention && { isMention: true }),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
||||
@ -615,6 +639,7 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
|
||||
}));
|
||||
|
||||
return {
|
||||
mediaType: 'poll',
|
||||
id: String(id),
|
||||
summary: {
|
||||
isPublic: poll.publicVoters,
|
||||
@ -641,6 +666,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
|
||||
? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined;
|
||||
|
||||
return {
|
||||
mediaType: 'invoice',
|
||||
title,
|
||||
text,
|
||||
photo: buildApiWebDocument(photo),
|
||||
@ -722,6 +748,7 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: 'webpage',
|
||||
id: Number(id),
|
||||
...pick(media.webpage, [
|
||||
'url',
|
||||
@ -741,6 +768,40 @@ export function buildWebPage(media: GramJs.TypeMessageMedia): ApiWebPage | undef
|
||||
};
|
||||
}
|
||||
|
||||
function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined {
|
||||
if (!(media instanceof GramJs.MessageMediaPaidMedia)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { starsAmount, extendedMedia } = media;
|
||||
|
||||
const isBought = extendedMedia[0] instanceof GramJs.MessageExtendedMedia;
|
||||
|
||||
if (isBought) {
|
||||
return {
|
||||
mediaType: 'paidMedia',
|
||||
starsAmount: starsAmount.toJSNumber(),
|
||||
isBought,
|
||||
extendedMedia: extendedMedia
|
||||
.filter((paidMedia): paidMedia is GramJs.MessageExtendedMedia => (
|
||||
paidMedia instanceof GramJs.MessageExtendedMedia
|
||||
))
|
||||
.map((paidMedia) => buildMessageMediaContent(paidMedia.media))
|
||||
.filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: 'paidMedia',
|
||||
starsAmount: starsAmount.toJSNumber(),
|
||||
extendedMedia: extendedMedia
|
||||
.filter((paidMedia): paidMedia is GramJs.MessageExtendedMediaPreview => (
|
||||
paidMedia instanceof GramJs.MessageExtendedMediaPreview
|
||||
))
|
||||
.map((paidMedia) => buildApiMessageExtendedMediaPreview(paidMedia)),
|
||||
};
|
||||
}
|
||||
|
||||
function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') {
|
||||
const { mimeType, attributes } = document;
|
||||
const filenameAttribute = attributes
|
||||
@ -757,12 +818,13 @@ function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file'
|
||||
|
||||
export function buildApiMessageExtendedMediaPreview(
|
||||
preview: GramJs.MessageExtendedMediaPreview,
|
||||
): ApiMessageExtendedMediaPreview {
|
||||
): ApiMediaExtendedPreview {
|
||||
const {
|
||||
w, h, thumb, videoDuration,
|
||||
} = preview;
|
||||
|
||||
return {
|
||||
mediaType: 'extendedMediaPreview',
|
||||
width: w,
|
||||
height: h,
|
||||
duration: videoDuration,
|
||||
@ -783,6 +845,7 @@ export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDo
|
||||
const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h };
|
||||
|
||||
return {
|
||||
mediaType: 'webDocument',
|
||||
url,
|
||||
accessHash,
|
||||
size,
|
||||
@ -790,3 +853,16 @@ export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDo
|
||||
dimensions,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBoughtMediaContent(media: GramJs.TypeMessageExtendedMedia[]): MediaContent[] | undefined {
|
||||
const boughtMedia = media
|
||||
.filter((m): m is GramJs.MessageExtendedMedia => m instanceof GramJs.MessageExtendedMedia)
|
||||
.map((m) => buildMessageMediaContent(m.media))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!boughtMedia.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return boughtMedia;
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import type {
|
||||
ApiNewPoll,
|
||||
ApiPeer,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiQuickReply,
|
||||
ApiReplyInfo,
|
||||
ApiReplyKeyboard,
|
||||
@ -246,7 +247,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
|
||||
}
|
||||
|
||||
const {
|
||||
message, entities, replyTo, date,
|
||||
message, entities, replyTo, date, effect,
|
||||
} = draft;
|
||||
|
||||
const replyInfo = replyTo instanceof GramJs.InputReplyToMessage ? {
|
||||
@ -261,6 +262,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
|
||||
text: message ? buildMessageTextContent(message, entities) : undefined,
|
||||
replyInfo,
|
||||
date,
|
||||
effectId: effect?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -610,6 +612,7 @@ function buildAction(
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
text,
|
||||
type,
|
||||
targetUserIds,
|
||||
@ -777,13 +780,12 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool
|
||||
};
|
||||
}
|
||||
|
||||
function buildNewPoll(poll: ApiNewPoll, localId: number) {
|
||||
function buildNewPoll(poll: ApiNewPoll, localId: number): ApiPoll {
|
||||
return {
|
||||
poll: {
|
||||
id: String(localId),
|
||||
summary: pick(poll.summary, ['question', 'answers']),
|
||||
results: {},
|
||||
},
|
||||
mediaType: 'poll',
|
||||
id: String(localId),
|
||||
summary: pick(poll.summary, ['question', 'answers']),
|
||||
results: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -824,9 +826,12 @@ export function buildLocalMessage(
|
||||
...media,
|
||||
...(sticker && { sticker }),
|
||||
...(gif && { video: gif }),
|
||||
...(poll && buildNewPoll(poll, localId)),
|
||||
...(contact && { contact }),
|
||||
...(story && { storyData: story }),
|
||||
poll: poll && buildNewPoll(poll, localId),
|
||||
contact,
|
||||
storyData: story && {
|
||||
mediaType: 'storyData',
|
||||
...story,
|
||||
},
|
||||
},
|
||||
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
|
||||
isOutgoing: !isChannel,
|
||||
@ -981,10 +986,12 @@ export function buildUploadingMedia(
|
||||
const { width, height } = attachment.quick;
|
||||
return {
|
||||
photo: {
|
||||
mediaType: 'photo',
|
||||
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
||||
sizes: [],
|
||||
thumbnail: { width, height, dataUri: previewBlobUrl || blobUrl },
|
||||
blobUrl,
|
||||
date: Math.round(Date.now() / 1000),
|
||||
isSpoiler: shouldSendAsSpoiler,
|
||||
},
|
||||
};
|
||||
@ -993,6 +1000,7 @@ export function buildUploadingMedia(
|
||||
const { width, height, duration } = attachment.quick;
|
||||
return {
|
||||
video: {
|
||||
mediaType: 'video',
|
||||
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
||||
mimeType,
|
||||
duration: duration || 0,
|
||||
@ -1012,9 +1020,11 @@ export function buildUploadingMedia(
|
||||
const { data: inputWaveform } = interpolateArray(waveform, INPUT_WAVEFORM_LENGTH);
|
||||
return {
|
||||
voice: {
|
||||
mediaType: 'voice',
|
||||
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
||||
duration,
|
||||
waveform: inputWaveform,
|
||||
size,
|
||||
},
|
||||
ttlSeconds,
|
||||
};
|
||||
@ -1023,6 +1033,7 @@ export function buildUploadingMedia(
|
||||
const { duration, performer, title } = audio || {};
|
||||
return {
|
||||
audio: {
|
||||
mediaType: 'audio',
|
||||
id: LOCAL_MEDIA_UPLOADING_TEMP_ID,
|
||||
mimeType,
|
||||
fileName,
|
||||
@ -1036,6 +1047,7 @@ export function buildUploadingMedia(
|
||||
}
|
||||
return {
|
||||
document: {
|
||||
mediaType: 'document',
|
||||
mimeType,
|
||||
fileName,
|
||||
size,
|
||||
|
||||
@ -12,12 +12,13 @@ import type {
|
||||
ApiStarsTransaction,
|
||||
ApiStarsTransactionPeer,
|
||||
ApiStarTopupOption,
|
||||
BoughtPaidMedia,
|
||||
} from '../../types';
|
||||
|
||||
import { addWebDocumentToLocalDb } from '../helpers';
|
||||
import { buildApiMessageEntity } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument } from './messageContent';
|
||||
import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildPrepaidGiveaway, buildStatisticsPercentage } from './statistics';
|
||||
|
||||
@ -214,6 +215,7 @@ export function buildApiInvoiceFromForm(form: GramJs.payments.TypePaymentForm):
|
||||
const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0);
|
||||
|
||||
return {
|
||||
mediaType: 'invoice',
|
||||
text,
|
||||
title,
|
||||
photo: buildApiWebDocument(photo),
|
||||
@ -398,6 +400,10 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
return { type: 'fragment' };
|
||||
}
|
||||
|
||||
if (peer instanceof GramJs.StarsTransactionPeerAds) {
|
||||
return { type: 'ads' };
|
||||
}
|
||||
|
||||
if (peer instanceof GramJs.StarsTransactionPeer) {
|
||||
return { type: 'peer', id: getApiChatIdFromMtpPeer(peer.peer) };
|
||||
}
|
||||
@ -407,13 +413,16 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
|
||||
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
|
||||
const {
|
||||
date, id, peer, stars, description, photo, title, refund,
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending,
|
||||
} = transaction;
|
||||
|
||||
if (photo) {
|
||||
addWebDocumentToLocalDb(photo);
|
||||
}
|
||||
|
||||
const boughtExtendedMedia = extendedMedia?.map((m) => buildMessageMediaContent(m))
|
||||
.filter(Boolean) as BoughtPaidMedia[];
|
||||
|
||||
return {
|
||||
id,
|
||||
date,
|
||||
@ -423,6 +432,10 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
description,
|
||||
photo: photo && buildApiWebDocument(photo),
|
||||
isRefund: refund,
|
||||
hasFailed: failed,
|
||||
isPending: pending,
|
||||
messageId: msgId,
|
||||
extendedMedia: boughtExtendedMedia,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -150,7 +150,7 @@ export function buildApiStealthMode(stealthMode: GramJs.TypeStoriesStealthMode):
|
||||
|
||||
function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinates): ApiMediaAreaCoordinates {
|
||||
const {
|
||||
x, y, w, h, rotation,
|
||||
x, y, w, h, rotation, radius,
|
||||
} = coordinates;
|
||||
|
||||
return {
|
||||
@ -159,6 +159,7 @@ function buildApiMediaAreaCoordinates(coordinates: GramJs.TypeMediaAreaCoordinat
|
||||
width: w,
|
||||
height: h,
|
||||
rotation,
|
||||
radius,
|
||||
};
|
||||
}
|
||||
|
||||
@ -218,6 +219,16 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un
|
||||
};
|
||||
}
|
||||
|
||||
if (area instanceof GramJs.MediaAreaUrl) {
|
||||
const { coordinates, url } = area;
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
coordinates: buildApiMediaAreaCoordinates(coordinates),
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument,
|
||||
.some(({ type }) => type === 'f');
|
||||
|
||||
return {
|
||||
mediaType: 'sticker',
|
||||
id: String(document.id),
|
||||
stickerSetInfo,
|
||||
emoji,
|
||||
|
||||
@ -353,36 +353,6 @@ export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMess
|
||||
}
|
||||
}
|
||||
|
||||
export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServiceNotification) {
|
||||
const { media } = message;
|
||||
if (!media) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
media instanceof GramJs.MessageMediaPhoto
|
||||
|| media instanceof GramJs.MessageMediaDocument
|
||||
|| (
|
||||
media instanceof GramJs.MessageMediaWebPage
|
||||
&& media.webpage instanceof GramJs.WebPage
|
||||
&& (
|
||||
media.webpage.photo instanceof GramJs.Photo || (
|
||||
media.webpage.document instanceof GramJs.Document
|
||||
)
|
||||
)
|
||||
) || (
|
||||
media instanceof GramJs.MessageMediaGame
|
||||
&& (media.game.document instanceof GramJs.Document || media.game.photo instanceof GramJs.Photo)
|
||||
) || (
|
||||
media instanceof GramJs.MessageMediaInvoice && (media.photo || media.extendedMedia)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isServiceMessageWithMedia(message: GramJs.MessageService) {
|
||||
return 'photo' in message.action && message.action.photo instanceof GramJs.Photo;
|
||||
}
|
||||
|
||||
export function buildChatPhotoForLocalDb(photo: GramJs.TypePhoto) {
|
||||
if (photo instanceof GramJs.PhotoEmpty) {
|
||||
return new GramJs.ChatPhotoEmpty();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Api as GramJs } from '../../lib/gramjs';
|
||||
|
||||
import type { StoryRepairInfo } from './localDb';
|
||||
import type { RepairInfo } from './localDb';
|
||||
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
||||
import localDb from './localDb';
|
||||
@ -34,57 +34,76 @@ export function isChatFolder(
|
||||
return filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist;
|
||||
}
|
||||
|
||||
export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) {
|
||||
const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`;
|
||||
export function addMessageToLocalDb(message: GramJs.TypeMessage | GramJs.TypeSponsoredMessage) {
|
||||
if (message instanceof GramJs.Message) {
|
||||
if (message.media) addMediaToLocalDb(message.media, message);
|
||||
|
||||
let mockMessage = message;
|
||||
if (message instanceof GramJs.Message
|
||||
&& message.media instanceof GramJs.MessageMediaInvoice
|
||||
&& message.media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
mockMessage = new GramJs.Message({
|
||||
...message,
|
||||
media: message.media.extendedMedia.media,
|
||||
});
|
||||
}
|
||||
|
||||
localDb.messages[messageFullId] = mockMessage;
|
||||
|
||||
if (mockMessage instanceof GramJs.Message) {
|
||||
if (mockMessage.media) addMediaToLocalDb(mockMessage.media);
|
||||
|
||||
if (mockMessage.replyTo instanceof GramJs.MessageReplyHeader && mockMessage.replyTo.replyMedia) {
|
||||
addMediaToLocalDb(mockMessage.replyTo.replyMedia);
|
||||
if (message.replyTo instanceof GramJs.MessageReplyHeader && message.replyTo.replyMedia) {
|
||||
addMediaToLocalDb(message.replyTo.replyMedia, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (mockMessage instanceof GramJs.MessageService && 'photo' in mockMessage.action) {
|
||||
addPhotoToLocalDb(mockMessage.action.photo);
|
||||
if (message instanceof GramJs.MessageService && 'photo' in message.action) {
|
||||
const photo = addMessageRepairInfo(message.action.photo, message);
|
||||
addPhotoToLocalDb(photo);
|
||||
}
|
||||
|
||||
if (message instanceof GramJs.SponsoredMessage && message.photo) {
|
||||
addPhotoToLocalDb(message.photo);
|
||||
}
|
||||
}
|
||||
|
||||
function addMediaToLocalDb(media: GramJs.TypeMessageMedia) {
|
||||
if (media instanceof GramJs.MessageMediaDocument
|
||||
&& media.document instanceof GramJs.Document
|
||||
) {
|
||||
localDb.documents[String(media.document.id)] = media.document;
|
||||
export function addMediaToLocalDb(media: GramJs.TypeMessageMedia, message?: GramJs.TypeMessage) {
|
||||
if (media instanceof GramJs.MessageMediaDocument && media.document) {
|
||||
const document = addMessageRepairInfo(media.document, message);
|
||||
addDocumentToLocalDb(document);
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaWebPage
|
||||
&& media.webpage instanceof GramJs.WebPage
|
||||
&& media.webpage.document instanceof GramJs.Document
|
||||
) {
|
||||
localDb.documents[String(media.webpage.document.id)] = media.webpage.document;
|
||||
if (media.webpage.document) {
|
||||
const document = addMessageRepairInfo(media.webpage.document, message);
|
||||
addDocumentToLocalDb(document);
|
||||
}
|
||||
if (media.webpage.photo) {
|
||||
const photo = addMessageRepairInfo(media.webpage.photo, message);
|
||||
addPhotoToLocalDb(photo);
|
||||
}
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaGame) {
|
||||
if (media.game.document instanceof GramJs.Document) {
|
||||
localDb.documents[String(media.game.document.id)] = media.game.document;
|
||||
if (media.game.document) {
|
||||
const document = addMessageRepairInfo(media.game.document, message);
|
||||
addDocumentToLocalDb(document);
|
||||
}
|
||||
addPhotoToLocalDb(media.game.photo);
|
||||
|
||||
const photo = addMessageRepairInfo(media.game.photo, message);
|
||||
addPhotoToLocalDb(photo);
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaInvoice && media.photo) {
|
||||
addWebDocumentToLocalDb(media.photo);
|
||||
if (media instanceof GramJs.MessageMediaPhoto && media.photo) {
|
||||
const photo = addMessageRepairInfo(media.photo, message);
|
||||
addPhotoToLocalDb(photo);
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaInvoice) {
|
||||
if (media.photo) {
|
||||
const photo = addMessageRepairInfo(media.photo, message);
|
||||
addWebDocumentToLocalDb(photo);
|
||||
}
|
||||
|
||||
if (media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
addMediaToLocalDb(media.extendedMedia.media, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaPaidMedia) {
|
||||
media.extendedMedia.forEach((extendedMedia) => {
|
||||
if (extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
addMediaToLocalDb(extendedMedia.media, message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,27 +112,20 @@ export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storyData = {
|
||||
id: story.id,
|
||||
peerId,
|
||||
};
|
||||
|
||||
if (story.media instanceof GramJs.MessageMediaPhoto) {
|
||||
const photo = story.media.photo as GramJs.Photo & StoryRepairInfo;
|
||||
photo.storyData = storyData;
|
||||
if (story.media instanceof GramJs.MessageMediaPhoto && story.media.photo) {
|
||||
const photo = addStoryRepairInfo(story.media.photo, peerId, story);
|
||||
addPhotoToLocalDb(photo);
|
||||
}
|
||||
|
||||
if (story.media instanceof GramJs.MessageMediaDocument) {
|
||||
if (story.media.document instanceof GramJs.Document) {
|
||||
const doc = story.media.document as GramJs.Document & StoryRepairInfo;
|
||||
doc.storyData = storyData;
|
||||
localDb.documents[String(story.media.document.id)] = doc;
|
||||
const doc = addStoryRepairInfo(story.media.document, peerId, story);
|
||||
addDocumentToLocalDb(doc);
|
||||
}
|
||||
|
||||
if (story.media.altDocument instanceof GramJs.Document) {
|
||||
const doc = story.media.altDocument as GramJs.Document & StoryRepairInfo;
|
||||
doc.storyData = storyData;
|
||||
localDb.documents[String(story.media.altDocument.id)] = doc;
|
||||
const doc = addStoryRepairInfo(story.media.altDocument, peerId, story);
|
||||
addDocumentToLocalDb(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -124,6 +136,42 @@ export function addPhotoToLocalDb(photo: GramJs.TypePhoto) {
|
||||
}
|
||||
}
|
||||
|
||||
export function addDocumentToLocalDb(document: GramJs.TypeDocument) {
|
||||
if (document instanceof GramJs.Document) {
|
||||
localDb.documents[String(document.id)] = document;
|
||||
}
|
||||
}
|
||||
|
||||
export function addStoryRepairInfo<T extends GramJs.TypeDocument | GramJs.TypeWebDocument | GramJs.TypePhoto>(
|
||||
media: T, peerId: string, story: GramJs.TypeStoryItem,
|
||||
) : T & RepairInfo {
|
||||
if (!(media instanceof GramJs.Document && media instanceof GramJs.Photo)) return media;
|
||||
const repairableMedia = media as T & RepairInfo;
|
||||
repairableMedia.localRepairInfo = {
|
||||
type: 'story',
|
||||
peerId,
|
||||
id: story.id,
|
||||
};
|
||||
return repairableMedia;
|
||||
}
|
||||
|
||||
export function addMessageRepairInfo<T extends GramJs.TypeDocument | GramJs.TypeWebDocument | GramJs.TypePhoto>(
|
||||
media: T, message?: GramJs.TypeMessage,
|
||||
) : T & RepairInfo {
|
||||
if (!message?.peerId) return media;
|
||||
if (!(media instanceof GramJs.Document && media instanceof GramJs.Photo && media instanceof GramJs.WebDocument)) {
|
||||
return media;
|
||||
}
|
||||
|
||||
const repairableMedia = media as T & RepairInfo;
|
||||
repairableMedia.localRepairInfo = {
|
||||
type: 'message',
|
||||
peerId: getApiChatIdFromMtpPeer(message.peerId),
|
||||
id: message.id,
|
||||
};
|
||||
return repairableMedia;
|
||||
}
|
||||
|
||||
export function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel) {
|
||||
const id = buildApiPeerId(chat.id, chat instanceof GramJs.Chat ? 'chat' : 'channel');
|
||||
const storedChat = localDb.chats[id];
|
||||
@ -138,6 +186,11 @@ export function addChatToLocalDb(chat: GramJs.Chat | GramJs.Channel) {
|
||||
export function addUserToLocalDb(user: GramJs.User) {
|
||||
const id = buildApiPeerId(user.id, 'user');
|
||||
const storedUser = localDb.users[id];
|
||||
|
||||
if (user.photo instanceof GramJs.Photo) {
|
||||
addPhotoToLocalDb(user.photo);
|
||||
}
|
||||
|
||||
if (storedUser && !storedUser.min && user.min) return;
|
||||
|
||||
localDb.users[id] = user;
|
||||
@ -157,24 +210,6 @@ export function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) {
|
||||
localDb.webDocuments[webDocument.url] = webDocument;
|
||||
}
|
||||
|
||||
export function swapLocalInvoiceMedia(
|
||||
chatId: string, messageId: number, extendedMedia: GramJs.TypeMessageExtendedMedia,
|
||||
) {
|
||||
const localMessage = localDb.messages[`${chatId}-${messageId}`];
|
||||
if (!(localMessage instanceof GramJs.Message) || !localMessage.media) return;
|
||||
|
||||
if (extendedMedia instanceof GramJs.MessageExtendedMediaPreview) {
|
||||
if (!(localMessage.media instanceof GramJs.MessageMediaInvoice)) {
|
||||
return;
|
||||
}
|
||||
localMessage.media.extendedMedia = extendedMedia;
|
||||
}
|
||||
|
||||
if (extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
localMessage.media = extendedMedia.media;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeBytes(value: Buffer) {
|
||||
return String.fromCharCode(...value);
|
||||
}
|
||||
|
||||
@ -11,20 +11,28 @@ import { omitVirtualClassFields } from './apiBuilders/helpers';
|
||||
const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in self;
|
||||
|
||||
export type StoryRepairInfo = {
|
||||
storyData?: {
|
||||
peerId: string;
|
||||
id: number;
|
||||
};
|
||||
type: 'story';
|
||||
peerId: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type MessageRepairInfo = {
|
||||
type: 'message';
|
||||
peerId: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type RepairInfo = {
|
||||
localRepairInfo?: StoryRepairInfo | MessageRepairInfo;
|
||||
};
|
||||
|
||||
export interface LocalDb {
|
||||
// Used for loading avatars and media through in-memory Gram JS instances.
|
||||
chats: Record<string, GramJs.Chat | GramJs.Channel>;
|
||||
users: Record<string, GramJs.User>;
|
||||
messages: Record<string, GramJs.Message | GramJs.MessageService>;
|
||||
documents: Record<string, GramJs.Document & StoryRepairInfo>;
|
||||
documents: Record<string, GramJs.Document & RepairInfo>;
|
||||
stickerSets: Record<string, GramJs.StickerSet>;
|
||||
photos: Record<string, GramJs.Photo & StoryRepairInfo>;
|
||||
photos: Record<string, GramJs.Photo & RepairInfo>;
|
||||
webDocuments: Record<string, GramJs.TypeWebDocument>;
|
||||
commonBoxState: Record<string, number>;
|
||||
channelPtsById: Record<string, number>;
|
||||
|
||||
@ -34,9 +34,13 @@ import {
|
||||
generateRandomBigInt,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb, addUserToLocalDb, addWebDocumentToLocalDb, deserializeBytes,
|
||||
addDocumentToLocalDb,
|
||||
addEntitiesToLocalDb,
|
||||
addPhotoToLocalDb,
|
||||
addUserToLocalDb,
|
||||
addWebDocumentToLocalDb,
|
||||
deserializeBytes,
|
||||
} from '../helpers';
|
||||
import localDb from '../localDb';
|
||||
import { invokeRequest } from './client';
|
||||
|
||||
let onUpdate: OnApiUpdate;
|
||||
@ -209,7 +213,7 @@ export async function requestWebView({
|
||||
if (result instanceof GramJs.WebViewResultUrl) {
|
||||
return {
|
||||
url: result.url,
|
||||
queryId: result.queryId.toString(),
|
||||
queryId: result.queryId?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -555,14 +559,6 @@ function getInlineBotResultsNextOffset(username: string, nextOffset?: string) {
|
||||
return username === 'gif' && nextOffset === '0' ? '' : nextOffset;
|
||||
}
|
||||
|
||||
function addDocumentToLocalDb(document: GramJs.Document) {
|
||||
localDb.documents[String(document.id)] = document;
|
||||
}
|
||||
|
||||
function addPhotoToLocalDb(photo: GramJs.Photo) {
|
||||
localDb.photos[String(photo.id)] = photo;
|
||||
}
|
||||
|
||||
export function setBotInfo({
|
||||
bot,
|
||||
langCode,
|
||||
|
||||
@ -65,7 +65,6 @@ import {
|
||||
buildInputReplyTo,
|
||||
buildMtpMessageEntity,
|
||||
generateRandomBigInt,
|
||||
isMessageWithMedia,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
@ -74,7 +73,6 @@ import {
|
||||
deserializeBytes,
|
||||
isChatFolder,
|
||||
} from '../helpers';
|
||||
import localDb from '../localDb';
|
||||
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
|
||||
import {
|
||||
applyState, processAffectedHistory, updateChannelState,
|
||||
@ -518,8 +516,8 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
|
||||
translationsDisabled,
|
||||
} = result.fullChat;
|
||||
|
||||
if (chatPhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[chatPhoto.id.toString()] = chatPhoto;
|
||||
if (chatPhoto) {
|
||||
addPhotoToLocalDb(chatPhoto);
|
||||
}
|
||||
|
||||
const members = buildChatMembers(participants);
|
||||
@ -606,8 +604,8 @@ async function getFullChannelInfo(
|
||||
boostsUnrestrict,
|
||||
} = result.fullChat;
|
||||
|
||||
if (chatPhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[chatPhoto.id.toString()] = chatPhoto;
|
||||
if (chatPhoto) {
|
||||
addPhotoToLocalDb(chatPhoto);
|
||||
}
|
||||
|
||||
const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported
|
||||
@ -1555,9 +1553,7 @@ function updateLocalDb(result: (
|
||||
|
||||
if ('messages' in result) {
|
||||
result.messages.forEach((message) => {
|
||||
if (message instanceof GramJs.Message && isMessageWithMedia(message)) {
|
||||
addMessageToLocalDb(message);
|
||||
}
|
||||
addMessageToLocalDb(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,15 +20,15 @@ import {
|
||||
DEBUG, DEBUG_GRAMJS, IS_TEST, UPLOAD_WORKERS,
|
||||
} from '../../../config';
|
||||
import { pause } from '../../../util/schedulers';
|
||||
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
|
||||
import { buildApiMessage, setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
|
||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||
import { buildApiStory } from '../apiBuilders/stories';
|
||||
import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users';
|
||||
import { buildInputPeerFromLocalDb } from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log,
|
||||
addEntitiesToLocalDb, addMessageToLocalDb, addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log,
|
||||
} from '../helpers';
|
||||
import localDb, { clearLocalDb } from '../localDb';
|
||||
import localDb, { clearLocalDb, type RepairInfo } from '../localDb';
|
||||
import {
|
||||
getDifference,
|
||||
init as initUpdatesManager,
|
||||
@ -381,9 +381,6 @@ export async function fetchCurrentUser() {
|
||||
|
||||
const user = userFull.users[0];
|
||||
|
||||
if (user.photo instanceof GramJs.Photo) {
|
||||
localDb.photos[user.photo.id.toString()] = user.photo;
|
||||
}
|
||||
addUserToLocalDb(user);
|
||||
const currentUserFullInfo = buildApiUserFullInfo(userFull);
|
||||
const currentUser = buildApiUser(user)!;
|
||||
@ -441,62 +438,100 @@ export async function repairFileReference({
|
||||
if (!parsed) return undefined;
|
||||
|
||||
const {
|
||||
entityType, entityId, mediaMatchType,
|
||||
entityId, mediaMatchType,
|
||||
} = parsed;
|
||||
|
||||
if (mediaMatchType === 'document' || mediaMatchType === 'photo') {
|
||||
const entity = mediaMatchType === 'document' ? localDb.documents[entityId] : localDb.photos[entityId];
|
||||
if (!entity.storyData) return false;
|
||||
const peer = buildInputPeerFromLocalDb(entity.storyData.peerId);
|
||||
if (!peer) return false;
|
||||
if (mediaMatchType === 'document' || mediaMatchType === 'photo' || mediaMatchType === 'webDocument') {
|
||||
const entity = mediaMatchType === 'document'
|
||||
? localDb.documents[entityId] : mediaMatchType === 'webDocument'
|
||||
? localDb.webDocuments[entityId] : localDb.photos[entityId];
|
||||
if (!entity) return false;
|
||||
const repairableEntity = entity as RepairInfo;
|
||||
if (!repairableEntity.localRepairInfo) return false;
|
||||
const { localRepairInfo } = repairableEntity;
|
||||
|
||||
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
|
||||
peer,
|
||||
id: [entity.storyData.id],
|
||||
}));
|
||||
if (!result) return false;
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
result.stories.forEach((story) => addStoryToLocalDb(story, entity.storyData!.peerId));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entityType === 'msg') {
|
||||
const entity = localDb.messages[entityId]!;
|
||||
const messageId = entity.id;
|
||||
|
||||
const peer = 'channelId' in entity.peerId ? new GramJs.InputChannel({
|
||||
channelId: entity.peerId.channelId,
|
||||
accessHash: (localDb.chats[buildApiPeerId(entity.peerId.channelId, 'channel')] as GramJs.Channel).accessHash!,
|
||||
}) : undefined;
|
||||
const result = await invokeRequest(
|
||||
peer
|
||||
? new GramJs.channels.GetMessages({
|
||||
channel: peer,
|
||||
id: [new GramJs.InputMessageID({ id: messageId })],
|
||||
})
|
||||
: new GramJs.messages.GetMessages({
|
||||
id: [new GramJs.InputMessageID({ id: messageId })],
|
||||
}),
|
||||
);
|
||||
|
||||
if (!result || result instanceof GramJs.messages.MessagesNotModified) return false;
|
||||
|
||||
if (peer && 'pts' in result) {
|
||||
updateChannelState(buildApiPeerId(peer.channelId, 'channel'), result.pts);
|
||||
if (localRepairInfo.type === 'story') {
|
||||
const result = await repairStoryMedia(localRepairInfo.peerId, localRepairInfo.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
const message = result.messages[0];
|
||||
if (message instanceof GramJs.MessageEmpty) return false;
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
addMessageToLocalDb(message);
|
||||
return true;
|
||||
if (localRepairInfo.type === 'message') {
|
||||
const result = await repairMessageMedia(localRepairInfo.peerId, localRepairInfo.id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function repairMessageMedia(peerId: string, messageId: number) {
|
||||
const peer = buildInputPeerFromLocalDb(peerId);
|
||||
if (!peer) return false;
|
||||
const result = await invokeRequest(
|
||||
peer
|
||||
? new GramJs.channels.GetMessages({
|
||||
channel: peer,
|
||||
id: [new GramJs.InputMessageID({ id: messageId })],
|
||||
})
|
||||
: new GramJs.messages.GetMessages({
|
||||
id: [new GramJs.InputMessageID({ id: messageId })],
|
||||
}),
|
||||
{
|
||||
shouldIgnoreErrors: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result || result instanceof GramJs.messages.MessagesNotModified) return false;
|
||||
|
||||
if (peer && 'pts' in result) {
|
||||
updateChannelState(peerId, result.pts);
|
||||
}
|
||||
|
||||
const message = result.messages[0];
|
||||
if (message instanceof GramJs.MessageEmpty) return false;
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
addMessageToLocalDb(message);
|
||||
|
||||
const apiMessage = buildApiMessage(message);
|
||||
if (apiMessage) {
|
||||
onUpdate({
|
||||
'@type': 'updateMessage',
|
||||
chatId: apiMessage.chatId,
|
||||
id: apiMessage.id,
|
||||
message: apiMessage,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function repairStoryMedia(peerId: string, storyId: number) {
|
||||
const peer = buildInputPeerFromLocalDb(peerId);
|
||||
if (!peer) return false;
|
||||
|
||||
const result = await invokeRequest(new GramJs.stories.GetStoriesByID({
|
||||
peer,
|
||||
id: [storyId],
|
||||
}), {
|
||||
shouldIgnoreErrors: true,
|
||||
});
|
||||
if (!result) return false;
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
result.stories.forEach((story) => {
|
||||
addStoryToLocalDb(story, peerId);
|
||||
|
||||
const apiStory = buildApiStory(peerId, story);
|
||||
if (!apiStory || 'isDeleted' in apiStory) return;
|
||||
onUpdate({
|
||||
'@type': 'updateStory',
|
||||
peerId,
|
||||
story: apiStory,
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setForceHttpTransport(forceHttpTransport: boolean) {
|
||||
client.setForceHttpTransport(forceHttpTransport);
|
||||
}
|
||||
|
||||
@ -17,11 +17,12 @@ import * as cacheApi from '../../../util/cacheApi';
|
||||
import { getEntityTypeById } from '../gramjsBuilders';
|
||||
import localDb from '../localDb';
|
||||
|
||||
const MEDIA_ENTITY_TYPES = new Set([
|
||||
'msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document', 'videoAvatar',
|
||||
const MEDIA_ENTITY_TYPES: Set<EntityType> = new Set([
|
||||
'sticker', 'wallpaper', 'photo', 'webDocument', 'document',
|
||||
]);
|
||||
|
||||
const JPEG_SIZE_TYPES = new Set(['s', 'm', 'x', 'y', 'w', 'a', 'b', 'c', 'd']);
|
||||
const MP4_SIZES_TYPES = new Set(['u', 'v']);
|
||||
|
||||
export default async function downloadMedia(
|
||||
{
|
||||
@ -66,8 +67,8 @@ export default async function downloadMedia(
|
||||
}
|
||||
|
||||
export type EntityType = (
|
||||
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
||||
'document' | 'staticMap' | 'videoAvatar'
|
||||
'sticker' | 'wallpaper' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
||||
'document' | 'staticMap'
|
||||
);
|
||||
|
||||
async function download(
|
||||
@ -118,15 +119,11 @@ async function download(
|
||||
case 'user':
|
||||
entity = localDb.users[entityId];
|
||||
break;
|
||||
case 'msg':
|
||||
entity = localDb.messages[entityId];
|
||||
break;
|
||||
case 'sticker':
|
||||
case 'gif':
|
||||
case 'wallpaper':
|
||||
case 'document':
|
||||
entity = localDb.documents[entityId];
|
||||
break;
|
||||
case 'videoAvatar':
|
||||
case 'photo':
|
||||
entity = localDb.photos[entityId];
|
||||
break;
|
||||
@ -136,9 +133,6 @@ async function download(
|
||||
case 'webDocument':
|
||||
entity = localDb.webDocuments[entityId];
|
||||
break;
|
||||
case 'document':
|
||||
entity = localDb.documents[entityId];
|
||||
break;
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
@ -152,36 +146,18 @@ async function download(
|
||||
let mimeType;
|
||||
let fullSize;
|
||||
|
||||
if (entity instanceof GramJs.MessageService && entity.action instanceof GramJs.MessageActionSuggestProfilePhoto) {
|
||||
if (sizeType && JPEG_SIZE_TYPES.has(sizeType)) {
|
||||
mimeType = 'image/jpeg';
|
||||
} else if (entity instanceof GramJs.Message) {
|
||||
mimeType = getMessageMediaMimeType(entity, sizeType);
|
||||
if (entity.media instanceof GramJs.MessageMediaDocument && entity.media.document instanceof GramJs.Document) {
|
||||
fullSize = entity.media.document.size.toJSNumber();
|
||||
}
|
||||
if (entity.media instanceof GramJs.MessageMediaWebPage
|
||||
&& entity.media.webpage instanceof GramJs.WebPage
|
||||
&& entity.media.webpage.document instanceof GramJs.Document) {
|
||||
fullSize = entity.media.webpage.document.size.toJSNumber();
|
||||
}
|
||||
} else if (sizeType && MP4_SIZES_TYPES.has(sizeType)) {
|
||||
mimeType = 'video/mp4';
|
||||
} else if (entity instanceof GramJs.Photo) {
|
||||
if (entityType === 'videoAvatar') {
|
||||
mimeType = 'video/mp4';
|
||||
} else {
|
||||
mimeType = 'image/jpeg';
|
||||
}
|
||||
} else if (entityType === 'sticker' && sizeType) {
|
||||
mimeType = (entity as GramJs.Document).mimeType;
|
||||
} else if (entityType === 'webDocument') {
|
||||
mimeType = (entity as GramJs.TypeWebDocument).mimeType;
|
||||
fullSize = (entity as GramJs.TypeWebDocument).size;
|
||||
} else {
|
||||
if (JPEG_SIZE_TYPES.has(sizeType || '')) {
|
||||
mimeType = 'image/jpeg';
|
||||
} else {
|
||||
mimeType = (entity as GramJs.Document).mimeType;
|
||||
}
|
||||
fullSize = (entity as GramJs.Document).size.toJSNumber();
|
||||
mimeType = 'image/jpeg';
|
||||
} else if (entity instanceof GramJs.WebDocument) {
|
||||
mimeType = entity.mimeType;
|
||||
fullSize = entity.size;
|
||||
} else if (entity instanceof GramJs.Document) {
|
||||
mimeType = entity.mimeType;
|
||||
fullSize = entity.size.toJSNumber();
|
||||
}
|
||||
|
||||
// Prevent HTML-in-video attacks
|
||||
@ -203,41 +179,6 @@ async function download(
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageMediaMimeType(message: GramJs.Message, sizeType?: string) {
|
||||
if (!message || !message.media) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (message.media instanceof GramJs.MessageMediaPhoto) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
if (message.media instanceof GramJs.MessageMediaGeo
|
||||
|| message.media instanceof GramJs.MessageMediaVenue
|
||||
|| message.media instanceof GramJs.MessageMediaGeoLive) {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
if (message.media instanceof GramJs.MessageMediaDocument) {
|
||||
const document = message.media.document;
|
||||
if (document instanceof GramJs.Document) {
|
||||
return document.mimeType;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.media instanceof GramJs.MessageMediaWebPage
|
||||
&& message.media.webpage instanceof GramJs.WebPage
|
||||
&& message.media.webpage.document instanceof GramJs.Document) {
|
||||
if (sizeType) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
return message.media.webpage.document.mimeType;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-async-without-await/no-async-without-await
|
||||
async function parseMedia(
|
||||
data: Buffer, mediaFormat: ApiMediaFormat, mimeType?: string,
|
||||
@ -294,7 +235,7 @@ export function parseMediaUrl(url: string) {
|
||||
? url.match(/(webDocument):(.+)/)
|
||||
: url.match(
|
||||
// eslint-disable-next-line max-len
|
||||
/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|document|videoAvatar)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/,
|
||||
/(avatar|profile|photo|stickerSet|sticker|wallpaper|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/,
|
||||
);
|
||||
if (!mediaMatch) {
|
||||
return undefined;
|
||||
|
||||
@ -77,8 +77,6 @@ import {
|
||||
buildSendMessageAction,
|
||||
generateRandomBigInt,
|
||||
getEntityTypeById,
|
||||
isMessageWithMedia,
|
||||
isServiceMessageWithMedia,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
@ -239,9 +237,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (mtpMessage instanceof GramJs.Message) {
|
||||
addMessageToLocalDb(mtpMessage);
|
||||
}
|
||||
addMessageToLocalDb(mtpMessage);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
@ -1576,11 +1572,7 @@ function updateLocalDb(result: (
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
|
||||
result.messages.forEach((message) => {
|
||||
if ((message instanceof GramJs.Message && isMessageWithMedia(message))
|
||||
|| (message instanceof GramJs.MessageService && isServiceMessageWithMedia(message))
|
||||
) {
|
||||
addMessageToLocalDb(message);
|
||||
}
|
||||
addMessageToLocalDb(message);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1924,9 +1916,7 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU
|
||||
}
|
||||
|
||||
const mtpMessage = buildMessageFromUpdate(messageUpdate.id, localMessage.chatId, messageUpdate);
|
||||
if (isMessageWithMedia(mtpMessage)) {
|
||||
addMessageToLocalDb(mtpMessage);
|
||||
}
|
||||
addMessageToLocalDb(mtpMessage);
|
||||
}
|
||||
|
||||
// Edge case for "Send When Online"
|
||||
|
||||
@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiInputStorePaymentPurpose, ApiPeer, ApiRequestInputInvoice,
|
||||
ApiThemeParameters,
|
||||
OnApiUpdate,
|
||||
} from '../../types';
|
||||
|
||||
@ -25,7 +26,7 @@ import {
|
||||
} from '../apiBuilders/payments';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import {
|
||||
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildShippingInfo,
|
||||
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
@ -156,9 +157,10 @@ export async function sendStarPaymentForm({
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) {
|
||||
export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) {
|
||||
const result = await invokeRequest(new GramJs.payments.GetPaymentForm({
|
||||
invoice: buildInputInvoice(inputInvoice),
|
||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
|
||||
@ -52,21 +52,21 @@ export async function fetchFullUser({
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
|
||||
if (result.fullUser.profilePhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[result.fullUser.profilePhoto.id.toString()] = result.fullUser.profilePhoto;
|
||||
if (result.fullUser.profilePhoto) {
|
||||
addPhotoToLocalDb(result.fullUser.profilePhoto);
|
||||
}
|
||||
|
||||
if (result.fullUser.personalPhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[result.fullUser.personalPhoto.id.toString()] = result.fullUser.personalPhoto;
|
||||
if (result.fullUser.personalPhoto) {
|
||||
addPhotoToLocalDb(result.fullUser.personalPhoto);
|
||||
}
|
||||
|
||||
if (result.fullUser.fallbackPhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[result.fullUser.fallbackPhoto.id.toString()] = result.fullUser.fallbackPhoto;
|
||||
if (result.fullUser.fallbackPhoto) {
|
||||
addPhotoToLocalDb(result.fullUser.fallbackPhoto);
|
||||
}
|
||||
|
||||
const botInfo = result.fullUser.botInfo;
|
||||
if (botInfo?.descriptionPhoto instanceof GramJs.Photo) {
|
||||
localDb.photos[botInfo.descriptionPhoto.id.toString()] = botInfo.descriptionPhoto;
|
||||
if (botInfo?.descriptionPhoto) {
|
||||
addPhotoToLocalDb(botInfo.descriptionPhoto);
|
||||
}
|
||||
if (botInfo?.descriptionDocument instanceof GramJs.Document) {
|
||||
localDb.documents[botInfo.descriptionDocument.id.toString()] = botInfo.descriptionDocument;
|
||||
|
||||
@ -2,8 +2,8 @@ import { Api as GramJs, connection } from '../../../lib/gramjs';
|
||||
|
||||
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
||||
import type {
|
||||
ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped,
|
||||
ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate,
|
||||
ApiMessage, ApiStory, ApiStorySkipped,
|
||||
ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate,
|
||||
} from '../../types';
|
||||
|
||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
|
||||
@ -29,7 +29,7 @@ import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuild
|
||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||
import {
|
||||
buildApiMessageExtendedMediaPreview,
|
||||
buildMessageMediaContent,
|
||||
buildBoughtMediaContent,
|
||||
buildPoll,
|
||||
buildPollResults,
|
||||
} from '../apiBuilders/messageContent';
|
||||
@ -61,7 +61,6 @@ import {
|
||||
import {
|
||||
buildChatPhotoForLocalDb,
|
||||
buildMessageFromUpdate,
|
||||
isMessageWithMedia,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
@ -72,7 +71,6 @@ import {
|
||||
log,
|
||||
resolveMessageApiChatId,
|
||||
serializeBytes,
|
||||
swapLocalInvoiceMedia,
|
||||
} from '../helpers';
|
||||
import localDb from '../localDb';
|
||||
import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from '../scheduleUnmute';
|
||||
@ -84,8 +82,6 @@ export type Update = (
|
||||
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
||||
) | typeof connection.UpdateConnectionState | UpdatePts | LocalUpdatePremiumFloodWait;
|
||||
|
||||
const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000;
|
||||
|
||||
let onUpdate: OnApiUpdate;
|
||||
|
||||
export function init(_onUpdate: OnApiUpdate) {
|
||||
@ -205,12 +201,7 @@ export function updater(update: Update) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((update.message instanceof GramJs.Message && isMessageWithMedia(update.message))
|
||||
|| (update.message instanceof GramJs.MessageService
|
||||
&& update.message.action instanceof GramJs.MessageActionSuggestProfilePhoto)
|
||||
) {
|
||||
addMessageToLocalDb(update.message);
|
||||
}
|
||||
addMessageToLocalDb(update.message);
|
||||
|
||||
message = buildApiMessage(update.message)!;
|
||||
dispatchThreadInfoUpdates([update.message]);
|
||||
@ -385,9 +376,7 @@ export function updater(update: Update) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) {
|
||||
addMessageToLocalDb(update.message);
|
||||
}
|
||||
addMessageToLocalDb(update.message);
|
||||
|
||||
// Workaround for a weird server behavior when own message is marked as incoming
|
||||
const message = omit(buildApiMessage(update.message)!, ['isOutgoing']);
|
||||
@ -407,28 +396,35 @@ export function updater(update: Update) {
|
||||
reactions: buildMessageReactions(update.reactions),
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateMessageExtendedMedia) {
|
||||
let media: MediaContent | undefined;
|
||||
if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) {
|
||||
media = buildMessageMediaContent(update.extendedMedia.media);
|
||||
}
|
||||
|
||||
let preview: ApiMessageExtendedMediaPreview | undefined;
|
||||
if (update.extendedMedia instanceof GramJs.MessageExtendedMediaPreview) {
|
||||
preview = buildApiMessageExtendedMediaPreview(update.extendedMedia);
|
||||
}
|
||||
|
||||
if (!media && !preview) return;
|
||||
|
||||
const chatId = getApiChatIdFromMtpPeer(update.peer);
|
||||
const isBought = update.extendedMedia[0] instanceof GramJs.MessageExtendedMedia;
|
||||
if (isBought) {
|
||||
const boughtMedia = buildBoughtMediaContent(update.extendedMedia);
|
||||
|
||||
swapLocalInvoiceMedia(chatId, update.msgId, update.extendedMedia);
|
||||
if (!boughtMedia?.length) return;
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateMessageExtendedMedia',
|
||||
id: update.msgId,
|
||||
chatId,
|
||||
isBought,
|
||||
extendedMedia: boughtMedia,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const previewMedia = !isBought ? update.extendedMedia
|
||||
.filter((m): m is GramJs.MessageExtendedMediaPreview => m instanceof GramJs.MessageExtendedMediaPreview)
|
||||
.map((m) => buildApiMessageExtendedMediaPreview(m))
|
||||
.filter(Boolean) : undefined;
|
||||
|
||||
if (!previewMedia?.length) return;
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateMessageExtendedMedia',
|
||||
id: update.msgId,
|
||||
chatId,
|
||||
media,
|
||||
preview,
|
||||
extendedMedia: previewMedia,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateDeleteMessages) {
|
||||
onUpdate({
|
||||
@ -443,43 +439,12 @@ export function updater(update: Update) {
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateDeleteChannelMessages) {
|
||||
const chatId = buildApiPeerId(update.channelId, 'channel');
|
||||
const ids = update.messages;
|
||||
const existingIds = ids.filter((id) => localDb.messages[`${chatId}-${id}`]);
|
||||
const missingIds = ids.filter((id) => !localDb.messages[`${chatId}-${id}`]);
|
||||
const profilePhotoIds = ids.map((id) => {
|
||||
const message = localDb.messages[`${chatId}-${id}`];
|
||||
|
||||
return message && message instanceof GramJs.MessageService && 'photo' in message.action
|
||||
? String(message.action.photo.id)
|
||||
: undefined;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (existingIds.length) {
|
||||
onUpdate({
|
||||
'@type': 'deleteMessages',
|
||||
ids: existingIds,
|
||||
chatId,
|
||||
});
|
||||
}
|
||||
|
||||
if (profilePhotoIds.length) {
|
||||
onUpdate({
|
||||
'@type': 'deleteProfilePhotos',
|
||||
ids: profilePhotoIds,
|
||||
chatId,
|
||||
});
|
||||
}
|
||||
|
||||
// For some reason delete message update sometimes comes before new message update
|
||||
if (missingIds.length) {
|
||||
setTimeout(() => {
|
||||
onUpdate({
|
||||
'@type': 'deleteMessages',
|
||||
ids: missingIds,
|
||||
chatId,
|
||||
});
|
||||
}, DELETE_MISSING_CHANNEL_MESSAGE_DELAY);
|
||||
}
|
||||
onUpdate({
|
||||
'@type': 'deleteMessages',
|
||||
ids: update.messages,
|
||||
chatId,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateServiceNotification) {
|
||||
if (update.popup) {
|
||||
onUpdate({
|
||||
@ -492,9 +457,7 @@ export function updater(update: Update) {
|
||||
const currentDate = Date.now() / 1000 + getServerTimeOffset();
|
||||
const message = buildApiMessageFromNotification(update, currentDate);
|
||||
|
||||
if (isMessageWithMedia(update)) {
|
||||
addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update));
|
||||
}
|
||||
addMessageToLocalDb(buildMessageFromUpdate(message.id, message.chatId, update));
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateServiceNotification',
|
||||
|
||||
@ -30,7 +30,6 @@ const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
||||
const savedLocalDb: LocalDb = {
|
||||
chats: {},
|
||||
users: {},
|
||||
messages: {},
|
||||
documents: {},
|
||||
stickerSets: {},
|
||||
photos: {},
|
||||
|
||||
@ -9,6 +9,7 @@ export type ApiInlineResultType = (
|
||||
);
|
||||
|
||||
export interface ApiWebDocument {
|
||||
mediaType: 'webDocument';
|
||||
url: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
|
||||
@ -11,7 +11,7 @@ export interface ApiDimensions {
|
||||
}
|
||||
|
||||
export interface ApiPhotoSize extends ApiDimensions {
|
||||
type: 's' | 'm' | 'x' | 'y' | 'z';
|
||||
type: 's' | 'm' | 'x' | 'y' | 'w';
|
||||
}
|
||||
|
||||
export interface ApiVideoSize extends ApiDimensions {
|
||||
@ -25,7 +25,9 @@ export interface ApiThumbnail extends ApiDimensions {
|
||||
}
|
||||
|
||||
export interface ApiPhoto {
|
||||
mediaType: 'photo';
|
||||
id: string;
|
||||
date: number;
|
||||
thumbnail?: ApiThumbnail;
|
||||
isVideo?: boolean;
|
||||
sizes: ApiPhotoSize[];
|
||||
@ -35,6 +37,7 @@ export interface ApiPhoto {
|
||||
}
|
||||
|
||||
export interface ApiSticker {
|
||||
mediaType: 'sticker';
|
||||
id: string;
|
||||
stickerSetInfo: ApiStickerSetInfo;
|
||||
emoji?: string;
|
||||
@ -85,6 +88,7 @@ type ApiStickerSetInfoMissing = {
|
||||
export type ApiStickerSetInfo = ApiStickerSetInfoShortName | ApiStickerSetInfoId | ApiStickerSetInfoMissing;
|
||||
|
||||
export interface ApiVideo {
|
||||
mediaType: 'video';
|
||||
id: string;
|
||||
mimeType: string;
|
||||
duration: number;
|
||||
@ -103,6 +107,7 @@ export interface ApiVideo {
|
||||
}
|
||||
|
||||
export interface ApiAudio {
|
||||
mediaType: 'audio';
|
||||
id: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
@ -114,12 +119,15 @@ export interface ApiAudio {
|
||||
}
|
||||
|
||||
export interface ApiVoice {
|
||||
mediaType: 'voice';
|
||||
id: string;
|
||||
duration: number;
|
||||
waveform?: number[];
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ApiDocument {
|
||||
mediaType: 'document';
|
||||
id?: string;
|
||||
fileName: string;
|
||||
size: number;
|
||||
@ -127,17 +135,29 @@ export interface ApiDocument {
|
||||
mimeType: string;
|
||||
thumbnail?: ApiThumbnail;
|
||||
previewBlobUrl?: string;
|
||||
mediaType?: 'photo' | 'video';
|
||||
innerMediaType?: 'photo' | 'video';
|
||||
mediaSize?: ApiDimensions;
|
||||
}
|
||||
|
||||
export interface ApiContact {
|
||||
mediaType: 'contact';
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type ApiPaidMedia = {
|
||||
mediaType: 'paidMedia';
|
||||
starsAmount: number;
|
||||
} & ({
|
||||
isBought?: true;
|
||||
extendedMedia: BoughtPaidMedia[];
|
||||
} | {
|
||||
isBought?: undefined;
|
||||
extendedMedia: ApiMediaExtendedPreview[];
|
||||
});
|
||||
|
||||
export interface ApiPollAnswer {
|
||||
text: ApiFormattedText;
|
||||
option: string;
|
||||
@ -151,6 +171,7 @@ export interface ApiPollResult {
|
||||
}
|
||||
|
||||
export interface ApiPoll {
|
||||
mediaType: 'poll';
|
||||
id: string;
|
||||
summary: {
|
||||
closed?: true;
|
||||
@ -243,6 +264,7 @@ export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestI
|
||||
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars;
|
||||
|
||||
export interface ApiInvoice {
|
||||
mediaType: 'invoice';
|
||||
text: string;
|
||||
title: string;
|
||||
photo?: ApiWebDocument;
|
||||
@ -252,12 +274,13 @@ export interface ApiInvoice {
|
||||
isTest?: boolean;
|
||||
isRecurring?: boolean;
|
||||
termsUrl?: string;
|
||||
extendedMedia?: ApiMessageExtendedMediaPreview;
|
||||
extendedMedia?: ApiMediaExtendedPreview;
|
||||
maxTipAmount?: number;
|
||||
suggestedTipAmounts?: number[];
|
||||
}
|
||||
|
||||
export interface ApiMessageExtendedMediaPreview {
|
||||
export interface ApiMediaExtendedPreview {
|
||||
mediaType: 'extendedMediaPreview';
|
||||
width?: number;
|
||||
height?: number;
|
||||
thumbnail?: ApiThumbnail;
|
||||
@ -277,12 +300,12 @@ export interface ApiGeoPoint {
|
||||
}
|
||||
|
||||
interface ApiGeo {
|
||||
type: 'geo';
|
||||
mediaType: 'geo';
|
||||
geo: ApiGeoPoint;
|
||||
}
|
||||
|
||||
interface ApiVenue {
|
||||
type: 'venue';
|
||||
mediaType: 'venue';
|
||||
geo: ApiGeoPoint;
|
||||
title: string;
|
||||
address: string;
|
||||
@ -292,7 +315,7 @@ interface ApiVenue {
|
||||
}
|
||||
|
||||
interface ApiGeoLive {
|
||||
type: 'geoLive';
|
||||
mediaType: 'geoLive';
|
||||
geo: ApiGeoPoint;
|
||||
heading?: number;
|
||||
period: number;
|
||||
@ -301,6 +324,7 @@ interface ApiGeoLive {
|
||||
export type ApiLocation = ApiGeo | ApiVenue | ApiGeoLive;
|
||||
|
||||
export type ApiGame = {
|
||||
mediaType: 'game';
|
||||
title: string;
|
||||
description: string;
|
||||
photo?: ApiPhoto;
|
||||
@ -311,6 +335,7 @@ export type ApiGame = {
|
||||
};
|
||||
|
||||
export type ApiGiveaway = {
|
||||
mediaType: 'giveaway';
|
||||
quantity: number;
|
||||
months: number;
|
||||
untilDate: number;
|
||||
@ -321,6 +346,7 @@ export type ApiGiveaway = {
|
||||
};
|
||||
|
||||
export type ApiGiveawayResults = {
|
||||
mediaType: 'giveawayResults';
|
||||
months: number;
|
||||
untilDate: number;
|
||||
isRefunded?: true;
|
||||
@ -344,6 +370,7 @@ export type ApiNewPoll = {
|
||||
};
|
||||
|
||||
export interface ApiAction {
|
||||
mediaType: 'action';
|
||||
text: string;
|
||||
targetUserIds?: string[];
|
||||
targetChatId?: string;
|
||||
@ -378,6 +405,7 @@ export interface ApiAction {
|
||||
}
|
||||
|
||||
export interface ApiWebPage {
|
||||
mediaType: 'webpage';
|
||||
id: number;
|
||||
url: string;
|
||||
displayUrl: string;
|
||||
@ -546,6 +574,7 @@ export type MediaContent = {
|
||||
storyData?: ApiMessageStoryData;
|
||||
giveaway?: ApiGiveaway;
|
||||
giveawayResults?: ApiGiveawayResults;
|
||||
paidMedia?: ApiPaidMedia;
|
||||
isExpiredVoice?: boolean;
|
||||
isExpiredRoundVideo?: boolean;
|
||||
ttlSeconds?: number;
|
||||
@ -554,6 +583,8 @@ export type MediaContainer = {
|
||||
content: MediaContent;
|
||||
};
|
||||
|
||||
export type BoughtPaidMedia = Pick<MediaContent, 'photo' | 'video'>;
|
||||
|
||||
export interface ApiMessage {
|
||||
id: number;
|
||||
chatId: string;
|
||||
@ -841,6 +872,7 @@ export type ApiThemeParameters = {
|
||||
section_header_text_color: string;
|
||||
subtitle_text_color: string;
|
||||
destructive_text_color: string;
|
||||
section_separator_color: string;
|
||||
};
|
||||
|
||||
export type ApiBotApp = {
|
||||
|
||||
@ -2,7 +2,9 @@ import type { ApiPremiumSection } from '../../global/types';
|
||||
import type { ApiInvoiceContainer } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiChat } from './chats';
|
||||
import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages';
|
||||
import type {
|
||||
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia, MediaContent,
|
||||
} from './messages';
|
||||
import type { PrepaidGiveaway, StatisticsOverviewPercentage } from './statistics';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
@ -67,9 +69,11 @@ export interface ApiReceiptStars {
|
||||
title?: string;
|
||||
text?: string;
|
||||
photo?: ApiWebDocument;
|
||||
media?: BoughtPaidMedia[];
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
transactionId: string;
|
||||
messageId?: number;
|
||||
}
|
||||
|
||||
export interface ApiReceiptRegular {
|
||||
@ -227,6 +231,10 @@ export interface ApiStarsTransactionPeerFragment {
|
||||
type: 'fragment';
|
||||
}
|
||||
|
||||
export interface ApiStarsTransactionPeerAds {
|
||||
type: 'ads';
|
||||
}
|
||||
|
||||
export interface ApiStarsTransactionPeerPeer {
|
||||
type: 'peer';
|
||||
id: string;
|
||||
@ -238,17 +246,22 @@ export type ApiStarsTransactionPeer =
|
||||
| ApiStarsTransactionPeerPlayMarket
|
||||
| ApiStarsTransactionPeerPremiumBot
|
||||
| ApiStarsTransactionPeerFragment
|
||||
| ApiStarsTransactionPeerAds
|
||||
| ApiStarsTransactionPeerPeer;
|
||||
|
||||
export interface ApiStarsTransaction {
|
||||
id: string;
|
||||
peer: ApiStarsTransactionPeer;
|
||||
messageId?: number;
|
||||
stars: number;
|
||||
isRefund?: true;
|
||||
hasFailed?: true;
|
||||
isPending?: true;
|
||||
date: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
photo?: ApiWebDocument;
|
||||
extendedMedia?: MediaContent[];
|
||||
}
|
||||
|
||||
export interface ApiStarTopupOption {
|
||||
|
||||
@ -66,6 +66,7 @@ export type ApiPeerStories = {
|
||||
};
|
||||
|
||||
export type ApiMessageStoryData = {
|
||||
mediaType: 'storyData';
|
||||
id: number;
|
||||
peerId: string;
|
||||
isMention?: boolean;
|
||||
@ -124,6 +125,7 @@ export type ApiMediaAreaCoordinates = {
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
radius?: number;
|
||||
};
|
||||
|
||||
export type ApiMediaAreaVenue = {
|
||||
@ -154,5 +156,11 @@ export type ApiMediaAreaChannelPost = {
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
export type ApiMediaAreaUrl = {
|
||||
type: 'url';
|
||||
coordinates: ApiMediaAreaCoordinates;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction
|
||||
| ApiMediaAreaChannelPost;
|
||||
| ApiMediaAreaChannelPost | ApiMediaAreaUrl;
|
||||
|
||||
@ -21,8 +21,8 @@ import type {
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiInputInvoice,
|
||||
ApiMediaExtendedPreview,
|
||||
ApiMessage,
|
||||
ApiMessageExtendedMediaPreview,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiQuickReply,
|
||||
@ -30,7 +30,7 @@ import type {
|
||||
ApiReactions,
|
||||
ApiStickerSet,
|
||||
ApiThreadInfo,
|
||||
MediaContent,
|
||||
BoughtPaidMedia,
|
||||
} from './messages';
|
||||
import type {
|
||||
ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
||||
@ -344,12 +344,6 @@ export type ApiUpdateDeleteSavedHistory = {
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateDeleteProfilePhotos = {
|
||||
'@type': 'deleteProfilePhotos';
|
||||
ids: string[];
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateResetMessages = {
|
||||
'@type': 'resetMessages';
|
||||
id: string;
|
||||
@ -373,9 +367,13 @@ export type ApiUpdateMessageExtendedMedia = {
|
||||
'@type': 'updateMessageExtendedMedia';
|
||||
id: number;
|
||||
chatId: string;
|
||||
media?: MediaContent;
|
||||
preview?: ApiMessageExtendedMediaPreview;
|
||||
};
|
||||
} & ({
|
||||
isBought?: true;
|
||||
extendedMedia: BoughtPaidMedia[];
|
||||
} | {
|
||||
isBought?: undefined;
|
||||
extendedMedia: ApiMediaExtendedPreview[];
|
||||
});
|
||||
|
||||
export type ApiDeleteContact = {
|
||||
'@type': 'deleteContact';
|
||||
@ -753,7 +751,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfos | ApiUpdateCommonBoxMessages |
|
||||
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
|
||||
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
|
||||
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos |
|
||||
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo |
|
||||
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
|
||||
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
|
||||
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
||||
|
||||
1
src/assets/font-icons/stars-lock.svg
Normal file
1
src/assets/font-icons/stars-lock.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M8.1 10.35a7.9 7.9 0 0 1 15.8 0v4.66a5.1 5.1 0 0 1 1.95 2.34c.39.942.39 2.134.39 4.52s0 3.578-.39 4.52a5.12 5.12 0 0 1-2.77 2.77c-.942.39-2.134.39-4.52.39h-5.12c-2.386 0-3.578 0-4.52-.39a5.12 5.12 0 0 1-2.77-2.77c-.39-.942-.39-2.134-.39-4.52s0-3.578.39-4.52a5.1 5.1 0 0 1 1.95-2.34zm7.9-4.9a4.9 4.9 0 0 0-4.9 4.9v3.865c.63-.025 1.39-.025 2.34-.025h5.12c.949 0 1.709 0 2.34.024V10.35a4.9 4.9 0 0 0-4.9-4.9m-2.417 20.815 2.16-1.384a.46.46 0 0 1 .499 0l2.178 1.395a.46.46 0 0 0 .36.06.5.5 0 0 0 .353-.602l-.597-2.592a.51.51 0 0 1 .154-.497l1.942-1.73a.5.5 0 0 0 .166-.341.49.49 0 0 0-.438-.536l-2.546-.208a.48.48 0 0 1-.404-.307l-.977-2.465a.47.47 0 0 0-.881 0l-.977 2.465a.48.48 0 0 1-.403.307l-2.533.207a.47.47 0 0 0-.328.177.515.515 0 0 0 .06.702l.828.726c.415.364.962.518 1.495.421l2.623-.475a.22.22 0 0 1 .237.128.235.235 0 0 1-.103.308l-2.357 1.183a1.72 1.72 0 0 0-.888 1.134l-.335 1.376a.52.52 0 0 0 .056.383.465.465 0 0 0 .656.165"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
6
src/assets/premium/PremiumEffects.svg
Normal file
6
src/assets/premium/PremiumEffects.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.1 KiB |
@ -4,7 +4,7 @@ import type { ApiSticker } from '../../api/types';
|
||||
import type { OwnProps as AnimatedIconProps } from './AnimatedIcon';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { getStickerPreviewHash } from '../../global/helpers';
|
||||
import { getStickerMediaHash } from '../../global/helpers';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
|
||||
@ -22,7 +22,7 @@ function AnimatedIconFromSticker(props: OwnProps) {
|
||||
const thumbDataUri = sticker?.thumbnail?.dataUri;
|
||||
const localMediaHash = sticker && `sticker${sticker.id}`;
|
||||
const previewBlobUrl = useMedia(
|
||||
sticker ? getStickerPreviewHash(sticker.id) : undefined,
|
||||
sticker ? getStickerMediaHash(sticker, 'preview') : undefined,
|
||||
noLoad && !forcePreview,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
);
|
||||
|
||||
@ -13,9 +13,9 @@ import { AudioOrigin } from '../../types';
|
||||
|
||||
import {
|
||||
getMediaDuration,
|
||||
getMediaFormat,
|
||||
getMediaHash,
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageWebPageAudio,
|
||||
hasMessageTtl,
|
||||
isMessageLocal,
|
||||
@ -72,7 +72,7 @@ type OwnProps = {
|
||||
onPause?: NoneToVoidFunction;
|
||||
onReadMedia?: () => void;
|
||||
onCancelUpload?: () => void;
|
||||
onDateClick?: (messageId: number, chatId: string) => void;
|
||||
onDateClick?: (arg: ApiMessage) => void;
|
||||
};
|
||||
|
||||
export const TINY_SCREEN_WIDTH_MQL = window.matchMedia('(max-width: 375px)');
|
||||
@ -108,7 +108,7 @@ const Audio: FC<OwnProps> = ({
|
||||
onDateClick,
|
||||
}) => {
|
||||
const {
|
||||
cancelMessageMediaDownload, downloadMessageMedia, transcribeAudio, openOneTimeMediaModal,
|
||||
cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal,
|
||||
} = getActions();
|
||||
|
||||
const {
|
||||
@ -117,6 +117,7 @@ const Audio: FC<OwnProps> = ({
|
||||
}, isMediaUnread,
|
||||
} = message;
|
||||
const audio = contentAudio || getMessageWebPageAudio(message);
|
||||
const media = (voice || video || audio)!;
|
||||
const isVoice = Boolean(voice || video);
|
||||
const isSeeking = useRef<boolean>(false);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -127,22 +128,22 @@ const Audio: FC<OwnProps> = ({
|
||||
const { isMobile } = useAppLayout();
|
||||
const [isActivated, setIsActivated] = useState(false);
|
||||
const shouldLoad = isActivated || PRELOAD;
|
||||
const coverHash = getMessageMediaHash(message, 'pictogram');
|
||||
const coverHash = getMediaHash(media, 'pictogram');
|
||||
const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl);
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const isInOneTimeModal = origin === AudioOrigin.OneTimeModal;
|
||||
const trackType = isVoice ? (hasTtl ? 'oneTimeVoice' : 'voice') : 'audio';
|
||||
|
||||
const mediaData = useMedia(
|
||||
getMessageMediaHash(message, 'inline'),
|
||||
getMediaHash(media, 'inline'),
|
||||
!shouldLoad,
|
||||
getMessageMediaFormat(message, 'inline'),
|
||||
getMediaFormat(media, 'inline'),
|
||||
);
|
||||
|
||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'download'),
|
||||
getMediaHash(media, 'download'),
|
||||
!isDownloading,
|
||||
getMessageMediaFormat(message, 'download'),
|
||||
getMediaFormat(media, 'download'),
|
||||
);
|
||||
|
||||
const handleForcePlay = useLastCallback(() => {
|
||||
@ -204,7 +205,6 @@ const Audio: FC<OwnProps> = ({
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || downloadProgress,
|
||||
isLoadingForPlaying || isDownloading,
|
||||
uploadProgress !== undefined,
|
||||
@ -246,9 +246,9 @@ const Audio: FC<OwnProps> = ({
|
||||
|
||||
const handleDownloadClick = useLastCallback(() => {
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message });
|
||||
cancelMediaDownload({ media });
|
||||
} else {
|
||||
downloadMessageMedia({ message });
|
||||
downloadMedia({ media });
|
||||
}
|
||||
});
|
||||
|
||||
@ -273,7 +273,7 @@ const Audio: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
const handleDateClick = useLastCallback(() => {
|
||||
onDateClick!(message.id, message.chatId);
|
||||
onDateClick!(message);
|
||||
});
|
||||
|
||||
const handleTranscribe = useLastCallback(() => {
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
getChatTitle,
|
||||
getPeerStoryHtmlId,
|
||||
getUserFullName,
|
||||
getVideoAvatarMediaHash,
|
||||
getWebDocumentHash,
|
||||
isAnonymousForwardsChat,
|
||||
isChatWithRepliesBot,
|
||||
@ -120,7 +121,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
} else if (photo) {
|
||||
imageHash = `photo${photo.id}?size=m`;
|
||||
if (photo.isVideo && withVideo) {
|
||||
videoHash = `videoAvatar${photo.id}?size=u`;
|
||||
videoHash = getVideoAvatarMediaHash(photo);
|
||||
}
|
||||
} else if (webPhoto) {
|
||||
imageHash = getWebDocumentHash(webPhoto);
|
||||
|
||||
@ -31,11 +31,24 @@
|
||||
}
|
||||
|
||||
.effect-icon {
|
||||
font-size: 1.25rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: grid;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
place-items: center;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
right: -0.25rem;
|
||||
bottom: -0.25rem;
|
||||
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid var(--color-borders);
|
||||
color: var(--color-text);
|
||||
|
||||
border-radius: 50%;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
& > .emoji {
|
||||
width: 1rem !important;
|
||||
height: 1rem !important;
|
||||
|
||||
@ -93,6 +93,8 @@ import {
|
||||
import { selectCurrentLimit } from '../../global/selectors/limits';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates/dateFormat';
|
||||
import { processDeepLink } from '../../util/deeplink';
|
||||
import { tryParseDeepLink } from '../../util/deepLinkParser';
|
||||
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
||||
import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager';
|
||||
import focusEditableElement from '../../util/focusEditableElement';
|
||||
@ -1093,9 +1095,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
callAttachBot({
|
||||
chatId, url: botMenuButton.url, threadId,
|
||||
});
|
||||
const parsedLink = tryParseDeepLink(botMenuButton.url);
|
||||
|
||||
if (parsedLink?.type === 'publicUsernameOrBotLink' && parsedLink.appName) {
|
||||
processDeepLink(botMenuButton.url);
|
||||
} else {
|
||||
callAttachBot({
|
||||
chatId, url: botMenuButton.url, threadId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleActivateBotCommandMenu = useLastCallback(() => {
|
||||
@ -2033,7 +2041,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
{isInMessageList && <i className="icon icon-check" />}
|
||||
</Button>
|
||||
{effectEmoji && (
|
||||
<span className="effect-icon">
|
||||
<span className="effect-icon" onClick={handleRemoveEffect}>
|
||||
{renderText(effectEmoji)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -1,20 +1,18 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ApiDocument, ApiMessage } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../config';
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getMediaFormat,
|
||||
getMediaThumbUri,
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessageWebPageDocument,
|
||||
isMessageDocumentVideo,
|
||||
isDocumentVideo,
|
||||
} from '../../global/helpers';
|
||||
import { getDocumentExtension, getDocumentHasPreview } from './helpers/documentInfo';
|
||||
|
||||
@ -30,7 +28,7 @@ import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import File from './File';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
document: ApiDocument;
|
||||
observeIntersection?: ObserveFn;
|
||||
smaller?: boolean;
|
||||
isSelected?: boolean;
|
||||
@ -46,14 +44,19 @@ type OwnProps = {
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
onCancelUpload?: () => void;
|
||||
onMediaClick?: () => void;
|
||||
onDateClick?: (messageId: number, chatId: string) => void;
|
||||
};
|
||||
} & ({
|
||||
message: ApiMessage;
|
||||
onDateClick: (arg: ApiMessage) => void;
|
||||
} | {
|
||||
message?: never;
|
||||
onDateClick?: never;
|
||||
});
|
||||
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
const SVG_EXTENSIONS = new Set(['svg', 'svgz']);
|
||||
|
||||
const Document: FC<OwnProps> = ({
|
||||
message,
|
||||
const Document = ({
|
||||
document,
|
||||
observeIntersection,
|
||||
smaller,
|
||||
canAutoLoad,
|
||||
@ -67,11 +70,12 @@ const Document: FC<OwnProps> = ({
|
||||
isSelectable,
|
||||
shouldWarnAboutSvg,
|
||||
isDownloading,
|
||||
message,
|
||||
onCancelUpload,
|
||||
onMediaClick,
|
||||
onDateClick,
|
||||
}) => {
|
||||
const { cancelMessageMediaDownload, downloadMessageMedia, setSettingOption } = getActions();
|
||||
}: OwnProps) => {
|
||||
const { cancelMediaDownload, downloadMedia, setSettingOption } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -80,8 +84,6 @@ const Document: FC<OwnProps> = ({
|
||||
const [isSvgDialogOpen, openSvgDialog, closeSvgDialog] = useFlag();
|
||||
const [shouldNotWarnAboutSvg, setShouldNotWarnAboutSvg] = useState(false);
|
||||
|
||||
const document = message.content.document! || getMessageWebPageDocument(message);
|
||||
|
||||
const { fileName, size, timestamp } = document;
|
||||
const extension = getDocumentExtension(document) || '';
|
||||
|
||||
@ -100,32 +102,31 @@ const Document: FC<OwnProps> = ({
|
||||
|
||||
const shouldDownload = Boolean(isDownloading || (isLoadAllowed && wasIntersected));
|
||||
|
||||
const documentHash = getMessageMediaHash(message, 'download');
|
||||
const documentHash = getDocumentMediaHash(document, 'download');
|
||||
const { loadProgress: downloadProgress, mediaData } = useMediaWithLoadProgress(
|
||||
documentHash, !shouldDownload, getMessageMediaFormat(message, 'download'), undefined, true,
|
||||
documentHash, !shouldDownload, getMediaFormat(document, 'download'), undefined, true,
|
||||
);
|
||||
const isLoaded = Boolean(mediaData);
|
||||
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || downloadProgress,
|
||||
shouldDownload && !isLoaded,
|
||||
uploadProgress !== undefined,
|
||||
);
|
||||
|
||||
const hasPreview = getDocumentHasPreview(document);
|
||||
const thumbDataUri = hasPreview ? getMessageMediaThumbDataUri(message) : undefined;
|
||||
const thumbDataUri = hasPreview ? getMediaThumbUri(document) : undefined;
|
||||
const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined;
|
||||
const previewData = useMedia(getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
||||
const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting);
|
||||
|
||||
const withMediaViewer = onMediaClick && Boolean(document.mediaType) && (
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES.has(document.mimeType) || SUPPORTED_IMAGE_CONTENT_TYPES.has(document.mimeType)
|
||||
);
|
||||
|
||||
const handleDownload = useLastCallback(() => {
|
||||
downloadMessageMedia({ message });
|
||||
downloadMedia({ media: document });
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
@ -137,7 +138,7 @@ const Document: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message });
|
||||
cancelMediaDownload({ media: document });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -166,7 +167,7 @@ const Document: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
const handleDateClick = useLastCallback(() => {
|
||||
onDateClick!(message.id, message.chatId);
|
||||
onDateClick?.(message);
|
||||
});
|
||||
|
||||
return (
|
||||
@ -187,7 +188,7 @@ const Document: FC<OwnProps> = ({
|
||||
sender={sender}
|
||||
isSelectable={isSelectable}
|
||||
isSelected={isSelected}
|
||||
actionIcon={withMediaViewer ? (isMessageDocumentVideo(message) ? 'play' : 'eye') : 'download'}
|
||||
actionIcon={withMediaViewer ? (isDocumentVideo(document) ? 'play' : 'eye') : 'download'}
|
||||
onClick={handleClick}
|
||||
onDateClick={onDateClick ? handleDateClick : undefined}
|
||||
/>
|
||||
|
||||
@ -7,6 +7,7 @@ import type { ApiVideo } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { getVideoMediaHash } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
@ -52,13 +53,12 @@ const GifButton: FC<OwnProps> = ({
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const localMediaHash = `gif${gif.id}`;
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
const loadAndPlay = isIntersecting && !isDisabled;
|
||||
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !loadAndPlay, ApiMediaFormat.BlobUrl);
|
||||
const previewBlobUrl = useMedia(getVideoMediaHash(gif, 'preview'), !loadAndPlay, ApiMediaFormat.BlobUrl);
|
||||
const [withThumb] = useState(gif.thumbnail?.dataUri && !previewBlobUrl);
|
||||
const thumbRef = useCanvasBlur(gif.thumbnail?.dataUri, !withThumb);
|
||||
const videoData = useMedia(localMediaHash, !loadAndPlay, ApiMediaFormat.BlobUrl);
|
||||
const videoData = useMedia(getVideoMediaHash(gif, 'full'), !loadAndPlay, ApiMediaFormat.BlobUrl);
|
||||
const shouldRenderVideo = Boolean(loadAndPlay && videoData);
|
||||
const { isBuffered, bufferingHandlers } = useBuffering(true);
|
||||
const shouldRenderSpinner = loadAndPlay && !isBuffered;
|
||||
@ -128,7 +128,6 @@ const GifButton: FC<OwnProps> = ({
|
||||
'GifButton',
|
||||
gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal',
|
||||
onClick && 'interactive',
|
||||
localMediaHash,
|
||||
className,
|
||||
);
|
||||
|
||||
|
||||
@ -130,8 +130,9 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
if (chat && hasMedia) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
avatarOwnerId: chat.id,
|
||||
mediaId: 0,
|
||||
isAvatarView: true,
|
||||
chatId: chat.id,
|
||||
mediaIndex: 0,
|
||||
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dots {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
@ -39,12 +40,15 @@ const MediaSpoiler: FC<OwnProps> = ({
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!ref.current) return;
|
||||
const el = ref.current;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const shiftX = x - (rect.width / 2);
|
||||
const shiftY = y - (rect.height / 2);
|
||||
ref.current.setAttribute('style', `--click-shift-x: ${shiftX}px; --click-shift-y: ${shiftY}px`);
|
||||
requestMutation(() => {
|
||||
el.setAttribute('style', `--click-shift-x: ${shiftX}px; --click-shift-y: ${shiftY}px`);
|
||||
});
|
||||
});
|
||||
|
||||
if (!shouldRender) {
|
||||
|
||||
@ -6,7 +6,8 @@ import type { ApiFormattedText, ApiMessage, ApiStory } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import { extractMessageText, getMessageText, stripCustomEmoji } from '../../global/helpers';
|
||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||
import { extractMessageText, stripCustomEmoji } from '../../global/helpers';
|
||||
import trimText from '../../util/trimText';
|
||||
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
||||
|
||||
@ -80,8 +81,7 @@ function MessageText({
|
||||
}, [entities]) || 0;
|
||||
|
||||
if (!text) {
|
||||
const contentNotSupportedText = getMessageText(messageOrStory);
|
||||
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined as any;
|
||||
return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -121,8 +121,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
if (user && hasMedia) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
avatarOwnerId: user.id,
|
||||
mediaId: 0,
|
||||
isAvatarView: true,
|
||||
chatId: user.id,
|
||||
mediaIndex: 0,
|
||||
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ type StateProps =
|
||||
user?: ApiUser;
|
||||
userStatus?: ApiUserStatus;
|
||||
chat?: ApiChat;
|
||||
mediaId?: number;
|
||||
mediaIndex?: number;
|
||||
avatarOwnerId?: string;
|
||||
topic?: ApiTopic;
|
||||
messagesCount?: number;
|
||||
@ -75,7 +75,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
userStatus,
|
||||
chat,
|
||||
isSynced,
|
||||
mediaId,
|
||||
mediaIndex,
|
||||
avatarOwnerId,
|
||||
topic,
|
||||
messagesCount,
|
||||
@ -98,9 +98,9 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
const { id: userId } = user || {};
|
||||
const { id: chatId } = chat || {};
|
||||
const photos = user?.photos || chat?.photos || MEMO_EMPTY_ARRAY;
|
||||
const prevMediaId = usePrevious(mediaId);
|
||||
const prevMediaIndex = usePrevious(mediaIndex);
|
||||
const prevAvatarOwnerId = usePrevious(avatarOwnerId);
|
||||
const mediaIdRef = useStateRef(mediaId);
|
||||
const mediaIndexRef = useStateRef(mediaIndex);
|
||||
const [hasSlideAnimation, setHasSlideAnimation] = useState(true);
|
||||
// slideOptimized doesn't work well when animation is dynamically disabled
|
||||
const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none';
|
||||
@ -111,17 +111,17 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
// Set the current avatar photo to the last selected photo in Media Viewer after it is closed
|
||||
useEffect(() => {
|
||||
if (prevAvatarOwnerId && prevMediaId !== undefined && mediaId === undefined) {
|
||||
if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) {
|
||||
setHasSlideAnimation(false);
|
||||
setCurrentPhotoIndex(prevMediaId);
|
||||
setCurrentPhotoIndex(prevMediaIndex);
|
||||
}
|
||||
}, [mediaId, prevMediaId, prevAvatarOwnerId]);
|
||||
}, [mediaIndex, prevMediaIndex, prevAvatarOwnerId]);
|
||||
|
||||
// Reset the current avatar photo to the one selected in Media Viewer if photos have changed
|
||||
useEffect(() => {
|
||||
setHasSlideAnimation(false);
|
||||
setCurrentPhotoIndex(mediaIdRef.current || 0);
|
||||
}, [mediaIdRef, photos]);
|
||||
setCurrentPhotoIndex(mediaIndexRef.current || 0);
|
||||
}, [mediaIndexRef, photos]);
|
||||
|
||||
// Deleting the last profile photo may result in an error
|
||||
useEffect(() => {
|
||||
@ -141,8 +141,9 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleProfilePhotoClick = useLastCallback(() => {
|
||||
openMediaViewer({
|
||||
avatarOwnerId: userId || chatId,
|
||||
mediaId: currentPhotoIndex,
|
||||
isAvatarView: true,
|
||||
chatId: userId || chatId,
|
||||
mediaIndex: currentPhotoIndex,
|
||||
origin: forceShowSelf ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar,
|
||||
});
|
||||
});
|
||||
@ -386,7 +387,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isPrivate = isUserId(userId);
|
||||
const userStatus = selectUserStatus(global, userId);
|
||||
const chat = selectChat(global, userId);
|
||||
const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer;
|
||||
const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer;
|
||||
const isForum = chat?.isForum;
|
||||
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
|
||||
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;
|
||||
@ -405,7 +406,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
userProfilePhoto: userFullInfo?.profilePhoto,
|
||||
userFallbackPhoto: userFullInfo?.fallbackPhoto,
|
||||
chatProfilePhoto: chatFullInfo?.profilePhoto,
|
||||
mediaId,
|
||||
mediaIndex,
|
||||
avatarOwnerId,
|
||||
emojiStatusSticker,
|
||||
...(topic && {
|
||||
|
||||
@ -54,7 +54,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
const animationId = availableReaction?.selectAnimation?.id;
|
||||
const coords = useCoordsInSharedCanvas(ref, sharedCanvasRef);
|
||||
const mediaData = useMedia(
|
||||
availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation) : undefined,
|
||||
availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation, 'full') : undefined,
|
||||
!animationId,
|
||||
);
|
||||
const handleClick = useLastCallback(() => {
|
||||
|
||||
@ -177,6 +177,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
const handleDefaultTopicIconClick = useLastCallback(() => {
|
||||
onStickerSelect?.({
|
||||
mediaType: 'sticker',
|
||||
id: DEFAULT_TOPIC_ICON_STICKER_ID,
|
||||
isLottie: false,
|
||||
isVideo: false,
|
||||
@ -188,6 +189,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
const handleDefaultStatusIconClick = useLastCallback(() => {
|
||||
onStickerSelect?.({
|
||||
mediaType: 'sticker',
|
||||
id: DEFAULT_STATUS_ICON_ID,
|
||||
isLottie: false,
|
||||
isVideo: false,
|
||||
|
||||
@ -5,7 +5,7 @@ import { getGlobal } from '../../global';
|
||||
import type { ApiSticker } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getStickerPreviewHash } from '../../global/helpers';
|
||||
import { getStickerMediaHash } from '../../global/helpers';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
@ -86,7 +86,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
const isUnsupportedVideo = sticker.isVideo && (!IS_WEBM_SUPPORTED || isVideoBroken);
|
||||
const isVideo = sticker.isVideo && !isUnsupportedVideo;
|
||||
const isStatic = !isLottie && !isVideo;
|
||||
const previewMediaHash = getStickerPreviewHash(sticker.id);
|
||||
const previewMediaHash = getStickerMediaHash(sticker, 'preview');
|
||||
|
||||
const dpr = useDevicePixelRatio();
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ type OwnProps = {
|
||||
senderTitle?: string;
|
||||
isProtected?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onMessageClick: (messageId: number, chatId: string) => void;
|
||||
onMessageClick: (message: ApiMessage) => void;
|
||||
};
|
||||
|
||||
type ApiWebPageWithFormatted =
|
||||
@ -61,7 +61,7 @@ const WebLink: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const handleMessageClick = useLastCallback(() => {
|
||||
onMessageClick(message.id, message.chatId);
|
||||
onMessageClick(message);
|
||||
});
|
||||
|
||||
if (!linkData) {
|
||||
|
||||
@ -89,16 +89,20 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
|
||||
const wrappedMedia = useMemo(() => {
|
||||
const replyMedia = replyInfo?.type === 'message' && replyInfo.replyMedia;
|
||||
if (!replyMedia) return undefined;
|
||||
return {
|
||||
content: replyMedia,
|
||||
};
|
||||
}, [replyInfo]);
|
||||
const containedMedia: MediaContainer | undefined = useMemo(() => {
|
||||
const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content;
|
||||
if (!media) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
||||
const mediaThumbnail = useThumbnail(message || wrappedMedia);
|
||||
return {
|
||||
content: media,
|
||||
};
|
||||
}, [message, replyInfo]);
|
||||
|
||||
const mediaHash = containedMedia && getMessageMediaHash(containedMedia, 'pictogram');
|
||||
const mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
|
||||
const mediaThumbnail = useThumbnail(containedMedia);
|
||||
const isRoundVideo = Boolean(message && getMessageRoundVideo(message));
|
||||
const isSpoiler = Boolean(message && getMessageIsSpoiler(message));
|
||||
const isQuote = Boolean(replyInfo?.type === 'message' && replyInfo.isQuote);
|
||||
@ -131,7 +135,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return customText || renderMediaContentType(wrappedMedia) || NBSP;
|
||||
return customText || renderMediaContentType(containedMedia) || NBSP;
|
||||
}
|
||||
|
||||
if (isActionMessage(message)) {
|
||||
|
||||
@ -3,7 +3,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||
import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { preloadImage } from '../../../util/files';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
@ -78,7 +78,7 @@ const EmojiIconBackground = ({
|
||||
const lang = useOldLang();
|
||||
|
||||
const { customEmoji } = useCustomEmoji(emojiDocumentId);
|
||||
const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined;
|
||||
const previewMediaHash = customEmoji ? getStickerMediaHash(customEmoji, 'preview') : undefined;
|
||||
const previewUrl = useMedia(previewMediaHash);
|
||||
|
||||
const customColor = useDynamicColorListener(containerRef);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ApiDimensions, ApiPhoto, ApiSticker, ApiVideo,
|
||||
ApiDimensions, ApiMediaExtendedPreview, ApiPhoto, ApiSticker, ApiVideo,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { STICKER_SIZE_INLINE_DESKTOP_FACTOR, STICKER_SIZE_INLINE_MOBILE_FACTOR } from '../../../config';
|
||||
@ -25,7 +25,7 @@ let cachedMaxWidthOwn: number | undefined;
|
||||
let cachedMaxWidth: number | undefined;
|
||||
let cachedMaxWidthNoAvatar: number | undefined;
|
||||
|
||||
function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean, isMobile?: boolean) {
|
||||
function getMaxMessageWidthRem(fromOwnMessage?: boolean, noAvatars?: boolean, isMobile?: boolean) {
|
||||
const regularMaxWidth = fromOwnMessage ? MESSAGE_OWN_MAX_WIDTH_REM : MESSAGE_MAX_WIDTH_REM;
|
||||
if (!isMobile) {
|
||||
return regularMaxWidth;
|
||||
@ -59,7 +59,7 @@ function getMaxMessageWidthRem(fromOwnMessage: boolean, noAvatars?: boolean, isM
|
||||
}
|
||||
|
||||
export function getAvailableWidth(
|
||||
fromOwnMessage: boolean,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
@ -94,7 +94,7 @@ export function calculateDimensionsForMessageMedia({
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
fromOwnMessage: boolean;
|
||||
fromOwnMessage?: boolean;
|
||||
asForwarded?: boolean;
|
||||
isWebPageMedia?: boolean;
|
||||
isGif?: boolean;
|
||||
@ -125,7 +125,7 @@ export function getMediaViewerAvailableDimensions(withFooter: boolean, isVideo:
|
||||
|
||||
export function calculateInlineImageDimensions(
|
||||
photo: ApiPhoto,
|
||||
fromOwnMessage: boolean,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
@ -146,7 +146,7 @@ export function calculateInlineImageDimensions(
|
||||
|
||||
export function calculateVideoDimensions(
|
||||
video: ApiVideo,
|
||||
fromOwnMessage: boolean,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
@ -166,6 +166,27 @@ export function calculateVideoDimensions(
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateExtendedPreviewDimensions(
|
||||
preview: ApiMediaExtendedPreview,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
isMobile?: boolean,
|
||||
) {
|
||||
const { width = DEFAULT_MEDIA_DIMENSIONS.width, height = DEFAULT_MEDIA_DIMENSIONS.height } = preview;
|
||||
|
||||
return calculateDimensionsForMessageMedia({
|
||||
width,
|
||||
height,
|
||||
fromOwnMessage,
|
||||
asForwarded,
|
||||
isWebPageMedia,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPictogramDimensions(): ApiDimensions {
|
||||
return {
|
||||
width: 2 * REM,
|
||||
|
||||
@ -105,12 +105,12 @@ const UserBirthday = ({
|
||||
if (!isToday || !numbersForAge) return;
|
||||
|
||||
numbersForAge.forEach((sticker) => {
|
||||
const hash = getStickerMediaHash(sticker.id);
|
||||
const hash = getStickerMediaHash(sticker, 'preview');
|
||||
mediaLoader.fetch(hash, ApiMediaFormat.BlobUrl);
|
||||
});
|
||||
|
||||
if (effectSticker) {
|
||||
const effectHash = getStickerMediaHash(effectSticker.id);
|
||||
const effectHash = getStickerMediaHash(effectSticker, 'preview');
|
||||
mediaLoader.fetch(effectHash, ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
}, [effectSticker, isToday, numbersForAge]);
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types';
|
||||
|
||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||
import { getStickerHashById } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
@ -31,7 +31,7 @@ const CustomEmojiEffect: FC<OwnProps> = ({
|
||||
particleSize,
|
||||
onEnded,
|
||||
}) => {
|
||||
const stickerHash = getStickerPreviewHash(reaction.documentId);
|
||||
const stickerHash = getStickerHashById(reaction.documentId);
|
||||
|
||||
const previewMediaData = useMedia(!isLottie ? stickerHash : undefined);
|
||||
|
||||
|
||||
@ -89,10 +89,10 @@ export default function useChatListEntry({
|
||||
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
|
||||
useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage);
|
||||
|
||||
const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage)
|
||||
? getMessageMediaThumbDataUri(lastMessage)
|
||||
: undefined;
|
||||
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
||||
const mediaHasPreview = lastMessage && !getMessageSticker(lastMessage);
|
||||
|
||||
const mediaThumbnail = mediaHasPreview ? getMessageMediaThumbDataUri(lastMessage) : undefined;
|
||||
const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
||||
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
||||
|
||||
const actionTargetUsers = useMemo(() => {
|
||||
|
||||
@ -2,10 +2,12 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, 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 } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
@ -70,8 +72,8 @@ const AudioResults: FC<OwnProps & StateProps> = ({
|
||||
}).filter(Boolean);
|
||||
}, [globalMessagesByChatId, foundIds]);
|
||||
|
||||
const handleMessageFocus = useCallback((messageId: number, chatId: string) => {
|
||||
focusMessage({ chatId, messageId });
|
||||
const handleMessageFocus = useCallback((message: ApiMessage) => {
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}, [focusMessage]);
|
||||
|
||||
const handlePlayAudio = useCallback((messageId: number, chatId: string) => {
|
||||
@ -111,7 +113,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
|
||||
onPlay={handlePlayAudio}
|
||||
onDateClick={handleMessageFocus}
|
||||
canDownload={!chatsById[message.chatId]?.isProtected && !message.isProtected}
|
||||
isDownloading={activeDownloads[message.chatId]?.ids?.includes(message.id)}
|
||||
isDownloading={getIsDownloading(activeDownloads, message.content.audio!)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ import type { StateProps } from './helpers/createMapStateToProps';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
||||
import { getMessageDocument } from '../../../global/helpers';
|
||||
import { getIsDownloading, getMessageDocument } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
@ -84,8 +84,8 @@ const FileResults: FC<OwnProps & StateProps> = ({
|
||||
}).filter(Boolean) as ApiMessage[];
|
||||
}, [globalMessagesByChatId, foundIds]);
|
||||
|
||||
const handleMessageFocus = useCallback((messageId: number, chatId: string) => {
|
||||
focusMessage({ chatId, messageId });
|
||||
const handleMessageFocus = useCallback((message: ApiMessage) => {
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}, [focusMessage]);
|
||||
|
||||
function renderList() {
|
||||
@ -111,13 +111,14 @@ const FileResults: FC<OwnProps & StateProps> = ({
|
||||
</p>
|
||||
)}
|
||||
<Document
|
||||
document={getMessageDocument(message)!}
|
||||
message={message}
|
||||
withDate
|
||||
datetime={message.date}
|
||||
smaller
|
||||
sender={getSenderName(lang, message, chatsById, usersById)}
|
||||
className="scroll-item"
|
||||
isDownloading={activeDownloads[message.chatId]?.ids?.includes(message.id)}
|
||||
isDownloading={getIsDownloading(activeDownloads, message.content.document!)}
|
||||
shouldWarnAboutSvg={shouldWarnAboutSvg}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
onDateClick={handleMessageFocus}
|
||||
|
||||
@ -4,6 +4,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { StateProps } from './helpers/createMapStateToProps';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
@ -80,8 +81,8 @@ const LinkResults: FC<OwnProps & StateProps> = ({
|
||||
}).filter(Boolean);
|
||||
}, [globalMessagesByChatId, foundIds]);
|
||||
|
||||
const handleMessageFocus = useCallback((messageId: number, chatId: string) => {
|
||||
focusMessage({ chatId, messageId });
|
||||
const handleMessageFocus = useCallback((message: ApiMessage) => {
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}, [focusMessage]);
|
||||
|
||||
function renderList() {
|
||||
|
||||
@ -80,7 +80,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
const handleSelectMedia = useCallback((id: number, chatId: string) => {
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
mediaId: id,
|
||||
messageId: id,
|
||||
origin: MediaViewerOrigin.SearchResult,
|
||||
});
|
||||
}, [openMediaViewer]);
|
||||
|
||||
@ -14,7 +14,7 @@ export type StateProps = {
|
||||
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
||||
foundIds?: string[];
|
||||
searchChatId?: string;
|
||||
activeDownloads: TabState['activeDownloads']['byChatId'];
|
||||
activeDownloads: TabState['activeDownloads'];
|
||||
isChatProtected?: boolean;
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
};
|
||||
@ -36,7 +36,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||
const { byChatId: globalMessagesByChatId } = global.messages;
|
||||
const foundIds = resultsByType?.[currentType]?.foundIds;
|
||||
|
||||
const activeDownloads = tabState.activeDownloads.byChatId;
|
||||
const activeDownloads = tabState.activeDownloads;
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo, useEffect } from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { GlobalState, TabState } from '../../global/types';
|
||||
import type { TabState } from '../../global/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import {
|
||||
getMessageContentFilename, getMessageMediaFormat, getMessageMediaHash,
|
||||
} from '../../global/helpers';
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import download from '../../util/download';
|
||||
import { compact } from '../../util/iteratees';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
import { IS_OPFS_SUPPORTED, IS_SERVICE_WORKER_SUPPORTED, MAX_BUFFER_SIZE } from '../../util/windowEnvironment';
|
||||
|
||||
@ -19,85 +14,64 @@ import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useRunDebounced from '../../hooks/useRunDebounced';
|
||||
|
||||
type StateProps = {
|
||||
activeDownloads: TabState['activeDownloads']['byChatId'];
|
||||
messages?: GlobalState['messages']['byChatId'];
|
||||
activeDownloads: TabState['activeDownloads'];
|
||||
};
|
||||
|
||||
const GLOBAL_UPDATE_DEBOUNCE = 1000;
|
||||
|
||||
const processedMessages = new Set<ApiMessage>();
|
||||
const downloadedMessages = new Set<ApiMessage>();
|
||||
const processedHashes = new Set<string>();
|
||||
const downloadedHashes = new Set<string>();
|
||||
|
||||
const DownloadManager: FC<StateProps> = ({
|
||||
activeDownloads,
|
||||
}) => {
|
||||
const { cancelMessagesMediaDownload, showNotification } = getActions();
|
||||
const { cancelMediaHashDownloads, showNotification } = getActions();
|
||||
|
||||
const runDebounced = useRunDebounced(GLOBAL_UPDATE_DEBOUNCE, true);
|
||||
|
||||
const handleMessageDownloaded = useLastCallback((message: ApiMessage) => {
|
||||
downloadedMessages.add(message);
|
||||
const handleMediaDownloaded = useLastCallback((hash: string) => {
|
||||
downloadedHashes.add(hash);
|
||||
runDebounced(() => {
|
||||
if (downloadedMessages.size) {
|
||||
cancelMessagesMediaDownload({ messages: Array.from(downloadedMessages) });
|
||||
downloadedMessages.clear();
|
||||
if (downloadedHashes.size) {
|
||||
cancelMediaHashDownloads({ mediaHashes: Array.from(downloadedHashes) });
|
||||
downloadedHashes.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// No need for expensive global updates on messages, so we avoid them
|
||||
const messages = getGlobal().messages.byChatId;
|
||||
const scheduledMessages = getGlobal().scheduledMessages.byChatId;
|
||||
|
||||
const activeMessages = Object.entries(activeDownloads).map(([chatId, chatActiveDownloads]) => {
|
||||
const chatMessages = chatActiveDownloads.ids?.map((id) => messages[chatId]?.byId[id]);
|
||||
const chatScheduledMessages = chatActiveDownloads.scheduledIds?.map((id) => scheduledMessages[chatId]?.byId[id]);
|
||||
|
||||
return compact([...chatMessages || [], ...chatScheduledMessages || []]);
|
||||
}).flat();
|
||||
|
||||
if (!activeMessages.length) {
|
||||
processedMessages.clear();
|
||||
if (!Object.keys(activeDownloads).length) {
|
||||
processedHashes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
activeMessages.forEach((message) => {
|
||||
if (processedMessages.has(message)) {
|
||||
return;
|
||||
}
|
||||
processedMessages.add(message);
|
||||
const downloadHash = getMessageMediaHash(message, 'download');
|
||||
if (!downloadHash) {
|
||||
handleMessageDownloaded(message);
|
||||
Object.entries(activeDownloads).forEach(([mediaHash, metadata]) => {
|
||||
if (processedHashes.has(mediaHash)) {
|
||||
return;
|
||||
}
|
||||
processedHashes.add(mediaHash);
|
||||
|
||||
const mediaData = mediaLoader.getFromMemory(downloadHash);
|
||||
const { size, filename, format: mediaFormat } = metadata;
|
||||
|
||||
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
||||
|
||||
if (mediaData) {
|
||||
download(mediaData, getMessageContentFilename(message));
|
||||
handleMessageDownloaded(message);
|
||||
download(mediaData, filename);
|
||||
handleMediaDownloaded(mediaHash);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
document, video, audio,
|
||||
} = message.content;
|
||||
const mediaSize = (document || video || audio)?.size || 0;
|
||||
if (mediaSize > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) {
|
||||
if (size > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED && !IS_SERVICE_WORKER_SUPPORTED) {
|
||||
showNotification({
|
||||
message: 'Downloading files bigger than 2GB is not supported in your browser.',
|
||||
});
|
||||
handleMessageDownloaded(message);
|
||||
handleMediaDownloaded(mediaHash);
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaFormat = getMessageMediaFormat(message, 'download');
|
||||
mediaLoader.fetch(downloadHash, mediaFormat, true).then((result) => {
|
||||
mediaLoader.fetch(mediaHash, mediaFormat, true).then((result) => {
|
||||
if (mediaFormat === ApiMediaFormat.DownloadUrl) {
|
||||
const url = new URL(result, window.document.baseURI);
|
||||
const filename = getMessageContentFilename(message);
|
||||
url.searchParams.set('filename', encodeURIComponent(filename));
|
||||
const downloadWindow = window.open(url.toString());
|
||||
downloadWindow?.addEventListener('beforeunload', () => {
|
||||
@ -106,20 +80,20 @@ const DownloadManager: FC<StateProps> = ({
|
||||
});
|
||||
});
|
||||
} else if (result) {
|
||||
download(result, getMessageContentFilename(message));
|
||||
download(result, filename);
|
||||
}
|
||||
|
||||
handleMessageDownloaded(message);
|
||||
handleMediaDownloaded(mediaHash);
|
||||
});
|
||||
});
|
||||
}, [activeDownloads, cancelMessagesMediaDownload, handleMessageDownloaded, showNotification]);
|
||||
}, [activeDownloads]);
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const activeDownloads = selectTabState(global).activeDownloads.byChatId;
|
||||
const activeDownloads = selectTabState(global).activeDownloads;
|
||||
|
||||
return {
|
||||
activeDownloads,
|
||||
|
||||
@ -46,6 +46,7 @@ export const PREMIUM_FEATURE_TITLES: Record<ApiPremiumSection, string> = {
|
||||
saved_tags: 'PremiumPreviewTags2',
|
||||
last_seen: 'PremiumPreviewLastSeen',
|
||||
message_privacy: 'PremiumPreviewMessagePrivacy',
|
||||
effects: 'PremiumPreviewEffects',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
|
||||
@ -66,6 +67,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
|
||||
saved_tags: 'PremiumPreviewTagsDescription2',
|
||||
last_seen: 'PremiumPreviewLastSeenDescription',
|
||||
message_privacy: 'PremiumPreviewMessagePrivacyDescription',
|
||||
effects: 'PremiumPreviewEffectsDescription',
|
||||
};
|
||||
|
||||
const LIMITS_TITLES: Record<ApiLimitTypeForPromo, string> = {
|
||||
|
||||
@ -42,6 +42,7 @@ import styles from './PremiumMainModal.module.scss';
|
||||
import PremiumAds from '../../../assets/premium/PremiumAds.svg';
|
||||
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
||||
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
||||
import PremiumEffects from '../../../assets/premium/PremiumEffects.svg';
|
||||
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
||||
import PremiumFile from '../../../assets/premium/PremiumFile.svg';
|
||||
import PremiumLastSeen from '../../../assets/premium/PremiumLastSeen.svg';
|
||||
@ -78,6 +79,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
|
||||
saved_tags: PremiumTags,
|
||||
last_seen: PremiumLastSeen,
|
||||
message_privacy: PremiumMessagePrivacy,
|
||||
effects: PremiumEffects,
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiPhoto, ApiUser,
|
||||
ApiChat,
|
||||
ApiMessage, ApiPeer, ApiPhoto,
|
||||
} from '../../api/types';
|
||||
import { MediaViewerOrigin, type ThreadId } from '../../types';
|
||||
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessage,
|
||||
selectChatMessages,
|
||||
selectChatScheduledMessages,
|
||||
@ -21,17 +22,17 @@ import {
|
||||
selectIsChatWithSelf,
|
||||
selectListedIds,
|
||||
selectOutlyingListByMessageId,
|
||||
selectPeer,
|
||||
selectPerformanceSettingsValue,
|
||||
selectScheduledMessage,
|
||||
selectTabState,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
||||
import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
import getViewableMedia, { getMediaViewerItem, type MediaViewerItem } from './helpers/getViewableMedia';
|
||||
import { animateClosing, animateOpening } from './helpers/ghostAnimation';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
@ -44,7 +45,6 @@ import useOldLang from '../../hooks/useOldLang';
|
||||
import { exitPictureInPictureIfNeeded, usePictureInPictureSignal } from '../../hooks/usePictureInPicture';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
|
||||
import ReportModal from '../common/ReportModal';
|
||||
@ -60,16 +60,17 @@ import './MediaViewer.scss';
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
threadId?: ThreadId;
|
||||
mediaId?: number;
|
||||
senderId?: string;
|
||||
messageId?: number;
|
||||
message?: ApiMessage;
|
||||
collectedMessageIds?: number[];
|
||||
isChatWithSelf?: boolean;
|
||||
canUpdateMedia?: boolean;
|
||||
origin?: MediaViewerOrigin;
|
||||
avatar?: ApiPhoto;
|
||||
avatarOwner?: ApiPeer;
|
||||
avatarOwnerFallbackPhoto?: ApiPhoto;
|
||||
message?: ApiMessage;
|
||||
chatMessages?: Record<number, ApiMessage>;
|
||||
collectionIds?: number[];
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
mediaIndex?: number;
|
||||
isHidden?: boolean;
|
||||
withAnimation?: boolean;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
@ -80,26 +81,27 @@ type StateProps = {
|
||||
|
||||
const ANIMATION_DURATION = 250;
|
||||
|
||||
const MediaViewer: FC<StateProps> = ({
|
||||
const MediaViewer = ({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId,
|
||||
senderId,
|
||||
messageId,
|
||||
message,
|
||||
collectedMessageIds,
|
||||
isChatWithSelf,
|
||||
canUpdateMedia,
|
||||
origin,
|
||||
avatar,
|
||||
avatarOwner,
|
||||
avatarOwnerFallbackPhoto,
|
||||
message,
|
||||
chatMessages,
|
||||
collectionIds,
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
withAnimation,
|
||||
isHidden,
|
||||
shouldSkipHistoryAnimations,
|
||||
withDynamicLoading,
|
||||
isLoadingMoreMedia,
|
||||
isSynced,
|
||||
}) => {
|
||||
}: StateProps) => {
|
||||
const {
|
||||
openMediaViewer,
|
||||
closeMediaViewer,
|
||||
@ -109,11 +111,12 @@ const MediaViewer: FC<StateProps> = ({
|
||||
searchChatMediaMessages,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(avatarOwner || mediaId);
|
||||
const isOpen = Boolean(avatarOwner || message || standaloneMedia);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
/* Animation */
|
||||
const animationKey = useRef<number>();
|
||||
const senderId = message?.senderId || avatarOwner?.id;
|
||||
const prevSenderId = usePrevious<string | undefined>(senderId);
|
||||
const headerAnimation = withAnimation ? 'slideFade' : 'none';
|
||||
const isGhostAnimation = Boolean(withAnimation && !shouldSkipHistoryAnimations);
|
||||
@ -121,40 +124,34 @@ const MediaViewer: FC<StateProps> = ({
|
||||
/* Controls */
|
||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||
|
||||
const currentItem = getMediaViewerItem({
|
||||
message, avatarOwner, standaloneMedia, mediaIndex,
|
||||
});
|
||||
const { media, isSingle } = getViewableMedia(currentItem) || {};
|
||||
|
||||
const {
|
||||
webPagePhoto,
|
||||
webPageVideo,
|
||||
isVideo,
|
||||
actionPhoto,
|
||||
isPhoto,
|
||||
bestImageData,
|
||||
bestData,
|
||||
dimensions,
|
||||
isGif,
|
||||
isFromSharedMedia,
|
||||
avatarPhoto,
|
||||
fileName,
|
||||
} = useMediaProps({
|
||||
message, avatarOwner, mediaId, origin, delay: isGhostAnimation && ANIMATION_DURATION,
|
||||
media, isAvatar: Boolean(avatarOwner), origin, delay: isGhostAnimation && ANIMATION_DURATION,
|
||||
});
|
||||
|
||||
const canReport = !!avatarPhoto && !isChatWithSelf;
|
||||
const canReport = avatarOwner && !isChatWithSelf;
|
||||
const isVisible = !isHidden && isOpen;
|
||||
|
||||
/* Navigation */
|
||||
const singleMediaId = webPagePhoto || webPageVideo || actionPhoto || isGif ? mediaId : undefined;
|
||||
const messageMediaIds = useMemo(() => {
|
||||
return withDynamicLoading
|
||||
? collectedMessageIds
|
||||
: getChatMediaMessageIds(chatMessages || {}, collectedMessageIds || [], isFromSharedMedia);
|
||||
}, [chatMessages, collectedMessageIds, isFromSharedMedia, withDynamicLoading]);
|
||||
|
||||
const mediaIds = useMemo(() => {
|
||||
if (singleMediaId) return [singleMediaId];
|
||||
if (avatarOwner) return avatarOwner.photos?.map((p, i) => i) || [];
|
||||
if (withDynamicLoading) return collectionIds || [];
|
||||
return getChatMediaMessageIds(chatMessages || {}, collectionIds || [], isFromSharedMedia);
|
||||
}, [singleMediaId, avatarOwner, chatMessages, collectionIds, isFromSharedMedia, withDynamicLoading]);
|
||||
|
||||
const selectedMediaIndex = mediaId ? mediaIds.indexOf(mediaId) : -1;
|
||||
|
||||
if (isOpen && (!prevSenderId || prevSenderId !== senderId || !animationKey.current)) {
|
||||
animationKey.current = selectedMediaIndex;
|
||||
if (isOpen && (!prevSenderId || prevSenderId !== senderId || animationKey.current === undefined)) {
|
||||
animationKey.current = isSingle ? 0 : (messageId || mediaIndex);
|
||||
}
|
||||
|
||||
const [getIsPictureInPicture] = usePictureInPictureSignal();
|
||||
@ -202,65 +199,48 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const prevMessage = usePrevious<ApiMessage | undefined>(message);
|
||||
const prevIsHidden = usePrevious<boolean | undefined>(isHidden);
|
||||
const prevOrigin = usePrevious(origin);
|
||||
const prevMediaId = usePrevious(mediaId);
|
||||
const prevAvatarOwner = usePrevious<ApiPeer | undefined>(avatarOwner);
|
||||
const prevItem = usePrevious(currentItem);
|
||||
const prevBestImageData = usePrevious(bestImageData);
|
||||
const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined;
|
||||
const hasFooter = Boolean(textParts);
|
||||
const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId;
|
||||
const shouldAnimateOpening = prevIsHidden && prevItem !== currentItem;
|
||||
|
||||
useEffect(() => {
|
||||
if (isGhostAnimation && isOpen && (!prevMessage || shouldAnimateOpening) && !prevAvatarOwner) {
|
||||
if (isGhostAnimation && isOpen && (shouldAnimateOpening || !prevItem)) {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateOpening(hasFooter, origin!, bestImageData!, dimensions!, isVideo, message);
|
||||
animateOpening(hasFooter, origin!, bestImageData!, dimensions!, isVideo, message, mediaIndex);
|
||||
}
|
||||
|
||||
if (isGhostAnimation && !isOpen && (prevMessage || prevAvatarOwner)) {
|
||||
if (isGhostAnimation && !isOpen && prevItem) {
|
||||
dispatchHeavyAnimationEvent(ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
animateClosing(prevOrigin!, prevBestImageData!, prevMessage || undefined);
|
||||
animateClosing(prevOrigin!, prevBestImageData!, prevMessage, prevItem?.mediaIndex);
|
||||
}
|
||||
}, [
|
||||
isGhostAnimation, isOpen, shouldAnimateOpening, origin, prevOrigin, message, prevMessage, prevAvatarOwner,
|
||||
bestImageData, prevBestImageData, dimensions, isVideo, hasFooter,
|
||||
bestImageData, dimensions, hasFooter, isGhostAnimation, isOpen, isVideo, message, origin,
|
||||
prevBestImageData, prevItem, prevMessage, prevOrigin, shouldAnimateOpening, mediaIndex,
|
||||
]);
|
||||
|
||||
const handleClose = useLastCallback(() => closeMediaViewer());
|
||||
|
||||
const mediaIdRef = useStateRef(mediaId);
|
||||
const handleFooterClick = useLastCallback(() => {
|
||||
handleClose();
|
||||
|
||||
const currentMediaId = mediaIdRef.current;
|
||||
|
||||
if (!chatId || !currentMediaId) return;
|
||||
if (!chatId || !messageId) return;
|
||||
|
||||
if (isMobile) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, threadId, messageId: currentMediaId });
|
||||
focusMessage({ chatId, threadId, messageId });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
focusMessage({ chatId, threadId, messageId: currentMediaId });
|
||||
focusMessage({ chatId, threadId, messageId });
|
||||
}
|
||||
});
|
||||
|
||||
const handleForward = useLastCallback(() => {
|
||||
openForwardMenu({
|
||||
fromChatId: chatId!,
|
||||
messageIds: [mediaId!],
|
||||
});
|
||||
});
|
||||
|
||||
const selectMedia = useLastCallback((id?: number) => {
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId: id,
|
||||
avatarOwnerId: avatarOwner?.id,
|
||||
origin: origin!,
|
||||
withDynamicLoading,
|
||||
}, {
|
||||
forceOnHeavyAnimation: true,
|
||||
messageIds: [messageId!],
|
||||
});
|
||||
});
|
||||
|
||||
@ -274,56 +254,106 @@ const MediaViewer: FC<StateProps> = ({
|
||||
}
|
||||
}, [isGif, isVideo]);
|
||||
|
||||
const mediaIdsRef = useStateRef(mediaIds);
|
||||
|
||||
const loadMoreMediaIfNeeded = useLastCallback((activeMediaId?: number) => {
|
||||
if (!activeMediaId || !withDynamicLoading || isLoadingMoreMedia) return;
|
||||
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: activeMediaId });
|
||||
const loadMoreItemsIfNeeded = useLastCallback((item?: MediaViewerItem) => {
|
||||
if (!item || !withDynamicLoading || isLoadingMoreMedia) return;
|
||||
if (item.type !== 'message') return;
|
||||
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: item.message.id });
|
||||
});
|
||||
|
||||
const getMediaId = useLastCallback((fromId?: number, direction?: number): number | undefined => {
|
||||
if (fromId === undefined) return undefined;
|
||||
const mIds = mediaIdsRef.current;
|
||||
const index = mIds.indexOf(fromId);
|
||||
if ((direction === -1 && index > 0) || (direction === 1 && index < mIds.length - 1)) {
|
||||
return mIds[index + direction];
|
||||
const getNextItem = useLastCallback((from: MediaViewerItem, direction: number): MediaViewerItem | undefined => {
|
||||
if (direction === 0 || isSingle) return undefined;
|
||||
|
||||
if (from.type === 'standalone') {
|
||||
const { media: fromMedia, mediaIndex: fromMediaIndex } = from;
|
||||
const nextIndex = fromMediaIndex + direction;
|
||||
if (nextIndex >= 0 && nextIndex < fromMedia.length) {
|
||||
return { type: 'standalone', media: fromMedia, mediaIndex: nextIndex };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
// Fallback
|
||||
if (isVisible) loadMoreMediaIfNeeded(fromId);
|
||||
|
||||
if (from.type === 'avatar') {
|
||||
const { avatarOwner: fromAvatarOwner, mediaIndex: fromMediaIndex } = from;
|
||||
const nextIndex = fromMediaIndex + direction;
|
||||
if (nextIndex >= 0 && fromAvatarOwner.photos && nextIndex < fromAvatarOwner.photos.length) {
|
||||
return { type: 'avatar', avatarOwner: fromAvatarOwner, mediaIndex: nextIndex };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { message: fromMessage, mediaIndex: fromMediaIndex } = from;
|
||||
|
||||
const paidMedia = getMessagePaidMedia(fromMessage);
|
||||
if (paidMedia) {
|
||||
const nextIndex = fromMediaIndex! + direction;
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < paidMedia.extendedMedia.length) {
|
||||
return { type: 'message', message: fromMessage, mediaIndex: nextIndex };
|
||||
}
|
||||
}
|
||||
|
||||
const index = messageMediaIds?.indexOf(fromMessage.id);
|
||||
if (index === undefined) return undefined;
|
||||
const nextIndex = index + direction;
|
||||
const nextMessageId = messageMediaIds![nextIndex];
|
||||
const nextMessage = chatMessages?.[nextMessageId];
|
||||
if (nextMessage) {
|
||||
return { type: 'message', message: nextMessage };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const handleBeforeDelete = useLastCallback(() => {
|
||||
if (mediaIds.length <= 1) {
|
||||
const openMediaViewerItem = useLastCallback((item?: MediaViewerItem) => {
|
||||
if (!item) {
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
let index = mediaId ? mediaIds.indexOf(mediaId) : -1;
|
||||
// Before deleting, select previous media or the first one
|
||||
index = index > 0 ? index - 1 : 0;
|
||||
selectMedia(mediaIds[index]);
|
||||
|
||||
const itemChatId = item.type === 'avatar'
|
||||
? item.avatarOwner.id : item.type === 'message'
|
||||
? item.message.chatId : undefined;
|
||||
const itemMessageId = item.type === 'message' ? item.message.id : undefined;
|
||||
const itemStandaloneMedia = item.type === 'standalone' ? item.media : undefined;
|
||||
|
||||
openMediaViewer({
|
||||
origin: origin!,
|
||||
chatId: itemChatId,
|
||||
messageId: itemMessageId,
|
||||
standaloneMedia: itemStandaloneMedia,
|
||||
mediaIndex: item.mediaIndex,
|
||||
isAvatarView: item.type === 'avatar',
|
||||
withDynamicLoading,
|
||||
}, {
|
||||
forceOnHeavyAnimation: true,
|
||||
});
|
||||
});
|
||||
|
||||
const handleBeforeDelete = useLastCallback(() => {
|
||||
const mediaCount = avatarOwner?.photos?.length || standaloneMedia?.length || messageMediaIds?.length || 0;
|
||||
if (mediaCount <= 1 || !currentItem) {
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
// Before deleting, select previous media
|
||||
const prevMedia = getNextItem(currentItem, -1);
|
||||
if (prevMedia) {
|
||||
openMediaViewerItem(prevMedia);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentItem.type === 'avatar' || currentItem.type === 'standalone') {
|
||||
// Keep current item, it'll update when indexes shift
|
||||
return;
|
||||
}
|
||||
|
||||
handleClose();
|
||||
});
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
function renderSenderInfo() {
|
||||
return avatarOwner ? (
|
||||
<SenderInfo
|
||||
key={mediaId}
|
||||
chatId={avatarOwner.id}
|
||||
isAvatar
|
||||
isFallbackAvatar={isUserId(avatarOwner.id)
|
||||
&& (avatarOwner as ApiUser).photos?.[mediaId!]?.id === avatarOwnerFallbackPhoto?.id}
|
||||
/>
|
||||
) : (
|
||||
<SenderInfo
|
||||
key={mediaId}
|
||||
chatId={chatId}
|
||||
messageId={mediaId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowTransition
|
||||
id="MediaViewer"
|
||||
@ -346,18 +376,17 @@ const MediaViewer: FC<StateProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
<Transition activeKey={animationKey.current!} name={headerAnimation}>
|
||||
{renderSenderInfo()}
|
||||
<SenderInfo
|
||||
key={media?.id}
|
||||
item={currentItem}
|
||||
/>
|
||||
</Transition>
|
||||
<MediaViewerActions
|
||||
mediaData={bestData}
|
||||
isVideo={isVideo}
|
||||
message={message}
|
||||
item={currentItem}
|
||||
canUpdateMedia={canUpdateMedia}
|
||||
avatarPhoto={avatarPhoto}
|
||||
avatarOwner={avatarOwner}
|
||||
fileName={fileName}
|
||||
canReport={canReport}
|
||||
selectMedia={selectMedia}
|
||||
onBeforeDelete={handleBeforeDelete}
|
||||
onReport={openReportModal}
|
||||
onCloseMediaViewer={handleClose}
|
||||
@ -367,16 +396,16 @@ const MediaViewer: FC<StateProps> = ({
|
||||
isOpen={isReportModalOpen}
|
||||
onClose={closeReportModal}
|
||||
subject="media"
|
||||
photo={avatarPhoto}
|
||||
photo={avatar}
|
||||
peerId={avatarOwner?.id}
|
||||
/>
|
||||
</div>
|
||||
<MediaViewerSlides
|
||||
mediaId={mediaId}
|
||||
loadMoreMediaIfNeeded={loadMoreMediaIfNeeded}
|
||||
item={currentItem}
|
||||
loadMoreItemsIfNeeded={loadMoreItemsIfNeeded}
|
||||
isLoadingMoreMedia={isLoadingMoreMedia}
|
||||
isSynced={isSynced}
|
||||
getMediaId={getMediaId}
|
||||
getNextItem={getNextItem}
|
||||
chatId={chatId}
|
||||
isPhoto={isPhoto}
|
||||
isGif={isGif}
|
||||
@ -388,7 +417,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
isVideo={isVideo}
|
||||
withAnimation={withAnimation}
|
||||
onClose={handleClose}
|
||||
selectMedia={selectMedia}
|
||||
selectItem={openMediaViewerItem}
|
||||
isHidden={isHidden}
|
||||
onFooterClick={handleFooterClick}
|
||||
/>
|
||||
@ -402,122 +431,95 @@ export default memo(withGlobal(
|
||||
const {
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
messageId,
|
||||
origin,
|
||||
isHidden,
|
||||
withDynamicLoading,
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
isAvatarView,
|
||||
} = mediaViewer;
|
||||
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
|
||||
|
||||
const { currentUserId, isSynced } = global;
|
||||
let isChatWithSelf = !!chatId && selectIsChatWithSelf(global, chatId);
|
||||
const isChatWithSelf = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && mediaId)) {
|
||||
return { withAnimation, shouldSkipHistoryAnimations };
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, mediaId);
|
||||
if (!message) {
|
||||
return { withAnimation, shouldSkipHistoryAnimations };
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
isChatWithSelf,
|
||||
origin,
|
||||
message,
|
||||
withAnimation,
|
||||
isHidden,
|
||||
shouldSkipHistoryAnimations,
|
||||
};
|
||||
}
|
||||
|
||||
if (avatarOwnerId) {
|
||||
const user = selectUser(global, avatarOwnerId);
|
||||
const chat = selectChat(global, avatarOwnerId);
|
||||
if (isAvatarView) {
|
||||
const peer = selectPeer(global, chatId!);
|
||||
let canUpdateMedia = false;
|
||||
if (user) {
|
||||
canUpdateMedia = avatarOwnerId === currentUserId;
|
||||
} else if (chat) {
|
||||
canUpdateMedia = isChatAdmin(chat);
|
||||
if (peer) {
|
||||
canUpdateMedia = isUserId(peer.id) ? peer.id === currentUserId : isChatAdmin(peer as ApiChat);
|
||||
}
|
||||
|
||||
isChatWithSelf = selectIsChatWithSelf(global, avatarOwnerId);
|
||||
|
||||
return {
|
||||
mediaId,
|
||||
senderId: avatarOwnerId,
|
||||
avatarOwner: user || chat,
|
||||
avatarOwnerFallbackPhoto: user ? selectUserFullInfo(global, avatarOwnerId)?.fallbackPhoto : undefined,
|
||||
avatar: peer?.photos?.[mediaIndex!],
|
||||
avatarOwner: peer,
|
||||
isChatWithSelf,
|
||||
canUpdateMedia,
|
||||
withAnimation,
|
||||
origin,
|
||||
shouldSkipHistoryAnimations,
|
||||
isHidden,
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(chatId && threadId && mediaId)) {
|
||||
return { withAnimation, shouldSkipHistoryAnimations };
|
||||
}
|
||||
|
||||
let message: ApiMessage | undefined;
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
message = selectScheduledMessage(global, chatId, mediaId);
|
||||
} else {
|
||||
message = selectChatMessage(global, chatId, mediaId);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return { withAnimation, shouldSkipHistoryAnimations };
|
||||
if (chatId && messageId) {
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
message = selectScheduledMessage(global, chatId, messageId);
|
||||
} else {
|
||||
message = selectChatMessage(global, chatId, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
let chatMessages: Record<number, ApiMessage> | undefined;
|
||||
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
chatMessages = selectChatScheduledMessages(global, chatId);
|
||||
} else {
|
||||
chatMessages = selectChatMessages(global, chatId);
|
||||
if (chatId) {
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
chatMessages = selectChatScheduledMessages(global, chatId);
|
||||
} else {
|
||||
chatMessages = selectChatMessages(global, chatId);
|
||||
}
|
||||
}
|
||||
|
||||
let isLoadingMoreMedia = false;
|
||||
const isOriginInline = origin === MediaViewerOrigin.Inline;
|
||||
const isOriginAlbum = origin === MediaViewerOrigin.Album;
|
||||
let collectionIds: number[] | undefined;
|
||||
let collectedMessageIds: number[] | undefined;
|
||||
|
||||
if (withDynamicLoading && (isOriginInline || isOriginAlbum)) {
|
||||
const currentSearch = selectCurrentChatMediaSearch(global);
|
||||
isLoadingMoreMedia = Boolean(currentSearch?.isLoading);
|
||||
const { foundIds } = (currentSearch?.currentSegment) || {};
|
||||
collectionIds = foundIds;
|
||||
} else if (origin === MediaViewerOrigin.SharedMedia) {
|
||||
const currentSearch = selectCurrentSharedMediaSearch(global);
|
||||
const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {};
|
||||
collectionIds = foundIds;
|
||||
} else if (isOriginInline || isOriginAlbum) {
|
||||
const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, message.id);
|
||||
collectionIds = outlyingList || selectListedIds(global, chatId, threadId);
|
||||
if (chatId && threadId && messageId) {
|
||||
if (withDynamicLoading && (isOriginInline || isOriginAlbum)) {
|
||||
const currentSearch = selectCurrentChatMediaSearch(global);
|
||||
isLoadingMoreMedia = Boolean(currentSearch?.isLoading);
|
||||
const { foundIds } = (currentSearch?.currentSegment) || {};
|
||||
collectedMessageIds = foundIds;
|
||||
} else if (origin === MediaViewerOrigin.SharedMedia) {
|
||||
const currentSearch = selectCurrentSharedMediaSearch(global);
|
||||
const { foundIds } = (currentSearch && currentSearch.resultsByType && currentSearch.resultsByType.media) || {};
|
||||
collectedMessageIds = foundIds;
|
||||
} else if (isOriginInline || isOriginAlbum) {
|
||||
const outlyingList = selectOutlyingListByMessageId(global, chatId, threadId, messageId);
|
||||
collectedMessageIds = outlyingList || selectListedIds(global, chatId, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
messageId,
|
||||
isChatWithSelf,
|
||||
origin,
|
||||
message,
|
||||
chatMessages,
|
||||
collectionIds,
|
||||
collectedMessageIds,
|
||||
withAnimation,
|
||||
isHidden,
|
||||
shouldSkipHistoryAnimations,
|
||||
withDynamicLoading,
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
isLoadingMoreMedia,
|
||||
isSynced,
|
||||
};
|
||||
|
||||
@ -2,20 +2,27 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiPhoto,
|
||||
} from '../../api/types';
|
||||
import type { MessageListType } from '../../global/types';
|
||||
import type { ActiveDownloads, MessageListType } from '../../global/types';
|
||||
import type { MediaViewerOrigin } from '../../types';
|
||||
import type { MenuItemProps } from '../ui/MenuItem';
|
||||
import type { MediaViewerItem } from './helpers/getViewableMedia';
|
||||
|
||||
import { getMessageMediaFormat, getMessageMediaHash, isUserId } from '../../global/helpers';
|
||||
import {
|
||||
getIsDownloading,
|
||||
getMediaFilename,
|
||||
getMediaFormat,
|
||||
getMediaHash,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectActiveDownloads,
|
||||
selectAllowedMessageActions,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatProtected,
|
||||
selectIsDownloading,
|
||||
selectIsMessageProtected,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import getViewableMedia from './helpers/getViewableMedia';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -34,26 +41,22 @@ import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
import './MediaViewerActions.scss';
|
||||
|
||||
type StateProps = {
|
||||
isDownloading?: boolean;
|
||||
activeDownloads: ActiveDownloads;
|
||||
isProtected?: boolean;
|
||||
isChatProtected?: boolean;
|
||||
canDelete?: boolean;
|
||||
canUpdate?: boolean;
|
||||
messageListType?: MessageListType;
|
||||
avatarOwnerId?: string;
|
||||
origin?: MediaViewerOrigin;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
item?: MediaViewerItem;
|
||||
mediaData?: string;
|
||||
isVideo: boolean;
|
||||
message?: ApiMessage;
|
||||
canUpdateMedia?: boolean;
|
||||
isSingleMedia?: boolean;
|
||||
avatarPhoto?: ApiPhoto;
|
||||
avatarOwner?: ApiPeer;
|
||||
fileName?: string;
|
||||
canReport?: boolean;
|
||||
selectMedia: (mediaId?: number) => void;
|
||||
activeDownloads?: ActiveDownloads;
|
||||
onReport: NoneToVoidFunction;
|
||||
onBeforeDelete: NoneToVoidFunction;
|
||||
onCloseMediaViewer: NoneToVoidFunction;
|
||||
@ -61,20 +64,17 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
item,
|
||||
mediaData,
|
||||
isVideo,
|
||||
message,
|
||||
avatarPhoto,
|
||||
avatarOwnerId,
|
||||
fileName,
|
||||
isChatProtected,
|
||||
isDownloading,
|
||||
isProtected,
|
||||
canReport,
|
||||
canDelete,
|
||||
canUpdate,
|
||||
messageListType,
|
||||
selectMedia,
|
||||
activeDownloads,
|
||||
origin,
|
||||
onReport,
|
||||
onCloseMediaViewer,
|
||||
onBeforeDelete,
|
||||
@ -85,23 +85,32 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const {
|
||||
downloadMessageMedia,
|
||||
cancelMessageMediaDownload,
|
||||
downloadMedia,
|
||||
cancelMediaDownload,
|
||||
updateProfilePhoto,
|
||||
updateChatPhoto,
|
||||
openMediaViewer,
|
||||
} = getActions();
|
||||
|
||||
const isMessage = item?.type === 'message';
|
||||
|
||||
const { media } = getViewableMedia(item) || {};
|
||||
const fileName = media && getMediaFilename(media);
|
||||
const isDownloading = media && getIsDownloading(activeDownloads, media);
|
||||
|
||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||
message && getMessageMediaHash(message, 'download'),
|
||||
media && getMediaHash(media, 'download'),
|
||||
!isDownloading,
|
||||
message && getMessageMediaFormat(message, 'download'),
|
||||
media && getMediaFormat(media, 'download'),
|
||||
);
|
||||
|
||||
const handleDownloadClick = useLastCallback(() => {
|
||||
if (!media) return;
|
||||
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message: message! });
|
||||
cancelMediaDownload({ media });
|
||||
} else {
|
||||
downloadMessageMedia({ message: message! });
|
||||
downloadMedia({ media });
|
||||
}
|
||||
});
|
||||
|
||||
@ -118,13 +127,23 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleUpdate = useLastCallback(() => {
|
||||
if (!avatarPhoto || !avatarOwnerId) return;
|
||||
if (isUserId(avatarOwnerId)) {
|
||||
if (item?.type !== 'avatar') return;
|
||||
const { avatarOwner, mediaIndex } = item;
|
||||
const avatarPhoto = avatarOwner.photos?.[mediaIndex]!;
|
||||
if (isUserId(avatarOwner.id)) {
|
||||
updateProfilePhoto({ photo: avatarPhoto });
|
||||
} else {
|
||||
updateChatPhoto({ chatId: avatarOwnerId, photo: avatarPhoto });
|
||||
updateChatPhoto({ chatId: avatarOwner.id, photo: avatarPhoto });
|
||||
}
|
||||
selectMedia(0);
|
||||
|
||||
openMediaViewer({
|
||||
origin: origin!,
|
||||
chatId: avatarOwner.id,
|
||||
mediaIndex: 0,
|
||||
isAvatarView: true,
|
||||
}, {
|
||||
forceOnHeavyAnimation: true,
|
||||
});
|
||||
});
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -145,29 +164,34 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
}, []);
|
||||
|
||||
function renderDeleteModals() {
|
||||
return message
|
||||
? (
|
||||
if (item?.type === 'message') {
|
||||
return (
|
||||
<DeleteMessageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
isSchedule={messageListType === 'scheduled'}
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={onBeforeDelete}
|
||||
message={message}
|
||||
message={item.message}
|
||||
/>
|
||||
)
|
||||
: (avatarOwnerId && avatarPhoto) ? (
|
||||
);
|
||||
}
|
||||
if (item?.type === 'avatar') {
|
||||
return (
|
||||
<DeleteProfilePhotoModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={onBeforeDelete}
|
||||
profileId={avatarOwnerId}
|
||||
photo={avatarPhoto}
|
||||
profileId={item.avatarOwner.id}
|
||||
photo={item.avatarOwner.photos![item.mediaIndex!]}
|
||||
/>
|
||||
) : undefined;
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function renderDownloadButton() {
|
||||
if (isProtected) {
|
||||
if (isProtected || item?.type === 'standalone') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -201,7 +225,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isMobile) {
|
||||
const menuItems: MenuItemProps[] = [];
|
||||
if (message?.isForwardingAllowed && !isChatProtected) {
|
||||
if (isMessage && item.message.isForwardingAllowed && !item.message.content.action && !isChatProtected) {
|
||||
menuItems.push({
|
||||
icon: 'forward',
|
||||
onClick: onForward,
|
||||
@ -283,7 +307,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="MediaViewerActions">
|
||||
{message?.isForwardingAllowed && !isChatProtected && (
|
||||
{isMessage && item.message.isForwardingAllowed && !isChatProtected && (
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
@ -362,12 +386,19 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, {
|
||||
message, canUpdateMedia, avatarPhoto, avatarOwner,
|
||||
item, canUpdateMedia,
|
||||
}): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const { origin } = tabState.mediaViewer;
|
||||
|
||||
const message = item?.type === 'message' ? item.message : undefined;
|
||||
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined;
|
||||
const avatarPhoto = avatarOwner?.photos?.[item!.mediaIndex!];
|
||||
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
const isDownloading = message ? selectIsDownloading(global, message) : false;
|
||||
const isProtected = selectIsMessageProtected(global, message);
|
||||
const activeDownloads = selectActiveDownloads(global);
|
||||
const isChatProtected = message && selectIsChatProtected(global, message?.chatId);
|
||||
const { canDelete: canDeleteMessage } = (threadId
|
||||
&& message && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||
@ -378,13 +409,13 @@ export default memo(withGlobal<OwnProps>(
|
||||
const messageListType = currentMessageList?.type;
|
||||
|
||||
return {
|
||||
isDownloading,
|
||||
activeDownloads,
|
||||
isProtected,
|
||||
isChatProtected,
|
||||
canDelete,
|
||||
canUpdate,
|
||||
messageListType,
|
||||
avatarOwnerId: avatarOwner?.id,
|
||||
origin,
|
||||
};
|
||||
},
|
||||
)(MediaViewerActions));
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiDimensions, ApiMessage, ApiPeer,
|
||||
ApiDimensions, ApiMessage,
|
||||
} from '../../api/types';
|
||||
import { MediaViewerOrigin, type ThreadId } from '../../types';
|
||||
import type { MediaViewerOrigin } from '../../types';
|
||||
import type { MediaViewerItem } from './helpers/getViewableMedia';
|
||||
|
||||
import {
|
||||
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectTabState, selectUser,
|
||||
selectIsMessageProtected, selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import { ARE_WEBCODECS_SUPPORTED, IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
import getViewableMedia from './helpers/getViewableMedia';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
@ -29,25 +30,16 @@ import VideoPlayer from './VideoPlayer';
|
||||
import './MediaViewerContent.scss';
|
||||
|
||||
type OwnProps = {
|
||||
mediaId?: number;
|
||||
chatId?: string;
|
||||
threadId?: ThreadId;
|
||||
avatarOwnerId?: string;
|
||||
origin?: MediaViewerOrigin;
|
||||
item: MediaViewerItem;
|
||||
isActive?: boolean;
|
||||
withAnimation?: boolean;
|
||||
isMoving?: boolean;
|
||||
onClose: () => void;
|
||||
onFooterClick: () => void;
|
||||
isMoving?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
mediaId?: number;
|
||||
senderId?: string;
|
||||
threadId?: ThreadId;
|
||||
avatarOwner?: ApiPeer;
|
||||
message?: ApiMessage;
|
||||
textMessage?: ApiMessage;
|
||||
origin?: MediaViewerOrigin;
|
||||
isProtected?: boolean;
|
||||
volume: number;
|
||||
@ -59,56 +51,56 @@ type StateProps = {
|
||||
const ANIMATION_DURATION = 350;
|
||||
const MOBILE_VERSION_CONTROL_WIDTH = 350;
|
||||
|
||||
const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
const {
|
||||
mediaId,
|
||||
isActive,
|
||||
avatarOwner,
|
||||
chatId,
|
||||
message,
|
||||
origin,
|
||||
withAnimation,
|
||||
isProtected,
|
||||
volume,
|
||||
playbackRate,
|
||||
isMuted,
|
||||
isHidden,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
isMoving,
|
||||
} = props;
|
||||
|
||||
const MediaViewerContent = ({
|
||||
item,
|
||||
isActive,
|
||||
textMessage,
|
||||
origin,
|
||||
withAnimation,
|
||||
isProtected,
|
||||
volume,
|
||||
playbackRate,
|
||||
isMuted,
|
||||
isHidden,
|
||||
isMoving,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
}: OwnProps & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const isAvatar = item.type === 'avatar';
|
||||
const { media } = getViewableMedia(item) || {};
|
||||
|
||||
const {
|
||||
isVideo,
|
||||
isPhoto,
|
||||
actionPhoto,
|
||||
bestImageData,
|
||||
bestData,
|
||||
dimensions,
|
||||
isGif,
|
||||
isLocal,
|
||||
isVideoAvatar,
|
||||
videoSize,
|
||||
mediaSize,
|
||||
loadProgress,
|
||||
} = useMediaProps({
|
||||
message, avatarOwner, mediaId, origin, delay: withAnimation ? ANIMATION_DURATION : false,
|
||||
media, isAvatar, origin, delay: withAnimation ? ANIMATION_DURATION : false,
|
||||
});
|
||||
|
||||
const [, toggleControls] = useControlsSignal();
|
||||
|
||||
const isOpen = Boolean(avatarOwner || mediaId);
|
||||
const isOpen = Boolean(media);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const toggleControlsOnMove = useLastCallback(() => {
|
||||
toggleControls(true);
|
||||
});
|
||||
|
||||
if (avatarOwner || actionPhoto) {
|
||||
if (!media) return undefined;
|
||||
|
||||
if (item.type !== 'message') {
|
||||
if (!isVideoAvatar) {
|
||||
return (
|
||||
<div key={chatId} className="MediaViewerContent">
|
||||
<div key={media.id} className="MediaViewerContent">
|
||||
{renderPhoto(
|
||||
bestData,
|
||||
calculateMediaViewerDimensions(dimensions!, false),
|
||||
@ -119,15 +111,15 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={chatId} className="MediaViewerContent">
|
||||
<div key={media.id} className="MediaViewerContent">
|
||||
<VideoPlayer
|
||||
key={mediaId}
|
||||
key={media.id}
|
||||
url={bestData}
|
||||
isGif
|
||||
posterData={bestImageData}
|
||||
posterSize={calculateMediaViewerDimensions(dimensions!, false, true)}
|
||||
loadProgress={loadProgress}
|
||||
fileSize={videoSize!}
|
||||
fileSize={mediaSize!}
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
isProtected={isProtected}
|
||||
isPreviewDisabled={!ARE_WEBCODECS_SUPPORTED || isLocal}
|
||||
@ -144,13 +136,13 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) return undefined;
|
||||
const textParts = message.content.action?.type === 'suggestProfilePhoto'
|
||||
if (!textMessage) return undefined;
|
||||
const textParts = textMessage.content.action?.type === 'suggestProfilePhoto'
|
||||
? lang('Conversation.SuggestedPhotoTitle')
|
||||
: renderMessageText({ message, forcePlayback: true, isForMediaViewer: true });
|
||||
: renderMessageText({ message: textMessage, forcePlayback: true, isForMediaViewer: true });
|
||||
|
||||
const hasFooter = Boolean(textParts);
|
||||
const posterSize = message && calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);
|
||||
const posterSize = calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);
|
||||
const isForceMobileVersion = isMobile || shouldForceMobileVersion(posterSize);
|
||||
|
||||
return (
|
||||
@ -171,13 +163,13 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
isProtected,
|
||||
) : (
|
||||
<VideoPlayer
|
||||
key={mediaId}
|
||||
key={media.id}
|
||||
url={bestData}
|
||||
isGif={isGif}
|
||||
posterData={bestImageData}
|
||||
posterSize={posterSize}
|
||||
loadProgress={loadProgress}
|
||||
fileSize={videoSize!}
|
||||
fileSize={mediaSize!}
|
||||
isMediaViewerOpen={isOpen && isActive}
|
||||
noPlay={!isActive}
|
||||
isPreviewDisabled={!ARE_WEBCODECS_SUPPORTED || isLocal}
|
||||
@ -205,84 +197,20 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps): StateProps => {
|
||||
const {
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId,
|
||||
avatarOwnerId,
|
||||
origin,
|
||||
} = ownProps;
|
||||
|
||||
(global, { item }): StateProps => {
|
||||
const {
|
||||
volume,
|
||||
isMuted,
|
||||
playbackRate,
|
||||
isHidden,
|
||||
origin,
|
||||
} = selectTabState(global).mediaViewer;
|
||||
|
||||
if (origin === MediaViewerOrigin.SearchResult) {
|
||||
if (!(chatId && mediaId)) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, mediaId);
|
||||
if (!message) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
volume,
|
||||
isMuted,
|
||||
isHidden,
|
||||
playbackRate,
|
||||
};
|
||||
}
|
||||
|
||||
if (avatarOwnerId) {
|
||||
const sender = selectUser(global, avatarOwnerId) || selectChat(global, avatarOwnerId);
|
||||
|
||||
return {
|
||||
mediaId,
|
||||
senderId: avatarOwnerId,
|
||||
avatarOwner: sender,
|
||||
origin,
|
||||
volume,
|
||||
isMuted,
|
||||
isHidden,
|
||||
playbackRate,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(chatId && threadId && mediaId)) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
|
||||
let message: ApiMessage | undefined;
|
||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||
message = selectScheduledMessage(global, chatId, mediaId);
|
||||
} else {
|
||||
message = selectChatMessage(global, chatId, mediaId);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return { volume, isMuted, playbackRate };
|
||||
}
|
||||
const textMessage = item.type === 'message' ? item.message : undefined;
|
||||
|
||||
return {
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId,
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
textMessage,
|
||||
isProtected: textMessage && selectIsMessageProtected(global, textMessage),
|
||||
volume,
|
||||
isMuted,
|
||||
isHidden,
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect, useRef, useSignal, useState,
|
||||
memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { MediaViewerOrigin, ThreadId } from '../../types';
|
||||
import type { RealTouchEvent } from '../../util/captureEvents';
|
||||
import type { MediaViewerItem } from './helpers/getViewableMedia';
|
||||
|
||||
import { animateNumber, timingFunctions } from '../../util/animation';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -37,25 +38,25 @@ import './MediaViewerSlides.scss';
|
||||
const { easeOutCubic, easeOutQuart } = timingFunctions;
|
||||
|
||||
type OwnProps = {
|
||||
mediaId?: number;
|
||||
loadMoreMediaIfNeeded: (activeMediaId?: number) => void;
|
||||
item?: MediaViewerItem;
|
||||
isLoadingMoreMedia?: boolean;
|
||||
isSynced?: boolean;
|
||||
getMediaId: (fromId?: number, direction?: number) => number | undefined;
|
||||
isVideo?: boolean;
|
||||
isGif?: boolean;
|
||||
isPhoto?: boolean;
|
||||
isOpen?: boolean;
|
||||
selectMedia: (id?: number) => void;
|
||||
chatId?: string;
|
||||
threadId?: ThreadId;
|
||||
avatarOwnerId?: string;
|
||||
origin?: MediaViewerOrigin;
|
||||
withAnimation?: boolean;
|
||||
onClose: () => void;
|
||||
isHidden?: boolean;
|
||||
hasFooter?: boolean;
|
||||
getNextItem: (from: MediaViewerItem, direction: number) => MediaViewerItem | undefined;
|
||||
selectItem: (item: MediaViewerItem) => void;
|
||||
loadMoreItemsIfNeeded: (item: MediaViewerItem) => void;
|
||||
onFooterClick: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const SWIPE_X_THRESHOLD = 50;
|
||||
@ -85,10 +86,7 @@ enum SwipeDirection {
|
||||
}
|
||||
|
||||
const MediaViewerSlides: FC<OwnProps> = ({
|
||||
mediaId,
|
||||
loadMoreMediaIfNeeded,
|
||||
getMediaId,
|
||||
selectMedia,
|
||||
item,
|
||||
isVideo,
|
||||
isGif,
|
||||
isOpen,
|
||||
@ -96,7 +94,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
isHidden,
|
||||
isLoadingMoreMedia,
|
||||
isSynced,
|
||||
...rest
|
||||
loadMoreItemsIfNeeded,
|
||||
getNextItem,
|
||||
selectItem,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -117,13 +119,12 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const [getTransform, setTransform] = useSignal<Transform>({ x: 0, y: 0, scale: 1 });
|
||||
const transformRef = useSignalRef(getTransform);
|
||||
const [getActiveMediaId, setActiveMediaId] = useSignal<number | undefined>(mediaId);
|
||||
const activeMediaIdRef = useSignalRef(getActiveMediaId);
|
||||
const [getActiveItem, setActiveItem] = useSignal<MediaViewerItem | undefined>(item);
|
||||
const activeItemRef = useSignalRef(getActiveItem);
|
||||
const isScaled = useDerivedState(() => getTransform().scale !== 1, [getTransform]);
|
||||
const activeMediaId = useDerivedState(getActiveMediaId);
|
||||
const activeItem = useDerivedState(getActiveItem);
|
||||
const { height: windowHeight, width: windowWidth, isResizing } = useWindowSize();
|
||||
const [getControlsVisible, setControlsVisible, lockControls] = useControlsSignal();
|
||||
const { onClose } = rest;
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -133,7 +134,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
shouldBeReplaced: true,
|
||||
});
|
||||
|
||||
const selectMediaDebounced = useDebouncedCallback(selectMedia, [selectMedia], DEBOUNCE_SELECT_MEDIA, true);
|
||||
const selectItemDebounced = useDebouncedCallback(selectItem, [selectItem], DEBOUNCE_SELECT_MEDIA, true);
|
||||
const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
|
||||
swipeDirectionRef.current = undefined;
|
||||
}, [], DEBOUNCE_SWIPE, true);
|
||||
@ -157,14 +158,14 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const { scale, x, y } = transformRef.current;
|
||||
// Only update active media if slide is in default position
|
||||
if (x === 0 && y === 0 && scale === 1) {
|
||||
setActiveMediaId(mediaId);
|
||||
setActiveItem(item);
|
||||
}
|
||||
}, [mediaId, setActiveMediaId, transformRef]);
|
||||
}, [item, setActiveItem, transformRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSynced || isLoadingMoreMedia) return;
|
||||
loadMoreMediaIfNeeded(activeMediaId);
|
||||
}, [activeMediaId, loadMoreMediaIfNeeded, isSynced, isLoadingMoreMedia]);
|
||||
if (!isSynced || !activeItem || isLoadingMoreMedia) return;
|
||||
loadMoreItemsIfNeeded(activeItem);
|
||||
}, [activeItem, loadMoreItemsIfNeeded, isSynced, isLoadingMoreMedia]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { x, y, scale } = getTransform();
|
||||
@ -181,7 +182,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}, [getTransform, lockControls, windowWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || activeMediaIdRef.current === undefined || isHidden || isFullscreen) {
|
||||
if (!containerRef.current || activeItemRef.current === undefined || isHidden || isFullscreen) {
|
||||
return undefined;
|
||||
}
|
||||
let lastTransform = lastTransformRef.current;
|
||||
@ -204,14 +205,16 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}, 500, false, true);
|
||||
|
||||
const changeSlide = (direction: number) => {
|
||||
const mId = getMediaId(activeMediaIdRef.current, direction);
|
||||
if (mId !== undefined) {
|
||||
const cActiveItem = activeItemRef.current;
|
||||
if (cActiveItem === undefined) return false;
|
||||
const nextItem = getNextItem(cActiveItem, direction);
|
||||
if (nextItem !== undefined) {
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
const transform = transformRef.current;
|
||||
const x = transform.x + offset;
|
||||
setIsActive(false);
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
setActiveItem(nextItem);
|
||||
selectItemDebounced(nextItem);
|
||||
setIsActiveDebounced(true);
|
||||
lastTransform = { x: 0, y: 0, scale: 1 };
|
||||
if (!withAnimation) {
|
||||
@ -397,19 +400,20 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
}
|
||||
// Get horizontal swipe direction
|
||||
const direction = x < 0 ? 1 : -1;
|
||||
const mId = getMediaId(activeMediaIdRef.current, x < 0 ? 1 : -1);
|
||||
const cActiveItem = activeItemRef.current;
|
||||
const nextItem = cActiveItem && getNextItem(cActiveItem, x < 0 ? 1 : -1);
|
||||
// Get the direction of the last pan gesture.
|
||||
// Could be different from the total horizontal swipe direction
|
||||
// if user starts a swipe in one direction and then changes the direction
|
||||
// we need to cancel slide transition
|
||||
const dirX = panDelta.x < 0 ? -1 : 1;
|
||||
if (mId !== undefined && absX >= SWIPE_X_THRESHOLD && direction === dirX) {
|
||||
if (nextItem !== undefined && absX >= SWIPE_X_THRESHOLD && direction === dirX) {
|
||||
const offset = (windowWidth + SLIDES_GAP) * direction;
|
||||
// If image is shifted by more than SWIPE_X_THRESHOLD,
|
||||
// We shift everything by one screen width and then set new active message id
|
||||
x += offset;
|
||||
setActiveMediaId(mId);
|
||||
selectMediaDebounced(mId);
|
||||
setActiveItem(nextItem);
|
||||
selectItemDebounced(nextItem);
|
||||
}
|
||||
// Then we always return to the original position
|
||||
cancelAnimation = animateNumber({
|
||||
@ -651,25 +655,22 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
};
|
||||
},
|
||||
[
|
||||
onClose,
|
||||
setTransform,
|
||||
loadMoreMediaIfNeeded,
|
||||
getMediaId,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
clickXThreshold,
|
||||
shouldCloseOnVideo,
|
||||
selectMediaDebounced,
|
||||
setIsActiveDebounced,
|
||||
activeItemRef,
|
||||
clearSwipeDirectionDebounced,
|
||||
withAnimation,
|
||||
setIsMouseDown,
|
||||
setIsActive,
|
||||
isHidden,
|
||||
clickXThreshold,
|
||||
getNextItem,
|
||||
isFullscreen,
|
||||
isHidden,
|
||||
onClose,
|
||||
selectItemDebounced,
|
||||
setActiveItem,
|
||||
setIsActiveDebounced,
|
||||
setTransform,
|
||||
shouldCloseOnVideo,
|
||||
transformRef,
|
||||
setActiveMediaId,
|
||||
activeMediaIdRef,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
withAnimation,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -707,12 +708,15 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
});
|
||||
}, [getZoomChange, isHidden, isFullscreen, transformRef]);
|
||||
|
||||
if (activeMediaId === undefined) return undefined;
|
||||
const [prevItem, nextItem] = useMemo(() => {
|
||||
if (activeItem === undefined) return [undefined, undefined];
|
||||
return [getNextItem(activeItem, -1), getNextItem(activeItem, 1)];
|
||||
}, [activeItem, getNextItem]);
|
||||
|
||||
const nextMediaId = getMediaId(activeMediaId, 1);
|
||||
const prevMediaId = getMediaId(activeMediaId, -1);
|
||||
const hasPrev = prevMediaId !== undefined;
|
||||
const hasNext = nextMediaId !== undefined;
|
||||
if (activeItem === undefined) return undefined;
|
||||
|
||||
const hasPrev = prevItem !== undefined;
|
||||
const hasNext = nextItem !== undefined;
|
||||
const isMoving = isMouseDown && isScaled;
|
||||
|
||||
return (
|
||||
@ -720,11 +724,11 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
<div className="MediaViewerSlide" ref={leftSlideRef}>
|
||||
{hasPrev && !isScaled && !isResizing && (
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
withAnimation={withAnimation}
|
||||
isMoving={isMoving}
|
||||
mediaId={prevMediaId}
|
||||
item={prevItem}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -738,22 +742,22 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
ref={activeSlideRef}
|
||||
>
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
mediaId={activeMediaId}
|
||||
item={activeItem}
|
||||
withAnimation={withAnimation}
|
||||
isActive={isActive}
|
||||
isMoving={isMoving}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="MediaViewerSlide" ref={rightSlideRef}>
|
||||
{hasNext && !isScaled && !isResizing && (
|
||||
<MediaViewerContent
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...rest}
|
||||
withAnimation={withAnimation}
|
||||
isMoving={isMoving}
|
||||
mediaId={nextMediaId}
|
||||
item={nextItem}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
import React, { useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiMessage, ApiPeer } from '../../api/types';
|
||||
import type { ApiChat, ApiPeer } from '../../api/types';
|
||||
import type { MediaViewerItem } from './helpers/getViewableMedia';
|
||||
|
||||
import { getSenderTitle } from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessage,
|
||||
selectPeer,
|
||||
getSenderTitle, isChatChannel, isChatGroup, isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectSender,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { formatMediaDateTime } from '../../util/dates/dateFormat';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -22,26 +24,21 @@ import Avatar from '../common/Avatar';
|
||||
import './SenderInfo.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
isAvatar?: boolean;
|
||||
isFallbackAvatar?: boolean;
|
||||
item?: MediaViewerItem;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sender?: ApiPeer;
|
||||
message?: ApiMessage;
|
||||
owner?: ApiPeer;
|
||||
isFallbackAvatar?: boolean;
|
||||
};
|
||||
|
||||
const BULLET = '\u2022';
|
||||
const ANIMATION_DURATION = 350;
|
||||
|
||||
const SenderInfo: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
messageId,
|
||||
sender,
|
||||
owner,
|
||||
item,
|
||||
isFallbackAvatar,
|
||||
isAvatar,
|
||||
message,
|
||||
}) => {
|
||||
const {
|
||||
closeMediaViewer,
|
||||
@ -54,37 +51,64 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
||||
const handleFocusMessage = useLastCallback(() => {
|
||||
closeMediaViewer();
|
||||
|
||||
if (!chatId || !messageId) return;
|
||||
if (item?.type !== 'message') return;
|
||||
|
||||
const message = item.message;
|
||||
|
||||
if (isMobile) {
|
||||
setTimeout(() => {
|
||||
toggleChatInfo({ force: false }, { forceSyncOnIOs: true });
|
||||
focusMessage({ chatId, messageId });
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
focusMessage({ chatId, messageId });
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
}
|
||||
});
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
if (!sender || (!message && !isAvatar)) {
|
||||
const subtitle = useMemo(() => {
|
||||
if (!item || item.type === 'standalone') return undefined;
|
||||
|
||||
const avatarOwner = item.type === 'avatar' ? item.avatarOwner : undefined;
|
||||
const avatar = avatarOwner?.photos?.[item.mediaIndex!];
|
||||
const date = item.type === 'message' ? item.message.date : avatar?.date;
|
||||
if (!date) return undefined;
|
||||
|
||||
const formattedDate = formatMediaDateTime(lang, date * 1000, true);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (avatar) {
|
||||
const chat = !isUserId(avatarOwner!.id) ? avatarOwner as ApiChat : undefined;
|
||||
const isChannel = chat && isChatChannel(chat);
|
||||
const isGroup = chat && isChatGroup(chat);
|
||||
parts.push(lang(
|
||||
isFallbackAvatar ? 'lng_mediaview_profile_public_photo'
|
||||
: isChannel ? 'lng_mediaview_channel_photo'
|
||||
: isGroup ? 'lng_mediaview_group_photo' : 'lng_mediaview_profile_photo',
|
||||
));
|
||||
}
|
||||
|
||||
parts.push(formattedDate);
|
||||
|
||||
return parts.join(` ${BULLET} `);
|
||||
}, [item, isFallbackAvatar, lang]);
|
||||
|
||||
if (!owner) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const senderTitle = getSenderTitle(lang, sender);
|
||||
const senderTitle = getSenderTitle(lang, owner);
|
||||
|
||||
return (
|
||||
<div className="SenderInfo" onClick={handleFocusMessage}>
|
||||
<Avatar key={sender.id} size="medium" peer={sender} />
|
||||
<Avatar key={owner.id} size="medium" peer={owner} />
|
||||
<div className="meta">
|
||||
<div className="title" dir="auto">
|
||||
{senderTitle && renderText(senderTitle)}
|
||||
</div>
|
||||
<div className="date" dir="auto">
|
||||
{isAvatar
|
||||
? lang(isFallbackAvatar ? 'lng_mediaview_profile_public_photo' : 'lng_mediaview_profile_photo')
|
||||
: formatMediaDateTime(lang, message!.date * 1000, true)}
|
||||
{subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,22 +116,21 @@ const SenderInfo: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default withGlobal<OwnProps>(
|
||||
(global, { chatId, messageId, isAvatar }): StateProps => {
|
||||
if (isAvatar && chatId) {
|
||||
return {
|
||||
sender: selectPeer(global, chatId),
|
||||
};
|
||||
}
|
||||
(global, { item }): StateProps => {
|
||||
const message = item?.type === 'message' ? item.message : undefined;
|
||||
const messageSender = message && selectSender(global, message);
|
||||
|
||||
if (!messageId || !chatId) {
|
||||
return {};
|
||||
}
|
||||
const owner = item?.type === 'avatar' ? item.avatarOwner : messageSender;
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
const fallbackAvatar = item?.type === 'avatar'
|
||||
? selectUserFullInfo(global, item.avatarOwner.id)?.fallbackPhoto : undefined;
|
||||
|
||||
const isFallbackAvatar = fallbackAvatar && item?.type === 'avatar'
|
||||
&& item.avatarOwner.photos?.[item.mediaIndex].id === fallbackAvatar.id;
|
||||
|
||||
return {
|
||||
message,
|
||||
sender: message && selectSender(global, message),
|
||||
owner,
|
||||
isFallbackAvatar,
|
||||
};
|
||||
},
|
||||
)(SenderInfo);
|
||||
|
||||
129
src/components/mediaViewer/helpers/getViewableMedia.ts
Normal file
129
src/components/mediaViewer/helpers/getViewableMedia.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type { ApiMessage, ApiPeer } from '../../../api/types';
|
||||
import type { MediaViewerMedia } from '../../../types';
|
||||
|
||||
import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers';
|
||||
|
||||
export type MediaViewerItem = {
|
||||
type: 'message';
|
||||
message: ApiMessage;
|
||||
mediaIndex?: number;
|
||||
} | {
|
||||
type: 'avatar';
|
||||
avatarOwner: ApiPeer;
|
||||
mediaIndex: number;
|
||||
} | {
|
||||
type: 'standalone';
|
||||
media: MediaViewerMedia[];
|
||||
mediaIndex: number;
|
||||
};
|
||||
|
||||
type ViewableMedia = {
|
||||
media: MediaViewerMedia;
|
||||
isSingle?: boolean;
|
||||
};
|
||||
|
||||
export function getMediaViewerItem({
|
||||
message, avatarOwner, standaloneMedia, mediaIndex,
|
||||
}: {
|
||||
message?: ApiMessage;
|
||||
avatarOwner?: ApiPeer;
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
mediaIndex?: number;
|
||||
}): MediaViewerItem | undefined {
|
||||
if (avatarOwner) {
|
||||
return {
|
||||
type: 'avatar',
|
||||
avatarOwner,
|
||||
mediaIndex: mediaIndex!,
|
||||
};
|
||||
}
|
||||
|
||||
if (standaloneMedia) {
|
||||
return {
|
||||
type: 'standalone',
|
||||
media: standaloneMedia!,
|
||||
mediaIndex: mediaIndex!,
|
||||
};
|
||||
}
|
||||
|
||||
if (message) {
|
||||
return {
|
||||
type: 'message',
|
||||
message,
|
||||
mediaIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function getViewableMedia(params?: MediaViewerItem): ViewableMedia | undefined {
|
||||
if (!params) return undefined;
|
||||
|
||||
if (params.type === 'standalone') {
|
||||
return {
|
||||
media: params.media[params.mediaIndex],
|
||||
isSingle: params.media.length === 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.type === 'avatar') {
|
||||
const avatar = params.avatarOwner.photos?.[params.mediaIndex];
|
||||
if (avatar) {
|
||||
return {
|
||||
media: avatar,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
action, document, photo, video, webPage, paidMedia,
|
||||
} = getMessageContent(params.message);
|
||||
|
||||
if (action?.photo) {
|
||||
return {
|
||||
media: action.photo,
|
||||
isSingle: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (document && (isDocumentPhoto(document) || isDocumentVideo(document))) {
|
||||
return {
|
||||
media: document,
|
||||
isSingle: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
const { photo: webPagePhoto, video: webPageVideo } = webPage;
|
||||
const media = webPageVideo || webPagePhoto;
|
||||
if (media) {
|
||||
return {
|
||||
media,
|
||||
isSingle: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (paidMedia) {
|
||||
const extendedMedia = paidMedia.extendedMedia[params.mediaIndex || 0];
|
||||
if (!('mediaType' in extendedMedia)) {
|
||||
const { photo: extendedPhoto, video: extendedVideo } = extendedMedia;
|
||||
return {
|
||||
media: (extendedPhoto || extendedVideo)!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const media = video || photo;
|
||||
|
||||
if (media) {
|
||||
return {
|
||||
media,
|
||||
isSingle: video?.isGif,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -25,8 +25,9 @@ export function animateOpening(
|
||||
dimensions: ApiDimensions,
|
||||
isVideo: boolean,
|
||||
message?: ApiMessage,
|
||||
mediaIndex?: number,
|
||||
) {
|
||||
const { mediaEl: fromImage } = getNodes(origin, message);
|
||||
const { mediaEl: fromImage } = getNodes(origin, message, mediaIndex);
|
||||
if (!fromImage) {
|
||||
return;
|
||||
}
|
||||
@ -93,8 +94,10 @@ export function animateOpening(
|
||||
});
|
||||
}
|
||||
|
||||
export function animateClosing(origin: MediaViewerOrigin, bestImageData: string, message?: ApiMessage) {
|
||||
const { container, mediaEl: toImage } = getNodes(origin, message);
|
||||
export function animateClosing(
|
||||
origin: MediaViewerOrigin, bestImageData: string, message?: ApiMessage, mediaIndex?: number,
|
||||
) {
|
||||
const { container, mediaEl: toImage } = getNodes(origin, message, mediaIndex);
|
||||
if (!toImage) {
|
||||
return;
|
||||
}
|
||||
@ -102,7 +105,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
|
||||
const fromImage = document.getElementById('MediaViewer')!.querySelector<HTMLImageElement>(
|
||||
'.MediaViewerSlide--active img, .MediaViewerSlide--active video',
|
||||
);
|
||||
if (!fromImage || !toImage) {
|
||||
if (!fromImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -284,24 +287,25 @@ function getTopOffset(hasFooter: boolean) {
|
||||
return topOffsetRem * REM;
|
||||
}
|
||||
|
||||
function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: number) {
|
||||
let containerSelector;
|
||||
let mediaSelector;
|
||||
|
||||
switch (origin) {
|
||||
case MediaViewerOrigin.Album:
|
||||
case MediaViewerOrigin.ScheduledAlbum:
|
||||
containerSelector = `.Transition_slide-active > .MessageList #album-media-${getMessageHtmlId(message!.id)}`;
|
||||
// eslint-disable-next-line max-len
|
||||
containerSelector = `.Transition_slide-active > .MessageList #album-media-${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = '.full-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SharedMedia:
|
||||
containerSelector = `#shared-media${getMessageHtmlId(message!.id)}`;
|
||||
containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = 'img';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SearchResult:
|
||||
containerSelector = `#search-media${getMessageHtmlId(message!.id)}`;
|
||||
containerSelector = `#search-media${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = 'img';
|
||||
break;
|
||||
|
||||
@ -321,19 +325,25 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SuggestedAvatar:
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id)}`;
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = '.Avatar img';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.StarsTransaction:
|
||||
containerSelector = '.transaction-media-preview';
|
||||
mediaSelector = index === 0 ? `.stars-transaction-media-${index} :is(img, video)` : undefined;
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.Inline:
|
||||
default:
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id)}`;
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} .full-media,${MESSAGE_CONTENT_SELECTOR} .thumbnail:not(.blurred-bg)`;
|
||||
}
|
||||
|
||||
const container = document.querySelector<HTMLElement>(containerSelector)!;
|
||||
const mediaEls = container && container.querySelectorAll<HTMLImageElement | HTMLVideoElement>(mediaSelector);
|
||||
const mediaEls = mediaSelector
|
||||
? container?.querySelectorAll<HTMLImageElement | HTMLVideoElement>(mediaSelector) : undefined;
|
||||
|
||||
return {
|
||||
container,
|
||||
@ -347,6 +357,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
||||
case MediaViewerOrigin.ScheduledAlbum:
|
||||
case MediaViewerOrigin.Inline:
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.StarsTransaction:
|
||||
ghost.classList.add('rounded-corners');
|
||||
break;
|
||||
|
||||
|
||||
@ -1,29 +1,19 @@
|
||||
import { useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer,
|
||||
} from '../../../api/types';
|
||||
import type { MediaViewerMedia } from '../../../types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
import {
|
||||
getChatAvatarHash,
|
||||
getMessageActionPhoto,
|
||||
getMessageDocument,
|
||||
getMessageFileName,
|
||||
getMessageFileSize,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessagePhoto,
|
||||
getMessageVideo,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
getMediaFileSize,
|
||||
getMediaFormat,
|
||||
getMediaHash,
|
||||
getMediaThumbUri,
|
||||
getPhotoFullDimensions,
|
||||
getVideoAvatarMediaHash,
|
||||
getVideoDimensions,
|
||||
isMessageDocumentPhoto,
|
||||
isMessageDocumentVideo,
|
||||
isDocumentPhoto,
|
||||
isDocumentVideo,
|
||||
} from '../../../global/helpers';
|
||||
import { AVATAR_FULL_DIMENSIONS, VIDEO_AVATAR_FULL_DIMENSIONS } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
@ -32,67 +22,46 @@ import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||
|
||||
type UseMediaProps = {
|
||||
mediaId?: number;
|
||||
message?: ApiMessage;
|
||||
avatarOwner?: ApiPeer;
|
||||
media?: MediaViewerMedia;
|
||||
isAvatar?: boolean;
|
||||
origin?: MediaViewerOrigin;
|
||||
delay: number | false;
|
||||
};
|
||||
|
||||
export const useMediaProps = ({
|
||||
message,
|
||||
mediaId = 0,
|
||||
avatarOwner,
|
||||
media,
|
||||
isAvatar,
|
||||
origin,
|
||||
delay,
|
||||
}: UseMediaProps) => {
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
const actionPhoto = message ? getMessageActionPhoto(message) : undefined;
|
||||
const video = message ? getMessageVideo(message) : undefined;
|
||||
const webPagePhoto = message ? getMessageWebPagePhoto(message) : undefined;
|
||||
const webPageVideo = message ? getMessageWebPageVideo(message) : undefined;
|
||||
const isDocumentPhoto = message ? isMessageDocumentPhoto(message) : false;
|
||||
const isDocumentVideo = message ? isMessageDocumentVideo(message) : false;
|
||||
const videoSize = message ? getMessageFileSize(message) : undefined;
|
||||
const avatarMedia = avatarOwner?.photos?.[mediaId];
|
||||
const isVideoAvatar = Boolean(avatarMedia?.isVideo || actionPhoto?.isVideo);
|
||||
const isVideo = Boolean(video || webPageVideo || isDocumentVideo);
|
||||
const isPhoto = Boolean(!isVideo && (photo || webPagePhoto || isDocumentPhoto || actionPhoto));
|
||||
const { isGif } = video || webPageVideo || {};
|
||||
const isVideoAvatar = isAvatar && media?.mediaType === 'photo' && media.isVideo;
|
||||
const isDocument = media?.mediaType === 'document';
|
||||
const isVideo = (media?.mediaType === 'video' && !media.isRound) || (isDocument && isDocumentVideo(media));
|
||||
const isPhoto = media?.mediaType === 'photo' || (isDocument && isDocumentPhoto(media));
|
||||
const isGif = media?.mediaType === 'video' && media.isGif;
|
||||
const isFromSharedMedia = origin === MediaViewerOrigin.SharedMedia;
|
||||
const isFromSearch = origin === MediaViewerOrigin.SearchResult;
|
||||
|
||||
const getMediaHash = useMemo(() => (isFull?: boolean) => {
|
||||
if (avatarOwner) {
|
||||
if (avatarMedia) {
|
||||
if (avatarMedia.isVideo && isFull) {
|
||||
return getVideoAvatarMediaHash(avatarMedia);
|
||||
} else if (mediaId === 0) {
|
||||
// Show preloaded avatar if this is the first media (when user clicks on profile info avatar)
|
||||
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
||||
} else {
|
||||
return `photo${avatarMedia.id}?size=c`;
|
||||
}
|
||||
} else {
|
||||
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
||||
}
|
||||
const getMediaOrAvatarHash = useMemo(() => (isFull?: boolean) => {
|
||||
if (!media) return undefined;
|
||||
|
||||
if (isVideoAvatar && isFull) {
|
||||
return getVideoAvatarMediaHash(media);
|
||||
}
|
||||
if (actionPhoto && isVideoAvatar && isFull) {
|
||||
return `videoAvatar${actionPhoto.id}?size=u`;
|
||||
}
|
||||
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
|
||||
}, [avatarOwner, actionPhoto, isVideoAvatar, message, avatarMedia, mediaId]);
|
||||
|
||||
return getMediaHash(media, isFull ? 'full' : 'preview');
|
||||
}, [isVideoAvatar, media]);
|
||||
|
||||
const pictogramBlobUrl = useMedia(
|
||||
message
|
||||
media
|
||||
// Only use pictogram if it's already loaded
|
||||
&& (isFromSharedMedia || isFromSearch || isDocumentPhoto || isDocumentVideo)
|
||||
&& getMessageMediaHash(message, 'pictogram'),
|
||||
&& (isFromSharedMedia || isFromSearch || isDocument)
|
||||
&& getMediaHash(media, 'pictogram'),
|
||||
undefined,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
delay,
|
||||
);
|
||||
const previewMediaHash = getMediaHash();
|
||||
const previewMediaHash = getMediaOrAvatarHash();
|
||||
const previewBlobUrl = useMedia(
|
||||
previewMediaHash,
|
||||
undefined,
|
||||
@ -103,15 +72,15 @@ export const useMediaProps = ({
|
||||
mediaData: fullMediaBlobUrl,
|
||||
loadProgress,
|
||||
} = useMediaWithLoadProgress(
|
||||
getMediaHash(true),
|
||||
getMediaOrAvatarHash(true),
|
||||
undefined,
|
||||
message && getMessageMediaFormat(message, 'full'),
|
||||
media && getMediaFormat(media, 'full'),
|
||||
delay,
|
||||
);
|
||||
|
||||
const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined;
|
||||
const localBlobUrl = media && 'blobUrl' in media ? media.blobUrl : undefined;
|
||||
let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl;
|
||||
const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message));
|
||||
const thumbDataUri = useBlurSync(!bestImageData && media && getMediaThumbUri(media));
|
||||
if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) {
|
||||
bestImageData = thumbDataUri;
|
||||
}
|
||||
@ -122,59 +91,43 @@ export const useMediaProps = ({
|
||||
(!isVideoAvatar && !isVideo) ? (previewBlobUrl || pictogramBlobUrl || bestImageData) : undefined
|
||||
);
|
||||
|
||||
const mediaSize = media && getMediaFileSize(media);
|
||||
|
||||
const isLocal = Boolean(localBlobUrl);
|
||||
const fileName = message
|
||||
? getMessageFileName(message)
|
||||
: avatarOwner
|
||||
? `avatar${avatarOwner!.id}.${avatarOwner?.hasVideoAvatar ? 'mp4' : 'jpg'}`
|
||||
: undefined;
|
||||
|
||||
const dimensions = useMemo(() => {
|
||||
if (message) {
|
||||
if (isDocumentPhoto || isDocumentVideo) {
|
||||
return getMessageDocument(message)!.mediaSize!;
|
||||
} else if (photo || webPagePhoto || actionPhoto) {
|
||||
return getPhotoFullDimensions((photo || webPagePhoto || actionPhoto)!)!;
|
||||
} else if (video || webPageVideo) {
|
||||
return getVideoDimensions((video || webPageVideo)!)!;
|
||||
}
|
||||
} else {
|
||||
if (isAvatar) {
|
||||
return isVideoAvatar ? VIDEO_AVATAR_FULL_DIMENSIONS : AVATAR_FULL_DIMENSIONS;
|
||||
}
|
||||
|
||||
if (isDocument) {
|
||||
return media.mediaSize!;
|
||||
}
|
||||
|
||||
if (isPhoto) {
|
||||
return getPhotoFullDimensions(media);
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
return getVideoDimensions(media);
|
||||
}
|
||||
return undefined;
|
||||
}, [
|
||||
isDocumentPhoto,
|
||||
isDocumentVideo,
|
||||
isVideoAvatar,
|
||||
message,
|
||||
photo,
|
||||
video,
|
||||
actionPhoto,
|
||||
webPagePhoto,
|
||||
webPageVideo,
|
||||
]);
|
||||
}, [isAvatar, isDocument, isPhoto, isVideo, isVideoAvatar, media]);
|
||||
|
||||
return {
|
||||
getMediaHash,
|
||||
photo,
|
||||
video,
|
||||
webPagePhoto,
|
||||
actionPhoto,
|
||||
webPageVideo,
|
||||
getMediaHash: getMediaOrAvatarHash,
|
||||
media,
|
||||
isVideo,
|
||||
isPhoto,
|
||||
isGif,
|
||||
isDocumentPhoto,
|
||||
isDocumentVideo,
|
||||
fileName,
|
||||
isDocument,
|
||||
bestImageData,
|
||||
bestData,
|
||||
dimensions,
|
||||
isFromSharedMedia,
|
||||
avatarPhoto: avatarMedia,
|
||||
isVideoAvatar,
|
||||
isLocal,
|
||||
loadProgress,
|
||||
videoSize,
|
||||
mediaSize,
|
||||
};
|
||||
};
|
||||
|
||||
@ -4,11 +4,11 @@ import { getActions } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { TextPart } from '../../types';
|
||||
import { ApiMediaFormat, MAIN_THREAD_ID } from '../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { MediaViewerOrigin, SettingsScreens } from '../../types';
|
||||
|
||||
import { getMessageMediaHash } from '../../global/helpers';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
import { getPhotoMediaHash, getVideoAvatarMediaHash } from '../../global/helpers';
|
||||
import { fetchBlob } from '../../util/files';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
@ -37,7 +37,9 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
const lang = useOldLang();
|
||||
const [cropModalBlob, setCropModalBlob] = useState<Blob | undefined>();
|
||||
const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false);
|
||||
const suggestedPhotoUrl = useMedia(getMessageMediaHash(message, 'full'));
|
||||
const photo = message.content.action!.photo!;
|
||||
const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full'));
|
||||
const suggestedVideoUrl = useMedia(getVideoAvatarMediaHash(photo));
|
||||
const isVideo = message.content.action!.photo?.isVideo;
|
||||
|
||||
const showAvatarNotification = useLastCallback(() => {
|
||||
@ -65,13 +67,13 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
const handleSetVideo = useLastCallback(async () => {
|
||||
if (!suggestedVideoUrl) return;
|
||||
|
||||
closeVideoModal();
|
||||
showAvatarNotification();
|
||||
|
||||
// TODO Once we support uploading video avatars, add crop/trim modal here
|
||||
const photo = message.content.action!.photo!;
|
||||
const blobUrl = await mediaLoader.fetch(`videoAvatar${photo.id}?size=u`, ApiMediaFormat.BlobUrl);
|
||||
const blob = await fetch(blobUrl).then((r) => r.blob());
|
||||
const blob = await fetchBlob(suggestedVideoUrl);
|
||||
uploadProfilePhoto({
|
||||
file: new File([blob], 'avatar.mp4'),
|
||||
isVideo: true,
|
||||
@ -84,12 +86,12 @@ const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
if (isVideo) {
|
||||
openVideoModal();
|
||||
} else {
|
||||
setCropModalBlob(await fetch(suggestedPhotoUrl).then((r) => r.blob()));
|
||||
setCropModalBlob(await fetchBlob(suggestedPhotoUrl));
|
||||
}
|
||||
} else {
|
||||
openMediaViewer({
|
||||
chatId: message.chatId,
|
||||
mediaId: message.id,
|
||||
messageId: message.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
origin: MediaViewerOrigin.SuggestedAvatar,
|
||||
});
|
||||
|
||||
@ -6,9 +6,9 @@ import type { ApiBotInfo } from '../../api/types';
|
||||
|
||||
import {
|
||||
getBotCoverMediaHash,
|
||||
getDocumentMediaHash,
|
||||
getPhotoFullDimensions,
|
||||
getVideoDimensions,
|
||||
getVideoMediaHash,
|
||||
} from '../../global/helpers';
|
||||
import { selectBot, selectUserFullInfo } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -43,7 +43,7 @@ const MessageListBotInfo: FC<OwnProps & StateProps> = ({
|
||||
const dpr = useDevicePixelRatio();
|
||||
|
||||
const botInfoPhotoUrl = useMedia(botInfo?.photo ? getBotCoverMediaHash(botInfo.photo) : undefined);
|
||||
const botInfoGifUrl = useMedia(botInfo?.gif ? getDocumentMediaHash(botInfo.gif) : undefined);
|
||||
const botInfoGifUrl = useMedia(botInfo?.gif ? getVideoMediaHash(botInfo.gif, 'full') : undefined);
|
||||
const botInfoDimensions = botInfo?.photo ? getPhotoFullDimensions(botInfo.photo) : botInfo?.gif
|
||||
? getVideoDimensions(botInfo.gif) : undefined;
|
||||
const botInfoRealDimensions = botInfoDimensions && {
|
||||
|
||||
@ -27,7 +27,7 @@ const AttachBotIcon: FC<OwnProps> = ({
|
||||
icon, theme,
|
||||
}) => {
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text);
|
||||
const mediaData = useMedia(getDocumentMediaHash(icon, 'full'), false, ApiMediaFormat.Text);
|
||||
|
||||
const iconSvg = useMemo(() => {
|
||||
if (!mediaData) return '';
|
||||
|
||||
@ -6,7 +6,7 @@ import type { ApiStickerSet } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
|
||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||
import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getFirstLetters } from '../../../util/textFormat';
|
||||
@ -62,7 +62,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
const hasOnlyStaticThumb = hasStaticThumb && !hasVideoThumb && !hasAnimatedThumb && !thumbCustomEmojiId;
|
||||
|
||||
const shouldFallbackToStatic = hasOnlyStaticThumb || (hasVideoThumb && !IS_WEBM_SUPPORTED && !hasAnimatedThumb);
|
||||
const staticHash = shouldFallbackToStatic && getStickerPreviewHash(stickerSet.stickers![0].id);
|
||||
const staticHash = shouldFallbackToStatic && getStickerMediaHash(stickerSet.stickers![0], 'preview');
|
||||
const staticMediaData = useMedia(staticHash, !isIntersecting);
|
||||
|
||||
const mediaHash = ((hasThumbnail && !shouldFallbackToStatic) || hasAnimatedThumb) && `stickerSet${stickerSet.id}`;
|
||||
@ -75,10 +75,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
useEffect(() => {
|
||||
if (isIntersecting && !stickerSet.stickers?.length) {
|
||||
loadStickers({
|
||||
stickerSetInfo: {
|
||||
id: stickerSet.id,
|
||||
accessHash: stickerSet.accessHash,
|
||||
},
|
||||
stickerSetInfo: stickerSet,
|
||||
});
|
||||
}
|
||||
}, [isIntersecting, loadStickers, stickerSet]);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { type TeactNode } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiKeyboardButton } from '../../../../api/types';
|
||||
|
||||
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
||||
import { replaceWithTeact } from '../../../../util/replaceWithTeact';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
|
||||
@ -15,7 +16,7 @@ export default function renderKeyboardButtonText(lang: LangFn, button: ApiKeyboa
|
||||
}
|
||||
|
||||
if (button.type === 'buy') {
|
||||
return replaceWithTeact(button.text, '⭐', <Icon className="star-currency-icon" name="star" />);
|
||||
return replaceWithTeact(button.text, STARS_ICON_PLACEHOLDER, <Icon className="star-currency-icon" name="star" />);
|
||||
}
|
||||
|
||||
return renderText(button.text);
|
||||
|
||||
@ -39,7 +39,7 @@ export function groupMessages(
|
||||
messages: [message],
|
||||
mainMessage: message,
|
||||
hasMultipleCaptions: false,
|
||||
};
|
||||
} satisfies IAlbum;
|
||||
} else {
|
||||
currentAlbum.messages.push(message);
|
||||
if (message.hasComments) {
|
||||
@ -54,6 +54,14 @@ export function groupMessages(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ((message.content.paidMedia?.extendedMedia.length || 0) > 1) {
|
||||
currentSenderGroup.push({
|
||||
albumId: `paid-${message.id}`,
|
||||
messages: [message],
|
||||
mainMessage: message,
|
||||
hasMultipleCaptions: false,
|
||||
isPaidMedia: true,
|
||||
} satisfies IAlbum);
|
||||
} else {
|
||||
currentSenderGroup.push(message);
|
||||
}
|
||||
@ -67,6 +75,7 @@ export function groupMessages(
|
||||
currentSenderGroup.push(currentAlbum);
|
||||
currentAlbum = undefined;
|
||||
}
|
||||
|
||||
const lastSenderGroupItem = currentSenderGroup[currentSenderGroup.length - 1];
|
||||
if (nextMessage) {
|
||||
const nextMessageDayStartsAt = getDayStartAt(nextMessage.date * 1000);
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
|
||||
.message-content.media.text & {
|
||||
margin: -0.3125rem -0.5rem 0.3125rem;
|
||||
|
||||
+ .message-paid-media-status {
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
body.is-ios .Message.own .message-content.has-solid-background :not(.forwarded-message) & {
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
import React, { useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { IAlbum, ISettings } from '../../../types';
|
||||
import type { IAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
|
||||
import { getMessageContent, getMessageHtmlId } from '../../../global/helpers';
|
||||
import {
|
||||
getIsDownloading, getMessageContent, getMessageHtmlId, getMessagePhoto,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectActiveDownloads,
|
||||
selectCanAutoLoadMedia,
|
||||
@ -36,13 +38,13 @@ type OwnProps = {
|
||||
isOwn: boolean;
|
||||
isProtected?: boolean;
|
||||
albumLayout: IAlbumLayout;
|
||||
onMediaClick: (messageId: number) => void;
|
||||
onMediaClick: (messageId: number, index?: number) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
theme: ISettings['theme'];
|
||||
uploadsByKey: GlobalState['fileUploads']['byMessageKey'];
|
||||
activeDownloadIds?: number[];
|
||||
activeDownloads: TabState['activeDownloads'];
|
||||
};
|
||||
|
||||
const Album: FC<OwnProps & StateProps> = ({
|
||||
@ -54,19 +56,44 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
albumLayout,
|
||||
onMediaClick,
|
||||
uploadsByKey,
|
||||
activeDownloadIds,
|
||||
activeDownloads,
|
||||
theme,
|
||||
}) => {
|
||||
const { cancelUploadMedia } = getActions();
|
||||
|
||||
const mediaCount = album.messages.length;
|
||||
const { content: { paidMedia } } = album.mainMessage;
|
||||
|
||||
const handleCancelUpload = useLastCallback((message: ApiMessage) => {
|
||||
cancelUploadMedia({ chatId: message.chatId, messageId: message.id });
|
||||
const mediaCount = album.isPaidMedia ? paidMedia!.extendedMedia.length : album.messages.length;
|
||||
|
||||
const handlePaidMediaClick = useLastCallback((index: number) => {
|
||||
onMediaClick(album.mainMessage.id, index);
|
||||
});
|
||||
|
||||
const handleAlbumMessageClick = useLastCallback((messageId: number) => {
|
||||
onMediaClick(messageId);
|
||||
});
|
||||
|
||||
const handleCancelUpload = useLastCallback((messageId: number) => {
|
||||
cancelUploadMedia({ chatId: album.mainMessage.chatId, messageId });
|
||||
});
|
||||
|
||||
const messages = useMemo(() => {
|
||||
if (album.isPaidMedia) {
|
||||
return album.mainMessage.content.paidMedia!.extendedMedia.map(() => album.mainMessage);
|
||||
}
|
||||
|
||||
return album.messages;
|
||||
}, [album]);
|
||||
|
||||
function renderAlbumMessage(message: ApiMessage, index: number) {
|
||||
const { photo, video } = getMessageContent(message);
|
||||
const renderingPaidMedia = album.isPaidMedia ? message.content.paidMedia?.extendedMedia[index] : undefined;
|
||||
const paidPhotoOrPreview = renderingPaidMedia && 'mediaType' in renderingPaidMedia
|
||||
? renderingPaidMedia : renderingPaidMedia?.photo;
|
||||
const paidVideoOrPreview = renderingPaidMedia && 'mediaType' in renderingPaidMedia
|
||||
? renderingPaidMedia : renderingPaidMedia?.video;
|
||||
const photo = paidPhotoOrPreview || getMessagePhoto(message);
|
||||
const video = paidVideoOrPreview || getMessageContent(message).video;
|
||||
|
||||
const fileUpload = uploadsByKey[getMessageKey(message)];
|
||||
const uploadProgress = fileUpload?.progress;
|
||||
const { dimensions, sides } = albumLayout.layout[index];
|
||||
@ -83,17 +110,19 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<PhotoWithSelect
|
||||
id={`album-media-${getMessageHtmlId(message.id)}`}
|
||||
message={message}
|
||||
id={`album-media-${getMessageHtmlId(message.id, album.isPaidMedia ? index : undefined)}`}
|
||||
photo={photo}
|
||||
isOwn={isOwn}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
canAutoLoad={canAutoLoad}
|
||||
shouldAffectAppendix={shouldAffectAppendix}
|
||||
uploadProgress={uploadProgress}
|
||||
dimensions={dimensions}
|
||||
isProtected={isProtected}
|
||||
onClick={onMediaClick}
|
||||
clickArg={album.isPaidMedia ? index : message.id}
|
||||
onClick={album.isPaidMedia ? handlePaidMediaClick : handleAlbumMessageClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={activeDownloadIds?.includes(message.id)}
|
||||
isDownloading={photo.mediaType !== 'extendedMediaPreview' && getIsDownloading(activeDownloads, photo)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
@ -101,16 +130,17 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<VideoWithSelect
|
||||
id={`album-media-${getMessageHtmlId(message.id)}`}
|
||||
message={message}
|
||||
video={video}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
canAutoLoad={canAutoLoad}
|
||||
canAutoPlay={canAutoPlay}
|
||||
uploadProgress={uploadProgress}
|
||||
dimensions={dimensions}
|
||||
isProtected={isProtected}
|
||||
onClick={onMediaClick}
|
||||
clickArg={album.isPaidMedia ? index : message.id}
|
||||
onClick={album.isPaidMedia ? handlePaidMediaClick : handleAlbumMessageClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={activeDownloadIds?.includes(message.id)}
|
||||
isDownloading={video.mediaType !== 'extendedMediaPreview' && getIsDownloading(activeDownloads, video)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
@ -126,22 +156,20 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
className="Album"
|
||||
style={`width: ${containerWidth}px; height: ${containerHeight}px;`}
|
||||
>
|
||||
{album.messages.map(renderAlbumMessage)}
|
||||
{messages.map(renderAlbumMessage)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withGlobal<OwnProps>(
|
||||
(global, { album }): StateProps => {
|
||||
const { chatId } = album.mainMessage;
|
||||
(global): StateProps => {
|
||||
const theme = selectTheme(global);
|
||||
const activeDownloads = selectActiveDownloads(global, chatId);
|
||||
const isScheduled = album.mainMessage.isScheduled;
|
||||
const activeDownloads = selectActiveDownloads(global);
|
||||
|
||||
return {
|
||||
theme,
|
||||
uploadsByKey: global.fileUploads.byMessageKey,
|
||||
activeDownloadIds: isScheduled ? activeDownloads?.scheduledIds : activeDownloads?.ids,
|
||||
activeDownloads,
|
||||
};
|
||||
},
|
||||
)(Album);
|
||||
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { MessageListType, TabState } from '../../../global/types';
|
||||
import type { ActiveDownloads, MessageListType } from '../../../global/types';
|
||||
import type { IAlbum, IAnchorPosition, ThreadId } from '../../../types';
|
||||
import {
|
||||
type ApiAvailableReaction,
|
||||
@ -20,6 +20,8 @@ import {
|
||||
import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
import {
|
||||
areReactionsEmpty,
|
||||
getIsDownloading,
|
||||
getMessageDownloadableMedia,
|
||||
getMessageVideo,
|
||||
hasMessageTtl,
|
||||
isActionMessage,
|
||||
@ -120,7 +122,7 @@ type StateProps = {
|
||||
canClosePoll?: boolean;
|
||||
canLoadReadDate?: boolean;
|
||||
shouldRenderShowWhen?: boolean;
|
||||
activeDownloads?: TabState['activeDownloads']['byChatId'][number];
|
||||
activeDownloads: ActiveDownloads;
|
||||
canShowSeenBy?: boolean;
|
||||
enabledReactions?: ApiChatReactions;
|
||||
canScheduleUntilOnline?: boolean;
|
||||
@ -200,8 +202,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
toggleMessageSelection,
|
||||
sendScheduledMessages,
|
||||
rescheduleMessage,
|
||||
downloadMessageMedia,
|
||||
cancelMessageMediaDownload,
|
||||
downloadMedia,
|
||||
cancelMediaDownload,
|
||||
loadSeenBy,
|
||||
openSeenByModal,
|
||||
openReactorListModal,
|
||||
@ -291,11 +293,15 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
const isDownloading = useMemo(() => {
|
||||
if (album) {
|
||||
return album.messages.some((msg) => {
|
||||
return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(msg.id);
|
||||
const downloadableMedia = getMessageDownloadableMedia(msg);
|
||||
if (!downloadableMedia) return false;
|
||||
return getIsDownloading(activeDownloads, downloadableMedia);
|
||||
});
|
||||
}
|
||||
|
||||
return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id);
|
||||
const downloadableMedia = getMessageDownloadableMedia(message);
|
||||
if (!downloadableMedia) return false;
|
||||
return getIsDownloading(activeDownloads, downloadableMedia);
|
||||
}, [activeDownloads, album, message]);
|
||||
|
||||
const selectionRange = canReply && selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
||||
@ -483,10 +489,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleDownloadClick = useLastCallback(() => {
|
||||
(album?.messages || [message]).forEach((msg) => {
|
||||
const downloadableMedia = getMessageDownloadableMedia(msg);
|
||||
if (!downloadableMedia) return;
|
||||
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message: msg });
|
||||
cancelMediaDownload({ media: downloadableMedia });
|
||||
} else {
|
||||
downloadMessageMedia({ message: msg });
|
||||
downloadMedia({ media: downloadableMedia });
|
||||
}
|
||||
});
|
||||
closeMenu();
|
||||
@ -666,7 +675,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const { defaultTags, topReactions, availableReactions } = global.reactions;
|
||||
|
||||
const activeDownloads = selectActiveDownloads(global, message.chatId);
|
||||
const activeDownloads = selectActiveDownloads(global);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const {
|
||||
seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions, readDateExpiresAt,
|
||||
|
||||
@ -55,7 +55,7 @@ const Invoice: FC<OwnProps> = ({
|
||||
|
||||
const photoUrl = useMedia(getWebDocumentHash(photo));
|
||||
const withBlurredBackground = Boolean(forcedWidth);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(message, !withBlurredBackground, photoUrl);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(photoUrl, !withBlurredBackground);
|
||||
|
||||
useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => {
|
||||
if (!shouldAffectAppendix) {
|
||||
|
||||
@ -66,16 +66,16 @@ const Location: FC<OwnProps> = ({
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const location = getMessageLocation(message)!;
|
||||
const { type, geo } = location;
|
||||
const { mediaType, geo } = location;
|
||||
|
||||
const serverTime = getServerTime();
|
||||
const isExpired = isGeoLiveExpired(message);
|
||||
const secondsBeforeEnd = (type === 'geoLive' && !isExpired) ? message.date + location.period - serverTime
|
||||
const secondsBeforeEnd = (mediaType === 'geoLive' && !isExpired) ? message.date + location.period - serverTime
|
||||
: undefined;
|
||||
|
||||
const [point, setPoint] = useState(geo);
|
||||
|
||||
const shouldRenderText = type === 'venue' || (type === 'geoLive' && !isExpired);
|
||||
const shouldRenderText = mediaType === 'venue' || (mediaType === 'geoLive' && !isExpired);
|
||||
const { width, height, zoom } = DEFAULT_MAP_CONFIG;
|
||||
const dpr = useDevicePixelRatio();
|
||||
|
||||
@ -85,20 +85,20 @@ const Location: FC<OwnProps> = ({
|
||||
const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl;
|
||||
|
||||
const accuracyRadiusPx = useMemo(() => {
|
||||
if (type !== 'geoLive' || !point.accuracyRadius) {
|
||||
if (mediaType !== 'geoLive' || !point.accuracyRadius) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { lat, accuracyRadius } = point;
|
||||
return accuracyRadius / getMetersPerPixel(lat, zoom);
|
||||
}, [type, point, zoom]);
|
||||
}, [mediaType, point, zoom]);
|
||||
|
||||
const handleClick = () => {
|
||||
openMapModal({ geoPoint: point, zoom });
|
||||
};
|
||||
|
||||
const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => {
|
||||
if (type !== 'geoLive') return;
|
||||
if (mediaType !== 'geoLive') return;
|
||||
const svgEl = countdownEl.lastElementChild!;
|
||||
const timerEl = countdownEl.firstElementChild!;
|
||||
|
||||
@ -144,7 +144,7 @@ const Location: FC<OwnProps> = ({
|
||||
|
||||
function renderInfo() {
|
||||
if (!shouldRenderText) return undefined;
|
||||
if (type === 'venue') {
|
||||
if (mediaType === 'venue') {
|
||||
return (
|
||||
<div className="location-info">
|
||||
<div className="location-info-title">
|
||||
@ -156,7 +156,7 @@ const Location: FC<OwnProps> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (type === 'geoLive') {
|
||||
if (mediaType === 'geoLive') {
|
||||
return (
|
||||
<div className="location-info">
|
||||
<div className="location-info-title">{lang('AttachLiveLocation')}</div>
|
||||
@ -201,10 +201,10 @@ const Location: FC<OwnProps> = ({
|
||||
function renderPin() {
|
||||
const pinClassName = buildClassName(
|
||||
'pin',
|
||||
type,
|
||||
mediaType,
|
||||
isExpired && 'expired',
|
||||
);
|
||||
if (type === 'geoLive') {
|
||||
if (mediaType === 'geoLive') {
|
||||
return (
|
||||
<div className={pinClassName}>
|
||||
<PinSvg />
|
||||
@ -216,7 +216,7 @@ const Location: FC<OwnProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'venue') {
|
||||
if (mediaType === 'venue') {
|
||||
const color = getVenueColor(location.venueType);
|
||||
const iconSrc = getVenueIconUrl(location.venueType);
|
||||
if (iconSrc) {
|
||||
|
||||
@ -38,8 +38,10 @@ import { AudioOrigin } from '../../../types';
|
||||
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config';
|
||||
import {
|
||||
areReactionsEmpty,
|
||||
getIsDownloading,
|
||||
getMessageContent,
|
||||
getMessageCustomShape,
|
||||
getMessageDownloadableMedia,
|
||||
getMessageHtmlId,
|
||||
getMessageSingleCustomEmoji,
|
||||
getMessageSingleRegularEmoji,
|
||||
@ -61,6 +63,7 @@ import {
|
||||
} from '../../../global/helpers';
|
||||
import { getMessageReplyInfo, getStoryReplyInfo } from '../../../global/helpers/replies';
|
||||
import {
|
||||
selectActiveDownloads,
|
||||
selectAllowedMessageActions,
|
||||
selectAnimatedEmoji,
|
||||
selectCanAutoLoadMedia,
|
||||
@ -76,7 +79,6 @@ import {
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsDocumentGroupSelected,
|
||||
selectIsDownloading,
|
||||
selectIsInSelectMode,
|
||||
selectIsMessageFocused,
|
||||
selectIsMessageProtected,
|
||||
@ -117,6 +119,7 @@ import renderText from '../../common/helpers/renderText';
|
||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||
import { buildContentClassName } from './helpers/buildContentClassName';
|
||||
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
import getSingularPaidMedia from './helpers/getSingularPaidMedia';
|
||||
import { calculateMediaDimensions, getMinMediaWidth, MIN_MEDIA_WIDTH_WITH_TEXT } from './helpers/mediaDimensions';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
@ -170,6 +173,7 @@ import MessageAppendix from './MessageAppendix';
|
||||
import MessageEffect from './MessageEffect';
|
||||
import MessageMeta from './MessageMeta';
|
||||
import MessagePhoneCall from './MessagePhoneCall';
|
||||
import PaidMediaOverlay from './PaidMediaOverlay';
|
||||
import Photo from './Photo';
|
||||
import Poll from './Poll';
|
||||
import Reactions from './reactions/Reactions';
|
||||
@ -495,6 +499,17 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const isScheduled = messageListType === 'scheduled' || message.isScheduled;
|
||||
const hasMessageReply = isReplyToMessage(message) && !shouldHideReply;
|
||||
|
||||
const { paidMedia } = getMessageContent(message);
|
||||
const { photo: paidMediaPhoto, video: paidMediaVideo } = getSingularPaidMedia(paidMedia);
|
||||
|
||||
const {
|
||||
photo = paidMediaPhoto, video = paidMediaVideo, audio,
|
||||
voice, document, sticker, contact,
|
||||
poll, webPage, invoice, location,
|
||||
action, game, storyData, giveaway,
|
||||
giveawayResults,
|
||||
} = getMessageContent(message);
|
||||
|
||||
const messageReplyInfo = getMessageReplyInfo(message);
|
||||
const storyReplyInfo = getStoryReplyInfo(message);
|
||||
|
||||
@ -510,11 +525,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
&& !isAnonymousForwards
|
||||
&& !forwardInfo.isLinkedChannelPost
|
||||
&& !isCustomShape
|
||||
) || Boolean(message.content.storyData && !message.content.storyData.isMention);
|
||||
) || Boolean(storyData && !storyData.isMention);
|
||||
const canShowSenderBoosts = Boolean(senderBoosts) && !asForwarded && isFirstInGroup;
|
||||
const isStoryMention = message.content.storyData?.isMention;
|
||||
const isAlbum = Boolean(album) && album!.messages.length > 1
|
||||
&& !album?.messages.some((msg) => Object.keys(msg.content).length === 0);
|
||||
const isStoryMention = storyData?.isMention;
|
||||
const isRoundVideo = video?.mediaType === 'video' && video.isRound;
|
||||
const isAlbum = Boolean(album)
|
||||
&& (
|
||||
(album.isPaidMedia && paidMedia!.extendedMedia.length > 1)
|
||||
|| album.messages.length > 1
|
||||
) && !album.messages.some((msg) => Object.keys(msg.content).length === 0);
|
||||
const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup;
|
||||
const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup;
|
||||
const isContextMenuShown = contextMenuPosition !== undefined;
|
||||
@ -552,7 +571,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
&& (isChatWithSelf || isRepliesChat || isAnonymousForwards || !messageSender);
|
||||
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
|
||||
const messageColorPeer = originSender || sender;
|
||||
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
|
||||
const senderPeer = (forwardInfo || storyData) ? originSender : messageSender;
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
|
||||
const {
|
||||
@ -676,13 +695,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isStoryMention && 'is-story-mention',
|
||||
);
|
||||
|
||||
const {
|
||||
photo, video, audio,
|
||||
voice, document, sticker, contact,
|
||||
poll, webPage, invoice, location,
|
||||
action, game, storyData, giveaway,
|
||||
giveawayResults,
|
||||
} = getMessageContent(message);
|
||||
const text = textMessage && getMessageContent(textMessage).text;
|
||||
const isInvertedMedia = Boolean(message.isInvertedMedia);
|
||||
|
||||
@ -726,7 +738,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
&& !isInDocumentGroupNotLast && !isStoryMention && !hasTtl;
|
||||
|
||||
const hasOutsideReactions = hasReactions
|
||||
&& (isCustomShape || ((photo || video || storyData || (location?.type === 'geo')) && !hasText));
|
||||
&& (isCustomShape || ((photo || video || storyData || (location?.mediaType === 'geo')) && !hasText));
|
||||
|
||||
const contentClassName = buildContentClassName(message, album, {
|
||||
hasSubheader,
|
||||
@ -738,7 +750,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
hasCommentCounter: hasThread && repliesThreadInfo.messagesCount > 0,
|
||||
hasActionButton: canForward || canFocus,
|
||||
hasReactions,
|
||||
isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message),
|
||||
isGeoLiveActive: location?.mediaType === 'geoLive' && !isGeoLiveExpired(message),
|
||||
withVoiceTranscription,
|
||||
peerColorClass: getPeerColorClass(messageColorPeer, noUserColors),
|
||||
hasOutsideReactions,
|
||||
@ -868,12 +880,24 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
if (!isAlbum && (photo || video || invoice?.extendedMedia)) {
|
||||
let width: number | undefined;
|
||||
if (photo) {
|
||||
width = calculateMediaDimensions(message, asForwarded, noAvatars, isMobile).width;
|
||||
width = calculateMediaDimensions({
|
||||
media: photo,
|
||||
isOwn,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
}).width;
|
||||
} else if (video) {
|
||||
if (video.isRound) {
|
||||
if (isRoundVideo) {
|
||||
width = ROUND_VIDEO_DIMENSIONS_PX;
|
||||
} else {
|
||||
width = calculateMediaDimensions(message, asForwarded, noAvatars, isMobile).width;
|
||||
width = calculateMediaDimensions({
|
||||
media: video,
|
||||
isOwn,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
}).width;
|
||||
}
|
||||
} else if (invoice?.extendedMedia && (
|
||||
invoice.extendedMedia.width && invoice.extendedMedia.height
|
||||
@ -921,7 +945,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
}, [
|
||||
albumLayout, asForwarded, extraPadding, hasSubheader, invoice?.extendedMedia, isAlbum, isMediaWithCommentButton,
|
||||
isMobile, isOwn, message, noAvatars, photo, sticker, text?.text, video,
|
||||
isMobile, isOwn, noAvatars, photo, sticker, text?.text, video, isRoundVideo,
|
||||
]);
|
||||
|
||||
const {
|
||||
@ -1133,7 +1157,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
chatId={chatId}
|
||||
/>
|
||||
)}
|
||||
{!isAlbum && video && video.isRound && (
|
||||
{!isAlbum && isRoundVideo && (
|
||||
<RoundVideo
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
@ -1166,7 +1190,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
{document && (
|
||||
<Document
|
||||
message={message}
|
||||
document={document}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb}
|
||||
@ -1282,7 +1306,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
outgoingStatus && 'with-outgoing-icon',
|
||||
);
|
||||
|
||||
const hasMediaAfterText = isAlbum || (!isAlbum && photo) || (!isAlbum && video && !video.isRound);
|
||||
const hasMediaAfterText = isAlbum || (!isAlbum && photo) || (!isAlbum && video && !isRoundVideo);
|
||||
const hasContentAfterText = hasMediaAfterText || (!hasAnimatedEmoji && hasFactCheck);
|
||||
const isMetaInText = metaPosition === 'in-text';
|
||||
|
||||
@ -1346,8 +1370,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderInvertibleMediaContent(hasCustomAppendix : boolean) {
|
||||
return (
|
||||
function renderInvertibleMediaContent(hasCustomAppendix: boolean) {
|
||||
const content = (
|
||||
<>
|
||||
{isAlbum && (
|
||||
<Album
|
||||
@ -1362,7 +1386,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
{!isAlbum && photo && (
|
||||
<Photo
|
||||
message={message}
|
||||
messageText={text?.text}
|
||||
photo={photo}
|
||||
isOwn={isOwn}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
@ -1377,9 +1403,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
onCancelUpload={handleCancelUpload}
|
||||
/>
|
||||
)}
|
||||
{!isAlbum && video && !video.isRound && (
|
||||
{!isAlbum && video && !isRoundVideo && (
|
||||
<Video
|
||||
message={message}
|
||||
video={video}
|
||||
isOwn={isOwn}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
forcedWidth={contentWidth}
|
||||
@ -1396,10 +1423,20 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (paidMedia) {
|
||||
return (
|
||||
<PaidMediaOverlay chatId={chatId} messageId={messageId} paidMedia={paidMedia} isOutgoing={isOwn}>
|
||||
{content}
|
||||
</PaidMediaOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function renderSenderName() {
|
||||
const media = photo || video || location;
|
||||
const media = photo || video || location || paidMedia;
|
||||
const shouldRender = !(isCustomShape && !viaBotId) && (
|
||||
(withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId || forceSenderName
|
||||
) && !isInDocumentGroupNotFirst && !(hasMessageReply && isCustomShape);
|
||||
@ -1714,7 +1751,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
}
|
||||
|
||||
const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||
const isDownloading = selectIsDownloading(global, message);
|
||||
const activeDownloads = selectActiveDownloads(global);
|
||||
const downloadableMedia = getMessageDownloadableMedia(message);
|
||||
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
|
||||
|
||||
const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id);
|
||||
|
||||
|
||||
25
src/components/middle/message/PaidMediaOverlay.module.scss
Normal file
25
src/components/middle/message/PaidMediaOverlay.module.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.buyButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.boughtStatus {
|
||||
right: 0.1875rem;
|
||||
left: auto !important;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-inline: 0.25rem 0.0625rem;
|
||||
}
|
||||
|
||||
.buttonText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
83
src/components/middle/message/PaidMediaOverlay.tsx
Normal file
83
src/components/middle/message/PaidMediaOverlay.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { memo, type TeactNode, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiPaidMedia } from '../../../api/types';
|
||||
|
||||
import { STARS_CURRENCY_CODE, STARS_ICON_PLACEHOLDER } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { replaceWithTeact } from '../../../util/replaceWithTeact';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import styles from './PaidMediaOverlay.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
paidMedia: ApiPaidMedia;
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
isOutgoing?: boolean;
|
||||
children?: TeactNode;
|
||||
};
|
||||
|
||||
const PaidMediaOverlay = ({
|
||||
paidMedia,
|
||||
chatId,
|
||||
messageId,
|
||||
isOutgoing,
|
||||
children,
|
||||
}: OwnProps) => {
|
||||
const { openInvoice } = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
const isClickable = !paidMedia.isBought;
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
const value = lang('UnlockPaidContent', paidMedia.starsAmount);
|
||||
|
||||
return replaceWithTeact(
|
||||
value, STARS_ICON_PLACEHOLDER, <StarIcon className={styles.star} type="gold" size="adaptive" />,
|
||||
);
|
||||
}, [lang, paidMedia]);
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent) => {
|
||||
openInvoice({
|
||||
type: 'message',
|
||||
chatId,
|
||||
messageId,
|
||||
});
|
||||
stopEvent(e);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
onClick={isClickable ? handleClick : undefined}
|
||||
>
|
||||
{children}
|
||||
{isClickable && (
|
||||
<Button
|
||||
className={styles.buyButton}
|
||||
color="dark"
|
||||
size="tiny"
|
||||
fluid
|
||||
pill
|
||||
>
|
||||
<span className={styles.buttonText}>{buttonText}</span>
|
||||
</Button>
|
||||
)}
|
||||
{paidMedia.isBought && (
|
||||
<div className={buildClassName('message-paid-media-status', styles.boughtStatus)}>
|
||||
{isOutgoing ? formatCurrency(paidMedia.starsAmount, STARS_CURRENCY_CODE) : lang('Chat.PaidMedia.Purchased')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PaidMediaOverlay);
|
||||
@ -1,7 +1,6 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useEffect, useRef, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ApiMediaExtendedPreview, ApiPhoto } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { ISettings } from '../../../types';
|
||||
import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||
@ -9,13 +8,10 @@ import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||
import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getMediaFormat,
|
||||
getMediaThumbUri,
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessagePhoto,
|
||||
getMessageWebPagePhoto,
|
||||
isOwnMessage,
|
||||
getPhotoMediaHash,
|
||||
} from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import getCustomAppendixBg from './helpers/getCustomAppendixBg';
|
||||
@ -35,9 +31,12 @@ import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
||||
import MediaSpoiler from '../../common/MediaSpoiler';
|
||||
import ProgressSpinner from '../../ui/ProgressSpinner';
|
||||
|
||||
export type OwnProps = {
|
||||
export type OwnProps<T> = {
|
||||
id?: string;
|
||||
message: ApiMessage;
|
||||
photo: ApiPhoto | ApiMediaExtendedPreview;
|
||||
isInWebPage?: boolean;
|
||||
messageText?: string;
|
||||
isOwn?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
noAvatars?: boolean;
|
||||
canAutoLoad?: boolean;
|
||||
@ -53,13 +52,18 @@ export type OwnProps = {
|
||||
isDownloading?: boolean;
|
||||
isProtected?: boolean;
|
||||
theme: ISettings['theme'];
|
||||
onClick?: (id: number) => void;
|
||||
onCancelUpload?: (message: ApiMessage) => void;
|
||||
className?: string;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T, e: React.MouseEvent<HTMLElement>) => void;
|
||||
onCancelUpload?: (arg: T) => void;
|
||||
};
|
||||
|
||||
const Photo: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line @typescript-eslint/comma-dangle
|
||||
const Photo = <T,>({
|
||||
id,
|
||||
message,
|
||||
photo,
|
||||
messageText,
|
||||
isOwn,
|
||||
observeIntersection,
|
||||
noAvatars,
|
||||
canAutoLoad,
|
||||
@ -75,53 +79,57 @@ const Photo: FC<OwnProps> = ({
|
||||
isDownloading,
|
||||
isProtected,
|
||||
theme,
|
||||
isInWebPage,
|
||||
clickArg,
|
||||
className,
|
||||
onClick,
|
||||
onCancelUpload,
|
||||
}) => {
|
||||
}: OwnProps<T>) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isPaidPreview = photo.mediaType === 'extendedMediaPreview';
|
||||
|
||||
const photo = (getMessagePhoto(message) || getMessageWebPagePhoto(message))!;
|
||||
const localBlobUrl = photo.blobUrl;
|
||||
const localBlobUrl = !isPaidPreview ? photo.blobUrl : undefined;
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
|
||||
const shouldLoad = isLoadAllowed && isIntersecting;
|
||||
const shouldLoad = isLoadAllowed && isIntersecting && !isPaidPreview;
|
||||
const {
|
||||
mediaData, loadProgress,
|
||||
} = useMediaWithLoadProgress(getMessageMediaHash(message, size), !shouldLoad);
|
||||
} = useMediaWithLoadProgress(!isPaidPreview ? getPhotoMediaHash(photo, size) : undefined, !shouldLoad);
|
||||
const fullMediaData = localBlobUrl || mediaData;
|
||||
|
||||
const withBlurredBackground = Boolean(forcedWidth);
|
||||
const [withThumb] = useState(!fullMediaData);
|
||||
const noThumb = Boolean(fullMediaData);
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(message, !withBlurredBackground);
|
||||
const thumbRef = useBlurredMediaThumbRef(photo, noThumb);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(photo, !withBlurredBackground);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(message);
|
||||
const thumbDataUri = getMediaThumbUri(photo);
|
||||
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(photo.isSpoiler);
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || photo.isSpoiler);
|
||||
|
||||
useEffect(() => {
|
||||
if (photo.isSpoiler) {
|
||||
if (isPaidPreview || photo.isSpoiler) {
|
||||
showSpoiler();
|
||||
} else {
|
||||
hideSpoiler();
|
||||
}
|
||||
}, [photo.isSpoiler]);
|
||||
}, [isPaidPreview, photo]);
|
||||
|
||||
const {
|
||||
loadProgress: downloadProgress,
|
||||
} = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'download'), !isDownloading, getMessageMediaFormat(message, 'download'),
|
||||
!isPaidPreview ? getPhotoMediaHash(photo, 'download') : undefined,
|
||||
!isDownloading,
|
||||
!isPaidPreview ? getMediaFormat(photo, 'download') : undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || (isDownloading ? downloadProgress : loadProgress),
|
||||
shouldLoad && !fullMediaData,
|
||||
uploadProgress !== undefined,
|
||||
@ -137,9 +145,9 @@ const Photo: FC<OwnProps> = ({
|
||||
transitionClassNames: downloadButtonClassNames,
|
||||
} = useShowTransition(!fullMediaData && !isLoadAllowed);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (isUploading) {
|
||||
onCancelUpload?.(message);
|
||||
onCancelUpload?.(clickArg!);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -153,10 +161,9 @@ const Photo: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(message.id);
|
||||
onClick?.(clickArg!, e);
|
||||
});
|
||||
|
||||
const isOwn = isOwnMessage(message);
|
||||
useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => {
|
||||
if (!shouldAffectAppendix) {
|
||||
if (prevShouldAffectAppendix) {
|
||||
@ -167,7 +174,7 @@ const Photo: FC<OwnProps> = ({
|
||||
|
||||
const contentEl = ref.current!.closest<HTMLDivElement>(MESSAGE_CONTENT_SELECTOR)!;
|
||||
if (fullMediaData) {
|
||||
getCustomAppendixBg(fullMediaData, isOwn, isSelected, theme).then((appendixBg) => {
|
||||
getCustomAppendixBg(fullMediaData, Boolean(isOwn), isSelected, theme).then((appendixBg) => {
|
||||
requestMutation(() => {
|
||||
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||
@ -178,14 +185,23 @@ const Photo: FC<OwnProps> = ({
|
||||
}
|
||||
}, [shouldAffectAppendix, fullMediaData, isOwn, isInSelectMode, isSelected, theme]);
|
||||
|
||||
const { width, height, isSmall } = dimensions || calculateMediaDimensions(message, asForwarded, noAvatars, isMobile);
|
||||
const { width, height, isSmall } = dimensions || calculateMediaDimensions({
|
||||
media: photo,
|
||||
isOwn,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
messageText,
|
||||
isInWebPage,
|
||||
});
|
||||
|
||||
const className = buildClassName(
|
||||
const componentClassName = buildClassName(
|
||||
'media-inner',
|
||||
!isUploading && !nonInteractive && 'interactive',
|
||||
isSmall && 'small-image',
|
||||
width === height && 'square-image',
|
||||
height < MIN_MEDIA_HEIGHT && 'fix-min-height',
|
||||
className,
|
||||
);
|
||||
|
||||
const dimensionsStyle = dimensions ? ` width: ${width}px; left: ${dimensions.x}px; top: ${dimensions.y}px;` : '';
|
||||
@ -195,7 +211,7 @@ const Photo: FC<OwnProps> = ({
|
||||
<div
|
||||
id={id}
|
||||
ref={ref}
|
||||
className={className}
|
||||
className={componentClassName}
|
||||
style={style}
|
||||
onClick={isUploading ? undefined : handleClick}
|
||||
>
|
||||
|
||||
@ -13,7 +13,7 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import {
|
||||
getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, hasMessageTtl,
|
||||
getMediaFormat, getMessageMediaThumbDataUri, getVideoMediaHash, hasMessageTtl,
|
||||
} from '../../../global/helpers';
|
||||
import { stopCurrentAudio } from '../../../util/audioPlayer';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -76,20 +76,20 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
|
||||
const video = message.content.video!;
|
||||
|
||||
const { cancelMessageMediaDownload, openOneTimeMediaModal } = getActions();
|
||||
const { cancelMediaDownload, openOneTimeMediaModal } = getActions();
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
|
||||
const shouldLoad = Boolean(isLoadAllowed && isIntersecting);
|
||||
const { mediaData, loadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'inline'),
|
||||
getVideoMediaHash(video, 'inline'),
|
||||
!shouldLoad,
|
||||
getMessageMediaFormat(message, 'inline'),
|
||||
getMediaFormat(video, 'inline'),
|
||||
);
|
||||
|
||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'download'),
|
||||
getVideoMediaHash(video, 'download'),
|
||||
!isDownloading,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
);
|
||||
@ -100,7 +100,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
const shouldRenderSpoiler = hasTtl && !isInOneTimeModal;
|
||||
const hasThumb = Boolean(getMessageMediaThumbDataUri(message));
|
||||
const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler;
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const thumbRef = useBlurredMediaThumbRef(video, noThumb);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(message);
|
||||
const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading;
|
||||
@ -186,7 +186,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message });
|
||||
cancelMediaDownload({ media: video });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import type { ApiMessage } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getMessageMediaHash } from '../../../global/helpers';
|
||||
import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { IS_WEBM_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
||||
@ -58,7 +58,7 @@ const Sticker: FC<OwnProps> = ({
|
||||
const isMirrored = !message.isOutgoing;
|
||||
|
||||
const mediaHash = sticker.isPreloadedGlobally ? undefined : (
|
||||
getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
||||
getStickerMediaHash(sticker, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
||||
);
|
||||
|
||||
const canLoad = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
@ -1,24 +1,20 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useEffect, useRef, useState } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ApiMediaExtendedPreview, ApiVideo } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||
|
||||
import {
|
||||
getMediaFormat,
|
||||
getMediaThumbUri,
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessageVideo,
|
||||
getMessageWebPageVideo,
|
||||
isOwnMessage,
|
||||
getVideoMediaHash,
|
||||
} from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatMediaDuration } from '../../../util/dates/dateFormat';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions';
|
||||
import { calculateExtendedPreviewDimensions, calculateVideoDimensions } from '../../common/helpers/mediaDimensions';
|
||||
import { MIN_MEDIA_HEIGHT } from './helpers/mediaDimensions';
|
||||
|
||||
import useUnsupportedMedia from '../../../hooks/media/useUnsupportedMedia';
|
||||
@ -37,10 +33,12 @@ import MediaSpoiler from '../../common/MediaSpoiler';
|
||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
import ProgressSpinner from '../../ui/ProgressSpinner';
|
||||
|
||||
export type OwnProps = {
|
||||
export type OwnProps<T> = {
|
||||
id?: string;
|
||||
message: ApiMessage;
|
||||
observeIntersectionForLoading: ObserveFn;
|
||||
video: ApiVideo | ApiMediaExtendedPreview;
|
||||
isOwn?: boolean;
|
||||
isInWebPage?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
noAvatars?: boolean;
|
||||
canAutoLoad?: boolean;
|
||||
@ -51,13 +49,18 @@ export type OwnProps = {
|
||||
asForwarded?: boolean;
|
||||
isDownloading?: boolean;
|
||||
isProtected?: boolean;
|
||||
onClick?: (id: number, isGif?: boolean) => void;
|
||||
onCancelUpload?: (message: ApiMessage) => void;
|
||||
className?: string;
|
||||
clickArg?: T;
|
||||
onClick?: (arg: T, e: React.MouseEvent<HTMLElement>) => void;
|
||||
onCancelUpload?: (arg: T) => void;
|
||||
};
|
||||
|
||||
const Video: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line @typescript-eslint/comma-dangle
|
||||
const Video = <T,>({
|
||||
id,
|
||||
message,
|
||||
video,
|
||||
isOwn,
|
||||
isInWebPage,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
noAvatars,
|
||||
@ -69,26 +72,30 @@ const Video: FC<OwnProps> = ({
|
||||
asForwarded,
|
||||
isDownloading,
|
||||
isProtected,
|
||||
className,
|
||||
clickArg,
|
||||
onClick,
|
||||
onCancelUpload,
|
||||
}) => {
|
||||
}: OwnProps<T>) => {
|
||||
const { cancelMediaDownload } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!;
|
||||
const localBlobUrl = video.blobUrl;
|
||||
const isPaidPreview = video.mediaType === 'extendedMediaPreview';
|
||||
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(video.isSpoiler);
|
||||
const localBlobUrl = !isPaidPreview ? video.blobUrl : undefined;
|
||||
|
||||
const [isSpoilerShown, showSpoiler, hideSpoiler] = useFlag(isPaidPreview || video.isSpoiler);
|
||||
|
||||
useEffect(() => {
|
||||
if (video.isSpoiler) {
|
||||
if (isPaidPreview || video.isSpoiler) {
|
||||
showSpoiler();
|
||||
} else {
|
||||
hideSpoiler();
|
||||
}
|
||||
}, [video.isSpoiler]);
|
||||
}, [isPaidPreview, video]);
|
||||
|
||||
const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
const isIntersectingForPlaying = (
|
||||
@ -102,43 +109,44 @@ const Video: FC<OwnProps> = ({
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
|
||||
const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading);
|
||||
const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading && !isPaidPreview);
|
||||
const [isPlayAllowed, setIsPlayAllowed] = useState(Boolean(canAutoPlay && !isSpoilerShown));
|
||||
|
||||
const fullMediaHash = getMessageMediaHash(message, 'inline');
|
||||
const fullMediaHash = !isPaidPreview ? getVideoMediaHash(video, 'inline') : undefined;
|
||||
const [isFullMediaPreloaded] = useState(Boolean(fullMediaHash && mediaLoader.getFromMemory(fullMediaHash)));
|
||||
const { mediaData, loadProgress } = useMediaWithLoadProgress(
|
||||
fullMediaHash, !shouldLoad, getMessageMediaFormat(message, 'inline'),
|
||||
fullMediaHash,
|
||||
!shouldLoad,
|
||||
!isPaidPreview ? getMediaFormat(video, 'inline') : undefined,
|
||||
);
|
||||
const fullMediaData = localBlobUrl || mediaData;
|
||||
const [isPlayerReady, markPlayerReady] = useFlag();
|
||||
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(message);
|
||||
const thumbDataUri = getMediaThumbUri(video);
|
||||
const hasThumb = Boolean(thumbDataUri);
|
||||
const withBlurredBackground = Boolean(forcedWidth);
|
||||
|
||||
const previewMediaHash = getMessageMediaHash(message, 'preview');
|
||||
const previewMediaHash = !isPaidPreview ? getVideoMediaHash(video, 'preview') : undefined;
|
||||
const [isPreviewPreloaded] = useState(Boolean(previewMediaHash && mediaLoader.getFromMemory(previewMediaHash)));
|
||||
const canLoadPreview = isIntersectingForLoading;
|
||||
const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview);
|
||||
const previewClassNames = useMediaTransition((hasThumb || previewBlobUrl) && !isPlayerReady);
|
||||
|
||||
const noThumb = !hasThumb || previewBlobUrl || isPlayerReady;
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(message, !withBlurredBackground);
|
||||
const noThumb = Boolean(!hasThumb || previewBlobUrl || isPlayerReady);
|
||||
const thumbRef = useBlurredMediaThumbRef(video, noThumb);
|
||||
const blurredBackgroundRef = useBlurredMediaThumbRef(video, !withBlurredBackground);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
|
||||
const isInline = fullMediaData && wasIntersectedRef.current;
|
||||
|
||||
const isUnsupported = useUnsupportedMedia(videoRef, true, !isInline);
|
||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'download'),
|
||||
!isPaidPreview ? getVideoMediaHash(video, 'download') : undefined,
|
||||
!isDownloading,
|
||||
getMessageMediaFormat(message, 'download'),
|
||||
!isPaidPreview ? getMediaFormat(video, 'download') : undefined,
|
||||
);
|
||||
|
||||
const { isUploading, isTransferring, transferProgress } = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || (isDownloading ? downloadProgress : loadProgress),
|
||||
(shouldLoad && !isPlayerReady && !isFullMediaPreloaded) || isDownloading,
|
||||
uploadProgress !== undefined,
|
||||
@ -160,20 +168,22 @@ const Video: FC<OwnProps> = ({
|
||||
|
||||
const duration = (Number.isFinite(videoRef.current?.duration) ? videoRef.current?.duration : video.duration) || 0;
|
||||
|
||||
const isOwn = isOwnMessage(message);
|
||||
const isWebPageVideo = Boolean(getMessageWebPageVideo(message));
|
||||
const {
|
||||
width, height,
|
||||
} = dimensions || calculateVideoDimensions(video, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile);
|
||||
} = dimensions || (
|
||||
isPaidPreview
|
||||
? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
||||
: calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
||||
);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (isUploading) {
|
||||
onCancelUpload?.(message);
|
||||
onCancelUpload?.(clickArg!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
getActions().cancelMessageMediaDownload({ message });
|
||||
if (!isPaidPreview && isDownloading) {
|
||||
cancelMediaDownload({ media: video });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -191,13 +201,14 @@ const Video: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(message.id, video?.isGif);
|
||||
onClick?.(clickArg!, e);
|
||||
});
|
||||
|
||||
const className = buildClassName(
|
||||
const componentClassName = buildClassName(
|
||||
'media-inner dark',
|
||||
!isUploading && 'interactive',
|
||||
height < MIN_MEDIA_HEIGHT && 'fix-min-height',
|
||||
className,
|
||||
);
|
||||
|
||||
const dimensionsStyle = dimensions ? ` width: ${width}px; left: ${dimensions.x}px; top: ${dimensions.y}px;` : '';
|
||||
@ -207,7 +218,7 @@ const Video: FC<OwnProps> = ({
|
||||
<div
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={className}
|
||||
className={componentClassName}
|
||||
style={style}
|
||||
onClick={isUploading ? undefined : handleClick}
|
||||
>
|
||||
@ -266,7 +277,7 @@ const Video: FC<OwnProps> = ({
|
||||
</span>
|
||||
) : (
|
||||
<div className="message-media-duration">
|
||||
{video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))}
|
||||
{!isPaidPreview && video.isGif ? 'GIF' : formatMediaDuration(Math.max(duration - playProgress, 0))}
|
||||
{isUnsupported && <i className="icon icon-message-failed playback-failed" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -137,7 +137,14 @@ const WebPage: FC<OwnProps> = ({
|
||||
const isArticle = Boolean(truncatedDescription || title || siteName);
|
||||
let isSquarePhoto = Boolean(stickers);
|
||||
if (isArticle && webPage?.photo && !webPage.video) {
|
||||
const { width, height } = calculateMediaDimensions(message, undefined, undefined, isMobile);
|
||||
const { width, height } = calculateMediaDimensions({
|
||||
media: webPage.photo,
|
||||
isOwn: message.isOutgoing,
|
||||
isInWebPage: true,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
});
|
||||
isSquarePhoto = width === height;
|
||||
}
|
||||
const isMediaInteractive = (photo || video) && onMediaClick && !isSquarePhoto;
|
||||
@ -188,7 +195,9 @@ const WebPage: FC<OwnProps> = ({
|
||||
)}
|
||||
{photo && !video && (
|
||||
<Photo
|
||||
message={message}
|
||||
photo={photo}
|
||||
isOwn={message.isOutgoing}
|
||||
isInWebPage
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoad}
|
||||
@ -215,7 +224,9 @@ const WebPage: FC<OwnProps> = ({
|
||||
)}
|
||||
{!inPreview && video && (
|
||||
<Video
|
||||
message={message}
|
||||
video={video}
|
||||
isOwn={message.isOutgoing}
|
||||
isInWebPage
|
||||
observeIntersectionForLoading={observeIntersectionForLoading!}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoad}
|
||||
@ -240,7 +251,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
)}
|
||||
{!inPreview && document && (
|
||||
<Document
|
||||
message={message}
|
||||
document={document}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb}
|
||||
onMediaClick={handleMediaClick}
|
||||
|
||||
@ -37,6 +37,11 @@
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.content-unsupported {
|
||||
font-style: italic;
|
||||
color: var(--color-text-meta);
|
||||
}
|
||||
|
||||
.text-content,
|
||||
.transcription {
|
||||
margin: 0;
|
||||
@ -678,7 +683,8 @@
|
||||
}
|
||||
|
||||
.message-media-duration,
|
||||
.message-transfer-progress {
|
||||
.message-transfer-progress,
|
||||
.message-paid-media-status {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
|
||||
@ -3,6 +3,7 @@ import type { IAlbum } from '../../../../types';
|
||||
|
||||
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
|
||||
import { getMessageContent } from '../../../../global/helpers';
|
||||
import getSingularPaidMedia from './getSingularPaidMedia';
|
||||
|
||||
export function buildContentClassName(
|
||||
message: ApiMessage,
|
||||
@ -37,19 +38,24 @@ export function buildContentClassName(
|
||||
hasOutsideReactions?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const { paidMedia } = getMessageContent(message);
|
||||
const { photo: paidMediaPhoto, video: paidMediaVideo } = getSingularPaidMedia(paidMedia);
|
||||
|
||||
const {
|
||||
photo, video, audio, voice, document, poll, webPage, contact, location, invoice, storyData,
|
||||
photo = paidMediaPhoto, video = paidMediaVideo,
|
||||
audio, voice, document, poll, webPage, contact, location, invoice, storyData,
|
||||
giveaway, giveawayResults,
|
||||
} = getMessageContent(message);
|
||||
const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text;
|
||||
const hasFactCheck = Boolean(message.factCheck?.text);
|
||||
|
||||
const isRoundVideo = video?.mediaType === 'video' && video.isRound;
|
||||
const isInvertedMedia = message.isInvertedMedia;
|
||||
const isInvertibleMedia = photo || (video && !video?.isRound) || album || webPage;
|
||||
const isInvertibleMedia = photo || (video && !isRoundVideo) || album || webPage;
|
||||
|
||||
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
|
||||
const isMedia = storyData || photo || video || location || invoice?.extendedMedia;
|
||||
const hasText = text || location?.type === 'venue' || isGeoLiveActive || hasFactCheck;
|
||||
const isMedia = storyData || photo || video || location || invoice?.extendedMedia || paidMedia;
|
||||
const hasText = text || location?.mediaType === 'venue' || isGeoLiveActive || hasFactCheck;
|
||||
const isMediaWithNoText = isMedia && !hasText;
|
||||
const isViaBot = Boolean(message.viaBotId);
|
||||
|
||||
@ -84,7 +90,7 @@ export function buildContentClassName(
|
||||
|
||||
if (isCustomShape) {
|
||||
classNames.push('custom-shape');
|
||||
if (video?.isRound) {
|
||||
if (isRoundVideo) {
|
||||
classNames.push('round');
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import type { ApiDimensions, ApiMessage } from '../../../../api/types';
|
||||
import type { IAlbum } from '../../../../types';
|
||||
|
||||
import { getMessageContent } from '../../../../global/helpers';
|
||||
import { clamp } from '../../../../util/math';
|
||||
import { getAvailableWidth, REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { calculateMediaDimensions } from './mediaDimensions';
|
||||
@ -46,10 +47,23 @@ export type IAlbumLayout = {
|
||||
containerStyle: ApiDimensions;
|
||||
};
|
||||
|
||||
function getRatios(messages: ApiMessage[], isMobile?: boolean) {
|
||||
return messages.map(
|
||||
(message) => {
|
||||
const dimensions = calculateMediaDimensions(message, undefined, undefined, isMobile) as ApiDimensions;
|
||||
function getRatios(messages: ApiMessage[], isSingleMessage?: boolean, isMobile?: boolean) {
|
||||
const isOutgoing = messages[0].isOutgoing;
|
||||
const allMedia = (isSingleMessage
|
||||
? messages[0].content.paidMedia!.extendedMedia.map((media) => (
|
||||
'mediaType' in media ? media : (media.photo || media.video)
|
||||
))
|
||||
: messages.map((message) => (
|
||||
getMessageContent(message).photo || getMessageContent(message).video
|
||||
))
|
||||
).filter(Boolean);
|
||||
return allMedia.map(
|
||||
(media) => {
|
||||
const dimensions = calculateMediaDimensions({
|
||||
media,
|
||||
isOwn: isOutgoing,
|
||||
isMobile,
|
||||
}) as ApiDimensions;
|
||||
|
||||
return dimensions.width / dimensions.height;
|
||||
},
|
||||
@ -99,7 +113,7 @@ export function calculateAlbumLayout(
|
||||
isMobile?: boolean,
|
||||
): IAlbumLayout {
|
||||
const spacing = 2;
|
||||
const ratios = getRatios(album.messages, isMobile);
|
||||
const ratios = getRatios(album.messages, album.isPaidMedia, isMobile);
|
||||
const proportions = getProportions(ratios);
|
||||
const averageRatio = getAverageRatio(ratios);
|
||||
const albumCount = ratios.length;
|
||||
|
||||
@ -5,12 +5,12 @@ import { ApiMediaFormat } from '../../../../api/types';
|
||||
import {
|
||||
getMessageContact,
|
||||
getMessageHtmlId,
|
||||
getMessageMediaHash,
|
||||
getMessagePhoto,
|
||||
getMessageText,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
hasMessageLocalBlobUrl,
|
||||
getPhotoMediaHash,
|
||||
hasMediaLocalBlobUrl,
|
||||
} from '../../../../global/helpers';
|
||||
import { getMessageTextWithSpoilers } from '../../../../global/helpers/messageSummary';
|
||||
import {
|
||||
@ -44,8 +44,8 @@ export function getMessageCopyOptions(
|
||||
const photo = getMessagePhoto(message)
|
||||
|| (!getMessageWebPageVideo(message) ? getMessageWebPagePhoto(message) : undefined);
|
||||
const contact = getMessageContact(message);
|
||||
const mediaHash = getMessageMediaHash(message, 'inline');
|
||||
const canImageBeCopied = canCopy && photo && (mediaHash || hasMessageLocalBlobUrl(message))
|
||||
const mediaHash = photo ? getPhotoMediaHash(photo, 'inline') : undefined;
|
||||
const canImageBeCopied = canCopy && photo && (mediaHash || hasMediaLocalBlobUrl(photo))
|
||||
&& CLIPBOARD_ITEM_SUPPORTED && !IS_SAFARI;
|
||||
const selection = window.getSelection();
|
||||
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import type { ApiPaidMedia } from '../../../../api/types';
|
||||
|
||||
export default function getSingularPaidMedia(media?: ApiPaidMedia) {
|
||||
if (!media || media.extendedMedia.length !== 1) {
|
||||
return {
|
||||
photo: undefined,
|
||||
video: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const singularMedia = media.extendedMedia[0];
|
||||
const isPreview = 'mediaType' in singularMedia;
|
||||
const photo = isPreview ? (!singularMedia.duration ? singularMedia : undefined) : singularMedia.photo;
|
||||
const video = isPreview ? (singularMedia.duration ? singularMedia : undefined) : singularMedia.video;
|
||||
|
||||
return { photo, video };
|
||||
}
|
||||
@ -1,14 +1,11 @@
|
||||
import type { ApiMessage } from '../../../../api/types';
|
||||
import type { ApiMediaExtendedPreview, ApiPhoto, ApiVideo } from '../../../../api/types';
|
||||
|
||||
import {
|
||||
getMessagePhoto,
|
||||
getMessageText,
|
||||
getMessageVideo,
|
||||
getMessageWebPagePhoto,
|
||||
getMessageWebPageVideo,
|
||||
isOwnMessage,
|
||||
} from '../../../../global/helpers';
|
||||
import { calculateInlineImageDimensions, calculateVideoDimensions, REM } from '../../../common/helpers/mediaDimensions';
|
||||
calculateExtendedPreviewDimensions,
|
||||
calculateInlineImageDimensions,
|
||||
calculateVideoDimensions,
|
||||
REM,
|
||||
} from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
const SMALL_IMAGE_THRESHOLD = 12;
|
||||
const MIN_MESSAGE_LENGTH_FOR_BLUR = 40;
|
||||
@ -22,20 +19,32 @@ export function getMinMediaWidth(text?: string, hasCommentButton?: boolean) {
|
||||
: MIN_MEDIA_WIDTH;
|
||||
}
|
||||
|
||||
export function calculateMediaDimensions(
|
||||
message: ApiMessage, asForwarded?: boolean, noAvatars?: boolean, isMobile?: boolean,
|
||||
) {
|
||||
const isOwn = isOwnMessage(message);
|
||||
const photo = getMessagePhoto(message) || getMessageWebPagePhoto(message);
|
||||
const video = getMessageVideo(message);
|
||||
export function calculateMediaDimensions({
|
||||
media,
|
||||
messageText,
|
||||
isOwn,
|
||||
isInWebPage,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
} : {
|
||||
media: ApiPhoto | ApiVideo | ApiMediaExtendedPreview;
|
||||
messageText?: string;
|
||||
isOwn?: boolean;
|
||||
isInWebPage?: boolean;
|
||||
asForwarded?: boolean;
|
||||
noAvatars?: boolean;
|
||||
isMobile?: boolean;
|
||||
}) {
|
||||
const isPhoto = media.mediaType === 'photo';
|
||||
const isVideo = media.mediaType === 'video';
|
||||
const isWebPagePhoto = isPhoto && isInWebPage;
|
||||
const isWebPageVideo = isVideo && isInWebPage;
|
||||
const { width, height } = isPhoto
|
||||
? calculateInlineImageDimensions(media, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
|
||||
: isVideo ? calculateVideoDimensions(media, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile)
|
||||
: calculateExtendedPreviewDimensions(media, isOwn, asForwarded, isInWebPage, noAvatars, isMobile);
|
||||
|
||||
const isWebPagePhoto = Boolean(getMessageWebPagePhoto(message));
|
||||
const isWebPageVideo = Boolean(getMessageWebPageVideo(message));
|
||||
const { width, height } = photo
|
||||
? calculateInlineImageDimensions(photo, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
|
||||
: calculateVideoDimensions(video!, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile);
|
||||
|
||||
const messageText = getMessageText(message);
|
||||
const minMediaWidth = getMinMediaWidth(messageText);
|
||||
|
||||
let stretchFactor = 1;
|
||||
|
||||
@ -14,9 +14,10 @@ import buildClassName from '../../../../util/buildClassName';
|
||||
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
type OwnProps =
|
||||
PhotoProps
|
||||
& VideoProps;
|
||||
type OwnProps<T> =
|
||||
(PhotoProps<T> | VideoProps<T>) & {
|
||||
clickArg: number;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
isInSelectMode?: boolean;
|
||||
@ -24,18 +25,19 @@ type StateProps = {
|
||||
};
|
||||
|
||||
export default function withSelectControl(WrappedComponent: FC) {
|
||||
const ComponentWithSelectControl: FC<OwnProps & StateProps> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/comma-dangle
|
||||
const ComponentWithSelectControl = <T,>(props: OwnProps<T> & StateProps) => {
|
||||
const {
|
||||
isInSelectMode,
|
||||
isSelected,
|
||||
message,
|
||||
dimensions,
|
||||
clickArg,
|
||||
} = props;
|
||||
const { toggleMessageSelection } = getActions();
|
||||
|
||||
const handleMessageSelect = useLastCallback((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
toggleMessageSelection({ messageId: message.id, withShift: e?.shiftKey });
|
||||
toggleMessageSelection({ messageId: clickArg, withShift: e?.shiftKey });
|
||||
});
|
||||
|
||||
const newProps = useMemo(() => {
|
||||
@ -72,13 +74,13 @@ export default function withSelectControl(WrappedComponent: FC) {
|
||||
);
|
||||
};
|
||||
|
||||
return memo(withGlobal<OwnProps>(
|
||||
return memo(withGlobal<OwnProps<unknown>>(
|
||||
(global, ownProps) => {
|
||||
const { message } = ownProps;
|
||||
const { clickArg } = ownProps;
|
||||
return {
|
||||
isInSelectMode: selectIsInSelectMode(global),
|
||||
isSelected: selectIsMessageSelected(global, message.id),
|
||||
isSelected: selectIsMessageSelected(global, clickArg),
|
||||
};
|
||||
},
|
||||
)(ComponentWithSelectControl));
|
||||
)(ComponentWithSelectControl)) as typeof ComponentWithSelectControl;
|
||||
}
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
import type { ApiMessage } from '../../../../api/types';
|
||||
|
||||
import { getMessageMediaThumbDataUri } from '../../../../global/helpers';
|
||||
import { getMediaThumbUri, type MediaWithThumbs } from '../../../../global/helpers';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../../util/windowEnvironment';
|
||||
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../../../hooks/useCanvasBlur';
|
||||
|
||||
type CanvasBlurReturnType = ReturnType<typeof useCanvasBlur>;
|
||||
|
||||
export default function useBlurredMediaThumbRef(
|
||||
message: ApiMessage,
|
||||
isDisabled?: boolean | string,
|
||||
forcedUri?: string,
|
||||
forcedUri: string | undefined, isDisabled: boolean,
|
||||
): CanvasBlurReturnType;
|
||||
export default function useBlurredMediaThumbRef(media: MediaWithThumbs, isDisabled?: boolean) : CanvasBlurReturnType;
|
||||
export default function useBlurredMediaThumbRef(
|
||||
media: MediaWithThumbs | string | undefined,
|
||||
isDisabled?: boolean,
|
||||
) {
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const dataUri = forcedUri || getMessageMediaThumbDataUri(message);
|
||||
const dataUri = media ? typeof media === 'string' ? media : getMediaThumbUri(media) : undefined;
|
||||
|
||||
return useCanvasBlur(
|
||||
dataUri,
|
||||
Boolean(isDisabled),
|
||||
isDisabled,
|
||||
isMobile && !IS_CANVAS_FILTER_SUPPORTED,
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export default function useInnerHandlers(
|
||||
} = getActions();
|
||||
|
||||
const {
|
||||
id: messageId, forwardInfo, groupedId,
|
||||
id: messageId, forwardInfo, groupedId, content: { paidMedia },
|
||||
} = message;
|
||||
|
||||
const {
|
||||
@ -98,18 +98,20 @@ export default function useInnerHandlers(
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId: messageId,
|
||||
messageId,
|
||||
origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
|
||||
});
|
||||
});
|
||||
const openMediaViewerWithPhotoOrVideo = useLastCallback((withDynamicLoading: boolean): void => {
|
||||
if (paidMedia && !paidMedia.isBought) return;
|
||||
if (paidMedia) return; // TODO: Implement MV and remove this line
|
||||
if (withDynamicLoading) {
|
||||
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: messageId });
|
||||
}
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId: messageId,
|
||||
messageId,
|
||||
origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
|
||||
withDynamicLoading,
|
||||
});
|
||||
@ -118,7 +120,8 @@ export default function useInnerHandlers(
|
||||
const withDynamicLoading = !isScheduled;
|
||||
openMediaViewerWithPhotoOrVideo(withDynamicLoading);
|
||||
});
|
||||
const handleVideoMediaClick = useLastCallback((id: number, isGif?: boolean): void => {
|
||||
const handleVideoMediaClick = useLastCallback(() => {
|
||||
const isGif = message.content?.video?.isGif;
|
||||
const withDynamicLoading = !isGif && !isScheduled;
|
||||
openMediaViewerWithPhotoOrVideo(withDynamicLoading);
|
||||
});
|
||||
@ -127,14 +130,17 @@ export default function useInnerHandlers(
|
||||
openAudioPlayer({ chatId, messageId });
|
||||
});
|
||||
|
||||
const handleAlbumMediaClick = useLastCallback((albumMessageId: number): void => {
|
||||
const handleAlbumMediaClick = useLastCallback((albumMessageId: number, albumIndex?: number): void => {
|
||||
if (paidMedia && !paidMedia.isBought) return;
|
||||
|
||||
searchChatMediaMessages({ chatId, threadId, currentMediaMessageId: messageId });
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
threadId,
|
||||
mediaId: albumMessageId,
|
||||
messageId: albumMessageId,
|
||||
mediaIndex: albumIndex,
|
||||
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
|
||||
withDynamicLoading: true,
|
||||
withDynamicLoading: !paidMedia,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -26,9 +26,11 @@ type OwnProps = {
|
||||
headerImageUrl?: string;
|
||||
headerAvatarPeer?: ApiPeer | CustomPeer;
|
||||
headerAvatarWebPhoto?: ApiWebDocument;
|
||||
noHeaderImage?: boolean;
|
||||
header?: TeactNode;
|
||||
footer?: TeactNode;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
onClose: NoneToVoidFunction;
|
||||
onButtonClick?: NoneToVoidFunction;
|
||||
};
|
||||
@ -40,9 +42,11 @@ const TableInfoModal = ({
|
||||
headerImageUrl,
|
||||
headerAvatarPeer,
|
||||
headerAvatarWebPhoto,
|
||||
noHeaderImage,
|
||||
header,
|
||||
footer,
|
||||
buttonText,
|
||||
className,
|
||||
onClose,
|
||||
onButtonClick,
|
||||
}: OwnProps) => {
|
||||
@ -61,13 +65,16 @@ const TableInfoModal = ({
|
||||
hasAbsoluteCloseButton={!title}
|
||||
isSlim
|
||||
title={title}
|
||||
className={className}
|
||||
contentClassName={styles.content}
|
||||
onClose={onClose}
|
||||
>
|
||||
{withAvatar ? (
|
||||
<Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} />
|
||||
) : (
|
||||
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} />
|
||||
{!noHeaderImage && (
|
||||
withAvatar ? (
|
||||
<Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} />
|
||||
) : (
|
||||
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} />
|
||||
)
|
||||
)}
|
||||
{header}
|
||||
<table className={styles.table}>
|
||||
|
||||
90
src/components/modals/stars/PaidMediaThumb.module.scss
Normal file
90
src/components/modals/stars/PaidMediaThumb.module.scss
Normal file
@ -0,0 +1,90 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-auto-columns: 0.5rem;
|
||||
grid-auto-rows: 0.5rem;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
|
||||
height: 5rem;
|
||||
position: relative;
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.preview {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
|
||||
grid-auto-columns: 0.25rem;
|
||||
grid-auto-rows: 0.25rem;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: relative;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
outline: 0.25rem solid var(--color-background);
|
||||
}
|
||||
|
||||
.preview .thumb {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
outline-width: 0.125rem;
|
||||
}
|
||||
|
||||
.noOutline {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.thumb:nth-child(1),
|
||||
.itemCount1 .count {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.thumb:nth-child(2),
|
||||
.itemCount2 .count {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.thumb:nth-child(3),
|
||||
.itemCount3 .count {
|
||||
grid-row: 3;
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.blurry, .full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.full {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
92
src/components/modals/stars/PaidMediaThumb.tsx
Normal file
92
src/components/modals/stars/PaidMediaThumb.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiMediaExtendedPreview, BoughtPaidMedia } from '../../../api/types';
|
||||
|
||||
import { getMediaHash, getMediaThumbUri } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import MediaSpoiler from '../../common/MediaSpoiler';
|
||||
|
||||
import styles from './PaidMediaThumb.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
media: ApiMediaExtendedPreview[] | BoughtPaidMedia[];
|
||||
isTransactionPreview?: boolean;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const THUMB_LIMIT = 3;
|
||||
const PREVIEW_THUMB_LIMIT = 2;
|
||||
|
||||
const PaidMediaThumb = ({
|
||||
media, className, isTransactionPreview, onClick,
|
||||
}: OwnProps) => {
|
||||
const count = Math.min(media.length, isTransactionPreview ? PREVIEW_THUMB_LIMIT : THUMB_LIMIT);
|
||||
const isLocked = 'mediaType' in media[0];
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
styles[`itemCount${count}`],
|
||||
isTransactionPreview && styles.preview,
|
||||
className,
|
||||
)}
|
||||
dir="rtl"
|
||||
onClick={onClick}
|
||||
>
|
||||
{media.slice(0, count).reverse().map((item, i, arr) => {
|
||||
const realIndex = arr.length - i - 1;
|
||||
return 'mediaType' in item ? (
|
||||
<MediaSpoiler
|
||||
className={styles.thumb}
|
||||
isVisible
|
||||
width={item.width}
|
||||
height={item.height}
|
||||
thumbDataUri={item.thumbnail?.dataUri}
|
||||
/>
|
||||
) : (
|
||||
<SingleMediaThumb
|
||||
className={buildClassName(isTransactionPreview && realIndex > 0 && styles.noOutline)}
|
||||
boughtMedia={item}
|
||||
index={realIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isLocked && (
|
||||
<div className={styles.count}>
|
||||
<Icon name="stars-lock" />
|
||||
{media.length > 1 ? media.length : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function SingleMediaThumb({
|
||||
boughtMedia,
|
||||
index,
|
||||
className,
|
||||
}: {
|
||||
boughtMedia: BoughtPaidMedia;
|
||||
index?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const media = (boughtMedia.video || boughtMedia.photo)!;
|
||||
const mediaHash = getMediaHash(media, 'pictogram');
|
||||
const thumb = getMediaThumbUri(media);
|
||||
|
||||
const mediaBlob = useMedia(mediaHash);
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.thumb, index !== undefined && `stars-transaction-media-${index}`, className)}>
|
||||
{thumb && <img className={styles.blurry} src={thumb} alt="" />}
|
||||
{mediaBlob && <img className={styles.full} src={mediaBlob} alt="" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PaidMediaThumb);
|
||||
@ -1,5 +1,9 @@
|
||||
@use '../../../styles/mixins';
|
||||
|
||||
.root {
|
||||
z-index: calc(var(--z-media-viewer) - 1);
|
||||
}
|
||||
|
||||
.root :global(.modal-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type {
|
||||
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectTabState, selectUser } from '../../../global/selectors';
|
||||
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectTabState, selectUser,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
@ -18,6 +22,7 @@ import StarIcon from '../../common/icons/StarIcon';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
import BalanceBlock from './BalanceBlock';
|
||||
import PaidMediaThumb from './PaidMediaThumb';
|
||||
|
||||
import styles from './StarsBalanceModal.module.scss';
|
||||
|
||||
@ -31,10 +36,17 @@ type StateProps = {
|
||||
payment?: TabState['payment'];
|
||||
starsBalanceState?: GlobalState['stars'];
|
||||
bot?: ApiUser;
|
||||
paidMediaMessage?: ApiMessage;
|
||||
paidMediaChat?: ApiChat;
|
||||
};
|
||||
|
||||
const StarPaymentModal = ({
|
||||
modal, bot, starsBalanceState, payment,
|
||||
modal,
|
||||
bot,
|
||||
starsBalanceState,
|
||||
payment,
|
||||
paidMediaMessage,
|
||||
paidMediaChat,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions();
|
||||
const [isLoading, markLoading, unmarkLoading] = useFlag();
|
||||
@ -58,8 +70,21 @@ const StarPaymentModal = ({
|
||||
const botName = getUserFullName(bot);
|
||||
const starsText = lang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount);
|
||||
|
||||
if (paidMediaMessage) {
|
||||
const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[];
|
||||
const areAllPhotos = extendedMedia.every((media) => !media.duration);
|
||||
const areAllVideos = extendedMedia.every((media) => !!media.duration);
|
||||
|
||||
const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', extendedMedia.length)
|
||||
: areAllVideos ? lang('Stars.Transfer.Videos', extendedMedia.length)
|
||||
: lang('Media', extendedMedia.length);
|
||||
|
||||
const channelTitle = getChatTitle(lang, paidMediaChat!);
|
||||
return lang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]);
|
||||
}
|
||||
|
||||
return lang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]);
|
||||
}, [bot, payment, lang]);
|
||||
}, [payment?.invoice, bot, lang, paidMediaMessage, paidMediaChat]);
|
||||
|
||||
const handlePayment = useLastCallback(() => {
|
||||
const price = payment?.invoice?.amount;
|
||||
@ -89,8 +114,14 @@ const StarPaymentModal = ({
|
||||
>
|
||||
<BalanceBlock balance={starsBalanceState?.balance || 0} className={styles.modalBalance} />
|
||||
<div className={styles.paymentImages} dir={lang.isRtl ? 'ltr' : 'rtl'}>
|
||||
<Avatar peer={bot} size="giant" />
|
||||
{photo && <Avatar className={styles.paymentPhoto} webPhoto={photo} size="giant" />}
|
||||
{paidMediaMessage ? (
|
||||
<PaidMediaThumb media={paidMediaMessage.content.paidMedia!.extendedMedia} />
|
||||
) : (
|
||||
<>
|
||||
<Avatar peer={bot} size="giant" />
|
||||
{photo && <Avatar className={styles.paymentPhoto} webPhoto={photo} size="giant" />}
|
||||
</>
|
||||
)}
|
||||
<img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} />
|
||||
</div>
|
||||
<h2 className={styles.headerText}>
|
||||
@ -114,10 +145,19 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const payment = selectTabState(global).payment;
|
||||
const bot = payment?.botId ? selectUser(global, payment.botId) : undefined;
|
||||
|
||||
const messageInputInvoice = payment.inputInvoice?.type === 'message' ? payment.inputInvoice : undefined;
|
||||
const message = messageInputInvoice
|
||||
? selectChatMessage(global, messageInputInvoice.chatId, messageInputInvoice.messageId) : undefined;
|
||||
const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined;
|
||||
const isPaidMedia = message?.content.paidMedia;
|
||||
|
||||
return {
|
||||
bot,
|
||||
starsBalanceState: global.stars,
|
||||
payment,
|
||||
paidMediaMessage: isPaidMedia ? message : undefined,
|
||||
paidMediaChat: isPaidMedia ? chat : undefined,
|
||||
};
|
||||
},
|
||||
)(StarPaymentModal));
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.title, .description, .date {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import PaidMediaThumb from './PaidMediaThumb';
|
||||
|
||||
import styles from './StarsTransactionItem.module.scss';
|
||||
|
||||
@ -40,8 +41,8 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
date,
|
||||
stars,
|
||||
photo,
|
||||
isRefund,
|
||||
peer: transactionPeer,
|
||||
extendedMedia,
|
||||
} = transaction;
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -49,8 +50,9 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
const peer = useSelector(selectOptionalPeer(peerId));
|
||||
|
||||
const data = useMemo(() => {
|
||||
let title = transaction.title;
|
||||
let title = transaction.title || (transaction.extendedMedia ? lang('StarMediaPurchase') : undefined);
|
||||
let description;
|
||||
let status: string | undefined;
|
||||
let avatarPeer: ApiPeer | CustomPeer | undefined;
|
||||
|
||||
if (transaction.peer.type === 'peer') {
|
||||
@ -67,10 +69,23 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
avatarPeer = undefined;
|
||||
}
|
||||
|
||||
if (transaction.isRefund) {
|
||||
status = lang('StarsRefunded');
|
||||
}
|
||||
|
||||
if (transaction.hasFailed) {
|
||||
status = lang('StarsFailed');
|
||||
}
|
||||
|
||||
if (transaction.isPending) {
|
||||
status = lang('StarsPending');
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
avatarPeer,
|
||||
status,
|
||||
};
|
||||
}, [lang, peer, transaction]);
|
||||
|
||||
@ -80,20 +95,21 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
|
||||
return (
|
||||
<div className={styles.root} onClick={handleClick}>
|
||||
<Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />
|
||||
{extendedMedia ? <PaidMediaThumb media={extendedMedia} isTransactionPreview />
|
||||
: <Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />}
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.title}>{data.title}</h3>
|
||||
<p className={styles.description}>{data.description}</p>
|
||||
<p className={styles.date}>
|
||||
{formatDateTimeToString(date * 1000, lang.code, true)}
|
||||
{isRefund && ` — (${lang('StarsRefunded')})`}
|
||||
{data.status && ` — (${data.status})`}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.stars}>
|
||||
<span className={buildClassName(styles.amount, stars < 0 ? styles.negative : styles.positive)}>
|
||||
{formatStarsTransactionAmount(stars)}
|
||||
</span>
|
||||
<StarIcon type="gold" size="big" />
|
||||
<StarIcon className={styles.star} type="gold" size="big" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user