Browse Source

Custom emoji interaction effect

master
Eduard Kuzmenko 2 years ago committed by r4sas
parent
commit
4e9766a6ac
  1. 22
      src/components/chat/bubbles.ts
  2. 220
      src/components/wrappers/sticker.ts
  3. 2
      src/lib/appManagers/appImManager.ts
  4. 4
      src/lib/richTextProcessor/wrapRichText.ts
  5. 26
      src/lib/rlottie/rlottiePlayer.ts
  6. 12
      src/scss/partials/_chatBubble.scss

22
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 I18n, {FormatterArguments, i18n, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n} from '../../lib/langPack';
import AvatarElement from '../avatar'; import AvatarElement from '../avatar';
import ripple from '../ripple'; 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 {MessageRender} from './messageRender';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import ListenerSetter from '../../helpers/listenerSetter'; 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 getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
import generateMessageId from '../../lib/appManagers/utils/messageId/generateMessageId'; import generateMessageId from '../../lib/appManagers/utils/messageId/generateMessageId';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import {Awaited} from '../../types'; import {Awaited, SendMessageEmojiInteractionData} from '../../types';
import idleController from '../../helpers/idleController'; import idleController from '../../helpers/idleController';
import overlayCounter from '../../helpers/overlayCounter'; import overlayCounter from '../../helpers/overlayCounter';
import {cancelContextMenuOpening} from '../../helpers/dom/attachContextMenuListener'; 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 getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import attachStickerViewerListeners from '../stickerViewer'; import attachStickerViewerListeners from '../stickerViewer';
import {makeMediaSize, MediaSize} from '../../helpers/mediaSize'; 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 USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([ const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -1553,6 +1558,19 @@ export default class ChatBubbles {
return; 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'); const commentsDiv: HTMLElement = findUpClassName(target, 'replies');
if(commentsDiv) { if(commentsDiv) {
const bubbleMid = +bubble.dataset.mid; const bubbleMid = +bubble.dataset.mid;

220
src/components/wrappers/sticker.ts

@ -81,6 +81,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
}) { }) {
div = Array.isArray(div) ? div : [div]; div = Array.isArray(div) ? div : [div];
if(isCustomEmoji) {
emoji = doc.stickerEmojiRaw;
}
const stickerType = doc.sticker; const stickerType = doc.sticker;
if(stickerType === 1) { if(stickerType === 1) {
asStatic = true; asStatic = true;
@ -101,6 +105,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
div.forEach((div) => { div.forEach((div) => {
div.dataset.docId = '' + doc.id; div.dataset.docId = '' + doc.id;
if(emoji) {
div.dataset.stickerEmoji = emoji;
}
div.classList.add('media-sticker-wrapper'); div.classList.add('media-sticker-wrapper');
}); });
@ -162,7 +170,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
await getCacheContext(fullThumb?.type); await getCacheContext(fullThumb?.type);
} }
const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; const toneIndex = emoji && !isCustomEmoji ? getEmojiToneIndex(emoji) : -1;
const downloaded = cacheContext.downloaded && !needFadeIn; const downloaded = cacheContext.downloaded && !needFadeIn;
const isThumbNeededForType = isAnimated; const isThumbNeededForType = isAnimated;
@ -357,7 +365,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
const animation = await lottieLoader.loadAnimationWorker({ const animation = await lottieLoader.loadAnimationWorker({
container: (div as HTMLElement[])[0], container: (div as HTMLElement[])[0],
loop: loop && !emoji, loop: loop && (!emoji || isCustomEmoji),
autoplay: play, autoplay: play,
animationData: blob, animationData: blob,
width, width,
@ -427,109 +435,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
}, {once: true}); }, {once: true});
if(emoji) { if(emoji) {
const data: SendMessageEmojiInteractionData = {
a: [],
v: 1
};
let sendInteractionThrottled: () => void;
managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji); managers.appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji);
const container = (div as HTMLElement[])[0];
attachClickEvent(container, async(e) => {
cancelEvent(e);
const animation = lottieLoader.getAnimation(container);
if(animation.paused) {
const doc = await managers.appStickersManager.getAnimatedEmojiSoundDocument(emoji);
if(doc) {
const audio = document.createElement('audio');
audio.style.display = 'none';
container.parentElement.append(audio);
try {
const url = await appDownloadManager.downloadMediaURL({media: doc});
audio.src = url;
audio.play();
await onMediaLoad(audio, undefined, true);
audio.addEventListener('ended', () => {
audio.src = '';
audio.remove();
}, {once: true});
} catch(err) {
}
}
animation.autoplay = true;
animation.restart();
}
const peerId = appImManager.chat.peerId;
if(!peerId.isUser()) {
return;
}
const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji, true);
if(!doc) {
return;
}
const {animationDiv} = wrapStickerAnimation({
doc,
middleware,
side: isOut ? 'right' : 'left',
size: 280,
target: container,
play: true,
withRandomOffset: true
});
if(isOut !== undefined && !isOut) {
animationDiv.classList.add('reflect-x');
}
if(!sendInteractionThrottled) {
sendInteractionThrottled = throttle(() => {
const length = data.a.length;
if(!length) {
return;
}
const firstTime = data.a[0].t;
data.a.forEach((a) => {
a.t = (a.t - firstTime) / 1000;
});
const bubble = findUpClassName(container, 'bubble');
managers.appMessagesManager.setTyping(appImManager.chat.peerId, {
_: 'sendMessageEmojiInteraction',
msg_id: getServerMessageId(+bubble.dataset.mid),
emoticon: emoji,
interaction: {
_: 'dataJSON',
data: JSON.stringify(data)
}
}, true);
data.a.length = 0;
}, 1000, false);
}
// using a trick here: simulated event from interlocutor's interaction won't fire ours
if(e.isTrusted) {
data.a.push({
i: 1,
t: Date.now()
});
sendInteractionThrottled();
}
});
} }
return animation; return 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();
}
// });
}

2
src/lib/appManagers/appImManager.ts

@ -282,7 +282,7 @@ export class AppImManager extends EventListenerBase<{
if(typing?.action?._ === 'sendMessageEmojiInteraction') { if(typing?.action?._ === 'sendMessageEmojiInteraction') {
const action = typing.action; const action = typing.action;
const bubble = chat.bubbles.bubbles[generateMessageId(typing.action.msg_id)]; 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 stickerWrapper: HTMLElement = bubble.querySelector('.media-sticker-wrapper:not(.bubble-hover-reaction-sticker):not(.reaction-sticker)');
const data: SendMessageEmojiInteractionData = JSON.parse(action.interaction.data); const data: SendMessageEmojiInteractionData = JSON.parse(action.interaction.data);

4
src/lib/richTextProcessor/wrapRichText.ts

@ -23,7 +23,7 @@ import IS_CUSTOM_EMOJI_SUPPORTED from '../../environment/customEmojiSupport';
import rootScope from '../rootScope'; import rootScope from '../rootScope';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import {wrapSticker} from '../../components/wrappers'; import {wrapSticker} from '../../components/wrappers';
import RLottiePlayer from '../rlottie/rlottiePlayer'; import RLottiePlayer, {getLottiePixelRatio} from '../rlottie/rlottiePlayer';
import animationIntersector from '../../components/animationIntersector'; import animationIntersector from '../../components/animationIntersector';
import type {MyDocument} from '../appManagers/appDocsManager'; import type {MyDocument} from '../appManagers/appDocsManager';
import LazyLoadQueue from '../../components/lazyLoadQueue'; import LazyLoadQueue from '../../components/lazyLoadQueue';
@ -196,7 +196,7 @@ export class CustomEmojiRendererElement extends HTMLElement {
public setDimensionsFromRect(rect: DOMRect) { public setDimensionsFromRect(rect: DOMRect) {
const {canvas} = this; 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.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr); canvas.height = Math.round(rect.height * dpr);
} }

26
src/lib/rlottie/rlottiePlayer.ts

@ -114,6 +114,21 @@ const cache = new RLottieCache();
export type RLottieColor = [number, number, number]; 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<{ export default class RLottiePlayer extends EventListenerBase<{
enterFrame: (frameNo: number) => void, enterFrame: (frameNo: number) => void,
ready: () => void, ready: () => void,
@ -226,16 +241,7 @@ export default class RLottiePlayer extends EventListenerBase<{
// options.needUpscale = true; // options.needUpscale = true;
// * Pixel ratio // * Pixel ratio
let pixelRatio = clamp(window.devicePixelRatio, 1, 2); const pixelRatio = getLottiePixelRatio(this.width, this.height, options.needUpscale);
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 * pixelRatio); this.width = Math.round(this.width * pixelRatio);
this.height = Math.round(this.height * pixelRatio); this.height = Math.round(this.height * pixelRatio);

12
src/scss/partials/_chatBubble.scss

@ -645,12 +645,12 @@ $bubble-beside-button-width: 38px;
} }
} }
.chat:not(.no-forwards) & { // .chat:not(.no-forwards) & {
.attachment { // .attachment {
cursor: text; // cursor: text;
user-select: text; // user-select: text;
} // }
} // }
.message { .message {
margin-top: -1.125rem; margin-top: -1.125rem;

Loading…
Cancel
Save