Browse Source

Custom emoji

master
Eduard Kuzmenko 2 years ago committed by r4sas
parent
commit
62434a06f1
  1. 109
      src/components/animationIntersector.ts
  2. 2
      src/components/appMediaViewer.ts
  3. 87
      src/components/chat/bubbles.ts
  4. 10
      src/components/monkeys/tracking.ts
  5. 338
      src/components/wrappers/sticker.ts
  6. 3
      src/environment/customEmojiSupport.ts
  7. 3
      src/environment/imageBitmapSupport.ts
  8. 3
      src/environment/webAssemblySupport.ts
  9. 9
      src/helpers/mediaSizes.ts
  10. 2
      src/helpers/preloadAnimatedEmojiSticker.ts
  11. 2
      src/helpers/sequentialDom.ts
  12. 2
      src/lib/appManagers/appDialogsManager.ts
  13. 3
      src/lib/appManagers/appDocsManager.ts
  14. 63
      src/lib/appManagers/appEmojiManager.ts
  15. 12
      src/lib/appManagers/appImManager.ts
  16. 6
      src/lib/appManagers/appMessagesManager.ts
  17. 11
      src/lib/mtproto/referenceDatabase.ts
  18. 1
      src/lib/richTextProcessor/parseEntities.ts
  19. 424
      src/lib/richTextProcessor/wrapRichText.ts
  20. 99
      src/lib/rlottie/lottieLoader.ts
  21. 33
      src/lib/rlottie/queryableWorker.ts
  22. 63
      src/lib/rlottie/rlottie.worker.ts
  23. 244
      src/lib/rlottie/rlottiePlayer.ts
  24. 3
      src/pages/pageIm.ts
  25. 4
      src/scss/partials/_chat.scss
  26. 48
      src/scss/partials/_chatBubble.scss
  27. 1
      src/scss/partials/_chatPinned.scss
  28. 1
      src/scss/partials/_chatlist.scss
  29. 54
      src/scss/partials/_customEmoji.scss
  30. 38
      src/scss/partials/_spoiler.scss
  31. 8
      src/scss/style.scss

109
src/components/animationIntersector.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText';
import rootScope from '../lib/rootScope'; import rootScope from '../lib/rootScope';
import {IS_SAFARI} from '../environment/userAgent'; import {IS_SAFARI} from '../environment/userAgent';
import {MOUNT_CLASS_TO} from '../config/debug'; import {MOUNT_CLASS_TO} from '../config/debug';
@ -13,14 +14,15 @@ import indexOfAndSplice from '../helpers/array/indexOfAndSplice';
import forEachReverse from '../helpers/array/forEachReverse'; import forEachReverse from '../helpers/array/forEachReverse';
import idleController from '../helpers/idleController'; import idleController from '../helpers/idleController';
import appMediaPlaybackController from './appMediaPlaybackController'; import appMediaPlaybackController from './appMediaPlaybackController';
import {fastRaf} from '../helpers/schedulers';
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' | export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' | 'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' |
`CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS' | 'STICKER-VIEWER'; `CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS' | 'STICKER-VIEWER' | 'EMOJI';
export interface AnimationItem { export interface AnimationItem {
el: HTMLElement, el: HTMLElement,
group: AnimationItemGroup, group: AnimationItemGroup,
animation: RLottiePlayer | HTMLVideoElement animation: RLottiePlayer | HTMLVideoElement | CustomEmojiRendererElement
}; };
export class AnimationIntersector { export class AnimationIntersector {
@ -47,33 +49,35 @@ export class AnimationIntersector {
continue; continue;
} }
const player = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target); const animation = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target);
if(player) { if(!animation) {
if(entry.isIntersecting) { continue;
this.visible.add(player); }
this.checkAnimation(player, false);
if(entry.isIntersecting) {
/* if(animation instanceof HTMLVideoElement && animation.dataset.src) { this.visible.add(animation);
animation.src = animation.dataset.src; this.checkAnimation(animation, false);
animation.load();
} */ /* if(animation instanceof HTMLVideoElement && animation.dataset.src) {
} else { animation.src = animation.dataset.src;
this.visible.delete(player); animation.load();
this.checkAnimation(player, true); } */
} else {
const animation = player.animation; this.visible.delete(animation);
if(animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) { this.checkAnimation(animation, true);
// console.warn('will clear cache', player);
animation.clearCache(); const _animation = animation.animation;
}/* else if(animation instanceof HTMLVideoElement && animation.src) { if(_animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) {
animation.dataset.src = animation.src; // console.warn('will clear cache', player);
animation.src = ''; _animation.clearCache();
animation.load(); }/* else if(animation instanceof HTMLVideoElement && animation.src) {
} */ animation.dataset.src = animation.src;
} animation.src = '';
animation.load();
break; } */
} }
break;
} }
} }
}); });
@ -118,7 +122,6 @@ export class AnimationIntersector {
} }
public removeAnimation(player: AnimationItem) { public removeAnimation(player: AnimationItem) {
// console.log('destroy animation');
const {el, animation} = player; const {el, animation} = player;
animation.remove(); animation.remove();
@ -141,21 +144,21 @@ export class AnimationIntersector {
this.visible.delete(player); this.visible.delete(player);
} }
public addAnimation(animation: RLottiePlayer | HTMLVideoElement, group: AnimationItemGroup = '') { public addAnimation(_animation: AnimationItem['animation'], group: AnimationItemGroup = '') {
const player: AnimationItem = { const animation: AnimationItem = {
el: animation instanceof RLottiePlayer ? animation.el : animation, el: _animation instanceof RLottiePlayer ? _animation.el[0] : (_animation instanceof HTMLVideoElement ? _animation : _animation.canvas),
animation: animation, animation: _animation,
group group
}; };
if(animation instanceof RLottiePlayer) { if(_animation instanceof RLottiePlayer) {
if(!rootScope.settings.stickers.loop && animation.loop) { if(!rootScope.settings.stickers.loop && _animation.loop) {
animation.loop = rootScope.settings.stickers.loop; _animation.loop = rootScope.settings.stickers.loop;
} }
} }
(this.byGroups[group as AnimationItemGroup] ??= []).push(player); (this.byGroups[group as AnimationItemGroup] ??= []).push(animation);
this.observer.observe(player.el); this.observer.observe(animation.el);
} }
public checkAnimations(blurred?: boolean, group?: AnimationItemGroup, destroy = false) { public checkAnimations(blurred?: boolean, group?: AnimationItemGroup, destroy = false) {
@ -171,8 +174,8 @@ export class AnimationIntersector {
for(const group of groups) { for(const group of groups) {
const animations = this.byGroups[group]; const animations = this.byGroups[group];
forEachReverse(animations, (player) => { forEachReverse(animations, (animation) => {
this.checkAnimation(player, blurred, destroy); this.checkAnimation(animation, blurred, destroy);
}); });
} }
} }
@ -180,7 +183,7 @@ export class AnimationIntersector {
public checkAnimation(player: AnimationItem, blurred = false, destroy = false) { public checkAnimation(player: AnimationItem, blurred = false, destroy = false) {
const {el, animation, group} = player; const {el, animation, group} = player;
// return; // return;
if((destroy || (!isInDOM(el) && !this.lockedGroups[group]))/* && false */) { if(destroy || (!this.lockedGroups[group] && !isInDOM(el))) {
this.removeAnimation(player); this.removeAnimation(player);
return; return;
} }
@ -220,17 +223,19 @@ export class AnimationIntersector {
public refreshGroup(group: AnimationItemGroup) { public refreshGroup(group: AnimationItemGroup) {
const animations = this.byGroups[group]; const animations = this.byGroups[group];
if(animations && animations.length) { if(!animations?.length) {
animations.forEach((animation) => { return;
this.observer.unobserve(animation.el); }
});
animations.forEach((animation) => {
this.observer.unobserve(animation.el);
});
window.requestAnimationFrame(() => { fastRaf(() => {
animations.forEach((animation) => { animations.forEach((animation) => {
this.observer.observe(animation.el); this.observer.observe(animation.el);
});
}); });
} });
} }
public lockIntersectionGroup(group: AnimationItemGroup) { public lockIntersectionGroup(group: AnimationItemGroup) {
@ -244,7 +249,5 @@ export class AnimationIntersector {
} }
const animationIntersector = new AnimationIntersector(); const animationIntersector = new AnimationIntersector();
if(MOUNT_CLASS_TO) { MOUNT_CLASS_TO && (MOUNT_CLASS_TO.animationIntersector = animationIntersector);
MOUNT_CLASS_TO.animationIntersector = animationIntersector;
}
export default animationIntersector; export default animationIntersector;

2
src/components/appMediaViewer.ts

@ -70,7 +70,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
this.content.main.prepend(stub); */ this.content.main.prepend(stub); */
this.content.caption = document.createElement('div'); this.content.caption = document.createElement('div');
this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption', 'message'/* , 'media-viewer-stub' */); this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption', 'spoilers-container'/* , 'media-viewer-stub' */);
let captionTimeout: number; let captionTimeout: number;
const setCaptionTimeout = () => { const setCaptionTimeout = () => {

87
src/components/chat/bubbles.ts

@ -113,6 +113,7 @@ import PopupPayment from '../popups/payment';
import isInDOM from '../../helpers/dom/isInDOM'; import isInDOM from '../../helpers/dom/isInDOM';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import attachStickerViewerListeners from '../stickerViewer'; import attachStickerViewerListeners from '../stickerViewer';
import {makeMediaSize, MediaSize} from '../../helpers/mediaSize';
const USE_MEDIA_TAILS = false; const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([ const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -3434,7 +3435,7 @@ export default class ChatBubbles {
const our = this.chat.isOurMessage(message); const our = this.chat.isOurMessage(message);
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.classList.add('message'); messageDiv.classList.add('message', 'spoilers-container');
const contentWrapper = document.createElement('div'); const contentWrapper = document.createElement('div');
contentWrapper.classList.add('bubble-content-wrapper'); contentWrapper.classList.add('bubble-content-wrapper');
@ -3526,54 +3527,68 @@ export default class ChatBubbles {
} }
} }
/* let richText = wrapRichText(messageMessage, { let bigEmojis = 0, customEmojiSize: MediaSize;
entities: totalEntities if(totalEntities && !messageMedia) {
}); */ const emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji'/* || e._ === 'messageEntityCustomEmoji' */);
const strLength = messageMessage.replace(/\s/g, '').length;
const emojiStrLength = emojiEntities.reduce((acc, curr) => acc + curr.length, 0);
if(emojiStrLength === strLength /* && emojiEntities.length <= 3 *//* && totalEntities.length === emojiEntities.length */) {
bigEmojis = Math.min(4, emojiEntities.length);
customEmojiSize = mediaSizes.active.customEmoji;
const sizes: {[size: number]: number} = {
1: 96,
2: 64,
3: 52,
4: 36
};
const size = sizes[bigEmojis];
if(size) {
customEmojiSize = makeMediaSize(size, size);
bubble.style.setProperty('--emoji-size', size + 'px');
}
}
}
const richText = wrapRichText(messageMessage, { const richText = wrapRichText(messageMessage, {
entities: totalEntities, entities: totalEntities,
passEntities: this.passEntities passEntities: this.passEntities,
loadPromises,
lazyLoadQueue: this.lazyLoadQueue,
customEmojiSize
}); });
let canHaveTail = true; let canHaveTail = true;
let isStandaloneMedia = false; let isStandaloneMedia = false;
let needToSetHTML = true; let needToSetHTML = true;
if(totalEntities && !messageMedia) { if(bigEmojis) {
const emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji'); if(rootScope.settings.emoji.big) {
const strLength = messageMessage.length; const sticker = bigEmojis === 1 &&
const emojiStrLength = emojiEntities.reduce((acc, curr) => acc + curr.length, 0); !totalEntities.find((entity) => entity._ === 'messageEntityCustomEmoji') &&
await this.managers.appStickersManager.getAnimatedEmojiSticker(messageMessage);
if(emojiStrLength === strLength && emojiEntities.length <= 3 && totalEntities.length === emojiEntities.length) { if(bigEmojis === 1 && !messageMedia && sticker) {
if(rootScope.settings.emoji.big) { messageMedia = {
const sticker = await this.managers.appStickersManager.getAnimatedEmojiSticker(messageMessage); _: 'messageMediaDocument',
if(emojiEntities.length === 1 && !messageMedia && sticker) { document: sticker
messageMedia = { };
_: 'messageMediaDocument', } else {
document: sticker const attachmentDiv = document.createElement('div');
}; attachmentDiv.classList.add('attachment', 'spoilers-container');
} else {
const attachmentDiv = document.createElement('div');
attachmentDiv.classList.add('attachment');
setInnerHTML(attachmentDiv, richText);
bubble.classList.add('emoji-' + emojiEntities.length + 'x'); setInnerHTML(attachmentDiv, richText);
bubbleContainer.append(attachmentDiv); bubbleContainer.append(attachmentDiv);
}
bubble.classList.add('is-message-empty', 'emoji-big');
isStandaloneMedia = true;
canHaveTail = false;
needToSetHTML = false;
} }
bubble.classList.add('can-have-big-emoji'); bubble.classList.add('is-message-empty', 'emoji-big');
isStandaloneMedia = true;
canHaveTail = false;
needToSetHTML = false;
} }
/* if(strLength === emojiStrLength) { bubble.classList.add('can-have-big-emoji');
messageDiv.classList.add('emoji-only');
messageDiv.classList.add('message-empty');
} */
} }
if(needToSetHTML) { if(needToSetHTML) {

10
src/components/monkeys/tracking.ts

@ -50,10 +50,10 @@ export default class TrackingMonkey {
if(this.idleAnimation) { if(this.idleAnimation) {
this.idleAnimation.stop(true); this.idleAnimation.stop(true);
this.idleAnimation.canvas.style.display = 'none'; this.idleAnimation.canvas[0].style.display = 'none';
} }
this.animation.canvas.style.display = ''; this.animation.canvas[0].style.display = '';
} else { } else {
/* const cb = (frameNo: number) => { /* const cb = (frameNo: number) => {
if(frameNo <= 1) { */ if(frameNo <= 1) { */
@ -116,7 +116,7 @@ export default class TrackingMonkey {
this.animation = _animation; this.animation = _animation;
if(!this.inputField.value.length) { if(!this.inputField.value.length) {
this.animation.canvas.style.display = 'none'; this.animation.canvas[0].style.display = 'none';
} }
this.animation.addEventListener('enterFrame', currentFrame => { this.animation.addEventListener('enterFrame', currentFrame => {
@ -133,9 +133,9 @@ export default class TrackingMonkey {
// animation.curFrame = 0; // animation.curFrame = 0;
if(this.idleAnimation) { if(this.idleAnimation) {
this.idleAnimation.canvas.style.display = ''; this.idleAnimation.canvas[0].style.display = '';
this.idleAnimation.play(); this.idleAnimation.play();
this.animation.canvas.style.display = 'none'; this.animation.canvas[0].style.display = 'none';
} }
} }
}); });

338
src/components/wrappers/sticker.ts

@ -19,6 +19,7 @@ import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import {makeMediaSize} from '../../helpers/mediaSize'; import {makeMediaSize} from '../../helpers/mediaSize';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import noop from '../../helpers/noop';
import onMediaLoad from '../../helpers/onMediaLoad'; import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle'; import throttle from '../../helpers/schedulers/throttle';
@ -32,7 +33,6 @@ import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerM
import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize'; import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import lottieLoader from '../../lib/rlottie/lottieLoader'; import lottieLoader from '../../lib/rlottie/lottieLoader';
import RLottiePlayer from '../../lib/rlottie/rlottiePlayer';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
import type {ThumbCache} from '../../lib/storages/thumbs'; import type {ThumbCache} from '../../lib/storages/thumbs';
import webpWorkerController from '../../lib/webp/webpWorkerController'; import webpWorkerController from '../../lib/webp/webpWorkerController';
@ -50,11 +50,13 @@ const EMOJI_EFFECT_MULTIPLIER = 3;
const locksUrls: {[docId: string]: string} = {}; const locksUrls: {[docId: string]: string} = {};
export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers, fullThumb, isOut, noPremium, withLock, relativeEffect, loopEffect}: { 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, doc: MyDocument,
div: HTMLElement, div: HTMLElement | HTMLElement[],
middleware?: () => boolean, middleware?: () => boolean,
loadStickerMiddleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
exportLoad?: boolean,
group?: AnimationItemGroup, group?: AnimationItemGroup,
play?: boolean, play?: boolean,
onlyThumb?: boolean, onlyThumb?: boolean,
@ -74,8 +76,11 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
noPremium?: boolean, noPremium?: boolean,
withLock?: boolean, withLock?: boolean,
relativeEffect?: boolean, relativeEffect?: boolean,
loopEffect?: boolean loopEffect?: boolean,
isCustomEmoji?: boolean
}) { }) {
div = Array.isArray(div) ? div : [div];
const stickerType = doc.sticker; const stickerType = doc.sticker;
if(stickerType === 1) { if(stickerType === 1) {
asStatic = true; asStatic = true;
@ -94,13 +99,10 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
lottieLoader.loadLottieWorkers(); lottieLoader.loadLottieWorkers();
} }
if(!stickerType) { div.forEach((div) => {
console.error('wrong doc for wrapSticker!', doc); div.dataset.docId = '' + doc.id;
throw new Error('wrong doc for wrapSticker!'); div.classList.add('media-sticker-wrapper');
} });
div.dataset.docId = '' + doc.id;
div.classList.add('media-sticker-wrapper');
/* if(stickerType === 3) { /* if(stickerType === 3) {
const videoRes = wrapVideo({ const videoRes = wrapVideo({
@ -141,14 +143,16 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
const effectThumb = getStickerEffectThumb(doc); const effectThumb = getStickerEffectThumb(doc);
if(isOut !== undefined && effectThumb && !isOut) { if(isOut !== undefined && effectThumb && !isOut) {
div.classList.add('reflect-x'); div.forEach((div) => div.classList.add('reflect-x'));
} }
const willHaveLock = effectThumb && withLock; const willHaveLock = effectThumb && withLock;
if(willHaveLock) { if(willHaveLock) {
div.classList.add('is-premium-sticker', 'tgico-premium_lock');
const lockUrl = locksUrls[doc.id]; const lockUrl = locksUrls[doc.id];
lockUrl && div.style.setProperty('--lock-url', `url(${lockUrl})`); div.forEach((div) => {
div.classList.add('is-premium-sticker', 'tgico-premium_lock');
lockUrl && div.style.setProperty('--lock-url', `url(${lockUrl})`);
});
} }
if(asStatic && stickerType !== 1) { if(asStatic && stickerType !== 1) {
@ -164,13 +168,14 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
const isThumbNeededForType = isAnimated; const isThumbNeededForType = isAnimated;
const lottieCachedThumb = stickerType === 2 || stickerType === 3 ? await managers.appDocsManager.getLottieCachedThumb(doc.id, toneIndex) : undefined; 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 loadThumbPromise = deferredPromise<void>();
let haveThumbCached = false; let haveThumbCached = false;
if(( if((
doc.thumbs?.length || doc.thumbs?.length ||
lottieCachedThumb lottieCachedThumb
) && ) &&
!div.firstElementChild && ( !div[0].firstElementChild && (
!downloaded || !downloaded ||
isThumbNeededForType || isThumbNeededForType ||
onlyThumb onlyThumb
@ -180,8 +185,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
// console.log('wrap sticker', thumb, div); // console.log('wrap sticker', thumb, div);
let thumbImage: HTMLImageElement | HTMLCanvasElement; const afterRender = (div: HTMLElement, thumbImage: HTMLElement) => {
const afterRender = () => {
if(!div.childElementCount) { if(!div.childElementCount) {
thumbImage.classList.add('media-sticker', 'thumbnail'); thumbImage.classList.add('media-sticker', 'thumbnail');
@ -193,105 +197,133 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
}; };
if('url' in thumb) { if('url' in thumb) {
thumbImage = new Image();
renderImageFromUrl(thumbImage, thumb.url, afterRender);
haveThumbCached = true; haveThumbCached = true;
div.forEach((div) => {
const thumbImage = new Image();
renderImageFromUrl(thumbImage, (thumb as any).url, () => afterRender(div, thumbImage));
});
} else if('bytes' in thumb) { } else if('bytes' in thumb) {
if(thumb._ === 'photoPathSize') { if(thumb._ === 'photoPathSize') {
if(thumb.bytes.length) { if(!thumb.bytes.length) {
const d = getPathFromBytes(thumb.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) path.setAttributeNS(null, 'fill', 'url(#g)');
svg.append(path);
div.append(svg);
} else {
thumb = doc.thumbs.find((t) => (t as PhotoSize.photoStrippedSize).bytes?.length) || thumb; 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) { } else if(toneIndex <= 0) {
thumbImage = new Image(); 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 */) { if((IS_WEBP_SUPPORTED || doc.pFlags.stickerThumbConverted || cacheContext.url)/* && false */) {
renderImageFromUrl(thumbImage, getPreviewURLFromThumb(doc, thumb, true), afterRender);
haveThumbCached = true; haveThumbCached = true;
r();
} else { } else {
haveThumbCached = true;
webpWorkerController.convert('main-' + doc.id, thumb.bytes).then((bytes) => { webpWorkerController.convert('main-' + doc.id, thumb.bytes).then((bytes) => {
managers.appDocsManager.saveWebPConvertedStrippedThumb(doc.id, bytes); managers.appDocsManager.saveWebPConvertedStrippedThumb(doc.id, bytes);
(thumb as PhotoSize.photoStrippedSize).bytes = bytes; (thumb as PhotoSize.photoStrippedSize).bytes = bytes;
doc.pFlags.stickerThumbConverted = true; doc.pFlags.stickerThumbConverted = true;
if(middleware && !middleware()) return; if((middleware && !middleware()) || (div as HTMLElement[])[0].childElementCount) {
loadThumbPromise.resolve();
if(!div.childElementCount) { return;
renderImageFromUrl(thumbImage, getPreviewURLFromThumb(doc, thumb as PhotoSize.photoStrippedSize, true), afterRender);
} }
}).catch(() => {});
r();
}).catch(() => loadThumbPromise.resolve());
} }
} }
} else if(((stickerType === 2 && toneIndex <= 0) || stickerType === 3) && (withThumb || onlyThumb)) { } else if(((stickerType === 2 && toneIndex <= 0) || stickerType === 3) && (withThumb || onlyThumb)) {
const load = async() => { const load = async() => {
if(div.childElementCount || (middleware && !middleware())) return; if((div as HTMLElement[])[0].childElementCount || (middleware && !middleware())) {
loadThumbPromise.resolve();
return;
}
const r = () => { const r = (div: HTMLElement, thumbImage: HTMLElement) => {
if(div.childElementCount || (middleware && !middleware())) return; if(div.childElementCount || (middleware && !middleware())) {
renderImageFromUrl(thumbImage, cacheContext.url, afterRender); loadThumbPromise.resolve();
return;
}
renderImageFromUrl(thumbImage, cacheContext.url, () => afterRender(div, thumbImage));
}; };
await getCacheContext(); await getCacheContext();
if(cacheContext.url) { (div as HTMLElement[]).forEach((div) => {
r(); if(cacheContext.url) {
return; r(div, new Image());
} else { } else if('bytes' in thumb) {
const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true); const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true);
thumbImage = res.image; res.loadPromise.then(() => r(div, res.image));
res.loadPromise.then(r);
// return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r);
// 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) { if(lazyLoadQueue && onlyThumb) {
lazyLoadQueue.push({div, load}); lazyLoadQueue.push({div: div[0], load});
return; loadThumbPromise.resolve();
return ret;
} else { } else {
load(); load();
@ -307,7 +339,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
} }
if(onlyThumb/* || true */) { // for sticker panel if(onlyThumb/* || true */) { // for sticker panel
return; return ret;
} }
const middlewareError = makeError('MIDDLEWARE'); const middlewareError = makeError('MIDDLEWARE');
@ -317,27 +349,14 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
} }
if(stickerType === 2 && !asStatic) { if(stickerType === 2 && !asStatic) {
/* if(doc.id === '1860749763008266301') { return appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId, thumb: fullThumb})
console.log('loaded sticker:', doc, div);
} */
// await new Promise((resolve) => setTimeout(resolve, 500));
// return;
// console.time('download sticker' + doc.id);
// appDocsManager.downloadDocNew(doc.id).promise.then((res) => res.json()).then(async(json) => {
// fetch(doc.url).then((res) => res.json()).then(async(json) => {
return await appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId, thumb: fullThumb})
.then(async(blob) => { .then(async(blob) => {
// console.timeEnd('download sticker' + doc.id);
// console.log('loaded sticker:', doc, div/* , blob */);
if(middleware && !middleware()) { if(middleware && !middleware()) {
throw middlewareError; throw middlewareError;
} }
const animation = await lottieLoader.loadAnimationWorker({ const animation = await lottieLoader.loadAnimationWorker({
container: div, container: (div as HTMLElement[])[0],
loop: loop && !emoji, loop: loop && !emoji,
autoplay: play, autoplay: play,
animationData: blob, animationData: blob,
@ -346,24 +365,25 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
name: 'doc' + doc.id, name: 'doc' + doc.id,
needUpscale, needUpscale,
skipRatio, skipRatio,
toneIndex toneIndex,
}, group, middleware); sync: isCustomEmoji
}, group, loadStickerMiddleware ?? middleware);
// const deferred = deferredPromise<void>(); // const deferred = deferredPromise<void>();
const setLockColor = willHaveLock ? () => { const setLockColor = willHaveLock ? () => {
const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas); const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas[0]);
div.style.setProperty('--lock-url', `url(${lockUrl})`); (div as HTMLElement[]).forEach((div) => div.style.setProperty('--lock-url', `url(${lockUrl})`));
} : undefined; } : undefined;
animation.addEventListener('firstFrame', () => { const onFirstFrame = (container: HTMLElement, canvas: HTMLCanvasElement) => {
const element = div.firstElementChild; const element = container.firstElementChild;
if(needFadeIn !== false) { if(needFadeIn !== false) {
needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled; needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled;
} }
const cb = () => { const cb = () => {
if(element && element !== animation.canvas && element.tagName !== 'DIV') { if(element && element !== canvas && element.tagName !== 'DIV') {
element.remove(); element.remove();
} }
}; };
@ -374,29 +394,36 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
} }
} else { } else {
sequentialDom.mutate(() => { sequentialDom.mutate(() => {
animation.canvas.classList.add('fade-in'); canvas && canvas.classList.add('fade-in');
if(element) { if(element) {
element.classList.add('fade-out'); element.classList.add('fade-out');
} }
animation.canvas.addEventListener('animationend', () => { (canvas || element).addEventListener('animationend', () => {
sequentialDom.mutate(() => { sequentialDom.mutate(() => {
animation.canvas.classList.remove('fade-in'); canvas && canvas.classList.remove('fade-in');
cb(); cb();
}); });
}, {once: true}); }, {once: true});
}); });
} }
};
animation.addEventListener('firstFrame', () => {
const canvas = animation.canvas[0];
if(withThumb !== false) { if(withThumb !== false) {
saveLottiePreview(doc, animation.canvas, toneIndex); saveLottiePreview(doc, canvas, toneIndex);
} }
if(willHaveLock) { if(willHaveLock) {
setLockColor(); setLockColor();
} }
// deferred.resolve(); if(!isCustomEmoji) {
(div as HTMLElement[]).forEach((container, idx) => {
onFirstFrame(container, animation.canvas[idx]);
});
}
}, {once: true}); }, {once: true});
if(emoji) { if(emoji) {
@ -409,16 +436,17 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji); managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji);
attachClickEvent(div, async(e) => { const container = (div as HTMLElement[])[0];
attachClickEvent(container, async(e) => {
cancelEvent(e); cancelEvent(e);
const animation = lottieLoader.getAnimation(div); const animation = lottieLoader.getAnimation(container);
if(animation.paused) { if(animation.paused) {
const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji); const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji);
if(doc) { if(doc) {
const audio = document.createElement('audio'); const audio = document.createElement('audio');
audio.style.display = 'none'; audio.style.display = 'none';
div.parentElement.append(audio); container.parentElement.append(audio);
try { try {
const url = await appDownloadManager.downloadMediaURL({media: doc}); const url = await appDownloadManager.downloadMediaURL({media: doc});
@ -455,7 +483,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
middleware, middleware,
side: isOut ? 'right' : 'left', side: isOut ? 'right' : 'left',
size: 280, size: 280,
target: div, target: container,
play: true, play: true,
withRandomOffset: true withRandomOffset: true
}); });
@ -477,7 +505,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
a.t = (a.t - firstTime) / 1000; a.t = (a.t - firstTime) / 1000;
}); });
const bubble = findUpClassName(div, 'bubble'); const bubble = findUpClassName(container, 'bubble');
managers.appMessagesManager.setTyping(appImManager.chat.peerId, { managers.appMessagesManager.setTyping(appImManager.chat.peerId, {
_: 'sendMessageEmojiInteraction', _: 'sendMessageEmojiInteraction',
msg_id: getServerMessageId(+bubble.dataset.mid), msg_id: getServerMessageId(+bubble.dataset.mid),
@ -509,34 +537,29 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
// return deferred; // return deferred;
// await new Promise((resolve) => setTimeout(resolve, 5e3)); // await new Promise((resolve) => setTimeout(resolve, 5e3));
}); });
// console.timeEnd('render sticker' + doc.id);
} else if(asStatic || stickerType === 3) { } else if(asStatic || stickerType === 3) {
let media: HTMLElement; const media: HTMLElement[] = (div as HTMLElement[]).map(() => {
if(asStatic) { let media: HTMLElement;
media = new Image(); if(asStatic) {
} else { media = new Image();
media = createVideo(); } else {
(media as HTMLVideoElement).muted = true; const video = media = createVideo();
video.muted = true;
if(play) { if(play) video.autoplay = true;
(media as HTMLVideoElement).autoplay = true; if(loop) video.loop = true;
} }
if(loop) { media.classList.add('media-sticker');
(media as HTMLVideoElement).loop = true; return media;
} });
}
const thumbImage = div.firstElementChild !== media && div.firstElementChild; const thumbImage = (div as HTMLElement[]).map((div, idx) => (div.firstElementChild as HTMLElement) !== media[idx] && div.firstElementChild) as HTMLElement[];
if(needFadeIn !== false) { if(needFadeIn !== false) {
needFadeIn = (needFadeIn || !downloaded || (asStatic ? thumbImage : (!thumbImage || thumbImage.tagName === 'svg'))) && rootScope.settings.animationsEnabled; needFadeIn = (needFadeIn || !downloaded || (asStatic ? thumbImage[0] : (!thumbImage[0] || thumbImage[0].tagName === 'svg'))) && rootScope.settings.animationsEnabled;
} }
media.classList.add('media-sticker');
if(needFadeIn) { if(needFadeIn) {
media.classList.add('fade-in'); media.forEach((media) => media.classList.add('fade-in'));
} }
return new Promise<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => { return new Promise<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => {
@ -546,7 +569,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
return; return;
} }
const onLoad = () => { const onLoad = (div: HTMLElement, media: HTMLElement, thumbImage: HTMLElement) => {
sequentialDom.mutateElement(div, () => { sequentialDom.mutateElement(div, () => {
div.append(media); div.append(media);
thumbImage && thumbImage.classList.add('fade-out'); thumbImage && thumbImage.classList.add('fade-out');
@ -579,19 +602,22 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
}; };
await getCacheContext(); await getCacheContext();
if(asStatic) { media.forEach((media, idx) => {
renderImageFromUrl(media, cacheContext.url, onLoad); const cb = () => onLoad((div as HTMLElement[])[idx], media, thumbImage[idx]);
} else { if(asStatic) {
(media as HTMLVideoElement).src = cacheContext.url; renderImageFromUrl(media, cacheContext.url, cb);
onMediaLoad(media as HTMLVideoElement).then(onLoad); } else {
} (media as HTMLVideoElement).src = cacheContext.url;
onMediaLoad(media as HTMLVideoElement).then(cb);
}
});
}; };
await getCacheContext(); await getCacheContext();
if(cacheContext.url) r(); if(cacheContext.url) r();
else { else {
let promise: Promise<any>; let promise: Promise<any>;
if(stickerType === 2 && asStatic) { if(stickerType !== 1 && asStatic) {
const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize; const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize;
// promise = managers.appDocsManager.getThumbURL(doc, thumb).promise // promise = managers.appDocsManager.getThumbURL(doc, thumb).promise
promise = appDownloadManager.downloadMediaURL({media: doc, thumb, queueId: lazyLoadQueue?.queueId}); promise = appDownloadManager.downloadMediaURL({media: doc, thumb, queueId: lazyLoadQueue?.queueId});
@ -605,8 +631,13 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
} }
}; };
if(exportLoad && (!downloaded || isAnimated)) {
ret.load = load;
return ret;
}
const loadPromise: Promise<Awaited<ReturnType<typeof load>> | void> = lazyLoadQueue && (!downloaded || isAnimated) ? const loadPromise: Promise<Awaited<ReturnType<typeof load>> | void> = lazyLoadQueue && (!downloaded || isAnimated) ?
(lazyLoadQueue.push({div, load}), Promise.resolve()) : (lazyLoadQueue.push({div: div[0], load}), Promise.resolve()) :
load(); load();
if(downloaded && (asStatic/* || stickerType === 3 */)) { if(downloaded && (asStatic/* || stickerType === 3 */)) {
@ -618,7 +649,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
if(stickerType === 2 && effectThumb && isOut !== undefined && !noPremium) { if(stickerType === 2 && effectThumb && isOut !== undefined && !noPremium) {
attachStickerEffectHandler({ attachStickerEffectHandler({
container: div, container: div[0],
doc, doc,
managers, managers,
middleware, middleware,
@ -630,7 +661,8 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
}); });
} }
return {render: loadPromise}; ret.render = loadPromise as any;
return ret;
} }
function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise, relativeEffect, loopEffect}: { function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise, relativeEffect, loopEffect}: {

3
src/environment/customEmojiSupport.ts

@ -0,0 +1,3 @@
const IS_CUSTOM_EMOJI_SUPPORTED = true;
export default IS_CUSTOM_EMOJI_SUPPORTED;

3
src/environment/imageBitmapSupport.ts

@ -0,0 +1,3 @@
const IS_IMAGE_BITMAP_SUPPORTED = typeof(ImageBitmap) !== 'undefined';
export default IS_IMAGE_BITMAP_SUPPORTED;

3
src/environment/webAssemblySupport.ts

@ -0,0 +1,3 @@
const IS_WEB_ASSEMBLY_SUPPORTED = typeof(WebAssembly) !== 'undefined';
export default IS_WEB_ASSEMBLY_SUPPORTED;

9
src/helpers/mediaSizes.ts

@ -19,7 +19,8 @@ type MediaTypeSizes = {
poll: MediaSize, poll: MediaSize,
round: MediaSize, round: MediaSize,
documentName: MediaSize, documentName: MediaSize,
invoice: MediaSize invoice: MediaSize,
customEmoji: MediaSize
}; };
export type MediaSizeType = keyof MediaTypeSizes; export type MediaSizeType = keyof MediaTypeSizes;
@ -56,7 +57,8 @@ class MediaSizes extends EventListenerBase<{
poll: makeMediaSize(240, 0), poll: makeMediaSize(240, 0),
round: makeMediaSize(200, 200), round: makeMediaSize(200, 200),
documentName: makeMediaSize(200, 0), documentName: makeMediaSize(200, 0),
invoice: makeMediaSize(240, 240) invoice: makeMediaSize(240, 240),
customEmoji: makeMediaSize(18, 18)
}, },
desktop: { desktop: {
regular: makeMediaSize(420, 340), regular: makeMediaSize(420, 340),
@ -69,7 +71,8 @@ class MediaSizes extends EventListenerBase<{
poll: makeMediaSize(330, 0), poll: makeMediaSize(330, 0),
round: makeMediaSize(280, 280), round: makeMediaSize(280, 280),
documentName: makeMediaSize(240, 0), documentName: makeMediaSize(240, 0),
invoice: makeMediaSize(320, 260) invoice: makeMediaSize(320, 260),
customEmoji: makeMediaSize(18, 18)
} }
}; };

2
src/helpers/preloadAnimatedEmojiSticker.ts

@ -33,7 +33,7 @@ export default function preloadAnimatedEmojiSticker(emoji: string, width?: numbe
}, 'none'); }, 'none');
animation.addEventListener('firstFrame', () => { animation.addEventListener('firstFrame', () => {
saveLottiePreview(doc, animation.canvas, toneIndex); saveLottiePreview(doc, animation.canvas[0], toneIndex);
animation.remove(); animation.remove();
}, {once: true}); }, {once: true});
}); });

2
src/helpers/sequentialDom.ts

@ -49,7 +49,7 @@ class SequentialDom {
const promise = isConnected ? this.mutate() : Promise.resolve(); const promise = isConnected ? this.mutate() : Promise.resolve();
if(callback !== undefined) { if(callback !== undefined) {
if(isConnected) { if(!isConnected) {
callback(); callback();
} else { } else {
promise.then(() => callback()); promise.then(() => callback());

2
src/lib/appManagers/appDialogsManager.ts

@ -234,7 +234,7 @@ export class AppDialogsManager {
private managers: AppManagers; private managers: AppManagers;
private selectTab: ReturnType<typeof horizontalMenu>; private selectTab: ReturnType<typeof horizontalMenu>;
constructor() { public start() {
const managers = this.managers = getProxiedManagers(); const managers = this.managers = getProxiedManagers();
this.contextMenu = new DialogsContextMenu(managers); this.contextMenu = new DialogsContextMenu(managers);

3
src/lib/appManagers/appDocsManager.ts

@ -134,6 +134,7 @@ export class AppDocsManager extends AppManager {
} }
break; break;
case 'documentAttributeCustomEmoji':
case 'documentAttributeSticker': case 'documentAttributeSticker':
if(attribute.alt !== undefined) { if(attribute.alt !== undefined) {
doc.stickerEmojiRaw = attribute.alt; doc.stickerEmojiRaw = attribute.alt;
@ -153,7 +154,7 @@ export class AppDocsManager extends AppManager {
doc.sticker = 1; doc.sticker = 1;
} else if(doc.mime_type === 'video/webm') { } else if(doc.mime_type === 'video/webm') {
if(!getEnvironment().IS_WEBM_SUPPORTED) { if(!getEnvironment().IS_WEBM_SUPPORTED) {
return; break;
} }
doc.type = 'sticker'; doc.type = 'sticker';

63
src/lib/appManagers/appEmojiManager.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {MyDocument} from './appDocsManager';
import App from '../../config/app'; import App from '../../config/app';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import isObject from '../../helpers/object/isObject'; import isObject from '../../helpers/object/isObject';
@ -13,6 +14,8 @@ import fixEmoji from '../richTextProcessor/fixEmoji';
import SearchIndex from '../searchIndex'; import SearchIndex from '../searchIndex';
import stateStorage from '../stateStorage'; import stateStorage from '../stateStorage';
import {AppManager} from './manager'; import {AppManager} from './manager';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import pause from '../../helpers/schedulers/pause';
type EmojiLangPack = { type EmojiLangPack = {
keywords: { keywords: {
@ -44,6 +47,9 @@ export class AppEmojiManager extends AppManager {
private recent: string[]; private recent: string[];
private getRecentEmojisPromise: Promise<AppEmojiManager['recent']>; private getRecentEmojisPromise: Promise<AppEmojiManager['recent']>;
private getCustomEmojiDocumentsPromise: Promise<any>;
private getCustomEmojiDocumentPromises: Map<DocId, CancellablePromise<MyDocument>> = new Map();
/* public getPopularEmoji() { /* public getPopularEmoji() {
return stateStorage.get('emojis_popular').then((popEmojis) => { return stateStorage.get('emojis_popular').then((popEmojis) => {
var result = [] var result = []
@ -230,4 +236,61 @@ export class AppEmojiManager extends AppManager {
this.rootScope.dispatchEvent('emoji_recent', emoji); this.rootScope.dispatchEvent('emoji_recent', emoji);
}); });
} }
public getCustomEmojiDocuments(docIds: DocId[]) {
return this.apiManager.invokeApi('messages.getCustomEmojiDocuments', {document_id: docIds}).then((documents) => {
return documents.map((doc) => {
return this.appDocsManager.saveDoc(doc, {
type: 'customEmoji',
docId: doc.id
});
});
});
}
public getCachedCustomEmojiDocuments(docIds: DocId[]) {
return docIds.map((docId) => this.appDocsManager.getDoc(docId));
}
private setDebouncedGetCustomEmojiDocuments() {
if(this.getCustomEmojiDocumentsPromise || !this.getCustomEmojiDocumentPromises.size) {
return;
}
this.getCustomEmojiDocumentsPromise = pause(0).then(() => {
const allIds = [...this.getCustomEmojiDocumentPromises.keys()];
do {
const ids = allIds.splice(0, 100);
this.getCustomEmojiDocuments(ids).then((docs) => {
docs.forEach((doc, idx) => {
const docId = ids[idx];
const deferred = this.getCustomEmojiDocumentPromises.get(docId);
this.getCustomEmojiDocumentPromises.delete(docId);
deferred.resolve(doc);
});
}).finally(() => {
this.setDebouncedGetCustomEmojiDocuments();
});
} while(allIds.length);
});
}
public getCustomEmojiDocument(id: DocId) {
let promise = this.getCustomEmojiDocumentPromises.get(id);
if(promise) {
return promise;
}
const doc = this.appDocsManager.getDoc(id);
if(doc) {
return Promise.resolve(doc);
}
promise = deferredPromise();
this.getCustomEmojiDocumentPromises.set(id, promise);
this.setDebouncedGetCustomEmojiDocuments();
return promise;
}
} }

12
src/lib/appManagers/appImManager.ts

@ -25,7 +25,6 @@ import appNavigationController from '../../components/appNavigationController';
import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
import I18n, {i18n, join, LangPackKey} from '../langPack'; import I18n, {i18n, join, LangPackKey} from '../langPack';
import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction} from '../../layer'; import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction} from '../../layer';
import {hslaStringToHex} from '../../helpers/color';
import PeerTitle from '../../components/peerTitle'; import PeerTitle from '../../components/peerTitle';
import PopupPeer from '../../components/popups/peer'; import PopupPeer from '../../components/popups/peer';
import blurActiveElement from '../../helpers/dom/blurActiveElement'; import blurActiveElement from '../../helpers/dom/blurActiveElement';
@ -92,15 +91,6 @@ import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount
import findUpClassName from '../../helpers/dom/findUpClassName'; import findUpClassName from '../../helpers/dom/findUpClassName';
import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent';
import PopupPayment from '../../components/popups/payment'; import PopupPayment from '../../components/popups/payment';
import {getMiddleware} from '../../helpers/middleware';
import {wrapSticker} from '../../components/wrappers';
import windowSize from '../../helpers/windowSize';
import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb';
import {makeMediaSize} from '../../helpers/mediaSize';
import RLottiePlayer from '../rlottie/rlottiePlayer';
import type {MyDocument} from './appDocsManager';
import deferredPromise from '../../helpers/cancellablePromise';
import {STICKER_EFFECT_MULTIPLIER} from '../../components/wrappers/sticker';
export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat'; export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat';
@ -380,7 +370,7 @@ export class AppImManager extends EventListenerBase<{
(window as any).onSpoilerClick = (e: MouseEvent) => { (window as any).onSpoilerClick = (e: MouseEvent) => {
const spoiler = findUpClassName(e.target, 'spoiler'); const spoiler = findUpClassName(e.target, 'spoiler');
const parentElement = findUpClassName(spoiler, 'message') || spoiler.parentElement; const parentElement = findUpClassName(spoiler, 'spoilers-container') || spoiler.parentElement;
const className = 'is-spoiler-visible'; const className = 'is-spoiler-visible';
const isVisible = parentElement.classList.contains(className); const isVisible = parentElement.classList.contains(className);

6
src/lib/appManagers/appMessagesManager.ts

@ -2789,9 +2789,9 @@ export class AppMessagesManager extends AppManager {
} }
} }
if(isMessage && !unsupported && message.entities) { // if(isMessage && !unsupported && message.entities) {
unsupported = message.entities.some((entity) => entity._ === 'messageEntityCustomEmoji'); // unsupported = message.entities.some((entity) => entity._ === 'messageEntityCustomEmoji');
} // }
if(isMessage && unsupported) { if(isMessage && unsupported) {
message.media = {_: 'messageMediaUnsupported'}; message.media = {_: 'messageMediaUnsupported'};

11
src/lib/mtproto/referenceDatabase.ts

@ -11,7 +11,7 @@ import deepEqual from '../../helpers/object/deepEqual';
import {AppManager} from '../appManagers/manager'; import {AppManager} from '../appManagers/manager';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds | ReferenceContext.referenceContextReactions | ReferenceContext.referenceContextUserFull; export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds | ReferenceContext.referenceContextReactions | ReferenceContext.referenceContextUserFull | ReferenceContext.referenceContextCustomEmoji;
export namespace ReferenceContext { export namespace ReferenceContext {
export type referenceContextProfilePhoto = { export type referenceContextProfilePhoto = {
type: 'profilePhoto', type: 'profilePhoto',
@ -36,6 +36,11 @@ export namespace ReferenceContext {
type: 'userFull', type: 'userFull',
userId: UserId userId: UserId
}; };
export type referenceContextCustomEmoji = {
type: 'customEmoji',
docId: DocId
};
} }
export type ReferenceBytes = Photo.photo['file_reference']; export type ReferenceBytes = Photo.photo['file_reference'];
@ -150,6 +155,10 @@ export class ReferenceDatabase extends AppManager {
break; break;
} }
case 'customEmoji': {
promise = this.appEmojiManager.getCustomEmojiDocuments([context.docId]);
}
default: { default: {
this.log.warn('refreshReference: not implemented context', context); this.log.warn('refreshReference: not implemented context', context);
return Promise.reject(); return Promise.reject();

1
src/lib/richTextProcessor/parseEntities.ts

@ -79,7 +79,6 @@ export default function parseEntities(text: string) {
length: 1 length: 1
}); });
} else if(match[8]) { // Emoji } else if(match[8]) { // Emoji
// console.log('hit', match[8]);
const unified = getEmojiUnified(match[8]); const unified = getEmojiUnified(match[8]);
if(unified) { if(unified) {
entities.push({ entities.push({

424
src/lib/richTextProcessor/wrapRichText.ts

@ -19,6 +19,237 @@ import setBlankToAnchor from './setBlankToAnchor';
import wrapUrl from './wrapUrl'; import wrapUrl from './wrapUrl';
import EMOJI_VERSIONS_SUPPORTED from '../../environment/emojiVersionsSupport'; import EMOJI_VERSIONS_SUPPORTED from '../../environment/emojiVersionsSupport';
import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent';
import IS_CUSTOM_EMOJI_SUPPORTED from '../../environment/customEmojiSupport';
import rootScope from '../rootScope';
import mediaSizes from '../../helpers/mediaSizes';
import {wrapSticker} from '../../components/wrappers';
import RLottiePlayer from '../rlottie/rlottiePlayer';
import animationIntersector from '../../components/animationIntersector';
import type {MyDocument} from '../appManagers/appDocsManager';
import LazyLoadQueue from '../../components/lazyLoadQueue';
import {Awaited} from '../../types';
import sequentialDom from '../../helpers/sequentialDom';
import {MediaSize} from '../../helpers/mediaSize';
import IS_WEBM_SUPPORTED from '../../environment/webmSupport';
const resizeObserver = new ResizeObserver((entries) => {
for(const entry of entries) {
const renderer = entry.target.parentElement as CustomEmojiRendererElement;
renderer.setDimensionsFromRect(entry.contentRect);
}
});
class CustomEmojiElement extends HTMLElement {
}
export class CustomEmojiRendererElement extends HTMLElement {
public canvas: HTMLCanvasElement;
public context: CanvasRenderingContext2D;
public players: Map<CustomEmojiElement[], RLottiePlayer>;
public clearedContainers: Set<CustomEmojiElement[]>;
public paused: boolean;
public autoplay: boolean;
public middleware: () => boolean;
public keys: string[];
constructor() {
super();
this.classList.add('custom-emoji-renderer');
this.canvas = document.createElement('canvas');
this.canvas.classList.add('custom-emoji-canvas');
this.context = this.canvas.getContext('2d');
this.append(this.canvas);
this.paused = false;
this.autoplay = true;
this.players = new Map();
this.clearedContainers = new Set();
this.keys = [];
}
public connectedCallback() {
// this.setDimensions();
animationIntersector.addAnimation(this, 'EMOJI');
resizeObserver.observe(this.canvas);
this.connectedCallback = undefined;
}
public disconnectedCallback() {
for(const key of this.keys) {
const l = lotties.get(key);
if(!l) {
continue;
}
if(!--l.counter) {
if(l.player instanceof RLottiePlayer) {
l.player.remove();
}
lotties.delete(key);
if(!lotties.size) {
clearRenderInterval();
}
}
}
resizeObserver.unobserve(this.canvas);
this.disconnectedCallback = undefined;
}
public getOffsets(offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map()) {
for(const [containers, player] of this.players) {
const offsets = containers.map((container) => {
return {
top: container.offsetTop,
left: container.offsetLeft
};
});
offsetsMap.set(containers, offsets);
}
return offsetsMap;
}
public clearCanvas() {
const {context, canvas} = this;
context.clearRect(0, 0, canvas.width, canvas.height);
}
public render(offsetsMap: ReturnType<CustomEmojiRendererElement['getOffsets']>) {
const {context, canvas} = this;
const {width, height, dpr} = canvas;
for(const [containers, player] of this.players) {
const frame = topFrames.get(player);
if(!frame) {
continue;
}
const isImageData = frame instanceof ImageData;
const {width: stickerWidth, height: stickerHeight} = player.canvas[0];
const offsets = offsetsMap.get(containers);
const maxTop = height - stickerHeight;
const maxLeft = width - stickerWidth;
if(!this.clearedContainers.has(containers)) {
containers.forEach((container) => {
container.textContent = '';
});
this.clearedContainers.add(containers);
}
offsets.forEach(({top, left}) => {
top = Math.round(top * dpr), left = Math.round(left * dpr);
if(/* top > maxTop || */left > maxLeft) {
return;
}
if(isImageData) {
context.putImageData(frame as ImageData, left, top);
} else {
// context.clearRect(left, top, width, height);
context.drawImage(frame as ImageBitmap, left, top, stickerWidth, stickerHeight);
}
});
}
}
public checkForAnyFrame() {
for(const [containers, player] of this.players) {
if(topFrames.has(player)) {
return true;
}
}
return false;
}
public pause() {
this.paused = true;
}
public play() {
this.paused = false;
}
public remove() {
this.canvas.remove();
}
public setDimensions() {
const {canvas} = this;
sequentialDom.mutateElement(canvas, () => {
const rect = canvas.getBoundingClientRect();
this.setDimensionsFromRect(rect);
});
}
public setDimensionsFromRect(rect: DOMRect) {
const {canvas} = this;
const dpr = canvas.dpr ??= Math.min(2, window.devicePixelRatio);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
}
}
type R = CustomEmojiRendererElement;
let renderInterval: number;
const top: Array<R> = [];
const topFrames: Map<RLottiePlayer, Parameters<RLottiePlayer['overrideRender']>[0]> = new Map();
const lotties: Map<string, {player: Promise<RLottiePlayer> | RLottiePlayer, middlewares: Set<() => boolean>, counter: number}> = new Map();
const rerere = () => {
const t = top.filter((r) => !r.paused && r.isConnected && r.checkForAnyFrame());
if(!t.length) {
return;
}
const offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map();
for(const r of t) {
r.getOffsets(offsetsMap);
}
for(const r of t) {
r.clearCanvas();
}
for(const r of t) {
r.render(offsetsMap);
}
};
const CUSTOM_EMOJI_FPS = 60;
const CUSTOM_EMOJI_FRAME_INTERVAL = 1000 / CUSTOM_EMOJI_FPS;
const setRenderInterval = () => {
if(renderInterval) {
return;
}
renderInterval = window.setInterval(rerere, CUSTOM_EMOJI_FRAME_INTERVAL);
rerere();
};
const clearRenderInterval = () => {
if(!renderInterval) {
return;
}
clearInterval(renderInterval);
renderInterval = undefined;
};
(window as any).lotties = lotties;
customElements.define('custom-emoji-element', CustomEmojiElement);
customElements.define('custom-emoji-renderer-element', CustomEmojiRendererElement);
/** /**
* * Expecting correctly sorted nested entities (RichTextProcessor.sortEntities) * * Expecting correctly sorted nested entities (RichTextProcessor.sortEntities)
@ -46,7 +277,13 @@ export default function wrapRichText(text: string, options: Partial<{
text: string, text: string,
lastEntity?: MessageEntity lastEntity?: MessageEntity
}, },
voodoo?: boolean voodoo?: boolean,
customEmojis?: {[docId: DocId]: CustomEmojiElement[]},
loadPromises?: Promise<any>[],
middleware?: () => boolean,
wrappingSpoiler?: boolean,
lazyLoadQueue?: LazyLoadQueue,
customEmojiSize?: MediaSize
}> = {}) { }> = {}) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
if(!text) { if(!text) {
@ -59,6 +296,8 @@ export default function wrapRichText(text: string, options: Partial<{
text text
}; };
const customEmojis = options.customEmojis ??= {};
const entities = options.entities ??= parseEntities(nasty.text); const entities = options.entities ??= parseEntities(nasty.text);
const passEntities = options.passEntities ??= {}; const passEntities = options.passEntities ??= {};
@ -217,6 +456,25 @@ export default function wrapRichText(text: string, options: Partial<{
break; break;
} }
case 'messageEntityCustomEmoji': {
if(!IS_CUSTOM_EMOJI_SUPPORTED) {
break;
}
if(nextEntity?._ === 'messageEntityEmoji') {
++nasty.i;
nasty.lastEntity = nextEntity;
nasty.usedLength += nextEntity.length;
nextEntity = entities[nasty.i + 1];
}
(customEmojis[entity.document_id] ??= []).push(element = new CustomEmojiElement());
element.classList.add('custom-emoji');
property = 'alt';
break;
}
case 'messageEntityEmoji': { case 'messageEntityEmoji': {
let isSupported = IS_EMOJI_SUPPORTED; let isSupported = IS_EMOJI_SUPPORTED;
if(isSupported) { if(isSupported) {
@ -287,7 +545,8 @@ export default function wrapRichText(text: string, options: Partial<{
if(nextEntity?._ === 'messageEntityUrl' && if(nextEntity?._ === 'messageEntityUrl' &&
nextEntity.length === entity.length && nextEntity.length === entity.length &&
nextEntity.offset === entity.offset) { nextEntity.offset === entity.offset) {
nasty.i++; nasty.lastEntity = nextEntity;
++nasty.i;
} }
if(url !== fullEntityText) { if(url !== fullEntityText) {
@ -389,6 +648,14 @@ export default function wrapRichText(text: string, options: Partial<{
const encoded = encodeSpoiler(nasty.text, entity); const encoded = encodeSpoiler(nasty.text, entity);
nasty.text = encoded.text; nasty.text = encoded.text;
partText = encoded.entityText; partText = encoded.entityText;
nasty.usedLength += partText.length;
let n: MessageEntity;
for(; n = entities[nasty.i + 1], n && n.offset < endOffset;) {
// nasty.usedLength += n.length;
++nasty.i;
nasty.lastEntity = n;
nextEntity = entities[nasty.i + 1];
}
} else if(options.wrappingDraft) { } else if(options.wrappingDraft) {
element = document.createElement('span'); element = document.createElement('span');
element.style.fontFamily = 'spoiler'; element.style.fontFamily = 'spoiler';
@ -464,6 +731,159 @@ export default function wrapRichText(text: string, options: Partial<{
(lastElement || fragment).append(nasty.text.slice(nasty.usedLength)); (lastElement || fragment).append(nasty.text.slice(nasty.usedLength));
} }
const docIds = Object.keys(customEmojis) as DocId[];
if(docIds.length) {
const managers = rootScope.managers;
const middleware = options.middleware;
const renderer = new CustomEmojiRendererElement();
renderer.middleware = middleware;
top.push(renderer);
fragment.prepend(renderer);
const size = options.customEmojiSize || mediaSizes.active.customEmoji;
const loadPromise = managers.appEmojiManager.getCachedCustomEmojiDocuments(docIds).then((docs) => {
console.log(docs);
if(middleware && !middleware()) return;
const loadPromises: Promise<any>[] = [];
const wrap = (doc: MyDocument, _loadPromises?: Promise<any>[]): Promise<Awaited<ReturnType<typeof wrapSticker>> & {onRender?: () => void}> => {
const containers = customEmojis[doc.id];
const isLottie = doc.sticker === 2;
const loadPromises: Promise<any>[] = [];
const promise = wrapSticker({
div: containers,
doc,
width: size.width,
height: size.height,
loop: true,
play: true,
managers,
isCustomEmoji: true,
group: 'none',
loadPromises,
middleware,
exportLoad: true,
needFadeIn: false,
loadStickerMiddleware: isLottie && middleware ? () => {
if(lotties.get(key) !== l) {
return false;
}
let good = !l.middlewares.size;
for(const middleware of l.middlewares) {
if(middleware()) {
good = true;
}
}
return good;
} : undefined,
static: doc.mime_type === 'video/webm' && !IS_WEBM_SUPPORTED
});
if(_loadPromises) {
promise.then(() => _loadPromises.push(...loadPromises));
}
if(!isLottie) {
return promise;
}
const onRender = (player: Awaited<Awaited<typeof promise>['render']>) => Promise.all(loadPromises).then(() => {
if(player instanceof RLottiePlayer && (!middleware || middleware())) {
l.player = player;
const playerCanvas = player.canvas[0];
renderer.canvas.dpr = playerCanvas.dpr;
renderer.players.set(containers, player);
setRenderInterval();
player.overrideRender ??= (frame) => {
topFrames.set(player, frame);
// frames.set(containers, frame);
};
l.middlewares.delete(middleware);
}
});
const key = [doc.id, size.width, size.height].join('-');
renderer.keys.push(key);
let l = lotties.get(key);
if(!l) {
l = {
player: undefined,
middlewares: new Set(),
counter: 0
};
lotties.set(key, l);
}
++l.counter;
if(middleware) {
l.middlewares.add(middleware);
}
return promise.then((res) => ({...res, onRender}));
};
const missing: DocId[] = [];
const cachedPromises = docs.map((doc, idx) => {
if(!doc) {
missing.push(docIds[idx]);
return;
}
return wrap(doc, loadPromises);
}).filter(Boolean);
const uncachedPromisesPromise = managers.appEmojiManager.getCustomEmojiDocuments(missing).then((docs) => {
if(middleware && !middleware()) return [];
return docs.filter(Boolean).map((doc) => wrap(doc));
});
const loadFromPromises = (promises: typeof cachedPromises) => {
return Promise.all(promises).then((arr) => {
const promises = arr.map(({load, onRender}) => {
if(!load) {
return;
}
return load().then(onRender);
});
return Promise.all(promises);
});
};
const load = () => {
if(middleware && !middleware()) return;
const cached = loadFromPromises(cachedPromises);
const uncached = uncachedPromisesPromise.then((promises) => loadFromPromises(promises));
return Promise.all([cached, uncached]);
};
if(options.lazyLoadQueue) {
options.lazyLoadQueue.push({
div: renderer.canvas,
load
});
} else {
load();
}
return Promise.all(cachedPromises).then(() => Promise.all(loadPromises)).then(() => {});
});
// recordPromise(loadPromise, 'render emojis: ' + docIds.length);
options.loadPromises?.push(loadPromise);
}
return fragment; return fragment;
} }

99
src/lib/rlottie/lottieLoader.ts

@ -11,8 +11,9 @@ import {logger, LogTypes} from '../logger';
import RLottiePlayer, {RLottieOptions} from './rlottiePlayer'; import RLottiePlayer, {RLottieOptions} from './rlottiePlayer';
import QueryableWorker from './queryableWorker'; import QueryableWorker from './queryableWorker';
import blobConstruct from '../../helpers/blob/blobConstruct'; import blobConstruct from '../../helpers/blob/blobConstruct';
import rootScope from '../rootScope';
import apiManagerProxy from '../mtproto/mtprotoworker'; import apiManagerProxy from '../mtproto/mtprotoworker';
import IS_WEB_ASSEMBLY_SUPPORTED from '../../environment/webAssemblySupport';
import makeError from '../../helpers/makeError';
export type LottieAssetName = 'EmptyFolder' | 'Folders_1' | 'Folders_2' | export type LottieAssetName = 'EmptyFolder' | 'Folders_1' | 'Folders_2' |
'TwoFactorSetupMonkeyClose' | 'TwoFactorSetupMonkeyCloseAndPeek' | 'TwoFactorSetupMonkeyClose' | 'TwoFactorSetupMonkeyCloseAndPeek' |
@ -21,12 +22,12 @@ export type LottieAssetName = 'EmptyFolder' | 'Folders_1' | 'Folders_2' |
'voice_outlined2' | 'voip_filled' | 'voice_mini'; 'voice_outlined2' | 'voip_filled' | 'voice_mini';
export class LottieLoader { export class LottieLoader {
private isWebAssemblySupported = typeof(WebAssembly) !== 'undefined'; private loadPromise: Promise<void> = !IS_WEB_ASSEMBLY_SUPPORTED ? Promise.reject() : undefined;
private loadPromise: Promise<void> = !this.isWebAssemblySupported ? Promise.reject() : undefined;
private loaded = false; private loaded = false;
private workersLimit = 4; private workersLimit = 4;
private players: {[reqId: number]: RLottiePlayer} = {}; private players: {[reqId: number]: RLottiePlayer} = {};
private playersByCacheName: {[cacheName: string]: Set<RLottiePlayer>} = {};
private workers: QueryableWorker[] = []; private workers: QueryableWorker[] = [];
private curWorkerNum = 0; private curWorkerNum = 0;
@ -35,7 +36,7 @@ export class LottieLoader {
public getAnimation(element: HTMLElement) { public getAnimation(element: HTMLElement) {
for(const i in this.players) { for(const i in this.players) {
if(this.players[i].el === element) { if(this.players[i].el.includes(element)) {
return this.players[i]; return this.players[i];
} }
} }
@ -91,7 +92,7 @@ export class LottieLoader {
} }
public loadAnimationFromURL(params: Omit<RLottieOptions, 'animationData'>, url: string): Promise<RLottiePlayer> { public loadAnimationFromURL(params: Omit<RLottieOptions, 'animationData'>, url: string): Promise<RLottiePlayer> {
if(!this.isWebAssemblySupported) { if(!IS_WEB_ASSEMBLY_SUPPORTED) {
return this.loadPromise as any; return this.loadPromise as any;
} }
@ -136,22 +137,30 @@ export class LottieLoader {
group: AnimationItemGroup = params.group || '', group: AnimationItemGroup = params.group || '',
middleware?: () => boolean middleware?: () => boolean
): Promise<RLottiePlayer> { ): Promise<RLottiePlayer> {
if(!this.isWebAssemblySupported) { if(!IS_WEB_ASSEMBLY_SUPPORTED) {
return this.loadPromise as any; return this.loadPromise as any;
} }
// params.autoplay = true;
if(!this.loaded) { if(!this.loaded) {
await this.loadLottieWorkers(); await this.loadLottieWorkers();
} }
if(middleware && !middleware()) { if(middleware && !middleware()) {
throw new Error('middleware'); throw makeError('MIDDLEWARE');
} }
if(params.sync) {
const cacheName = RLottiePlayer.CACHE.generateName(params.name, params.width, params.height, params.color, params.toneIndex);
const players = this.playersByCacheName[cacheName];
if(players?.size) {
return Promise.resolve(players.entries().next().value[0]);
}
}
const containers = Array.isArray(params.container) ? params.container : [params.container];
if(!params.width || !params.height) { if(!params.width || !params.height) {
params.width = parseInt(params.container.style.width); params.width = parseInt(containers[0].style.width);
params.height = parseInt(params.container.style.height); params.height = parseInt(containers[0].style.height);
} }
if(!params.width || !params.height) { if(!params.width || !params.height) {
@ -160,7 +169,7 @@ export class LottieLoader {
params.group = group; params.group = group;
const player = this.initPlayer(params.container, params); const player = this.initPlayer(containers, params);
if(group !== 'none') { if(group !== 'none') {
animationIntersector.addAnimation(player, group); animationIntersector.addAnimation(player, group);
@ -170,42 +179,41 @@ export class LottieLoader {
} }
private onPlayerLoaded = (reqId: number, frameCount: number, fps: number) => { private onPlayerLoaded = (reqId: number, frameCount: number, fps: number) => {
const rlPlayer = this.players[reqId]; const player = this.players[reqId];
if(!rlPlayer) { if(!player) {
this.log.warn('onPlayerLoaded on destroyed player:', reqId, frameCount); this.log.warn('onPlayerLoaded on destroyed player:', reqId, frameCount);
return; return;
} }
this.log.debug('onPlayerLoaded'); this.log.debug('onPlayerLoaded');
rlPlayer.onLoad(frameCount, fps); player.onLoad(frameCount, fps);
// rlPlayer.addListener('firstFrame', () => {
// animationIntersector.addAnimation(player, group);
// }, true);
}; };
private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray) => { private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray | ImageBitmap) => {
const rlPlayer = this.players[reqId]; const player = this.players[reqId];
if(!rlPlayer) { if(!player) {
this.log.warn('onFrame on destroyed player:', reqId, frameNo); this.log.warn('onFrame on destroyed player:', reqId, frameNo);
return; return;
} }
if(rlPlayer.clamped !== undefined) { if(player.clamped !== undefined && frame instanceof Uint8ClampedArray) {
rlPlayer.clamped = frame; player.clamped = frame;
} }
rlPlayer.renderFrame(frame, frameNo); player.renderFrame(frame, frameNo);
}; };
private onPlayerError = (reqId: number, error: Error) => { private onPlayerError = (reqId: number, error: Error) => {
const rlPlayer = this.players[reqId]; const player = this.players[reqId];
if(rlPlayer) { if(!player) {
// ! will need refactoring later, this is not the best way to remove the animation return;
const animations = animationIntersector.getAnimations(rlPlayer.el);
animations.forEach((animation) => {
animationIntersector.checkAnimation(animation, true, true);
});
} }
// ! will need refactoring later, this is not the best way to remove the animation
const animations = animationIntersector.getAnimations(player.el[0]);
animations.forEach((animation) => {
animationIntersector.checkAnimation(animation, true, true);
});
}; };
public onDestroy(reqId: number) { public onDestroy(reqId: number) {
@ -213,6 +221,10 @@ export class LottieLoader {
} }
public destroyWorkers() { public destroyWorkers() {
if(!IS_WEB_ASSEMBLY_SUPPORTED) {
return;
}
this.workers.forEach((worker, idx) => { this.workers.forEach((worker, idx) => {
worker.terminate(); worker.terminate();
this.log('worker #' + idx + ' terminated'); this.log('worker #' + idx + ' terminated');
@ -220,23 +232,40 @@ export class LottieLoader {
this.log('workers destroyed'); this.log('workers destroyed');
this.workers.length = 0; this.workers.length = 0;
this.curWorkerNum = 0;
this.loaded = false;
this.loadPromise = undefined;
} }
private initPlayer(el: HTMLElement, options: RLottieOptions) { private initPlayer(el: RLottiePlayer['el'], options: RLottieOptions) {
const rlPlayer = new RLottiePlayer({ const player = new RLottiePlayer({
el, el,
worker: this.workers[this.curWorkerNum++], worker: this.workers[this.curWorkerNum++],
options options
}); });
this.players[rlPlayer.reqId] = rlPlayer; const {reqId, cacheName} = player;
this.players[reqId] = player;
const playersByCacheName = cacheName ? this.playersByCacheName[cacheName] ??= new Set() : undefined;
if(cacheName) {
playersByCacheName.add(player);
}
if(this.curWorkerNum >= this.workers.length) { if(this.curWorkerNum >= this.workers.length) {
this.curWorkerNum = 0; this.curWorkerNum = 0;
} }
rlPlayer.loadFromData(options.animationData); player.addEventListener('destroy', () => {
this.onDestroy(reqId);
if(playersByCacheName.delete(player) && !playersByCacheName.size) {
delete this.playersByCacheName[cacheName];
}
});
return rlPlayer; player.loadFromData(options.animationData);
return player;
} }
} }

33
src/lib/rlottie/queryableWorker.ts

@ -4,12 +4,12 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import {IS_SAFARI} from '../../environment/userAgent'; import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
import EventListenerBase from '../../helpers/eventListenerBase'; import EventListenerBase from '../../helpers/eventListenerBase';
export default class QueryableWorker extends EventListenerBase<{ export default class QueryableWorker extends EventListenerBase<{
ready: () => void, ready: () => void,
frame: (reqId: number, frameNo: number, frame: Uint8ClampedArray) => void, frame: (reqId: number, frameNo: number, frame: Uint8ClampedArray | ImageBitmap) => void,
loaded: (reqId: number, frameCount: number, fps: number) => void, loaded: (reqId: number, frameCount: number, fps: number) => void,
error: (reqId: number, error: Error) => void, error: (reqId: number, error: Error) => void,
workerError: (error: ErrorEvent) => void workerError: (error: ErrorEvent) => void
@ -40,29 +40,10 @@ export default class QueryableWorker extends EventListenerBase<{
this.worker.terminate(); this.worker.terminate();
} }
public sendQuery(queryMethod: string, ...args: any[]) { public sendQuery(args: any[], transfer?: Transferable[]) {
if(IS_SAFARI) { this.worker.postMessage({
this.worker.postMessage({ queryMethod: args.shift(),
queryMethod: queryMethod, queryMethodArguments: args
queryMethodArguments: args }, CAN_USE_TRANSFERABLES ? transfer: undefined);
});
} else {
const transfer: Transferable[] = [];
args.forEach((arg) => {
if(arg instanceof ArrayBuffer) {
transfer.push(arg);
}
if(typeof(arg) === 'object' && arg.buffer instanceof ArrayBuffer) {
transfer.push(arg.buffer);
}
});
// console.log('transfer', transfer);
this.worker.postMessage({
queryMethod: queryMethod,
queryMethodArguments: args
}, transfer);
}
} }
} }

63
src/lib/rlottie/rlottie.worker.ts

@ -5,6 +5,7 @@
*/ */
import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables'; import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport';
import readBlobAsText from '../../helpers/blob/readBlobAsText'; import readBlobAsText from '../../helpers/blob/readBlobAsText';
import applyReplacements from './applyReplacements'; import applyReplacements from './applyReplacements';
@ -28,6 +29,8 @@ export class RLottieItem {
private dead: boolean; private dead: boolean;
// private context: OffscreenCanvasRenderingContext2D; // private context: OffscreenCanvasRenderingContext2D;
private imageData: ImageData;
constructor( constructor(
private reqId: number, private reqId: number,
private width: number, private width: number,
@ -62,10 +65,14 @@ export class RLottieItem {
worker.Api.resize(this.handle, this.width, this.height); worker.Api.resize(this.handle, this.width, this.height);
reply('loaded', this.reqId, this.frameCount, this.fps); reply(['loaded', this.reqId, this.frameCount, this.fps]);
if(IS_IMAGE_BITMAP_SUPPORTED) {
this.imageData = new ImageData(this.width, this.height);
}
} catch(e) { } catch(e) {
console.error('init RLottieItem error:', e); console.error('init RLottieItem error:', e);
reply('error', this.reqId, e); reply(['error', this.reqId, e]);
} }
} }
@ -84,19 +91,26 @@ export class RLottieItem {
const data = _Module.HEAPU8.subarray(bufferPointer, bufferPointer + (this.width * this.height * 4)); const data = _Module.HEAPU8.subarray(bufferPointer, bufferPointer + (this.width * this.height * 4));
if(!clamped) { if(this.imageData) {
clamped = new Uint8ClampedArray(data); this.imageData.data.set(data);
createImageBitmap(this.imageData).then((imageBitmap) => {
reply(['frame', this.reqId, frameNo, imageBitmap], [imageBitmap]);
});
} else { } else {
clamped.set(data); if(!clamped) {
} clamped = new Uint8ClampedArray(data);
} else {
clamped.set(data);
}
// this.context.putImageData(new ImageData(clamped, this.width, this.height), 0, 0); // this.context.putImageData(new ImageData(clamped, this.width, this.height), 0, 0);
reply('frame', this.reqId, frameNo, clamped); reply(['frame', this.reqId, frameNo, clamped], [clamped]);
}
} catch(e) { } catch(e) {
console.error('Render error:', e); console.error('Render error:', e);
this.dead = true; this.dead = true;
reply('error', this.reqId, e); reply(['error', this.reqId, e]);
} }
} }
@ -132,7 +146,7 @@ class RLottieWorker {
public init() { public init() {
this.initApi(); this.initApi();
reply('ready'); reply(['ready']);
} }
} }
@ -174,7 +188,7 @@ const queryableFunctions = {
item.init(json, frameRate); item.init(json, frameRate);
} catch(err) { } catch(err) {
console.error('Invalid file for sticker:', json); console.error('Invalid file for sticker:', json);
reply('error', reqId, err); reply(['error', reqId, err]);
} }
}); });
}, },
@ -193,31 +207,8 @@ const queryableFunctions = {
} }
}; };
function reply(...args: any[]) { function reply(args: any[], transfer?: Transferable[]) {
if(arguments.length < 1) { postMessage({queryMethodListener: args.shift(), queryMethodArguments: args}, CAN_USE_TRANSFERABLES ? transfer : undefined);
throw new TypeError('reply - not enough arguments');
}
// if(arguments[0] === 'frame') return;
args = Array.prototype.slice.call(arguments, 1);
if(!CAN_USE_TRANSFERABLES) {
postMessage({queryMethodListener: arguments[0], queryMethodArguments: args});
} else {
const transfer: ArrayBuffer[] = [];
for(let i = 0; i < args.length; ++i) {
if(args[i] instanceof ArrayBuffer) {
transfer.push(args[i]);
}
if(args[i].buffer && args[i].buffer instanceof ArrayBuffer) {
transfer.push(args[i].buffer);
}
}
postMessage({queryMethodListener: arguments[0], queryMethodArguments: args}, transfer);
}
} }
onmessage = function(e) { onmessage = function(e) {

244
src/lib/rlottie/rlottiePlayer.ts

@ -10,12 +10,12 @@ import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environmen
import EventListenerBase from '../../helpers/eventListenerBase'; import EventListenerBase from '../../helpers/eventListenerBase';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import clamp from '../../helpers/number/clamp'; import clamp from '../../helpers/number/clamp';
import lottieLoader from './lottieLoader';
import QueryableWorker from './queryableWorker'; import QueryableWorker from './queryableWorker';
import {AnimationItemGroup} from '../../components/animationIntersector'; import {AnimationItemGroup} from '../../components/animationIntersector';
import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport';
export type RLottieOptions = { export type RLottieOptions = {
container: HTMLElement, container: HTMLElement | HTMLElement[],
canvas?: HTMLCanvasElement, canvas?: HTMLCanvasElement,
autoplay?: boolean, autoplay?: boolean,
animationData: Blob, animationData: Blob,
@ -31,27 +31,58 @@ export type RLottieOptions = {
inverseColor?: RLottieColor, inverseColor?: RLottieColor,
name?: string, name?: string,
skipFirstFrameRendering?: boolean, skipFirstFrameRendering?: boolean,
toneIndex?: number toneIndex?: number,
sync?: boolean
}; };
type RLottieCacheMap = Map<number, Uint8ClampedArray>; type RLottieCacheMap = Map<number, Uint8ClampedArray>;
type RLottieCacheMapNew = Map<number, HTMLCanvasElement | ImageBitmap>;
type RLottieCacheMapURLs = Map<number, string>;
type RLottieCacheItem = {
frames: RLottieCacheMap,
framesNew: RLottieCacheMapNew,
framesURLs: RLottieCacheMapURLs,
clearCache: () => void,
counter: number
};
class RLottieCache { class RLottieCache {
private cache: Map<string, {frames: RLottieCacheMap, counter: number}>; private cache: Map<string, RLottieCacheItem>;
constructor() { constructor() {
this.cache = new Map(); this.cache = new Map();
} }
public static createCache(): RLottieCacheItem {
const cache: RLottieCacheItem = {
frames: new Map(),
framesNew: new Map(),
framesURLs: new Map(),
clearCache: () => {
cache.framesNew.forEach((value) => {
(value as ImageBitmap).close?.();
});
cache.frames.clear();
cache.framesNew.clear();
cache.framesURLs.clear();
},
counter: 0
};
return cache;
}
public getCache(name: string) { public getCache(name: string) {
let cache = this.cache.get(name); let cache = this.cache.get(name);
if(!cache) { if(!cache) {
this.cache.set(name, cache = {frames: new Map(), counter: 0}); this.cache.set(name, cache = RLottieCache.createCache());
} else { } else {
// console.warn('[RLottieCache] cache will be reused', cache); // console.warn('[RLottieCache] cache will be reused', cache);
} }
++cache.counter; ++cache.counter;
return cache.frames; return cache;
} }
public releaseCache(name: string) { public releaseCache(name: string) {
@ -90,6 +121,7 @@ export default class RLottiePlayer extends EventListenerBase<{
cached: () => void, cached: () => void,
destroy: () => void destroy: () => void
}> { }> {
public static CACHE = cache;
private static reqId = 0; private static reqId = 0;
public reqId = 0; public reqId = 0;
@ -97,8 +129,8 @@ export default class RLottiePlayer extends EventListenerBase<{
private frameCount: number; private frameCount: number;
private fps: number; private fps: number;
private skipDelta: number; private skipDelta: number;
private name: string; public name: string;
private cacheName: string; public cacheName: string;
private toneIndex: number; private toneIndex: number;
private worker: QueryableWorker; private worker: QueryableWorker;
@ -106,9 +138,9 @@ export default class RLottiePlayer extends EventListenerBase<{
private width = 0; private width = 0;
private height = 0; private height = 0;
public el: HTMLElement; public el: HTMLElement[];
public canvas: HTMLCanvasElement; public canvas: HTMLCanvasElement[];
private context: CanvasRenderingContext2D; private contexts: CanvasRenderingContext2D[];
public paused = true; public paused = true;
// public paused = false; // public paused = false;
@ -127,7 +159,7 @@ export default class RLottiePlayer extends EventListenerBase<{
// private caching = false; // private caching = false;
// private removed = false; // private removed = false;
private frames: RLottieCacheMap; private cache: RLottieCacheItem;
private imageData: ImageData; private imageData: ImageData;
public clamped: Uint8ClampedArray; public clamped: Uint8ClampedArray;
private cachingDelta = 0; private cachingDelta = 0;
@ -146,8 +178,11 @@ export default class RLottiePlayer extends EventListenerBase<{
private skipFirstFrameRendering: boolean; private skipFirstFrameRendering: boolean;
private playToFrameOnFrameCallback: (frameNo: number) => void; private playToFrameOnFrameCallback: (frameNo: number) => void;
public overrideRender: (frame: ImageData | HTMLCanvasElement | ImageBitmap) => void;
private renderedFirstFrame: boolean;
constructor({el, worker, options}: { constructor({el, worker, options}: {
el: HTMLElement, el: RLottiePlayer['el'],
worker: QueryableWorker, worker: QueryableWorker,
options: RLottieOptions options: RLottieOptions
}) { }) {
@ -175,6 +210,10 @@ export default class RLottiePlayer extends EventListenerBase<{
this.skipFirstFrameRendering = options.skipFirstFrameRendering; this.skipFirstFrameRendering = options.skipFirstFrameRendering;
this.toneIndex = options.toneIndex; this.toneIndex = options.toneIndex;
if(this.name) {
this.cacheName = cache.generateName(this.name, this.width, this.height, this.color, this.toneIndex);
}
// * Skip ratio (30fps) // * Skip ratio (30fps)
let skipRatio: number; let skipRatio: number;
if(options.skipRatio !== undefined) skipRatio = options.skipRatio; if(options.skipRatio !== undefined) skipRatio = options.skipRatio;
@ -187,33 +226,19 @@ export default class RLottiePlayer extends EventListenerBase<{
// options.needUpscale = true; // options.needUpscale = true;
// * Pixel ratio // * Pixel ratio
// const pixelRatio = window.devicePixelRatio; let pixelRatio = clamp(window.devicePixelRatio, 1, 2);
const pixelRatio = clamp(window.devicePixelRatio, 1, 2); if(pixelRatio > 1 && !options.needUpscale) {
if(pixelRatio > 1) { if(this.width > 100 && this.height > 100) {
// this.cachingEnabled = true;//this.width < 100 && this.height < 100; if(!IS_APPLE && mediaSizes.isMobile) {
if(options.needUpscale) { pixelRatio = 1;
this.width = Math.round(this.width * pixelRatio);
this.height = Math.round(this.height * pixelRatio);
} else if(pixelRatio > 1) {
if(this.width > 100 && this.height > 100) {
if(IS_APPLE || !mediaSizes.isMobile) {
/* this.width = Math.round(this.width * (pixelRatio - 1));
this.height = Math.round(this.height * (pixelRatio - 1)); */
this.width = Math.round(this.width * pixelRatio);
this.height = Math.round(this.height * pixelRatio);
} else if(pixelRatio > 2.5) {
this.width = Math.round(this.width * (pixelRatio - 1.5));
this.height = Math.round(this.height * (pixelRatio - 1.5));
}
} else {
this.width = Math.round(this.width * Math.max(1.5, pixelRatio - 1.5));
this.height = Math.round(this.height * Math.max(1.5, pixelRatio - 1.5));
} }
} else if(this.width > 60 && this.height > 60) {
pixelRatio = Math.max(1.5, pixelRatio - 1.5);
} }
} }
this.width = Math.round(this.width); this.width = Math.round(this.width * pixelRatio);
this.height = Math.round(this.height); this.height = Math.round(this.height * pixelRatio);
// options.noCache = true; // options.noCache = true;
@ -230,35 +255,36 @@ export default class RLottiePlayer extends EventListenerBase<{
} }
// this.cachingDelta = Infinity; // this.cachingDelta = Infinity;
// this.cachingDelta = 0;
// if(isApple) { // if(isApple) {
// this.cachingDelta = 0; //2 // 50% // this.cachingDelta = 0; //2 // 50%
// } // }
/* this.width *= 0.8;
this.height *= 0.8; */
// console.log("RLottiePlayer width:", this.width, this.height, options);
if(!this.canvas) { if(!this.canvas) {
this.canvas = document.createElement('canvas'); this.canvas = this.el.map(() => {
this.canvas.classList.add('rlottie'); const canvas = document.createElement('canvas');
this.canvas.width = this.width; canvas.classList.add('rlottie');
this.canvas.height = this.height; canvas.width = this.width;
this.canvas.dpr = pixelRatio; canvas.height = this.height;
canvas.dpr = pixelRatio;
return canvas;
});
} }
this.context = this.canvas.getContext('2d'); this.contexts = this.canvas.map((canvas) => canvas.getContext('2d'));
if(CAN_USE_TRANSFERABLES) { if(!IS_IMAGE_BITMAP_SUPPORTED) {
this.clamped = new Uint8ClampedArray(this.width * this.height * 4); this.imageData = new ImageData(this.width, this.height);
}
this.imageData = new ImageData(this.width, this.height); if(CAN_USE_TRANSFERABLES) {
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
}
}
if(this.name) { if(this.name) {
this.cacheName = cache.generateName(this.name, this.width, this.height, this.color, this.toneIndex); this.cache = cache.getCache(this.cacheName);
this.frames = cache.getCache(this.cacheName);
} else { } else {
this.frames = new Map(); this.cache = RLottieCache.createCache();
} }
} }
@ -267,20 +293,19 @@ export default class RLottiePlayer extends EventListenerBase<{
return; return;
} }
if(this.cacheName && cache.getCacheCounter(this.cacheName) > 1) { // skip clearing because same sticker can be still visible if(this.cacheName && this.cache.counter > 1) { // skip clearing because same sticker can be still visible
return; return;
} }
this.frames.clear(); this.cache.clearCache();
} }
public sendQuery(methodName: string, ...args: any[]) { public sendQuery(args: any[]) {
// console.trace('RLottie sendQuery:', methodName); this.worker.sendQuery([args.shift(), this.reqId, ...args]);
this.worker.sendQuery(methodName, this.reqId, ...args);
} }
public loadFromData(data: RLottieOptions['animationData']) { public loadFromData(data: RLottieOptions['animationData']) {
this.sendQuery('loadFromData', data, this.width, this.height, this.toneIndex/* , this.canvas.transferControlToOffscreen() */); this.sendQuery(['loadFromData', data, this.width, this.height, this.toneIndex/* , this.canvas.transferControlToOffscreen() */]);
} }
public play() { public play() {
@ -288,10 +313,6 @@ export default class RLottiePlayer extends EventListenerBase<{
return; return;
} }
// return;
// console.log('RLOTTIE PLAY' + this.reqId);
this.paused = false; this.paused = false;
this.setMainLoop(); this.setMainLoop();
} }
@ -352,13 +373,10 @@ export default class RLottiePlayer extends EventListenerBase<{
} }
public remove() { public remove() {
// alert('remove');
lottieLoader.onDestroy(this.reqId);
this.pause(); this.pause();
this.sendQuery('destroy'); this.sendQuery(['destroy']);
if(this.cacheName) cache.releaseCache(this.cacheName); if(this.cacheName) cache.releaseCache(this.cacheName);
this.dispatchEvent('destroy'); this.dispatchEvent('destroy');
// this.removed = true;
this.cleanup(); this.cleanup();
} }
@ -387,40 +405,73 @@ export default class RLottiePlayer extends EventListenerBase<{
} }
} }
public renderFrame2(frame: Uint8ClampedArray, frameNo: number) { public renderFrame2(frame: Uint8ClampedArray | HTMLCanvasElement | ImageBitmap, frameNo: number) {
/* this.setListenerResult('enterFrame', frameNo); /* this.setListenerResult('enterFrame', frameNo);
return; */ return; */
try { try {
if(this.color) { if(frame instanceof Uint8ClampedArray) {
this.applyColor(frame); if(this.color) {
} this.applyColor(frame);
}
if(this.inverseColor) { if(this.inverseColor) {
this.applyInversing(frame); this.applyInversing(frame);
} }
this.imageData.data.set(frame); this.imageData.data.set(frame);
}
// this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0); // this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0);
// let perf = performance.now(); this.contexts.forEach((context, idx) => {
this.context.putImageData(this.imageData, 0, 0); let cachedSource: HTMLCanvasElement | ImageBitmap = this.cache.framesNew.get(frameNo);
// console.log('renderFrame2 perf:', performance.now() - perf); if(!(frame instanceof Uint8ClampedArray)) {
cachedSource = frame;
} else if(idx > 0) {
cachedSource = this.canvas[0];
}
if(!cachedSource) {
// console.log('drawing from data');
const c = document.createElement('canvas');
c.width = context.canvas.width;
c.height = context.canvas.height;
c.getContext('2d').putImageData(this.imageData, 0, 0);
this.cache.framesNew.set(frameNo, c);
cachedSource = c;
}
if(this.overrideRender && this.renderedFirstFrame) {
this.overrideRender(cachedSource || this.imageData);
} else if(cachedSource) {
// console.log('drawing from canvas');
context.clearRect(0, 0, cachedSource.width, cachedSource.height);
context.drawImage(cachedSource, 0, 0);
} else {
context.putImageData(this.imageData, 0, 0);
}
if(!this.renderedFirstFrame) {
this.renderedFirstFrame = true;
}
});
this.dispatchEvent('enterFrame', frameNo);
} catch(err) { } catch(err) {
console.error('RLottiePlayer renderFrame error:', err/* , frame */, this.width, this.height); console.error('RLottiePlayer renderFrame error:', err/* , frame */, this.width, this.height);
this.autoplay = false; this.autoplay = false;
this.pause(); this.pause();
return;
} }
// console.log('set result enterFrame', frameNo);
this.dispatchEvent('enterFrame', frameNo);
} }
public renderFrame(frame: Uint8ClampedArray, frameNo: number) { public renderFrame(frame: Parameters<RLottiePlayer['renderFrame2']>[0], frameNo: number) {
// console.log('renderFrame', frameNo, this); const canCacheFrame = this.cachingDelta && (frameNo % this.cachingDelta || !frameNo);
if(this.cachingDelta && (frameNo % this.cachingDelta || !frameNo) && !this.frames.has(frameNo)) { if(canCacheFrame) {
this.frames.set(frameNo, new Uint8ClampedArray(frame));// frame; if(frame instanceof Uint8ClampedArray && !this.cache.frames.has(frameNo)) {
this.cache.frames.set(frameNo, new Uint8ClampedArray(frame));// frame;
} else if(IS_IMAGE_BITMAP_SUPPORTED && frame instanceof ImageBitmap && !this.cache.framesNew.has(frameNo)) {
this.cache.framesNew.set(frameNo, frame);
}
} }
/* if(!this.listenerResults.hasOwnProperty('cached')) { /* if(!this.listenerResults.hasOwnProperty('cached')) {
@ -434,14 +485,15 @@ export default class RLottiePlayer extends EventListenerBase<{
if(this.frInterval) { if(this.frInterval) {
const now = Date.now(), delta = now - this.frThen; const now = Date.now(), delta = now - this.frThen;
// console.log(`renderFrame delta${this.reqId}:`, this, delta, this.frInterval);
if(delta < 0) { if(delta < 0) {
const timeout = this.frInterval > -delta ? -delta % this.frInterval : this.frInterval;
if(this.rafId) clearTimeout(this.rafId); if(this.rafId) clearTimeout(this.rafId);
return this.rafId = window.setTimeout(() => { this.rafId = window.setTimeout(() => {
this.renderFrame2(frame, frameNo); this.renderFrame2(frame, frameNo);
}, this.frInterval > -delta ? -delta % this.frInterval : this.frInterval); }, timeout);
// await new Promise((resolve) => setTimeout(resolve, -delta % this.frInterval)); // await new Promise((resolve) => setTimeout(resolve, -delta % this.frInterval));
return;
} }
} }
@ -449,15 +501,18 @@ export default class RLottiePlayer extends EventListenerBase<{
} }
public requestFrame(frameNo: number) { public requestFrame(frameNo: number) {
const frame = this.frames.get(frameNo); const frame = this.cache.frames.get(frameNo);
if(frame) { const frameNew = this.cache.framesNew.get(frameNo);
if(frameNew) {
this.renderFrame(frameNew, frameNo);
} else if(frame) {
this.renderFrame(frame, frameNo); this.renderFrame(frame, frameNo);
} else { } else {
if(this.clamped && !this.clamped.length) { // fix detached if(this.clamped && !this.clamped.length) { // fix detached
this.clamped = new Uint8ClampedArray(this.width * this.height * 4); this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
} }
this.sendQuery('renderFrame', frameNo, this.clamped); this.sendQuery(['renderFrame', frameNo, this.clamped]);
} }
} }
@ -644,8 +699,8 @@ export default class RLottiePlayer extends EventListenerBase<{
this.addEventListener('enterFrame', () => { this.addEventListener('enterFrame', () => {
this.dispatchEvent('firstFrame'); this.dispatchEvent('firstFrame');
if(!this.canvas.parentNode && this.el) { if(!this.canvas[0].parentNode && this.el && !this.overrideRender) {
this.el.appendChild(this.canvas); this.el.forEach((container, idx) => container.append(this.canvas[idx]));
} }
// console.log('enterFrame firstFrame'); // console.log('enterFrame firstFrame');
@ -672,6 +727,7 @@ export default class RLottiePlayer extends EventListenerBase<{
}; };
this.addEventListener('enterFrame', this.frameListener); this.addEventListener('enterFrame', this.frameListener);
// setInterval(this.frameListener, this.frInterval);
// ! fix autoplaying since there will be no animationIntersector for it, // ! fix autoplaying since there will be no animationIntersector for it,
if(this.group === 'none' && this.autoplay) { if(this.group === 'none' && this.autoplay) {

3
src/pages/pageIm.ts

@ -43,7 +43,8 @@ const onFirstMount = () => {
return Promise.all([ return Promise.all([
loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */, loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */,
import('../lib/appManagers/appDialogsManager') import('../lib/appManagers/appDialogsManager')
]).then(() => { ]).then(([_, appDialogsManager]) => {
appDialogsManager.default.start();
setTimeout(() => { setTimeout(() => {
document.getElementById('auth-pages').remove(); document.getElementById('auth-pages').remove();
}, 1e3); }, 1e3);

4
src/scss/partials/_chat.scss

@ -1174,6 +1174,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
&-subtitle { &-subtitle {
color: var(--secondary-text-color) !important; color: var(--secondary-text-color) !important;
height: 1.125rem;
} }
.peer-title { .peer-title {
@ -1682,7 +1683,8 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
margin-bottom: 1rem; // .25rem is eaten by the last bubble's margin-bottom margin-bottom: 1rem; // .25rem is eaten by the last bubble's margin-bottom
} */ } */
&:not(.is-channel), &.is-chat { &:not(.is-channel),
&.is-chat {
.message { .message {
max-width: 480px; max-width: 480px;
} }

48
src/scss/partials/_chatBubble.scss

@ -608,11 +608,23 @@ $bubble-beside-button-width: 38px;
} }
&.emoji-big { &.emoji-big {
--emoji-size: 1rem;
font-size: 0; font-size: 0;
.bubble-content { .bubble-content {
line-height: 1; line-height: 1;
} }
.attachment {
--custom-emoji-size: var(--emoji-size);
img.emoji {
width: var(--emoji-size);
height: var(--emoji-size);
max-width: 64px;
max-height: 64px;
}
}
&:not(.sticker) { &:not(.sticker) {
.attachment { .attachment {
@ -620,6 +632,11 @@ $bubble-beside-button-width: 38px;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
//max-width: fit-content!important; //max-width: fit-content!important;
max-height: fit-content!important; max-height: fit-content!important;
display: block;
white-space: pre-wrap;
word-break: break-word;
font-size: var(--emoji-size);
span.emoji { span.emoji {
height: auto; height: auto;
@ -637,6 +654,10 @@ $bubble-beside-button-width: 38px;
.message { .message {
margin-top: -1.125rem; margin-top: -1.125rem;
} }
.bubble-content {
max-width: unquote('min(420px, 100%)');
}
} }
/* &.sticker .bubble-content { /* &.sticker .bubble-content {
@ -646,33 +667,6 @@ $bubble-beside-button-width: 38px;
} */ } */
} }
&.emoji-1x .attachment {
font-size: 96px;
img.emoji {
height: 64px;
width: 64px;
}
}
&.emoji-2x .attachment {
font-size: 64px;
img.emoji {
height: 48px;
width: 48px;
}
}
&.emoji-3x .attachment {
font-size: 52px;
img.emoji {
height: 32px;
width: 32px;
}
}
&.just-media { &.just-media {
.bubble-content { .bubble-content {
// cursor: pointer; // cursor: pointer;

1
src/scss/partials/_chatPinned.scss

@ -153,6 +153,7 @@
&-subtitle { &-subtitle {
font-size: var(--font-size-14); font-size: var(--font-size-14);
line-height: var(--line-height-14); line-height: var(--line-height-14);
position: relative; // ! WARNING (for custom emoji)
@include text-overflow(); @include text-overflow();
} }

1
src/scss/partials/_chatlist.scss

@ -479,6 +479,7 @@ ul.chatlist {
.user-title, .user-title,
.user-last-message { .user-last-message {
flex-grow: 1; flex-grow: 1;
position: relative; // * for custom emoji
i { i {
font-style: normal; font-style: normal;

54
src/scss/partials/_customEmoji.scss

@ -0,0 +1,54 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.custom-emoji {
display: inline;
width: var(--custom-emoji-size);
height: var(--custom-emoji-size);
min-height: var(--custom-emoji-size);
min-width: var(--custom-emoji-size);
position: relative;
// pointer-events: none;
&:before {
content: " ";
display: inline-block;
width: inherit;
height: inherit;
min-width: inherit;
min-height: inherit;
}
.media-sticker,
.rlottie {
width: inherit !important;
height: inherit !important;
}
.media-sticker.thumbnail {
margin: 0;
}
&-canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&-renderer {
pointer-events: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}

38
src/scss/partials/_spoiler.scss

@ -6,7 +6,7 @@
.spoiler { .spoiler {
--anim: .4s ease; --anim: .4s ease;
position: relative; // position: relative; // ! idk what it was for
background-color: var(--spoiler-background-color); background-color: var(--spoiler-background-color);
&-text { &-text {
@ -23,24 +23,28 @@
font-family: inherit !important; font-family: inherit !important;
} }
.message { .spoilers-container {
&.will-change { .custom-emoji-canvas {
.spoiler { z-index: -1;
// box-shadow: 0 0 var(--spoiler-background-color);
&-text {
filter: blur(6px);
}
}
} }
// &.will-change {
// .spoiler {
// // box-shadow: 0 0 var(--spoiler-background-color);
// &-text {
// filter: blur(6px);
// }
// }
// }
&.is-spoiler-visible { &.is-spoiler-visible {
&.animating { &.animating {
.spoiler { .spoiler {
transition: /* box-shadow var(--anim), */ background-color var(--anim); transition: /* box-shadow var(--anim), */ background-color var(--anim);
&-text { &-text {
transition: opacity var(--anim), filter var(--anim); transition: opacity var(--anim)/* , filter var(--anim) */;
} }
} }
} }
@ -51,17 +55,17 @@
// box-shadow: 0 0 30px 30px transparent; // box-shadow: 0 0 30px 30px transparent;
&-text { &-text {
filter: blur(0); // filter: blur(0);
opacity: 1; opacity: 1;
} }
} }
} }
&.backwards { // &.backwards {
.spoiler-text { // .spoiler-text {
filter: blur(3px); // filter: blur(3px);
} // }
} // }
} }
&:not(.is-spoiler-visible) { &:not(.is-spoiler-visible) {

8
src/scss/style.scss

@ -114,6 +114,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--call-button-size: 3.375rem; --call-button-size: 3.375rem;
--call-button-margin: 2rem; --call-button-margin: 2rem;
--custom-emoji-size: 1.125rem;
// https://github.com/overtake/TelegramSwift/blob/5cc7d2475fe4738a6aa0486c23eaf80a89d33b97/submodules/TGUIKit/TGUIKit/PresentationTheme.swift#L2054 // https://github.com/overtake/TelegramSwift/blob/5cc7d2475fe4738a6aa0486c23eaf80a89d33b97/submodules/TGUIKit/TGUIKit/PresentationTheme.swift#L2054
--peer-avatar-red-top: #ff885e; --peer-avatar-red-top: #ff885e;
--peer-avatar-red-bottom: #ff516a; --peer-avatar-red-bottom: #ff516a;
@ -362,6 +364,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/reaction"; @import "partials/reaction";
@import "partials/stackedAvatars"; @import "partials/stackedAvatars";
@import "partials/stickerViewer"; @import "partials/stickerViewer";
@import "partials/customEmoji";
@import "partials/popups/popup"; @import "partials/popups/popup";
@import "partials/popups/editAvatar"; @import "partials/popups/editAvatar";
@ -913,12 +916,13 @@ hr {
span.emoji { span.emoji {
display: inline !important; display: inline !important;
vertical-align: unset !important;
//line-height: 1em; //line-height: 1em;
//font-size: 1em; //font-size: 1em;
font-family: apple color emoji,segoe ui emoji,noto color emoji,android emoji,emojisymbols,emojione mozilla,twemoji mozilla,segoe ui symbol; font-family: apple color emoji,segoe ui emoji,noto color emoji,android emoji,emojisymbols,emojione mozilla,twemoji mozilla,segoe ui symbol;
vertical-align: unset !important;
line-height: 1 !important; line-height: 1 !important;
// vertical-align: text-top !important;
} }
// non-Retina device // non-Retina device

Loading…
Cancel
Save