662 lines
18 KiB
TypeScript
662 lines
18 KiB
TypeScript
import type {
|
|
ApiAttachment,
|
|
ApiAudio,
|
|
ApiDimensions,
|
|
ApiDocument,
|
|
ApiGame,
|
|
ApiLocation,
|
|
ApiMediaExtendedPreview,
|
|
ApiMessage,
|
|
ApiMessageSearchType,
|
|
ApiPhoto,
|
|
ApiSticker,
|
|
ApiVideo,
|
|
ApiVoice,
|
|
ApiWebDocument,
|
|
MediaContainer,
|
|
} from '../../api/types';
|
|
import type { ActiveDownloads } from '../types';
|
|
import { ApiMediaFormat } from '../../api/types';
|
|
|
|
import {
|
|
IS_OPFS_SUPPORTED,
|
|
IS_OPUS_SUPPORTED,
|
|
IS_PROGRESSIVE_SUPPORTED,
|
|
IS_SAFARI,
|
|
MAX_BUFFER_SIZE,
|
|
} from '../../util/windowEnvironment';
|
|
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
|
|
import { getAttachmentType, matchLinkInMessageText } from './messages';
|
|
|
|
export type MediaWithThumbs = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiMediaExtendedPreview;
|
|
export type DownloadableMedia = ApiPhoto | ApiVideo | ApiDocument | ApiSticker | ApiAudio | ApiVoice | ApiWebDocument;
|
|
|
|
type Target =
|
|
'micro'
|
|
| 'pictogram'
|
|
| 'inline'
|
|
| 'preview'
|
|
| 'full'
|
|
| 'download';
|
|
|
|
export function getMessageContent(message: MediaContainer) {
|
|
return message.content;
|
|
}
|
|
|
|
export function hasMessageMedia(message: MediaContainer) {
|
|
return Boolean((
|
|
getMessagePhoto(message)
|
|
|| getMessageVideo(message)
|
|
|| getMessageDocument(message)
|
|
|| getMessageSticker(message)
|
|
|| getMessageContact(message)
|
|
|| getMessagePoll(message)
|
|
|| getMessageAction(message)
|
|
|| getMessageAudio(message)
|
|
|| getMessageVoice(message)
|
|
));
|
|
}
|
|
|
|
export function hasReplaceableMedia(message: MediaContainer) {
|
|
const video = getMessageVideo(message);
|
|
return Boolean((
|
|
getMessagePhoto(message)
|
|
|| (video && !video?.isRound)
|
|
|| getMessageDocument(message)
|
|
|| getMessageSticker(message)
|
|
|| getMessageAudio(message)
|
|
));
|
|
}
|
|
|
|
export function getMessagePhoto(message: MediaContainer) {
|
|
return message.content.photo;
|
|
}
|
|
|
|
export function getMessageActionPhoto(message: MediaContainer) {
|
|
return message.content.action?.type === 'suggestProfilePhoto' ? message.content.action.photo : undefined;
|
|
}
|
|
|
|
export function getMessageVideo(message: MediaContainer) {
|
|
return message.content.video;
|
|
}
|
|
|
|
export function getMessageRoundVideo(message: MediaContainer) {
|
|
const { video } = message.content;
|
|
|
|
return video?.isRound ? video : undefined;
|
|
}
|
|
|
|
export function getMessageAction(message: MediaContainer) {
|
|
return message.content.action;
|
|
}
|
|
|
|
export function getMessageAudio(message: MediaContainer) {
|
|
return message.content.audio;
|
|
}
|
|
|
|
export function getMessageVoice(message: MediaContainer) {
|
|
return message.content.voice;
|
|
}
|
|
|
|
export function getMessageSticker(message: MediaContainer) {
|
|
return message.content.sticker;
|
|
}
|
|
|
|
export function getMessageDocument(message: MediaContainer) {
|
|
return message.content.document;
|
|
}
|
|
|
|
export function getMessageWebPageDocument(message: MediaContainer) {
|
|
return getMessageWebPage(message)?.document;
|
|
}
|
|
|
|
export function isDocumentPhoto(document: ApiDocument) {
|
|
return document.innerMediaType === 'photo';
|
|
}
|
|
|
|
export function isDocumentVideo(document: ApiDocument) {
|
|
return document.innerMediaType === 'video';
|
|
}
|
|
|
|
export function isMessageDocumentSticker(message: MediaContainer) {
|
|
const document = getMessageDocument(message);
|
|
return document ? document.mimeType === 'image/webp' : undefined;
|
|
}
|
|
|
|
export function getMessageContact(message: MediaContainer) {
|
|
return message.content.contact;
|
|
}
|
|
|
|
export function getMessagePoll(message: MediaContainer) {
|
|
return message.content.poll;
|
|
}
|
|
|
|
export function getMessageInvoice(message: MediaContainer) {
|
|
return message.content.invoice;
|
|
}
|
|
|
|
export function getMessageLocation(message: MediaContainer) {
|
|
return message.content.location;
|
|
}
|
|
|
|
export function getMessageWebPage(message: MediaContainer) {
|
|
return message.content.webPage;
|
|
}
|
|
|
|
export function getMessagePaidMedia(message: MediaContainer) {
|
|
return message.content.paidMedia;
|
|
}
|
|
|
|
export function getMessageWebPagePhoto(message: MediaContainer) {
|
|
return getMessageWebPage(message)?.photo;
|
|
}
|
|
|
|
export function getMessageDocumentPhoto(message: MediaContainer) {
|
|
const document = getMessageDocument(message);
|
|
return document && isDocumentPhoto(document) ? document : undefined;
|
|
}
|
|
|
|
export function getMessageWebPageVideo(message: MediaContainer) {
|
|
return getMessageWebPage(message)?.video;
|
|
}
|
|
|
|
export function getMessageWebPageAudio(message: MediaContainer) {
|
|
return getMessageWebPage(message)?.audio;
|
|
}
|
|
|
|
export function getMessageDocumentVideo(message: MediaContainer) {
|
|
const document = getMessageDocument(message);
|
|
return document && isDocumentVideo(document) ? document : undefined;
|
|
}
|
|
|
|
export function getMessageDownloadableMedia(message: MediaContainer): DownloadableMedia | undefined {
|
|
return (
|
|
getMessagePhoto(message)
|
|
|| getMessageVideo(message)
|
|
|| getMessageDocument(message)
|
|
|| getMessageSticker(message)
|
|
|| getMessageAudio(message)
|
|
|| getMessageVoice(message)
|
|
|| getMessageWebPagePhoto(message)
|
|
|| getMessageWebPageVideo(message)
|
|
|| getMessageWebPageAudio(message)
|
|
);
|
|
}
|
|
|
|
function getMessageMediaThumbnail(message: MediaContainer) {
|
|
const media = getMessagePhoto(message)
|
|
|| getMessageVideo(message)
|
|
|| getMessageDocument(message)
|
|
|| getMessageSticker(message)
|
|
|| getMessageWebPagePhoto(message)
|
|
|| getMessageWebPageVideo(message)
|
|
|| getMessageInvoice(message)?.extendedMedia;
|
|
|
|
if (!media) {
|
|
return undefined;
|
|
}
|
|
|
|
return media.thumbnail;
|
|
}
|
|
|
|
export function getMessageMediaThumbDataUri(message: MediaContainer) {
|
|
return getMessageMediaThumbnail(message)?.dataUri;
|
|
}
|
|
|
|
export function getMediaThumbUri(media: MediaWithThumbs) {
|
|
return media.thumbnail?.dataUri;
|
|
}
|
|
|
|
export function getMessageIsSpoiler(message: MediaContainer) {
|
|
const media = getMessagePhoto(message)
|
|
|| getMessageVideo(message);
|
|
|
|
const invoiceMedia = getMessageInvoice(message)?.extendedMedia;
|
|
return Boolean(invoiceMedia || media?.isSpoiler);
|
|
}
|
|
|
|
export function buildStaticMapHash(
|
|
geo: ApiLocation['geo'],
|
|
width: number,
|
|
height: number,
|
|
zoom: number,
|
|
scale: number,
|
|
) {
|
|
const {
|
|
long, lat, accessHash, accuracyRadius,
|
|
} = geo;
|
|
|
|
// eslint-disable-next-line max-len
|
|
return `staticMap:${accessHash}?lat=${lat}&long=${long}&w=${width}&h=${height}&zoom=${zoom}&scale=${scale}&accuracyRadius=${accuracyRadius}`;
|
|
}
|
|
|
|
export function getMessageMediaHash(
|
|
message: MediaContainer,
|
|
target: Target,
|
|
) {
|
|
const {
|
|
video, sticker, audio, voice, document,
|
|
} = message.content;
|
|
|
|
const messagePhoto = getMessagePhoto(message) || getMessageWebPagePhoto(message);
|
|
const actionPhoto = getMessageActionPhoto(message);
|
|
const messageVideo = video || getMessageWebPageVideo(message);
|
|
const messageDocument = document || getMessageWebPageDocument(message);
|
|
const messageAudio = audio || getMessageWebPageAudio(message);
|
|
|
|
if (messageVideo) {
|
|
return getVideoMediaHash(messageVideo, target);
|
|
}
|
|
|
|
if (messagePhoto || actionPhoto) {
|
|
return getPhotoMediaHash(messagePhoto || actionPhoto!, target, Boolean(actionPhoto));
|
|
}
|
|
|
|
if (messageDocument) {
|
|
return getDocumentMediaHash(messageDocument, target);
|
|
}
|
|
|
|
if (sticker) {
|
|
return getStickerMediaHash(sticker, target);
|
|
}
|
|
|
|
if (messageAudio) {
|
|
return getAudioMediaHash(messageAudio, target);
|
|
}
|
|
|
|
if (voice) {
|
|
return getVoiceMediaHash(voice, target);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getPhotoMediaHash(photo: ApiPhoto | ApiDocument, target: Target, isAction?: boolean) {
|
|
const base = `photo${photo.id}`;
|
|
const isVideo = photo.mediaType === 'photo' && photo.isVideo;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
return `${base}?size=${isAction ? 'a' : 'm'}`;
|
|
case 'inline':
|
|
return !hasMediaLocalBlobUrl(photo) ? `${base}?size=${isAction ? 'b' : 'x'}` : undefined;
|
|
case 'preview':
|
|
return `${base}?size=${isAction ? 'b' : 'x'}`;
|
|
case 'download':
|
|
return !isVideo ? base : getVideoProfilePhotoMediaHash(photo);
|
|
case 'full':
|
|
default:
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export function getProfilePhotoMediaHash(photo: ApiPhoto) {
|
|
return `photo${photo.id}?size=c`;
|
|
}
|
|
|
|
export function getVideoProfilePhotoMediaHash(photo: ApiPhoto) {
|
|
if (!photo.isVideo) return undefined;
|
|
return `photo${photo.id}?size=u`;
|
|
}
|
|
|
|
export function getVideoMediaHash(video: ApiVideo | ApiDocument, target: Target) {
|
|
const base = `document${video.id}`;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
return `${base}?size=m`;
|
|
case 'inline':
|
|
return !hasMediaLocalBlobUrl(video) ? appendProgressiveQueryParameters(video, base) : undefined;
|
|
case 'preview':
|
|
return `${base}?size=x`;
|
|
case 'download':
|
|
return `${base}?download`;
|
|
case 'full':
|
|
default:
|
|
return appendProgressiveQueryParameters(video, base);
|
|
}
|
|
}
|
|
|
|
export function getDocumentMediaHash(document: ApiDocument, target: Target) {
|
|
const base = `document${document.id}`;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
case 'inline':
|
|
case 'preview':
|
|
if (!getDocumentHasPreview(document) || hasMediaLocalBlobUrl(document)) {
|
|
return undefined;
|
|
}
|
|
|
|
return `${base}?size=m`;
|
|
case 'full':
|
|
case 'download':
|
|
default:
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export function getAudioMediaHash(audio: ApiAudio, target: Target) {
|
|
const base = `document${audio.id}`;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
return getAudioHasCover(audio) ? `${base}?size=m` : undefined;
|
|
case 'inline':
|
|
return appendProgressiveQueryParameters(audio, base);
|
|
case 'download':
|
|
return `${base}?download`;
|
|
default:
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export function getVoiceMediaHash(voice: ApiVoice, target: Target) {
|
|
const base = `document${voice.id}`;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
return undefined;
|
|
case 'download':
|
|
return `${base}?download`;
|
|
case 'inline':
|
|
default:
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export function getWebDocumentHash(webDocument?: ApiWebDocument) {
|
|
if (!webDocument) return undefined;
|
|
return `webDocument:${webDocument.url}`;
|
|
}
|
|
|
|
export function getStickerMediaHash(sticker: ApiSticker, target: Target) {
|
|
const base = `document${sticker.id}`;
|
|
|
|
switch (target) {
|
|
case 'micro':
|
|
case 'pictogram':
|
|
return `${base}?size=s`;
|
|
case 'preview':
|
|
return `${base}?size=m`;
|
|
case 'download':
|
|
return `${base}?download`;
|
|
case 'inline':
|
|
default:
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export function getMediaHash(media: DownloadableMedia, target: Target) {
|
|
switch (media.mediaType) {
|
|
case 'photo':
|
|
return getPhotoMediaHash(media, target);
|
|
case 'video':
|
|
return getVideoMediaHash(media, target);
|
|
case 'document':
|
|
return getDocumentMediaHash(media, target);
|
|
case 'audio':
|
|
return getAudioMediaHash(media, target);
|
|
case 'voice':
|
|
return getVoiceMediaHash(media, target);
|
|
case 'sticker':
|
|
return getStickerMediaHash(media, target);
|
|
case 'webDocument':
|
|
return getWebDocumentHash(media);
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function getGamePreviewPhotoHash(game: ApiGame) {
|
|
const { photo } = game;
|
|
|
|
if (photo) {
|
|
return `photo${photo.id}?size=x`;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getGamePreviewVideoHash(game: ApiGame) {
|
|
const { document } = game;
|
|
|
|
if (document) {
|
|
return `document${document.id}`;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function appendProgressiveQueryParameters(media: ApiAudio | ApiVideo | ApiDocument, base: string) {
|
|
if (IS_PROGRESSIVE_SUPPORTED && IS_SAFARI) {
|
|
return `${base}?fileSize=${media.size}&mimeType=${media.mimeType}`;
|
|
}
|
|
|
|
return base;
|
|
}
|
|
|
|
export function getAudioHasCover(media: ApiAudio) {
|
|
return media.thumbnailSizes && media.thumbnailSizes.length > 0;
|
|
}
|
|
|
|
export function getMediaFormat(
|
|
media: DownloadableMedia, target: Target,
|
|
): ApiMediaFormat {
|
|
const isDocument = media.mediaType === 'document';
|
|
const hasInnerVideo = isDocument && media.innerMediaType === 'video';
|
|
const isVideo = media.mediaType === 'video' || hasInnerVideo;
|
|
const isAudio = media.mediaType === 'audio';
|
|
const isVoice = media.mediaType === 'voice';
|
|
|
|
const size = getMediaFileSize(media) || 0; // Media types that do not have `size` are smaller than `MAX_BUFFER_SIZE`
|
|
|
|
if (target === 'download') {
|
|
if (IS_PROGRESSIVE_SUPPORTED && size > MAX_BUFFER_SIZE && !IS_OPFS_SUPPORTED) {
|
|
return ApiMediaFormat.DownloadUrl;
|
|
}
|
|
return ApiMediaFormat.BlobUrl;
|
|
}
|
|
|
|
if (isVideo && IS_PROGRESSIVE_SUPPORTED && (
|
|
target === 'full' || target === 'inline'
|
|
)) {
|
|
return ApiMediaFormat.Progressive;
|
|
}
|
|
|
|
if (isAudio || isVoice) {
|
|
// Safari
|
|
if (isVoice && !IS_OPUS_SUPPORTED) {
|
|
return ApiMediaFormat.BlobUrl;
|
|
}
|
|
|
|
return ApiMediaFormat.Progressive;
|
|
}
|
|
|
|
return ApiMediaFormat.BlobUrl;
|
|
}
|
|
|
|
export function getMediaFileSize(media: DownloadableMedia) {
|
|
return 'size' in media ? media.size : undefined;
|
|
}
|
|
|
|
export function hasMediaLocalBlobUrl(media: ApiPhoto | ApiVideo | ApiDocument) {
|
|
if ('blobUrl' in media) {
|
|
return Boolean(media.blobUrl);
|
|
}
|
|
|
|
if ('previewBlobUrl' in media) {
|
|
return Boolean(media.previewBlobUrl);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function getChatMediaMessageIds(
|
|
messages: Record<number, ApiMessage>, listedIds: number[], isFromSharedMedia = false,
|
|
) {
|
|
return getMessageContentIds(messages, listedIds, isFromSharedMedia ? 'media' : 'inlineMedia');
|
|
}
|
|
|
|
export function getPhotoFullDimensions(photo: Pick<ApiPhoto, 'sizes' | 'thumbnail'>): ApiDimensions | undefined {
|
|
return (
|
|
photo.sizes.find((size) => size.type === 'w')
|
|
|| photo.sizes.find((size) => size.type === 'y')
|
|
|| getPhotoInlineDimensions(photo)
|
|
);
|
|
}
|
|
|
|
export function getPhotoInlineDimensions(photo: Pick<ApiPhoto, 'sizes' | 'thumbnail'>): ApiDimensions | undefined {
|
|
return (
|
|
photo.sizes.find((size) => size.type === 'x')
|
|
|| photo.sizes.find((size) => size.type === 'm')
|
|
|| photo.sizes.find((size) => size.type === 's')
|
|
|| photo.thumbnail
|
|
);
|
|
}
|
|
|
|
export function getVideoDimensions(video: ApiVideo): ApiDimensions | undefined {
|
|
if (video.width && video.height) {
|
|
return video as ApiDimensions;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getMediaTransferState(
|
|
progress?: number, isLoadNeeded = false, isUploading = false,
|
|
) {
|
|
const isTransferring = isUploading || isLoadNeeded;
|
|
const transferProgress = Number(progress);
|
|
|
|
return {
|
|
isUploading, isTransferring, transferProgress,
|
|
};
|
|
}
|
|
|
|
export function getMessageContentIds(
|
|
messages: Record<number, ApiMessage>, messageIds: number[], contentType: ApiMessageSearchType | 'inlineMedia',
|
|
) {
|
|
let validator: Function;
|
|
|
|
switch (contentType) {
|
|
case 'media':
|
|
validator = (message: ApiMessage) => {
|
|
const video = getMessageVideo(message);
|
|
return getMessagePhoto(message) || (video && !video.isRound && !video.isGif);
|
|
};
|
|
break;
|
|
|
|
case 'documents':
|
|
validator = getMessageDocument;
|
|
break;
|
|
|
|
case 'links':
|
|
validator = (message: ApiMessage) => getMessageWebPage(message) || matchLinkInMessageText(message);
|
|
break;
|
|
|
|
case 'audio':
|
|
validator = getMessageAudio;
|
|
break;
|
|
|
|
case 'voice':
|
|
validator = (message: ApiMessage) => {
|
|
const video = getMessageVideo(message);
|
|
return getMessageVoice(message) || (video && video.isRound);
|
|
};
|
|
break;
|
|
|
|
case 'inlineMedia':
|
|
validator = (message: ApiMessage) => {
|
|
const video = getMessageVideo(message);
|
|
const document = getMessageDocument(message);
|
|
return (
|
|
getMessagePhoto(message)
|
|
|| (video && !video.isRound && !video.isGif)
|
|
|| (document && isDocumentPhoto(document))
|
|
|| (document && isDocumentVideo(document))
|
|
);
|
|
};
|
|
break;
|
|
|
|
default:
|
|
return [] as Array<number>;
|
|
}
|
|
|
|
return messageIds.reduce((result, messageId) => {
|
|
if (messages[messageId] && validator(messages[messageId])) {
|
|
result.push(messageId);
|
|
}
|
|
|
|
return result;
|
|
}, [] as Array<number>);
|
|
}
|
|
|
|
export function getMediaDuration(message: ApiMessage) {
|
|
const { audio, voice, video } = getMessageContent(message);
|
|
const media = audio || voice || video || getMessageWebPageVideo(message) || getMessageWebPageAudio(message);
|
|
if (!media) {
|
|
return undefined;
|
|
}
|
|
|
|
return media.duration;
|
|
}
|
|
|
|
export function canReplaceMessageMedia(message: ApiMessage, attachment: ApiAttachment) {
|
|
const isPhotoOrVideo = Boolean(getMessagePhoto(message)
|
|
|| getMessageWebPagePhoto(message) || Boolean(getMessageVideo(message)
|
|
|| getMessageWebPageVideo(message)));
|
|
const isFile = Boolean(getMessageAudio(message)
|
|
|| getMessageVoice(message) || getMessageDocument(message));
|
|
|
|
const fileType = getAttachmentType(attachment);
|
|
|
|
return (
|
|
(isPhotoOrVideo && (fileType === 'image' || fileType === 'video'))
|
|
|| (isFile && (fileType === 'audio' || fileType === 'file'))
|
|
);
|
|
}
|
|
|
|
export function isMediaLoadableInViewer(newMessage: ApiMessage) {
|
|
if (!newMessage.content) return false;
|
|
if (newMessage.content.photo) return true;
|
|
if (newMessage.content.video && !newMessage.content.video.isRound && !newMessage.content.video.isGif) return true;
|
|
return false;
|
|
}
|
|
|
|
export function getMediaFilename(media: DownloadableMedia) {
|
|
if ('fileName' in media && media.fileName) {
|
|
return media.fileName;
|
|
}
|
|
|
|
if (media.mediaType === 'sticker') {
|
|
const extension = media.isLottie ? 'tgs' : media.isVideo ? 'webm' : 'webp';
|
|
return `${media.id}.${extension}`;
|
|
}
|
|
|
|
if (media.mediaType === 'photo') {
|
|
return `${media.id}.${media.isVideo ? 'mp4' : 'jpg'}`;
|
|
}
|
|
|
|
if (media.mediaType === 'voice') {
|
|
return `${media.id}.${IS_OPUS_SUPPORTED ? 'ogg' : 'wav'}`;
|
|
}
|
|
|
|
if ('id' in media && media.id) {
|
|
return media.id;
|
|
}
|
|
|
|
return `${media.mediaType}-${Math.random().toString(36).slice(4)}`;
|
|
}
|
|
|
|
export function getIsDownloading(activeDownloads: ActiveDownloads, media: DownloadableMedia) {
|
|
const hash = getMediaHash(media, 'download');
|
|
if (!hash) return false;
|
|
return Boolean(activeDownloads[hash]);
|
|
}
|