From 62434a06f1b86790a722c71da88a3732ee09ac14 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 31 Aug 2022 06:22:16 +0200 Subject: [PATCH] Custom emoji --- src/components/animationIntersector.ts | 109 +++--- src/components/appMediaViewer.ts | 2 +- src/components/chat/bubbles.ts | 87 +++-- src/components/monkeys/tracking.ts | 10 +- src/components/wrappers/sticker.ts | 338 ++++++++-------- src/environment/customEmojiSupport.ts | 3 + src/environment/imageBitmapSupport.ts | 3 + src/environment/webAssemblySupport.ts | 3 + src/helpers/mediaSizes.ts | 9 +- src/helpers/preloadAnimatedEmojiSticker.ts | 2 +- src/helpers/sequentialDom.ts | 2 +- src/lib/appManagers/appDialogsManager.ts | 2 +- src/lib/appManagers/appDocsManager.ts | 3 +- src/lib/appManagers/appEmojiManager.ts | 63 +++ src/lib/appManagers/appImManager.ts | 12 +- src/lib/appManagers/appMessagesManager.ts | 6 +- src/lib/mtproto/referenceDatabase.ts | 11 +- src/lib/richTextProcessor/parseEntities.ts | 1 - src/lib/richTextProcessor/wrapRichText.ts | 424 ++++++++++++++++++++- src/lib/rlottie/lottieLoader.ts | 99 +++-- src/lib/rlottie/queryableWorker.ts | 33 +- src/lib/rlottie/rlottie.worker.ts | 63 ++- src/lib/rlottie/rlottiePlayer.ts | 244 +++++++----- src/pages/pageIm.ts | 3 +- src/scss/partials/_chat.scss | 4 +- src/scss/partials/_chatBubble.scss | 48 +-- src/scss/partials/_chatPinned.scss | 1 + src/scss/partials/_chatlist.scss | 1 + src/scss/partials/_customEmoji.scss | 54 +++ src/scss/partials/_spoiler.scss | 38 +- src/scss/style.scss | 8 +- 31 files changed, 1174 insertions(+), 512 deletions(-) create mode 100644 src/environment/customEmojiSupport.ts create mode 100644 src/environment/imageBitmapSupport.ts create mode 100644 src/environment/webAssemblySupport.ts create mode 100644 src/scss/partials/_customEmoji.scss diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index eb376cae..a1c0d815 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type {CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText'; import rootScope from '../lib/rootScope'; import {IS_SAFARI} from '../environment/userAgent'; import {MOUNT_CLASS_TO} from '../config/debug'; @@ -13,14 +14,15 @@ import indexOfAndSplice from '../helpers/array/indexOfAndSplice'; import forEachReverse from '../helpers/array/forEachReverse'; import idleController from '../helpers/idleController'; import appMediaPlaybackController from './appMediaPlaybackController'; +import {fastRaf} from '../helpers/schedulers'; export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' | '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 { el: HTMLElement, group: AnimationItemGroup, - animation: RLottiePlayer | HTMLVideoElement + animation: RLottiePlayer | HTMLVideoElement | CustomEmojiRendererElement }; export class AnimationIntersector { @@ -47,33 +49,35 @@ export class AnimationIntersector { continue; } - const player = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target); - if(player) { - if(entry.isIntersecting) { - this.visible.add(player); - this.checkAnimation(player, false); - - /* if(animation instanceof HTMLVideoElement && animation.dataset.src) { - animation.src = animation.dataset.src; - animation.load(); - } */ - } else { - this.visible.delete(player); - this.checkAnimation(player, true); - - const animation = player.animation; - if(animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) { - // console.warn('will clear cache', player); - animation.clearCache(); - }/* else if(animation instanceof HTMLVideoElement && animation.src) { - animation.dataset.src = animation.src; - animation.src = ''; - animation.load(); - } */ - } - - break; + const animation = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target); + if(!animation) { + continue; + } + + if(entry.isIntersecting) { + this.visible.add(animation); + this.checkAnimation(animation, false); + + /* if(animation instanceof HTMLVideoElement && animation.dataset.src) { + animation.src = animation.dataset.src; + animation.load(); + } */ + } else { + this.visible.delete(animation); + this.checkAnimation(animation, true); + + const _animation = animation.animation; + if(_animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) { + // console.warn('will clear cache', player); + _animation.clearCache(); + }/* else if(animation instanceof HTMLVideoElement && animation.src) { + animation.dataset.src = animation.src; + animation.src = ''; + animation.load(); + } */ } + + break; } } }); @@ -118,7 +122,6 @@ export class AnimationIntersector { } public removeAnimation(player: AnimationItem) { - // console.log('destroy animation'); const {el, animation} = player; animation.remove(); @@ -141,21 +144,21 @@ export class AnimationIntersector { this.visible.delete(player); } - public addAnimation(animation: RLottiePlayer | HTMLVideoElement, group: AnimationItemGroup = '') { - const player: AnimationItem = { - el: animation instanceof RLottiePlayer ? animation.el : animation, - animation: animation, + public addAnimation(_animation: AnimationItem['animation'], group: AnimationItemGroup = '') { + const animation: AnimationItem = { + el: _animation instanceof RLottiePlayer ? _animation.el[0] : (_animation instanceof HTMLVideoElement ? _animation : _animation.canvas), + animation: _animation, group }; - if(animation instanceof RLottiePlayer) { - if(!rootScope.settings.stickers.loop && animation.loop) { - animation.loop = rootScope.settings.stickers.loop; + if(_animation instanceof RLottiePlayer) { + if(!rootScope.settings.stickers.loop && _animation.loop) { + _animation.loop = rootScope.settings.stickers.loop; } } - (this.byGroups[group as AnimationItemGroup] ??= []).push(player); - this.observer.observe(player.el); + (this.byGroups[group as AnimationItemGroup] ??= []).push(animation); + this.observer.observe(animation.el); } public checkAnimations(blurred?: boolean, group?: AnimationItemGroup, destroy = false) { @@ -171,8 +174,8 @@ export class AnimationIntersector { for(const group of groups) { const animations = this.byGroups[group]; - forEachReverse(animations, (player) => { - this.checkAnimation(player, blurred, destroy); + forEachReverse(animations, (animation) => { + this.checkAnimation(animation, blurred, destroy); }); } } @@ -180,7 +183,7 @@ export class AnimationIntersector { public checkAnimation(player: AnimationItem, blurred = false, destroy = false) { const {el, animation, group} = player; // return; - if((destroy || (!isInDOM(el) && !this.lockedGroups[group]))/* && false */) { + if(destroy || (!this.lockedGroups[group] && !isInDOM(el))) { this.removeAnimation(player); return; } @@ -220,17 +223,19 @@ export class AnimationIntersector { public refreshGroup(group: AnimationItemGroup) { const animations = this.byGroups[group]; - if(animations && animations.length) { - animations.forEach((animation) => { - this.observer.unobserve(animation.el); - }); + if(!animations?.length) { + return; + } + + animations.forEach((animation) => { + this.observer.unobserve(animation.el); + }); - window.requestAnimationFrame(() => { - animations.forEach((animation) => { - this.observer.observe(animation.el); - }); + fastRaf(() => { + animations.forEach((animation) => { + this.observer.observe(animation.el); }); - } + }); } public lockIntersectionGroup(group: AnimationItemGroup) { @@ -244,7 +249,5 @@ export class AnimationIntersector { } const animationIntersector = new AnimationIntersector(); -if(MOUNT_CLASS_TO) { - MOUNT_CLASS_TO.animationIntersector = animationIntersector; -} +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.animationIntersector = animationIntersector); export default animationIntersector; diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 647806e8..dfdf6340 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -70,7 +70,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet this.content.main.prepend(stub); */ 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; const setCaptionTimeout = () => { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index f04298c6..bc4efb9a 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -113,6 +113,7 @@ import PopupPayment from '../popups/payment'; import isInDOM from '../../helpers/dom/isInDOM'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import attachStickerViewerListeners from '../stickerViewer'; +import {makeMediaSize, MediaSize} from '../../helpers/mediaSize'; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Set = new Set([ @@ -3434,7 +3435,7 @@ export default class ChatBubbles { const our = this.chat.isOurMessage(message); const messageDiv = document.createElement('div'); - messageDiv.classList.add('message'); + messageDiv.classList.add('message', 'spoilers-container'); const contentWrapper = document.createElement('div'); contentWrapper.classList.add('bubble-content-wrapper'); @@ -3526,54 +3527,68 @@ export default class ChatBubbles { } } - /* let richText = wrapRichText(messageMessage, { - entities: totalEntities - }); */ + let bigEmojis = 0, customEmojiSize: MediaSize; + 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, { entities: totalEntities, - passEntities: this.passEntities + passEntities: this.passEntities, + loadPromises, + lazyLoadQueue: this.lazyLoadQueue, + customEmojiSize }); let canHaveTail = true; let isStandaloneMedia = false; let needToSetHTML = true; - if(totalEntities && !messageMedia) { - const emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji'); - const strLength = messageMessage.length; - const emojiStrLength = emojiEntities.reduce((acc, curr) => acc + curr.length, 0); - - if(emojiStrLength === strLength && emojiEntities.length <= 3 && totalEntities.length === emojiEntities.length) { - if(rootScope.settings.emoji.big) { - const sticker = await this.managers.appStickersManager.getAnimatedEmojiSticker(messageMessage); - if(emojiEntities.length === 1 && !messageMedia && sticker) { - messageMedia = { - _: 'messageMediaDocument', - document: sticker - }; - } else { - const attachmentDiv = document.createElement('div'); - attachmentDiv.classList.add('attachment'); - - setInnerHTML(attachmentDiv, richText); + if(bigEmojis) { + if(rootScope.settings.emoji.big) { + const sticker = bigEmojis === 1 && + !totalEntities.find((entity) => entity._ === 'messageEntityCustomEmoji') && + await this.managers.appStickersManager.getAnimatedEmojiSticker(messageMessage); + if(bigEmojis === 1 && !messageMedia && sticker) { + messageMedia = { + _: 'messageMediaDocument', + document: sticker + }; + } else { + const attachmentDiv = document.createElement('div'); + attachmentDiv.classList.add('attachment', 'spoilers-container'); - bubble.classList.add('emoji-' + emojiEntities.length + 'x'); + setInnerHTML(attachmentDiv, richText); - bubbleContainer.append(attachmentDiv); - } - - bubble.classList.add('is-message-empty', 'emoji-big'); - isStandaloneMedia = true; - canHaveTail = false; - needToSetHTML = false; + bubbleContainer.append(attachmentDiv); } - bubble.classList.add('can-have-big-emoji'); + bubble.classList.add('is-message-empty', 'emoji-big'); + isStandaloneMedia = true; + canHaveTail = false; + needToSetHTML = false; } - /* if(strLength === emojiStrLength) { - messageDiv.classList.add('emoji-only'); - messageDiv.classList.add('message-empty'); - } */ + bubble.classList.add('can-have-big-emoji'); } if(needToSetHTML) { diff --git a/src/components/monkeys/tracking.ts b/src/components/monkeys/tracking.ts index 33953ede..96dcfb16 100644 --- a/src/components/monkeys/tracking.ts +++ b/src/components/monkeys/tracking.ts @@ -50,10 +50,10 @@ export default class TrackingMonkey { if(this.idleAnimation) { 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 { /* const cb = (frameNo: number) => { if(frameNo <= 1) { */ @@ -116,7 +116,7 @@ export default class TrackingMonkey { this.animation = _animation; if(!this.inputField.value.length) { - this.animation.canvas.style.display = 'none'; + this.animation.canvas[0].style.display = 'none'; } this.animation.addEventListener('enterFrame', currentFrame => { @@ -133,9 +133,9 @@ export default class TrackingMonkey { // animation.curFrame = 0; if(this.idleAnimation) { - this.idleAnimation.canvas.style.display = ''; + this.idleAnimation.canvas[0].style.display = ''; this.idleAnimation.play(); - this.animation.canvas.style.display = 'none'; + this.animation.canvas[0].style.display = 'none'; } } }); diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index 10614eff..52054313 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -19,6 +19,7 @@ import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb'; import makeError from '../../helpers/makeError'; import {makeMediaSize} from '../../helpers/mediaSize'; import mediaSizes from '../../helpers/mediaSizes'; +import noop from '../../helpers/noop'; import onMediaLoad from '../../helpers/onMediaLoad'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import throttle from '../../helpers/schedulers/throttle'; @@ -32,7 +33,6 @@ import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerM import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import lottieLoader from '../../lib/rlottie/lottieLoader'; -import RLottiePlayer from '../../lib/rlottie/rlottiePlayer'; import rootScope from '../../lib/rootScope'; import type {ThumbCache} from '../../lib/storages/thumbs'; import webpWorkerController from '../../lib/webp/webpWorkerController'; @@ -50,11 +50,13 @@ const EMOJI_EFFECT_MULTIPLIER = 3; 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, - div: HTMLElement, + div: HTMLElement | HTMLElement[], middleware?: () => boolean, + loadStickerMiddleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, + exportLoad?: boolean, group?: AnimationItemGroup, play?: boolean, onlyThumb?: boolean, @@ -74,8 +76,11 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, noPremium?: boolean, withLock?: boolean, relativeEffect?: boolean, - loopEffect?: boolean + loopEffect?: boolean, + isCustomEmoji?: boolean }) { + div = Array.isArray(div) ? div : [div]; + const stickerType = doc.sticker; if(stickerType === 1) { asStatic = true; @@ -94,13 +99,10 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, lottieLoader.loadLottieWorkers(); } - if(!stickerType) { - console.error('wrong doc for wrapSticker!', doc); - throw new Error('wrong doc for wrapSticker!'); - } - - div.dataset.docId = '' + doc.id; - div.classList.add('media-sticker-wrapper'); + div.forEach((div) => { + div.dataset.docId = '' + doc.id; + div.classList.add('media-sticker-wrapper'); + }); /* if(stickerType === 3) { const videoRes = wrapVideo({ @@ -141,14 +143,16 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, const effectThumb = getStickerEffectThumb(doc); if(isOut !== undefined && effectThumb && !isOut) { - div.classList.add('reflect-x'); + div.forEach((div) => div.classList.add('reflect-x')); } const willHaveLock = effectThumb && withLock; if(willHaveLock) { - div.classList.add('is-premium-sticker', 'tgico-premium_lock'); 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) { @@ -164,13 +168,14 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, const isThumbNeededForType = isAnimated; const lottieCachedThumb = stickerType === 2 || stickerType === 3 ? await managers.appDocsManager.getLottieCachedThumb(doc.id, toneIndex) : undefined; + const ret = {render: undefined as typeof loadPromise, load: undefined as typeof load}; let loadThumbPromise = deferredPromise(); let haveThumbCached = false; if(( doc.thumbs?.length || lottieCachedThumb ) && - !div.firstElementChild && ( + !div[0].firstElementChild && ( !downloaded || isThumbNeededForType || onlyThumb @@ -180,8 +185,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, // console.log('wrap sticker', thumb, div); - let thumbImage: HTMLImageElement | HTMLCanvasElement; - const afterRender = () => { + const afterRender = (div: HTMLElement, thumbImage: HTMLElement) => { if(!div.childElementCount) { thumbImage.classList.add('media-sticker', 'thumbnail'); @@ -193,105 +197,133 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, }; if('url' in thumb) { - thumbImage = new Image(); - renderImageFromUrl(thumbImage, thumb.url, afterRender); haveThumbCached = true; + div.forEach((div) => { + const thumbImage = new Image(); + renderImageFromUrl(thumbImage, (thumb as any).url, () => afterRender(div, thumbImage)); + }); } else if('bytes' in thumb) { if(thumb._ === 'photoPathSize') { - if(thumb.bytes.length) { - 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 { + if(!thumb.bytes.length) { thumb = doc.thumbs.find((t) => (t as PhotoSize.photoStrippedSize).bytes?.length) || thumb; } + + const d = getPathFromBytes((thumb as PhotoSize.photoStrippedSize).bytes); + const ns = 'http://www.w3.org/2000/svg'; + const svg = document.createElementNS(ns, 'svg'); + svg.classList.add('rlottie-vector', 'media-sticker', 'thumbnail'); + svg.setAttributeNS(null, 'viewBox', `0 0 ${doc.w || 512} ${doc.h || 512}`); + + // const defs = document.createElementNS(ns, 'defs'); + // const linearGradient = document.createElementNS(ns, 'linearGradient'); + // linearGradient.setAttributeNS(null, 'id', 'g'); + // linearGradient.setAttributeNS(null, 'x1', '-300%'); + // linearGradient.setAttributeNS(null, 'x2', '-200%'); + // linearGradient.setAttributeNS(null, 'y1', '0'); + // linearGradient.setAttributeNS(null, 'y2', '0'); + // const stops = [ + // ['-10%', '.1'], + // ['30%', '.07'], + // ['70%', '.07'], + // ['110%', '.1'] + // ].map(([offset, stopOpacity]) => { + // const stop = document.createElementNS(ns, 'stop'); + // stop.setAttributeNS(null, 'offset', offset); + // stop.setAttributeNS(null, 'stop-opacity', stopOpacity); + // return stop; + // }); + // const animates = [ + // ['-300%', '1200%'], + // ['-200%', '1300%'] + // ].map(([from, to], idx) => { + // const animate = document.createElementNS(ns, 'animate'); + // animate.setAttributeNS(null, 'attributeName', 'x' + (idx + 1)); + // animate.setAttributeNS(null, 'from', from); + // animate.setAttributeNS(null, 'to', to); + // animate.setAttributeNS(null, 'dur', '3s'); + // animate.setAttributeNS(null, 'repeatCount', 'indefinite'); + // return animate; + // }); + // linearGradient.append(...stops, ...animates); + // defs.append(linearGradient); + // svg.append(defs); + + const path = document.createElementNS(ns, 'path'); + path.setAttributeNS(null, 'd', d); + if(rootScope.settings.animationsEnabled && !isCustomEmoji) path.setAttributeNS(null, 'fill', 'url(#g)'); + svg.append(path); + div.forEach((div, idx) => div.append(idx > 0 ? svg.cloneNode(true) : svg)); + haveThumbCached = true; + loadThumbPromise.resolve(); } else if(toneIndex <= 0) { - 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 */) { - renderImageFromUrl(thumbImage, getPreviewURLFromThumb(doc, thumb, true), afterRender); haveThumbCached = true; + r(); } else { + haveThumbCached = true; webpWorkerController.convert('main-' + doc.id, thumb.bytes).then((bytes) => { managers.appDocsManager.saveWebPConvertedStrippedThumb(doc.id, bytes); (thumb as PhotoSize.photoStrippedSize).bytes = bytes; doc.pFlags.stickerThumbConverted = true; - if(middleware && !middleware()) return; - - if(!div.childElementCount) { - renderImageFromUrl(thumbImage, getPreviewURLFromThumb(doc, thumb as PhotoSize.photoStrippedSize, true), afterRender); + if((middleware && !middleware()) || (div as HTMLElement[])[0].childElementCount) { + loadThumbPromise.resolve(); + return; } - }).catch(() => {}); + + r(); + }).catch(() => loadThumbPromise.resolve()); } } } else if(((stickerType === 2 && toneIndex <= 0) || stickerType === 3) && (withThumb || onlyThumb)) { const load = async() => { - if(div.childElementCount || (middleware && !middleware())) return; + if((div as HTMLElement[])[0].childElementCount || (middleware && !middleware())) { + loadThumbPromise.resolve(); + return; + } - const r = () => { - if(div.childElementCount || (middleware && !middleware())) return; - renderImageFromUrl(thumbImage, cacheContext.url, afterRender); + const r = (div: HTMLElement, thumbImage: HTMLElement) => { + if(div.childElementCount || (middleware && !middleware())) { + loadThumbPromise.resolve(); + return; + } + + renderImageFromUrl(thumbImage, cacheContext.url, () => afterRender(div, thumbImage)); }; await getCacheContext(); - if(cacheContext.url) { - r(); - return; - } else { - const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true); - thumbImage = res.image; - res.loadPromise.then(r); - - // return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r); - } + (div as HTMLElement[]).forEach((div) => { + if(cacheContext.url) { + r(div, new Image()); + } else if('bytes' in thumb) { + const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true); + res.loadPromise.then(() => r(div, res.image)); + + // return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r); + } else { + appDownloadManager.downloadMediaURL({ + media: doc, + thumb: thumb as PhotoSize + }).then(async() => { + await getCacheContext(); + return r(div, new Image()); + }); + } + }); }; if(lazyLoadQueue && onlyThumb) { - lazyLoadQueue.push({div, load}); - return; + lazyLoadQueue.push({div: div[0], load}); + loadThumbPromise.resolve(); + return ret; } else { load(); @@ -307,7 +339,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, } if(onlyThumb/* || true */) { // for sticker panel - return; + return ret; } const middlewareError = makeError('MIDDLEWARE'); @@ -317,27 +349,14 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, } if(stickerType === 2 && !asStatic) { - /* if(doc.id === '1860749763008266301') { - 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}) + return appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId, thumb: fullThumb}) .then(async(blob) => { - // console.timeEnd('download sticker' + doc.id); - // console.log('loaded sticker:', doc, div/* , blob */); if(middleware && !middleware()) { throw middlewareError; } const animation = await lottieLoader.loadAnimationWorker({ - container: div, + container: (div as HTMLElement[])[0], loop: loop && !emoji, autoplay: play, animationData: blob, @@ -346,24 +365,25 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, name: 'doc' + doc.id, needUpscale, skipRatio, - toneIndex - }, group, middleware); + toneIndex, + sync: isCustomEmoji + }, group, loadStickerMiddleware ?? middleware); // const deferred = deferredPromise(); const setLockColor = willHaveLock ? () => { - const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas); - div.style.setProperty('--lock-url', `url(${lockUrl})`); + const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas[0]); + (div as HTMLElement[]).forEach((div) => div.style.setProperty('--lock-url', `url(${lockUrl})`)); } : undefined; - animation.addEventListener('firstFrame', () => { - const element = div.firstElementChild; + const onFirstFrame = (container: HTMLElement, canvas: HTMLCanvasElement) => { + const element = container.firstElementChild; if(needFadeIn !== false) { needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled; } const cb = () => { - if(element && element !== animation.canvas && element.tagName !== 'DIV') { + if(element && element !== canvas && element.tagName !== 'DIV') { element.remove(); } }; @@ -374,29 +394,36 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, } } else { sequentialDom.mutate(() => { - animation.canvas.classList.add('fade-in'); + canvas && canvas.classList.add('fade-in'); if(element) { element.classList.add('fade-out'); } - animation.canvas.addEventListener('animationend', () => { + (canvas || element).addEventListener('animationend', () => { sequentialDom.mutate(() => { - animation.canvas.classList.remove('fade-in'); + canvas && canvas.classList.remove('fade-in'); cb(); }); }, {once: true}); }); } + }; + animation.addEventListener('firstFrame', () => { + const canvas = animation.canvas[0]; if(withThumb !== false) { - saveLottiePreview(doc, animation.canvas, toneIndex); + saveLottiePreview(doc, canvas, toneIndex); } if(willHaveLock) { setLockColor(); } - // deferred.resolve(); + if(!isCustomEmoji) { + (div as HTMLElement[]).forEach((container, idx) => { + onFirstFrame(container, animation.canvas[idx]); + }); + } }, {once: true}); if(emoji) { @@ -409,16 +436,17 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji); - attachClickEvent(div, async(e) => { + const container = (div as HTMLElement[])[0]; + attachClickEvent(container, async(e) => { cancelEvent(e); - const animation = lottieLoader.getAnimation(div); + const animation = lottieLoader.getAnimation(container); if(animation.paused) { const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji); if(doc) { const audio = document.createElement('audio'); audio.style.display = 'none'; - div.parentElement.append(audio); + container.parentElement.append(audio); try { const url = await appDownloadManager.downloadMediaURL({media: doc}); @@ -455,7 +483,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, middleware, side: isOut ? 'right' : 'left', size: 280, - target: div, + target: container, play: true, withRandomOffset: true }); @@ -477,7 +505,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, a.t = (a.t - firstTime) / 1000; }); - const bubble = findUpClassName(div, 'bubble'); + const bubble = findUpClassName(container, 'bubble'); managers.appMessagesManager.setTyping(appImManager.chat.peerId, { _: 'sendMessageEmojiInteraction', msg_id: getServerMessageId(+bubble.dataset.mid), @@ -509,34 +537,29 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, // return deferred; // await new Promise((resolve) => setTimeout(resolve, 5e3)); }); - - // console.timeEnd('render sticker' + doc.id); } else if(asStatic || stickerType === 3) { - let media: HTMLElement; - if(asStatic) { - media = new Image(); - } else { - media = createVideo(); - (media as HTMLVideoElement).muted = true; - - if(play) { - (media as HTMLVideoElement).autoplay = true; + const media: HTMLElement[] = (div as HTMLElement[]).map(() => { + let media: HTMLElement; + if(asStatic) { + media = new Image(); + } else { + const video = media = createVideo(); + video.muted = true; + if(play) video.autoplay = true; + if(loop) video.loop = true; } - if(loop) { - (media as HTMLVideoElement).loop = true; - } - } + media.classList.add('media-sticker'); + 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) { - 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) { - media.classList.add('fade-in'); + media.forEach((media) => media.classList.add('fade-in')); } return new Promise(async(resolve, reject) => { @@ -546,7 +569,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, return; } - const onLoad = () => { + const onLoad = (div: HTMLElement, media: HTMLElement, thumbImage: HTMLElement) => { sequentialDom.mutateElement(div, () => { div.append(media); thumbImage && thumbImage.classList.add('fade-out'); @@ -579,19 +602,22 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, }; await getCacheContext(); - if(asStatic) { - renderImageFromUrl(media, cacheContext.url, onLoad); - } else { - (media as HTMLVideoElement).src = cacheContext.url; - onMediaLoad(media as HTMLVideoElement).then(onLoad); - } + media.forEach((media, idx) => { + const cb = () => onLoad((div as HTMLElement[])[idx], media, thumbImage[idx]); + if(asStatic) { + renderImageFromUrl(media, cacheContext.url, cb); + } else { + (media as HTMLVideoElement).src = cacheContext.url; + onMediaLoad(media as HTMLVideoElement).then(cb); + } + }); }; await getCacheContext(); if(cacheContext.url) r(); else { let promise: Promise; - if(stickerType === 2 && asStatic) { + if(stickerType !== 1 && asStatic) { const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize; // promise = managers.appDocsManager.getThumbURL(doc, thumb).promise promise = appDownloadManager.downloadMediaURL({media: doc, thumb, queueId: lazyLoadQueue?.queueId}); @@ -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> | void> = lazyLoadQueue && (!downloaded || isAnimated) ? - (lazyLoadQueue.push({div, load}), Promise.resolve()) : + (lazyLoadQueue.push({div: div[0], load}), Promise.resolve()) : load(); 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) { attachStickerEffectHandler({ - container: div, + container: div[0], doc, managers, 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}: { diff --git a/src/environment/customEmojiSupport.ts b/src/environment/customEmojiSupport.ts new file mode 100644 index 00000000..072e7ab2 --- /dev/null +++ b/src/environment/customEmojiSupport.ts @@ -0,0 +1,3 @@ +const IS_CUSTOM_EMOJI_SUPPORTED = true; + +export default IS_CUSTOM_EMOJI_SUPPORTED; diff --git a/src/environment/imageBitmapSupport.ts b/src/environment/imageBitmapSupport.ts new file mode 100644 index 00000000..c0570ec6 --- /dev/null +++ b/src/environment/imageBitmapSupport.ts @@ -0,0 +1,3 @@ +const IS_IMAGE_BITMAP_SUPPORTED = typeof(ImageBitmap) !== 'undefined'; + +export default IS_IMAGE_BITMAP_SUPPORTED; diff --git a/src/environment/webAssemblySupport.ts b/src/environment/webAssemblySupport.ts new file mode 100644 index 00000000..0fc7dbfe --- /dev/null +++ b/src/environment/webAssemblySupport.ts @@ -0,0 +1,3 @@ +const IS_WEB_ASSEMBLY_SUPPORTED = typeof(WebAssembly) !== 'undefined'; + +export default IS_WEB_ASSEMBLY_SUPPORTED; diff --git a/src/helpers/mediaSizes.ts b/src/helpers/mediaSizes.ts index f7130daf..ccf10153 100644 --- a/src/helpers/mediaSizes.ts +++ b/src/helpers/mediaSizes.ts @@ -19,7 +19,8 @@ type MediaTypeSizes = { poll: MediaSize, round: MediaSize, documentName: MediaSize, - invoice: MediaSize + invoice: MediaSize, + customEmoji: MediaSize }; export type MediaSizeType = keyof MediaTypeSizes; @@ -56,7 +57,8 @@ class MediaSizes extends EventListenerBase<{ poll: makeMediaSize(240, 0), round: makeMediaSize(200, 200), documentName: makeMediaSize(200, 0), - invoice: makeMediaSize(240, 240) + invoice: makeMediaSize(240, 240), + customEmoji: makeMediaSize(18, 18) }, desktop: { regular: makeMediaSize(420, 340), @@ -69,7 +71,8 @@ class MediaSizes extends EventListenerBase<{ poll: makeMediaSize(330, 0), round: makeMediaSize(280, 280), documentName: makeMediaSize(240, 0), - invoice: makeMediaSize(320, 260) + invoice: makeMediaSize(320, 260), + customEmoji: makeMediaSize(18, 18) } }; diff --git a/src/helpers/preloadAnimatedEmojiSticker.ts b/src/helpers/preloadAnimatedEmojiSticker.ts index f9497796..b4e627d4 100644 --- a/src/helpers/preloadAnimatedEmojiSticker.ts +++ b/src/helpers/preloadAnimatedEmojiSticker.ts @@ -33,7 +33,7 @@ export default function preloadAnimatedEmojiSticker(emoji: string, width?: numbe }, 'none'); animation.addEventListener('firstFrame', () => { - saveLottiePreview(doc, animation.canvas, toneIndex); + saveLottiePreview(doc, animation.canvas[0], toneIndex); animation.remove(); }, {once: true}); }); diff --git a/src/helpers/sequentialDom.ts b/src/helpers/sequentialDom.ts index aceff1cd..96de9b88 100644 --- a/src/helpers/sequentialDom.ts +++ b/src/helpers/sequentialDom.ts @@ -49,7 +49,7 @@ class SequentialDom { const promise = isConnected ? this.mutate() : Promise.resolve(); if(callback !== undefined) { - if(isConnected) { + if(!isConnected) { callback(); } else { promise.then(() => callback()); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 58193df7..bddad55d 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -234,7 +234,7 @@ export class AppDialogsManager { private managers: AppManagers; private selectTab: ReturnType; - constructor() { + public start() { const managers = this.managers = getProxiedManagers(); this.contextMenu = new DialogsContextMenu(managers); diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 0a67f8af..4e41580b 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -134,6 +134,7 @@ export class AppDocsManager extends AppManager { } break; + case 'documentAttributeCustomEmoji': case 'documentAttributeSticker': if(attribute.alt !== undefined) { doc.stickerEmojiRaw = attribute.alt; @@ -153,7 +154,7 @@ export class AppDocsManager extends AppManager { doc.sticker = 1; } else if(doc.mime_type === 'video/webm') { if(!getEnvironment().IS_WEBM_SUPPORTED) { - return; + break; } doc.type = 'sticker'; diff --git a/src/lib/appManagers/appEmojiManager.ts b/src/lib/appManagers/appEmojiManager.ts index 636c4090..29bef0d6 100644 --- a/src/lib/appManagers/appEmojiManager.ts +++ b/src/lib/appManagers/appEmojiManager.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type {MyDocument} from './appDocsManager'; import App from '../../config/app'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import isObject from '../../helpers/object/isObject'; @@ -13,6 +14,8 @@ import fixEmoji from '../richTextProcessor/fixEmoji'; import SearchIndex from '../searchIndex'; import stateStorage from '../stateStorage'; import {AppManager} from './manager'; +import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; +import pause from '../../helpers/schedulers/pause'; type EmojiLangPack = { keywords: { @@ -44,6 +47,9 @@ export class AppEmojiManager extends AppManager { private recent: string[]; private getRecentEmojisPromise: Promise; + private getCustomEmojiDocumentsPromise: Promise; + private getCustomEmojiDocumentPromises: Map> = new Map(); + /* public getPopularEmoji() { return stateStorage.get('emojis_popular').then((popEmojis) => { var result = [] @@ -230,4 +236,61 @@ export class AppEmojiManager extends AppManager { 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; + } } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index f3fa434d..67fc7d0b 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -25,7 +25,6 @@ import appNavigationController from '../../components/appNavigationController'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import I18n, {i18n, join, LangPackKey} from '../langPack'; import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction} from '../../layer'; -import {hslaStringToHex} from '../../helpers/color'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; import blurActiveElement from '../../helpers/dom/blurActiveElement'; @@ -92,15 +91,6 @@ import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount import findUpClassName from '../../helpers/dom/findUpClassName'; import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; 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'; @@ -380,7 +370,7 @@ export class AppImManager extends EventListenerBase<{ (window as any).onSpoilerClick = (e: MouseEvent) => { 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 isVisible = parentElement.classList.contains(className); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 9bd2ba17..1418b387 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2789,9 +2789,9 @@ export class AppMessagesManager extends AppManager { } } - if(isMessage && !unsupported && message.entities) { - unsupported = message.entities.some((entity) => entity._ === 'messageEntityCustomEmoji'); - } + // if(isMessage && !unsupported && message.entities) { + // unsupported = message.entities.some((entity) => entity._ === 'messageEntityCustomEmoji'); + // } if(isMessage && unsupported) { message.media = {_: 'messageMediaUnsupported'}; diff --git a/src/lib/mtproto/referenceDatabase.ts b/src/lib/mtproto/referenceDatabase.ts index db8d2050..194e51cd 100644 --- a/src/lib/mtproto/referenceDatabase.ts +++ b/src/lib/mtproto/referenceDatabase.ts @@ -11,7 +11,7 @@ import deepEqual from '../../helpers/object/deepEqual'; import {AppManager} from '../appManagers/manager'; 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 type referenceContextProfilePhoto = { type: 'profilePhoto', @@ -36,6 +36,11 @@ export namespace ReferenceContext { type: 'userFull', userId: UserId }; + + export type referenceContextCustomEmoji = { + type: 'customEmoji', + docId: DocId + }; } export type ReferenceBytes = Photo.photo['file_reference']; @@ -150,6 +155,10 @@ export class ReferenceDatabase extends AppManager { break; } + case 'customEmoji': { + promise = this.appEmojiManager.getCustomEmojiDocuments([context.docId]); + } + default: { this.log.warn('refreshReference: not implemented context', context); return Promise.reject(); diff --git a/src/lib/richTextProcessor/parseEntities.ts b/src/lib/richTextProcessor/parseEntities.ts index 2c0d9774..bfa9e12c 100644 --- a/src/lib/richTextProcessor/parseEntities.ts +++ b/src/lib/richTextProcessor/parseEntities.ts @@ -79,7 +79,6 @@ export default function parseEntities(text: string) { length: 1 }); } else if(match[8]) { // Emoji - // console.log('hit', match[8]); const unified = getEmojiUnified(match[8]); if(unified) { entities.push({ diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index a0ec4865..c0fa7a66 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -19,6 +19,237 @@ import setBlankToAnchor from './setBlankToAnchor'; import wrapUrl from './wrapUrl'; import EMOJI_VERSIONS_SUPPORTED from '../../environment/emojiVersionsSupport'; 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; + public clearedContainers: Set; + + 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 = 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) { + 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 = []; +const topFrames: Map[0]> = new Map(); +const lotties: Map | 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 = 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) @@ -46,7 +277,13 @@ export default function wrapRichText(text: string, options: Partial<{ text: string, lastEntity?: MessageEntity }, - voodoo?: boolean + voodoo?: boolean, + customEmojis?: {[docId: DocId]: CustomEmojiElement[]}, + loadPromises?: Promise[], + middleware?: () => boolean, + wrappingSpoiler?: boolean, + lazyLoadQueue?: LazyLoadQueue, + customEmojiSize?: MediaSize }> = {}) { const fragment = document.createDocumentFragment(); if(!text) { @@ -59,6 +296,8 @@ export default function wrapRichText(text: string, options: Partial<{ text }; + const customEmojis = options.customEmojis ??= {}; + const entities = options.entities ??= parseEntities(nasty.text); const passEntities = options.passEntities ??= {}; @@ -217,6 +456,25 @@ export default function wrapRichText(text: string, options: Partial<{ 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': { let isSupported = IS_EMOJI_SUPPORTED; if(isSupported) { @@ -287,7 +545,8 @@ export default function wrapRichText(text: string, options: Partial<{ if(nextEntity?._ === 'messageEntityUrl' && nextEntity.length === entity.length && nextEntity.offset === entity.offset) { - nasty.i++; + nasty.lastEntity = nextEntity; + ++nasty.i; } if(url !== fullEntityText) { @@ -389,6 +648,14 @@ export default function wrapRichText(text: string, options: Partial<{ const encoded = encodeSpoiler(nasty.text, entity); nasty.text = encoded.text; 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) { element = document.createElement('span'); element.style.fontFamily = 'spoiler'; @@ -464,6 +731,159 @@ export default function wrapRichText(text: string, options: Partial<{ (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[] = []; + const wrap = (doc: MyDocument, _loadPromises?: Promise[]): Promise> & {onRender?: () => void}> => { + const containers = customEmojis[doc.id]; + const isLottie = doc.sticker === 2; + + const loadPromises: Promise[] = []; + 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['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; } diff --git a/src/lib/rlottie/lottieLoader.ts b/src/lib/rlottie/lottieLoader.ts index f2c19d81..534ac6a4 100644 --- a/src/lib/rlottie/lottieLoader.ts +++ b/src/lib/rlottie/lottieLoader.ts @@ -11,8 +11,9 @@ import {logger, LogTypes} from '../logger'; import RLottiePlayer, {RLottieOptions} from './rlottiePlayer'; import QueryableWorker from './queryableWorker'; import blobConstruct from '../../helpers/blob/blobConstruct'; -import rootScope from '../rootScope'; 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' | 'TwoFactorSetupMonkeyClose' | 'TwoFactorSetupMonkeyCloseAndPeek' | @@ -21,12 +22,12 @@ export type LottieAssetName = 'EmptyFolder' | 'Folders_1' | 'Folders_2' | 'voice_outlined2' | 'voip_filled' | 'voice_mini'; export class LottieLoader { - private isWebAssemblySupported = typeof(WebAssembly) !== 'undefined'; - private loadPromise: Promise = !this.isWebAssemblySupported ? Promise.reject() : undefined; + private loadPromise: Promise = !IS_WEB_ASSEMBLY_SUPPORTED ? Promise.reject() : undefined; private loaded = false; private workersLimit = 4; private players: {[reqId: number]: RLottiePlayer} = {}; + private playersByCacheName: {[cacheName: string]: Set} = {}; private workers: QueryableWorker[] = []; private curWorkerNum = 0; @@ -35,7 +36,7 @@ export class LottieLoader { public getAnimation(element: HTMLElement) { for(const i in this.players) { - if(this.players[i].el === element) { + if(this.players[i].el.includes(element)) { return this.players[i]; } } @@ -91,7 +92,7 @@ export class LottieLoader { } public loadAnimationFromURL(params: Omit, url: string): Promise { - if(!this.isWebAssemblySupported) { + if(!IS_WEB_ASSEMBLY_SUPPORTED) { return this.loadPromise as any; } @@ -136,22 +137,30 @@ export class LottieLoader { group: AnimationItemGroup = params.group || '', middleware?: () => boolean ): Promise { - if(!this.isWebAssemblySupported) { + if(!IS_WEB_ASSEMBLY_SUPPORTED) { return this.loadPromise as any; } - // params.autoplay = true; if(!this.loaded) { await this.loadLottieWorkers(); } 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) { - params.width = parseInt(params.container.style.width); - params.height = parseInt(params.container.style.height); + params.width = parseInt(containers[0].style.width); + params.height = parseInt(containers[0].style.height); } if(!params.width || !params.height) { @@ -160,7 +169,7 @@ export class LottieLoader { params.group = group; - const player = this.initPlayer(params.container, params); + const player = this.initPlayer(containers, params); if(group !== 'none') { animationIntersector.addAnimation(player, group); @@ -170,42 +179,41 @@ export class LottieLoader { } private onPlayerLoaded = (reqId: number, frameCount: number, fps: number) => { - const rlPlayer = this.players[reqId]; - if(!rlPlayer) { + const player = this.players[reqId]; + if(!player) { this.log.warn('onPlayerLoaded on destroyed player:', reqId, frameCount); return; } this.log.debug('onPlayerLoaded'); - rlPlayer.onLoad(frameCount, fps); - // rlPlayer.addListener('firstFrame', () => { - // animationIntersector.addAnimation(player, group); - // }, true); + player.onLoad(frameCount, fps); }; - private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray) => { - const rlPlayer = this.players[reqId]; - if(!rlPlayer) { + private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray | ImageBitmap) => { + const player = this.players[reqId]; + if(!player) { this.log.warn('onFrame on destroyed player:', reqId, frameNo); return; } - if(rlPlayer.clamped !== undefined) { - rlPlayer.clamped = frame; + if(player.clamped !== undefined && frame instanceof Uint8ClampedArray) { + player.clamped = frame; } - rlPlayer.renderFrame(frame, frameNo); + player.renderFrame(frame, frameNo); }; private onPlayerError = (reqId: number, error: Error) => { - const rlPlayer = this.players[reqId]; - if(rlPlayer) { - // ! will need refactoring later, this is not the best way to remove the animation - const animations = animationIntersector.getAnimations(rlPlayer.el); - animations.forEach((animation) => { - animationIntersector.checkAnimation(animation, true, true); - }); + const player = this.players[reqId]; + if(!player) { + return; } + + // ! 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) { @@ -213,6 +221,10 @@ export class LottieLoader { } public destroyWorkers() { + if(!IS_WEB_ASSEMBLY_SUPPORTED) { + return; + } + this.workers.forEach((worker, idx) => { worker.terminate(); this.log('worker #' + idx + ' terminated'); @@ -220,23 +232,40 @@ export class LottieLoader { this.log('workers destroyed'); this.workers.length = 0; + this.curWorkerNum = 0; + this.loaded = false; + this.loadPromise = undefined; } - private initPlayer(el: HTMLElement, options: RLottieOptions) { - const rlPlayer = new RLottiePlayer({ + private initPlayer(el: RLottiePlayer['el'], options: RLottieOptions) { + const player = new RLottiePlayer({ el, worker: this.workers[this.curWorkerNum++], 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) { 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; } } diff --git a/src/lib/rlottie/queryableWorker.ts b/src/lib/rlottie/queryableWorker.ts index 3b700a86..2ce29e5f 100644 --- a/src/lib/rlottie/queryableWorker.ts +++ b/src/lib/rlottie/queryableWorker.ts @@ -4,12 +4,12 @@ * 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'; export default class QueryableWorker extends EventListenerBase<{ 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, error: (reqId: number, error: Error) => void, workerError: (error: ErrorEvent) => void @@ -40,29 +40,10 @@ export default class QueryableWorker extends EventListenerBase<{ this.worker.terminate(); } - public sendQuery(queryMethod: string, ...args: any[]) { - if(IS_SAFARI) { - this.worker.postMessage({ - queryMethod: queryMethod, - queryMethodArguments: args - }); - } 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); - } + public sendQuery(args: any[], transfer?: Transferable[]) { + this.worker.postMessage({ + queryMethod: args.shift(), + queryMethodArguments: args + }, CAN_USE_TRANSFERABLES ? transfer: undefined); } } diff --git a/src/lib/rlottie/rlottie.worker.ts b/src/lib/rlottie/rlottie.worker.ts index cfc40d40..f755c50e 100644 --- a/src/lib/rlottie/rlottie.worker.ts +++ b/src/lib/rlottie/rlottie.worker.ts @@ -5,6 +5,7 @@ */ import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables'; +import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport'; import readBlobAsText from '../../helpers/blob/readBlobAsText'; import applyReplacements from './applyReplacements'; @@ -28,6 +29,8 @@ export class RLottieItem { private dead: boolean; // private context: OffscreenCanvasRenderingContext2D; + private imageData: ImageData; + constructor( private reqId: number, private width: number, @@ -62,10 +65,14 @@ export class RLottieItem { 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) { 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)); - if(!clamped) { - clamped = new Uint8ClampedArray(data); + if(this.imageData) { + this.imageData.data.set(data); + createImageBitmap(this.imageData).then((imageBitmap) => { + reply(['frame', this.reqId, frameNo, imageBitmap], [imageBitmap]); + }); } 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) { console.error('Render error:', e); this.dead = true; - reply('error', this.reqId, e); + reply(['error', this.reqId, e]); } } @@ -132,7 +146,7 @@ class RLottieWorker { public init() { this.initApi(); - reply('ready'); + reply(['ready']); } } @@ -174,7 +188,7 @@ const queryableFunctions = { item.init(json, frameRate); } catch(err) { 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[]) { - if(arguments.length < 1) { - 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); - } +function reply(args: any[], transfer?: Transferable[]) { + postMessage({queryMethodListener: args.shift(), queryMethodArguments: args}, CAN_USE_TRANSFERABLES ? transfer : undefined); } onmessage = function(e) { diff --git a/src/lib/rlottie/rlottiePlayer.ts b/src/lib/rlottie/rlottiePlayer.ts index b7ca1537..0637c283 100644 --- a/src/lib/rlottie/rlottiePlayer.ts +++ b/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 mediaSizes from '../../helpers/mediaSizes'; import clamp from '../../helpers/number/clamp'; -import lottieLoader from './lottieLoader'; import QueryableWorker from './queryableWorker'; import {AnimationItemGroup} from '../../components/animationIntersector'; +import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport'; export type RLottieOptions = { - container: HTMLElement, + container: HTMLElement | HTMLElement[], canvas?: HTMLCanvasElement, autoplay?: boolean, animationData: Blob, @@ -31,27 +31,58 @@ export type RLottieOptions = { inverseColor?: RLottieColor, name?: string, skipFirstFrameRendering?: boolean, - toneIndex?: number + toneIndex?: number, + sync?: boolean }; type RLottieCacheMap = Map; +type RLottieCacheMapNew = Map; +type RLottieCacheMapURLs = Map; +type RLottieCacheItem = { + frames: RLottieCacheMap, + framesNew: RLottieCacheMapNew, + framesURLs: RLottieCacheMapURLs, + clearCache: () => void, + counter: number +}; + class RLottieCache { - private cache: Map; + private cache: Map; constructor() { 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) { let cache = this.cache.get(name); if(!cache) { - this.cache.set(name, cache = {frames: new Map(), counter: 0}); + this.cache.set(name, cache = RLottieCache.createCache()); } else { // console.warn('[RLottieCache] cache will be reused', cache); } ++cache.counter; - return cache.frames; + return cache; } public releaseCache(name: string) { @@ -90,6 +121,7 @@ export default class RLottiePlayer extends EventListenerBase<{ cached: () => void, destroy: () => void }> { + public static CACHE = cache; private static reqId = 0; public reqId = 0; @@ -97,8 +129,8 @@ export default class RLottiePlayer extends EventListenerBase<{ private frameCount: number; private fps: number; private skipDelta: number; - private name: string; - private cacheName: string; + public name: string; + public cacheName: string; private toneIndex: number; private worker: QueryableWorker; @@ -106,9 +138,9 @@ export default class RLottiePlayer extends EventListenerBase<{ private width = 0; private height = 0; - public el: HTMLElement; - public canvas: HTMLCanvasElement; - private context: CanvasRenderingContext2D; + public el: HTMLElement[]; + public canvas: HTMLCanvasElement[]; + private contexts: CanvasRenderingContext2D[]; public paused = true; // public paused = false; @@ -127,7 +159,7 @@ export default class RLottiePlayer extends EventListenerBase<{ // private caching = false; // private removed = false; - private frames: RLottieCacheMap; + private cache: RLottieCacheItem; private imageData: ImageData; public clamped: Uint8ClampedArray; private cachingDelta = 0; @@ -146,8 +178,11 @@ export default class RLottiePlayer extends EventListenerBase<{ private skipFirstFrameRendering: boolean; private playToFrameOnFrameCallback: (frameNo: number) => void; + public overrideRender: (frame: ImageData | HTMLCanvasElement | ImageBitmap) => void; + private renderedFirstFrame: boolean; + constructor({el, worker, options}: { - el: HTMLElement, + el: RLottiePlayer['el'], worker: QueryableWorker, options: RLottieOptions }) { @@ -175,6 +210,10 @@ export default class RLottiePlayer extends EventListenerBase<{ this.skipFirstFrameRendering = options.skipFirstFrameRendering; this.toneIndex = options.toneIndex; + if(this.name) { + this.cacheName = cache.generateName(this.name, this.width, this.height, this.color, this.toneIndex); + } + // * Skip ratio (30fps) let skipRatio: number; if(options.skipRatio !== undefined) skipRatio = options.skipRatio; @@ -187,33 +226,19 @@ export default class RLottiePlayer extends EventListenerBase<{ // options.needUpscale = true; // * Pixel ratio - // const pixelRatio = window.devicePixelRatio; - const pixelRatio = clamp(window.devicePixelRatio, 1, 2); - if(pixelRatio > 1) { - // this.cachingEnabled = true;//this.width < 100 && this.height < 100; - if(options.needUpscale) { - 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)); + let pixelRatio = clamp(window.devicePixelRatio, 1, 2); + if(pixelRatio > 1 && !options.needUpscale) { + if(this.width > 100 && this.height > 100) { + if(!IS_APPLE && mediaSizes.isMobile) { + pixelRatio = 1; } + } else if(this.width > 60 && this.height > 60) { + pixelRatio = Math.max(1.5, pixelRatio - 1.5); } } - this.width = Math.round(this.width); - this.height = Math.round(this.height); + this.width = Math.round(this.width * pixelRatio); + this.height = Math.round(this.height * pixelRatio); // options.noCache = true; @@ -230,35 +255,36 @@ export default class RLottiePlayer extends EventListenerBase<{ } // this.cachingDelta = Infinity; + // this.cachingDelta = 0; // if(isApple) { // 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) { - this.canvas = document.createElement('canvas'); - this.canvas.classList.add('rlottie'); - this.canvas.width = this.width; - this.canvas.height = this.height; - this.canvas.dpr = pixelRatio; + this.canvas = this.el.map(() => { + const canvas = document.createElement('canvas'); + canvas.classList.add('rlottie'); + canvas.width = this.width; + 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) { - this.clamped = new Uint8ClampedArray(this.width * this.height * 4); - } + if(!IS_IMAGE_BITMAP_SUPPORTED) { + 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) { - this.cacheName = cache.generateName(this.name, this.width, this.height, this.color, this.toneIndex); - this.frames = cache.getCache(this.cacheName); + this.cache = cache.getCache(this.cacheName); } else { - this.frames = new Map(); + this.cache = RLottieCache.createCache(); } } @@ -267,20 +293,19 @@ export default class RLottiePlayer extends EventListenerBase<{ 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; } - this.frames.clear(); + this.cache.clearCache(); } - public sendQuery(methodName: string, ...args: any[]) { - // console.trace('RLottie sendQuery:', methodName); - this.worker.sendQuery(methodName, this.reqId, ...args); + public sendQuery(args: any[]) { + this.worker.sendQuery([args.shift(), this.reqId, ...args]); } 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() { @@ -288,10 +313,6 @@ export default class RLottiePlayer extends EventListenerBase<{ return; } - // return; - - // console.log('RLOTTIE PLAY' + this.reqId); - this.paused = false; this.setMainLoop(); } @@ -352,13 +373,10 @@ export default class RLottiePlayer extends EventListenerBase<{ } public remove() { - // alert('remove'); - lottieLoader.onDestroy(this.reqId); this.pause(); - this.sendQuery('destroy'); + this.sendQuery(['destroy']); if(this.cacheName) cache.releaseCache(this.cacheName); this.dispatchEvent('destroy'); - // this.removed = true; 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); return; */ try { - if(this.color) { - this.applyColor(frame); - } + if(frame instanceof Uint8ClampedArray) { + if(this.color) { + this.applyColor(frame); + } - if(this.inverseColor) { - this.applyInversing(frame); - } + if(this.inverseColor) { + 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); - // let perf = performance.now(); - this.context.putImageData(this.imageData, 0, 0); - // console.log('renderFrame2 perf:', performance.now() - perf); + this.contexts.forEach((context, idx) => { + let cachedSource: HTMLCanvasElement | ImageBitmap = this.cache.framesNew.get(frameNo); + 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) { console.error('RLottiePlayer renderFrame error:', err/* , frame */, this.width, this.height); this.autoplay = false; this.pause(); - return; } - - // console.log('set result enterFrame', frameNo); - this.dispatchEvent('enterFrame', frameNo); } - public renderFrame(frame: Uint8ClampedArray, frameNo: number) { - // console.log('renderFrame', frameNo, this); - if(this.cachingDelta && (frameNo % this.cachingDelta || !frameNo) && !this.frames.has(frameNo)) { - this.frames.set(frameNo, new Uint8ClampedArray(frame));// frame; + public renderFrame(frame: Parameters[0], frameNo: number) { + const canCacheFrame = this.cachingDelta && (frameNo % this.cachingDelta || !frameNo); + if(canCacheFrame) { + 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')) { @@ -434,14 +485,15 @@ export default class RLottiePlayer extends EventListenerBase<{ if(this.frInterval) { const now = Date.now(), delta = now - this.frThen; - // console.log(`renderFrame delta${this.reqId}:`, this, delta, this.frInterval); if(delta < 0) { + const timeout = this.frInterval > -delta ? -delta % this.frInterval : this.frInterval; if(this.rafId) clearTimeout(this.rafId); - return this.rafId = window.setTimeout(() => { + this.rafId = window.setTimeout(() => { this.renderFrame2(frame, frameNo); - }, this.frInterval > -delta ? -delta % this.frInterval : this.frInterval); + }, timeout); // await new Promise((resolve) => setTimeout(resolve, -delta % this.frInterval)); + return; } } @@ -449,15 +501,18 @@ export default class RLottiePlayer extends EventListenerBase<{ } public requestFrame(frameNo: number) { - const frame = this.frames.get(frameNo); - if(frame) { + const frame = this.cache.frames.get(frameNo); + const frameNew = this.cache.framesNew.get(frameNo); + if(frameNew) { + this.renderFrame(frameNew, frameNo); + } else if(frame) { this.renderFrame(frame, frameNo); } else { if(this.clamped && !this.clamped.length) { // fix detached 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.dispatchEvent('firstFrame'); - if(!this.canvas.parentNode && this.el) { - this.el.appendChild(this.canvas); + if(!this.canvas[0].parentNode && this.el && !this.overrideRender) { + this.el.forEach((container, idx) => container.append(this.canvas[idx])); } // console.log('enterFrame firstFrame'); @@ -672,6 +727,7 @@ export default class RLottiePlayer extends EventListenerBase<{ }; this.addEventListener('enterFrame', this.frameListener); + // setInterval(this.frameListener, this.frInterval); // ! fix autoplaying since there will be no animationIntersector for it, if(this.group === 'none' && this.autoplay) { diff --git a/src/pages/pageIm.ts b/src/pages/pageIm.ts index 36edd364..dd404625 100644 --- a/src/pages/pageIm.ts +++ b/src/pages/pageIm.ts @@ -43,7 +43,8 @@ const onFirstMount = () => { return Promise.all([ loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */, import('../lib/appManagers/appDialogsManager') - ]).then(() => { + ]).then(([_, appDialogsManager]) => { + appDialogsManager.default.start(); setTimeout(() => { document.getElementById('auth-pages').remove(); }, 1e3); diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index c146e902..b891d6de 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -1174,6 +1174,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi &-subtitle { color: var(--secondary-text-color) !important; + height: 1.125rem; } .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 } */ - &:not(.is-channel), &.is-chat { + &:not(.is-channel), + &.is-chat { .message { max-width: 480px; } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 9ce02602..2374d149 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -608,11 +608,23 @@ $bubble-beside-button-width: 38px; } &.emoji-big { + --emoji-size: 1rem; font-size: 0; .bubble-content { 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) { .attachment { @@ -620,6 +632,11 @@ $bubble-beside-button-width: 38px; padding-bottom: 1.5rem; //max-width: 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 { height: auto; @@ -637,6 +654,10 @@ $bubble-beside-button-width: 38px; .message { margin-top: -1.125rem; } + + .bubble-content { + max-width: unquote('min(420px, 100%)'); + } } /* &.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 { .bubble-content { // cursor: pointer; diff --git a/src/scss/partials/_chatPinned.scss b/src/scss/partials/_chatPinned.scss index 8d26c47e..b5ad2422 100644 --- a/src/scss/partials/_chatPinned.scss +++ b/src/scss/partials/_chatPinned.scss @@ -153,6 +153,7 @@ &-subtitle { font-size: var(--font-size-14); line-height: var(--line-height-14); + position: relative; // ! WARNING (for custom emoji) @include text-overflow(); } diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 7ccb6236..8fd47276 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -479,6 +479,7 @@ ul.chatlist { .user-title, .user-last-message { flex-grow: 1; + position: relative; // * for custom emoji i { font-style: normal; diff --git a/src/scss/partials/_customEmoji.scss b/src/scss/partials/_customEmoji.scss new file mode 100644 index 00000000..e10d61d3 --- /dev/null +++ b/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; + } +} diff --git a/src/scss/partials/_spoiler.scss b/src/scss/partials/_spoiler.scss index 38b4b79d..a1b11450 100644 --- a/src/scss/partials/_spoiler.scss +++ b/src/scss/partials/_spoiler.scss @@ -6,7 +6,7 @@ .spoiler { --anim: .4s ease; - position: relative; + // position: relative; // ! idk what it was for background-color: var(--spoiler-background-color); &-text { @@ -23,24 +23,28 @@ font-family: inherit !important; } -.message { - &.will-change { - .spoiler { - // box-shadow: 0 0 var(--spoiler-background-color); - - &-text { - filter: blur(6px); - } - } +.spoilers-container { + .custom-emoji-canvas { + z-index: -1; } + // &.will-change { + // .spoiler { + // // box-shadow: 0 0 var(--spoiler-background-color); + + // &-text { + // filter: blur(6px); + // } + // } + // } + &.is-spoiler-visible { &.animating { .spoiler { transition: /* box-shadow var(--anim), */ background-color var(--anim); &-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; &-text { - filter: blur(0); + // filter: blur(0); opacity: 1; } } } - &.backwards { - .spoiler-text { - filter: blur(3px); - } - } + // &.backwards { + // .spoiler-text { + // filter: blur(3px); + // } + // } } &:not(.is-spoiler-visible) { diff --git a/src/scss/style.scss b/src/scss/style.scss index ce9eda04..8602980e 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -114,6 +114,8 @@ $chat-input-inner-padding-handhelds: .25rem; --call-button-size: 3.375rem; --call-button-margin: 2rem; + --custom-emoji-size: 1.125rem; + // https://github.com/overtake/TelegramSwift/blob/5cc7d2475fe4738a6aa0486c23eaf80a89d33b97/submodules/TGUIKit/TGUIKit/PresentationTheme.swift#L2054 --peer-avatar-red-top: #ff885e; --peer-avatar-red-bottom: #ff516a; @@ -362,6 +364,7 @@ $chat-input-inner-padding-handhelds: .25rem; @import "partials/reaction"; @import "partials/stackedAvatars"; @import "partials/stickerViewer"; +@import "partials/customEmoji"; @import "partials/popups/popup"; @import "partials/popups/editAvatar"; @@ -913,12 +916,13 @@ hr { span.emoji { display: inline !important; - vertical-align: unset !important; //line-height: 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; + vertical-align: unset !important; line-height: 1 !important; + // vertical-align: text-top !important; } // non-Retina device