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