Browse Source

Emoji packs

Fix editing sensitive content setting
Fix emoji memory leaks
master
Eduard Kuzmenko 2 years ago committed by r4sas
parent
commit
71c4afaca0
  1. 33
      src/components/animationIntersector.ts
  2. 4
      src/components/appSearchSuper..ts
  3. 31
      src/components/chat/bubbles.ts
  4. 83
      src/components/chat/contextMenu.ts
  5. 3
      src/components/chat/replyContainer.ts
  6. 5
      src/components/popups/index.ts
  7. 291
      src/components/popups/stickers.ts
  8. 14
      src/components/row.ts
  9. 14
      src/components/sidebarLeft/tabs/chatFolders.ts
  10. 8
      src/components/sidebarLeft/tabs/generalSettings.ts
  11. 2
      src/components/sidebarLeft/tabs/privacyAndSecurity.ts
  12. 4
      src/components/sidebarRight/tabs/gifs.ts
  13. 8
      src/components/stickerViewer.ts
  14. 3
      src/components/wrappers/album.ts
  15. 3
      src/components/wrappers/photo.ts
  16. 75
      src/components/wrappers/sticker.ts
  17. 3
      src/components/wrappers/stickerAnimation.ts
  18. 3
      src/components/wrappers/video.ts
  19. 8
      src/global.d.ts
  20. 10
      src/helpers/dom/createStickersContextMenu.ts
  21. 63
      src/helpers/dom/getViewportSlice.ts
  22. 12
      src/helpers/dom/getVisibleRect.ts
  23. 21
      src/helpers/dom/renderImageFromUrl.ts
  24. 43
      src/helpers/dom/requestVideoFrameCallbackPolyfill.ts
  25. 2
      src/helpers/dom/setInnerHTML.ts
  26. 86
      src/helpers/framesCache.ts
  27. 5
      src/helpers/idleController.ts
  28. 9
      src/helpers/mediaSizes.ts
  29. 96
      src/helpers/middleware.ts
  30. 12
      src/lang.ts
  31. 63
      src/lib/appManagers/appImManager.ts
  32. 51
      src/lib/appManagers/appStickersManager.ts
  33. 506
      src/lib/richTextProcessor/wrapRichText.ts
  34. 2
      src/lib/richTextProcessor/wrapUrl.ts
  35. 4
      src/lib/rlottie/lottieLoader.ts
  36. 100
      src/lib/rlottie/rlottiePlayer.ts
  37. 5
      src/pages/pageIm.ts
  38. 25
      src/scss/partials/_button.scss
  39. 2
      src/scss/partials/_chatBubble.scss
  40. 21
      src/scss/partials/_leftSidebar.scss
  41. 25
      src/scss/partials/_row.scss
  42. 33
      src/scss/partials/popups/_stickers.scss
  43. 11
      src/scss/style.scss

33
src/components/animationIntersector.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText'; import {CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText';
import rootScope from '../lib/rootScope'; import rootScope from '../lib/rootScope';
import {IS_SAFARI} from '../environment/userAgent'; import {IS_SAFARI} from '../environment/userAgent';
import {MOUNT_CLASS_TO} from '../config/debug'; import {MOUNT_CLASS_TO} from '../config/debug';
@ -22,7 +22,16 @@ export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
export interface AnimationItem { export interface AnimationItem {
el: HTMLElement, el: HTMLElement,
group: AnimationItemGroup, group: AnimationItemGroup,
animation: RLottiePlayer | HTMLVideoElement | CustomEmojiRendererElement animation: AnimationItemWrapper
};
export interface AnimationItemWrapper {
remove: () => void;
paused: boolean;
pause: () => any;
play: () => any;
autoplay: boolean;
// onVisibilityChange?: (visible: boolean) => boolean;
}; };
export class AnimationIntersector { export class AnimationIntersector {
@ -145,8 +154,21 @@ export class AnimationIntersector {
} }
public addAnimation(_animation: AnimationItem['animation'], group: AnimationItemGroup = '') { public addAnimation(_animation: AnimationItem['animation'], group: AnimationItemGroup = '') {
if(group === 'none') {
return;
}
let el: HTMLElement;
if(_animation instanceof RLottiePlayer) {
el = _animation.el[0];
} else if(_animation instanceof CustomEmojiRendererElement) {
el = _animation.canvas;
} else if(_animation instanceof HTMLElement) {
el = _animation;
}
const animation: AnimationItem = { const animation: AnimationItem = {
el: _animation instanceof RLottiePlayer ? _animation.el[0] : (_animation instanceof HTMLVideoElement ? _animation : _animation.canvas), el,
animation: _animation, animation: _animation,
group group
}; };
@ -188,7 +210,10 @@ export class AnimationIntersector {
return; return;
} }
if(blurred || (this.onlyOnePlayableGroup && this.onlyOnePlayableGroup !== group) || (animation instanceof HTMLVideoElement && this.videosLocked)) { if(blurred ||
(this.onlyOnePlayableGroup && this.onlyOnePlayableGroup !== group) ||
(animation instanceof HTMLVideoElement && this.videosLocked)
) {
if(!animation.paused) { if(!animation.paused) {
// console.warn('pause animation:', animation); // console.warn('pause animation:', animation);
animation.pause(); animation.pause();

4
src/components/appSearchSuper..ts

@ -18,7 +18,7 @@ import {wrapDocument, wrapPhoto, wrapVideo} from './wrappers';
import useHeavyAnimationCheck, {getHeavyAnimationPromise} from '../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck, {getHeavyAnimationPromise} from '../hooks/useHeavyAnimationCheck';
import I18n, {LangPackKey, i18n} from '../lib/langPack'; import I18n, {LangPackKey, i18n} from '../lib/langPack';
import findUpClassName from '../helpers/dom/findUpClassName'; import findUpClassName from '../helpers/dom/findUpClassName';
import {getMiddleware} from '../helpers/middleware'; import {getMiddleware, Middleware} from '../helpers/middleware';
import {ChannelParticipant, ChatFull, ChatParticipant, ChatParticipants, Document, Message, MessageMedia, Photo, WebPage} from '../layer'; import {ChannelParticipant, ChatFull, ChatParticipant, ChatParticipants, Document, Message, MessageMedia, Photo, WebPage} from '../layer';
import SortedUserList from './sortedUserList'; import SortedUserList from './sortedUserList';
import findUpTag from '../helpers/dom/findUpTag'; import findUpTag from '../helpers/dom/findUpTag';
@ -252,7 +252,7 @@ class SearchContextMenu {
export type ProcessSearchSuperResult = { export type ProcessSearchSuperResult = {
message: Message.message, message: Message.message,
middleware: () => boolean, middleware: Middleware,
promises: Promise<any>[], promises: Promise<any>[],
elemsToAppend: {element: HTMLElement, message: any}[], elemsToAppend: {element: HTMLElement, message: any}[],
inputFilter: MyInputMessagesFilter, inputFilter: MyInputMessagesFilter,

31
src/components/chat/bubbles.ts

@ -45,7 +45,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import findUpTag from '../../helpers/dom/findUpTag'; import findUpTag from '../../helpers/dom/findUpTag';
import {toast, toastNew} from '../toast'; import {toast, toastNew} from '../toast';
import {getElementByPoint} from '../../helpers/dom/getElementByPoint'; import {getElementByPoint} from '../../helpers/dom/getElementByPoint';
import {getMiddleware} from '../../helpers/middleware'; import {getMiddleware, Middleware} from '../../helpers/middleware';
import cancelEvent from '../../helpers/dom/cancelEvent'; import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent'; import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import htmlToDocumentFragment from '../../helpers/dom/htmlToDocumentFragment'; import htmlToDocumentFragment from '../../helpers/dom/htmlToDocumentFragment';
@ -210,7 +210,7 @@ export default class ChatBubbles {
public lazyLoadQueue: LazyLoadQueue; public lazyLoadQueue: LazyLoadQueue;
private middleware = getMiddleware(); private middlewareHelper = getMiddleware();
private log: ReturnType<typeof logger>; private log: ReturnType<typeof logger>;
@ -2142,6 +2142,8 @@ export default class ChatBubbles {
const bubble = this.bubbles[mid]; const bubble = this.bubbles[mid];
if(!bubble) return; if(!bubble) return;
bubble.middlewareHelper.destroy();
deleted = true; deleted = true;
/* const mounted = this.getMountedBubble(mid); /* const mounted = this.getMountedBubble(mid);
if(!mounted) return; */ if(!mounted) return; */
@ -2617,7 +2619,7 @@ export default class ChatBubbles {
this.viewsMids.clear(); this.viewsMids.clear();
} }
this.middleware.clean(); this.middlewareHelper.clean();
this.onAnimateLadder = undefined; this.onAnimateLadder = undefined;
this.resolveLadderAnimation = undefined; this.resolveLadderAnimation = undefined;
@ -3354,7 +3356,7 @@ export default class ChatBubbles {
} }
public getMiddleware(additionalCallback?: () => boolean) { public getMiddleware(additionalCallback?: () => boolean) {
return this.middleware.get(additionalCallback); return this.middlewareHelper.get(additionalCallback);
} }
private async safeRenderMessage( private async safeRenderMessage(
@ -3368,7 +3370,8 @@ export default class ChatBubbles {
return; return;
} }
const middleware = this.getMiddleware(); const middlewareHelper = this.getMiddleware().create();
const middleware = middlewareHelper.get();
let result: Awaited<ReturnType<ChatBubbles['renderMessage']>> & {updatePosition: typeof updatePosition}; let result: Awaited<ReturnType<ChatBubbles['renderMessage']>> & {updatePosition: typeof updatePosition};
try { try {
@ -3376,6 +3379,7 @@ export default class ChatBubbles {
// const groupedId = (message as Message.message).grouped_id; // const groupedId = (message as Message.message).grouped_id;
const newBubble = document.createElement('div'); const newBubble = document.createElement('div');
newBubble.middlewareHelper = middlewareHelper;
newBubble.dataset.mid = '' + message.mid; newBubble.dataset.mid = '' + message.mid;
newBubble.dataset.peerId = '' + message.peerId; newBubble.dataset.peerId = '' + message.peerId;
newBubble.dataset.timestamp = '' + message.date; newBubble.dataset.timestamp = '' + message.date;
@ -3389,6 +3393,7 @@ export default class ChatBubbles {
// bubbleNew.mids.add(message.mid); // bubbleNew.mids.add(message.mid);
if(bubble) { if(bubble) {
bubble.middlewareHelper.destroy();
this.skippedMids.delete(message.mid); this.skippedMids.delete(message.mid);
this.bubblesToEject.add(bubble); this.bubblesToEject.add(bubble);
@ -3398,7 +3403,7 @@ export default class ChatBubbles {
} }
bubble = this.bubbles[message.mid] = newBubble; bubble = this.bubbles[message.mid] = newBubble;
let originalPromise = this.renderMessage(message, reverse, bubble); let originalPromise = this.renderMessage(message, reverse, bubble, middleware);
if(processResult) { if(processResult) {
originalPromise = processResult(originalPromise, bubble); originalPromise = processResult(originalPromise, bubble);
} }
@ -3431,7 +3436,8 @@ export default class ChatBubbles {
private async renderMessage( private async renderMessage(
message: Message.message | Message.messageService, message: Message.message | Message.messageService,
reverse = false, reverse = false,
bubble: HTMLElement bubble: HTMLElement,
middleware: Middleware
) { ) {
// if(DEBUG) { // if(DEBUG) {
// this.log('message to render:', message); // this.log('message to render:', message);
@ -3588,7 +3594,8 @@ export default class ChatBubbles {
passEntities: this.passEntities, passEntities: this.passEntities,
loadPromises, loadPromises,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
customEmojiSize customEmojiSize,
middleware
}); });
let canHaveTail = true; let canHaveTail = true;
@ -4074,7 +4081,7 @@ export default class ChatBubbles {
wrapSticker({ wrapSticker({
doc, doc,
div: attachmentDiv, div: attachmentDiv,
middleware: this.getMiddleware(), middleware,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
group: CHAT_ANIMATION_GROUP, group: CHAT_ANIMATION_GROUP,
// play: !!message.pending || !multipleRender, // play: !!message.pending || !multipleRender,
@ -4113,7 +4120,7 @@ export default class ChatBubbles {
wrapAlbum({ wrapAlbum({
messages: albumMessages, messages: albumMessages,
attachmentDiv, attachmentDiv,
middleware: this.getMiddleware(), middleware,
isOut: our, isOut: our,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
chat: this.chat, chat: this.chat,
@ -4132,7 +4139,7 @@ export default class ChatBubbles {
withTail, withTail,
isOut, isOut,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(), middleware,
group: CHAT_ANIMATION_GROUP, group: CHAT_ANIMATION_GROUP,
loadPromises, loadPromises,
autoDownload: this.chat.autoDownload, autoDownload: this.chat.autoDownload,
@ -4317,7 +4324,7 @@ export default class ChatBubbles {
withTail: false, withTail: false,
isOut, isOut,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(), middleware,
loadPromises, loadPromises,
boxWidth: mediaSize.width, boxWidth: mediaSize.width,
boxHeight: mediaSize.height boxHeight: mediaSize.height

83
src/components/chat/contextMenu.ts

@ -19,7 +19,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import cancelEvent from '../../helpers/dom/cancelEvent'; import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent'; import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import isSelectionEmpty from '../../helpers/dom/isSelectionEmpty'; import isSelectionEmpty from '../../helpers/dom/isSelectionEmpty';
import {Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction} from '../../layer'; import {Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction, MessageEntity, InputStickerSet, StickerSet, Document} from '../../layer';
import PopupReportMessages from '../popups/reportMessages'; import PopupReportMessages from '../popups/reportMessages';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import PopupSponsored from '../popups/sponsored'; import PopupSponsored from '../popups/sponsored';
@ -40,9 +40,14 @@ import filterAsync from '../../helpers/array/filterAsync';
import appDownloadManager from '../../lib/appManagers/appDownloadManager'; import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {SERVICE_PEER_ID} from '../../lib/mtproto/mtproto_config'; import {SERVICE_PEER_ID} from '../../lib/mtproto/mtproto_config';
import {MessagesStorageKey} from '../../lib/appManagers/appMessagesManager'; import {MessagesStorageKey} from '../../lib/appManagers/appMessagesManager';
import filterUnique from '../../helpers/array/filterUnique';
import replaceContent from '../../helpers/dom/replaceContent';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import PopupStickers from '../popups/stickers';
export default class ChatContextMenu { export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise<boolean>, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[]; private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise<boolean>, notDirect?: () => boolean, withSelection?: true, isSponsored?: true, localName?: 'views' | 'emojis'})[];
private element: HTMLElement; private element: HTMLElement;
private isSelectable: boolean; private isSelectable: boolean;
@ -67,6 +72,8 @@ export default class ChatContextMenu {
private middleware: ReturnType<typeof getMiddleware>; private middleware: ReturnType<typeof getMiddleware>;
private canOpenReactedList: boolean; private canOpenReactedList: boolean;
private emojiInputsPromise: CancellablePromise<InputStickerSet.inputStickerSetID[]>;
constructor( constructor(
private chat: Chat, private chat: Chat,
private managers: AppManagers private managers: AppManagers
@ -508,7 +515,8 @@ export default class ChatContextMenu {
} }
}, },
verify: async() => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactions?.length || await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message)), verify: async() => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactions?.length || await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message)),
notDirect: () => true notDirect: () => true,
localName: 'views'
}, { }, {
icon: 'delete danger', icon: 'delete danger',
text: 'Delete', text: 'Delete',
@ -529,6 +537,20 @@ export default class ChatContextMenu {
}, },
verify: () => false, verify: () => false,
isSponsored: true isSponsored: true
}, {
// icon: 'smile',
text: 'Loading',
onClick: () => {
this.emojiInputsPromise.then((inputs) => {
new PopupStickers(inputs, true).show();
});
},
verify: () => {
const entities = (this.message as Message.message).entities;
return entities?.some((entity) => entity._ === 'messageEntityCustomEmoji');
},
notDirect: () => true,
localName: 'emojis'
}]; }];
} }
@ -545,12 +567,14 @@ export default class ChatContextMenu {
element.id = 'bubble-contextmenu'; element.id = 'bubble-contextmenu';
element.classList.add('contextmenu'); element.classList.add('contextmenu');
const viewsButton = filteredButtons.find((button) => !button.icon); const viewsButton = filteredButtons.find((button) => button.localName === 'views');
if(viewsButton) { if(viewsButton) {
const reactions = (this.message as Message.message).reactions; const reactions = (this.message as Message.message).reactions;
const recentReactions = reactions?.recent_reactions; const recentReactions = reactions?.recent_reactions;
const isViewingReactions = !!recentReactions?.length; const isViewingReactions = !!recentReactions?.length;
const participantsCount = await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message) ? ((await this.managers.appPeersManager.getPeer(this.peerId)) as MTChat.chat).participants_count : undefined; const participantsCount = await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message) ?
((await this.managers.appPeersManager.getPeer(this.peerId)) as MTChat.chat).participants_count :
undefined;
const reactedLength = reactions ? reactions.results.reduce((acc, r) => acc + r.count, 0) : undefined; const reactedLength = reactions ? reactions.results.reduce((acc, r) => acc + r.count, 0) : undefined;
viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks')); viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks'));
@ -677,6 +701,55 @@ export default class ChatContextMenu {
} }
} }
const emojisButton = filteredButtons.find((button) => button.localName === 'emojis');
if(emojisButton) {
emojisButton.element.classList.add('is-multiline');
emojisButton.element.parentElement.insertBefore(document.createElement('hr'), emojisButton.element);
const setPadding = () => {
menuPadding ??= {};
menuPadding.bottom = 24;
};
const entities = (this.message as Message.message).entities.filter((entity) => entity._ === 'messageEntityCustomEmoji') as MessageEntity.messageEntityCustomEmoji[];
const docIds = filterUnique(entities.map((entity) => entity.document_id));
const inputsPromise = this.emojiInputsPromise = deferredPromise();
await this.managers.appEmojiManager.getCachedCustomEmojiDocuments(docIds).then(async(docs) => {
const p = async(docs: Document.document[]) => {
const s: Map<StickerSet['id'], InputStickerSet.inputStickerSetID> = new Map();
docs.forEach((doc) => {
if(!doc || s.has(doc.stickerSetInput.id)) {
return;
}
s.set(doc.stickerSetInput.id, doc.stickerSetInput);
});
const inputs = [...s.values()];
inputsPromise.resolve(inputs);
if(s.size === 1) {
const result = await this.managers.acknowledged.appStickersManager.getStickerSet(inputs[0]);
const promise = result.result.then((set) => {
const el = i18n('MessageContainsEmojiPack', [wrapEmojiText(set.set.title)]);
replaceContent(emojisButton.textElement, el);
});
return result.cached ? promise : (setPadding(), undefined);
}
replaceContent(emojisButton.textElement, i18n('MessageContainsEmojiPacks', [s.size]));
};
if(docs.some((doc) => !doc)) {
setPadding();
this.managers.appEmojiManager.getCustomEmojiDocuments(docIds).then(p);
} else {
return p(docs);
}
});
// emojisButton.element.append(i18n('Loading'));
}
this.chat.container.append(element); this.chat.container.append(element);
return { return {

3
src/components/chat/replyContainer.ts

@ -5,6 +5,7 @@
*/ */
import replaceContent from '../../helpers/dom/replaceContent'; import replaceContent from '../../helpers/dom/replaceContent';
import {Middleware} from '../../helpers/middleware';
import limitSymbols from '../../helpers/string/limitSymbols'; import limitSymbols from '../../helpers/string/limitSymbols';
import {Document, MessageMedia, Photo, WebPage} from '../../layer'; import {Document, MessageMedia, Photo, WebPage} from '../../layer';
import appImManager, {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager'; import appImManager, {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager';
@ -42,7 +43,7 @@ export async function wrapReplyDivAndCaption(options: {
let messageMedia: MessageMedia | WebPage.webPage = message?.media; let messageMedia: MessageMedia | WebPage.webPage = message?.media;
let setMedia = false, isRound = false; let setMedia = false, isRound = false;
const mediaChildren = mediaEl ? Array.from(mediaEl.children).slice() : []; const mediaChildren = mediaEl ? Array.from(mediaEl.children).slice() : [];
let middleware: () => boolean; let middleware: Middleware;
if(messageMedia && mediaEl) { if(messageMedia && mediaEl) {
subtitleEl.textContent = ''; subtitleEl.textContent = '';
subtitleEl.append(await wrapMessageForReply(message, undefined, undefined, undefined, undefined, true)); subtitleEl.append(await wrapMessageForReply(message, undefined, undefined, undefined, undefined, true));

5
src/components/popups/index.ts

@ -20,6 +20,7 @@ import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import overlayCounter from '../../helpers/overlayCounter'; import overlayCounter from '../../helpers/overlayCounter';
import Scrollable from '../scrollable'; import Scrollable from '../scrollable';
import {getMiddleware, MiddlewareHelper} from '../../helpers/middleware';
export type PopupButton = { export type PopupButton = {
text?: string, text?: string,
@ -92,6 +93,8 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
protected buttons: Array<PopupButton>; protected buttons: Array<PopupButton>;
protected middlewareHelper: MiddlewareHelper;
constructor(className: string, options: PopupOptions = {}) { constructor(className: string, options: PopupOptions = {}) {
super(false); super(false);
this.element.classList.add('popup'); this.element.classList.add('popup');
@ -109,6 +112,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.header.append(this.title); this.header.append(this.title);
} }
this.middlewareHelper = getMiddleware();
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
this.managers = PopupElement.MANAGERS; this.managers = PopupElement.MANAGERS;
@ -268,6 +272,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.element.classList.add('hiding'); this.element.classList.add('hiding');
this.element.classList.remove('active'); this.element.classList.remove('active');
this.listenerSetter.removeAll(); this.listenerSetter.removeAll();
this.middlewareHelper.destroy();
if(!this.withoutOverlay) { if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = false; overlayCounter.isOverlayActive = false;

291
src/components/popups/stickers.ts

@ -22,130 +22,269 @@ import setInnerHTML from '../../helpers/dom/setInnerHTML';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText'; import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import createStickersContextMenu from '../../helpers/dom/createStickersContextMenu'; import createStickersContextMenu from '../../helpers/dom/createStickersContextMenu';
import attachStickerViewerListeners from '../stickerViewer'; import attachStickerViewerListeners from '../stickerViewer';
import wrapRichText from '../../lib/richTextProcessor/wrapRichText';
import {Document, MessageEntity, StickerSet} from '../../layer';
import Row from '../row';
import replaceContent from '../../helpers/dom/replaceContent';
import rootScope from '../../lib/rootScope';
const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP'; const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP';
export default class PopupStickers extends PopupElement { export default class PopupStickers extends PopupElement {
private stickersFooter: HTMLElement; private stickersFooter: HTMLElement;
private stickersDiv: HTMLElement; private appendTo: HTMLElement;
private updateAdded: {[setId: Long]: (added: boolean) => void};
constructor(private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0]) { private sets: StickerSet.stickerSet[];
private button: HTMLElement;
constructor(
private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0] | Parameters<AppStickersManager['getStickerSet']>[0][],
private isEmojis?: boolean
) {
super('popup-stickers', {closable: true, overlayClosable: true, body: true, scrollable: true, title: true}); super('popup-stickers', {closable: true, overlayClosable: true, body: true, scrollable: true, title: true});
this.title.append(i18n('Loading')); this.title.append(i18n('Loading'));
this.updateAdded = {};
this.addEventListener('close', () => { this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup(); animationIntersector.setOnlyOnePlayableGroup();
destroy();
}); });
const div = document.createElement('div'); this.appendTo = this.scrollable.container;
div.classList.add('sticker-set');
this.stickersDiv = document.createElement('div');
this.stickersDiv.classList.add('sticker-set-stickers', 'is-loading');
attachClickEvent(this.stickersDiv, this.onStickersClick, {listenerSetter: this.listenerSetter}); this.appendTo.classList.add('is-loading');
putPreloader(this.appendTo, true);
putPreloader(this.stickersDiv, true);
this.stickersFooter = document.createElement('div'); this.stickersFooter = document.createElement('div');
this.stickersFooter.classList.add('sticker-set-footer'); this.stickersFooter.classList.add('sticker-set-footer');
div.append(this.stickersDiv);
const btn = Button('btn-primary btn-primary-transparent disable-hover', {noRipple: true, text: 'Loading'}); const btn = Button('btn-primary btn-primary-transparent disable-hover', {noRipple: true, text: 'Loading'});
this.stickersFooter.append(btn); this.stickersFooter.append(btn);
this.scrollable.append(div);
this.body.append(this.stickersFooter); this.body.append(this.stickersFooter);
const {destroy} = createStickersContextMenu({ attachStickerViewerListeners({listenTo: this.appendTo, listenerSetter: this.listenerSetter});
listenTo: this.stickersDiv,
isStickerPack: true const onStickerSetUpdate = (set: StickerSet.stickerSet) => {
}); const idx = this.sets.findIndex((_set) => _set.id === set.id);
if(idx === -1) {
return;
}
attachStickerViewerListeners({listenTo: this.stickersDiv, listenerSetter: this.listenerSetter}); this.sets[idx] = set;
const updateAdded = this.updateAdded[set.id];
updateAdded?.(!!set.installed_date);
this.updateButton();
};
this.listenerSetter.add(rootScope)('stickers_installed', onStickerSetUpdate);
this.listenerSetter.add(rootScope)('stickers_deleted', onStickerSetUpdate);
this.loadStickerSet(); this.loadStickerSet();
} }
private onStickersClick = (e: MouseEvent) => { private createStickerSetElements(set?: StickerSet.stickerSet) {
const container = document.createElement('div');
container.classList.add('sticker-set');
let headerRow: Row, updateAdded: (added: boolean) => void;
if(set) {
headerRow = new Row({
title: wrapEmojiText(set.title),
subtitle: i18n(set.pFlags.emojis ? 'EmojiCount' : 'Stickers', [set.count]),
buttonRight: true
});
updateAdded = (added) => {
replaceContent(headerRow.buttonRight, i18n(added ? 'Stickers.SearchAdded' : 'Stickers.SearchAdd'));
headerRow.buttonRight.classList.toggle('active', added);
};
updateAdded(!!set.installed_date);
container.append(headerRow.container);
}
const itemsContainer = document.createElement('div');
itemsContainer.classList.add('sticker-set-stickers');
container.append(itemsContainer);
return {container, headerRow, updateAdded, itemsContainer};
}
private onStickersClick = async(e: MouseEvent) => {
const target = findUpClassName(e.target, 'sticker-set-sticker'); const target = findUpClassName(e.target, 'sticker-set-sticker');
if(!target) return; if(!target) return;
const docId = target.dataset.docId; const docId = target.dataset.docId;
if(appImManager.chat.input.sendMessageWithDocument(docId)) { if(await appImManager.chat.input.sendMessageWithDocument(docId)) {
this.hide(); this.hide();
} }
}; };
private loadStickerSet() { private async loadStickerSet() {
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then(async(set) => { const middleware = this.middlewareHelper.get();
if(!set) { const inputs = Array.isArray(this.stickerSetInput) ? this.stickerSetInput : [this.stickerSetInput];
toastNew({langPackKey: 'StickerSet.DontExist'}); const setsPromises = inputs.map((input) => this.managers.appStickersManager.getStickerSet(input));
this.hide(); let sets = await Promise.all(setsPromises);
return; if(!middleware()) return;
} let firstSet = sets[0];
if(sets.length === 1 && !firstSet) {
toastNew({langPackKey: this.isEmojis ? 'AddEmojiNotFound' : 'StickerSet.DontExist'});
this.hide();
return;
}
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP); sets = sets.filter(Boolean);
firstSet = sets[0];
let button: HTMLElement; this.sets = sets.map((set) => set.set);
const s = i18n('Stickers', [set.set.count]);
if(set.set.installed_date) { const isEmojis = this.isEmojis ??= !!firstSet.set.pFlags.emojis;
button = Button('btn-primary btn-primary-transparent danger', {noRipple: true});
button.append(i18n('RemoveStickersCount', [s])); if(!isEmojis) {
} else { attachClickEvent(this.appendTo, this.onStickersClick, {listenerSetter: this.listenerSetter});
button = Button('btn-primary btn-color-primary', {noRipple: true});
button.append(i18n('AddStickersCount', [s])); const {destroy} = createStickersContextMenu({
listenTo: this.appendTo,
isStickerPack: true,
onSend: () => this.hide()
});
this.addEventListener('close', destroy);
}
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
const lazyLoadQueue = new LazyLoadQueue();
const loadPromises: Promise<any>[] = [];
const containersPromises = sets.map(async(set) => {
const {container, itemsContainer, headerRow, updateAdded} = this.createStickerSetElements(sets.length > 1 ? set.set : undefined);
if(headerRow) {
attachClickEvent(headerRow.buttonRight, () => {
this.managers.appStickersManager.toggleStickerSet(set.set);
}, {listenerSetter: this.listenerSetter});
} }
attachClickEvent(button, () => { this.updateAdded[set.set.id] = updateAdded;
const toggle = toggleDisability([button], true);
let divs: (HTMLElement | DocumentFragment)[];
const docs = set.documents.filter((doc) => doc?._ === 'document') as Document.document[];
if(isEmojis) {
let text = '';
const entities: MessageEntity[] = [];
docs.forEach((doc) => {
entities.push({
_: 'messageEntityCustomEmoji',
offset: text.length,
length: doc.stickerEmojiRaw.length,
document_id: doc.id
});
this.managers.appStickersManager.toggleStickerSet(set.set).then(() => { text += doc.stickerEmojiRaw;
this.hide();
}).catch(() => {
toggle();
}); });
});
const lazyLoadQueue = new LazyLoadQueue(); const wrapped = wrapRichText(text, {
const divs = await Promise.all(set.documents.map(async(doc) => { entities,
if(doc._ === 'documentEmpty') { loadPromises,
return; animationGroup: ANIMATION_GROUP,
} customEmojiSize: mediaSizes.active.esgCustomEmoji,
middleware
const div = document.createElement('div'); // lazyLoadQueue
div.classList.add('sticker-set-sticker');
const size = mediaSizes.active.esgSticker.width;
await wrapSticker({
doc,
div,
lazyLoadQueue,
group: ANIMATION_GROUP,
play: true,
loop: true,
width: size,
height: size,
withLock: true
}); });
return div; divs = [wrapped];
}));
setInnerHTML(this.title, wrapEmojiText(set.set.title)); itemsContainer.classList.add('is-emojis');
this.stickersFooter.classList.toggle('add', !set.set.installed_date); } else {
this.stickersFooter.textContent = ''; divs = await Promise.all(docs.map(async(doc) => {
this.stickersFooter.append(button); const div = document.createElement('div');
div.classList.add('sticker-set-sticker');
const size = mediaSizes.active.esgSticker.width;
await wrapSticker({
doc,
div,
lazyLoadQueue,
group: ANIMATION_GROUP,
play: true,
loop: true,
width: size,
height: size,
withLock: true,
loadPromises,
middleware
});
return div;
}));
}
this.stickersDiv.classList.remove('is-loading'); itemsContainer.append(...divs.filter(Boolean));
this.stickersDiv.innerHTML = '';
this.stickersDiv.append(...divs.filter(Boolean));
this.scrollable.onAdditionalScroll(); return container;
}); });
const containers = await Promise.all(containersPromises);
await Promise.all(loadPromises);
const button = this.button = Button('', {noRipple: true});
this.updateButton();
attachClickEvent(button, () => {
const toggle = toggleDisability([button], true);
this.managers.appStickersManager.toggleStickerSets(sets.map((set) => set.set)).then(() => {
this.hide();
}).catch(() => {
toggle();
});
}, {listenerSetter: this.listenerSetter});
if(sets.length === 1) {
setInnerHTML(this.title, wrapEmojiText(firstSet.set.title));
} else {
setInnerHTML(this.title, i18n('Emoji'));
}
this.stickersFooter.textContent = '';
this.stickersFooter.append(button);
this.appendTo.classList.remove('is-loading');
this.appendTo.textContent = '';
this.appendTo.append(...containers);
this.scrollable.onAdditionalScroll();
}
private updateButton() {
const {sets, isEmojis} = this;
let isAdd: boolean, buttonAppend: HTMLElement;
if(sets.length === 1) {
const firstSet = sets[0];
buttonAppend = i18n(isEmojis ? 'EmojiCount' : 'Stickers', [firstSet.count]);
isAdd = !firstSet.installed_date;
} else {
const installed = sets.filter((set) => set.installed_date);
let count: number;
if(sets.length === installed.length) {
isAdd = false;
count = sets.length;
} else {
isAdd = true;
count = sets.length - installed.length;
}
buttonAppend = i18n('EmojiPackCount', [count]);
}
this.button.className = isAdd ? 'btn-primary btn-color-primary' : 'btn-primary btn-primary-transparent danger';
replaceContent(this.button, i18n(isAdd ? 'AddStickersCount' : 'RemoveStickersCount', [buttonAppend]));
} }
} }

14
src/components/row.ts

@ -14,6 +14,7 @@ import replaceContent from '../helpers/dom/replaceContent';
import setInnerHTML from '../helpers/dom/setInnerHTML'; import setInnerHTML from '../helpers/dom/setInnerHTML';
import {attachClickEvent} from '../helpers/dom/clickEvent'; import {attachClickEvent} from '../helpers/dom/clickEvent';
import ListenerSetter from '../helpers/listenerSetter'; import ListenerSetter from '../helpers/listenerSetter';
import Button from './button';
export default class Row { export default class Row {
public container: HTMLElement; public container: HTMLElement;
@ -27,6 +28,8 @@ export default class Row {
public freezed = false; public freezed = false;
public buttonRight: HTMLElement;
constructor(options: Partial<{ constructor(options: Partial<{
icon: string, icon: string,
subtitle: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment,
@ -44,7 +47,9 @@ export default class Row {
havePadding: boolean, havePadding: boolean,
noRipple: boolean, noRipple: boolean,
noWrap: boolean, noWrap: boolean,
listenerSetter: ListenerSetter listenerSetter: ListenerSetter,
buttonRight?: HTMLElement | boolean,
buttonRightLangKey: LangPackKey
}> = {}) { }> = {}) {
this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div'); this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div');
this.container.classList.add('row'); this.container.classList.add('row');
@ -173,6 +178,13 @@ export default class Row {
this.container.prepend(this.container.lastElementChild); this.container.prepend(this.container.lastElementChild);
} */ } */
} }
if(options.buttonRight || options.buttonRightLangKey) {
this.buttonRight = options.buttonRight instanceof HTMLElement ?
options.buttonRight :
Button('btn-primary btn-color-primary', {text: options.buttonRightLangKey});
this.container.append(this.buttonRight);
}
} }
public createMedia(size?: 'small') { public createMedia(size?: 'small') {

14
src/components/sidebarLeft/tabs/chatFolders.ts

@ -87,7 +87,8 @@ export default class AppChatFoldersTab extends SliderSuperTab {
row = new Row({ row = new Row({
title: filter.id === FOLDER_ID_ALL ? i18n('FilterAllChats') : wrapEmojiText(filter.title), title: filter.id === FOLDER_ID_ALL ? i18n('FilterAllChats') : wrapEmojiText(filter.title),
subtitle: description, subtitle: description,
clickable: filter.id !== FOLDER_ID_ALL clickable: filter.id !== FOLDER_ID_ALL,
buttonRightLangKey: dialogFilter._ === 'dialogFilterSuggested' ? 'Add' : undefined
}); });
if(d.length) { if(d.length) {
@ -125,7 +126,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
} }
} }
return div; return row;
} }
protected async init() { protected async init() {
@ -274,11 +275,10 @@ export default class AppChatFoldersTab extends SliderSuperTab {
Array.from(this.suggestedSection.content.children).slice(1).forEach((el) => el.remove()); Array.from(this.suggestedSection.content.children).slice(1).forEach((el) => el.remove());
for(const filter of suggestedFilters) { for(const filter of suggestedFilters) {
const div = await this.renderFolder(filter); const row = await this.renderFolder(filter);
const button = Button('btn-primary btn-color-primary', {text: 'Add'}); this.suggestedSection.content.append(row.container);
div.append(button);
this.suggestedSection.content.append(div);
const button = row.buttonRight;
attachClickEvent(button, async(e) => { attachClickEvent(button, async(e) => {
cancelEvent(e); cancelEvent(e);
@ -296,7 +296,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
this.managers.filtersStorage.createDialogFilter(f, true).then((bool) => { this.managers.filtersStorage.createDialogFilter(f, true).then((bool) => {
if(bool) { if(bool) {
div.remove(); row.container.remove();
} }
}).finally(() => { }).finally(() => {
button.removeAttribute('disabled'); button.removeAttribute('disabled');

8
src/components/sidebarLeft/tabs/generalSettings.ts

@ -331,17 +331,13 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
} }
}); });
this.listenerSetter.add(rootScope)('stickers_installed', (e) => { this.listenerSetter.add(rootScope)('stickers_installed', (set) => {
const set: StickerSet.stickerSet = e;
if(!stickerSets[set.id]) { if(!stickerSets[set.id]) {
renderStickerSet(set, 'prepend'); renderStickerSet(set, 'prepend');
} }
}); });
this.listenerSetter.add(rootScope)('stickers_deleted', (e) => { this.listenerSetter.add(rootScope)('stickers_deleted', (set) => {
const set: StickerSet.stickerSet = e;
if(stickerSets[set.id]) { if(stickerSets[set.id]) {
stickerSets[set.id].container.remove(); stickerSets[set.id].container.remove();
delete stickerSets[set.id]; delete stickerSets[set.id];

2
src/components/sidebarLeft/tabs/privacyAndSecurity.ts

@ -343,7 +343,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
return; return;
} }
enabled = settings.pFlags.sensitive_enabled; enabled = !!settings.pFlags.sensitive_enabled;
checkboxField.setValueSilently(enabled); checkboxField.setValueSilently(enabled);
section.container.classList.remove('hide'); section.container.classList.remove('hide');
}); });

4
src/components/sidebarRight/tabs/gifs.ts

@ -51,12 +51,12 @@ export default class AppGifsTab extends SliderSuperTab {
// this.backBtn.parentElement.append(this.inputSearch.container); // this.backBtn.parentElement.append(this.inputSearch.container);
} }
private onGifsClick = (e: MouseEvent | TouchEvent) => { private onGifsClick = async(e: MouseEvent | TouchEvent) => {
const target = findUpClassName(e.target, 'gif'); const target = findUpClassName(e.target, 'gif');
if(!target) return; if(!target) return;
const fileId = target.dataset.docId; const fileId = target.dataset.docId;
if(appImManager.chat.input.sendMessageWithDocument(fileId)) { if(await appImManager.chat.input.sendMessageWithDocument(fileId)) {
if(mediaSizes.isMobile) { if(mediaSizes.isMobile) {
appSidebarRight.onCloseBtnClick(); appSidebarRight.onCloseBtnClick();
} }

8
src/components/stickerViewer.ts

@ -12,7 +12,7 @@ import findUpClassName from '../helpers/dom/findUpClassName';
import getVisibleRect from '../helpers/dom/getVisibleRect'; import getVisibleRect from '../helpers/dom/getVisibleRect';
import ListenerSetter from '../helpers/listenerSetter'; import ListenerSetter from '../helpers/listenerSetter';
import {makeMediaSize} from '../helpers/mediaSize'; import {makeMediaSize} from '../helpers/mediaSize';
import {getMiddleware} from '../helpers/middleware'; import {getMiddleware, Middleware} from '../helpers/middleware';
import {doubleRaf} from '../helpers/schedulers'; import {doubleRaf} from '../helpers/schedulers';
import pause from '../helpers/schedulers/pause'; import pause from '../helpers/schedulers/pause';
import windowSize from '../helpers/windowSize'; import windowSize from '../helpers/windowSize';
@ -70,7 +70,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter,
const doThatSticker = async({mediaContainer, doc, middleware, lockGroups, isSwitching}: { const doThatSticker = async({mediaContainer, doc, middleware, lockGroups, isSwitching}: {
mediaContainer: HTMLElement, mediaContainer: HTMLElement,
doc: MyDocument, doc: MyDocument,
middleware: () => boolean, middleware: Middleware,
lockGroups?: boolean, lockGroups?: boolean,
isSwitching?: boolean isSwitching?: boolean
}) => { }) => {
@ -129,7 +129,7 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter,
transformer.append(stickerContainer, stickerEmoji); transformer.append(stickerContainer, stickerEmoji);
container.append(transformer); container.append(transformer);
const player = await wrapSticker({ const o = await wrapSticker({
doc, doc,
div: stickerContainer, div: stickerContainer,
group, group,
@ -151,6 +151,8 @@ export default function attachStickerViewerListeners({listenTo, listenerSetter,
document.body.append(container); document.body.append(container);
} }
const player = Array.isArray(o) ? o[0] : o;
const firstFramePromise = player instanceof RLottiePlayer ? const firstFramePromise = player instanceof RLottiePlayer ?
new Promise<void>((resolve) => player.addEventListener('firstFrame', resolve, {once: true})) : new Promise<void>((resolve) => player.addEventListener('firstFrame', resolve, {once: true})) :
Promise.resolve(); Promise.resolve();

3
src/components/wrappers/album.ts

@ -6,6 +6,7 @@
import {ChatAutoDownloadSettings} from '../../helpers/autoDownload'; import {ChatAutoDownloadSettings} from '../../helpers/autoDownload';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import {Middleware} from '../../helpers/middleware';
import {Message, PhotoSize} from '../../layer'; import {Message, PhotoSize} from '../../layer';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage'; import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
@ -20,7 +21,7 @@ import wrapVideo from './video';
export default function wrapAlbum({messages, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat, loadPromises, autoDownload, managers = rootScope.managers}: { export default function wrapAlbum({messages, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat, loadPromises, autoDownload, managers = rootScope.managers}: {
messages: Message.message[], messages: Message.message[],
attachmentDiv: HTMLElement, attachmentDiv: HTMLElement,
middleware?: () => boolean, middleware?: Middleware,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
uploading?: boolean, uploading?: boolean,
isOut: boolean, isOut: boolean,

3
src/components/wrappers/photo.ts

@ -23,6 +23,7 @@ import isWebDocument from '../../lib/appManagers/utils/webDocs/isWebDocument';
import createVideo from '../../helpers/dom/createVideo'; import createVideo from '../../helpers/dom/createVideo';
import noop from '../../helpers/noop'; import noop from '../../helpers/noop';
import {THUMB_TYPE_FULL} from '../../lib/mtproto/mtproto_config'; import {THUMB_TYPE_FULL} from '../../lib/mtproto/mtproto_config';
import {Middleware} from '../../helpers/middleware';
export default async function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, autoDownloadSize, noBlur, noThumb, noFadeIn, blurAfter, managers = rootScope.managers}: { export default async function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, autoDownloadSize, noBlur, noThumb, noFadeIn, blurAfter, managers = rootScope.managers}: {
photo: MyPhoto | MyDocument | WebDocument, photo: MyPhoto | MyDocument | WebDocument,
@ -33,7 +34,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
withTail?: boolean, withTail?: boolean,
isOut?: boolean, isOut?: boolean,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
middleware?: () => boolean, middleware?: Middleware,
size?: PhotoSize | VideoSize, size?: PhotoSize | VideoSize,
withoutPreloader?: boolean, withoutPreloader?: boolean,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],

75
src/components/wrappers/sticker.ts

@ -13,13 +13,13 @@ import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent} from '../../helpers/dom/clickEvent'; import {attachClickEvent} from '../../helpers/dom/clickEvent';
import createVideo from '../../helpers/dom/createVideo'; import createVideo from '../../helpers/dom/createVideo';
import findUpClassName from '../../helpers/dom/findUpClassName'; import findUpClassName from '../../helpers/dom/findUpClassName';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; import renderImageFromUrl, {renderImageFromUrlPromise} from '../../helpers/dom/renderImageFromUrl';
import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb'; import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb';
import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb'; import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import {makeMediaSize} from '../../helpers/mediaSize'; import {makeMediaSize} from '../../helpers/mediaSize';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import noop from '../../helpers/noop'; import {Middleware} from '../../helpers/middleware';
import onMediaLoad from '../../helpers/onMediaLoad'; import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle'; import throttle from '../../helpers/schedulers/throttle';
@ -53,8 +53,8 @@ const locksUrls: {[docId: string]: string} = {};
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}: { 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, doc: MyDocument,
div: HTMLElement | HTMLElement[], div: HTMLElement | HTMLElement[],
middleware?: () => boolean, middleware?: Middleware,
loadStickerMiddleware?: () => boolean, loadStickerMiddleware?: Middleware,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
exportLoad?: boolean, exportLoad?: boolean,
group?: AnimationItemGroup, group?: AnimationItemGroup,
@ -298,13 +298,14 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
return; return;
} }
const r = (div: HTMLElement, thumbImage: HTMLElement) => { const r = (div: HTMLElement, thumbImage: HTMLElement, rendered?: boolean) => {
if(div.childElementCount || (middleware && !middleware())) { if(div.childElementCount || (middleware && !middleware())) {
loadThumbPromise.resolve(); loadThumbPromise.resolve();
return; return;
} }
renderImageFromUrl(thumbImage, cacheContext.url, () => afterRender(div, thumbImage)); if(rendered) afterRender(div, thumbImage);
else renderImageFromUrl(thumbImage, cacheContext.url, () => afterRender(div, thumbImage));
}; };
await getCacheContext(); await getCacheContext();
@ -313,7 +314,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
r(div, new Image()); r(div, new Image());
} else if('bytes' in thumb) { } else if('bytes' in thumb) {
const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true); const res = getImageFromStrippedThumb(doc, thumb as PhotoSize.photoStrippedSize, true);
res.loadPromise.then(() => r(div, res.image)); res.loadPromise.then(() => r(div, res.image, true));
// return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r); // return managers.appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r);
} else { } else {
@ -444,7 +445,9 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
// await new Promise((resolve) => setTimeout(resolve, 5e3)); // await new Promise((resolve) => setTimeout(resolve, 5e3));
}); });
} else if(asStatic || stickerType === 3) { } else if(asStatic || stickerType === 3) {
const media: HTMLElement[] = (div as HTMLElement[]).map(() => { const isSingleVideo = isAnimated && false;
const d = isSingleVideo ? (div as HTMLElement[]).slice(0, 1) : div as HTMLElement[];
const media: HTMLElement[] = d.map(() => {
let media: HTMLElement; let media: HTMLElement;
if(asStatic) { if(asStatic) {
media = new Image(); media = new Image();
@ -468,19 +471,31 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
media.forEach((media) => media.classList.add('fade-in')); media.forEach((media) => media.classList.add('fade-in'));
} }
return new Promise<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => { return new Promise<HTMLVideoElement[] | HTMLImageElement[]>(async(resolve, reject) => {
const r = async() => { const r = async() => {
if(middleware && !middleware()) { if(middleware && !middleware()) {
reject(middlewareError); reject(middlewareError);
return; return;
} }
const mediaLength = media.length;
const loaded: HTMLElement[] = [];
const onLoad = (div: HTMLElement, media: HTMLElement, thumbImage: HTMLElement) => { const onLoad = (div: HTMLElement, media: HTMLElement, thumbImage: HTMLElement) => {
sequentialDom.mutateElement(div, () => { sequentialDom.mutateElement(div, () => {
div.append(media); if(middleware && !middleware()) {
thumbImage && thumbImage.classList.add('fade-out'); reject(middlewareError);
return;
}
if(!media) {
if(!isSingleVideo || !isAnimated) {
thumbImage?.remove();
}
if(stickerType === 3 && !isSavingLottiePreview(doc, toneIndex)) { return;
}
if(media as HTMLVideoElement && !isSavingLottiePreview(doc, toneIndex)) {
// const perf = performance.now(); // const perf = performance.now();
assumeType<HTMLVideoElement>(media); assumeType<HTMLVideoElement>(media);
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -492,30 +507,40 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
// console.log('perf', performance.now() - perf); // console.log('perf', performance.now() - perf);
} }
if(stickerType === 3 && group) { if(isSingleVideo) {
animationIntersector.addAnimation(media as HTMLVideoElement, group); resolve(media as any);
return;
} }
resolve(media as any); div.append(media);
if(needFadeIn) { if(needFadeIn) {
thumbImage && thumbImage.classList.add('fade-out');
media.addEventListener('animationend', () => { media.addEventListener('animationend', () => {
media.classList.remove('fade-in'); media.classList.remove('fade-in');
thumbImage?.remove(); thumbImage?.remove();
}, {once: true}); }, {once: true});
} else {
thumbImage?.remove();
}
if(isAnimated) {
animationIntersector.addAnimation(media as HTMLVideoElement, group);
}
if(loaded.push(media) === mediaLength) {
resolve(loaded as any);
} }
}); });
}; };
await getCacheContext(); await getCacheContext();
media.forEach((media, idx) => { let lastPromise: Promise<any>;
const cb = () => onLoad((div as HTMLElement[])[idx], media, thumbImage[idx]); (div as HTMLElement[]).forEach((div, idx) => {
if(asStatic) { const _media = media[idx];
renderImageFromUrl(media, cacheContext.url, cb); const cb = () => onLoad(div, _media, thumbImage[idx]);
} else { if(_media) lastPromise = renderImageFromUrlPromise(_media, cacheContext.url);
(media as HTMLVideoElement).src = cacheContext.url; lastPromise.then(cb);
onMediaLoad(media as HTMLVideoElement).then(cb);
}
}); });
}; };
@ -575,7 +600,7 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut
container: HTMLElement, container: HTMLElement,
doc: MyDocument, doc: MyDocument,
managers: AppManagers, managers: AppManagers,
middleware: () => boolean, middleware: Middleware,
isOut: boolean, isOut: boolean,
width: number, width: number,
loadPromise: Promise<any>, loadPromise: Promise<any>,
@ -634,7 +659,7 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m
container: HTMLElement, container: HTMLElement,
managers: AppManagers, managers: AppManagers,
peerId: PeerId, peerId: PeerId,
middleware: () => boolean middleware: Middleware
}) { }) {
if(!peerId.isUser()) { if(!peerId.isUser()) {
return; return;

3
src/components/wrappers/stickerAnimation.ts

@ -7,6 +7,7 @@
import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport'; import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import isInDOM from '../../helpers/dom/isInDOM'; import isInDOM from '../../helpers/dom/isInDOM';
import {Middleware} from '../../helpers/middleware';
import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf'; import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf';
import windowSize from '../../helpers/windowSize'; import windowSize from '../../helpers/windowSize';
import {PhotoSize, VideoSize} from '../../layer'; import {PhotoSize, VideoSize} from '../../layer';
@ -32,7 +33,7 @@ export default function wrapStickerAnimation({
}: { }: {
size: number, size: number,
doc: MyDocument, doc: MyDocument,
middleware?: () => boolean, middleware?: Middleware,
target: HTMLElement, target: HTMLElement,
side: 'left' | 'center' | 'right', side: 'left' | 'center' | 'right',
skipRatio?: number, skipRatio?: number,

3
src/components/wrappers/video.ts

@ -15,6 +15,7 @@ import isInDOM from '../../helpers/dom/isInDOM';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import getStrippedThumbIfNeeded from '../../helpers/getStrippedThumbIfNeeded'; import getStrippedThumbIfNeeded from '../../helpers/getStrippedThumbIfNeeded';
import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes'; import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes';
import {Middleware} from '../../helpers/middleware';
import noop from '../../helpers/noop'; import noop from '../../helpers/noop';
import onMediaLoad from '../../helpers/onMediaLoad'; import onMediaLoad from '../../helpers/onMediaLoad';
import {fastRaf} from '../../helpers/schedulers'; import {fastRaf} from '../../helpers/schedulers';
@ -69,7 +70,7 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH
boxHeight?: number, boxHeight?: number,
withTail?: boolean, withTail?: boolean,
isOut?: boolean, isOut?: boolean,
middleware?: () => boolean, middleware?: Middleware,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
noInfo?: boolean, noInfo?: boolean,
noPlayButton?: boolean, noPlayButton?: boolean,

8
src/global.d.ts vendored

@ -1,4 +1,5 @@
import type ListenerSetter from './helpers/listenerSetter'; import type ListenerSetter from './helpers/listenerSetter';
import type {Middleware, MiddlewareHelper} from './helpers/middleware';
import type {Chat, Document, User} from './layer'; import type {Chat, Document, User} from './layer';
declare global { declare global {
@ -12,6 +13,11 @@ declare global {
dpr?: number dpr?: number
} }
interface HTMLElement {
middlewareHelper?: MiddlewareHelper;
middleware?: Middleware;
}
type UserId = User.user['id']; type UserId = User.user['id'];
type ChatId = Chat.chat['id']; type ChatId = Chat.chat['id'];
// type PeerId = `u${UserId}` | `c${ChatId}`; // type PeerId = `u${UserId}` | `c${ChatId}`;
@ -65,4 +71,6 @@ declare global {
declare const electronHelpers: { declare const electronHelpers: {
openExternal(url): void; openExternal(url): void;
} | undefined; } | undefined;
type DOMRectMinified = {top: number, right: number, bottom: number, left: number};
} }

10
src/helpers/dom/createStickersContextMenu.ts

@ -18,9 +18,10 @@ export default function createStickersContextMenu(options: {
verifyRecent?: (target: HTMLElement) => boolean, verifyRecent?: (target: HTMLElement) => boolean,
appendTo?: HTMLElement, appendTo?: HTMLElement,
onOpen?: () => any, onOpen?: () => any,
onClose?: () => any onClose?: () => any,
onSend?: () => any
}) { }) {
const {listenTo, isStickerPack, verifyRecent, appendTo, onOpen, onClose} = options; const {listenTo, isStickerPack, verifyRecent, appendTo, onOpen, onClose, onSend} = options;
let target: HTMLElement, doc: MyDocument; let target: HTMLElement, doc: MyDocument;
const verifyFavoriteSticker = async(toAdd: boolean) => { const verifyFavoriteSticker = async(toAdd: boolean) => {
const favedStickers = await rootScope.managers.acknowledged.appStickersManager.getFavedStickersStickers(); const favedStickers = await rootScope.managers.acknowledged.appStickersManager.getFavedStickersStickers();
@ -64,7 +65,10 @@ export default function createStickersContextMenu(options: {
}, { }, {
icon: 'mute', icon: 'mute',
text: 'Chat.Send.WithoutSound', text: 'Chat.Send.WithoutSound',
onClick: () => EmoticonsDropdown.sendDocId(doc.id, false, true), onClick: () => {
onSend?.();
return EmoticonsDropdown.sendDocId(doc.id, false, true);
},
verify: () => !!(appImManager.chat.peerId && appImManager.chat.peerId !== rootScope.myId) verify: () => !!(appImManager.chat.peerId && appImManager.chat.peerId !== rootScope.myId)
}, { }, {
icon: 'schedule', icon: 'schedule',

63
src/helpers/dom/getViewportSlice.ts

@ -8,14 +8,25 @@ import getVisibleRect from './getVisibleRect';
export type ViewportSlicePart = {element: HTMLElement, rect: DOMRect, visibleRect: ReturnType<typeof getVisibleRect>}[]; export type ViewportSlicePart = {element: HTMLElement, rect: DOMRect, visibleRect: ReturnType<typeof getVisibleRect>}[];
export default function getViewportSlice({overflowElement, selector, extraSize}: { export default function getViewportSlice({overflowElement, overflowRect, selector, extraSize, elements}: {
overflowElement: HTMLElement, overflowElement: HTMLElement,
selector: string, overflowRect?: DOMRectMinified,
extraSize?: number extraSize?: number,
selector?: string,
elements?: HTMLElement[]
}) { }) {
// const perf = performance.now(); // const perf = performance.now();
const overflowRect = overflowElement.getBoundingClientRect(); overflowRect ??= overflowElement.getBoundingClientRect();
const elements = Array.from(overflowElement.querySelectorAll<HTMLElement>(selector)); elements ??= Array.from(overflowElement.querySelectorAll<HTMLElement>(selector));
if(extraSize) {
overflowRect = {
top: overflowRect.top - extraSize,
right: overflowRect.right + extraSize,
bottom: overflowRect.bottom + extraSize,
left: overflowRect.left - extraSize
};
}
const invisibleTop: ViewportSlicePart = [], const invisibleTop: ViewportSlicePart = [],
visible: typeof invisibleTop = [], visible: typeof invisibleTop = [],
@ -43,29 +54,29 @@ export default function getViewportSlice({overflowElement, selector, extraSize}:
}); });
} }
if(extraSize && visible.length) { // if(extraSize && visible.length) {
const maxTop = visible[0].rect.top; // const maxTop = visible[0].rect.top;
const minTop = maxTop - extraSize; // const minTop = maxTop - extraSize;
const minBottom = visible[visible.length - 1].rect.bottom; // const minBottom = visible[visible.length - 1].rect.bottom;
const maxBottom = minBottom + extraSize; // const maxBottom = minBottom + extraSize;
for(let length = invisibleTop.length, i = length - 1; i >= 0; --i) { // for(let length = invisibleTop.length, i = length - 1; i >= 0; --i) {
const element = invisibleTop[i]; // const element = invisibleTop[i];
if(element.rect.top >= minTop) { // if(element.rect.top >= minTop) {
invisibleTop.splice(i, 1); // invisibleTop.splice(i, 1);
visible.unshift(element); // visible.unshift(element);
} // }
} // }
for(let i = 0, length = invisibleBottom.length; i < length; ++i) { // for(let i = 0, length = invisibleBottom.length; i < length; ++i) {
const element = invisibleBottom[i]; // const element = invisibleBottom[i];
if(element.rect.bottom <= maxBottom) { // if(element.rect.bottom <= maxBottom) {
invisibleBottom.splice(i--, 1); // invisibleBottom.splice(i--, 1);
--length; // --length;
visible.push(element); // visible.push(element);
} // }
} // }
} // }
// console.log('getViewportSlice time:', performance.now() - perf); // console.log('getViewportSlice time:', performance.now() - perf);

12
src/helpers/dom/getVisibleRect.ts

@ -4,12 +4,14 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import windowSize from '../windowSize';
export default function getVisibleRect( export default function getVisibleRect(
element: HTMLElement, element: HTMLElement,
overflowElement: HTMLElement, overflowElement: HTMLElement,
lookForSticky?: boolean, lookForSticky?: boolean,
rect = element.getBoundingClientRect(), rect: DOMRectMinified = element.getBoundingClientRect(),
overflowRect = overflowElement.getBoundingClientRect() overflowRect: DOMRectMinified = overflowElement.getBoundingClientRect()
) { ) {
let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect; let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect;
@ -38,10 +40,8 @@ export default function getVisibleRect(
horizontal: 0 as 0 | 1 | 2 horizontal: 0 as 0 | 1 | 2
}; };
// @ts-ignore const windowWidth = windowSize.width;
const w: any = 'visualViewport' in window ? window.visualViewport : window; const windowHeight = windowSize.height;
const windowWidth = w.width || w.innerWidth;
const windowHeight = w.height || w.innerHeight;
return { return {
rect: { rect: {

21
src/helpers/dom/renderImageFromUrl.ts

@ -48,7 +48,8 @@ export default function renderImageFromUrl(
// const loader = new Image(); // const loader = new Image();
loader.src = url; loader.src = url;
// let perf = performance.now(); // let perf = performance.now();
loader.addEventListener('load', () => {
const onLoad = () => {
if(!isImage && elem) { if(!isImage && elem) {
set(elem, url); set(elem, url);
} }
@ -58,14 +59,18 @@ export default function renderImageFromUrl(
// TODO: переделать прогрузки аватаров до начала анимации, иначе с этим ожиданием они неприятно появляются // TODO: переделать прогрузки аватаров до начала анимации, иначе с этим ожиданием они неприятно появляются
// callback && getHeavyAnimationPromise().then(() => callback()); // callback && getHeavyAnimationPromise().then(() => callback());
callback?.(); callback?.();
}, {once: true});
if(callback) { loader.removeEventListener('error', onError);
loader.addEventListener('error', (err) => { };
console.error('Render image from url failed:', err, url, loader);
callback(); const onError = (err: ErrorEvent) => {
}); console.error('Render image from url failed:', err, url, loader);
} loader.removeEventListener('load', onLoad);
callback?.();
};
loader.addEventListener('load', onLoad, {once: true});
loader.addEventListener('error', onError, {once: true});
} }
} }

43
src/helpers/dom/requestVideoFrameCallbackPolyfill.ts

@ -0,0 +1,43 @@
// @ts-nocheck
// https://github.com/ThaUnknown/rvfc-polyfill/blob/main/index.js
if(!('requestVideoFrameCallback' in HTMLVideoElement.prototype) && 'getVideoPlaybackQuality' in HTMLVideoElement.prototype) {
HTMLVideoElement.prototype._rvfcpolyfillmap = {};
HTMLVideoElement.prototype.requestVideoFrameCallback = function(callback) {
const quality = this.getVideoPlaybackQuality();
const baseline = this.mozPresentedFrames || this.mozPaintedFrames || quality.totalVideoFrames - quality.droppedVideoFrames;
const check = (old, now) => {
const newquality = this.getVideoPlaybackQuality();
const presentedFrames = this.mozPresentedFrames || this.mozPaintedFrames || newquality.totalVideoFrames - newquality.droppedVideoFrames;
if(presentedFrames > baseline) {
const processingDuration = this.mozFrameDelay || (newquality.totalFrameDelay - quality.totalFrameDelay) || 0;
const timediff = now - old; // HighRes diff
callback(now, {
presentationTime: now + processingDuration * 1000,
expectedDisplayTime: now + timediff,
width: this.videoWidth,
height: this.videoHeight,
mediaTime: Math.max(0, this.currentTime || 0) + timediff / 1000,
presentedFrames,
processingDuration
});
delete this._rvfcpolyfillmap[handle];
} else {
this._rvfcpolyfillmap[handle] = requestAnimationFrame(newer => check(now, newer));
}
}
const handle = Date.now();
const now = performance.now();
this._rvfcpolyfillmap[handle] = requestAnimationFrame(newer => check(now, newer));
return handle; // spec says long, not doube, so can't re-use performance.now
};
HTMLVideoElement.prototype.cancelVideoFrameCallback = function(handle) {
cancelAnimationFrame(this._rvfcpolyfillmap[handle]);
delete this._rvfcpolyfillmap[handle];
};
}
export {};

2
src/helpers/dom/setInnerHTML.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
export default function setInnerHTML(elem: Element, html: string | DocumentFragment) { export default function setInnerHTML(elem: Element, html: string | DocumentFragment | Element) {
elem.setAttribute('dir', 'auto'); elem.setAttribute('dir', 'auto');
if(typeof(html) === 'string') { if(typeof(html) === 'string') {
if(!html) elem.textContent = ''; if(!html) elem.textContent = '';

86
src/helpers/framesCache.ts

@ -0,0 +1,86 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {RLottieColor} from '../lib/rlottie/rlottiePlayer';
export type FramesCacheMap = Map<number, Uint8ClampedArray>;
export type FramesCacheMapNew = Map<number, HTMLCanvasElement | ImageBitmap>;
export type FramesCacheMapURLs = Map<number, string>;
export type FramesCacheItem = {
frames: FramesCacheMap,
framesNew: FramesCacheMapNew,
framesURLs: FramesCacheMapURLs,
clearCache: () => void,
counter: number
};
export class FramesCache {
private cache: Map<string, FramesCacheItem>;
constructor() {
this.cache = new Map();
}
public static createCache(): FramesCacheItem {
const cache: FramesCacheItem = {
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 = FramesCache.createCache());
} else {
// console.warn('[RLottieCache] cache will be reused', cache);
}
++cache.counter;
return cache;
}
public releaseCache(name: string) {
const cache = this.cache.get(name);
if(cache && !--cache.counter) {
this.cache.delete(name);
// console.warn('[RLottieCache] released cache', cache);
}
}
public getCacheCounter(name: string) {
const cache = this.cache.get(name);
return cache?.counter;
}
public generateName(name: string, width: number, height: number, color: RLottieColor, toneIndex: number) {
return [
name,
width,
height,
// color ? rgbaToHexa(color) : ''
color ? 'colored' : '',
toneIndex || ''
].filter(Boolean).join('-');
}
}
const framesCache = new FramesCache();
export default framesCache;

5
src/helpers/idleController.ts

@ -8,6 +8,7 @@ import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
import EventListenerBase from './eventListenerBase'; import EventListenerBase from './eventListenerBase';
const FOCUS_EVENT_NAME = IS_TOUCH_SUPPORTED ? 'touchstart' : 'mousemove'; const FOCUS_EVENT_NAME = IS_TOUCH_SUPPORTED ? 'touchstart' : 'mousemove';
const DO_NOT_IDLE = false;
export class IdleController extends EventListenerBase<{ export class IdleController extends EventListenerBase<{
change: (idle: boolean) => void change: (idle: boolean) => void
@ -61,6 +62,10 @@ export class IdleController extends EventListenerBase<{
return; return;
} }
if(DO_NOT_IDLE && value) {
return;
}
this._isIdle = value; this._isIdle = value;
this.dispatchEvent('change', value); this.dispatchEvent('change', value);
} }

9
src/helpers/mediaSizes.ts

@ -20,7 +20,8 @@ type MediaTypeSizes = {
round: MediaSize, round: MediaSize,
documentName: MediaSize, documentName: MediaSize,
invoice: MediaSize, invoice: MediaSize,
customEmoji: MediaSize customEmoji: MediaSize,
esgCustomEmoji: MediaSize
}; };
export type MediaSizeType = keyof MediaTypeSizes; export type MediaSizeType = keyof MediaTypeSizes;
@ -58,7 +59,8 @@ class MediaSizes extends EventListenerBase<{
round: makeMediaSize(200, 200), round: makeMediaSize(200, 200),
documentName: makeMediaSize(200, 0), documentName: makeMediaSize(200, 0),
invoice: makeMediaSize(240, 240), invoice: makeMediaSize(240, 240),
customEmoji: makeMediaSize(18, 18) customEmoji: makeMediaSize(18, 18),
esgCustomEmoji: makeMediaSize(32, 32)
}, },
desktop: { desktop: {
regular: makeMediaSize(420, 340), regular: makeMediaSize(420, 340),
@ -72,7 +74,8 @@ class MediaSizes extends EventListenerBase<{
round: makeMediaSize(280, 280), round: makeMediaSize(280, 280),
documentName: makeMediaSize(240, 0), documentName: makeMediaSize(240, 0),
invoice: makeMediaSize(320, 260), invoice: makeMediaSize(320, 260),
customEmoji: makeMediaSize(18, 18) customEmoji: makeMediaSize(18, 18),
esgCustomEmoji: makeMediaSize(32, 32)
} }
}; };

96
src/helpers/middleware.ts

@ -4,19 +4,87 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
// * will change .cleaned and new instance will be created import indexOfAndSplice from './array/indexOfAndSplice';
export const getMiddleware = () => { import makeError from './makeError';
let cleanupObj = {cleaned: false};
return { export type Middleware = {
clean: () => { (): boolean;
cleanupObj.cleaned = true; create(): MiddlewareHelper;
cleanupObj = {cleaned: false}; onClean: (callback: VoidFunction) => void;
}, onDestroy: (callback: VoidFunction) => void;
get: (additionalCallback?: () => boolean) => { };
const _cleanupObj = cleanupObj;
return () => { const createDetails = (): {
return !_cleanupObj.cleaned && (!additionalCallback || additionalCallback()); cleaned?: boolean,
}; inner: MiddlewareHelper[],
onCleanCallbacks: VoidFunction[]
} => ({
cleaned: false,
inner: [],
onCleanCallbacks: []
});
const MIDDLEWARE_ERROR = makeError('MIDDLEWARE');
// * onClean == cancel promises, etc
// * onDestroy == destructor
export class MiddlewareHelper {
private details = createDetails();
private onDestroyCallbacks: VoidFunction[] = [];
private parent: MiddlewareHelper;
private destroyed: boolean;
public clean() {
const details = this.details;
details.cleaned = true;
details.inner.splice(0, details.inner.length).forEach((helper) => helper.destroy());
details.onCleanCallbacks.splice(0, details.onCleanCallbacks.length).forEach((callback) => callback());
this.details = createDetails();
}
public destroy() {
this.destroyed = true;
this.clean();
this.onDestroyCallbacks.splice(0, this.onDestroyCallbacks.length).forEach((callback) => callback());
if(this.parent) {
indexOfAndSplice(this.parent.details.inner, this);
this.parent = undefined;
} }
}
public get(additionalCallback?: () => boolean) {
const details = this.details;
const middleware: Middleware = () => {
return !details.cleaned && (!additionalCallback || additionalCallback());
};
middleware.create = () => {
if(!middleware()) throw MIDDLEWARE_ERROR;
const helper = new MiddlewareHelper();
helper.parent = this;
details.inner.push(helper);
return helper;
};
middleware.onClean = (callback) => {
if(!middleware()) return callback();
details.onCleanCallbacks.push(callback);
};
middleware.onDestroy = this.onDestroy;
return middleware;
}
public onDestroy = (callback: VoidFunction) => {
if(this.destroyed) return callback();
this.onDestroyCallbacks.push(callback);
}; };
}; }
// * will change .cleaned and new instance will be created
export function getMiddleware() {
return new MiddlewareHelper();
}

12
src/lang.ts

@ -763,6 +763,18 @@ const lang = {
'PrivacyVoiceMessagesTitle': 'Who can send me voice or video messages?', 'PrivacyVoiceMessagesTitle': 'Who can send me voice or video messages?',
'PrivacyVoiceMessagesInfo': 'You can restrict who can send you voice or video messages with granular precision.', 'PrivacyVoiceMessagesInfo': 'You can restrict who can send you voice or video messages with granular precision.',
'PrivacyVoiceMessagesPremiumOnly': 'Only subscribers of *Telegram Premium* can restrict receiving voice messages.', 'PrivacyVoiceMessagesPremiumOnly': 'Only subscribers of *Telegram Premium* can restrict receiving voice messages.',
'EmojiCount': {
'other_value': '%1$d emoji'
},
'AddEmojiNotFound': 'Emoji pack not found.',
'MessageContainsEmojiPack': 'This message contains emoji from %s pack.',
'MessageContainsEmojiPacks': {
'other_value': 'This message contains emoji from **%d Packs**.'
},
'EmojiPackCount': {
'one_value': '%1$d Emoji Pack',
'other_value': '%1$d Emoji Packs'
},
// * macos // * macos
'AccountSettings.Filters': 'Chat Folders', 'AccountSettings.Filters': 'Chat Folders',

63
src/lib/appManagers/appImManager.ts

@ -543,20 +543,41 @@ export class AppImManager extends EventListenerBase<{
} }
}); });
this.addAnchorListener<{pathnameParams: ['addstickers', string]}>({ ([
name: 'addstickers', ['addstickers', INTERNAL_LINK_TYPE.STICKER_SET],
callback: ({pathnameParams}) => { ['addemoji', INTERNAL_LINK_TYPE.EMOJI_SET]
if(!pathnameParams[1]) { ] as [
return; 'addstickers' | 'addemoji',
} INTERNAL_LINK_TYPE.STICKER_SET | INTERNAL_LINK_TYPE.EMOJI_SET
][]).forEach(([name, type]) => {
this.addAnchorListener<{pathnameParams: [typeof name, string]}>({
name,
callback: ({pathnameParams}) => {
if(!pathnameParams[1]) {
return;
}
const link: InternalLink = { const link: InternalLink = {
_: INTERNAL_LINK_TYPE.STICKER_SET, _: type,
set: pathnameParams[1] set: pathnameParams[1]
}; };
this.processInternalLink(link); this.processInternalLink(link);
} }
});
this.addAnchorListener<{
uriParams: {
set: string
}
}>({
name,
protocol: 'tg',
callback: ({uriParams}) => {
const link = this.makeLink(type, uriParams);
this.processInternalLink(link);
}
});
}); });
// * t.me/invoice/asdasdad // * t.me/invoice/asdasdad
@ -693,19 +714,6 @@ export class AppImManager extends EventListenerBase<{
} }
}); });
this.addAnchorListener<{
uriParams: {
set: string
}
}>({
name: 'addstickers',
protocol: 'tg',
callback: ({uriParams}) => {
const link = this.makeLink(INTERNAL_LINK_TYPE.STICKER_SET, uriParams);
this.processInternalLink(link);
}
});
this.addAnchorListener<{ this.addAnchorListener<{
uriParams: { uriParams: {
slug: string slug: string
@ -889,8 +897,9 @@ export class AppImManager extends EventListenerBase<{
break; break;
} }
case INTERNAL_LINK_TYPE.EMOJI_SET:
case INTERNAL_LINK_TYPE.STICKER_SET: { case INTERNAL_LINK_TYPE.STICKER_SET: {
new PopupStickers({id: link.set}).show(); new PopupStickers({id: link.set}, link._ === INTERNAL_LINK_TYPE.EMOJI_SET).show();
break; break;
} }
@ -982,7 +991,7 @@ export class AppImManager extends EventListenerBase<{
private addAnchorListener<Params extends {pathnameParams?: any, uriParams?: any}>(options: { private addAnchorListener<Params extends {pathnameParams?: any, uriParams?: any}>(options: {
name: 'showMaskedAlert' | 'execBotCommand' | 'searchByHashtag' | 'addstickers' | 'im' | name: 'showMaskedAlert' | 'execBotCommand' | 'searchByHashtag' | 'addstickers' | 'im' |
'resolve' | 'privatepost' | 'addstickers' | 'voicechat' | 'joinchat' | 'join' | 'invoice' | 'resolve' | 'privatepost' | 'addstickers' | 'voicechat' | 'joinchat' | 'join' | 'invoice' |
'emoji', 'addemoji',
protocol?: 'tg', protocol?: 'tg',
callback: (params: Params, element?: HTMLAnchorElement) => boolean | any, callback: (params: Params, element?: HTMLAnchorElement) => boolean | any,
noPathnameParams?: boolean, noPathnameParams?: boolean,

51
src/lib/appManagers/appStickersManager.ts

@ -55,10 +55,13 @@ export class AppStickersManager extends AppManager {
private favedStickers: MyDocument[]; private favedStickers: MyDocument[];
private recentStickers: MyDocument[]; private recentStickers: MyDocument[];
private names: Record<string, InputStickerSet.inputStickerSetID>;
protected after() { protected after() {
this.getStickerSetPromises = {}; this.getStickerSetPromises = {};
this.getStickersByEmoticonsPromises = {}; this.getStickersByEmoticonsPromises = {};
this.sounds = {}; this.sounds = {};
this.names = {};
this.rootScope.addEventListener('user_auth', () => { this.rootScope.addEventListener('user_auth', () => {
setTimeout(() => { setTimeout(() => {
@ -133,25 +136,43 @@ export class AppStickersManager extends AppManager {
}); });
} }
public async getStickerSet(set: MyStickerSetInput, params: Partial<{ private canUseStickerSetCache(set: MyMessagesStickerSet, useCache?: boolean) {
return set && set.documents?.length && ((Date.now() - set.refreshTime) < CACHE_TIME || useCache);
}
public getStickerSet(set: MyStickerSetInput, params: Partial<{
overwrite: boolean, overwrite: boolean,
useCache: boolean, useCache: boolean,
saveById: boolean saveById: boolean
}> = {}): Promise<MyMessagesStickerSet> { }> = {}): Promise<MyMessagesStickerSet> | MyMessagesStickerSet {
const id = set.id; let {id} = set;
if(!set.access_hash) {
set = this.names[id] || set;
id = set.id;
}
if(this.getStickerSetPromises[id]) { if(this.getStickerSetPromises[id]) {
return this.getStickerSetPromises[id]; return this.getStickerSetPromises[id];
} }
return this.getStickerSetPromises[id] = new Promise(async(resolve) => { if(!params.overwrite) {
const cachedSet = this.storage.getFromCache(id);
if(this.canUseStickerSetCache(cachedSet, params.useCache)) {
return cachedSet;
}
}
const promise = this.getStickerSetPromises[id] = new Promise(async(resolve) => {
if(!params.overwrite) { if(!params.overwrite) {
// const perf = performance.now();
const cachedSet = await this.storage.get(id); const cachedSet = await this.storage.get(id);
if(cachedSet && cachedSet.documents?.length && ((Date.now() - cachedSet.refreshTime) < CACHE_TIME || params.useCache)) { if(this.canUseStickerSetCache(cachedSet, params.useCache)) {
this.saveStickers(cachedSet.documents); this.saveStickers(cachedSet.documents);
resolve(cachedSet); resolve(cachedSet);
delete this.getStickerSetPromises[id];
// console.log('get sticker set from cache time', id, performance.now() - perf); if(this.getStickerSetPromises[id] === promise) {
delete this.getStickerSetPromises[id];
}
return; return;
} }
} }
@ -170,8 +191,12 @@ export class AppStickersManager extends AppManager {
resolve(null); resolve(null);
} }
delete this.getStickerSetPromises[id]; if(this.getStickerSetPromises[id] === promise) {
delete this.getStickerSetPromises[id];
}
}); });
return promise;
} }
public getAnimatedEmojiStickerSet() { public getAnimatedEmojiStickerSet() {
@ -377,6 +402,10 @@ export class AppStickersManager extends AppManager {
stickerSet = this.storage.setToCache(id, newSet); stickerSet = this.storage.setToCache(id, newSet);
} }
if(stickerSet.set.short_name) {
this.names[stickerSet.set.short_name] = this.getStickerSetInput(newSet.set) as any;
}
this.saveStickers(res.documents); this.saveStickers(res.documents);
// console.log('stickers wrote', this.stickerSets); // console.log('stickers wrote', this.stickerSets);
@ -553,6 +582,10 @@ export class AppStickersManager extends AppManager {
return false; return false;
} }
public toggleStickerSets(sets: StickerSet.stickerSet[]) {
return Promise.all(sets.map((set) => this.toggleStickerSet(set)));
}
public async searchStickerSets(query: string, excludeFeatured = true) { public async searchStickerSets(query: string, excludeFeatured = true) {
const flags = excludeFeatured ? 1 : 0; const flags = excludeFeatured ? 1 : 0;
const res = await this.apiManager.invokeApiHashable({ const res = await this.apiManager.invokeApiHashable({

506
src/lib/richTextProcessor/wrapRichText.ts

@ -24,13 +24,18 @@ import rootScope from '../rootScope';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import {wrapSticker} from '../../components/wrappers'; import {wrapSticker} from '../../components/wrappers';
import RLottiePlayer from '../rlottie/rlottiePlayer'; import RLottiePlayer from '../rlottie/rlottiePlayer';
import animationIntersector from '../../components/animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../../components/animationIntersector';
import type {MyDocument} from '../appManagers/appDocsManager'; import type {MyDocument} from '../appManagers/appDocsManager';
import LazyLoadQueue from '../../components/lazyLoadQueue'; import LazyLoadQueue from '../../components/lazyLoadQueue';
import {Awaited} from '../../types'; import {Awaited} from '../../types';
// import sequentialDom from '../../helpers/sequentialDom';
import {MediaSize} from '../../helpers/mediaSize'; import {MediaSize} from '../../helpers/mediaSize';
import IS_WEBM_SUPPORTED from '../../environment/webmSupport'; import IS_WEBM_SUPPORTED from '../../environment/webmSupport';
import assumeType from '../../helpers/assumeType';
import noop from '../../helpers/noop';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import findUpClassName from '../../helpers/dom/findUpClassName';
import getViewportSlice from '../../helpers/dom/getViewportSlice';
import {getMiddleware, Middleware} from '../../helpers/middleware';
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for(const entry of entries) { for(const entry of entries) {
@ -40,25 +45,104 @@ const resizeObserver = new ResizeObserver((entries) => {
}); });
class CustomEmojiElement extends HTMLElement { class CustomEmojiElement extends HTMLElement {
public elements: CustomEmojiElement[];
public renderer: CustomEmojiRendererElement;
public player: RLottiePlayer | HTMLVideoElement;
public paused: boolean;
public syncedPlayer: SyncedPlayer;
constructor() {
super();
this.paused = true;
}
public connectedCallback() {
if(this.player) {
animationIntersector.addAnimation(this, this.renderer.animationGroup);
}
this.connectedCallback = undefined;
}
public disconnectedCallback() {
if(this.syncedPlayer) {
this.syncedPlayer.pausedElements.delete(this);
}
// otherwise https://bugs.chromium.org/p/chromium/issues/detail?id=1144736#c27 will happen
this.textContent = '';
this.disconnectedCallback = this.elements = this.renderer = this.player = this.syncedPlayer = undefined;
}
public pause() {
if(this.paused) {
return;
}
this.paused = true;
if(this.player instanceof HTMLVideoElement) {
this.renderer.lastPausedVideo = this.player;
this.player.pause();
}
if(this.syncedPlayer && !this.syncedPlayer.pausedElements.has(this)) {
this.syncedPlayer.pausedElements.add(this);
if(this.syncedPlayer.pausedElements.size === this.syncedPlayer.elementsCounter) {
this.syncedPlayer.player.pause();
}
}
}
public play() {
if(!this.paused) {
return;
}
this.paused = false;
if(this.player instanceof HTMLVideoElement) {
this.player.currentTime = this.renderer.lastPausedVideo?.currentTime || this.player.currentTime;
this.player.play().catch(noop);
}
if(this.syncedPlayer && this.syncedPlayer.pausedElements.has(this)) {
this.syncedPlayer.pausedElements.delete(this);
if(this.syncedPlayer.pausedElements.size !== this.syncedPlayer.elementsCounter) {
this.player.play();
}
}
}
public remove() {
this.elements = this.renderer = this.player = undefined;
}
public get autoplay() {
return true;
}
} }
export class CustomEmojiRendererElement extends HTMLElement { export class CustomEmojiRendererElement extends HTMLElement {
public canvas: HTMLCanvasElement; public canvas: HTMLCanvasElement;
public context: CanvasRenderingContext2D; public context: CanvasRenderingContext2D;
public players: Map<CustomEmojiElement[], RLottiePlayer>; public playersSynced: Map<CustomEmojiElement[], RLottiePlayer | HTMLVideoElement>;
public clearedContainers: Set<CustomEmojiElement[]>; public syncedElements: Map<SyncedPlayer, CustomEmojiElement[]>;
public clearedElements: Set<CustomEmojiElement[]>;
public paused: boolean; public lastPausedVideo: HTMLVideoElement;
public autoplay: boolean;
public middleware: () => boolean;
public keys: string[];
public lastRect: DOMRect; public lastRect: DOMRect;
public isDimensionsSet: boolean; public isDimensionsSet: boolean;
public animationGroup: AnimationItemGroup;
public size: MediaSize;
public lazyLoadQueue: LazyLoadQueue;
constructor() { constructor() {
super(); super();
@ -68,36 +152,49 @@ export class CustomEmojiRendererElement extends HTMLElement {
this.context = this.canvas.getContext('2d'); this.context = this.canvas.getContext('2d');
this.append(this.canvas); this.append(this.canvas);
this.paused = false; this.playersSynced = new Map();
this.autoplay = true; this.syncedElements = new Map();
this.players = new Map(); this.clearedElements = new Set();
this.clearedContainers = new Set();
this.keys = []; this.animationGroup = 'EMOJI';
} }
public connectedCallback() { public connectedCallback() {
// this.setDimensions(); // this.setDimensions();
animationIntersector.addAnimation(this, 'EMOJI'); // animationIntersector.addAnimation(this, this.animationGroup);
resizeObserver.observe(this.canvas); resizeObserver.observe(this.canvas);
emojiRenderers.push(this);
this.connectedCallback = undefined; this.connectedCallback = undefined;
} }
public disconnectedCallback() { public disconnectedCallback() {
for(const key of this.keys) { for(const [syncedPlayer, elements] of this.syncedElements) {
const l = lotties.get(key); if(syncedPlayers.get(syncedPlayer.key) !== syncedPlayer) {
if(!l) {
continue; continue;
} }
if(!--l.counter) { if(elements) {
if(l.player instanceof RLottiePlayer) { syncedPlayer.elementsCounter -= elements.length;
l.player.remove(); }
if(!--syncedPlayer.counter) {
if(syncedPlayer.player) {
const frame = syncedPlayersFrames.get(syncedPlayer.player);
if(frame) {
(frame as ImageBitmap).close?.();
syncedPlayersFrames.delete(syncedPlayer.player);
}
syncedPlayersFrames.delete(syncedPlayer.player);
syncedPlayer.player.overrideRender = noop;
syncedPlayer.player.remove();
syncedPlayer.player = undefined;
} }
lotties.delete(key); syncedPlayers.delete(syncedPlayer.key);
if(!lotties.size) { if(!syncedPlayers.size) {
clearRenderInterval(); clearRenderInterval();
} }
} }
@ -105,21 +202,61 @@ export class CustomEmojiRendererElement extends HTMLElement {
resizeObserver.unobserve(this.canvas); resizeObserver.unobserve(this.canvas);
this.disconnectedCallback = undefined; indexOfAndSplice(emojiRenderers, this);
this.playersSynced.clear();
this.syncedElements.clear();
this.clearedElements.clear();
this.lazyLoadQueue?.clear();
this.middlewareHelper?.clean();
this.disconnectedCallback = this.lastPausedVideo = this.lazyLoadQueue = undefined;
} }
public getOffsets(offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map()) { public getOffsets(offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map()) {
for(const [containers, player] of this.players) { if(!this.playersSynced.size) {
const offsets = containers.map((container) => { return offsetsMap;
return { }
top: container.offsetTop,
left: container.offsetLeft const overflowElement = findUpClassName(this, 'scrollable') || this.offsetParent as HTMLElement;
}; const overflowRect = overflowElement.getBoundingClientRect();
const rect = this.getBoundingClientRect();
for(const elements of this.playersSynced.keys()) {
const {visible} = getViewportSlice({
overflowElement,
overflowRect,
elements,
extraSize: this.size.height * 2.5 // let's add some margin
});
const offsets = visible.map(({rect: elementRect}) => {
const top = elementRect.top - rect.top;
const left = elementRect.left - rect.left;
return {top, left};
}); });
offsetsMap.set(containers, offsets); if(offsets.length) {
offsetsMap.set(elements, offsets);
}
} }
// const rect = this.getBoundingClientRect();
// const visibleRect = getVisibleRect(this, overflowElement, undefined, rect);
// const minTop = visibleRect ? visibleRect.rect.top - this.size.height : 0;
// const maxTop = Infinity;
// for(const elements of this.playersSynced.keys()) {
// const offsets = elements.map((element) => {
// const elementRect = element.getBoundingClientRect();
// const top = elementRect.top - rect.top;
// const left = elementRect.left - rect.left;
// return top >= minTop && (top + elementRect.height) <= maxTop ? {top, left} : undefined;
// }).filter(Boolean);
// if(offsets.length) {
// offsetsMap.set(elements, offsets);
// }
// }
return offsetsMap; return offsetsMap;
} }
@ -135,24 +272,24 @@ export class CustomEmojiRendererElement extends HTMLElement {
} }
const {width, height, dpr} = canvas; const {width, height, dpr} = canvas;
for(const [containers, player] of this.players) { for(const [elements, offsets] of offsetsMap) {
const frame = topFrames.get(player); const player = this.playersSynced.get(elements);
const frame = syncedPlayersFrames.get(player);
if(!frame) { if(!frame) {
continue; continue;
} }
const isImageData = frame instanceof ImageData; const isImageData = frame instanceof ImageData;
const {width: stickerWidth, height: stickerHeight} = player.canvas[0]; const {width: frameWidth, height: frameHeight} = frame;
const offsets = offsetsMap.get(containers); const maxTop = height - frameHeight;
const maxTop = height - stickerHeight; const maxLeft = width - frameWidth;
const maxLeft = width - stickerWidth;
if(!this.clearedElements.has(elements)) {
if(!this.clearedContainers.has(containers)) { elements.forEach((element) => {
containers.forEach((container) => { element.textContent = '';
container.textContent = '';
}); });
this.clearedContainers.add(containers); this.clearedElements.add(elements);
} }
offsets.forEach(({top, left}) => { offsets.forEach(({top, left}) => {
@ -165,15 +302,15 @@ export class CustomEmojiRendererElement extends HTMLElement {
context.putImageData(frame as ImageData, left, top); context.putImageData(frame as ImageData, left, top);
} else { } else {
// context.clearRect(left, top, width, height); // context.clearRect(left, top, width, height);
context.drawImage(frame as ImageBitmap, left, top, stickerWidth, stickerHeight); context.drawImage(frame as ImageBitmap, left, top, frameWidth, frameHeight);
} }
}); });
} }
} }
public checkForAnyFrame() { public checkForAnyFrame() {
for(const [containers, player] of this.players) { for(const player of this.playersSynced.values()) {
if(topFrames.has(player)) { if(syncedPlayersFrames.has(player)) {
return true; return true;
} }
} }
@ -181,16 +318,8 @@ export class CustomEmojiRendererElement extends HTMLElement {
return false; return false;
} }
public pause() {
this.paused = true;
}
public play() {
this.paused = false;
}
public remove() { public remove() {
this.canvas.remove(); // this.canvas.remove();
} }
// public setDimensions() { // public setDimensions() {
@ -219,51 +348,61 @@ export class CustomEmojiRendererElement extends HTMLElement {
} }
} }
type R = CustomEmojiRendererElement; type CustomEmojiRenderer = CustomEmojiRendererElement;
type SyncedPlayer = {
let renderInterval: number; player: RLottiePlayer,
const top: Array<R> = []; middlewares: Set<() => boolean>,
const topFrames: Map<RLottiePlayer, Parameters<RLottiePlayer['overrideRender']>[0]> = new Map(); pausedElements: Set<CustomEmojiElement>,
const lotties: Map<string, {player: Promise<RLottiePlayer> | RLottiePlayer, middlewares: Set<() => boolean>, counter: number}> = new Map(); elementsCounter: number,
const rerere = () => { counter: number,
const t = top.filter((r) => !r.paused && r.isConnected && r.checkForAnyFrame()); key: string
};
type CustomEmojiFrame = Parameters<RLottiePlayer['overrideRender']>[0] | HTMLVideoElement;
const CUSTOM_EMOJI_INSTANT_PLAY = true; // do not wait for animationIntersector
let emojiRenderInterval: number;
const emojiRenderers: Array<CustomEmojiRenderer> = [];
const syncedPlayers: Map<string, SyncedPlayer> = new Map();
const syncedPlayersFrames: Map<RLottiePlayer | HTMLVideoElement, CustomEmojiFrame> = new Map();
const renderEmojis = () => {
const t = emojiRenderers.filter((r) => r.isConnected && r.checkForAnyFrame());
if(!t.length) { if(!t.length) {
return; return;
} }
const offsetsMap: Map<CustomEmojiElement[], {top: number, left: number}[]> = new Map(); const o = t.map((renderer) => {
for(const r of t) { const offsets = renderer.getOffsets();
r.getOffsets(offsetsMap); return offsets.size ? [renderer, offsets] as const : undefined;
} }).filter(Boolean);
for(const r of t) { for(const [renderer] of o) {
r.clearCanvas(); renderer.clearCanvas();
} }
for(const r of t) { for(const [renderer, offsets] of o) {
r.render(offsetsMap); renderer.render(offsets);
} }
}; };
const CUSTOM_EMOJI_FPS = 60; const CUSTOM_EMOJI_FPS = 60;
const CUSTOM_EMOJI_FRAME_INTERVAL = 1000 / CUSTOM_EMOJI_FPS; const CUSTOM_EMOJI_FRAME_INTERVAL = 1000 / CUSTOM_EMOJI_FPS;
const setRenderInterval = () => { const setRenderInterval = () => {
if(renderInterval) { if(emojiRenderInterval) {
return; return;
} }
renderInterval = window.setInterval(rerere, CUSTOM_EMOJI_FRAME_INTERVAL); emojiRenderInterval = window.setInterval(renderEmojis, CUSTOM_EMOJI_FRAME_INTERVAL);
rerere(); renderEmojis();
}; };
const clearRenderInterval = () => { const clearRenderInterval = () => {
if(!renderInterval) { if(!emojiRenderInterval) {
return; return;
} }
clearInterval(renderInterval); clearInterval(emojiRenderInterval);
renderInterval = undefined; emojiRenderInterval = undefined;
}; };
(window as any).lotties = lotties; (window as any).syncedPlayers = syncedPlayers;
customElements.define('custom-emoji-element', CustomEmojiElement); customElements.define('custom-emoji-element', CustomEmojiElement);
customElements.define('custom-emoji-renderer-element', CustomEmojiRendererElement); customElements.define('custom-emoji-renderer-element', CustomEmojiRendererElement);
@ -288,6 +427,8 @@ export default function wrapRichText(text: string, options: Partial<{
noEncoding: boolean, noEncoding: boolean,
contextHashtag?: string, contextHashtag?: string,
// ! recursive, do not provide
nasty?: { nasty?: {
i: number, i: number,
usedLength: number, usedLength: number,
@ -296,11 +437,13 @@ export default function wrapRichText(text: string, options: Partial<{
}, },
voodoo?: boolean, voodoo?: boolean,
customEmojis?: {[docId: DocId]: CustomEmojiElement[]}, customEmojis?: {[docId: DocId]: CustomEmojiElement[]},
loadPromises?: Promise<any>[],
middleware?: () => boolean,
wrappingSpoiler?: boolean, wrappingSpoiler?: boolean,
loadPromises?: Promise<any>[],
middleware?: Middleware,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
customEmojiSize?: MediaSize customEmojiSize?: MediaSize,
animationGroup?: AnimationItemGroup
}> = {}) { }> = {}) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
if(!text) { if(!text) {
@ -751,30 +894,40 @@ export default function wrapRichText(text: string, options: Partial<{
const docIds = Object.keys(customEmojis) as DocId[]; const docIds = Object.keys(customEmojis) as DocId[];
if(docIds.length) { if(docIds.length) {
const managers = rootScope.managers; const managers = rootScope.managers;
const middleware = options.middleware; const size = options.customEmojiSize || mediaSizes.active.customEmoji;
const renderer = new CustomEmojiRendererElement(); const renderer = new CustomEmojiRendererElement();
renderer.middleware = middleware; // const middleware = () => !!renderer.disconnectedCallback && (!options.middleware || options.middleware());
top.push(renderer); let middleware: Middleware;
if(options.middleware) {
middleware = options.middleware;
options.middleware.onDestroy(() => {
renderer.disconnectedCallback?.();
});
} else {
renderer.middlewareHelper = getMiddleware();
middleware = renderer.middlewareHelper.get();
}
renderer.animationGroup = options.animationGroup;
renderer.size = size;
fragment.prepend(renderer); fragment.prepend(renderer);
const size = options.customEmojiSize || mediaSizes.active.customEmoji;
const loadPromise = managers.appEmojiManager.getCachedCustomEmojiDocuments(docIds).then((docs) => { const loadPromise = managers.appEmojiManager.getCachedCustomEmojiDocuments(docIds).then((docs) => {
// console.log(docs);
if(middleware && !middleware()) return; if(middleware && !middleware()) return;
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
const wrap = (doc: MyDocument, _loadPromises?: Promise<any>[]): Promise<Awaited<ReturnType<typeof wrapSticker>> & {onRender?: () => void}> => { const wrap = (doc: MyDocument, _loadPromises?: Promise<any>[]) => {
const containers = customEmojis[doc.id]; const elements = customEmojis[doc.id];
const isLottie = doc.sticker === 2; const isLottie = doc.sticker === 2;
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
const promise = wrapSticker({ const promise = wrapSticker({
div: containers, div: elements,
doc, doc,
width: size.width, width: size.width,
height: size.height, height: size.height,
loop: true, loop: true,
play: true, play: CUSTOM_EMOJI_INSTANT_PLAY,
managers, managers,
isCustomEmoji: true, isCustomEmoji: true,
group: 'none', group: 'none',
@ -782,20 +935,20 @@ export default function wrapRichText(text: string, options: Partial<{
middleware, middleware,
exportLoad: true, exportLoad: true,
needFadeIn: false, needFadeIn: false,
loadStickerMiddleware: isLottie && middleware ? () => { loadStickerMiddleware: isLottie && middleware ? middleware.create().get(() => {
if(lotties.get(key) !== l) { if(syncedPlayers.get(key) !== syncedPlayer) {
return false; return false;
} }
let good = !l.middlewares.size; let good = !syncedPlayer.middlewares.size;
for(const middleware of l.middlewares) { for(const middleware of syncedPlayer.middlewares) {
if(middleware()) { if(middleware()) {
good = true; good = true;
} }
} }
return good; return good;
} : undefined, }) : undefined,
static: doc.mime_type === 'video/webm' && !IS_WEBM_SUPPORTED static: doc.mime_type === 'video/webm' && !IS_WEBM_SUPPORTED
}); });
@ -803,49 +956,138 @@ export default function wrapRichText(text: string, options: Partial<{
promise.then(() => _loadPromises.push(...loadPromises)); promise.then(() => _loadPromises.push(...loadPromises));
} }
if(!isLottie) { const addition: {
return promise; onRender?: (_p: Awaited<Awaited<typeof promise>['render']>) => Promise<void>,
elements: typeof elements
} = {
elements
};
if(doc.sticker === 1) {
return promise.then((res) => ({...res, ...addition}));
} }
const onRender = (player: Awaited<Awaited<typeof promise>['render']>) => Promise.all(loadPromises).then(() => { // eslint-disable-next-line prefer-const
if(player instanceof RLottiePlayer && (!middleware || middleware())) { addition.onRender = (_p) => Promise.all(loadPromises).then(() => {
l.player = player; if((middleware && !middleware()) || !doc.animated) {
return;
}
const playerCanvas = player.canvas[0]; const players = Array.isArray(_p) ? _p as HTMLVideoElement[] : [_p as RLottiePlayer];
renderer.canvas.dpr = playerCanvas.dpr; const player = Array.isArray(players) ? players[0] : players;
renderer.players.set(containers, player); assumeType<RLottiePlayer | HTMLVideoElement>(player);
elements.forEach((element, idx) => {
const player = players[idx] || players[0];
element.renderer = renderer;
element.elements = elements;
element.player = player;
if(syncedPlayer) {
element.syncedPlayer = syncedPlayer;
if(element.paused) {
element.syncedPlayer.pausedElements.add(element);
}
}
setRenderInterval(); if(element.isConnected) {
animationIntersector.addAnimation(element, element.renderer.animationGroup);
}
});
if(syncedPlayer) {
syncedPlayer.elementsCounter += elements.length;
syncedPlayer.middlewares.delete(middleware);
renderer.syncedElements.set(syncedPlayer, elements);
}
if(player instanceof RLottiePlayer) {
syncedPlayer.player = player;
renderer.playersSynced.set(elements, player);
renderer.canvas.dpr = player.canvas[0].dpr;
player.group = renderer.animationGroup;
player.overrideRender ??= (frame) => { player.overrideRender ??= (frame) => {
topFrames.set(player, frame); syncedPlayersFrames.set(player, frame);
// frames.set(containers, frame); // frames.set(containers, frame);
}; };
l.middlewares.delete(middleware); setRenderInterval();
} else if(player instanceof HTMLVideoElement) {
// player.play();
// const cache = framesCache.getCache(key);
// let {width, height} = renderer.size;
// width *= dpr;
// height *= dpr;
// const onFrame = (frame: ImageBitmap | HTMLCanvasElement) => {
// topFrames.set(player, frame);
// player.requestVideoFrameCallback(callback);
// };
// let frameNo = -1, lastTime = 0;
// const callback: VideoFrameRequestCallback = (now, metadata) => {
// const time = player.currentTime;
// if(lastTime > time) {
// frameNo = -1;
// }
// const _frameNo = ++frameNo;
// lastTime = time;
// // const frameNo = Math.floor(player.currentTime * 1000 / CUSTOM_EMOJI_FRAME_INTERVAL);
// // const frameNo = metadata.presentedFrames;
// const imageBitmap = cache.framesNew.get(_frameNo);
// if(imageBitmap) {
// onFrame(imageBitmap);
// } else if(IS_IMAGE_BITMAP_SUPPORTED) {
// createImageBitmap(player, {resizeWidth: width, resizeHeight: height}).then((imageBitmap) => {
// cache.framesNew.set(_frameNo, imageBitmap);
// if(frameNo === _frameNo) onFrame(imageBitmap);
// });
// } else {
// const canvas = document.createElement('canvas');
// const context = canvas.getContext('2d');
// canvas.width = width;
// canvas.height = height;
// context.drawImage(player, 0, 0);
// cache.framesNew.set(_frameNo, canvas);
// onFrame(canvas);
// }
// };
// // player.requestVideoFrameCallback(callback);
// // setInterval(callback, CUSTOM_EMOJI_FRAME_INTERVAL);
} }
}); });
let syncedPlayer: SyncedPlayer;
const key = [doc.id, size.width, size.height].join('-'); const key = [doc.id, size.width, size.height].join('-');
renderer.keys.push(key); if(isLottie) {
let l = lotties.get(key); syncedPlayer = syncedPlayers.get(key);
if(!l) { if(!syncedPlayer) {
l = { syncedPlayer = {
player: undefined, player: undefined,
middlewares: new Set(), middlewares: new Set(),
counter: 0 pausedElements: new Set(),
}; elementsCounter: 0,
counter: 0,
lotties.set(key, l); key
} };
syncedPlayers.set(key, syncedPlayer);
}
++l.counter; renderer.syncedElements.set(syncedPlayer, undefined);
if(middleware) { ++syncedPlayer.counter;
l.middlewares.add(middleware);
if(middleware) {
syncedPlayer.middlewares.add(middleware);
}
} }
return promise.then((res) => ({...res, onRender})); return promise.then((res) => ({...res, ...addition}));
}; };
const missing: DocId[] = []; const missing: DocId[] = [];
@ -865,12 +1107,29 @@ export default function wrapRichText(text: string, options: Partial<{
const loadFromPromises = (promises: typeof cachedPromises) => { const loadFromPromises = (promises: typeof cachedPromises) => {
return Promise.all(promises).then((arr) => { return Promise.all(promises).then((arr) => {
const promises = arr.map(({load, onRender}) => { const promises = arr.map(({load, onRender, elements}) => {
if(!load) { if(!load) {
return; return;
} }
return load().then(onRender); const l = () => load().then(onRender);
if(renderer.lazyLoadQueue) {
elements.forEach((element) => {
renderer.lazyLoadQueue.push({
div: element,
load: () => {
elements.forEach((element) => {
renderer.lazyLoadQueue.unobserve(element);
});
return l();
}
});
});
} else {
return l();
}
}); });
return Promise.all(promises); return Promise.all(promises);
@ -890,6 +1149,7 @@ export default function wrapRichText(text: string, options: Partial<{
load load
}); });
} else { } else {
renderer.lazyLoadQueue = new LazyLoadQueue();
load(); load();
} }

2
src/lib/richTextProcessor/wrapUrl.ts

@ -37,7 +37,7 @@ export default function wrapUrl(url: string, unsafe?: number | boolean) {
case 'addemoji': case 'addemoji':
case 'voicechat': case 'voicechat':
case 'invoice': case 'invoice':
if(path.length !== 1) { if(path.length !== 1 && !tgMeMatch[1]) {
onclick = path[0]; onclick = path[0];
break; break;
} }

4
src/lib/rlottie/lottieLoader.ts

@ -171,9 +171,7 @@ export class LottieLoader {
const player = this.initPlayer(containers, params); const player = this.initPlayer(containers, params);
if(group !== 'none') { animationIntersector.addAnimation(player, group);
animationIntersector.addAnimation(player, group);
}
return player; return player;
} }

100
src/lib/rlottie/rlottiePlayer.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector';
import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables'; import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
import IS_APPLE_MX from '../../environment/appleMx'; import IS_APPLE_MX from '../../environment/appleMx';
import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent'; import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent';
@ -11,8 +12,8 @@ import EventListenerBase from '../../helpers/eventListenerBase';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import clamp from '../../helpers/number/clamp'; import clamp from '../../helpers/number/clamp';
import QueryableWorker from './queryableWorker'; import QueryableWorker from './queryableWorker';
import {AnimationItemGroup} from '../../components/animationIntersector';
import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport'; import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport';
import framesCache, {FramesCache, FramesCacheItem} from '../../helpers/framesCache';
export type RLottieOptions = { export type RLottieOptions = {
container: HTMLElement | HTMLElement[], container: HTMLElement | HTMLElement[],
@ -35,83 +36,6 @@ export type RLottieOptions = {
sync?: boolean 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, 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 = RLottieCache.createCache());
} else {
// console.warn('[RLottieCache] cache will be reused', cache);
}
++cache.counter;
return cache;
}
public releaseCache(name: string) {
const cache = this.cache.get(name);
if(cache && !--cache.counter) {
this.cache.delete(name);
// console.warn('[RLottieCache] released cache', cache);
}
}
public getCacheCounter(name: string) {
const cache = this.cache.get(name);
return cache?.counter;
}
public generateName(name: string, width: number, height: number, color: RLottieColor, toneIndex: number) {
return [
name,
width,
height,
// color ? rgbaToHexa(color) : ''
color ? 'colored' : '',
toneIndex || ''
].filter(Boolean).join('-');
}
}
const cache = new RLottieCache();
export type RLottieColor = [number, number, number]; export type RLottieColor = [number, number, number];
export function getLottiePixelRatio(width: number, height: number, needUpscale?: boolean) { export function getLottiePixelRatio(width: number, height: number, needUpscale?: boolean) {
@ -135,8 +59,8 @@ export default class RLottiePlayer extends EventListenerBase<{
firstFrame: () => void, firstFrame: () => void,
cached: () => void, cached: () => void,
destroy: () => void destroy: () => void
}> { }> implements AnimationItemWrapper {
public static CACHE = cache; public static CACHE = framesCache;
private static reqId = 0; private static reqId = 0;
public reqId = 0; public reqId = 0;
@ -165,7 +89,7 @@ export default class RLottiePlayer extends EventListenerBase<{
public _autoplay: boolean; // ! will be used to store original value for settings.stickers.loop public _autoplay: boolean; // ! will be used to store original value for settings.stickers.loop
public loop = true; public loop = true;
private _loop: boolean; // ! will be used to store original value for settings.stickers.loop private _loop: boolean; // ! will be used to store original value for settings.stickers.loop
private group = ''; public group: AnimationItemGroup = '';
private frInterval: number; private frInterval: number;
private frThen: number; private frThen: number;
@ -174,7 +98,7 @@ export default class RLottiePlayer extends EventListenerBase<{
// private caching = false; // private caching = false;
// private removed = false; // private removed = false;
private cache: RLottieCacheItem; private cache: FramesCacheItem;
private imageData: ImageData; private imageData: ImageData;
public clamped: Uint8ClampedArray; public clamped: Uint8ClampedArray;
private cachingDelta = 0; private cachingDelta = 0;
@ -226,7 +150,7 @@ export default class RLottiePlayer extends EventListenerBase<{
this.toneIndex = options.toneIndex; this.toneIndex = options.toneIndex;
if(this.name) { if(this.name) {
this.cacheName = cache.generateName(this.name, this.width, this.height, this.color, this.toneIndex); this.cacheName = RLottiePlayer.CACHE.generateName(this.name, this.width, this.height, this.color, this.toneIndex);
} }
// * Skip ratio (30fps) // * Skip ratio (30fps)
@ -288,9 +212,9 @@ export default class RLottiePlayer extends EventListenerBase<{
} }
if(this.name) { if(this.name) {
this.cache = cache.getCache(this.cacheName); this.cache = RLottiePlayer.CACHE.getCache(this.cacheName);
} else { } else {
this.cache = RLottieCache.createCache(); this.cache = FramesCache.createCache();
} }
} }
@ -381,7 +305,7 @@ export default class RLottiePlayer extends EventListenerBase<{
public remove() { public remove() {
this.pause(); this.pause();
this.sendQuery(['destroy']); this.sendQuery(['destroy']);
if(this.cacheName) cache.releaseCache(this.cacheName); if(this.cacheName) RLottiePlayer.CACHE.releaseCache(this.cacheName);
this.dispatchEvent('destroy'); this.dispatchEvent('destroy');
this.cleanup(); this.cleanup();
} }
@ -713,7 +637,7 @@ export default class RLottiePlayer extends EventListenerBase<{
// let lastTime = this.frThen; // let lastTime = this.frThen;
this.frameListener = () => { this.frameListener = () => {
if(this.paused) { if(this.paused || !this.currentMethod) {
return; return;
} }
@ -735,7 +659,7 @@ export default class RLottiePlayer extends EventListenerBase<{
this.addEventListener('enterFrame', this.frameListener); this.addEventListener('enterFrame', this.frameListener);
// setInterval(this.frameListener, this.frInterval); // setInterval(this.frameListener, this.frInterval);
// ! fix autoplaying since there will be no animationIntersector for it, // ! fix autoplaying since there will be no animationIntersector for it
if(this.group === 'none' && this.autoplay) { if(this.group === 'none' && this.autoplay) {
this.play(); this.play();
} }

5
src/pages/pageIm.ts

@ -42,8 +42,9 @@ const onFirstMount = () => {
return Promise.all([ return Promise.all([
loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */, loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */,
import('../lib/appManagers/appDialogsManager') import('../lib/appManagers/appDialogsManager'),
]).then(([_, appDialogsManager]) => { 'requestVideoFrameCallback' in HTMLVideoElement.prototype ? Promise.resolve() : import('../helpers/dom/requestVideoFrameCallbackPolyfill')
]).then(([_, appDialogsManager, __]) => {
appDialogsManager.default.start(); appDialogsManager.default.start();
setTimeout(() => { setTimeout(() => {
document.getElementById('auth-pages').remove(); document.getElementById('auth-pages').remove();

25
src/scss/partials/_button.scss

@ -202,13 +202,14 @@ $btn-menu-z-index: 4;
} }
&-item { &-item {
--padding-vertical: .25rem;
--padding-left: .75rem; --padding-left: .75rem;
--padding-right: .75rem; --padding-right: .75rem;
--icon-margin: 1.25rem; --icon-margin: 1.25rem;
--icon-size: 1.25rem; --icon-size: 1.25rem;
display: flex; display: flex;
position: relative; position: relative;
padding: 0 var(--padding-right) 0 var(--padding-left); padding: var(--padding-vertical) var(--padding-right) var(--padding-vertical) var(--padding-left);
height: 2rem; height: 2rem;
cursor: pointer !important; cursor: pointer !important;
pointer-events: all !important; pointer-events: all !important;
@ -270,7 +271,7 @@ $btn-menu-z-index: 4;
&, &,
&-fake { &-fake {
margin-top: 1px; // margin-top: 1px;
pointer-events: none; pointer-events: none;
} }
@ -309,6 +310,19 @@ $btn-menu-z-index: 4;
margin-right: -.875rem; margin-right: -.875rem;
} */ } */
} }
&.is-multiline {
height: auto;
font-size: .75rem;
width: fit-content;
min-width: calc(100% - .625rem);
max-width: fit-content;
.btn-menu-item-text {
white-space: pre-wrap;
width: fit-content;
}
}
} }
/* &-overlay { /* &-overlay {
@ -365,8 +379,13 @@ $btn-menu-z-index: 4;
hr { hr {
padding: 0; padding: 0;
margin: .5rem 0; margin: .3125rem 0;
display: block !important; display: block !important;
margin-left: auto;
margin-right: auto;
width: calc(100% - 1.875rem);
opacity: .6;
} }
.badge { .badge {

2
src/scss/partials/_chatBubble.scss

@ -1747,7 +1747,7 @@ $bubble-beside-button-width: 38px;
margin: inherit; margin: inherit;
&:after { &:after {
color: #fff; color: #fff !important;
} }
} }
} }

21
src/scss/partials/_leftSidebar.scss

@ -673,27 +673,6 @@
padding-bottom: .5rem; padding-bottom: .5rem;
} */ } */
} }
.row {
.btn-primary {
height: 1.875rem;
padding: 0 .75rem;
font-size: .9375rem;
width: auto;
transition: width 0.2s;
margin: 0;
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
border-radius: .9375rem;
line-height: 1.875rem;
body.animation-level-0 & {
transition: none;
}
}
}
} }
.edit-folder-container { .edit-folder-container {

25
src/scss/partials/_row.scss

@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
@use "sass:math";
$row-border-radius: $border-radius-medium; $row-border-radius: $border-radius-medium;
.row { .row {
@ -168,4 +170,27 @@ $row-border-radius: $border-radius-medium;
&.menu-open { &.menu-open {
background-color: var(--light-secondary-text-color); background-color: var(--light-secondary-text-color);
} }
> .btn-primary {
height: 1.875rem;
padding: 0 .75rem;
font-size: .9375rem;
width: auto;
margin: 0;
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
border-radius: .9375rem;
line-height: 1.875rem;
@include animation-level(2) {
transition: width 0.2s, background-color .2s, color .2s;
}
&.active {
background-color: var(--light-primary-color) !important;
color: var(--primary-color);
}
}
} }

33
src/scss/partials/popups/_stickers.scss

@ -21,6 +21,13 @@
} }
} }
.scrollable {
&.is-loading {
min-height: 9rem;
position: relative;
}
}
.sticker-set-footer { .sticker-set-footer {
padding: 8px 0; padding: 8px 0;
} }
@ -47,24 +54,32 @@
.sticker-set { .sticker-set {
margin: .0625rem 0; margin: .0625rem 0;
&-stickers { .row-title {
font-weight: var(--font-weight-bold);
}
&-stickers {
--per-row: 5;
--item-size: var(--esg-sticker-size);
padding: 0 5px; padding: 0 5px;
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(var(--per-row), 1fr);
position: relative;
@include respond-to(handhelds) { @include respond-to(handhelds) {
grid-template-columns: repeat(4, 1fr); --per-row: 4;
} }
&.is-loading { &.is-emojis {
min-height: 9rem; --per-row: 10 !important;
position: relative; --item-size: var(--esg-custom-emoji-size);
--custom-emoji-size: var(--esg-custom-emoji-size);
} }
} }
&-sticker { .media-sticker-wrapper {
width: var(--esg-sticker-size); width: var(--item-size);
height: var(--esg-sticker-size); height: var(--item-size);
margin-bottom: 2px; margin-bottom: 2px;
justify-self: center; justify-self: center;
cursor: pointer; cursor: pointer;

11
src/scss/style.scss

@ -29,8 +29,8 @@ $chat-padding-handhelds: .5rem;
$chat-input-inner-padding: .5rem; $chat-input-inner-padding: .5rem;
$chat-input-inner-padding-handhelds: .25rem; $chat-input-inner-padding-handhelds: .25rem;
@function hover-color($color) { @function hover-color($color, $alpha: $hover-alpha) {
@return rgba($color, $hover-alpha); @return rgba($color, $alpha);
} }
@function rgba-to-rgb($rgba, $background: #fff) { @function rgba-to-rgb($rgba, $background: #fff) {
@ -100,6 +100,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--font-size-14: 14px; --font-size-14: 14px;
--font-size-12: 12px; --font-size-12: 12px;
--esg-sticker-size: 80px; --esg-sticker-size: 80px;
--esg-custom-emoji-size: 32px;
--disabled-opacity: .3; --disabled-opacity: .3;
--round-video-size: 280px; --round-video-size: 280px;
--menu-box-shadow: 0px 0px 10px var(--menu-box-shadow-color); --menu-box-shadow: 0px 0px 10px var(--menu-box-shadow-color);
@ -215,7 +216,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--chatlist-pinned-color: #a2abb2; --chatlist-pinned-color: #a2abb2;
--badge-text-color: #fff; --badge-text-color: #fff;
--link-color: #00488f; --link-color: #00488f;
--ripple-color: rgba(0, 0, 0, .08); --ripple-color: rgba(0, 0, 0, #{$hover-alpha});
--poll-circle-color: var(--border-color); --poll-circle-color: var(--border-color);
--spoiler-background-color: #e3e5e8; --spoiler-background-color: #e3e5e8;
--spoiler-draft-background-color: #d9d9d9; --spoiler-draft-background-color: #d9d9d9;
@ -290,7 +291,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--chatlist-pinned-color: var(--secondary-color); --chatlist-pinned-color: var(--secondary-color);
--badge-text-color: #fff; --badge-text-color: #fff;
--link-color: var(--primary-color); --link-color: var(--primary-color);
--ripple-color: rgba(255, 255, 255, .08); --ripple-color: rgba(255, 255, 255, #{$hover-alpha});
--poll-circle-color: #fff; --poll-circle-color: #fff;
--spoiler-background-color: #373e4e; --spoiler-background-color: #373e4e;
--spoiler-draft-background-color: #484848; --spoiler-draft-background-color: #484848;
@ -1285,7 +1286,7 @@ middle-ellipsis-element {
} }
.rlottie-vector { .rlottie-vector {
fill: rgba(0, 0, 0, .08); fill: rgba(0, 0, 0, $hover-alpha);
} }
.canvas-thumbnail { .canvas-thumbnail {

Loading…
Cancel
Save