Emoji packs
Fix editing sensitive content setting Fix emoji memory leaks
This commit is contained in:
parent
563f4e0ea3
commit
71c4afaca0
@ -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' |
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
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();
|
||||
|
@ -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 {
|
||||
|
||||
export type ProcessSearchSuperResult = {
|
||||
message: Message.message,
|
||||
middleware: () => boolean,
|
||||
middleware: Middleware,
|
||||
promises: Promise<any>[],
|
||||
elemsToAppend: {element: HTMLElement, message: any}[],
|
||||
inputFilter: MyInputMessagesFilter,
|
||||
|
@ -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 {
|
||||
|
||||
public lazyLoadQueue: LazyLoadQueue;
|
||||
|
||||
private middleware = getMiddleware();
|
||||
private middlewareHelper = getMiddleware();
|
||||
|
||||
private log: ReturnType<typeof logger>;
|
||||
|
||||
@ -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 {
|
||||
this.viewsMids.clear();
|
||||
}
|
||||
|
||||
this.middleware.clean();
|
||||
this.middlewareHelper.clean();
|
||||
|
||||
this.onAnimateLadder = undefined;
|
||||
this.resolveLadderAnimation = undefined;
|
||||
@ -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 {
|
||||
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 {
|
||||
|
||||
// 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 {
|
||||
// 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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
passEntities: this.passEntities,
|
||||
loadPromises,
|
||||
lazyLoadQueue: this.lazyLoadQueue,
|
||||
customEmojiSize
|
||||
customEmojiSize,
|
||||
middleware
|
||||
});
|
||||
|
||||
let canHaveTail = true;
|
||||
@ -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 {
|
||||
wrapAlbum({
|
||||
messages: albumMessages,
|
||||
attachmentDiv,
|
||||
middleware: this.getMiddleware(),
|
||||
middleware,
|
||||
isOut: our,
|
||||
lazyLoadQueue: this.lazyLoadQueue,
|
||||
chat: this.chat,
|
||||
@ -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 {
|
||||
withTail: false,
|
||||
isOut,
|
||||
lazyLoadQueue: this.lazyLoadQueue,
|
||||
middleware: this.getMiddleware(),
|
||||
middleware,
|
||||
loadPromises,
|
||||
boxWidth: mediaSize.width,
|
||||
boxHeight: mediaSize.height
|
||||
|
@ -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';
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
},
|
||||
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 {
|
||||
},
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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: {
|
||||
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));
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
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
|
||||
this.element.classList.add('hiding');
|
||||
this.element.classList.remove('active');
|
||||
this.listenerSetter.removeAll();
|
||||
this.middlewareHelper.destroy();
|
||||
|
||||
if(!this.withoutOverlay) {
|
||||
overlayCounter.isOverlayActive = false;
|
||||
|
@ -22,130 +22,269 @@ 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;
|
||||
private appendTo: HTMLElement;
|
||||
private updateAdded: {[setId: Long]: (added: boolean) => void};
|
||||
private sets: StickerSet.stickerSet[];
|
||||
private button: HTMLElement;
|
||||
|
||||
constructor(private stickerSetInput: Parameters<AppStickersManager['getStickerSet']>[0]) {
|
||||
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.appendTo = this.scrollable.container;
|
||||
|
||||
this.stickersDiv = document.createElement('div');
|
||||
this.stickersDiv.classList.add('sticker-set-stickers', 'is-loading');
|
||||
|
||||
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});
|
||||
|
||||
attachStickerViewerListeners({listenTo: this.stickersDiv, listenerSetter: this.listenerSetter});
|
||||
const onStickerSetUpdate = (set: StickerSet.stickerSet) => {
|
||||
const idx = this.sets.findIndex((_set) => _set.id === set.id);
|
||||
if(idx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
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'});
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
|
||||
sets = sets.filter(Boolean);
|
||||
firstSet = sets[0];
|
||||
|
||||
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]));
|
||||
}
|
||||
this.sets = sets.map((set) => set.set);
|
||||
|
||||
attachClickEvent(button, () => {
|
||||
const toggle = toggleDisability([button], true);
|
||||
const isEmojis = this.isEmojis ??= !!firstSet.set.pFlags.emojis;
|
||||
|
||||
this.managers.appStickersManager.toggleStickerSet(set.set).then(() => {
|
||||
this.hide();
|
||||
}).catch(() => {
|
||||
toggle();
|
||||
});
|
||||
if(!isEmojis) {
|
||||
attachClickEvent(this.appendTo, this.onStickersClick, {listenerSetter: this.listenerSetter});
|
||||
|
||||
const {destroy} = createStickersContextMenu({
|
||||
listenTo: this.appendTo,
|
||||
isStickerPack: true,
|
||||
onSend: () => this.hide()
|
||||
});
|
||||
|
||||
const lazyLoadQueue = new LazyLoadQueue();
|
||||
const divs = await Promise.all(set.documents.map(async(doc) => {
|
||||
if(doc._ === 'documentEmpty') {
|
||||
return;
|
||||
}
|
||||
this.addEventListener('close', destroy);
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('sticker-set-sticker');
|
||||
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
|
||||
|
||||
const size = mediaSizes.active.esgSticker.width;
|
||||
const lazyLoadQueue = new LazyLoadQueue();
|
||||
const loadPromises: Promise<any>[] = [];
|
||||
|
||||
await wrapSticker({
|
||||
doc,
|
||||
div,
|
||||
lazyLoadQueue,
|
||||
group: ANIMATION_GROUP,
|
||||
play: true,
|
||||
loop: true,
|
||||
width: size,
|
||||
height: size,
|
||||
withLock: true
|
||||
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});
|
||||
}
|
||||
|
||||
this.updateAdded[set.set.id] = updateAdded;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
return div;
|
||||
}));
|
||||
const wrapped = wrapRichText(text, {
|
||||
entities,
|
||||
loadPromises,
|
||||
animationGroup: ANIMATION_GROUP,
|
||||
customEmojiSize: mediaSizes.active.esgCustomEmoji,
|
||||
middleware
|
||||
// lazyLoadQueue
|
||||
});
|
||||
|
||||
setInnerHTML(this.title, wrapEmojiText(set.set.title));
|
||||
this.stickersFooter.classList.toggle('add', !set.set.installed_date);
|
||||
this.stickersFooter.textContent = '';
|
||||
this.stickersFooter.append(button);
|
||||
divs = [wrapped];
|
||||
|
||||
this.stickersDiv.classList.remove('is-loading');
|
||||
this.stickersDiv.innerHTML = '';
|
||||
this.stickersDiv.append(...divs.filter(Boolean));
|
||||
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');
|
||||
|
||||
this.scrollable.onAdditionalScroll();
|
||||
const size = mediaSizes.active.esgSticker.width;
|
||||
|
||||
await wrapSticker({
|
||||
doc,
|
||||
div,
|
||||
lazyLoadQueue,
|
||||
group: ANIMATION_GROUP,
|
||||
play: true,
|
||||
loop: true,
|
||||
width: size,
|
||||
height: size,
|
||||
withLock: true,
|
||||
loadPromises,
|
||||
middleware
|
||||
});
|
||||
|
||||
return div;
|
||||
}));
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
this.stickersFooter.textContent = '';
|
||||
this.stickersFooter.append(button);
|
||||
|
||||
this.appendTo.classList.remove('is-loading');
|
||||
this.appendTo.textContent = '';
|
||||
this.appendTo.append(...containers);
|
||||
|
||||
this.scrollable.onAdditionalScroll();
|
||||
}
|
||||
|
||||
private updateButton() {
|
||||
const {sets, isEmojis} = this;
|
||||
let isAdd: boolean, buttonAppend: HTMLElement;
|
||||
if(sets.length === 1) {
|
||||
const firstSet = sets[0];
|
||||
buttonAppend = i18n(isEmojis ? 'EmojiCount' : 'Stickers', [firstSet.count]);
|
||||
isAdd = !firstSet.installed_date;
|
||||
} else {
|
||||
const installed = sets.filter((set) => set.installed_date);
|
||||
let count: number;
|
||||
if(sets.length === installed.length) {
|
||||
isAdd = false;
|
||||
count = sets.length;
|
||||
} else {
|
||||
isAdd = true;
|
||||
count = sets.length - installed.length;
|
||||
}
|
||||
|
||||
buttonAppend = i18n('EmojiPackCount', [count]);
|
||||
}
|
||||
|
||||
this.button.className = isAdd ? 'btn-primary btn-color-primary' : 'btn-primary btn-primary-transparent danger';
|
||||
replaceContent(this.button, i18n(isAdd ? 'AddStickersCount' : 'RemoveStickersCount', [buttonAppend]));
|
||||
}
|
||||
}
|
||||
|
@ -14,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 {
|
||||
|
||||
public freezed = false;
|
||||
|
||||
public buttonRight: HTMLElement;
|
||||
|
||||
constructor(options: Partial<{
|
||||
icon: string,
|
||||
subtitle: string | HTMLElement | DocumentFragment,
|
||||
@ -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 {
|
||||
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') {
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
return div;
|
||||
return row;
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
@ -274,11 +275,10 @@ export default class AppChatFoldersTab extends SliderSuperTab {
|
||||
Array.from(this.suggestedSection.content.children).slice(1).forEach((el) => el.remove());
|
||||
|
||||
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 {
|
||||
|
||||
this.managers.filtersStorage.createDialogFilter(f, true).then((bool) => {
|
||||
if(bool) {
|
||||
div.remove();
|
||||
row.container.remove();
|
||||
}
|
||||
}).finally(() => {
|
||||
button.removeAttribute('disabled');
|
||||
|
@ -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];
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
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,
|
||||
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,
|
||||
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();
|
||||
|
@ -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';
|
||||
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,
|
||||
|
@ -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
|
||||
withTail?: boolean,
|
||||
isOut?: boolean,
|
||||
lazyLoadQueue?: LazyLoadQueue,
|
||||
middleware?: () => boolean,
|
||||
middleware?: Middleware,
|
||||
size?: PhotoSize | VideoSize,
|
||||
withoutPreloader?: boolean,
|
||||
loadPromises?: Promise<any>[],
|
||||
|
@ -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} = {};
|
||||
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
|
||||
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
|
||||
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
|
||||
// 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
|
||||
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(stickerType === 3 && !isSavingLottiePreview(doc, toneIndex)) {
|
||||
if(!media) {
|
||||
if(!isSingleVideo || !isAnimated) {
|
||||
thumbImage?.remove();
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
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
|
||||
container: HTMLElement,
|
||||
managers: AppManagers,
|
||||
peerId: PeerId,
|
||||
middleware: () => boolean
|
||||
middleware: Middleware
|
||||
}) {
|
||||
if(!peerId.isUser()) {
|
||||
return;
|
||||
|
@ -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({
|
||||
}: {
|
||||
size: number,
|
||||
doc: MyDocument,
|
||||
middleware?: () => boolean,
|
||||
middleware?: Middleware,
|
||||
target: HTMLElement,
|
||||
side: 'left' | 'center' | 'right',
|
||||
skipRatio?: number,
|
||||
|
@ -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
|
||||
boxHeight?: number,
|
||||
withTail?: boolean,
|
||||
isOut?: boolean,
|
||||
middleware?: () => boolean,
|
||||
middleware?: Middleware,
|
||||
lazyLoadQueue?: LazyLoadQueue,
|
||||
noInfo?: boolean,
|
||||
noPlayButton?: boolean,
|
||||
|
8
src/global.d.ts
vendored
8
src/global.d.ts
vendored
@ -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 {
|
||||
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 {
|
||||
declare const electronHelpers: {
|
||||
openExternal(url): void;
|
||||
} | undefined;
|
||||
|
||||
type DOMRectMinified = {top: number, right: number, bottom: number, left: number};
|
||||
}
|
||||
|
@ -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: {
|
||||
}, {
|
||||
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',
|
||||
|
@ -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}:
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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(
|
||||
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: {
|
||||
|
@ -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(
|
||||
// TODO: переделать прогрузки аватаров до начала анимации, иначе с этим ожиданием они неприятно появляются
|
||||
// callback && getHeavyAnimationPromise().then(() => callback());
|
||||
callback?.();
|
||||
}, {once: true});
|
||||
|
||||
if(callback) {
|
||||
loader.addEventListener('error', (err) => {
|
||||
console.error('Render image from url failed:', err, url, loader);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
loader.removeEventListener('error', onError);
|
||||
};
|
||||
|
||||
const onError = (err: ErrorEvent) => {
|
||||
console.error('Render image from url failed:', err, url, loader);
|
||||
loader.removeEventListener('load', onLoad);
|
||||
callback?.();
|
||||
};
|
||||
|
||||
loader.addEventListener('load', onLoad, {once: true});
|
||||
loader.addEventListener('error', onError, {once: true});
|
||||
}
|
||||
}
|
||||
|
||||
|
43
src/helpers/dom/requestVideoFrameCallbackPolyfill.ts
Normal file
43
src/helpers/dom/requestVideoFrameCallbackPolyfill.ts
Normal file
@ -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 {};
|
@ -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
Normal file
86
src/helpers/framesCache.ts
Normal file
@ -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;
|
@ -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<{
|
||||
return;
|
||||
}
|
||||
|
||||
if(DO_NOT_IDLE && value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isIdle = value;
|
||||
this.dispatchEvent('change', value);
|
||||
}
|
||||
|
@ -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<{
|
||||
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<{
|
||||
round: makeMediaSize(280, 280),
|
||||
documentName: makeMediaSize(240, 0),
|
||||
invoice: makeMediaSize(320, 260),
|
||||
customEmoji: makeMediaSize(18, 18)
|
||||
customEmoji: makeMediaSize(18, 18),
|
||||
esgCustomEmoji: makeMediaSize(32, 32)
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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
12
src/lang.ts
@ -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',
|
||||
|
@ -543,20 +543,41 @@ export class AppImManager extends EventListenerBase<{
|
||||
}
|
||||
});
|
||||
|
||||
this.addAnchorListener<{pathnameParams: ['addstickers', string]}>({
|
||||
name: 'addstickers',
|
||||
callback: ({pathnameParams}) => {
|
||||
if(!pathnameParams[1]) {
|
||||
return;
|
||||
([
|
||||
['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 = {
|
||||
_: type,
|
||||
set: pathnameParams[1]
|
||||
};
|
||||
|
||||
this.processInternalLink(link);
|
||||
}
|
||||
});
|
||||
|
||||
const link: InternalLink = {
|
||||
_: INTERNAL_LINK_TYPE.STICKER_SET,
|
||||
set: pathnameParams[1]
|
||||
};
|
||||
|
||||
this.processInternalLink(link);
|
||||
}
|
||||
this.addAnchorListener<{
|
||||
uriParams: {
|
||||
set: string
|
||||
}
|
||||
}>({
|
||||
name,
|
||||
protocol: 'tg',
|
||||
callback: ({uriParams}) => {
|
||||
const link = this.makeLink(type, uriParams);
|
||||
this.processInternalLink(link);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// * t.me/invoice/asdasdad
|
||||
@ -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<{
|
||||
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<{
|
||||
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,
|
||||
|
@ -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 {
|
||||
});
|
||||
}
|
||||
|
||||
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 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 perf = performance.now();
|
||||
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);
|
||||
delete this.getStickerSetPromises[id];
|
||||
// console.log('get sticker set from cache time', id, performance.now() - perf);
|
||||
|
||||
if(this.getStickerSetPromises[id] === promise) {
|
||||
delete this.getStickerSetPromises[id];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -170,8 +191,12 @@ export class AppStickersManager extends AppManager {
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
delete this.getStickerSetPromises[id];
|
||||
if(this.getStickerSetPromises[id] === promise) {
|
||||
delete this.getStickerSetPromises[id];
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getAnimatedEmojiStickerSet() {
|
||||
@ -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 {
|
||||
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({
|
||||
|
@ -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) => {
|
||||
});
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
if(!--syncedPlayer.counter) {
|
||||
if(syncedPlayer.player) {
|
||||
const frame = syncedPlayersFrames.get(syncedPlayer.player);
|
||||
if(frame) {
|
||||
(frame as ImageBitmap).close?.();
|
||||
syncedPlayersFrames.delete(syncedPlayer.player);
|
||||
}
|
||||
|
||||
syncedPlayersFrames.delete(syncedPlayer.player);
|
||||
syncedPlayer.player.overrideRender = noop;
|
||||
syncedPlayer.player.remove();
|
||||
syncedPlayer.player = undefined;
|
||||
}
|
||||
|
||||
lotties.delete(key);
|
||||
syncedPlayers.delete(syncedPlayer.key);
|
||||
|
||||
if(!lotties.size) {
|
||||
if(!syncedPlayers.size) {
|
||||
clearRenderInterval();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
|
||||
offsetsMap.set(containers, offsets);
|
||||
const offsets = visible.map(({rect: elementRect}) => {
|
||||
const top = elementRect.top - rect.top;
|
||||
const left = elementRect.left - rect.left;
|
||||
return {top, left};
|
||||
});
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
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;
|
||||
const {width: frameWidth, height: frameHeight} = frame;
|
||||
const maxTop = height - frameHeight;
|
||||
const maxLeft = width - frameWidth;
|
||||
|
||||
if(!this.clearedContainers.has(containers)) {
|
||||
containers.forEach((container) => {
|
||||
container.textContent = '';
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
type R = CustomEmojiRendererElement;
|
||||
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;
|
||||
|
||||
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());
|
||||
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 [renderer] of o) {
|
||||
renderer.clearCanvas();
|
||||
}
|
||||
|
||||
for(const r of t) {
|
||||
r.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<{
|
||||
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<{
|
||||
},
|
||||
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<{
|
||||
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<{
|
||||
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<{
|
||||
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 = {
|
||||
player: undefined,
|
||||
middlewares: new Set(),
|
||||
counter: 0
|
||||
};
|
||||
if(isLottie) {
|
||||
syncedPlayer = syncedPlayers.get(key);
|
||||
if(!syncedPlayer) {
|
||||
syncedPlayer = {
|
||||
player: undefined,
|
||||
middlewares: new Set(),
|
||||
pausedElements: new Set(),
|
||||
elementsCounter: 0,
|
||||
counter: 0,
|
||||
key
|
||||
};
|
||||
|
||||
lotties.set(key, l);
|
||||
syncedPlayers.set(key, syncedPlayer);
|
||||
}
|
||||
|
||||
renderer.syncedElements.set(syncedPlayer, undefined);
|
||||
|
||||
++syncedPlayer.counter;
|
||||
|
||||
if(middleware) {
|
||||
syncedPlayer.middlewares.add(middleware);
|
||||
}
|
||||
}
|
||||
|
||||
++l.counter;
|
||||
|
||||
if(middleware) {
|
||||
l.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<{
|
||||
|
||||
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<{
|
||||
load
|
||||
});
|
||||
} else {
|
||||
renderer.lazyLoadQueue = new LazyLoadQueue();
|
||||
load();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -171,9 +171,7 @@ export class LottieLoader {
|
||||
|
||||
const player = this.initPlayer(containers, params);
|
||||
|
||||
if(group !== 'none') {
|
||||
animationIntersector.addAnimation(player, group);
|
||||
}
|
||||
animationIntersector.addAnimation(player, group);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
@ -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';
|
||||
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 = {
|
||||
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<{
|
||||
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<{
|
||||
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<{
|
||||
// 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<{
|
||||
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<{
|
||||
}
|
||||
|
||||
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<{
|
||||
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<{
|
||||
|
||||
// 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<{
|
||||
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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
&,
|
||||
&-fake {
|
||||
margin-top: 1px;
|
||||
// margin-top: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
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 {
|
||||
|
@ -1747,7 +1747,7 @@ $bubble-beside-button-width: 38px;
|
||||
margin: inherit;
|
||||
|
||||
&:after {
|
||||
color: #fff;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
&.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
&.is-loading {
|
||||
min-height: 9rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-set-footer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
@ -47,24 +54,32 @@
|
||||
.sticker-set {
|
||||
margin: .0625rem 0;
|
||||
|
||||
&-stickers {
|
||||
.row-title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
&-stickers {
|
||||
--per-row: 5;
|
||||
--item-size: var(--esg-sticker-size);
|
||||
padding: 0 5px;
|
||||
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;
|
||||
|
@ -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;
|
||||
--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;
|
||||
--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;
|
||||
--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 {
|
||||
}
|
||||
|
||||
.rlottie-vector {
|
||||
fill: rgba(0, 0, 0, .08);
|
||||
fill: rgba(0, 0, 0, $hover-alpha);
|
||||
}
|
||||
|
||||
.canvas-thumbnail {
|
||||
|
Loading…
x
Reference in New Issue
Block a user