Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

724 lines
24 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import IS_WEBP_SUPPORTED from '../../environment/webpSupport';
import assumeType from '../../helpers/assumeType';
import getPathFromBytes from '../../helpers/bytes/getPathFromBytes';
import deferredPromise from '../../helpers/cancellablePromise';
import computeLockColor from '../../helpers/computeLockColor';
import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent} from '../../helpers/dom/clickEvent';
import createVideo from '../../helpers/dom/createVideo';
import findUpClassName from '../../helpers/dom/findUpClassName';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb';
import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb';
import makeError from '../../helpers/makeError';
import {makeMediaSize} from '../../helpers/mediaSize';
import mediaSizes from '../../helpers/mediaSizes';
import noop from '../../helpers/noop';
import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle';
import sequentialDom from '../../helpers/sequentialDom';
import {PhotoSize, VideoSize} from '../../layer';
import {MyDocument} from '../../lib/appManagers/appDocsManager';
import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import appImManager from '../../lib/appManagers/appImManager';
import {AppManagers} from '../../lib/appManagers/managers';
import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import lottieLoader from '../../lib/rlottie/lottieLoader';
import rootScope from '../../lib/rootScope';
import type {ThumbCache} from '../../lib/storages/thumbs';
import webpWorkerController from '../../lib/webp/webpWorkerController';
import {Awaited, SendMessageEmojiInteractionData} from '../../types';
import {getEmojiToneIndex} from '../../vendor/emoji';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue';
import PopupStickers from '../popups/stickers';
import {hideToast, toastNew} from '../toast';
import wrapStickerAnimation from './stickerAnimation';
// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L40
export const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2;
const EMOJI_EFFECT_MULTIPLIER = 3;
const locksUrls: {[docId: string]: string} = {};
export default async function wrapSticker({doc, div, middleware, loadStickerMiddleware, lazyLoadQueue, exportLoad, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers, fullThumb, isOut, noPremium, withLock, relativeEffect, loopEffect, isCustomEmoji}: {
doc: MyDocument,
div: HTMLElement | HTMLElement[],
middleware?: () => boolean,
loadStickerMiddleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue,
exportLoad?: boolean,
group?: AnimationItemGroup,
play?: boolean,
onlyThumb?: boolean,
emoji?: string,
width?: number,
height?: number,
withThumb?: boolean,
loop?: boolean,
loadPromises?: Promise<any>[],
needFadeIn?: boolean,
needUpscale?: boolean,
skipRatio?: number,
static?: boolean,
managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
isOut?: boolean,
noPremium?: boolean,
withLock?: boolean,
relativeEffect?: boolean,
loopEffect?: boolean,
isCustomEmoji?: boolean
}) {
div = Array.isArray(div) ? div : [div];
const stickerType = doc.sticker;
if(stickerType === 1) {
asStatic = true;
}
if(!width && !height) {
const sizes = mediaSizes.active;
const boxSize = emoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
const size = makeMediaSize(doc.w, doc.h).aspectFitted(boxSize);
width = size.width;
height = size.height;
}
if(stickerType === 2) {
// LottieLoader.loadLottie();
lottieLoader.loadLottieWorkers();
}
div.forEach((div) => {
div.dataset.docId = '' + doc.id;
div.classList.add('media-sticker-wrapper');
});
/* if(stickerType === 3) {
const videoRes = wrapVideo({
doc,
boxWidth: width,
boxHeight: height,
container: div,
group,
lazyLoadQueue,
middleware,
withoutPreloader: true,
loadPromises,
noPlayButton: true,
noInfo: true
});
if(videoRes.thumb) {
if(videoRes.thumb.images.thumb) {
videoRes.thumb.images.thumb.classList.add('media-sticker', 'thumbnail');
}
if(videoRes.thumb.images.full) {
videoRes.thumb.images.full.classList.add('media-sticker');
}
}
return videoRes.loadPromise;
} */
// console.log('wrap sticker', doc, div, onlyThumb);
let cacheContext: ThumbCache;
const getCacheContext = async(type: string = cacheContext?.type) => {
return cacheContext = await managers.thumbsStorage.getCacheContext(doc, type);
};
const isAnimated = !asStatic && (stickerType === 2 || stickerType === 3);
const effectThumb = getStickerEffectThumb(doc);
if(isOut !== undefined && effectThumb && !isOut) {
div.forEach((div) => div.classList.add('reflect-x'));
}
const willHaveLock = effectThumb && withLock;
if(willHaveLock) {
const lockUrl = locksUrls[doc.id];
div.forEach((div) => {
div.classList.add('is-premium-sticker', 'tgico-premium_lock');
lockUrl && div.style.setProperty('--lock-url', `url(${lockUrl})`);
});
}
if(asStatic && stickerType !== 1) {
const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize;
await getCacheContext(thumb.type);
} else {
await getCacheContext(fullThumb?.type);
}
const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1;
const downloaded = cacheContext.downloaded && !needFadeIn;
const isThumbNeededForType = isAnimated;
const lottieCachedThumb = stickerType === 2 || stickerType === 3 ? await managers.appDocsManager.getLottieCachedThumb(doc.id, toneIndex) : undefined;
const ret = {render: undefined as typeof loadPromise, load: undefined as typeof load};
let loadThumbPromise = deferredPromise<void>();
let haveThumbCached = false;
if((
doc.thumbs?.length ||
lottieCachedThumb
) &&
!div[0].firstElementChild && (
!downloaded ||
isThumbNeededForType ||
onlyThumb
) && withThumb !== false/* && doc.thumbs[0]._ !== 'photoSizeEmpty' */
) {
let thumb = lottieCachedThumb || doc.thumbs[0];
// console.log('wrap sticker', thumb, div);
const afterRender = (div: HTMLElement, thumbImage: HTMLElement) => {
if(!div.childElementCount) {
thumbImage.classList.add('media-sticker', 'thumbnail');
sequentialDom.mutateElement(div, () => {
div.append(thumbImage);
loadThumbPromise.resolve();
});
}
};
if('url' in thumb) {
haveThumbCached = true;
div.forEach((div) => {
const thumbImage = new Image();
renderImageFromUrl(thumbImage, (thumb as any).url, () => afterRender(div, thumbImage));
});
} else if('bytes' in thumb) {
if(thumb._ === 'photoPathSize') {
if(!thumb.bytes.length) {
thumb = doc.thumbs.find((t) => (t as PhotoSize.photoStrippedSize).bytes?.length) || thumb;
}
const d = getPathFromBytes((thumb as PhotoSize.photoStrippedSize).bytes);
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.classList.add('rlottie-vector', 'media-sticker', 'thumbnail');
svg.setAttributeNS(null, 'viewBox', `0 0 ${doc.w || 512} ${doc.h || 512}`);
// const defs = document.createElementNS(ns, 'defs');
// const linearGradient = document.createElementNS(ns, 'linearGradient');
// linearGradient.setAttributeNS(null, 'id', 'g');
// linearGradient.setAttributeNS(null, 'x1', '-300%');
// linearGradient.setAttributeNS(null, 'x2', '-200%');
// linearGradient.setAttributeNS(null, 'y1', '0');
// linearGradient.setAttributeNS(null, 'y2', '0');
// const stops = [
// ['-10%', '.1'],
// ['30%', '.07'],
// ['70%', '.07'],
// ['110%', '.1']
// ].map(([offset, stopOpacity]) => {
// const stop = document.createElementNS(ns, 'stop');
// stop.setAttributeNS(null, 'offset', offset);
// stop.setAttributeNS(null, 'stop-opacity', stopOpacity);
// return stop;
// });
// const animates = [
// ['-300%', '1200%'],
// ['-200%', '1300%']
// ].map(([from, to], idx) => {
// const animate = document.createElementNS(ns, 'animate');
// animate.setAttributeNS(null, 'attributeName', 'x' + (idx + 1));
// animate.setAttributeNS(null, 'from', from);
// animate.setAttributeNS(null, 'to', to);
// animate.setAttributeNS(null, 'dur', '3s');
// animate.setAttributeNS(null, 'repeatCount', 'indefinite');
// return animate;
// });
// linearGradient.append(...stops, ...animates);
// defs.append(linearGradient);
// svg.append(defs);
const path = document.createElementNS(ns, 'path');
path.setAttributeNS(null, 'd', d);
if(rootScope.settings.animationsEnabled && !isCustomEmoji) path.setAttributeNS(null, 'fill', 'url(#g)');
svg.append(path);
div.forEach((div, idx) => div.append(idx > 0 ? svg.cloneNode(true) : svg));
haveThumbCached = true;
loadThumbPromise.resolve();
} else if(toneIndex <= 0) {
const r = () => {
(div as HTMLElement[]).forEach((div) => {
const thumbImage = new Image();
const url = getPreviewURLFromThumb(doc, thumb as PhotoSize.photoStrippedSize, true);
renderImageFromUrl(thumbImage, url, () => afterRender(div, thumbImage));
});
};
if((IS_WEBP_SUPPORTED || doc.pFlags.stickerThumbConverted || cacheContext.url)/* && false */) {
haveThumbCached = true;
r();
} else {
haveThumbCached = true;
webpWorkerController.convert('main-' + doc.id, thumb.bytes).then((bytes) => {
managers.appDocsManager.saveWebPConvertedStrippedThumb(doc.id, bytes);
(thumb as PhotoSize.photoStrippedSize).bytes = bytes;
doc.pFlags.stickerThumbConverted = true;
if((middleware && !middleware()) || (div as HTMLElement[])[0].childElementCount) {
loadThumbPromise.resolve();
return;
}
r();
}).catch(() => loadThumbPromise.resolve());
}
}
} else if(((stickerType === 2 && toneIndex <= 0) || stickerType === 3) && (withThumb || onlyThumb)) {
const load = async() => {
if((div as HTMLElement[])[0].childElementCount || (middleware && !middleware())) {
loadThumbPromise.resolve();
return;
}
const r = (div: HTMLElement, thumbImage: HTMLElement) => {
if(div.childElementCount || (middleware && !middleware())) {
loadThumbPromise.resolve();
return;
}
renderImageFromUrl(thumbImage, cacheContext.url, () => afterRender(div, thumbImage));
};
await getCacheContext();
(div as HTMLElement[]).forEach((div) => {
if(cacheContext.url) {
r(div, new Image());
} else if('bytes' in thumb) {
const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true);
res.loadPromise.then(() => r(div, res.image));
// return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r);
} else {
appDownloadManager.downloadMediaURL({
media: doc,
thumb: thumb as PhotoSize
}).then(async() => {
await getCacheContext();
return r(div, new Image());
});
}
});
};
if(lazyLoadQueue && onlyThumb) {
lazyLoadQueue.push({div: div[0], load});
loadThumbPromise.resolve();
return ret;
} else {
load();
if((thumb as any).url) {
haveThumbCached = true;
}
}
}
}
if(loadPromises && haveThumbCached) {
loadPromises.push(loadThumbPromise);
}
if(onlyThumb/* || true */) { // for sticker panel
return ret;
}
const middlewareError = makeError('MIDDLEWARE');
const load = async() => {
if(middleware && !middleware()) {
throw middlewareError;
}
if(stickerType === 2 && !asStatic) {
return appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId, thumb: fullThumb})
.then(async(blob) => {
if(middleware && !middleware()) {
throw middlewareError;
}
const animation = await lottieLoader.loadAnimationWorker({
container: (div as HTMLElement[])[0],
loop: loop && !emoji,
autoplay: play,
animationData: blob,
width,
height,
name: 'doc' + doc.id,
needUpscale,
skipRatio,
toneIndex,
sync: isCustomEmoji
}, group, loadStickerMiddleware ?? middleware);
// const deferred = deferredPromise<void>();
const setLockColor = willHaveLock ? () => {
const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas[0]);
(div as HTMLElement[]).forEach((div) => div.style.setProperty('--lock-url', `url(${lockUrl})`));
} : undefined;
const onFirstFrame = (container: HTMLElement, canvas: HTMLCanvasElement) => {
const element = container.firstElementChild;
if(needFadeIn !== false) {
needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled;
}
const cb = () => {
if(element && element !== canvas && element.tagName !== 'DIV') {
element.remove();
}
};
if(!needFadeIn) {
if(element) {
sequentialDom.mutate(cb);
}
} else {
sequentialDom.mutate(() => {
canvas && canvas.classList.add('fade-in');
if(element) {
element.classList.add('fade-out');
}
(canvas || element).addEventListener('animationend', () => {
sequentialDom.mutate(() => {
canvas && canvas.classList.remove('fade-in');
cb();
});
}, {once: true});
});
}
};
animation.addEventListener('firstFrame', () => {
const canvas = animation.canvas[0];
if(withThumb !== false) {
saveLottiePreview(doc, canvas, toneIndex);
}
if(willHaveLock) {
setLockColor();
}
if(!isCustomEmoji) {
(div as HTMLElement[]).forEach((container, idx) => {
onFirstFrame(container, animation.canvas[idx]);
});
}
}, {once: true});
if(emoji) {
const data: SendMessageEmojiInteractionData = {
a: [],
v: 1
};
let sendInteractionThrottled: () => void;
managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji);
const container = (div as HTMLElement[])[0];
attachClickEvent(container, async(e) => {
cancelEvent(e);
const animation = lottieLoader.getAnimation(container);
if(animation.paused) {
const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji);
if(doc) {
const audio = document.createElement('audio');
audio.style.display = 'none';
container.parentElement.append(audio);
try {
const url = await appDownloadManager.downloadMediaURL({media: doc});
audio.src = url;
audio.play();
await onMediaLoad(audio, undefined, true);
audio.addEventListener('ended', () => {
audio.src = '';
audio.remove();
}, {once: true});
} catch(err) {
}
}
animation.autoplay = true;
animation.restart();
}
const peerId = appImManager.chat.peerId;
if(!peerId.isUser()) {
return;
}
const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji, true);
if(!doc) {
return;
}
const {animationDiv} = wrapStickerAnimation({
doc,
middleware,
side: isOut ? 'right' : 'left',
size: 280,
target: container,
play: true,
withRandomOffset: true
});
if(isOut !== undefined && !isOut) {
animationDiv.classList.add('reflect-x');
}
if(!sendInteractionThrottled) {
sendInteractionThrottled = throttle(() => {
const length = data.a.length;
if(!length) {
return;
}
const firstTime = data.a[0].t;
data.a.forEach((a) => {
a.t = (a.t - firstTime) / 1000;
});
const bubble = findUpClassName(container, 'bubble');
managers.appMessagesManager.setTyping(appImManager.chat.peerId, {
_: 'sendMessageEmojiInteraction',
msg_id: getServerMessageId(+bubble.dataset.mid),
emoticon: emoji,
interaction: {
_: 'dataJSON',
data: JSON.stringify(data)
}
}, true);
data.a.length = 0;
}, 1000, false);
}
// using a trick here: simulated event from interlocutor's interaction won't fire ours
if(e.isTrusted) {
data.a.push({
i: 1,
t: Date.now()
});
sendInteractionThrottled();
}
});
}
return animation;
// return deferred;
// await new Promise((resolve) => setTimeout(resolve, 5e3));
});
} else if(asStatic || stickerType === 3) {
const media: HTMLElement[] = (div as HTMLElement[]).map(() => {
let media: HTMLElement;
if(asStatic) {
media = new Image();
} else {
const video = media = createVideo();
video.muted = true;
if(play) video.autoplay = true;
if(loop) video.loop = true;
}
media.classList.add('media-sticker');
return media;
});
const thumbImage = (div as HTMLElement[]).map((div, idx) => (div.firstElementChild as HTMLElement) !== media[idx] && div.firstElementChild) as HTMLElement[];
if(needFadeIn !== false) {
needFadeIn = (needFadeIn || !downloaded || (asStatic ? thumbImage[0] : (!thumbImage[0] || thumbImage[0].tagName === 'svg'))) && rootScope.settings.animationsEnabled;
}
if(needFadeIn) {
media.forEach((media) => media.classList.add('fade-in'));
}
return new Promise<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => {
const r = async() => {
if(middleware && !middleware()) {
reject(middlewareError);
return;
}
const onLoad = (div: HTMLElement, media: HTMLElement, thumbImage: HTMLElement) => {
sequentialDom.mutateElement(div, () => {
div.append(media);
thumbImage && thumbImage.classList.add('fade-out');
if(stickerType === 3 && !isSavingLottiePreview(doc, toneIndex)) {
// const perf = performance.now();
assumeType<HTMLVideoElement>(media);
const canvas = document.createElement('canvas');
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
const ctx = canvas.getContext('2d');
ctx.drawImage(media, 0, 0, canvas.width, canvas.height);
saveLottiePreview(doc, canvas, toneIndex);
// console.log('perf', performance.now() - perf);
}
if(stickerType === 3 && group) {
animationIntersector.addAnimation(media as HTMLVideoElement, group);
}
resolve(media as any);
if(needFadeIn) {
media.addEventListener('animationend', () => {
media.classList.remove('fade-in');
thumbImage?.remove();
}, {once: true});
}
});
};
await getCacheContext();
media.forEach((media, idx) => {
const cb = () => onLoad((div as HTMLElement[])[idx], media, thumbImage[idx]);
if(asStatic) {
renderImageFromUrl(media, cacheContext.url, cb);
} else {
(media as HTMLVideoElement).src = cacheContext.url;
onMediaLoad(media as HTMLVideoElement).then(cb);
}
});
};
await getCacheContext();
if(cacheContext.url) r();
else {
let promise: Promise<any>;
if(stickerType !== 1 && asStatic) {
const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize;
// promise = managers.appDocsManager.getThumbURL(doc, thumb).promise
promise = appDownloadManager.downloadMediaURL({media: doc, thumb, queueId: lazyLoadQueue?.queueId});
} else {
promise = appDownloadManager.downloadMediaURL({media: doc, queueId: lazyLoadQueue?.queueId});
}
promise.then(r, reject);
}
});
}
};
if(exportLoad && (!downloaded || isAnimated)) {
ret.load = load;
return ret;
}
const loadPromise: Promise<Awaited<ReturnType<typeof load>> | void> = lazyLoadQueue && (!downloaded || isAnimated) ?
(lazyLoadQueue.push({div: div[0], load}), Promise.resolve()) :
load();
if(downloaded && (asStatic/* || stickerType === 3 */)) {
loadThumbPromise = loadPromise as any;
if(loadPromises) {
loadPromises.push(loadThumbPromise);
}
}
if(stickerType === 2 && effectThumb && isOut !== undefined && !noPremium) {
attachStickerEffectHandler({
container: div[0],
doc,
managers,
middleware,
isOut,
width,
loadPromise,
relativeEffect,
loopEffect
});
}
ret.render = loadPromise as any;
return ret;
}
function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise, relativeEffect, loopEffect}: {
container: HTMLElement,
doc: MyDocument,
managers: AppManagers,
middleware: () => boolean,
isOut: boolean,
width: number,
loadPromise: Promise<any>,
relativeEffect?: boolean,
loopEffect?: boolean
}) {
managers.appStickersManager.preloadSticker(doc.id, true);
let playing = false;
attachClickEvent(container, async(e) => {
cancelEvent(e);
if(playing) {
const a = document.createElement('a');
a.onclick = () => {
hideToast();
new PopupStickers(doc.stickerSetInput).show();
};
toastNew({
langPackKey: 'Sticker.Premium.Click.Info',
langPackArguments: [a]
});
return;
}
playing = true;
await loadPromise;
const {animationDiv, stickerPromise} = wrapStickerAnimation({
doc,
middleware,
side: isOut ? 'right' : 'left',
size: width * STICKER_EFFECT_MULTIPLIER,
target: container,
play: true,
fullThumb: getStickerEffectThumb(doc),
relativeEffect,
loopEffect
});
if(isOut !== undefined && !isOut/* && !relativeEffect */) {
animationDiv.classList.add('reflect-x');
}
stickerPromise.then((player) => {
player.addEventListener('destroy', () => {
playing = false;
});
});
});
}