Browse Source

Custom emoji

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

109
src/components/animationIntersector.ts

@ -4,6 +4,7 @@ @@ -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'; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

2
src/components/appMediaViewer.ts

@ -70,7 +70,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet @@ -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 = () => {

87
src/components/chat/bubbles.ts

@ -113,6 +113,7 @@ import PopupPayment from '../popups/payment'; @@ -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<Message.messageService['action']['_']> = new Set([
@ -3434,7 +3435,7 @@ export default class ChatBubbles { @@ -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 { @@ -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) {

10
src/components/monkeys/tracking.ts

@ -50,10 +50,10 @@ export default class TrackingMonkey { @@ -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 { @@ -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 { @@ -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';
}
}
});

338
src/components/wrappers/sticker.ts

@ -19,6 +19,7 @@ import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb'; @@ -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 @@ -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; @@ -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, @@ -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, @@ -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, @@ -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, @@ -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<void>();
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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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<void>();
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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => {
@ -546,7 +569,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -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, @@ -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<any>;
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, @@ -605,8 +631,13 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
}
};
if(exportLoad && (!downloaded || isAnimated)) {
ret.load = load;
return ret;
}
const loadPromise: Promise<Awaited<ReturnType<typeof load>> | void> = lazyLoadQueue && (!downloaded || isAnimated) ?
(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, @@ -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, @@ -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}: {

3
src/environment/customEmojiSupport.ts

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

3
src/environment/imageBitmapSupport.ts

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

3
src/environment/webAssemblySupport.ts

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

9
src/helpers/mediaSizes.ts

@ -19,7 +19,8 @@ type MediaTypeSizes = { @@ -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<{ @@ -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<{ @@ -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)
}
};

2
src/helpers/preloadAnimatedEmojiSticker.ts

@ -33,7 +33,7 @@ export default function preloadAnimatedEmojiSticker(emoji: string, width?: numbe @@ -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});
});

2
src/helpers/sequentialDom.ts

@ -49,7 +49,7 @@ class SequentialDom { @@ -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());

2
src/lib/appManagers/appDialogsManager.ts

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

3
src/lib/appManagers/appDocsManager.ts

@ -134,6 +134,7 @@ export class AppDocsManager extends AppManager { @@ -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 { @@ -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';

63
src/lib/appManagers/appEmojiManager.ts

@ -4,6 +4,7 @@ @@ -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'; @@ -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 { @@ -44,6 +47,9 @@ export class AppEmojiManager extends AppManager {
private recent: string[];
private getRecentEmojisPromise: Promise<AppEmojiManager['recent']>;
private getCustomEmojiDocumentsPromise: Promise<any>;
private getCustomEmojiDocumentPromises: Map<DocId, CancellablePromise<MyDocument>> = new Map();
/* public getPopularEmoji() {
return stateStorage.get('emojis_popular').then((popEmojis) => {
var result = []
@ -230,4 +236,61 @@ export class AppEmojiManager extends AppManager { @@ -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;
}
}

12
src/lib/appManagers/appImManager.ts

@ -25,7 +25,6 @@ import appNavigationController from '../../components/appNavigationController'; @@ -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 @@ -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<{ @@ -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);

6
src/lib/appManagers/appMessagesManager.ts

@ -2789,9 +2789,9 @@ export class AppMessagesManager extends AppManager { @@ -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'};

11
src/lib/mtproto/referenceDatabase.ts

@ -11,7 +11,7 @@ import deepEqual from '../../helpers/object/deepEqual'; @@ -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 { @@ -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 { @@ -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();

1
src/lib/richTextProcessor/parseEntities.ts

@ -79,7 +79,6 @@ export default function parseEntities(text: string) { @@ -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({

424
src/lib/richTextProcessor/wrapRichText.ts

@ -19,6 +19,237 @@ import setBlankToAnchor from './setBlankToAnchor'; @@ -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<CustomEmojiElement[], RLottiePlayer>;
public clearedContainers: Set<CustomEmojiElement[]>;
public paused: boolean;
public autoplay: boolean;
public middleware: () => boolean;
public keys: string[];
constructor() {
super();
this.classList.add('custom-emoji-renderer');
this.canvas = document.createElement('canvas');
this.canvas.classList.add('custom-emoji-canvas');
this.context = this.canvas.getContext('2d');
this.append(this.canvas);
this.paused = false;
this.autoplay = true;
this.players = new Map();
this.clearedContainers = new Set();
this.keys = [];
}
public connectedCallback() {
// this.setDimensions();
animationIntersector.addAnimation(this, 'EMOJI');
resizeObserver.observe(this.canvas);
this.connectedCallback = undefined;
}
public disconnectedCallback() {
for(const key of this.keys) {
const l = lotties.get(key);
if(!l) {
continue;
}
if(!--l.counter) {
if(l.player instanceof RLottiePlayer) {
l.player.remove();
}
lotties.delete(key);
if(!lotties.size) {
clearRenderInterval();
}
}
}
resizeObserver.unobserve(this.canvas);
this.disconnectedCallback = undefined;
}
public getOffsets(offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map()) {
for(const [containers, player] of this.players) {
const offsets = containers.map((container) => {
return {
top: container.offsetTop,
left: container.offsetLeft
};
});
offsetsMap.set(containers, offsets);
}
return offsetsMap;
}
public clearCanvas() {
const {context, canvas} = this;
context.clearRect(0, 0, canvas.width, canvas.height);
}
public render(offsetsMap: ReturnType<CustomEmojiRendererElement['getOffsets']>) {
const {context, canvas} = this;
const {width, height, dpr} = canvas;
for(const [containers, player] of this.players) {
const frame = topFrames.get(player);
if(!frame) {
continue;
}
const isImageData = frame instanceof ImageData;
const {width: stickerWidth, height: stickerHeight} = player.canvas[0];
const offsets = offsetsMap.get(containers);
const maxTop = height - stickerHeight;
const maxLeft = width - stickerWidth;
if(!this.clearedContainers.has(containers)) {
containers.forEach((container) => {
container.textContent = '';
});
this.clearedContainers.add(containers);
}
offsets.forEach(({top, left}) => {
top = Math.round(top * dpr), left = Math.round(left * dpr);
if(/* top > maxTop || */left > maxLeft) {
return;
}
if(isImageData) {
context.putImageData(frame as ImageData, left, top);
} else {
// context.clearRect(left, top, width, height);
context.drawImage(frame as ImageBitmap, left, top, stickerWidth, stickerHeight);
}
});
}
}
public checkForAnyFrame() {
for(const [containers, player] of this.players) {
if(topFrames.has(player)) {
return true;
}
}
return false;
}
public pause() {
this.paused = true;
}
public play() {
this.paused = false;
}
public remove() {
this.canvas.remove();
}
public setDimensions() {
const {canvas} = this;
sequentialDom.mutateElement(canvas, () => {
const rect = canvas.getBoundingClientRect();
this.setDimensionsFromRect(rect);
});
}
public setDimensionsFromRect(rect: DOMRect) {
const {canvas} = this;
const dpr = canvas.dpr ??= Math.min(2, window.devicePixelRatio);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
}
}
type R = CustomEmojiRendererElement;
let renderInterval: number;
const top: Array<R> = [];
const topFrames: Map<RLottiePlayer, Parameters<RLottiePlayer['overrideRender']>[0]> = new Map();
const lotties: Map<string, {player: Promise<RLottiePlayer> | RLottiePlayer, middlewares: Set<() => boolean>, counter: number}> = new Map();
const rerere = () => {
const t = top.filter((r) => !r.paused && r.isConnected && r.checkForAnyFrame());
if(!t.length) {
return;
}
const offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map();
for(const r of t) {
r.getOffsets(offsetsMap);
}
for(const r of t) {
r.clearCanvas();
}
for(const r of t) {
r.render(offsetsMap);
}
};
const CUSTOM_EMOJI_FPS = 60;
const CUSTOM_EMOJI_FRAME_INTERVAL = 1000 / CUSTOM_EMOJI_FPS;
const setRenderInterval = () => {
if(renderInterval) {
return;
}
renderInterval = window.setInterval(rerere, CUSTOM_EMOJI_FRAME_INTERVAL);
rerere();
};
const clearRenderInterval = () => {
if(!renderInterval) {
return;
}
clearInterval(renderInterval);
renderInterval = undefined;
};
(window as any).lotties = lotties;
customElements.define('custom-emoji-element', CustomEmojiElement);
customElements.define('custom-emoji-renderer-element', CustomEmojiRendererElement);
/**
* * Expecting correctly sorted nested entities (RichTextProcessor.sortEntities)
@ -46,7 +277,13 @@ export default function wrapRichText(text: string, options: Partial<{ @@ -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<any>[],
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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<any>[] = [];
const wrap = (doc: MyDocument, _loadPromises?: Promise<any>[]): Promise<Awaited<ReturnType<typeof wrapSticker>> & {onRender?: () => void}> => {
const containers = customEmojis[doc.id];
const isLottie = doc.sticker === 2;
const loadPromises: Promise<any>[] = [];
const promise = wrapSticker({
div: containers,
doc,
width: size.width,
height: size.height,
loop: true,
play: true,
managers,
isCustomEmoji: true,
group: 'none',
loadPromises,
middleware,
exportLoad: true,
needFadeIn: false,
loadStickerMiddleware: isLottie && middleware ? () => {
if(lotties.get(key) !== l) {
return false;
}
let good = !l.middlewares.size;
for(const middleware of l.middlewares) {
if(middleware()) {
good = true;
}
}
return good;
} : undefined,
static: doc.mime_type === 'video/webm' && !IS_WEBM_SUPPORTED
});
if(_loadPromises) {
promise.then(() => _loadPromises.push(...loadPromises));
}
if(!isLottie) {
return promise;
}
const onRender = (player: Awaited<Awaited<typeof promise>['render']>) => Promise.all(loadPromises).then(() => {
if(player instanceof RLottiePlayer && (!middleware || middleware())) {
l.player = player;
const playerCanvas = player.canvas[0];
renderer.canvas.dpr = playerCanvas.dpr;
renderer.players.set(containers, player);
setRenderInterval();
player.overrideRender ??= (frame) => {
topFrames.set(player, frame);
// frames.set(containers, frame);
};
l.middlewares.delete(middleware);
}
});
const key = [doc.id, size.width, size.height].join('-');
renderer.keys.push(key);
let l = lotties.get(key);
if(!l) {
l = {
player: undefined,
middlewares: new Set(),
counter: 0
};
lotties.set(key, l);
}
++l.counter;
if(middleware) {
l.middlewares.add(middleware);
}
return promise.then((res) => ({...res, onRender}));
};
const missing: DocId[] = [];
const cachedPromises = docs.map((doc, idx) => {
if(!doc) {
missing.push(docIds[idx]);
return;
}
return wrap(doc, loadPromises);
}).filter(Boolean);
const uncachedPromisesPromise = managers.appEmojiManager.getCustomEmojiDocuments(missing).then((docs) => {
if(middleware && !middleware()) return [];
return docs.filter(Boolean).map((doc) => wrap(doc));
});
const loadFromPromises = (promises: typeof cachedPromises) => {
return Promise.all(promises).then((arr) => {
const promises = arr.map(({load, onRender}) => {
if(!load) {
return;
}
return load().then(onRender);
});
return Promise.all(promises);
});
};
const load = () => {
if(middleware && !middleware()) return;
const cached = loadFromPromises(cachedPromises);
const uncached = uncachedPromisesPromise.then((promises) => loadFromPromises(promises));
return Promise.all([cached, uncached]);
};
if(options.lazyLoadQueue) {
options.lazyLoadQueue.push({
div: renderer.canvas,
load
});
} else {
load();
}
return Promise.all(cachedPromises).then(() => Promise.all(loadPromises)).then(() => {});
});
// recordPromise(loadPromise, 'render emojis: ' + docIds.length);
options.loadPromises?.push(loadPromise);
}
return fragment;
}

99
src/lib/rlottie/lottieLoader.ts

@ -11,8 +11,9 @@ import {logger, LogTypes} from '../logger'; @@ -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' | @@ -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<void> = !this.isWebAssemblySupported ? Promise.reject() : undefined;
private loadPromise: Promise<void> = !IS_WEB_ASSEMBLY_SUPPORTED ? Promise.reject() : undefined;
private loaded = false;
private workersLimit = 4;
private players: {[reqId: number]: RLottiePlayer} = {};
private playersByCacheName: {[cacheName: string]: Set<RLottiePlayer>} = {};
private workers: QueryableWorker[] = [];
private curWorkerNum = 0;
@ -35,7 +36,7 @@ export class LottieLoader { @@ -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 { @@ -91,7 +92,7 @@ export class LottieLoader {
}
public loadAnimationFromURL(params: Omit<RLottieOptions, 'animationData'>, url: string): Promise<RLottiePlayer> {
if(!this.isWebAssemblySupported) {
if(!IS_WEB_ASSEMBLY_SUPPORTED) {
return this.loadPromise as any;
}
@ -136,22 +137,30 @@ export class LottieLoader { @@ -136,22 +137,30 @@ export class LottieLoader {
group: AnimationItemGroup = params.group || '',
middleware?: () => boolean
): Promise<RLottiePlayer> {
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}
}

33
src/lib/rlottie/queryableWorker.ts

@ -4,12 +4,12 @@ @@ -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<{ @@ -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);
}
}

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

@ -5,6 +5,7 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -132,7 +146,7 @@ class RLottieWorker {
public init() {
this.initApi();
reply('ready');
reply(['ready']);
}
}
@ -174,7 +188,7 @@ const queryableFunctions = { @@ -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 = { @@ -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) {

244
src/lib/rlottie/rlottiePlayer.ts

@ -10,12 +10,12 @@ import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environmen @@ -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 = { @@ -31,27 +31,58 @@ export type RLottieOptions = {
inverseColor?: RLottieColor,
name?: string,
skipFirstFrameRendering?: boolean,
toneIndex?: number
toneIndex?: number,
sync?: boolean
};
type RLottieCacheMap = Map<number, Uint8ClampedArray>;
type RLottieCacheMapNew = Map<number, HTMLCanvasElement | ImageBitmap>;
type RLottieCacheMapURLs = Map<number, string>;
type RLottieCacheItem = {
frames: RLottieCacheMap,
framesNew: RLottieCacheMapNew,
framesURLs: RLottieCacheMapURLs,
clearCache: () => void,
counter: number
};
class RLottieCache {
private cache: Map<string, {frames: RLottieCacheMap, counter: number}>;
private cache: Map<string, RLottieCacheItem>;
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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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<RLottiePlayer['renderFrame2']>[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<{ @@ -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<{ @@ -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<{ @@ -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<{ @@ -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) {

3
src/pages/pageIm.ts

@ -43,7 +43,8 @@ const onFirstMount = () => { @@ -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);

4
src/scss/partials/_chat.scss

@ -1174,6 +1174,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi @@ -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 @@ -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;
}

48
src/scss/partials/_chatBubble.scss

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

1
src/scss/partials/_chatPinned.scss

@ -153,6 +153,7 @@ @@ -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();
}

1
src/scss/partials/_chatlist.scss

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

54
src/scss/partials/_customEmoji.scss

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

38
src/scss/partials/_spoiler.scss

@ -6,7 +6,7 @@ @@ -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 @@ @@ -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 @@ @@ -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) {

8
src/scss/style.scss

@ -114,6 +114,8 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -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; @@ -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 { @@ -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

Loading…
Cancel
Save