From 4e9766a6ac6d9dbd5a568bfa5e13e8a64451a287 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sat, 3 Sep 2022 19:04:48 +0200 Subject: [PATCH] Custom emoji interaction effect --- src/components/chat/bubbles.ts | 22 ++- src/components/wrappers/sticker.ts | 220 ++++++++++++---------- src/lib/appManagers/appImManager.ts | 2 +- src/lib/richTextProcessor/wrapRichText.ts | 4 +- src/lib/rlottie/rlottiePlayer.ts | 26 ++- src/scss/partials/_chatBubble.scss | 12 +- 6 files changed, 161 insertions(+), 125 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 22d67d15..befcf855 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -25,7 +25,7 @@ import {IS_ANDROID, IS_APPLE, IS_MOBILE, IS_SAFARI} from '../../environment/user import I18n, {FormatterArguments, i18n, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n} from '../../lib/langPack'; import AvatarElement from '../avatar'; import ripple from '../ripple'; -import {wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, wrapGroupedDocuments} from '../wrappers'; +import {wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, wrapGroupedDocuments, wrapStickerAnimation} from '../wrappers'; import {MessageRender} from './messageRender'; import LazyLoadQueue from '../lazyLoadQueue'; import ListenerSetter from '../../helpers/listenerSetter'; @@ -97,7 +97,7 @@ import getPeerId from '../../lib/appManagers/utils/peers/getPeerId'; import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId'; import generateMessageId from '../../lib/appManagers/utils/messageId/generateMessageId'; import {AppManagers} from '../../lib/appManagers/managers'; -import {Awaited} from '../../types'; +import {Awaited, SendMessageEmojiInteractionData} from '../../types'; import idleController from '../../helpers/idleController'; import overlayCounter from '../../helpers/overlayCounter'; import {cancelContextMenuOpening} from '../../helpers/dom/attachContextMenuListener'; @@ -114,6 +114,11 @@ import isInDOM from '../../helpers/dom/isInDOM'; import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import attachStickerViewerListeners from '../stickerViewer'; import {makeMediaSize, MediaSize} from '../../helpers/mediaSize'; +import lottieLoader from '../../lib/rlottie/lottieLoader'; +import appDownloadManager from '../../lib/appManagers/appDownloadManager'; +import onMediaLoad from '../../helpers/onMediaLoad'; +import throttle from '../../helpers/schedulers/throttle'; +import {onEmojiStickerClick} from '../wrappers/sticker'; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Set = new Set([ @@ -1553,6 +1558,19 @@ export default class ChatBubbles { return; } + const stickerEmojiEl = findUpAttribute(target, 'data-sticker-emoji'); + if(stickerEmojiEl) { + onEmojiStickerClick({ + event: e, + container: stickerEmojiEl, + managers: this.managers, + middleware: this.getMiddleware(), + peerId: this.peerId + }); + + return; + } + const commentsDiv: HTMLElement = findUpClassName(target, 'replies'); if(commentsDiv) { const bubbleMid = +bubble.dataset.mid; diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index 52054313..1589c744 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -81,6 +81,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd }) { div = Array.isArray(div) ? div : [div]; + if(isCustomEmoji) { + emoji = doc.stickerEmojiRaw; + } + const stickerType = doc.sticker; if(stickerType === 1) { asStatic = true; @@ -101,6 +105,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd div.forEach((div) => { div.dataset.docId = '' + doc.id; + if(emoji) { + div.dataset.stickerEmoji = emoji; + } + div.classList.add('media-sticker-wrapper'); }); @@ -162,7 +170,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd await getCacheContext(fullThumb?.type); } - const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; + const toneIndex = emoji && !isCustomEmoji ? getEmojiToneIndex(emoji) : -1; const downloaded = cacheContext.downloaded && !needFadeIn; const isThumbNeededForType = isAnimated; @@ -357,7 +365,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd const animation = await lottieLoader.loadAnimationWorker({ container: (div as HTMLElement[])[0], - loop: loop && !emoji, + loop: loop && (!emoji || isCustomEmoji), autoplay: play, animationData: blob, width, @@ -427,109 +435,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd }, {once: true}); if(emoji) { - const data: SendMessageEmojiInteractionData = { - a: [], - v: 1 - }; - - let sendInteractionThrottled: () => void; - managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji); - - const container = (div as HTMLElement[])[0]; - attachClickEvent(container, async(e) => { - cancelEvent(e); - const animation = lottieLoader.getAnimation(container); - - if(animation.paused) { - const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji); - if(doc) { - const audio = document.createElement('audio'); - audio.style.display = 'none'; - container.parentElement.append(audio); - - try { - const url = await appDownloadManager.downloadMediaURL({media: doc}); - - audio.src = url; - audio.play(); - await onMediaLoad(audio, undefined, true); - - audio.addEventListener('ended', () => { - audio.src = ''; - audio.remove(); - }, {once: true}); - } catch(err) { - - } - } - - animation.autoplay = true; - animation.restart(); - } - - const peerId = appImManager.chat.peerId; - if(!peerId.isUser()) { - return; - } - - const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji, true); - if(!doc) { - return; - } - - const {animationDiv} = wrapStickerAnimation({ - doc, - middleware, - side: isOut ? 'right' : 'left', - size: 280, - target: container, - play: true, - withRandomOffset: true - }); - - if(isOut !== undefined && !isOut) { - animationDiv.classList.add('reflect-x'); - } - - if(!sendInteractionThrottled) { - sendInteractionThrottled = throttle(() => { - const length = data.a.length; - if(!length) { - return; - } - - const firstTime = data.a[0].t; - - data.a.forEach((a) => { - a.t = (a.t - firstTime) / 1000; - }); - - const bubble = findUpClassName(container, 'bubble'); - managers.appMessagesManager.setTyping(appImManager.chat.peerId, { - _: 'sendMessageEmojiInteraction', - msg_id: getServerMessageId(+bubble.dataset.mid), - emoticon: emoji, - interaction: { - _: 'dataJSON', - data: JSON.stringify(data) - } - }, true); - - data.a.length = 0; - }, 1000, false); - } - - // using a trick here: simulated event from interlocutor's interaction won't fire ours - if(e.isTrusted) { - data.a.push({ - i: 1, - t: Date.now() - }); - - sendInteractionThrottled(); - } - }); } return animation; @@ -722,3 +628,109 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut }); }); } + +export async function onEmojiStickerClick({event, container, managers, peerId, middleware}: { + event: Event, + container: HTMLElement, + managers: AppManagers, + peerId: PeerId, + middleware: () => boolean +}) { + if(!peerId.isUser()) { + return; + } + + cancelEvent(event); + + const bubble = findUpClassName(container, 'bubble'); + const emoji = container.dataset.stickerEmoji; + const data: SendMessageEmojiInteractionData = (container as any).emojiData ??= { + a: [], + v: 1 + }; + + const sendInteractionThrottled: () => void = (container as any).sendInteractionThrottled = throttle(() => { + const length = data.a.length; + if(!length) { + return; + } + + const firstTime = data.a[0].t; + + data.a.forEach((a) => { + a.t = (a.t - firstTime) / 1000; + }); + + const bubble = findUpClassName(container, 'bubble'); + managers.appMessagesManager.setTyping(appImManager.chat.peerId, { + _: 'sendMessageEmojiInteraction', + msg_id: getServerMessageId(+bubble.dataset.mid), + emoticon: emoji, + interaction: { + _: 'dataJSON', + data: JSON.stringify(data) + } + }, true); + + data.a.length = 0; + }, 1000, false); + + const animation = lottieLoader.getAnimation(container); + if(animation.paused) { + const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji); + if(doc) { + const audio = document.createElement('audio'); + audio.style.display = 'none'; + container.parentElement.append(audio); + + try { + const url = await appDownloadManager.downloadMediaURL({media: doc}); + + audio.src = url; + audio.play(); + await onMediaLoad(audio, undefined, true); + + audio.addEventListener('ended', () => { + audio.src = ''; + audio.remove(); + }, {once: true}); + } catch(err) { + + } + } + + animation.autoplay = true; + animation.restart(); + } + + const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji, true); + if(!doc) { + return; + } + + const isOut = bubble ? bubble.classList.contains('is-out') : undefined; + const {animationDiv} = wrapStickerAnimation({ + doc, + middleware, + side: isOut ? 'right' : 'left', + size: 280, + target: container, + play: true, + withRandomOffset: true + }); + + if(isOut !== undefined && !isOut) { + animationDiv.classList.add('reflect-x'); + } + + // using a trick here: simulated event from interlocutor's interaction won't fire ours + if(event.isTrusted) { + data.a.push({ + i: 1, + t: Date.now() + }); + + sendInteractionThrottled(); + } + // }); +} diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 67fc7d0b..c3978cbf 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -282,7 +282,7 @@ export class AppImManager extends EventListenerBase<{ if(typing?.action?._ === 'sendMessageEmojiInteraction') { const action = typing.action; const bubble = chat.bubbles.bubbles[generateMessageId(typing.action.msg_id)]; - if(bubble && bubble.classList.contains('emoji-big') && bubble.classList.contains('sticker') && getVisibleRect(bubble, chat.bubbles.scrollable.container)) { + if(bubble && bubble.classList.contains('emoji-big') && getVisibleRect(bubble, chat.bubbles.scrollable.container)) { const stickerWrapper: HTMLElement = bubble.querySelector('.media-sticker-wrapper:not(.bubble-hover-reaction-sticker):not(.reaction-sticker)'); const data: SendMessageEmojiInteractionData = JSON.parse(action.interaction.data); diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index 388c7fe9..77507171 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -23,7 +23,7 @@ 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 RLottiePlayer, {getLottiePixelRatio} from '../rlottie/rlottiePlayer'; import animationIntersector from '../../components/animationIntersector'; import type {MyDocument} from '../appManagers/appDocsManager'; import LazyLoadQueue from '../../components/lazyLoadQueue'; @@ -196,7 +196,7 @@ export class CustomEmojiRendererElement extends HTMLElement { public setDimensionsFromRect(rect: DOMRect) { const {canvas} = this; - const dpr = canvas.dpr ??= Math.min(2, window.devicePixelRatio); + const dpr = canvas.dpr ??= getLottiePixelRatio(rect.width, rect.height); canvas.width = Math.round(rect.width * dpr); canvas.height = Math.round(rect.height * dpr); } diff --git a/src/lib/rlottie/rlottiePlayer.ts b/src/lib/rlottie/rlottiePlayer.ts index 0637c283..839db264 100644 --- a/src/lib/rlottie/rlottiePlayer.ts +++ b/src/lib/rlottie/rlottiePlayer.ts @@ -114,6 +114,21 @@ const cache = new RLottieCache(); export type RLottieColor = [number, number, number]; +export function getLottiePixelRatio(width: number, height: number, needUpscale?: boolean) { + let pixelRatio = clamp(window.devicePixelRatio, 1, 2); + if(pixelRatio > 1 && !needUpscale) { + if(width > 90 && height > 90) { + if(!IS_APPLE && mediaSizes.isMobile) { + pixelRatio = 1; + } + } else if(width > 60 && height > 60) { + pixelRatio = Math.max(1.5, pixelRatio - 1.5); + } + } + + return pixelRatio; +} + export default class RLottiePlayer extends EventListenerBase<{ enterFrame: (frameNo: number) => void, ready: () => void, @@ -226,16 +241,7 @@ export default class RLottiePlayer extends EventListenerBase<{ // options.needUpscale = true; // * Pixel ratio - 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); - } - } + const pixelRatio = getLottiePixelRatio(this.width, this.height, options.needUpscale); this.width = Math.round(this.width * pixelRatio); this.height = Math.round(this.height * pixelRatio); diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 1875e635..34a8ce42 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -645,12 +645,12 @@ $bubble-beside-button-width: 38px; } } - .chat:not(.no-forwards) & { - .attachment { - cursor: text; - user-select: text; - } - } + // .chat:not(.no-forwards) & { + // .attachment { + // cursor: text; + // user-select: text; + // } + // } .message { margin-top: -1.125rem;