From d2d184d242dea0818859419d06abfaccd83fa39d Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Tue, 26 Jul 2022 07:22:46 +0200 Subject: [PATCH] Fix memory leaks --- src/components/appSearchSuper..ts | 45 ++++++++++++++----- src/components/call/videoCanvasBlur.ts | 2 +- src/components/chat/bubbles.ts | 8 ++-- src/components/chat/chat.ts | 5 +++ src/components/chat/gradientRenderer.ts | 4 +- src/components/chat/replyKeyboard.ts | 5 ++- src/components/chat/selection.ts | 6 +-- src/components/checkboxField.ts | 33 +++++++++----- .../emoticonsDropdown/tabs/emoji.ts | 3 +- src/components/emoticonsDropdown/tabs/gifs.ts | 3 +- src/components/horizontalMenu.ts | 11 +++-- src/components/peerProfile.ts | 16 ++++--- src/components/peerProfileAvatars.ts | 1 + src/components/popups/reactedList.ts | 2 +- src/components/row.ts | 16 ++++--- src/components/scrollable.ts | 18 ++++++-- .../sidebarLeft/tabs/autoDownload/file.ts | 2 +- .../sidebarLeft/tabs/autoDownload/photo.ts | 17 ++++--- .../sidebarLeft/tabs/autoDownload/video.ts | 2 +- .../sidebarLeft/tabs/dataAndStorage.ts | 15 ++++--- src/components/sidebarLeft/tabs/editFolder.ts | 19 ++++---- .../sidebarLeft/tabs/generalSettings.ts | 21 ++++++--- .../sidebarLeft/tabs/includedChats.ts | 5 ++- src/components/sidebarLeft/tabs/newChannel.ts | 5 ++- src/components/sidebarLeft/tabs/newGroup.ts | 7 +-- .../sidebarLeft/tabs/notifications.ts | 6 ++- .../sidebarLeft/tabs/privacyAndSecurity.ts | 27 +++++++---- src/components/sidebarLeft/tabs/settings.ts | 25 +++++++---- src/components/sidebarRight/index.ts | 16 ++++--- .../sidebarRight/tabs/chatReactions.ts | 6 ++- src/components/sidebarRight/tabs/chatType.ts | 3 +- src/components/sidebarRight/tabs/editChat.ts | 7 ++- .../sidebarRight/tabs/editContact.ts | 3 +- .../sidebarRight/tabs/groupPermissions.ts | 3 +- .../sidebarRight/tabs/sharedMedia.ts | 11 ++--- src/components/singleTransition.ts | 3 ++ src/components/slider.ts | 2 +- src/components/sliderTab.ts | 1 + src/components/transition.ts | 30 ++++++++----- src/components/wrappers/video.ts | 13 ++++-- src/helpers/canvas/getTextWidth.ts | 2 +- src/helpers/dom/renderImageWithFadeIn.ts | 7 ++- src/lib/appManagers/appDialogsManager.ts | 2 +- src/lib/appManagers/appImManager.ts | 5 +++ src/lib/mediaPlayer.ts | 18 ++++---- src/lib/rlottie/queryableWorker.ts | 4 +- webpack.common.js | 3 +- 47 files changed, 305 insertions(+), 163 deletions(-) diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 2c2e0f3b..947f280c 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -69,6 +69,8 @@ import { attachContextMenuListener } from "../helpers/dom/attachContextMenuListe import contextMenuController from "../helpers/contextMenuController"; import positionMenu from "../helpers/positionMenu"; import apiManagerProxy from "../lib/mtproto/mtprotoworker"; +import ListenerSetter from "../helpers/listenerSetter"; +import SwipeHandler from "./swipeHandler"; //const testScroll = false; @@ -107,7 +109,8 @@ class SearchContextMenu { constructor( private attachTo: HTMLElement, - private searchSuper: AppSearchSuper + private searchSuper: AppSearchSuper, + private listenerSetter: ListenerSetter ) { this.managers = searchSuper.managers; @@ -162,7 +165,7 @@ class SearchContextMenu { if(IS_TOUCH_SUPPORTED) { } else { - attachContextMenuListener(attachTo, onContextMenu as any); + attachContextMenuListener(attachTo, onContextMenu as any, listenerSetter); } } @@ -323,14 +326,18 @@ export default class AppSearchSuper { public managers: AppManagers; private loadFirstTimePromise: Promise; + private listenerSetter: ListenerSetter; + private swipeHandler: SwipeHandler; + constructor(options: Pick) { safeAssign(this, options); this.container = document.createElement('div'); this.container.classList.add('search-super'); - this.searchContextMenu = new SearchContextMenu(this.container, this); - this.selection = new SearchSelection(this, this.managers); + this.listenerSetter = new ListenerSetter(); + this.searchContextMenu = new SearchContextMenu(this.container, this, this.listenerSetter); + this.selection = new SearchSelection(this, this.managers, this.listenerSetter); const navScrollableContainer = this.navScrollableContainer = document.createElement('div'); navScrollableContainer.classList.add('search-super-tabs-scrollable', 'menu-horizontal-scrollable', 'sticky'); @@ -369,7 +376,7 @@ export default class AppSearchSuper { let unlockScroll: ReturnType; if(IS_TOUCH_SUPPORTED) { - handleTabSwipe({ + this.swipeHandler = handleTabSwipe({ element: this.tabsContainer, onSwipe: (xDiff, yDiff, e) => { const prevId = this.selectTab.prevId(); @@ -521,14 +528,14 @@ export default class AppSearchSuper { } this.onTransitionEnd(); - }, undefined, navScrollable); + }, undefined, navScrollable, this.listenerSetter); attachClickEvent(this.tabsContainer, (e) => { if(this.selection.isSelecting) { cancelEvent(e); this.selection.toggleByElement(findUpClassName(e.target, 'search-super-item')); } - }, {capture: true, passive: false}); + }, {capture: true, passive: false, listenerSetter: this.listenerSetter}); const onMediaClick = async(className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => { const target = findUpClassName(e.target as HTMLDivElement, className); @@ -560,8 +567,8 @@ export default class AppSearchSuper { .openMedia(message, targets[idx].element, 0, false, targets.slice(0, idx), targets.slice(idx + 1)); }; - attachClickEvent(this.tabs.inputMessagesFilterPhotoVideo, onMediaClick.bind(null, 'grid-item', 'grid-item', 'inputMessagesFilterPhotoVideo')); - attachClickEvent(this.tabs.inputMessagesFilterDocument, onMediaClick.bind(null, 'document-with-thumb', 'media-container', 'inputMessagesFilterDocument')); + attachClickEvent(this.tabs.inputMessagesFilterPhotoVideo, onMediaClick.bind(null, 'grid-item', 'grid-item', 'inputMessagesFilterPhotoVideo'), {listenerSetter: this.listenerSetter}); + attachClickEvent(this.tabs.inputMessagesFilterDocument, onMediaClick.bind(null, 'document-with-thumb', 'media-container', 'inputMessagesFilterDocument'), {listenerSetter: this.listenerSetter}); /* attachClickEvent(this.tabs.inputMessagesFilterUrl, (e) => { const target = e.target as HTMLElement; @@ -581,7 +588,7 @@ export default class AppSearchSuper { this.lazyLoadQueue.lock(); }, () => { this.lazyLoadQueue.unlockAndRefresh(); // ! maybe not so efficient - }); + }, this.listenerSetter); } private onTransitionStart = () => { @@ -920,7 +927,7 @@ export default class AppSearchSuper { element.dataset.peerId = '' + message.peerId; monthContainer.items[method](element); - if(this.selection.isSelecting) { + if(this.selection?.isSelecting) { this.selection.toggleElementCheckbox(element, true); } }); @@ -1524,7 +1531,7 @@ export default class AppSearchSuper { this.usedFromHistory[mediaTab.inputFilter] = -1; }); - if(this.selection.isSelecting) { + if(this.selection?.isSelecting) { this.selection.cancelSelection(); } @@ -1641,4 +1648,18 @@ export default class AppSearchSuper { this.cleanup(); } + + public destroy() { + this.listenerSetter.removeAll(); + this.scrollable.destroy(); + this.swipeHandler?.removeListeners(); + this.selection?.cleanup(); + + this.scrollStartCallback = undefined; + this.onChangeTab = undefined; + this.selectTab = undefined; + this.searchContextMenu = undefined; + this.swipeHandler = undefined; + this.selection = undefined; + } } diff --git a/src/components/call/videoCanvasBlur.ts b/src/components/call/videoCanvasBlur.ts index dc11f142..4b4252e1 100644 --- a/src/components/call/videoCanvasBlur.ts +++ b/src/components/call/videoCanvasBlur.ts @@ -13,7 +13,7 @@ export default function callVideoCanvasBlur(video: HTMLVideoElement) { canvas.width = size; canvas.height = size; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d', {alpha: false}); ctx.filter = 'blur(2px)'; const renderFrame = () => { ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index db9a0d7c..d98faede 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1937,7 +1937,7 @@ export default class ChatBubbles { } public onScroll = (ignoreHeavyAnimation?: boolean, scrollDimensions?: ScrollStartCallbackDimensions) => { - //return; + // return; if(this.isHeavyAnimationInProgress) { if(this.sliceViewportDebounced) { @@ -2470,8 +2470,7 @@ export default class ChatBubbles { } private destroyScrollable() { - this.scrollable.removeListeners(); - this.scrollable.onScrolledTop = this.scrollable.onScrolledBottom = this.scrollable.onAdditionalScroll = null; + this.scrollable.destroy(); } public destroy() { @@ -2517,6 +2516,7 @@ export default class ChatBubbles { // clear messages if(bubblesToo) { this.scrollable.container.textContent = ''; + this.chatInner.textContent = ''; this.cleanupPlaceholders(); } @@ -2805,7 +2805,7 @@ export default class ChatBubbles { this.attachPlaceholderOnRender(); } - if(!isTarget && this.chat.type === 'chat') { + if(!isTarget && this.chat.type === 'chat' && this.chat.topbar.pinnedMessage) { this.chat.topbar.pinnedMessage.setCorrectIndex(0); } diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index a8fd0d92..d858ef40 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -458,6 +458,11 @@ export default class Chat extends EventListenerBase<{ this.cleanup(true); this.bubbles.setPeer(false, peerId); this.appImManager.dispatchEvent('peer_changed', peerId); + + appSidebarRight.replaceSharedMediaTab(); + this.destroySharedMediaTab(); + this.sharedMediaTab = undefined; + return; } diff --git a/src/components/chat/gradientRenderer.ts b/src/components/chat/gradientRenderer.ts index 9033ed8f..4a409516 100644 --- a/src/components/chat/gradientRenderer.ts +++ b/src/components/chat/gradientRenderer.ts @@ -283,11 +283,11 @@ export default class ChatBackgroundGradientRenderer { this._hc = document.createElement('canvas'); this._hc.width = this._width; this._hc.height = this._height; - this._hctx = this._hc.getContext('2d'); + this._hctx = this._hc.getContext('2d', {alpha: false}); } this._canvas = el; - this._ctx = this._canvas.getContext('2d'); + this._ctx = this._canvas.getContext('2d', {alpha: false}); this.update(); } diff --git a/src/components/chat/replyKeyboard.ts b/src/components/chat/replyKeyboard.ts index 7fcfdf3a..9e686b6c 100644 --- a/src/components/chat/replyKeyboard.ts +++ b/src/components/chat/replyKeyboard.ts @@ -19,6 +19,7 @@ import safeAssign from "../../helpers/object/safeAssign"; import setInnerHTML from "../../helpers/dom/setInnerHTML"; import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText"; import { AppManagers } from "../../lib/appManagers/managers"; +import { attachClickEvent } from "../../helpers/dom/clickEvent"; export default class ReplyKeyboard extends DropdownHover { private static BASE_CLASS = 'reply-keyboard'; @@ -74,7 +75,7 @@ export default class ReplyKeyboard extends DropdownHover { } }); - this.listenerSetter.add(this.element)('click', (e) => { + attachClickEvent(this.element, (e) => { const target = findUpClassName(e.target, 'btn'); if(!target) { return; @@ -103,7 +104,7 @@ export default class ReplyKeyboard extends DropdownHover { } this.toggle(false); - }); + }, {listenerSetter: this.listenerSetter}); return super.init(); } diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index f62526fd..b2ef4983 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -533,7 +533,7 @@ export class SearchSelection extends AppSelection { private isPrivate: boolean; - constructor(private searchSuper: AppSearchSuper, managers: AppManagers) { + constructor(private searchSuper: AppSearchSuper, managers: AppManagers, listenerSetter: ListenerSetter) { super({ managers, verifyTarget: (e, target) => !!target && this.isSelecting, @@ -544,7 +544,7 @@ export class SearchSelection extends AppSelection { }); this.isPrivate = !searchSuper.showSender; - this.attachListeners(searchSuper.container, new ListenerSetter()); + this.attachListeners(searchSuper.container, listenerSetter); } /* public appendCheckbox(element: HTMLElement, checkboxField: CheckboxField) { @@ -615,7 +615,7 @@ export class SearchSelection extends AppSelection { this.selectionContainer.classList.add(BASE_CLASS + '-container'); const btnCancel = ButtonIcon(`close ${BASE_CLASS}-cancel`, {noRipple: true}); - this.listenerSetter.add(btnCancel)('click', () => this.cancelSelection(), {once: true}); + attachClickEvent(btnCancel, () => this.cancelSelection(), {listenerSetter: this.listenerSetter, once: true}); this.selectionCountEl = document.createElement('div'); this.selectionCountEl.classList.add(BASE_CLASS + '-count'); diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts index fce9cc48..7ec5b2e7 100644 --- a/src/components/checkboxField.ts +++ b/src/components/checkboxField.ts @@ -9,6 +9,7 @@ import { LangPackKey, _i18n } from "../lib/langPack"; import getDeepProperty from "../helpers/object/getDeepProperty"; import rootScope from "../lib/rootScope"; import apiManagerProxy from "../lib/mtproto/mtprotoworker"; +import ListenerSetter from "../helpers/listenerSetter"; export type CheckboxFieldOptions = { text?: LangPackKey, @@ -23,6 +24,7 @@ export type CheckboxFieldOptions = { restriction?: boolean, withRipple?: boolean, withHover?: boolean, + listenerSetter?: ListenerSetter }; export default class CheckboxField { public input: HTMLInputElement; @@ -57,7 +59,24 @@ export default class CheckboxField { } if(options.stateKey) { + let loaded = false; + const onChange = () => { + if(!loaded) { + return; + } + + let value: any; + if(options.stateValues) { + value = options.stateValues[input.checked ? 1 : 0]; + } else { + value = input.checked; + } + + rootScope.managers.appStateManager.setByKey(options.stateKey, value); + }; + apiManagerProxy.getState().then((state) => { + loaded = true; const stateValue = getDeepProperty(state, options.stateKey); let checked: boolean; if(options.stateValues) { @@ -67,18 +86,10 @@ export default class CheckboxField { } this.setValueSilently(checked); - - input.addEventListener('change', () => { - let value: any; - if(options.stateValues) { - value = options.stateValues[input.checked ? 1 : 0]; - } else { - value = input.checked; - } - - rootScope.managers.appStateManager.setByKey(options.stateKey, value); - }); }); + + if(options.listenerSetter) options.listenerSetter.add(input)('change', onChange); + else input.addEventListener('change', onChange); } let span: HTMLSpanElement; diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index d8eca3c4..55dc3f21 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -24,6 +24,7 @@ import { AppManagers } from "../../../lib/appManagers/managers"; import fixEmoji from "../../../lib/richTextProcessor/fixEmoji"; import wrapEmojiText from "../../../lib/richTextProcessor/wrapEmojiText"; import wrapSingleEmoji from "../../../lib/richTextProcessor/wrapSingleEmoji"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; const loadedURLs: Set = new Set(); export function appendEmoji(emoji: string, container: HTMLElement, prepend = false, unify = false) { @@ -258,7 +259,7 @@ export default class EmojiTab implements EmoticonsTab { }); }); - this.content.addEventListener('click', this.onContentClick); + attachClickEvent(this.content, this.onContentClick); this.init = null; rootScope.addEventListener('emoji_recent', (emoji) => { diff --git a/src/components/emoticonsDropdown/tabs/gifs.ts b/src/components/emoticonsDropdown/tabs/gifs.ts index 0bce91a7..b4d1023c 100644 --- a/src/components/emoticonsDropdown/tabs/gifs.ts +++ b/src/components/emoticonsDropdown/tabs/gifs.ts @@ -9,6 +9,7 @@ import GifsMasonry from "../../gifsMasonry"; import Scrollable from "../../scrollable"; import { putPreloader } from "../../putPreloader"; import { AppManagers } from "../../../lib/appManagers/managers"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; export default class GifsTab implements EmoticonsTab { private content: HTMLElement; @@ -20,7 +21,7 @@ export default class GifsTab implements EmoticonsTab { init() { this.content = document.getElementById('content-gifs'); const gifsContainer = this.content.firstElementChild as HTMLDivElement; - gifsContainer.addEventListener('click', EmoticonsDropdown.onMediaClick); + attachClickEvent(gifsContainer, EmoticonsDropdown.onMediaClick); const scroll = new Scrollable(this.content, 'GIFS'); const masonry = new GifsMasonry(gifsContainer, EMOTICONSSTICKERGROUP, scroll); diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index dc2a2d0c..44dfe244 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -11,6 +11,8 @@ import { fastRaf } from "../helpers/schedulers"; import { FocusDirection } from "../helpers/fastSmoothScroll"; import findUpAsChild from "../helpers/dom/findUpAsChild"; import whichChild from "../helpers/dom/whichChild"; +import ListenerSetter from "../helpers/listenerSetter"; +import { attachClickEvent } from "../helpers/dom/clickEvent"; export function horizontalMenu( tabs: HTMLElement, @@ -18,9 +20,10 @@ export function horizontalMenu( onClick?: (id: number, tabContent: HTMLDivElement, animate: boolean) => void | boolean | Promise, onTransitionEnd?: () => void, transitionTime = 250, - scrollableX?: ScrollableX + scrollableX?: ScrollableX, + listenerSetter?: ListenerSetter ) { - const selectTab = TransitionSlider(content, tabs || content.dataset.animation === 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd); + const selectTab = TransitionSlider(content, tabs || content.dataset.animation === 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd, undefined, listenerSetter); if(!tabs) { return selectTab; @@ -109,7 +112,7 @@ export function horizontalMenu( //const tagName = tabs.classList.contains('menu-horizontal-div') ? 'BUTTON' : 'LI'; const tagName = tabs.firstElementChild.tagName; - tabs.addEventListener('click', function(e) { + attachClickEvent(tabs, (e) => { let target = e.target as HTMLElement; target = findUpAsChild(target, tabs); @@ -129,7 +132,7 @@ export function horizontalMenu( } selectTarget(target, id); - }); + }, {listenerSetter}); return proxy; } diff --git a/src/components/peerProfile.ts b/src/components/peerProfile.ts index 57d9a5a0..dc2278e0 100644 --- a/src/components/peerProfile.ts +++ b/src/components/peerProfile.ts @@ -105,7 +105,8 @@ export default class PeerProfile { const full = await this.managers.appProfileManager.getProfileByPeerId(this.peerId); copyTextToClipboard(full.about); toast(I18n.format('BioCopied', true)); - } + }, + listenerSetter: this.listenerSetter }); this.bio.title.classList.add('pre-wrap'); @@ -118,7 +119,8 @@ export default class PeerProfile { const peer: Channel | User.user = await this.managers.appPeersManager.getPeer(this.peerId); copyTextToClipboard('@' + peer.username); toast(I18n.format('UsernameCopied', true)); - } + }, + listenerSetter: this.listenerSetter }); this.phone = new Row({ @@ -129,7 +131,8 @@ export default class PeerProfile { const peer: User = await this.managers.appUsersManager.getUser(this.peerId); copyTextToClipboard('+' + peer.phone); toast(I18n.format('PhoneCopied', true)); - } + }, + listenerSetter: this.listenerSetter }); this.link = new Row({ @@ -142,7 +145,8 @@ export default class PeerProfile { // copyTextToClipboard(chatFull.exported_invite.link); toast(I18n.format('LinkCopied', true)); // }); - } + }, + listenerSetter: this.listenerSetter }); this.location = new Row({ @@ -164,7 +168,8 @@ export default class PeerProfile { this.notifications = new Row({ checkboxField: new CheckboxField({toggle: true}), titleLangKey: 'Notifications', - icon: 'unmute' + icon: 'unmute', + listenerSetter: this.listenerSetter }); listenerSetter.add(this.notifications.checkboxField.input)('change', (e) => { @@ -457,5 +462,6 @@ export default class PeerProfile { public destroy() { this.clearSetMoreDetailsTimeout(); clearInterval(this.setPeerStatusInterval); + this.avatars?.cleanup(); } } diff --git a/src/components/peerProfileAvatars.ts b/src/components/peerProfileAvatars.ts index 35c032f7..e575b780 100644 --- a/src/components/peerProfileAvatars.ts +++ b/src/components/peerProfileAvatars.ts @@ -409,5 +409,6 @@ export default class PeerProfileAvatars { public cleanup() { this.listenerSetter.removeAll(); this.swipeHandler.removeListeners(); + this.intersectionObserver?.disconnect(); } } diff --git a/src/components/popups/reactedList.ts b/src/components/popups/reactedList.ts index d716dc49..51130bb8 100644 --- a/src/components/popups/reactedList.ts +++ b/src/components/popups/reactedList.ts @@ -185,7 +185,7 @@ export default class PopupReactedList extends PopupElement { const loader = loaders.get(tabContent); loader.load(); - }); + }, undefined, undefined, undefined, this.listenerSetter); // selectTab(hasAllReactions && hasReadParticipants ? 1 : 0, false); selectTab(0, false); diff --git a/src/components/row.ts b/src/components/row.ts index e1173485..e7a1bfaf 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -12,6 +12,8 @@ import RadioForm from "./radioForm"; import { i18n, LangPackKey } from "../lib/langPack"; import replaceContent from "../helpers/dom/replaceContent"; import setInnerHTML from "../helpers/dom/setInnerHTML"; +import ListenerSetter from "../helpers/listenerSetter"; +import { attachClickEvent } from "../helpers/dom/clickEvent"; export default class Row { public container: HTMLElement; @@ -41,7 +43,8 @@ export default class Row { navigationTab: SliderSuperTab, havePadding: boolean, noRipple: boolean, - noWrap: boolean + noWrap: boolean, + listenerSetter: ListenerSetter }> = {}) { this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div'); this.container.classList.add('row'); @@ -81,9 +84,12 @@ export default class Row { } if(!options.noCheckboxSubtitle && !isToggle) { - this.checkboxField.input.addEventListener('change', () => { + const onChange = () => { replaceContent(this.subtitle, i18n(this.checkboxField.input.checked ? 'Checkbox.Enabled' : 'Checkbox.Disabled')); - }); + }; + + if(options.listenerSetter) options.listenerSetter.add(this.checkboxField.input)('change', onChange); + else this.checkboxField.input.addEventListener('change', onChange); } } @@ -151,10 +157,10 @@ export default class Row { if(options.clickable || options.radioField || options.checkboxField) { if(typeof(options.clickable) === 'function') { - this.container.addEventListener('click', (e) => { + attachClickEvent(this.container, (e) => { if(this.freezed) return; (options.clickable as any)(e); - }); + }, {listenerSetter: options.listenerSetter}); } this.container.classList.add('row-clickable', 'hover-effect'); diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index fde9bf0a..ccee9946 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -143,6 +143,13 @@ export class ScrollableBase { this.removeHeavyAnimationListener = undefined; } + public destroy() { + this.removeListeners(); + this.onAdditionalScroll = undefined; + this.onScrolledTop = undefined; + this.onScrolledBottom = undefined; + } + public append(element: HTMLElement) { this.container.append(element); } @@ -160,7 +167,7 @@ export class ScrollableBase { //this.log('onScroll call', this.onScrollMeasure); //} - //return; + // return; if(this.isHeavyAnimationInProgress) { this.cancelMeasure(); @@ -172,7 +179,8 @@ export class ScrollableBase { if((!this.onScrolledTop && !this.onScrolledBottom) && !this.splitUp && !this.onAdditionalScroll) return; if(this.onScrollMeasure) return; // if(this.onScrollMeasure) window.cancelAnimationFrame(this.onScrollMeasure); - this.onScrollMeasure = window.requestAnimationFrame(() => { + // this.onScrollMeasure = window.requestAnimationFrame(() => { + this.onScrollMeasure = window.setTimeout(() => { this.onScrollMeasure = 0; const scrollPosition = this.container[this.scrollProperty]; @@ -187,12 +195,14 @@ export class ScrollableBase { if(this.checkForTriggers) { this.checkForTriggers(); } - }); + // }); + }, 200); }; public cancelMeasure() { if(this.onScrollMeasure) { - window.cancelAnimationFrame(this.onScrollMeasure); + // window.cancelAnimationFrame(this.onScrollMeasure); + clearTimeout(this.onScrollMeasure); this.onScrollMeasure = 0; } } diff --git a/src/components/sidebarLeft/tabs/autoDownload/file.ts b/src/components/sidebarLeft/tabs/autoDownload/file.ts index 1a698c36..0eab9604 100644 --- a/src/components/sidebarLeft/tabs/autoDownload/file.ts +++ b/src/components/sidebarLeft/tabs/autoDownload/file.ts @@ -21,7 +21,7 @@ export default class AppAutoDownloadFileTab extends SliderSuperTabEventable { this.managers.appStateManager.setByKey('settings.autoDownloadNew.file_size_max', sizeMax); }, 200, false, true); - const section = autoDownloadPeerTypeSection('file', 'AutoDownloadFilesTitle'); + const section = autoDownloadPeerTypeSection('file', 'AutoDownloadFilesTitle', this.listenerSetter); const MIN = 512 * 1024; // const MAX = 2 * 1024 * 1024 * 1024; diff --git a/src/components/sidebarLeft/tabs/autoDownload/photo.ts b/src/components/sidebarLeft/tabs/autoDownload/photo.ts index 2b2750ad..e0eef22a 100644 --- a/src/components/sidebarLeft/tabs/autoDownload/photo.ts +++ b/src/components/sidebarLeft/tabs/autoDownload/photo.ts @@ -5,11 +5,12 @@ */ import { SettingSection } from "../.."; +import ListenerSetter from "../../../../helpers/listenerSetter"; import { LangPackKey } from "../../../../lib/langPack"; import CheckboxField from "../../../checkboxField"; import { SliderSuperTabEventable } from "../../../sliderTab"; -export function autoDownloadPeerTypeSection(type: 'photo' | 'video' | 'file', title: LangPackKey) { +export function autoDownloadPeerTypeSection(type: 'photo' | 'video' | 'file', title: LangPackKey, listenerSetter: ListenerSetter) { const section = new SettingSection({name: title}); const key = 'settings.autoDownload.' + type + '.'; @@ -17,25 +18,29 @@ export function autoDownloadPeerTypeSection(type: 'photo' | 'video' | 'file', ti text: 'AutodownloadContacts', name: 'contacts', stateKey: key + 'contacts', - withRipple: true + withRipple: true, + listenerSetter }); const privateCheckboxField = new CheckboxField({ text: 'AutodownloadPrivateChats', name: 'private', stateKey: key + 'private', - withRipple: true + withRipple: true, + listenerSetter }); const groupsCheckboxField = new CheckboxField({ text: 'AutodownloadGroupChats', name: 'groups', stateKey: key + 'groups', - withRipple: true + withRipple: true, + listenerSetter }); const channelsCheckboxField = new CheckboxField({ text: 'AutodownloadChannels', name: 'channels', stateKey: key + 'channels', - withRipple: true + withRipple: true, + listenerSetter }); section.content.append( @@ -53,7 +58,7 @@ export default class AppAutoDownloadPhotoTab extends SliderSuperTabEventable { this.header.classList.add('with-border'); this.setTitle('AutoDownloadPhotos'); - const section = autoDownloadPeerTypeSection('photo', 'AutoDownloadPhotosTitle'); + const section = autoDownloadPeerTypeSection('photo', 'AutoDownloadPhotosTitle', this.listenerSetter); this.scrollable.append(section.container); } } diff --git a/src/components/sidebarLeft/tabs/autoDownload/video.ts b/src/components/sidebarLeft/tabs/autoDownload/video.ts index bc13ac0b..62e3bc54 100644 --- a/src/components/sidebarLeft/tabs/autoDownload/video.ts +++ b/src/components/sidebarLeft/tabs/autoDownload/video.ts @@ -12,7 +12,7 @@ export default class AppAutoDownloadVideoTab extends SliderSuperTabEventable { this.header.classList.add('with-border'); this.setTitle('AutoDownloadVideos'); - const section = autoDownloadPeerTypeSection('video', 'AutoDownloadVideosTitle'); + const section = autoDownloadPeerTypeSection('video', 'AutoDownloadVideosTitle', this.listenerSetter); this.scrollable.append(section.container); } } diff --git a/src/components/sidebarLeft/tabs/dataAndStorage.ts b/src/components/sidebarLeft/tabs/dataAndStorage.ts index d5f92621..b59d5585 100644 --- a/src/components/sidebarLeft/tabs/dataAndStorage.ts +++ b/src/components/sidebarLeft/tabs/dataAndStorage.ts @@ -75,7 +75,8 @@ export default class AppDataAndStorageTab extends SliderSuperTabEventable { subtitle: '', clickable: () => { openTab(AppAutoDownloadPhotoTab); - } + }, + listenerSetter: this.listenerSetter }); const videoRow = new Row({ @@ -83,7 +84,8 @@ export default class AppDataAndStorageTab extends SliderSuperTabEventable { subtitle: '', clickable: () => { openTab(AppAutoDownloadVideoTab); - } + }, + listenerSetter: this.listenerSetter }); const fileRow = new Row({ @@ -91,7 +93,8 @@ export default class AppDataAndStorageTab extends SliderSuperTabEventable { subtitle: '', clickable: () => { openTab(AppAutoDownloadFileTab); - } + }, + listenerSetter: this.listenerSetter }); const resetButton = Button('btn-primary btn-transparent primary', {icon: 'delete', text: 'ResetAutomaticMediaDownload'}); @@ -154,13 +157,15 @@ export default class AppDataAndStorageTab extends SliderSuperTabEventable { text: 'AutoplayGIF', name: 'gifs', stateKey: 'settings.autoPlay.gifs', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); const videosCheckboxField = new CheckboxField({ text: 'AutoplayVideo', name: 'videos', stateKey: 'settings.autoPlay.videos', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); section.content.append(gifsCheckboxField.label, videosCheckboxField.label); diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index f1cc060c..2ee4d241 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -24,6 +24,7 @@ import deepEqual from "../../../helpers/object/deepEqual"; import documentFragmentToHTML from "../../../helpers/dom/documentFragmentToHTML"; import wrapDraftText from "../../../lib/richTextProcessor/wrapDraftText"; import filterAsync from "../../../helpers/array/filterAsync"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; const MAX_FOLDER_NAME_LENGTH = 12; @@ -79,7 +80,7 @@ export default class AppEditFolderTab extends SliderSuperTab { }).show(); } }; - this.menuBtn = ButtonMenuToggle({}, 'bottom-left', [deleteFolderButton]); + this.menuBtn = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', [deleteFolderButton]); this.menuBtn.classList.add('hide'); this.header.append(this.confirmBtn, this.menuBtn); @@ -174,15 +175,15 @@ export default class AppEditFolderTab extends SliderSuperTab { const includedFlagsContainer = this.includePeerIds.container.querySelector('.folder-categories'); const excludedFlagsContainer = this.excludePeerIds.container.querySelector('.folder-categories'); - includedFlagsContainer.querySelector('.btn').addEventListener('click', () => { + attachClickEvent(includedFlagsContainer.querySelector('.btn') as HTMLElement, () => { this.slider.createTab(AppIncludedChatsTab).open(this.filter, 'included', this); - }); + }, {listenerSetter: this.listenerSetter}); - excludedFlagsContainer.querySelector('.btn').addEventListener('click', () => { + attachClickEvent(excludedFlagsContainer.querySelector('.btn') as HTMLElement, () => { this.slider.createTab(AppIncludedChatsTab).open(this.filter, 'excluded', this); - }); + }, {listenerSetter: this.listenerSetter}); - this.confirmBtn.addEventListener('click', () => { + attachClickEvent(this.confirmBtn, () => { if(this.nameInputField.input.classList.contains('error')) { return; } @@ -222,9 +223,9 @@ export default class AppEditFolderTab extends SliderSuperTab { }).finally(() => { this.confirmBtn.removeAttribute('disabled'); }); - }); + }, {listenerSetter: this.listenerSetter}); - this.nameInputField.input.addEventListener('input', () => { + this.listenerSetter.add(this.nameInputField.input)('input', () => { this.filter.title = this.nameInputField.value; this.editCheckForChange(); }); @@ -336,7 +337,7 @@ export default class AppEditFolderTab extends SliderSuperTab { const content = section.generateContentElement(); showMore = Button('folder-category-button btn btn-primary btn-transparent', {icon: 'down'}); showMore.classList.add('load-more', 'rp-overflow'); - showMore.addEventListener('click', () => renderMore(20)); + attachClickEvent(showMore, () => renderMore(20), {listenerSetter: this.listenerSetter}); showMore.append(i18n('FilterShowMoreChats', [peers.length])); content.append(showMore); diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index 8627040e..c8ee7352 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -111,7 +111,8 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { text: 'EnableAnimations', name: 'animations', stateKey: 'settings.animationsEnabled', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); container.append(range.container, chatBackgroundButton, animationsCheckboxField.label); @@ -231,13 +232,15 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { text: 'GeneralSettings.EmojiPrediction', name: 'suggest-emoji', stateKey: 'settings.emoji.suggest', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); const bigCheckboxField = new CheckboxField({ text: 'GeneralSettings.BigEmoji', name: 'emoji-big', stateKey: 'settings.emoji.big', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); container.append(suggestCheckboxField.label, bigCheckboxField.label); @@ -251,7 +254,8 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { havePadding: true, clickable: () => { this.slider.createTab(AppQuickReactionTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const renderQuickReaction = () => { @@ -272,13 +276,15 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { text: 'Stickers.SuggestStickers', name: 'suggest', stateKey: 'settings.stickers.suggest', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); const loopCheckboxField = new CheckboxField({ text: 'InstalledStickers.LoopAnimated', name: 'loop', stateKey: 'settings.stickers.loop', - withRipple: true + withRipple: true, + listenerSetter: this.listenerSetter }); const stickerSets: {[id: string]: Row} = {}; @@ -294,7 +300,8 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { havePadding: true, clickable: () => { new PopupStickers({id: stickerSet.id, access_hash: stickerSet.access_hash}).show(); - } + }, + listenerSetter: this.listenerSetter }); stickerSets[stickerSet.id] = row; diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index 0cb1309c..9623f9c1 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -19,6 +19,7 @@ import copy from "../../../helpers/object/copy"; import forEachReverse from "../../../helpers/array/forEachReverse"; import setInnerHTML from "../../../helpers/dom/setInnerHTML"; import wrapEmojiText from "../../../lib/richTextProcessor/wrapEmojiText"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; export default class AppIncludedChatsTab extends SliderSuperTab { private editFolderTab: AppEditFolderTab; @@ -39,7 +40,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { this.header.append(this.confirmBtn); - this.confirmBtn.addEventListener('click', async() => { + attachClickEvent(this.confirmBtn, async() => { const selected = this.selector.getSelected(); //this.filter.pFlags = {}; @@ -103,7 +104,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { this.editFolderTab.setFilter(this.filter, false); this.close(); - }); + }, {listenerSetter: this.listenerSetter}); this.dialogsByFilters = new Map(); return this.managers.filtersStorage.getDialogFilters().then(async(filters) => { diff --git a/src/components/sidebarLeft/tabs/newChannel.ts b/src/components/sidebarLeft/tabs/newChannel.ts index df80a259..39fd8212 100644 --- a/src/components/sidebarLeft/tabs/newChannel.ts +++ b/src/components/sidebarLeft/tabs/newChannel.ts @@ -13,6 +13,7 @@ import AppAddMembersTab from "./addMembers"; import { _i18n } from "../../../lib/langPack"; import ButtonCorner from "../../buttonCorner"; import appImManager from "../../../lib/appManagers/appImManager"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; export default class AppNewChannelTab extends SliderSuperTab { private uploadAvatar: () => Promise = null; @@ -60,7 +61,7 @@ export default class AppNewChannelTab extends SliderSuperTab { this.nextBtn = ButtonCorner({icon: 'arrow_next'}); - this.nextBtn.addEventListener('click', () => { + attachClickEvent(this.nextBtn, () => { const title = this.channelNameInputField.value; const about = this.channelDescriptionInputField.value; @@ -89,7 +90,7 @@ export default class AppNewChannelTab extends SliderSuperTab { } }); }); - }); + }, {listenerSetter: this.listenerSetter}); this.content.append(this.nextBtn); section.content.append(this.avatarEdit.container, inputWrapper); diff --git a/src/components/sidebarLeft/tabs/newGroup.ts b/src/components/sidebarLeft/tabs/newGroup.ts index ffd944f6..f2f5a1a2 100644 --- a/src/components/sidebarLeft/tabs/newGroup.ts +++ b/src/components/sidebarLeft/tabs/newGroup.ts @@ -14,6 +14,7 @@ import I18n from "../../../lib/langPack"; import ButtonCorner from "../../buttonCorner"; import getUserStatusString from "../../wrappers/getUserStatusString"; import appImManager from "../../../lib/appManagers/appImManager"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; interface OpenStreetMapInterface { place_id?: number; @@ -68,7 +69,7 @@ export default class AppNewGroupTab extends SliderSuperTab { this.groupLocationInputField.container ); - this.groupNameInputField.input.addEventListener('input', () => { + this.listenerSetter.add(this.groupNameInputField.input)('input', () => { const value = this.groupNameInputField.value; let valueCheck = !!value.length && !this.groupNameInputField.input.classList.contains('error'); if(this.isGeoChat) valueCheck = valueCheck && !!this.userLocationCoords && !!this.userLocationAddress; @@ -77,7 +78,7 @@ export default class AppNewGroupTab extends SliderSuperTab { this.nextBtn = ButtonCorner({icon: 'arrow_next'}); - this.nextBtn.addEventListener('click', () => { + attachClickEvent(this.nextBtn, () => { const title = this.groupNameInputField.value; let promise: Promise; @@ -128,7 +129,7 @@ export default class AppNewGroupTab extends SliderSuperTab { appImManager.setInnerPeer({peerId: chatId.toPeerId(true)}); }); - }); + }, {listenerSetter: this.listenerSetter}); const chatsSection = new SettingSection({ name: 'Members', diff --git a/src/components/sidebarLeft/tabs/notifications.ts b/src/components/sidebarLeft/tabs/notifications.ts index 4e9c1541..f7e88777 100644 --- a/src/components/sidebarLeft/tabs/notifications.ts +++ b/src/components/sidebarLeft/tabs/notifications.ts @@ -36,11 +36,13 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { const enabledRow = new Row({ checkboxField: new CheckboxField({text: options.typeText, checked: true}), subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); const previewEnabledRow = new Row({ checkboxField: new CheckboxField({text: 'MessagePreview', checked: true}), subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); section.content.append(enabledRow.container, previewEnabledRow.container); @@ -112,11 +114,13 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { const contactsSignUpRow = new Row({ checkboxField: new CheckboxField({text: 'ContactJoined', checked: true}), subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); const soundRow = new Row({ - checkboxField: new CheckboxField({text: 'Notifications.Sound', checked: true, stateKey: 'settings.notifications.sound'}), + checkboxField: new CheckboxField({text: 'Notifications.Sound', checked: true, stateKey: 'settings.notifications.sound', listenerSetter: this.listenerSetter}), subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); apiManagerProxy.getState().then((state) => { diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index a8147e20..ba41c44c 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -56,7 +56,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { const tab = this.slider.createTab(AppBlockedUsersTab); tab.peerIds = blockedPeerIds; tab.open(); - } + }, + listenerSetter: this.listenerSetter }); blockedUsersRow.freezed = true; @@ -81,7 +82,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { tab.state = passwordState; tab.open(); - } + }, + listenerSetter: this.listenerSetter }; const twoFactorRow = new Row(twoFactorRowOptions); @@ -98,7 +100,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { this.updateActiveSessions(); }, {once: true}); tab.open(); - } + }, + listenerSetter: this.listenerSetter }); activeSessionsRow.freezed = true; @@ -157,7 +160,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyPhoneNumberTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const lastSeenTimeRow = rowsByKeys['inputPrivacyKeyStatusTimestamp'] = new Row({ @@ -165,7 +169,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyLastSeenTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const photoVisibilityRow = rowsByKeys['inputPrivacyKeyProfilePhoto'] = new Row({ @@ -173,7 +178,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyProfilePhotoTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const callRow = rowsByKeys['inputPrivacyKeyPhoneCall'] = new Row({ @@ -181,7 +187,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyCallsTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const linkAccountRow = rowsByKeys['inputPrivacyKeyForwards'] = new Row({ @@ -189,7 +196,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyForwardMessagesTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const groupChatsAddRow = rowsByKeys['inputPrivacyKeyChatInvite'] = new Row({ @@ -197,7 +205,8 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { subtitleLangKey: SUBTITLE, clickable: () => { this.slider.createTab(AppPrivacyAddToGroupsTab).open(); - } + }, + listenerSetter: this.listenerSetter }); const updatePrivacyRow = (key: InputPrivacyKey['_']) => { diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index c2e142e6..7c29562d 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -27,6 +27,7 @@ import { SliderSuperTabConstructable } from "../../sliderTab"; import PopupAvatar from "../../popups/avatar"; import { AccountAuthorizations, Authorization } from "../../../layer"; import PopupElement from "../../popups"; +import { attachClickEvent } from "../../../helpers/dom/clickEvent"; //import AppMediaViewer from "../../appMediaViewerNew"; export default class AppSettingsTab extends SliderSuperTab { @@ -50,7 +51,7 @@ export default class AppSettingsTab extends SliderSuperTab { this.container.classList.add('settings-container'); this.setTitle('Settings'); - const btnMenu = ButtonMenuToggle({}, 'bottom-left', [{ + const btnMenu = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', [{ icon: 'logout', text: 'EditAccount.Logout', onClick: () => { @@ -78,14 +79,14 @@ export default class AppSettingsTab extends SliderSuperTab { const fillPromise = this.profile.fillProfileElements(); const changeAvatarBtn = Button('btn-circle btn-corner z-depth-1 profile-change-avatar', {icon: 'cameraadd'}); - changeAvatarBtn.addEventListener('click', () => { + attachClickEvent(changeAvatarBtn, () => { const canvas = document.createElement('canvas'); PopupElement.createPopup(PopupAvatar).open(canvas, (upload) => { upload().then((inputFile) => { return this.managers.appProfileManager.uploadProfilePhoto(inputFile); }); }); - }); + }, {listenerSetter: this.listenerSetter}); this.profile.element.lastElementChild.firstElementChild.append(changeAvatarBtn); const updateChangeAvatarBtn = async() => { @@ -160,7 +161,8 @@ export default class AppSettingsTab extends SliderSuperTab { clickable: () => { this.slider.createTab(tabConstructor).open(); // new tabConstructor(this.slider, true).open(); - } + }, + listenerSetter: this.listenerSetter }); }); @@ -181,7 +183,8 @@ export default class AppSettingsTab extends SliderSuperTab { this.updateActiveSessions(true); }, {once: true}); tab.open(); - } + }, + listenerSetter: this.listenerSetter }), this.languageRow = new Row({ @@ -190,7 +193,8 @@ export default class AppSettingsTab extends SliderSuperTab { icon: 'language', clickable: () => { this.slider.createTab(AppLanguageTab).open(); - } + }, + listenerSetter: this.listenerSetter }) ); @@ -204,10 +208,10 @@ export default class AppSettingsTab extends SliderSuperTab { this.scrollable.append(this.profile.element/* profileSection.container */, buttonsSection.container); - this.buttons.edit.addEventListener('click', () => { + attachClickEvent(this.buttons.edit, () => { const tab = this.slider.createTab(AppEditProfileTab); tab.open(); - }); + }, {listenerSetter: this.listenerSetter}); lottieLoader.loadLottieWorkers(); @@ -235,4 +239,9 @@ export default class AppSettingsTab extends SliderSuperTab { this.devicesRow.titleRight.textContent = '' + this.authorizations.length; }); } + + public onCloseAfterTimeout() { + this.profile.destroy(); + return super.onCloseAfterTimeout(); + } } diff --git a/src/components/sidebarRight/index.ts b/src/components/sidebarRight/index.ts index 83c505de..0bc4858f 100644 --- a/src/components/sidebarRight/index.ts +++ b/src/components/sidebarRight/index.ts @@ -46,15 +46,19 @@ export class AppSidebarRight extends SidebarSlider { return tab; } - public replaceSharedMediaTab(tab: AppSharedMediaTab) { + public replaceSharedMediaTab(tab?: AppSharedMediaTab) { let previousTab = this.sharedMediaTab; if(previousTab) { - const wasActive = previousTab.container.classList.contains('active'); - if(wasActive) { - tab.container.classList.add('active'); + if(tab) { + const wasActive = previousTab.container.classList.contains('active'); + if(wasActive) { + tab.container.classList.add('active'); + } + + previousTab.container.replaceWith(tab.container); + } else { + previousTab.container.remove(); } - - previousTab.container.replaceWith(tab.container); } else { this.tabsContainer.prepend(tab.container); } diff --git a/src/components/sidebarRight/tabs/chatReactions.ts b/src/components/sidebarRight/tabs/chatReactions.ts index 5b8a4efb..4e4011c0 100644 --- a/src/components/sidebarRight/tabs/chatReactions.ts +++ b/src/components/sidebarRight/tabs/chatReactions.ts @@ -29,7 +29,8 @@ export default class AppChatReactionsTab extends SliderSuperTabEventable { const toggleCheckboxField = new CheckboxField({toggle: true, checked: !!enabledReactions.size}); const toggleRow = new Row({ checkboxField: toggleCheckboxField, - titleLangKey: 'EnableReactions' + titleLangKey: 'EnableReactions', + listenerSetter: this.listenerSetter }); toggleSection.content.append(toggleRow.container); @@ -65,7 +66,8 @@ export default class AppChatReactionsTab extends SliderSuperTabEventable { const row = new Row({ checkboxField, title: availableReaction.title, - havePadding: true + havePadding: true, + listenerSetter: this.listenerSetter }); wrapStickerToRow({ diff --git a/src/components/sidebarRight/tabs/chatType.ts b/src/components/sidebarRight/tabs/chatType.ts index 38629f9c..ef58421a 100644 --- a/src/components/sidebarRight/tabs/chatType.ts +++ b/src/components/sidebarRight/tabs/chatType.ts @@ -78,7 +78,8 @@ export default class AppChatTypeTab extends SliderSuperTabEventable { clickable: () => { copyTextToClipboard((this.chatFull.exported_invite as ExportedChatInvite.chatInviteExported).link); toast(I18n.format('LinkCopied', true)); - } + }, + listenerSetter: this.listenerSetter }); const btnRevoke = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'RevokeLink'}); diff --git a/src/components/sidebarRight/tabs/editChat.ts b/src/components/sidebarRight/tabs/editChat.ts index 3e6d0ec1..a156e00d 100644 --- a/src/components/sidebarRight/tabs/editChat.ts +++ b/src/components/sidebarRight/tabs/editChat.ts @@ -113,7 +113,8 @@ export default class AppEditChatTab extends SliderSuperTab { this.listenerSetter.add(tab.eventListener)('destroy', setChatTypeSubtitle); }, - icon: 'lock' + icon: 'lock', + listenerSetter: this.listenerSetter }); const setChatTypeSubtitle = () => { @@ -147,7 +148,8 @@ export default class AppEditChatTab extends SliderSuperTab { this.listenerSetter.add(tab.eventListener)('destroy', setReactionsLength); }); - } + }, + listenerSetter: this.listenerSetter }); const availableReactions = await this.managers.appReactionsManager.getAvailableReactions(); @@ -182,6 +184,7 @@ export default class AppEditChatTab extends SliderSuperTab { tab.open(); }, icon: 'permissions', + listenerSetter: this.listenerSetter }); const setPermissionsLength = async() => { diff --git a/src/components/sidebarRight/tabs/editContact.ts b/src/components/sidebarRight/tabs/editContact.ts index cb190fc9..180d0965 100644 --- a/src/components/sidebarRight/tabs/editContact.ts +++ b/src/components/sidebarRight/tabs/editContact.ts @@ -117,7 +117,8 @@ export default class AppEditContactTab extends SliderSuperTab { if(!isNew) { const notificationsRow = new Row({ - checkboxField: notificationsCheckboxField + checkboxField: notificationsCheckboxField, + listenerSetter: this.listenerSetter }); const enabled = !(await this.managers.appNotificationsManager.isPeerLocalMuted(this.peerId, false)); diff --git a/src/components/sidebarRight/tabs/groupPermissions.ts b/src/components/sidebarRight/tabs/groupPermissions.ts index e292a570..d6511ea1 100644 --- a/src/components/sidebarRight/tabs/groupPermissions.ts +++ b/src/components/sidebarRight/tabs/groupPermissions.ts @@ -185,7 +185,8 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable { placeholder: 'ExceptionModal.Search.Placeholder', peerId: -this.chatId, }); - } + }, + listenerSetter: this.listenerSetter }); const openPermissions = async(peerId: PeerId) => { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 1355e654..6ab5c942 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -85,7 +85,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { // * body - this.profile = new PeerProfile(this.managers, this.scrollable); + this.profile = new PeerProfile(this.managers, this.scrollable, this.listenerSetter); this.profile.init(); this.scrollable.append(this.profile.element); @@ -124,7 +124,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { } else if(!this.scrollable.isHeavyAnimationInProgress) { this.slider.onCloseBtnClick(); } - }); + }, {listenerSetter: this.listenerSetter}); attachClickEvent(this.editBtn, (e) => { let tab: AppEditChatTab | AppEditContactTab; @@ -143,7 +143,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { tab.open(); } - }); + }, {listenerSetter: this.listenerSetter}); this.listenerSetter.add(rootScope)('contacts_update', (userId) => { if(this.peerId === userId) { @@ -217,7 +217,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { const btnAddMembers = ButtonCorner({icon: 'addmember_filled'}); this.content.append(btnAddMembers); - btnAddMembers.addEventListener('click', async() => { + attachClickEvent(btnAddMembers, async() => { const peerId = this.peerId; const id = this.peerId.toChatId(); const isChannel = await this.managers.appChatsManager.isChannel(id); @@ -315,7 +315,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { }, }); } - }); + }, {listenerSetter: this.listenerSetter}); //console.log('construct shared media time:', performance.now() - perf); } @@ -458,5 +458,6 @@ export default class AppSharedMediaTab extends SliderSuperTab { this.destroyable = true; this.onCloseAfterTimeout(); this.profile.destroy(); + this.searchSuper.destroy(); } } diff --git a/src/components/singleTransition.ts b/src/components/singleTransition.ts index 50f38555..b9ea9620 100644 --- a/src/components/singleTransition.ts +++ b/src/components/singleTransition.ts @@ -19,6 +19,9 @@ const SetTransition = ( clearTimeout(+timeout); } + // useRafs = undefined; + // duration = 0; + if(raf !== undefined) { window.cancelAnimationFrame(+raf); if(!useRafs) { diff --git a/src/components/slider.ts b/src/components/slider.ts index dca5a8f7..331c6867 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -152,7 +152,7 @@ export default class SidebarSlider { if(tab.onCloseAfterTimeout) { setTimeout(() => { tab.onCloseAfterTimeout(); - }, TRANSITION_TIME); + }, TRANSITION_TIME + 30); } } } diff --git a/src/components/sliderTab.ts b/src/components/sliderTab.ts index 15053482..ef017150 100644 --- a/src/components/sliderTab.ts +++ b/src/components/sliderTab.ts @@ -107,6 +107,7 @@ export default class SliderSuperTab implements SliderTab { if(this.destroyable) { // ! WARNING, пока что это будет работать только с самой последней внутренней вкладкой ! this.slider.tabs.delete(this); this.container.remove(); + this.scrollable.destroy(); } if(this.listenerSetter) { diff --git a/src/components/transition.ts b/src/components/transition.ts index 9db90c7b..7ad11c1f 100644 --- a/src/components/transition.ts +++ b/src/components/transition.ts @@ -9,6 +9,7 @@ import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromi import { dispatchHeavyAnimationEvent } from "../hooks/useHeavyAnimationCheck"; import whichChild from "../helpers/dom/whichChild"; import cancelEvent from "../helpers/dom/cancelEvent"; +import ListenerSetter from "../helpers/listenerSetter"; function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { const width = prevTabContent.getBoundingClientRect().width; @@ -84,7 +85,8 @@ export const TransitionSlider = ( type: 'tabs' | 'navigation' | 'zoom-fade' | 'slide-fade' | 'none'/* | 'counter' */, transitionTime: number, onTransitionEnd?: (id: number) => void, - isHeavy = true + isHeavy = true, + listenerSetter?: ListenerSetter ) => { let animationFunction: TransitionFunction = null; @@ -101,7 +103,7 @@ export const TransitionSlider = ( content.dataset.animation = type; - return Transition(content, animationFunction, transitionTime, onTransitionEnd, isHeavy); + return Transition(content, animationFunction, transitionTime, onTransitionEnd, isHeavy, undefined, undefined, listenerSetter); }; type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void); @@ -113,7 +115,8 @@ const Transition = ( onTransitionEnd?: (id: number) => void, isHeavy = true, once = false, - withAnimationListener = true + withAnimationListener = true, + listenerSetter?: ListenerSetter ) => { const onTransitionEndCallbacks: Map = new Map(); let animationDeferred: CancellablePromise; @@ -133,7 +136,7 @@ const Transition = ( //console.log('Transition: transitionend', /* content, */ e, selectTab.prevId, performance.now() - animationStarted); const callback = onTransitionEndCallbacks.get(e.target as HTMLElement); - if(callback) callback(); + callback?.(); if(e.target !== from) { return; @@ -153,14 +156,16 @@ const Transition = ( content.classList.remove('animating', 'backwards', 'disable-hover'); if(once) { - content.removeEventListener(listenerName, onEndEvent/* , {capture: false} */); + if(listenerSetter) listenerSetter.removeManual(content, listenerName, onEndEvent); + else content.removeEventListener(listenerName, onEndEvent/* , {capture: false} */); from = animationDeferred = undefined; onTransitionEndCallbacks.clear(); } }; // TODO: check for transition type (transform, etc) using by animationFunction - content.addEventListener(listenerName, onEndEvent/* , {passive: true, capture: false} */); + if(listenerSetter) listenerSetter.add(content)(listenerName, onEndEvent); + else content.addEventListener(listenerName, onEndEvent/* , {passive: true, capture: false} */); } function selectTab(id: number | HTMLElement, animate = true, overrideFrom?: typeof from) { @@ -196,9 +201,7 @@ const Transition = ( if(from) from.classList.remove('active', 'to', 'from'); else if(to) { // fix instant opening back from closed slider (e.g. instant closening and opening right sidebar) const callback = onTransitionEndCallbacks.get(to); - if(callback) { - callback(); - } + callback?.(); } if(to) { @@ -254,21 +257,24 @@ const Transition = ( } if(from/* && false */) { + let timeout: number; const _from = from; const callback = () => { + clearTimeout(timeout); _from.classList.remove('active', 'from'); if(onTransitionEndCallback) { - onTransitionEndCallback(); + onTransitionEndCallback?.(); } onTransitionEndCallbacks.delete(_from); }; if(to) { + timeout = window.setTimeout(callback, transitionTime + 100); // something happened to container onTransitionEndCallbacks.set(_from, callback); } else { - const timeout = window.setTimeout(callback, transitionTime); + timeout = window.setTimeout(callback, transitionTime); onTransitionEndCallbacks.set(_from, () => { clearTimeout(timeout); onTransitionEndCallbacks.delete(_from); @@ -290,7 +296,7 @@ const Transition = ( //selectTab.prevId = -1; selectTab.prevId = () => from ? whichChild(from) : -1; - + return selectTab; }; diff --git a/src/components/wrappers/video.ts b/src/components/wrappers/video.ts index 7db1a23d..38a8c414 100644 --- a/src/components/wrappers/video.ts +++ b/src/components/wrappers/video.ts @@ -15,7 +15,8 @@ import isInDOM from "../../helpers/dom/isInDOM"; import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes"; import onMediaLoad from "../../helpers/onMediaLoad"; -import throttleWithRaf from "../../helpers/schedulers/throttleWithRaf"; +import { fastRaf } from "../../helpers/schedulers"; +import throttle from "../../helpers/schedulers/throttle"; import sequentialDom from "../../helpers/sequentialDom"; import toHHMMSS from "../../helpers/string/toHHMMSS"; import { Message, PhotoSize } from "../../layer"; @@ -245,7 +246,9 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH spanTime.innerText = toHHMMSS(globalVideo.duration - globalVideo.currentTime, false); }; - const throttledTimeUpdate = throttleWithRaf(onTimeUpdate); + const throttledTimeUpdate = throttle(() => { + fastRaf(onTimeUpdate); + }, 1000, false); const onPlay = () => { video.classList.add('hide'); @@ -437,14 +440,16 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH if(doc.type === 'video') { const onTimeUpdate = () => { - if(!video.videoWidth) { + if(!video.duration) { return; } spanTime.innerText = toHHMMSS(video.duration - video.currentTime, false); }; - const throttledTimeUpdate = throttleWithRaf(onTimeUpdate); + const throttledTimeUpdate = throttle(() => { + fastRaf(onTimeUpdate); + }, 1e3, false); video.addEventListener('timeupdate', throttledTimeUpdate); diff --git a/src/helpers/canvas/getTextWidth.ts b/src/helpers/canvas/getTextWidth.ts index f318bc34..25db802f 100644 --- a/src/helpers/canvas/getTextWidth.ts +++ b/src/helpers/canvas/getTextWidth.ts @@ -16,7 +16,7 @@ export default function getTextWidth(text: string, font: string) { // const perf = performance.now(); if(!context) { const canvas = document.createElement('canvas'); - context = canvas.getContext('2d'); + context = canvas.getContext('2d', {alpha: false}); context.font = font; } diff --git a/src/helpers/dom/renderImageWithFadeIn.ts b/src/helpers/dom/renderImageWithFadeIn.ts index 464dc091..c64550f3 100644 --- a/src/helpers/dom/renderImageWithFadeIn.ts +++ b/src/helpers/dom/renderImageWithFadeIn.ts @@ -38,12 +38,11 @@ export default function renderImageWithFadeIn( image.addEventListener('animationend', () => { sequentialDom.mutate(() => { image.classList.remove('fade-in'); - - if(thumbImage) { - thumbImage.remove(); - } + thumbImage?.remove(); }); }, {once: true}); + } else { + thumbImage?.remove(); } }); }); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index c0957828..cba975a4 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1516,7 +1516,7 @@ export class AppDialogsManager { }, {capture: true}); // cancel link click - list.addEventListener('click', (e) => { + attachClickEvent(list, (e) => { if(e.button === 0) { cancelEvent(e); } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index f1c59706..7404c2fb 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -89,6 +89,7 @@ import apiManagerProxy from '../mtproto/mtprotoworker'; import appRuntimeManager from './appRuntimeManager'; import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount'; import findUpClassName from '../../helpers/dom/findUpClassName'; +import { CLICK_EVENT_NAME } from '../../helpers/dom/clickEvent'; export const CHAT_ANIMATION_GROUP = 'chat'; @@ -368,6 +369,10 @@ export class AppImManager extends EventListenerBase<{ const isVisible = parentElement.classList.contains(className); if(!isVisible) { cancelEvent(e); + + if(CLICK_EVENT_NAME !== 'click') { + window.addEventListener('click', cancelEvent, {capture: true, once: true}); + } } const duration = 400 / 2; diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index ce562bf8..d5800388 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -11,7 +11,6 @@ import cancelEvent from "../helpers/dom/cancelEvent"; import ListenerSetter, { Listener } from "../helpers/listenerSetter"; import ButtonMenu from "../components/buttonMenu"; import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle"; -import rootScope from "./rootScope"; import ControlsHover from "../helpers/dom/controlsHover"; import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../helpers/dom/fullScreen"; import toHHMMSS from "../helpers/string/toHHMMSS"; @@ -20,6 +19,7 @@ import VolumeSelector from "../components/volumeSelector"; import debounce from "../helpers/schedulers/debounce"; import overlayCounter from "../helpers/overlayCounter"; import onMediaLoad from "../helpers/onMediaLoad"; +import { attachClickEvent } from "../helpers/dom/clickEvent"; export default class VideoPlayer extends ControlsHover { private static PLAYBACK_RATES = [0.5, 1, 1.5, 2]; @@ -127,15 +127,15 @@ export default class VideoPlayer extends ControlsHover { leftControls.insertBefore(volumeSelector.btn, timeElapsed.parentElement); Array.from(toggle).forEach((button) => { - listenerSetter.add(button)('click', () => { + attachClickEvent(button, () => { this.togglePlay(); - }); + }, {listenerSetter: this.listenerSetter}); }); if(this.pipButton) { - listenerSetter.add(this.pipButton)('click', () => { + attachClickEvent(this.pipButton, () => { this.video.requestPictureInPicture(); - }); + }, {listenerSetter: this.listenerSetter}); const onPip = (pip: boolean) => { this.wrapper.style.visibility = pip ? 'hidden': ''; @@ -170,9 +170,9 @@ export default class VideoPlayer extends ControlsHover { } if(!IS_TOUCH_SUPPORTED) { - listenerSetter.add(video)('click', () => { + attachClickEvent(video, () => { this.togglePlay(); - }); + }, {listenerSetter: this.listenerSetter}); listenerSetter.add(document)('keydown', (e: KeyboardEvent) => { if(overlayCounter.overlaysActive > 1 || document.pictureInPictureElement === video) { // forward popup is active, etc @@ -216,9 +216,9 @@ export default class VideoPlayer extends ControlsHover { } }); - listenerSetter.add(fullScreenButton)('click', () => { + attachClickEvent(fullScreenButton, () => { this.toggleFullScreen(); - }); + }, {listenerSetter: this.listenerSetter}); addFullScreenListener(wrapper, this.onFullScreen.bind(this, fullScreenButton), listenerSetter); diff --git a/src/lib/rlottie/queryableWorker.ts b/src/lib/rlottie/queryableWorker.ts index caccbce9..6bc716d9 100644 --- a/src/lib/rlottie/queryableWorker.ts +++ b/src/lib/rlottie/queryableWorker.ts @@ -47,7 +47,7 @@ export default class QueryableWorker extends EventListenerBase<{ queryMethodArguments: args }); } else { - const transfer: (ArrayBuffer | OffscreenCanvas)[] = []; + const transfer: Transferable[] = []; args.forEach((arg) => { if(arg instanceof ArrayBuffer) { transfer.push(arg); @@ -62,7 +62,7 @@ export default class QueryableWorker extends EventListenerBase<{ this.worker.postMessage({ queryMethod: queryMethod, queryMethodArguments: args - }, transfer as Transferable[]); + }, transfer); } } } diff --git a/webpack.common.js b/webpack.common.js index 47500022..70754039 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -185,7 +185,8 @@ module.exports = { // directory: path.join(__dirname, 'public') // }, compress: true, - http2: useLocalNotLocal ? true : (useLocal ? undefined : true), + // http2: useLocalNotLocal ? true : (useLocal ? undefined : true), + http2: true, https: useLocal ? undefined : { key: fs.readFileSync(__dirname + '/certs/server-key.pem', 'utf8'), cert: fs.readFileSync(__dirname + '/certs/server-cert.pem', 'utf8')