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. 243
      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. 19
      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. 94
      src/helpers/middleware.ts
  30. 12
      src/lang.ts
  31. 45
      src/lib/appManagers/appImManager.ts
  32. 47
      src/lib/appManagers/appStickersManager.ts
  33. 494
      src/lib/richTextProcessor/wrapRichText.ts
  34. 2
      src/lib/richTextProcessor/wrapUrl.ts
  35. 2
      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. 31
      src/scss/partials/popups/_stickers.scss
  43. 11
      src/scss/style.scss

33
src/components/animationIntersector.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* 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 {IS_SAFARI} from '../environment/userAgent';
import {MOUNT_CLASS_TO} from '../config/debug';
@ -22,7 +22,16 @@ export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' | @@ -22,7 +22,16 @@ export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
export interface AnimationItem {
el: HTMLElement,
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 {
@ -145,8 +154,21 @@ export class AnimationIntersector { @@ -145,8 +154,21 @@ export class AnimationIntersector {
}
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 = {
el: _animation instanceof RLottiePlayer ? _animation.el[0] : (_animation instanceof HTMLVideoElement ? _animation : _animation.canvas),
el,
animation: _animation,
group
};
@ -188,7 +210,10 @@ export class AnimationIntersector { @@ -188,7 +210,10 @@ export class AnimationIntersector {
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) {
// console.warn('pause animation:', animation);
animation.pause();

4
src/components/appSearchSuper..ts

@ -18,7 +18,7 @@ import {wrapDocument, wrapPhoto, wrapVideo} from './wrappers'; @@ -18,7 +18,7 @@ import {wrapDocument, wrapPhoto, wrapVideo} from './wrappers';
import useHeavyAnimationCheck, {getHeavyAnimationPromise} from '../hooks/useHeavyAnimationCheck';
import I18n, {LangPackKey, i18n} from '../lib/langPack';
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 SortedUserList from './sortedUserList';
import findUpTag from '../helpers/dom/findUpTag';
@ -252,7 +252,7 @@ class SearchContextMenu { @@ -252,7 +252,7 @@ class SearchContextMenu {
export type ProcessSearchSuperResult = {
message: Message.message,
middleware: () => boolean,
middleware: Middleware,
promises: Promise<any>[],
elemsToAppend: {element: HTMLElement, message: any}[],
inputFilter: MyInputMessagesFilter,

31
src/components/chat/bubbles.ts

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

83
src/components/chat/contextMenu.ts

@ -19,7 +19,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName'; @@ -19,7 +19,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
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 assumeType from '../../helpers/assumeType';
import PopupSponsored from '../popups/sponsored';
@ -40,9 +40,14 @@ import filterAsync from '../../helpers/array/filterAsync'; @@ -40,9 +40,14 @@ import filterAsync from '../../helpers/array/filterAsync';
import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {SERVICE_PEER_ID} from '../../lib/mtproto/mtproto_config';
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 {
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 isSelectable: boolean;
@ -67,6 +72,8 @@ export default class ChatContextMenu { @@ -67,6 +72,8 @@ export default class ChatContextMenu {
private middleware: ReturnType<typeof getMiddleware>;
private canOpenReactedList: boolean;
private emojiInputsPromise: CancellablePromise<InputStickerSet.inputStickerSetID[]>;
constructor(
private chat: Chat,
private managers: AppManagers
@ -508,7 +515,8 @@ export default class ChatContextMenu { @@ -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)),
notDirect: () => true
notDirect: () => true,
localName: 'views'
}, {
icon: 'delete danger',
text: 'Delete',
@ -529,6 +537,20 @@ export default class ChatContextMenu { @@ -529,6 +537,20 @@ export default class ChatContextMenu {
},
verify: () => false,
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 { @@ -545,12 +567,14 @@ export default class ChatContextMenu {
element.id = 'bubble-contextmenu';
element.classList.add('contextmenu');
const viewsButton = filteredButtons.find((button) => !button.icon);
const viewsButton = filteredButtons.find((button) => button.localName === 'views');
if(viewsButton) {
const reactions = (this.message as Message.message).reactions;
const recentReactions = reactions?.recent_reactions;
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;
viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks'));
@ -677,6 +701,55 @@ export default class ChatContextMenu { @@ -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);
return {

3
src/components/chat/replyContainer.ts

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
*/
import replaceContent from '../../helpers/dom/replaceContent';
import {Middleware} from '../../helpers/middleware';
import limitSymbols from '../../helpers/string/limitSymbols';
import {Document, MessageMedia, Photo, WebPage} from '../../layer';
import appImManager, {CHAT_ANIMATION_GROUP} from '../../lib/appManagers/appImManager';
@ -42,7 +43,7 @@ export async function wrapReplyDivAndCaption(options: { @@ -42,7 +43,7 @@ export async function wrapReplyDivAndCaption(options: {
let messageMedia: MessageMedia | WebPage.webPage = message?.media;
let setMedia = false, isRound = false;
const mediaChildren = mediaEl ? Array.from(mediaEl.children).slice() : [];
let middleware: () => boolean;
let middleware: Middleware;
if(messageMedia && mediaEl) {
subtitleEl.textContent = '';
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'; @@ -20,6 +20,7 @@ import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {AppManagers} from '../../lib/appManagers/managers';
import overlayCounter from '../../helpers/overlayCounter';
import Scrollable from '../scrollable';
import {getMiddleware, MiddlewareHelper} from '../../helpers/middleware';
export type PopupButton = {
text?: string,
@ -92,6 +93,8 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -92,6 +93,8 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
protected buttons: Array<PopupButton>;
protected middlewareHelper: MiddlewareHelper;
constructor(className: string, options: PopupOptions = {}) {
super(false);
this.element.classList.add('popup');
@ -109,6 +112,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -109,6 +112,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.header.append(this.title);
}
this.middlewareHelper = getMiddleware();
this.listenerSetter = new ListenerSetter();
this.managers = PopupElement.MANAGERS;
@ -268,6 +272,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends @@ -268,6 +272,7 @@ export default class PopupElement<T extends EventListenerListeners = {}> extends
this.element.classList.add('hiding');
this.element.classList.remove('active');
this.listenerSetter.removeAll();
this.middlewareHelper.destroy();
if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = false;

243
src/components/popups/stickers.ts

@ -22,100 +22,186 @@ import setInnerHTML from '../../helpers/dom/setInnerHTML'; @@ -22,100 +22,186 @@ import setInnerHTML from '../../helpers/dom/setInnerHTML';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import createStickersContextMenu from '../../helpers/dom/createStickersContextMenu';
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';
export default class PopupStickers extends PopupElement {
private stickersFooter: HTMLElement;
private stickersDiv: HTMLElement;
constructor(private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0]) {
private appendTo: HTMLElement;
private updateAdded: {[setId: Long]: (added: boolean) => void};
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});
this.title.append(i18n('Loading'));
this.updateAdded = {};
this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup();
destroy();
});
const div = document.createElement('div');
div.classList.add('sticker-set');
this.stickersDiv = document.createElement('div');
this.stickersDiv.classList.add('sticker-set-stickers', 'is-loading');
this.appendTo = this.scrollable.container;
attachClickEvent(this.stickersDiv, this.onStickersClick, {listenerSetter: this.listenerSetter});
putPreloader(this.stickersDiv, true);
this.appendTo.classList.add('is-loading');
putPreloader(this.appendTo, true);
this.stickersFooter = document.createElement('div');
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'});
this.stickersFooter.append(btn);
this.scrollable.append(div);
this.body.append(this.stickersFooter);
const {destroy} = createStickersContextMenu({
listenTo: this.stickersDiv,
isStickerPack: true
});
attachStickerViewerListeners({listenTo: this.appendTo, listenerSetter: this.listenerSetter});
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();
}
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');
if(!target) return;
const docId = target.dataset.docId;
if(appImManager.chat.input.sendMessageWithDocument(docId)) {
if(await appImManager.chat.input.sendMessageWithDocument(docId)) {
this.hide();
}
};
private loadStickerSet() {
return this.managers.appStickersManager.getStickerSet(this.stickerSetInput).then(async(set) => {
if(!set) {
toastNew({langPackKey: 'StickerSet.DontExist'});
private async loadStickerSet() {
const middleware = this.middlewareHelper.get();
const inputs = Array.isArray(this.stickerSetInput) ? this.stickerSetInput : [this.stickerSetInput];
const setsPromises = inputs.map((input) => this.managers.appStickersManager.getStickerSet(input));
let sets = await Promise.all(setsPromises);
if(!middleware()) return;
let firstSet = sets[0];
if(sets.length === 1 && !firstSet) {
toastNew({langPackKey: this.isEmojis ? 'AddEmojiNotFound' : 'StickerSet.DontExist'});
this.hide();
return;
}
sets = sets.filter(Boolean);
firstSet = sets[0];
this.sets = sets.map((set) => set.set);
const isEmojis = this.isEmojis ??= !!firstSet.set.pFlags.emojis;
if(!isEmojis) {
attachClickEvent(this.appendTo, this.onStickersClick, {listenerSetter: this.listenerSetter});
const {destroy} = createStickersContextMenu({
listenTo: this.appendTo,
isStickerPack: true,
onSend: () => this.hide()
});
this.addEventListener('close', destroy);
}
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
let button: HTMLElement;
const s = i18n('Stickers', [set.set.count]);
if(set.set.installed_date) {
button = Button('btn-primary btn-primary-transparent danger', {noRipple: true});
button.append(i18n('RemoveStickersCount', [s]));
} else {
button = Button('btn-primary btn-color-primary', {noRipple: true});
button.append(i18n('AddStickersCount', [s]));
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, () => {
const toggle = toggleDisability([button], true);
this.updateAdded[set.set.id] = updateAdded;
this.managers.appStickersManager.toggleStickerSet(set.set).then(() => {
this.hide();
}).catch(() => {
toggle();
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
});
text += doc.stickerEmojiRaw;
});
const wrapped = wrapRichText(text, {
entities,
loadPromises,
animationGroup: ANIMATION_GROUP,
customEmojiSize: mediaSizes.active.esgCustomEmoji,
middleware
// lazyLoadQueue
});
const lazyLoadQueue = new LazyLoadQueue();
const divs = await Promise.all(set.documents.map(async(doc) => {
if(doc._ === 'documentEmpty') {
return;
}
divs = [wrapped];
itemsContainer.classList.add('is-emojis');
} else {
divs = await Promise.all(docs.map(async(doc) => {
const div = document.createElement('div');
div.classList.add('sticker-set-sticker');
@ -130,22 +216,75 @@ export default class PopupStickers extends PopupElement { @@ -130,22 +216,75 @@ export default class PopupStickers extends PopupElement {
loop: true,
width: size,
height: size,
withLock: true
withLock: true,
loadPromises,
middleware
});
return div;
}));
}
itemsContainer.append(...divs.filter(Boolean));
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'));
}
setInnerHTML(this.title, wrapEmojiText(set.set.title));
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
this.stickersFooter.textContent = '';
this.stickersFooter.append(button);
this.stickersDiv.classList.remove('is-loading');
this.stickersDiv.innerHTML = '';
this.stickersDiv.append(...divs.filter(Boolean));
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'; @@ -14,6 +14,7 @@ import replaceContent from '../helpers/dom/replaceContent';
import setInnerHTML from '../helpers/dom/setInnerHTML';
import {attachClickEvent} from '../helpers/dom/clickEvent';
import ListenerSetter from '../helpers/listenerSetter';
import Button from './button';
export default class Row {
public container: HTMLElement;
@ -27,6 +28,8 @@ export default class Row { @@ -27,6 +28,8 @@ export default class Row {
public freezed = false;
public buttonRight: HTMLElement;
constructor(options: Partial<{
icon: string,
subtitle: string | HTMLElement | DocumentFragment,
@ -44,7 +47,9 @@ export default class Row { @@ -44,7 +47,9 @@ export default class Row {
havePadding: boolean,
noRipple: boolean,
noWrap: boolean,
listenerSetter: ListenerSetter
listenerSetter: ListenerSetter,
buttonRight?: HTMLElement | boolean,
buttonRightLangKey: LangPackKey
}> = {}) {
this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div');
this.container.classList.add('row');
@ -173,6 +178,13 @@ export default class Row { @@ -173,6 +178,13 @@ export default class Row {
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') {

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

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

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

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

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

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

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

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

8
src/components/stickerViewer.ts

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

3
src/components/wrappers/album.ts

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

3
src/components/wrappers/photo.ts

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

75
src/components/wrappers/sticker.ts

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

3
src/components/wrappers/stickerAnimation.ts

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

3
src/components/wrappers/video.ts

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

8
src/global.d.ts vendored

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import type ListenerSetter from './helpers/listenerSetter';
import type {Middleware, MiddlewareHelper} from './helpers/middleware';
import type {Chat, Document, User} from './layer';
declare global {
@ -12,6 +13,11 @@ declare global { @@ -12,6 +13,11 @@ declare global {
dpr?: number
}
interface HTMLElement {
middlewareHelper?: MiddlewareHelper;
middleware?: Middleware;
}
type UserId = User.user['id'];
type ChatId = Chat.chat['id'];
// type PeerId = `u${UserId}` | `c${ChatId}`;
@ -65,4 +71,6 @@ declare global { @@ -65,4 +71,6 @@ declare global {
declare const electronHelpers: {
openExternal(url): void;
} | 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: { @@ -18,9 +18,10 @@ export default function createStickersContextMenu(options: {
verifyRecent?: (target: HTMLElement) => boolean,
appendTo?: HTMLElement,
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;
const verifyFavoriteSticker = async(toAdd: boolean) => {
const favedStickers = await rootScope.managers.acknowledged.appStickersManager.getFavedStickersStickers();
@ -64,7 +65,10 @@ export default function createStickersContextMenu(options: { @@ -64,7 +65,10 @@ export default function createStickersContextMenu(options: {
}, {
icon: 'mute',
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)
}, {
icon: 'schedule',

63
src/helpers/dom/getViewportSlice.ts

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

12
src/helpers/dom/getVisibleRect.ts

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

19
src/helpers/dom/renderImageFromUrl.ts

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

86
src/helpers/framesCache.ts

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

9
src/helpers/mediaSizes.ts

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

94
src/helpers/middleware.ts

@ -4,19 +4,87 @@ @@ -4,19 +4,87 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
// * will change .cleaned and new instance will be created
export const getMiddleware = () => {
let cleanupObj = {cleaned: false};
return {
clean: () => {
cleanupObj.cleaned = true;
cleanupObj = {cleaned: false};
},
get: (additionalCallback?: () => boolean) => {
const _cleanupObj = cleanupObj;
return () => {
return !_cleanupObj.cleaned && (!additionalCallback || additionalCallback());
import indexOfAndSplice from './array/indexOfAndSplice';
import makeError from './makeError';
export type Middleware = {
(): boolean;
create(): MiddlewareHelper;
onClean: (callback: VoidFunction) => void;
onDestroy: (callback: VoidFunction) => void;
};
const createDetails = (): {
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 = { @@ -763,6 +763,18 @@ const lang = {
'PrivacyVoiceMessagesTitle': 'Who can send me voice or video messages?',
'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.',
'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
'AccountSettings.Filters': 'Chat Folders',

45
src/lib/appManagers/appImManager.ts

@ -543,15 +543,22 @@ export class AppImManager extends EventListenerBase<{ @@ -543,15 +543,22 @@ export class AppImManager extends EventListenerBase<{
}
});
this.addAnchorListener<{pathnameParams: ['addstickers', string]}>({
name: 'addstickers',
([
['addstickers', INTERNAL_LINK_TYPE.STICKER_SET],
['addemoji', INTERNAL_LINK_TYPE.EMOJI_SET]
] as [
'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 = {
_: INTERNAL_LINK_TYPE.STICKER_SET,
_: type,
set: pathnameParams[1]
};
@ -559,6 +566,20 @@ export class AppImManager extends EventListenerBase<{ @@ -559,6 +566,20 @@ export class AppImManager extends EventListenerBase<{
}
});
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/$asdasdad
this.addAnchorListener<{pathnameParams: ['invoice', string] | string}>({
@ -693,19 +714,6 @@ export class AppImManager extends EventListenerBase<{ @@ -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<{
uriParams: {
slug: string
@ -889,8 +897,9 @@ export class AppImManager extends EventListenerBase<{ @@ -889,8 +897,9 @@ export class AppImManager extends EventListenerBase<{
break;
}
case INTERNAL_LINK_TYPE.EMOJI_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;
}
@ -982,7 +991,7 @@ export class AppImManager extends EventListenerBase<{ @@ -982,7 +991,7 @@ export class AppImManager extends EventListenerBase<{
private addAnchorListener<Params extends {pathnameParams?: any, uriParams?: any}>(options: {
name: 'showMaskedAlert' | 'execBotCommand' | 'searchByHashtag' | 'addstickers' | 'im' |
'resolve' | 'privatepost' | 'addstickers' | 'voicechat' | 'joinchat' | 'join' | 'invoice' |
'emoji',
'addemoji',
protocol?: 'tg',
callback: (params: Params, element?: HTMLAnchorElement) => boolean | any,
noPathnameParams?: boolean,

47
src/lib/appManagers/appStickersManager.ts

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

494
src/lib/richTextProcessor/wrapRichText.ts

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

2
src/lib/richTextProcessor/wrapUrl.ts

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

2
src/lib/rlottie/lottieLoader.ts

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

100
src/lib/rlottie/rlottiePlayer.ts

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

5
src/pages/pageIm.ts

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

25
src/scss/partials/_button.scss

@ -202,13 +202,14 @@ $btn-menu-z-index: 4; @@ -202,13 +202,14 @@ $btn-menu-z-index: 4;
}
&-item {
--padding-vertical: .25rem;
--padding-left: .75rem;
--padding-right: .75rem;
--icon-margin: 1.25rem;
--icon-size: 1.25rem;
display: flex;
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;
cursor: pointer !important;
pointer-events: all !important;
@ -270,7 +271,7 @@ $btn-menu-z-index: 4; @@ -270,7 +271,7 @@ $btn-menu-z-index: 4;
&,
&-fake {
margin-top: 1px;
// margin-top: 1px;
pointer-events: none;
}
@ -309,6 +310,19 @@ $btn-menu-z-index: 4; @@ -309,6 +310,19 @@ $btn-menu-z-index: 4;
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 {
@ -365,8 +379,13 @@ $btn-menu-z-index: 4; @@ -365,8 +379,13 @@ $btn-menu-z-index: 4;
hr {
padding: 0;
margin: .5rem 0;
margin: .3125rem 0;
display: block !important;
margin-left: auto;
margin-right: auto;
width: calc(100% - 1.875rem);
opacity: .6;
}
.badge {

2
src/scss/partials/_chatBubble.scss

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

21
src/scss/partials/_leftSidebar.scss

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

25
src/scss/partials/_row.scss

@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
@use "sass:math";
$row-border-radius: $border-radius-medium;
.row {
@ -168,4 +170,27 @@ $row-border-radius: $border-radius-medium; @@ -168,4 +170,27 @@ $row-border-radius: $border-radius-medium;
&.menu-open {
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);
}
}
}

31
src/scss/partials/popups/_stickers.scss

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

11
src/scss/style.scss

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

Loading…
Cancel
Save