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 @@ -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'; @@ -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'; @@ -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<Message.messageService['action']['_']> = new Set([
@ -1553,6 +1558,19 @@ export default class ChatBubbles { @@ -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;

220
src/components/wrappers/sticker.ts

@ -81,6 +81,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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<{ @@ -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);

4
src/lib/richTextProcessor/wrapRichText.ts

@ -23,7 +23,7 @@ import IS_CUSTOM_EMOJI_SUPPORTED from '../../environment/customEmojiSupport'; @@ -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 { @@ -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);
}

26
src/lib/rlottie/rlottiePlayer.ts

@ -114,6 +114,21 @@ const cache = new RLottieCache(); @@ -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<{ @@ -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);

12
src/scss/partials/_chatBubble.scss

@ -645,12 +645,12 @@ $bubble-beside-button-width: 38px; @@ -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;

Loading…
Cancel
Save