diff --git a/.env b/.env index aeb53ba8..967c8711 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ API_ID=1025907 API_HASH=452b0359b988148995f22ff0f4229750 VERSION=1.5.0 -VERSION_FULL=1.5.0 (196) -BUILD=196 +VERSION_FULL=1.5.0 (215) +BUILD=215 diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index b17ee436..eef808b3 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -584,23 +584,29 @@ export class AppMediaPlaybackController extends EventListenerBase<{ const listLoader = this.listLoader; const current = listLoader.getCurrent(); if(!current || !verify(current)) { - const previous = listLoader.getPrevious(); - - let idx = previous.findIndex(verify); let jumpLength: number; - if(idx !== -1) { - jumpLength = -(previous.length - idx); - } else { - idx = listLoader.getNext().findIndex(verify); + + for(const withOtherSide of [false, true]) { + const previous = listLoader.getPrevious(withOtherSide); + + let idx = previous.findIndex(verify); if(idx !== -1) { - jumpLength = idx + 1; + jumpLength = -(previous.length - idx); + } else { + const next = listLoader.getNext(withOtherSide); + idx = next.findIndex(verify); + if(idx !== -1) { + jumpLength = idx + 1; + } } - } - if(idx !== -1) { - if(jumpLength) { - this.go(jumpLength, false); + if(jumpLength !== undefined) { + break; } + } + + if(jumpLength) { + this.go(jumpLength, false); } else { this.setTargets({peerId, mid}); } @@ -645,7 +651,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{ const listLoader = this.listLoader; if(this.lockedSwitchers || (!this.round && listLoader.current && !listLoader.next.length) || - !listLoader.getNext().length || + !listLoader.getNext(true).length || !this.next()) { this.stop(); this.dispatchEvent('stop'); diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 10107a19..647806e8 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -7,6 +7,8 @@ import MEDIA_MIME_TYPES_SUPPORTED from '../environment/mediaMimeTypesSupport'; import cancelEvent from '../helpers/dom/cancelEvent'; import {attachClickEvent, detachClickEvent} from '../helpers/dom/clickEvent'; +import findUpClassName from '../helpers/dom/findUpClassName'; +import findUpTag from '../helpers/dom/findUpTag'; import setInnerHTML from '../helpers/dom/setInnerHTML'; import mediaSizes from '../helpers/mediaSizes'; import SearchListLoader from '../helpers/searchListLoader'; @@ -68,7 +70,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet this.content.main.prepend(stub); */ this.content.caption = document.createElement('div'); - this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption'/* , 'media-viewer-stub' */); + this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption', 'message'/* , 'media-viewer-stub' */); let captionTimeout: number; const setCaptionTimeout = () => { @@ -129,8 +131,10 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet attachClickEvent(this.author.container, this.onAuthorClick); const onCaptionClick = (e: MouseEvent) => { - if(e.target instanceof HTMLAnchorElement) { // close viewer if it's t.me/ redirect - const onclick = (e.target as HTMLElement).getAttribute('onclick'); + const a = findUpTag(e.target, 'A'); + const spoiler = findUpClassName(e.target, 'spoiler'); + if(a instanceof HTMLAnchorElement && (!spoiler || this.content.caption.classList.contains('is-spoiler-visible'))) { // close viewer if it's t.me/ redirect + const onclick = a.getAttribute('onclick'); if(!onclick || onclick.includes('showMaskedAlert')) { return; } @@ -138,15 +142,15 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet cancelEvent(e); this.close().then(() => { - detachClickEvent(this.content.caption, onCaptionClick, {capture: true}); - (e.target as HTMLAnchorElement).click(); + this.content.caption.removeEventListener('click', onCaptionClick, {capture: true}); + a.click(); }); return false; } }; - attachClickEvent(this.content.caption, onCaptionClick, {capture: true}); + this.content.caption.addEventListener('click', onCaptionClick, {capture: true}); } /* public close(e?: MouseEvent) { diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index d561c2bb..117d29e6 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -69,6 +69,8 @@ import {attachContextMenuListener} from '../helpers/dom/attachContextMenuListene 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/appSelectPeers.ts b/src/components/appSelectPeers.ts index 01efdfaf..d0e86268 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -21,7 +21,7 @@ import debounce from '../helpers/schedulers/debounce'; import windowSize from '../helpers/windowSize'; import type {IsPeerType} from '../lib/appManagers/appPeersManager'; import {generateDelimiter, SettingSection} from './sidebarLeft'; -import {attachClickEvent} from '../helpers/dom/clickEvent'; +import {attachClickEvent, simulateClickEvent} from '../helpers/dom/clickEvent'; import filterUnique from '../helpers/array/filterUnique'; import indexOfAndSplice from '../helpers/array/indexOfAndSplice'; import safeAssign from '../helpers/object/safeAssign'; @@ -187,7 +187,7 @@ export default class AppSelectPeers { if(!li) { this.remove(peerId.toPeerId()); } else { - li.click(); + simulateClickEvent(li); } }); diff --git a/src/components/call/index.ts b/src/components/call/index.ts index bf08549c..eecf6f80 100644 --- a/src/components/call/index.ts +++ b/src/components/call/index.ts @@ -441,7 +441,7 @@ export default class PopupCall extends PopupElement { if(!this.emojisSubtitle.textContent && connectionState < CALL_STATE.EXCHANGING_KEYS) { Promise.resolve(instance.getEmojisFingerprint()).then((emojis) => { - this.emojisSubtitle.append(wrapEmojiText(emojis.join(''))); + replaceContent(this.emojisSubtitle, wrapEmojiText(emojis.join(''))); }); } diff --git a/src/components/call/videoCanvasBlur.ts b/src/components/call/videoCanvasBlur.ts index ffcefe6d..4a768f13 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 1cbcf858..73d3ecd0 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1509,41 +1509,6 @@ export default class ChatBubbles { return; } - const spoiler: HTMLElement = findUpClassName(target, 'spoiler'); - if(spoiler) { - const messageDiv = findUpClassName(spoiler, 'message'); - - const className = 'is-spoiler-visible'; - const isVisible = messageDiv.classList.contains(className); - if(!isVisible) { - cancelEvent(e); - } - - const duration = 400 / 2; - const showDuration = 5000; - const useRafs = !isVisible ? 2 : 0; - if(useRafs) { - messageDiv.classList.add('will-change'); - } - - const spoilerTimeout = messageDiv.dataset.spoilerTimeout; - if(spoilerTimeout !== null) { - clearTimeout(+spoilerTimeout); - delete messageDiv.dataset.spoilerTimeout; - } - - SetTransition(messageDiv, className, true, duration, () => { - messageDiv.dataset.spoilerTimeout = '' + window.setTimeout(() => { - SetTransition(messageDiv, className, false, duration, () => { - messageDiv.classList.remove('will-change'); - delete messageDiv.dataset.spoilerTimeout; - }); - }, showDuration); - }, useRafs); - - return; - } - const reactionElement = findUpTag(target, 'REACTION-ELEMENT') as ReactionElement; if(reactionElement) { cancelEvent(e); @@ -1607,11 +1572,17 @@ export default class ChatBubbles { if(typeof(peerIdStr) === 'string' || savedFrom) { if(savedFrom) { const [peerId, mid] = savedFrom.split('_'); - - this.chat.appImManager.setInnerPeer({ - peerId: peerId.toPeerId(), - lastMsgId: +mid - }); + if(target.classList.contains('is-receipt-link')) { + const message = await this.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), +mid); + if(message) { + new PopupPayment(message as Message.message, this.peerId, +bubble.dataset.mid); + } + } else { + this.chat.appImManager.setInnerPeer({ + peerId: peerId.toPeerId(), + lastMsgId: +mid + }); + } } else { const peerId = peerIdStr.toPeerId(); if(peerId !== NULL_PEER_ID) { @@ -2519,8 +2490,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() { @@ -2566,6 +2536,7 @@ export default class ChatBubbles { // clear messages if(bubblesToo) { this.scrollable.container.textContent = ''; + this.chatInner.textContent = ''; this.cleanupPlaceholders(); } @@ -2854,7 +2825,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 da20fc18..396148b7 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -439,6 +439,11 @@ export default class Chat extends EventListenerBase<{ this.inited = true; } + // const appMediaViewer = (window as any).appMediaViewer as AppMediaViewerBase; + // if(appMediaViewer) { + // appMediaViewer.close(); + // } + const samePeer = this.peerId === peerId; if(!samePeer) { this.appImManager.dispatchEvent('peer_changing', this); @@ -453,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 1abc9b44..4511adaf 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/inlineHelper.ts b/src/components/chat/inlineHelper.ts index 146de700..10a6c1cf 100644 --- a/src/components/chat/inlineHelper.ts +++ b/src/components/chat/inlineHelper.ts @@ -203,8 +203,8 @@ export default class InlineHelper extends AutocompleteHelper { } else if(media.type === 'sticker') { container.classList.add('super-sticker'); this.superStickerRenderer.renderSticker(media, container, loadPromises); - if(media.sticker === 2) { - this.superStickerRenderer.observeAnimatedDiv(container); + if(media.animated) { + this.superStickerRenderer.observeAnimated(container); } } } else if(media) { diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 2fdb55f7..cce98af5 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -1633,7 +1633,7 @@ export default class ChatInput { executed.push(document.execCommand('styleWithCSS', false, 'true')); - if(type === 'monospace' || type === 'spoiler') { + const checkType = (type: MarkdownType) => { let haveThisType = false; // executed.push(document.execCommand('styleWithCSS', false, 'true')); @@ -1648,14 +1648,30 @@ export default class ChatInput { } } + return haveThisType; + }; + + // * monospace can't be combined with different types + if(type === 'monospace' || type === 'spoiler') { + // executed.push(document.execCommand('styleWithCSS', false, 'true')); + + const haveThisType = checkType(type); // executed.push(document.execCommand('removeFormat', false, null)); if(haveThisType) { - executed.push(this.resetCurrentFormatting()); + executed.push(this.resetCurrentFontFormatting()); } else { + if(type === 'monospace' || checkType('monospace')) { + executed.push(this.resetCurrentFormatting()); + } + executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); } } else { + if(checkType('monospace')) { + executed.push(this.resetCurrentFormatting()); + } + executed.push(typeof(command) === 'function' ? command() : document.execCommand(command, false, null)); } @@ -1671,6 +1687,10 @@ export default class ChatInput { } private resetCurrentFormatting() { + return document.execCommand('removeFormat', false, null); + } + + private resetCurrentFontFormatting() { return document.execCommand('fontName', false, 'Roboto'); } @@ -1823,7 +1843,7 @@ export default class ChatInput { // document.execCommand('styleWithCSS', false, 'true'); setTimeout(() => { if(document.activeElement === this.messageInput) { - this.resetCurrentFormatting(); + this.resetCurrentFontFormatting(); } }, 0); // document.execCommand('styleWithCSS', false, 'false'); diff --git a/src/components/chat/replyKeyboard.ts b/src/components/chat/replyKeyboard.ts index 52e39491..17e3d11b 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 875663ef..f8585e8f 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 0d4c29b8..cddcffe6 100644 --- a/src/components/checkboxField.ts +++ b/src/components/checkboxField.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type ListenerSetter from '../helpers/listenerSetter'; import ripple from './ripple'; import {LangPackKey, _i18n} from '../lib/langPack'; import getDeepProperty from '../helpers/object/getDeepProperty'; @@ -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/index.ts b/src/components/emoticonsDropdown/index.ts index aecc81af..8ef80114 100644 --- a/src/components/emoticonsDropdown/index.ts +++ b/src/components/emoticonsDropdown/index.ts @@ -29,6 +29,7 @@ import pause from '../../helpers/schedulers/pause'; import {IS_APPLE_MOBILE} from '../../environment/userAgent'; import {AppManagers} from '../../lib/appManagers/managers'; import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector'; +import {simulateClickEvent} from '../../helpers/dom/clickEvent'; export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; @@ -176,7 +177,7 @@ export class EmoticonsDropdown extends DropdownHover { (this.tabsEl.children[1] as HTMLElement).classList.add('hide'); } - (this.tabsEl.children[INIT_TAB_ID + 1] as HTMLLIElement).click(); // set emoji tab + simulateClickEvent(this.tabsEl.children[INIT_TAB_ID + 1] as HTMLElement); // set emoji tab if(this.tabs[INIT_TAB_ID].init) { this.tabs[INIT_TAB_ID].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка } @@ -187,6 +188,10 @@ export class EmoticonsDropdown extends DropdownHover { return super.init(); } + public getElement() { + return this.element; + } + private onSelectTabClick = (id: number) => { if(this.tabId === id) { return; @@ -248,11 +253,11 @@ export class EmoticonsDropdown extends DropdownHover { setActive(which); if(menuScroll) { - if(which < menu.childElementCount - 4) { - menuScroll.container.scrollLeft = (which - 3) * 47; - } else { - menuScroll.container.scrollLeft = which * 47; - } + menuScroll.scrollIntoViewNew({ + element: menu.children[which] as HTMLElement, + position: 'center', + axis: 'x' + }); } }); diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index edb36ee3..27a7dd1f 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 93bff0e1..b07f5df9 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/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts index 34964089..42c5fcb8 100644 --- a/src/components/emoticonsDropdown/tabs/stickers.ts +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -22,62 +22,75 @@ import PopupStickers from '../../popups/stickers'; import Scrollable, {ScrollableX} from '../../scrollable'; import StickyIntersector from '../../stickyIntersector'; import {wrapSticker, wrapStickerSetThumb} from '../../wrappers'; +import findAndSplice from '../../../helpers/array/findAndSplice'; +import {attachClickEvent} from '../../../helpers/dom/clickEvent'; +import positionElementByIndex from '../../../helpers/dom/positionElementByIndex'; +import noop from '../../../helpers/noop'; +import ButtonIcon from '../../buttonIcon'; +import confirmationPopup from '../../confirmationPopup'; +import VisibilityIntersector, {OnVisibilityChange} from '../../visibilityIntersector'; export class SuperStickerRenderer { public lazyLoadQueue: LazyLoadQueueRepeat; - private animatedDivs: Set = new Set(); + private animated: Set = new Set(); constructor( private regularLazyLoadQueue: LazyLoadQueue, private group: string, - private managers: AppManagers + private managers: AppManagers, + private options?: IntersectionObserverInit ) { - this.lazyLoadQueue = new LazyLoadQueueRepeat(undefined, (target, visible) => { + this.lazyLoadQueue = new LazyLoadQueueRepeat(undefined, ({target, visible}) => { if(!visible) { - this.processInvisibleDiv(target as HTMLDivElement); + this.processInvisible(target); } - }); + }, options); } public clear() { this.lazyLoadQueue.clear(); } - public renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise[]) { - if(!div) { - div = document.createElement('div'); - div.classList.add('grid-item', 'super-sticker'); + public renderSticker(doc: MyDocument, element?: HTMLElement, loadPromises?: Promise[]) { + if(!element) { + element = document.createElement('div'); + element.classList.add('grid-item', 'super-sticker'); + element.dataset.docId = '' + doc.id; if(doc.animated) { - this.observeAnimatedDiv(div); + this.observeAnimated(element); } } // * This will wrap only a thumb - wrapSticker({ + /* !doc.animated && */wrapSticker({ doc, - div, + div: element, lazyLoadQueue: this.regularLazyLoadQueue, group: this.group, onlyThumb: doc.animated, loadPromises }); - return div; + return element; } - public observeAnimatedDiv(div: HTMLDivElement) { - this.animatedDivs.add(div); - + public observeAnimated(element: HTMLElement) { + this.animated.add(element); this.lazyLoadQueue.observe({ - div, - load: this.processVisibleDiv + div: element, + load: this.processVisible }); } - private checkAnimationContainer = (div: HTMLElement, visible: boolean) => { + public unobserveAnimated(element: HTMLElement) { + this.animated.delete(element); + this.lazyLoadQueue.unobserve(element); + } + + private checkAnimationContainer = (element: HTMLElement, visible: boolean) => { // console.error('checkAnimationContainer', div, visible); - const players = animationIntersector.getAnimations(div); + const players = animationIntersector.getAnimations(element); players.forEach((player) => { if(!visible) { animationIntersector.checkAnimation(player, true, true); @@ -87,8 +100,8 @@ export class SuperStickerRenderer { }); }; - private processVisibleDiv = async(div: HTMLElement) => { - const docId = div.dataset.docId; + private processVisible = async(element: HTMLElement) => { + const docId = element.dataset.docId; const doc = await this.managers.appDocsManager.getDoc(docId); const size = mediaSizes.active.esgSticker.width; @@ -97,7 +110,7 @@ export class SuperStickerRenderer { const promise = wrapSticker({ doc, - div: div as HTMLDivElement, + div: element, width: size, height: size, lazyLoadQueue: null, @@ -109,7 +122,7 @@ export class SuperStickerRenderer { promise.then(() => { // clearTimeout(timeout); - this.checkAnimationContainer(div, this.lazyLoadQueue.intersector.isVisible(div)); + this.checkAnimationContainer(element, this.lazyLoadQueue.intersector.isVisible(element)); }); /* let timeout = window.setTimeout(() => { @@ -119,123 +132,152 @@ export class SuperStickerRenderer { return promise; }; - public processInvisibleDiv = async(div: HTMLElement) => { - const docId = div.dataset.docId; + public processInvisible = async(element: HTMLElement) => { + const docId = element.dataset.docId; const doc = await this.managers.appDocsManager.getDoc(docId); // console.log('STICKER INvisible:', /* div, */docId); - this.checkAnimationContainer(div, false); + this.checkAnimationContainer(element, false); - div.innerHTML = ''; - this.renderSticker(doc, div as HTMLDivElement); + element.textContent = ''; + this.renderSticker(doc, element as HTMLDivElement); }; } +type StickersTabCategory = { + elements: { + container: HTMLElement, + title: HTMLElement, + items: HTMLElement, + menuTab: HTMLElement, + menuTabPadding: HTMLElement + }, + set: StickerSet.stickerSet, + items: { + document: MyDocument, + element: HTMLElement + }[] +}; + +const RECENT_STICKERS_COUNT = 20; + export default class StickersTab implements EmoticonsTab { private content: HTMLElement; - private stickersDiv: HTMLElement; - - private stickerSets: {[id: string]: { - stickers: HTMLElement, - tab: HTMLElement - }} = {}; - private recentDiv: HTMLElement; - private recentStickers: MyDocument[] = []; + private categories: {[id: string]: StickersTabCategory}; + private categoriesMap: Map; + private categoriesIntersector: VisibilityIntersector; private scroll: Scrollable; - private menu: HTMLElement; - private mounted = false; - - private queueCategoryPush: {element: HTMLElement, prepend: boolean}[] = []; - private stickyIntersector: StickyIntersector; - private superStickerRenderer: SuperStickerRenderer; constructor(private managers: AppManagers) { - + this.categories = {}; + this.categoriesMap = new Map(); } - categoryPush(categoryDiv: HTMLElement, categoryTitle: DocumentFragment | string = '', promise: Promise, prepend?: boolean) { - // if((docs.length % 5) !== 0) categoryDiv.classList.add('not-full'); + private createCategory(stickerSet: StickerSet.stickerSet, _title: HTMLElement | DocumentFragment) { + const container = document.createElement('div'); + container.classList.add('emoji-category', 'hide'); - const itemsDiv = document.createElement('div'); - itemsDiv.classList.add('category-items', 'super-stickers'); + const items = document.createElement('div'); + items.classList.add('category-items', 'super-stickers'); - const titleDiv = document.createElement('div'); - titleDiv.classList.add('category-title'); + const title = document.createElement('div'); + title.classList.add('category-title'); + title.append(_title); - if(categoryTitle) { - if(typeof(categoryTitle) === 'string') titleDiv.innerHTML = categoryTitle; - else titleDiv.append(categoryTitle); - } + const menuTab = ButtonIcon(undefined, {noRipple: true}); + menuTab.classList.add('menu-horizontal-div-item'); + + const menuTabPadding = document.createElement('div'); + menuTabPadding.classList.add('menu-horizontal-div-item-padding'); + + menuTab.append(menuTabPadding); + + const category: StickersTabCategory = { + elements: { + container, + title, + items, + menuTab, + menuTabPadding + }, + set: stickerSet, + items: [] + }; + + container.append(title, items); - categoryDiv.append(titleDiv, itemsDiv); + this.categories[stickerSet.id] = category; + this.categoriesMap.set(container, category); - this.stickyIntersector.observeStickyHeaderChanges(categoryDiv); + this.categoriesIntersector.observe(container); + this.stickyIntersector.observeStickyHeaderChanges(container); - this.queueCategoryPush.push({element: categoryDiv, prepend}); + return category; + } + + private categoryAppendStickers( + category: StickersTabCategory, + promise: Promise + ) { + const {container} = category.elements; promise.then((documents) => { - documents.forEach((doc) => { - // if(doc._ === 'documentEmpty') return; - itemsDiv.append(this.superStickerRenderer.renderSticker(doc)); + const isVisible = this.isCategoryVisible(category); + + documents.forEach((document) => { + const element = this.superStickerRenderer.renderSticker(document); + category.items.push({document, element}); + + if(isVisible) { + category.elements.items.append(element); + } }); - if(this.queueCategoryPush.length) { - this.queueCategoryPush.forEach(({element, prepend}) => { - if(prepend) { - if(this.recentDiv.parentElement) { - this.stickersDiv.prepend(element); - this.stickersDiv.prepend(this.recentDiv); - } else { - this.stickersDiv.prepend(element); - } - } else this.stickersDiv.append(element); - }); - - this.queueCategoryPush.length = 0; - } + this.setCategoryItemsHeight(category); + container.classList.remove('hide'); }); + } - return {titleDiv}; + private isCategoryVisible(category: StickersTabCategory) { + return this.categoriesIntersector.getVisible().includes(category.elements.container); } - async renderStickerSet(set: StickerSet.stickerSet, prepend = false) { - const categoryDiv = document.createElement('div'); - categoryDiv.classList.add('sticker-category'); - categoryDiv.dataset.id = '' + set.id; - categoryDiv.dataset.access_hash = '' + set.access_hash; + private setCategoryItemsHeight(category: StickersTabCategory) { + const containerWidth = this.content.getBoundingClientRect().width - 10; + const stickerSize = mediaSizes.active.esgSticker.width; - const button = document.createElement('button'); - button.classList.add('btn-icon', 'menu-horizontal-div-item'); + const itemsPerRow = Math.floor(containerWidth / stickerSize); + const rows = Math.ceil(category.items.length / itemsPerRow); + const height = rows * stickerSize; - this.stickerSets[set.id] = { - stickers: categoryDiv, - tab: button - }; + category.elements.items.style.minHeight = height + 'px'; + } - if(prepend) { - this.menu.insertBefore(button, this.menu.firstElementChild.nextSibling); - } else { - this.menu.append(button); - } + private async renderStickerSet(set: StickerSet.stickerSet, prepend = false) { + const category = this.createCategory(set, wrapEmojiText(set.title)); + const {menuTab, menuTabPadding, container} = category.elements; - // stickersScroll.append(categoryDiv); + positionElementByIndex(menuTab, this.menu, prepend ? 1 : 0xFFFF); const promise = this.managers.appStickersManager.getStickerSet(set); - this.categoryPush(categoryDiv, wrapEmojiText(set.title), promise.then((stickerSet) => stickerSet.documents as MyDocument[]), prepend); - const stickerSet = await promise; + this.categoryAppendStickers( + category, + promise.then((stickerSet) => stickerSet.documents as MyDocument[]) + ); + // const stickerSet = await promise; - // console.log('got stickerSet', stickerSet, li); + positionElementByIndex(container, this.scroll.container, prepend ? 1 : 0xFFFF, -1); wrapStickerSetThumb({ set, - container: button, + container: menuTabPadding, group: EMOTICONSSTICKERGROUP, lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, width: 32, @@ -244,21 +286,17 @@ export default class StickersTab implements EmoticonsTab { }); } - init() { + public init() { this.content = document.getElementById('content-stickers'); - // let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement; - - this.recentDiv = document.createElement('div'); - this.recentDiv.classList.add('sticker-category', 'stickers-recent'); - const menuWrapper = this.content.previousElementSibling as HTMLDivElement; this.menu = menuWrapper.firstElementChild as HTMLUListElement; const menuScroll = new ScrollableX(menuWrapper); - this.stickersDiv = document.createElement('div'); - this.stickersDiv.classList.add('stickers-categories'); - this.content.append(this.stickersDiv); + this.scroll = new Scrollable(this.content, 'STICKERS'); + this.scroll.onAdditionalScroll = () => { + setTyping(); + }; /* stickersDiv.addEventListener('mouseover', (e) => { let target = e.target as HTMLElement; @@ -277,30 +315,35 @@ export default class StickersTab implements EmoticonsTab { } }); */ - rootScope.addEventListener('stickers_installed', (e) => { - const set: StickerSet.stickerSet = e; - - if(!this.stickerSets[set.id] && this.mounted) { - this.renderStickerSet(set, true); + const onCategoryVisibility: OnVisibilityChange = ({target, visible, entry}) => { + const category = this.categoriesMap.get(target); + // console.log('roll the windows up', category, target, visible, entry); + if(!visible) { + category.elements.items.textContent = ''; + } else { + category.elements.items.append(...category.items.map(({element}) => element)); } - }); + }; - rootScope.addEventListener('stickers_deleted', (e) => { - const set: StickerSet.stickerSet = e; + const intersectionOptions: IntersectionObserverInit = {root: emoticonsDropdown.getElement()}; + this.categoriesIntersector = new VisibilityIntersector(onCategoryVisibility, intersectionOptions); - if(this.stickerSets[set.id] && this.mounted) { - const elements = this.stickerSets[set.id]; - elements.stickers.remove(); - elements.tab.remove(); - delete this.stickerSets[set.id]; - } - }); + const clearCategoryItems = (category: StickersTabCategory) => { + category.elements.items.textContent = ''; + category.items.forEach(({element}) => this.superStickerRenderer.unobserveAnimated(element)); + category.items.length = 0; + }; - this.stickersDiv.addEventListener('click', (e) => { + this.scroll.container.addEventListener('click', (e) => { const target = e.target as HTMLElement; if(findUpClassName(target, 'category-title')) { - const el = findUpAttribute(target, 'data-id'); - new PopupStickers({id: el.dataset.id, access_hash: el.dataset.access_hash}).show(); + const container = findUpClassName(target, 'emoji-category'); + const category = this.categoriesMap.get(container); + if(category.set.id === 'recent') { + return; + } + + new PopupStickers({id: category.set.id, access_hash: category.set.access_hash}).show(); return; } @@ -311,12 +354,6 @@ export default class StickersTab implements EmoticonsTab { rootScope.dispatchEvent('choosing_sticker', !cancel); }; - this.scroll = new Scrollable(this.content, 'STICKERS'); - this.scroll.setVirtualContainer(this.stickersDiv); - this.scroll.onAdditionalScroll = () => { - setTyping(); - }; - emoticonsDropdown.addEventListener('closed', () => { setTyping(true); }); @@ -325,24 +362,43 @@ export default class StickersTab implements EmoticonsTab { setTyping(); }); - this.stickyIntersector = EmoticonsDropdown.menuOnClick(this.menu, this.scroll, menuScroll).stickyIntersector; + const {stickyIntersector, setActive} = EmoticonsDropdown.menuOnClick(this.menu, this.scroll, menuScroll); + this.stickyIntersector = stickyIntersector; const preloader = putPreloader(this.content, true); - Promise.all([ - this.managers.appStickersManager.getRecentStickers().then((stickers) => { - this.recentStickers = stickers.stickers.slice(0, 20) as MyDocument[]; + const recentCategory = this.createCategory({id: 'recent'} as any, i18n('Stickers.Recent')); + recentCategory.elements.title.classList.add('disable-hover'); + recentCategory.elements.menuTab.classList.add('tgico-recent', 'active'); + recentCategory.elements.menuTabPadding.remove(); + this.toggleRecentCategory(recentCategory, false); + + const clearButton = ButtonIcon('close', {noRipple: true}); + recentCategory.elements.title.append(clearButton); + attachClickEvent(clearButton, () => { + confirmationPopup({ + titleLangKey: 'ClearRecentStickersAlertTitle', + descriptionLangKey: 'ClearRecentStickersAlertMessage', + button: { + langKey: 'Clear' + } + }).then(() => { + this.managers.appStickersManager.clearRecentStickers(); + }, noop); + }); - // stickersScroll.prepend(categoryDiv); + const onRecentStickers = (stickers: MyDocument[]) => { + const sliced = stickers.slice(0, RECENT_STICKERS_COUNT) as MyDocument[]; - this.stickerSets['recent'] = { - stickers: this.recentDiv, - tab: this.menu.firstElementChild as HTMLElement - }; + clearCategoryItems(recentCategory); + this.toggleRecentCategory(recentCategory, !!sliced.length); + this.categoryAppendStickers(recentCategory, Promise.resolve(sliced)); + }; + Promise.all([ + this.managers.appStickersManager.getRecentStickers().then((stickers) => { preloader.remove(); - const {titleDiv} = this.categoryPush(this.recentDiv, '', Promise.resolve(this.recentStickers), true); - titleDiv.append(i18n('Stickers.Recent')); + onRecentStickers(stickers.stickers as MyDocument[]); }), this.managers.appStickersManager.getAllStickers().then((res) => { @@ -355,41 +411,114 @@ export default class StickersTab implements EmoticonsTab { ]).finally(() => { this.mounted = true; setTyping(); + setActive(0); }); - this.superStickerRenderer = new SuperStickerRenderer(EmoticonsDropdown.lazyLoadQueue, EMOTICONSSTICKERGROUP, this.managers); + this.superStickerRenderer = new SuperStickerRenderer(EmoticonsDropdown.lazyLoadQueue, EMOTICONSSTICKERGROUP, this.managers, intersectionOptions); + + const rendererLazyLoadQueue = this.superStickerRenderer.lazyLoadQueue; + emoticonsDropdown.addLazyLoadQueueRepeat(rendererLazyLoadQueue, this.superStickerRenderer.processInvisible); + + // emoticonsDropdown.addEventListener('close', () => { + // this.categoriesIntersector.lock(); + // }); - emoticonsDropdown.addLazyLoadQueueRepeat(this.superStickerRenderer.lazyLoadQueue, this.superStickerRenderer.processInvisibleDiv); + // emoticonsDropdown.addEventListener('closed', () => { + // for(const [container] of this.categoriesMap) { + // onCategoryVisibility(container, false); + // } + // }); - /* setInterval(() => { - // @ts-ignore - const players = Object.values(lottieLoader.players).filter((p) => p.width === 80); + // emoticonsDropdown.addEventListener('opened', () => { + // this.categoriesIntersector.unlockAndRefresh(); + // }); - console.log('STICKERS RENDERED IN PANEL:', players.length, players.filter((p) => !p.paused).length, this.superStickerRenderer.lazyLoadQueue.intersector.getVisible().length); - }, .25e3); */ + // setInterval(() => { + // // @ts-ignore + // const players = Object.values(lottieLoader.players).filter((p) => p.width >= 80); + // console.log( + // 'STICKERS RENDERED IN PANEL:', + // players.length, + // players.filter((p) => !p.paused).length, + // rendererLazyLoadQueue.intersector.getVisible().length + // ); + // }, .25e3); + + rootScope.addEventListener('stickers_installed', (set) => { + if(!this.categories[set.id] && this.mounted) { + this.renderStickerSet(set, true); + } + }); + + rootScope.addEventListener('stickers_deleted', ({id}) => { + const category = this.categories[id]; + if(category && this.mounted) { + category.elements.container.remove(); + category.elements.menuTab.remove(); + this.categoriesIntersector.unobserve(category.elements.container); + clearCategoryItems(category); + delete this.categories[id]; + this.categoriesMap.delete(category.elements.container); + } + }); + + rootScope.addEventListener('stickers_recent', (stickers) => { + if(this.mounted) { + onRecentStickers(stickers); + } + }); + + const resizeCategories = () => { + for(const [container, category] of this.categoriesMap) { + this.setCategoryItemsHeight(category); + } + }; + + mediaSizes.addEventListener('resize', resizeCategories); + + emoticonsDropdown.addEventListener('opened', resizeCategories); this.init = null; } - pushRecentSticker(doc: MyDocument) { - this.managers.appStickersManager.pushRecentSticker(doc); + private toggleRecentCategory(category: StickersTabCategory, visible: boolean) { + if(!visible) { + category.elements.menuTab.remove(); + category.elements.container.remove(); + } else { + positionElementByIndex(category.elements.menuTab, this.menu, 0); + positionElementByIndex(category.elements.container, this.scroll.container, 0); + } + + // category.elements.container.classList.toggle('hide', !visible); + } + + public pushRecentSticker(doc: MyDocument) { + this.managers.appStickersManager.pushRecentSticker(doc.id); - if(!this.recentDiv?.parentElement) { + const category = this.categories['recent']; + if(!category) { return; } - let div = this.recentDiv.querySelector(`[data-doc-id="${doc.id}"]`); - if(!div) { - div = this.superStickerRenderer.renderSticker(doc); + const items = category.elements.items; + let item = findAndSplice(category.items, (item) => item.document.id === doc.id); + if(!item) { + item = { + element: this.superStickerRenderer.renderSticker(doc), + document: doc + }; } - const items = this.recentDiv.querySelector('.category-items'); - items.prepend(div); - - if(items.childElementCount > 20) { - (Array.from(items.children) as HTMLElement[]).slice(20).forEach((el) => el.remove()); + category.items.unshift(item); + if(items.childElementCount) items.prepend(item.element); + if(items.childElementCount > RECENT_STICKERS_COUNT) { + (Array.from(items.children) as HTMLElement[]).slice(RECENT_STICKERS_COUNT).forEach((el) => el.remove()); } + + this.setCategoryItemsHeight(category); + this.toggleRecentCategory(category, true); } onClose() { diff --git a/src/components/gifsMasonry.ts b/src/components/gifsMasonry.ts index 8d4d8008..69d287e6 100644 --- a/src/components/gifsMasonry.ts +++ b/src/components/gifsMasonry.ts @@ -34,7 +34,7 @@ export default class GifsMasonry { ) { this.managers = rootScope.managers; - this.lazyLoadQueue = new LazyLoadQueueRepeat2(undefined, (target, visible) => { + this.lazyLoadQueue = new LazyLoadQueueRepeat2(undefined, ({target, visible}) => { if(visible) { this.processVisibleDiv(target); } else { diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index 9fb68c57..e621266e 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; @@ -110,7 +113,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); @@ -130,7 +133,7 @@ export function horizontalMenu( } selectTarget(target, id); - }); + }, {listenerSetter}); return proxy; } diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index 9c8e696a..94c3a326 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import VisibilityIntersector from './visibilityIntersector'; +import VisibilityIntersector, {OnVisibilityChangeItem} from './visibilityIntersector'; import findAndSpliceAll from '../helpers/array/findAndSpliceAll'; import findAndSplice from '../helpers/array/findAndSplice'; import LazyLoadQueueIntersector, {LazyLoadElement} from './lazyLoadQueueIntersector'; @@ -16,7 +16,7 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector { this.intersector = new VisibilityIntersector(this.onVisibilityChange); } - private onVisibilityChange = (target: HTMLElement, visible: boolean) => { + private onVisibilityChange = ({target, visible}: OnVisibilityChangeItem) => { if(visible) { /* if(DEBUG) { this.log('isIntersecting', target); diff --git a/src/components/lazyLoadQueueRepeat.ts b/src/components/lazyLoadQueueRepeat.ts index 6df47d4a..b3a435f0 100644 --- a/src/components/lazyLoadQueueRepeat.ts +++ b/src/components/lazyLoadQueueRepeat.ts @@ -11,10 +11,11 @@ import VisibilityIntersector, {OnVisibilityChange} from './visibilityIntersector export default class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { private _queue: Map = new Map(); - constructor(parallelLimit?: number, protected onVisibilityChange?: OnVisibilityChange) { + constructor(parallelLimit?: number, protected onVisibilityChange?: OnVisibilityChange, options?: IntersectionObserverInit) { super(parallelLimit); - this.intersector = new VisibilityIntersector((target, visible) => { + this.intersector = new VisibilityIntersector((item) => { + const {target, visible} = item; const spliced = findAndSpliceAll(this.queue, (i) => i.div === target); if(visible) { const items = spliced.length ? spliced : [this._queue.get(target)]; @@ -23,9 +24,9 @@ export default class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { }); } - this.onVisibilityChange && this.onVisibilityChange(target, visible); + this.onVisibilityChange && this.onVisibilityChange(item); this.setProcessQueueTimeout(); - }); + }, options); } public clear() { diff --git a/src/components/lazyLoadQueueRepeat2.ts b/src/components/lazyLoadQueueRepeat2.ts index 2a868270..ab09355c 100644 --- a/src/components/lazyLoadQueueRepeat2.ts +++ b/src/components/lazyLoadQueueRepeat2.ts @@ -12,7 +12,8 @@ export default class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector { constructor(parallelLimit?: number, protected onVisibilityChange?: OnVisibilityChange) { super(parallelLimit); - this.intersector = new VisibilityIntersector((target, visible) => { + this.intersector = new VisibilityIntersector((item) => { + const {target, visible} = item; const spliced = findAndSpliceAll(this.queue, (i) => i.div === target); if(visible && spliced.length) { spliced.forEach((item) => { @@ -20,7 +21,7 @@ export default class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector { }); } - this.onVisibilityChange && this.onVisibilityChange(target, visible); + this.onVisibilityChange && this.onVisibilityChange(item); this.setProcessQueueTimeout(); }); } diff --git a/src/components/peerProfile.ts b/src/components/peerProfile.ts index b0359dc8..c7ae55ca 100644 --- a/src/components/peerProfile.ts +++ b/src/components/peerProfile.ts @@ -104,7 +104,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'); @@ -117,7 +118,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({ @@ -128,7 +130,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({ @@ -141,7 +144,8 @@ export default class PeerProfile { // copyTextToClipboard(chatFull.exported_invite.link); toast(I18n.format('LinkCopied', true)); // }); - } + }, + listenerSetter: this.listenerSetter }); this.location = new Row({ @@ -163,7 +167,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 5267a2b7..26fc6ace 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/poll.ts b/src/components/poll.ts index 72d5f5d2..5af5e861 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -16,7 +16,7 @@ import {fastRaf} from '../helpers/schedulers'; import SetTransition from './singleTransition'; import findUpClassName from '../helpers/dom/findUpClassName'; import cancelEvent from '../helpers/dom/cancelEvent'; -import {attachClickEvent, detachClickEvent} from '../helpers/dom/clickEvent'; +import {attachClickEvent, detachClickEvent, simulateClickEvent} from '../helpers/dom/clickEvent'; import replaceContent from '../helpers/dom/replaceContent'; import windowSize from '../helpers/windowSize'; import {Message, MessageMedia, Poll, PollResults} from '../layer'; @@ -470,7 +470,7 @@ export default class PollElement extends HTMLElement { if(this.sentVote) { const correctResult = results.results.find((r) => r.pFlags.correct); if(correctResult && !correctResult.pFlags.chosen) { - toggleHint.click(); + simulateClickEvent(toggleHint); } } } diff --git a/src/components/popups/payment.ts b/src/components/popups/payment.ts index f3a9ee5c..fc1484f5 100644 --- a/src/components/popups/payment.ts +++ b/src/components/popups/payment.ts @@ -52,6 +52,7 @@ const icons = [ 'mastercard', 'visa', 'unionpay', + 'mir', 'logo' ]; @@ -134,17 +135,6 @@ export default class PopupPayment extends PopupElement { } this.hide(); - showSuccessToast(); - }; - - const showSuccessToast = () => { - toastNew({ - langPackKey: 'PaymentInfoHint', - langPackArguments: [ - paymentsWrapCurrencyAmount(getTotalTotal(), currency), - wrapEmojiText(title) - ] - }); }; let {paymentForm, message} = this; @@ -182,7 +172,7 @@ export default class PopupPayment extends PopupElement { let photoEl: HTMLElement; if(photo) { photoEl = document.createElement('div'); - photoEl.classList.add(detailsClassName + '-photo', 'media-container-cover'); + photoEl.classList.add(detailsClassName + '-photo', 'media-container-contain'); wrapPhoto({ photo: photo, container: photoEl, @@ -238,7 +228,11 @@ export default class PopupPayment extends PopupElement { wrapPeerTitle({peerId: paymentForm.provider_id.toPeerId()}) ]); - // console.log(paymentForm, lastRequestedInfo); + console.log(paymentForm, lastRequestedInfo); + + await peerTitle.update({peerId: paymentForm.bot_id.toPeerId()}); + preloaderContainer.remove(); + this.element.classList.remove('is-loading'); const wrapAmount = (amount: string | number, skipSymbol?: boolean) => { return paymentsWrapCurrencyAmount(amount, currency, skipSymbol); @@ -274,8 +268,8 @@ export default class PopupPayment extends PopupElement { const _label = makeLabel(); _label.left.textContent = label; - const wrappedAmount = wrapAmount(Math.abs(+amount)); - _label.right.textContent = (amount < 0 ? '-' : '') + wrappedAmount; + const wrappedAmount = wrapAmount(amount); + _label.right.textContent = wrappedAmount; return _label.label; }); @@ -303,7 +297,7 @@ export default class PopupPayment extends PopupElement { _i18n(totalLabel.left, 'PaymentTransactionTotal'); const totalAmount = accumulate(invoice.prices.map(({amount}) => +amount), 0); - const canTip = invoice.max_tip_amount !== undefined; + const canTip = (invoice.max_tip_amount !== undefined && !isReceipt) || !!(paymentForm as PaymentsPaymentReceipt).tip_amount; if(canTip) { const tipsClassName = className + '-tips'; @@ -331,7 +325,7 @@ export default class PopupPayment extends PopupElement { placeCaretAtEnd(input); } - unsetActiveTip(); + unsetActiveTip && unsetActiveTip(); const tipEl = this.tipButtonsMap.get(amount); if(tipEl) { tipEl.classList.add('active'); @@ -350,7 +344,12 @@ export default class PopupPayment extends PopupElement { input.classList.add('input-clear', tipsClassName + '-input'); tipsLabel.right.append(input); - tipsLabel.label.style.cursor = 'text'; + if(!isReceipt) { + tipsLabel.label.style.cursor = 'text'; + } else { + tipsLabel.label.classList.add('disable-hover'); + } + tipsLabel.label.addEventListener('mousedown', (e) => { if(!findUpAsChild(e.target, input)) { placeCaretAtEnd(input); @@ -399,53 +398,58 @@ export default class PopupPayment extends PopupElement { pricesElements.push(tipsLabel.label); // - const tipsEl = document.createElement('div'); - tipsEl.classList.add(tipsClassName); - - const tipClassName = tipsClassName + '-tip'; - const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => { - const button = Button(tipClassName, {noRipple: true}); - button.textContent = wrapAmount(tipAmount); - - this.tipButtonsMap.set(+tipAmount, button); - return button; - }); + let unsetActiveTip: () => void; + if(!isReceipt) { + const tipsEl = document.createElement('div'); + tipsEl.classList.add(tipsClassName); + + const tipClassName = tipsClassName + '-tip'; + const tipButtons = invoice.suggested_tip_amounts.map((tipAmount) => { + const button = Button(tipClassName, {noRipple: true}); + button.textContent = wrapAmount(tipAmount); + + this.tipButtonsMap.set(+tipAmount, button); + return button; + }); - const unsetActiveTip = () => { - const prevTipEl = tipsEl.querySelector('.active'); - if(prevTipEl) { - prevTipEl.classList.remove('active'); - } - }; + unsetActiveTip = () => { + const prevTipEl = tipsEl.querySelector('.active'); + if(prevTipEl) { + prevTipEl.classList.remove('active'); + } + }; - attachClickEvent(tipsEl, (e) => { - const tipEl = findUpClassName(e.target, tipClassName); - if(!tipEl) { - return; - } + attachClickEvent(tipsEl, (e) => { + const tipEl = findUpClassName(e.target, tipClassName); + if(!tipEl) { + return; + } - let tipAmount = 0; - if(tipEl.classList.contains('active')) { - tipEl.classList.remove('active'); - } else { - unsetActiveTip(); - tipEl.classList.add('active'); + let tipAmount = 0; + if(tipEl.classList.contains('active')) { + tipEl.classList.remove('active'); + } else { + unsetActiveTip(); + tipEl.classList.add('active'); - for(const [amount, el] of this.tipButtonsMap) { - if(el === tipEl) { - tipAmount = amount; - break; + for(const [amount, el] of this.tipButtonsMap) { + if(el === tipEl) { + tipAmount = amount; + break; + } } } - } - setInputValue(tipAmount); - }); + setInputValue(tipAmount); + }); - setInputValue(0); + setInputValue(0); - tipsEl.append(...tipButtons); - pricesElements.push(tipsEl); + tipsEl.append(...tipButtons); + pricesElements.push(tipsEl); + } else { + setInputValue((paymentForm as PaymentsPaymentReceipt).tip_amount); + } } else { setTotal(); } @@ -476,6 +480,7 @@ export default class PopupPayment extends PopupElement { options.subtitleLangKey = options.titleLangKey; } + options.noWrap = true; const row = new Row(options); row.container.classList.add(className + '-row'); @@ -489,7 +494,7 @@ export default class PopupPayment extends PopupElement { const setRowTitle = (row: Row, textContent: string) => { row.title.textContent = textContent; if(!textContent) { - const e = I18n.weakMap.get(row.subtitle) as I18n.IntlElement; + const e = I18n.weakMap.get(row.subtitle.firstElementChild as HTMLElement) as I18n.IntlElement; row.title.append(i18n(e.key)); } @@ -558,7 +563,8 @@ export default class PopupPayment extends PopupElement { const postAddress = shippingAddress.shipping_address; setRowTitle(shippingAddressRow, [postAddress.city, postAddress.street_line1, postAddress.street_line2].filter(Boolean).join(', ')); - shippingMethodRow.container.classList.remove('hide'); + + shippingMethodRow.container.classList.toggle('hide', !lastRequestedInfo && !isReceipt); } : undefined; const setShippingInfo = (info: PaymentRequestedInfo) => { @@ -601,7 +607,13 @@ export default class PopupPayment extends PopupElement { shippingAmount = accumulate(shippingOption.prices.map(({amount}) => +amount), 0); lastShippingPricesElements = makePricesElements(shippingOption.prices); let l = totalLabel.label; - if(canTip) l = l.previousElementSibling.previousElementSibling as any; + if(canTip) { + l = l.previousElementSibling as any; + if(!isReceipt) { + l = l.previousElementSibling as any; + } + } + lastShippingPricesElements.forEach((element) => l.parentElement.insertBefore(element, l)); setTotal(); @@ -617,7 +629,7 @@ export default class PopupPayment extends PopupElement { let lastShippingPricesElements: HTMLElement[]; shippingMethodRow = createRow({ - icon: 'car', + icon: 'shipping', titleLangKey: 'PaymentCheckoutShippingMethod', clickable: !isReceipt && (onShippingMethodClick = () => { new PopupPaymentShippingMethods(paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => { @@ -762,6 +774,11 @@ export default class PopupPayment extends PopupElement { onConfirmed(); } else { popupPaymentVerification = new PopupPaymentVerification(paymentResult.url); + popupPaymentVerification.addEventListener('finish', () => { + popupPaymentVerification = undefined; + + onConfirmed(); + }); await new Promise((resolve, reject) => { popupPaymentVerification.addEventListener('close', () => { popupPaymentVerification = undefined; @@ -774,11 +791,6 @@ export default class PopupPayment extends PopupElement { } }); }); - - popupPaymentVerification.addEventListener('finish', () => { - popupPaymentVerification = undefined; - onConfirmed(); - }); } } catch(err) { if((err as ApiError).type === 'BOT_PRECHECKOUT_TIMEOUT') { diff --git a/src/components/popups/paymentCard.ts b/src/components/popups/paymentCard.ts index 557dc883..899f28d8 100644 --- a/src/components/popups/paymentCard.ts +++ b/src/components/popups/paymentCard.ts @@ -254,6 +254,7 @@ export default class PopupPaymentCard extends PopupElement<{ } }); + // putPreloader(this.body, true); this.body.append(iframe); this.show(); } diff --git a/src/components/popups/reactedList.ts b/src/components/popups/reactedList.ts index a0600585..4c9a2ac5 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 d807994a..22c66b6d 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 {attachClickEvent} from '../helpers/dom/clickEvent'; +import ListenerSetter from '../helpers/listenerSetter'; export default class Row { public container: HTMLElement; @@ -40,7 +42,9 @@ export default class Row { clickable: boolean | ((e: Event) => void), navigationTab: SliderSuperTab, havePadding: boolean, - noRipple: boolean + noRipple: boolean, + noWrap: boolean, + listenerSetter: ListenerSetter }> = {}) { this.container = document.createElement(options.radioField || options.checkboxField ? 'label' : 'div'); this.container.classList.add('row'); @@ -80,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); } } @@ -104,6 +111,7 @@ export default class Row { this.title = document.createElement('div'); this.title.classList.add('row-title'); this.title.setAttribute('dir', 'auto'); + if(options.noWrap) this.title.classList.add('no-wrap'); if(options.title) { if(typeof(options.title) === 'string') { this.title.innerHTML = options.title; @@ -149,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 f33dbd38..ce8e6361 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); } @@ -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/index.ts b/src/components/sidebarLeft/index.ts index bc3af26c..3b072151 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -34,7 +34,7 @@ import App from '../../config/app'; import ButtonMenuToggle from '../buttonMenuToggle'; import replaceContent from '../../helpers/dom/replaceContent'; import sessionStorage from '../../lib/sessionStorage'; -import {attachClickEvent, CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; +import {attachClickEvent, CLICK_EVENT_NAME, simulateClickEvent} from '../../helpers/dom/clickEvent'; import ButtonIcon from '../buttonIcon'; import confirmationPopup from '../confirmationPopup'; import IS_GEOLOCATION_SUPPORTED from '../../environment/geolocationSupport'; @@ -359,7 +359,7 @@ export class AppSidebarLeft extends SidebarSlider { const close = () => { // setTimeout(() => { - this.backBtn.click(); + simulateClickEvent(this.backBtn); // }, 0); }; @@ -632,7 +632,7 @@ export class AppSidebarLeft extends SidebarSlider { this.inputSearch.input.addEventListener('focus', onFocus); onFocus(); - this.backBtn.addEventListener('click', (e) => { + attachClickEvent(this.backBtn, (e) => { this.toolsBtn.classList.add(activeClassName); this.backBtn.classList.remove(activeClassName); this.toolsBtn.parentElement.firstElementChild.classList.toggle('state-back', false); diff --git a/src/components/sidebarLeft/tabs/autoDownload/file.ts b/src/components/sidebarLeft/tabs/autoDownload/file.ts index 87836b0a..361a93e8 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 6f9ebc13..efd6e5b4 100644 --- a/src/components/sidebarLeft/tabs/autoDownload/photo.ts +++ b/src/components/sidebarLeft/tabs/autoDownload/photo.ts @@ -4,12 +4,13 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type ListenerSetter from '../../../../helpers/listenerSetter'; import {SettingSection} from '../..'; 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 8115d368..6be6c382 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 0f44c717..d1c90842 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 ae8059eb..c2cf32d4 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 24d02a71..a00afc19 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 e7dda514..5f93a79d 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -22,6 +22,7 @@ import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText'; import {REAL_FOLDERS} from '../../../lib/mtproto/mtproto_config'; import rootScope from '../../../lib/rootScope'; import {MTAppConfig} from '../../../lib/mtproto/appConfig'; +import {attachClickEvent, simulateClickEvent} from '../../../helpers/dom/clickEvent'; export default class AppIncludedChatsTab extends SliderSuperTab { private editFolderTab: AppEditFolderTab; @@ -43,7 +44,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 = {}; @@ -107,7 +108,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { this.editFolderTab.setFilter(this.filter, false); this.close(); - }); + }, {listenerSetter: this.listenerSetter}); const onAppConfig = (appConfig: MTAppConfig) => { this.limit = rootScope.premium ? appConfig.dialog_filters_chats_limit_premium : appConfig.dialog_filters_chats_limit_default; @@ -266,7 +267,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab { for(const flag in filter.pFlags) { // @ts-ignore if(details.hasOwnProperty(flag) && !!filter.pFlags[flag]) { - (categoriesSection.content.querySelector(`[data-peer-id="${flag}"]`) as HTMLElement).click(); + simulateClickEvent(categoriesSection.content.querySelector(`[data-peer-id="${flag}"]`) as HTMLElement); } } } diff --git a/src/components/sidebarLeft/tabs/language.ts b/src/components/sidebarLeft/tabs/language.ts index 231090a5..61365515 100644 --- a/src/components/sidebarLeft/tabs/language.ts +++ b/src/components/sidebarLeft/tabs/language.ts @@ -21,11 +21,22 @@ export default class AppLanguageTab extends SliderSuperTab { const radioRows: Map = new Map(); - const promise = this.managers.apiManager.invokeApiCacheable('langpack.getLanguages', { - lang_pack: 'macos' - }).then((languages) => { + const promise = Promise.all([ + this.managers.apiManager.invokeApiCacheable('langpack.getLanguages', { + lang_pack: 'web' + }), + this.managers.apiManager.invokeApiCacheable('langpack.getLanguages', { + lang_pack: 'macos' + }), + ]).then(([languages1, languages2]) => { + const rendered: Set = new Set(); + const webLangCodes = languages1.map((language) => language.lang_code); + const random = randomLong(); - languages.forEach((language) => { + languages1.concat(languages2).forEach((language) => { + if(rendered.has(language.lang_code)) return; + rendered.add(language.lang_code); + const row = new Row({ radioField: new RadioField({ text: language.name, @@ -39,7 +50,7 @@ export default class AppLanguageTab extends SliderSuperTab { }); const form = RadioFormFromRows([...radioRows.values()], (value) => { - I18n.getLangPack(value); + I18n.getLangPack(value, webLangCodes.includes(value)); }); I18n.getCacheLangPack().then((langPack) => { diff --git a/src/components/sidebarLeft/tabs/newChannel.ts b/src/components/sidebarLeft/tabs/newChannel.ts index 370c7096..81f6d74c 100644 --- a/src/components/sidebarLeft/tabs/newChannel.ts +++ b/src/components/sidebarLeft/tabs/newChannel.ts @@ -60,7 +60,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 +89,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 1a8926ee..1e128a5b 100644 --- a/src/components/sidebarLeft/tabs/newGroup.ts +++ b/src/components/sidebarLeft/tabs/newGroup.ts @@ -68,7 +68,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 +77,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 +128,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 152e3032..d85e3f95 100644 --- a/src/components/sidebarLeft/tabs/notifications.ts +++ b/src/components/sidebarLeft/tabs/notifications.ts @@ -35,12 +35,14 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { const enabledRow = new Row({ checkboxField: new CheckboxField({text: options.typeText, checked: true}), - subtitleLangKey: 'Loading' + subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); const previewEnabledRow = new Row({ checkboxField: new CheckboxField({text: 'MessagePreview', checked: true}), - subtitleLangKey: 'Loading' + subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); section.content.append(enabledRow.container, previewEnabledRow.container); @@ -111,12 +113,14 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { const contactsSignUpRow = new Row({ checkboxField: new CheckboxField({text: 'ContactJoined', checked: true}), - subtitleLangKey: 'Loading' + subtitleLangKey: 'Loading', + listenerSetter: this.listenerSetter }); const soundRow = new Row({ - checkboxField: new CheckboxField({text: 'Notifications.Sound', checked: true, stateKey: 'settings.notifications.sound'}), - subtitleLangKey: 'Loading' + 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 af76bad0..aeb6fb1e 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 9d937f91..baa92bb1 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 7b818a41..1cc0bb52 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) { const 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); + previousTab.container.replaceWith(tab.container); + } else { + previousTab.container.remove(); + } } else { this.tabsContainer.prepend(tab.container); } diff --git a/src/components/sidebarRight/tabs/chatReactions.ts b/src/components/sidebarRight/tabs/chatReactions.ts index 6abbd81d..bd90ed48 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 2c739c18..0c25ab8b 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 acabfb47..fde7170c 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(); @@ -181,7 +183,8 @@ export default class AppEditChatTab extends SliderSuperTab { tab.chatId = this.chatId; tab.open(); }, - icon: 'permissions' + 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 c5efea76..9027fcb5 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 eec017f1..09e22ef9 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 13dc90cd..0177acdc 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 7f1be7ea..da2c40ac 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 366be077..f99f7d59 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 6da9a716..0bb285c5 100644 --- a/src/components/sliderTab.ts +++ b/src/components/sliderTab.ts @@ -107,10 +107,8 @@ export default class SliderSuperTab implements SliderTab { if(this.destroyable) { // ! WARNING, пока что это будет работать только с самой последней внутренней вкладкой ! this.slider.tabs.delete(this); this.container.remove(); - } - - if(this.listenerSetter) { - this.listenerSetter.removeAll(); + this.scrollable.destroy(); + this.listenerSetter?.removeAll(); } } diff --git a/src/components/transition.ts b/src/components/transition.ts index 30cbeee3..ad858ae8 100644 --- a/src/components/transition.ts +++ b/src/components/transition.ts @@ -9,6 +9,7 @@ import deferredPromise, {CancellablePromise} from '../helpers/cancellablePromise 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); diff --git a/src/components/visibilityIntersector.ts b/src/components/visibilityIntersector.ts index e57fa383..0bc478ba 100644 --- a/src/components/visibilityIntersector.ts +++ b/src/components/visibilityIntersector.ts @@ -5,20 +5,21 @@ */ type TargetType = HTMLElement; -export type OnVisibilityChange = (target: TargetType, visible: boolean) => void; +export type OnVisibilityChangeItem = {target: TargetType, visible: boolean, entry: IntersectionObserverEntry}; +export type OnVisibilityChange = (item: OnVisibilityChangeItem) => void; export default class VisibilityIntersector { private observer: IntersectionObserver; private items: Map = new Map(); private locked = false; - constructor(onVisibilityChange: OnVisibilityChange) { + constructor(onVisibilityChange: OnVisibilityChange, options?: IntersectionObserverInit) { this.observer = new IntersectionObserver((entries) => { if(this.locked) { return; } - const changed: {target: TargetType, visible: boolean}[] = []; + const changed: OnVisibilityChangeItem[] = []; entries.forEach((entry) => { const target = entry.target as TargetType; @@ -37,15 +38,19 @@ export default class VisibilityIntersector { return; } */ - changed[entry.isIntersecting ? 'unshift' : 'push']({target, visible: entry.isIntersecting}); + const change: typeof changed[0] = {target, visible: entry.isIntersecting, entry}; + + // ! order will be incorrect so can't use it + // changed[entry.isIntersecting ? 'unshift' : 'push'](change); + changed.push(change); // onVisibilityChange(target, entry.isIntersecting); }); - changed.forEach((smth) => { - onVisibilityChange(smth.target, smth.visible); + changed.forEach((item) => { + onVisibilityChange(item); }); - }); + }, options); } public getVisible() { diff --git a/src/components/wrappers/messageActionTextNewUnsafe.ts b/src/components/wrappers/messageActionTextNewUnsafe.ts index b74ac912..768bbaad 100644 --- a/src/components/wrappers/messageActionTextNewUnsafe.ts +++ b/src/components/wrappers/messageActionTextNewUnsafe.ts @@ -267,7 +267,10 @@ export default async function wrapMessageActionTextNewUnsafe(message: MyMessage, managers.appMessagesManager.fetchMessageReplyTo(message); } else { langPackKey = isRecurringUsed ? 'Chat.Service.PaymentSentRecurringUsed' : (isRecurringInit ? 'Chat.Service.PaymentSentRecurringInit' : 'Chat.Service.PaymentSent1'); - args.push(wrapLinkToMessage(invoiceMessage, plain)); + args.push(wrapLinkToMessage(invoiceMessage, plain).then((el) => { + el.classList.add('is-receipt-link'); + return el; + })); } } diff --git a/src/components/wrappers/stickerSetThumb.ts b/src/components/wrappers/stickerSetThumb.ts index 2a396548..8a73977d 100644 --- a/src/components/wrappers/stickerSetThumb.ts +++ b/src/components/wrappers/stickerSetThumb.ts @@ -11,6 +11,7 @@ import appDownloadManager from '../../lib/appManagers/appDownloadManager'; import {AppManagers} from '../../lib/appManagers/managers'; import lottieLoader from '../../lib/rlottie/lottieLoader'; import rootScope from '../../lib/rootScope'; +import animationIntersector from '../animationIntersector'; import LazyLoadQueue from '../lazyLoadQueue'; import wrapSticker from './sticker'; @@ -62,6 +63,10 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container return promise.then((blob) => { renderImageFromUrl(media, URL.createObjectURL(blob), () => { container.append(media); + + if(set.pFlags.videos) { + animationIntersector.addAnimation(media as HTMLVideoElement, group); + } }); }); } @@ -79,7 +84,9 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container div: container, group: group, lazyLoadQueue, - managers + managers, + width, + height }); // kostil } } diff --git a/src/components/wrappers/video.ts b/src/components/wrappers/video.ts index aaf93069..9b35ca84 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/bigInt/bigIntConstants.ts b/src/helpers/bigInt/bigIntConstants.ts new file mode 100644 index 00000000..d2ae8f26 --- /dev/null +++ b/src/helpers/bigInt/bigIntConstants.ts @@ -0,0 +1,5 @@ +import bigInt from 'big-integer'; + +export const safeBigInt = bigInt(Number.MAX_SAFE_INTEGER); +export const ulongBigInt = bigInt(bigInt[2]).pow(64); +export const longBigInt = ulongBigInt.divide(bigInt[2]); diff --git a/src/helpers/bigInt/bigIntConversion.ts b/src/helpers/bigInt/bigIntConversion.ts index 088e7359..65c9876c 100644 --- a/src/helpers/bigInt/bigIntConversion.ts +++ b/src/helpers/bigInt/bigIntConversion.ts @@ -1,4 +1,5 @@ import bigInt from 'big-integer'; +import { longBigInt, ulongBigInt } from './bigIntConstants'; export function bigIntFromBytes(bytes: Uint8Array | number[], base = 256) { return bigInt.fromArray(bytes instanceof Uint8Array ? [...bytes] : bytes, base); @@ -7,3 +8,11 @@ export function bigIntFromBytes(bytes: Uint8Array | number[], base = 256) { export function bigIntToBytes(bigInt: bigInt.BigInteger) { return new Uint8Array(bigInt.toArray(256).value); } + +export function bigIntToSigned(bigInt: bigInt.BigInteger) { + return bigInt.greater(longBigInt) ? bigInt.minus(ulongBigInt) : bigInt; +} + +export function bigIntToUnsigned(bigInt: bigInt.BigInteger) { + return bigInt.isNegative() ? ulongBigInt.add(bigInt) : bigInt; +} diff --git a/src/helpers/canvas/getTextWidth.ts b/src/helpers/canvas/getTextWidth.ts index 071b5b3f..f91718f1 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/cards/cardBrands.ts b/src/helpers/cards/cardBrands.ts index b7df8bc5..03b2faa1 100644 --- a/src/helpers/cards/cardBrands.ts +++ b/src/helpers/cards/cardBrands.ts @@ -3,14 +3,15 @@ import replaceNonNumber from '../string/replaceNonNumber'; const CARD_BRAND_REGEXP: {[brand: string]: RegExp} = { visa: /^4/, - mastercard: /^(51|52|53|54|55|22|23|24|25|26|27)/, + mastercard: /^(51|52|53|54|55|222|23|24|25|26|27)/, amex: /^(34|37)/, discover: /^(60|64|65)/, diners: /^(30|38|39)/, diners14: /^(36)/, jcb: /^(35)/, unionpay: /^(62[0-6,8-9]|627[0-6,8-9]|6277[0-7,9]|62778[1-9]|81)/, - elo: /^(5067|509|636368|627780)/ + elo: /^(5067|509|636368|627780)/, + mir: /^(220[0-4])/ }; // * taken from Stripe @@ -74,6 +75,12 @@ export const CARD_BRANDS: {[b: string]: { cvcMaxLength: 3, cvcMinLength: null }, + mir: { + minLength: 16, + maxLength: 16, + cvcMaxLength: 3, + cvcMinLength: null + }, unknown: { minLength: 16, maxLength: 16, diff --git a/src/helpers/cards/validateCard.ts b/src/helpers/cards/validateCard.ts index 4d5aba91..118f5212 100644 --- a/src/helpers/cards/validateCard.ts +++ b/src/helpers/cards/validateCard.ts @@ -16,6 +16,7 @@ function makeValidationError(code?: string) { } : null; } +// Luhn algorithm function validateCompleteCardNumber(card: string) { const t = '0'.charCodeAt(0); const n = card.length % 2; @@ -61,7 +62,11 @@ function getCardInfoByNumber(card: string) { } function makeCardNumberError(str: string, length: number, ignoreIncomplete: boolean) { - return str.length >= length ? (validateCompleteCardNumber(str) ? null : makeValidationError('invalid')) : (ignoreIncomplete ? null : makeValidationError('incomplete')); + if(str.length >= length) { + return validateCompleteCardNumber(str) || detectCardBrand(str) === 'mir' ? null : makeValidationError('invalid'); + } + + return ignoreIncomplete ? null : makeValidationError('incomplete'); } export function validateCardNumber(str: string, options: PatternValidationOptions = {}) { diff --git a/src/helpers/dom/renderImageWithFadeIn.ts b/src/helpers/dom/renderImageWithFadeIn.ts index 3a69f13f..f7c561ad 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/helpers/dropdownHover.ts b/src/helpers/dropdownHover.ts index 4ee3e296..45c0ba96 100644 --- a/src/helpers/dropdownHover.ts +++ b/src/helpers/dropdownHover.ts @@ -48,7 +48,10 @@ export default class DropdownHover extends EventListenerBase<{ listenerSetter.add(button)('mouseover', (e) => { // console.log('onmouseover button'); if(firstTime) { - listenerSetter.add(button)('mouseout', this.onMouseOut); + listenerSetter.add(button)('mouseout', (e) => { + clearTimeout(this.displayTimeout); + this.onMouseOut(e); + }); firstTime = false; } @@ -61,9 +64,8 @@ export default class DropdownHover extends EventListenerBase<{ } private onMouseOut = (e: MouseEvent) => { - if(KEEP_OPEN) return; + if(KEEP_OPEN || !this.isActive()) return; clearTimeout(this.displayTimeout); - if(!this.isActive()) return; const toElement = (e as any).toElement as Element; if(toElement && findUpAsChild(toElement, this.element)) { diff --git a/src/helpers/paymentsWrapCurrencyAmount.ts b/src/helpers/paymentsWrapCurrencyAmount.ts index aa008833..24f509c9 100644 --- a/src/helpers/paymentsWrapCurrencyAmount.ts +++ b/src/helpers/paymentsWrapCurrencyAmount.ts @@ -25,49 +25,58 @@ function number_format(number: any, decimals: any, dec_point: any, thousands_sep return s.join(dec); } -export default function paymentsWrapCurrencyAmount($amount: number | string, $currency: string, $skipSymbol?: boolean) { - $amount = +$amount; +export default function paymentsWrapCurrencyAmount(amount: number | string, currency: string, skipSymbol?: boolean) { + amount = +amount; - const $currency_data = Currencies[$currency]; // вытащить из json - if(!$currency_data) { + const isNegative = amount < 0; + + const currencyData = Currencies[currency]; + if(!currencyData) { throw new Error('CURRENCY_WRAP_INVALID'); } - const $amount_exp = $amount / Math.pow(10, $currency_data['exp']); + const amountExp = amount / Math.pow(10, currencyData.exp); - let $decimals = $currency_data['exp']; - if($currency == 'IRR' && - Math.floor($amount_exp) == $amount_exp) { - $decimals = 0; // у иранцев копейки почти всегда = 0 и не показываются в UI + let decimals = currencyData.exp; + if(currency == 'IRR' && Math.floor(amountExp) == amountExp) { + decimals = 0; // у иранцев копейки почти всегда = 0 и не показываются в UI } - const $formatted = number_format($amount_exp, $decimals, $currency_data['decimal_sep'], $currency_data['thousands_sep']); - if($skipSymbol) { - return $formatted; + let formatted = number_format(amountExp, decimals, currencyData.decimal_sep, currencyData.thousands_sep); + if(skipSymbol) { + return formatted; } - const $splitter = $currency_data['space_between'] ? ' ' : ''; - let $formatted_intern: string; - if($currency_data['symbol_left']) { - $formatted_intern = $currency_data['symbol'] + $splitter + $formatted; - } else { - $formatted_intern = $formatted + $splitter + $currency_data['symbol']; + let symbol = currencyData.symbol; + if(isNegative && !currencyData.space_between && currencyData.symbol_left) { + symbol = '-' + symbol; + formatted = formatted.replace('-', ''); } - return $formatted_intern; -} -function paymentsGetCurrencyExp($currency: string) { - if($currency == 'CLF') { - return 4; - } - if(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'].includes($currency)) { - return 3; - } - if(['BIF', 'BYR', 'CLP', 'CVE', 'DJF', 'GNF', 'ISK', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'UYI', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'].includes($currency)) { - return 0; - } - if($currency == 'MRO') { - return 1; + let out: string; + const splitter = currencyData.space_between ? ' ' : ''; + if(currencyData.symbol_left) { + out = symbol + splitter + formatted; + } else { + out = formatted + splitter + symbol; } - return 2; + return out; } + +(window as any).p = paymentsWrapCurrencyAmount; + +// function paymentsGetCurrencyExp($currency: string) { +// if($currency == 'CLF') { +// return 4; +// } +// if(['BHD','IQD','JOD','KWD','LYD','OMR','TND'].includes($currency)) { +// return 3; +// } +// if(['BIF','BYR','CLP','CVE','DJF','GNF','ISK','JPY','KMF','KRW','MGA', 'PYG','RWF','UGX','UYI','VND','VUV','XAF','XOF','XPF'].includes($currency)) { +// return 0; +// } +// if($currency == 'MRO') { +// return 1; +// } +// return 2; +// } diff --git a/src/helpers/searchListLoader.ts b/src/helpers/searchListLoader.ts index 3dd6ad89..aea52a96 100644 --- a/src/helpers/searchListLoader.ts +++ b/src/helpers/searchListLoader.ts @@ -191,20 +191,20 @@ export default class SearchListLoader
diff --git a/src/lang.ts b/src/lang.ts index 419f782a..d086ddf8 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -746,6 +746,8 @@ const lang = { 'Clear': 'Clear', 'Save': 'Save', 'PaymentCheckoutName': 'Name', + 'ClearRecentStickersAlertTitle': 'Clear recent stickers', + 'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?', // * macos 'AccountSettings.Filters': 'Chat Folders', diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 3aa34714..5db76173 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1571,6 +1571,7 @@ export class AppDialogsManager { }, {capture: true}); // cancel link click + // ! do not change it to attachClickEvent list.addEventListener('click', (e) => { if(e.button === 0) { cancelEvent(e); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index b99387f2..b010556e 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -24,7 +24,7 @@ import {MOUNT_CLASS_TO} from '../../config/debug'; import appNavigationController from '../../components/appNavigationController'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import I18n, {i18n, join, LangPackKey} from '../langPack'; -import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, SendMessageAction} from '../../layer'; +import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction} from '../../layer'; import {hslaStringToHex} from '../../helpers/color'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; @@ -87,8 +87,10 @@ import groupCallsController from '../calls/groupCallsController'; import callsController from '../calls/callsController'; import getFilesFromEvent from '../../helpers/files/getFilesFromEvent'; import apiManagerProxy from '../mtproto/mtprotoworker'; -import wrapPeerTitle from '../../components/wrappers/peerTitle'; import appRuntimeManager from './appRuntimeManager'; +import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount'; +import findUpClassName from '../../helpers/dom/findUpClassName'; +import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; import PopupPayment from '../../components/popups/payment'; export const CHAT_ANIMATION_GROUP = 'chat'; @@ -349,6 +351,59 @@ export class AppImManager extends EventListenerBase<{ }); }); + rootScope.addEventListener('payment_sent', async({peerId, mid, receiptMessage}) => { + const message = await this.managers.appMessagesManager.getMessageByPeer(peerId, mid); + if(!message) { + return; + } + + const action = receiptMessage.action as MessageAction.messageActionPaymentSent; + toastNew({ + langPackKey: 'PaymentInfoHint', + langPackArguments: [ + paymentsWrapCurrencyAmount(action.total_amount, action.currency), + wrapEmojiText(((message as Message.message).media as MessageMedia.messageMediaInvoice).title) + ] + }); + }); + + (window as any).onSpoilerClick = (e: MouseEvent) => { + const spoiler = findUpClassName(e.target, 'spoiler'); + const parentElement = findUpClassName(spoiler, 'message') || spoiler.parentElement; + + const className = 'is-spoiler-visible'; + 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; + const showDuration = 5000; + const useRafs = !isVisible ? 2 : 0; + if(useRafs) { + parentElement.classList.add('will-change'); + } + + const spoilerTimeout = parentElement.dataset.spoilerTimeout; + if(spoilerTimeout !== null) { + clearTimeout(+spoilerTimeout); + delete parentElement.dataset.spoilerTimeout; + } + + SetTransition(parentElement, className, true, duration, () => { + parentElement.dataset.spoilerTimeout = '' + window.setTimeout(() => { + SetTransition(parentElement, className, false, duration, () => { + parentElement.classList.remove('will-change'); + delete parentElement.dataset.spoilerTimeout; + }); + }, showDuration); + }, useRafs); + }; + apiManagerProxy.addEventListener('notificationBuild', (options) => { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) { return; @@ -360,13 +415,7 @@ export class AppImManager extends EventListenerBase<{ this.addEventListener('peer_changed', async(peerId) => { document.body.classList.toggle('has-chat', !!peerId); - let str: string; - if(peerId) { - const username = await this.managers.appPeersManager.getPeerUsername(peerId); - str = username ? '@' + username : '' + peerId; - } - - appNavigationController.overrideHash(str); + this.overrideHash(peerId); apiManagerProxy.updateTabState('chatPeerIds', this.chats.map((chat) => chat.peerId).filter(Boolean)); }); @@ -1513,6 +1562,16 @@ export class AppImManager extends EventListenerBase<{ } }; + private async overrideHash(peerId?: PeerId) { + let str: string; + if(peerId) { + const username = await this.managers.appPeersManager.getPeerUsername(peerId); + str = username ? '@' + username : '' + peerId; + } + + appNavigationController.overrideHash(str); + } + public selectTab(id: number, animate?: boolean) { if(animate === false) { // * will be used for Safari iOS history swipe disableTransition([appSidebarLeft.sidebarEl, this.columnEl, appSidebarRight.sidebarEl]); @@ -1521,6 +1580,9 @@ export class AppImManager extends EventListenerBase<{ document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id === 0); const prevTabId = this.tabId; + if(prevTabId !== -1) { + this.overrideHash(id > 0 ? this.chat?.peerId : undefined); + } this.log('selectTab', id, prevTabId); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 0d02703d..57eb491c 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -4030,7 +4030,8 @@ export class AppMessagesManager extends AppManager { if(message._ === 'messageService' && message.action._ === 'messageActionPaymentSent' && message.reply_to) { this.rootScope.dispatchEvent('payment_sent', { peerId: message.reply_to.reply_to_peer_id ? this.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : message.peerId, - mid: message.reply_to_mid + mid: message.reply_to_mid, + receiptMessage: message }); } @@ -4259,8 +4260,8 @@ export class AppMessagesManager extends AppManager { } releaseUnreadCount(); - this.rootScope.dispatchEvent('dialogs_multiupdate', {[peerId]: dialog}); this.dialogsStorage.setDialogToState(dialog); + this.rootScope.dispatchEvent('dialogs_multiupdate', {[peerId]: dialog}); } }; @@ -4320,8 +4321,8 @@ export class AppMessagesManager extends AppManager { if(isTopMessage || (message as Message.message).grouped_id) { const updatedDialogs: {[peerId: PeerId]: Dialog} = {}; updatedDialogs[peerId] = dialog; - this.rootScope.dispatchEvent('dialogs_multiupdate', updatedDialogs); this.dialogsStorage.setDialogToState(dialog); + this.rootScope.dispatchEvent('dialogs_multiupdate', updatedDialogs); } } }; diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index b946bd87..6f8944fe 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -72,6 +72,12 @@ export class AppStickersManager extends AppManager { const stickerSet = update.stickerset as MyMessagesStickerSet; this.saveStickerSet(stickerSet, stickerSet.set.id); this.rootScope.dispatchEvent('stickers_installed', stickerSet.set); + }, + + updateRecentStickers: () => { + this.getRecentStickers().then(({stickers}) => { + this.rootScope.dispatchEvent('stickers_recent', stickers as MyDocument[]); + }); } }); } @@ -103,7 +109,7 @@ export class AppStickersManager extends AppManager { }); } - public saveStickers(docs: Document[]) { + private saveStickers(docs: Document[]) { forEachReverse(docs, (doc, idx) => { doc = this.appDocsManager.saveDoc(doc); @@ -295,9 +301,7 @@ export class AppStickersManager extends AppManager { }); } - public saveStickerSet(res: Omit, id: DocId) { - // console.log('stickers save set', res);w - + private saveStickerSet(res: Omit, id: DocId) { const newSet: MessagesStickerSet = { _: 'messages.stickerSet', set: res.set, @@ -401,6 +405,8 @@ export class AppStickersManager extends AppManager { } public async toggleStickerSet(set: StickerSet.stickerSet) { + set = this.storage.getFromCache(set.id).set; + if(set.installed_date) { const res = await this.apiManager.invokeApi('messages.uninstallStickerSet', { stickerset: this.getStickerSetInput(set) @@ -559,7 +565,8 @@ export class AppStickersManager extends AppManager { }); } - public pushRecentSticker(doc: MyDocument) { + public pushRecentSticker(docId: DocId) { + const doc = this.appDocsManager.getDoc(docId); const docEmoticon = fixEmoji(doc.stickerEmojiRaw); for(const emoticon in this.getStickersByEmoticonsPromises) { const promise = this.getStickersByEmoticonsPromises[emoticon]; @@ -573,4 +580,9 @@ export class AppStickersManager extends AppManager { }); } } + + public clearRecentStickers() { + this.rootScope.dispatchEvent('stickers_recent', []); + return this.apiManager.invokeApi('messages.clearRecentStickers'); + } } diff --git a/src/lib/calls/callsController.ts b/src/lib/calls/callsController.ts index 66c94c87..a7c3cddc 100644 --- a/src/lib/calls/callsController.ts +++ b/src/lib/calls/callsController.ts @@ -115,7 +115,8 @@ export class CallsController extends EventListenerBase<{ const {key, key_fingerprint} = await this.managers.appCallsManager.computeKey(g_a, dh.b, dh.p); if(call.key_fingerprint !== key_fingerprint) { - this.log.error('Incorrect key fingerprint', call.key_fingerprint, key_fingerprint); + this.log.error('Incorrect key fingerprint', call.key_fingerprint, key_fingerprint, g_a, dh); + instance.hangUp('phoneCallDiscardReasonDisconnect'); break; } diff --git a/src/lib/crypto/computeDhKey.ts b/src/lib/crypto/computeDhKey.ts index cb0069d8..e4a91673 100644 --- a/src/lib/crypto/computeDhKey.ts +++ b/src/lib/crypto/computeDhKey.ts @@ -4,14 +4,14 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import {bigIntFromBytes} from '../../helpers/bigInt/bigIntConversion'; +import {bigIntFromBytes, bigIntToSigned} from '../../helpers/bigInt/bigIntConversion'; import cryptoWorker from './cryptoMessagePort'; export default async function computeDhKey(g_b: Uint8Array, a: Uint8Array, p: Uint8Array) { const key = await cryptoWorker.invokeCrypto('mod-pow', g_b, a, p); const keySha1Hashed = await cryptoWorker.invokeCrypto('sha1', key); - const key_fingerprint = keySha1Hashed.slice(-8).reverse(); // key_fingerprint: key_fingerprint as any // ! it doesn't work - const key_fingerprint_long = bigIntFromBytes(key_fingerprint).toString(10); // bigInt2str(str2bigInt(bytesToHex(key_fingerprint), 16), 10); + const key_fingerprint = keySha1Hashed.slice(-8).reverse(); + const key_fingerprint_long = bigIntToSigned(bigIntFromBytes(key_fingerprint)).toString(10); return {key, key_fingerprint: key_fingerprint_long}; } diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index cc33c7a8..ddba807f 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -81,9 +81,16 @@ namespace I18n { let cacheLangPackPromise: Promise; export let lastRequestedLangCode: string; + export let lastRequestedNormalizedLangCode: string; export let lastAppliedLangCode: string; export let requestedServerLanguage = false; export let timeFormat: State['settings']['timeFormat']; + + function setLangCode(langCode: string) { + lastRequestedLangCode = langCode; + lastRequestedNormalizedLangCode = langCode.split('-')[0]; + } + export function getCacheLangPack(): Promise { if(cacheLangPackPromise) return cacheLangPackPromise; return cacheLangPackPromise = Promise.all([ @@ -99,7 +106,7 @@ namespace I18n { } */ if(!lastRequestedLangCode) { - lastRequestedLangCode = langPack.lang_code; + setLangCode(langPack.lang_code); } applyLangPack(langPack); @@ -150,7 +157,7 @@ namespace I18n { export function loadLocalLangPack() { const defaultCode = App.langPackCode; - lastRequestedLangCode = defaultCode; + setLangCode(defaultCode); return Promise.all([ import('../lang'), import('../langSign'), @@ -173,15 +180,15 @@ namespace I18n { }); } - export function loadLangPack(langCode: string) { + export function loadLangPack(langCode: string, web?: boolean) { requestedServerLanguage = true; const managers = rootScope.managers; return Promise.all([ managers.apiManager.invokeApiCacheable('langpack.getLangPack', { lang_code: langCode, - lang_pack: App.langPack + lang_pack: web ? 'web' : App.langPack }), - managers.apiManager.invokeApiCacheable('langpack.getLangPack', { + !web && managers.apiManager.invokeApiCacheable('langpack.getLangPack', { lang_code: langCode, lang_pack: 'android' }), @@ -225,20 +232,16 @@ namespace I18n { return pushTo; } - export function getLangPack(langCode: string) { - lastRequestedLangCode = langCode; - return loadLangPack(langCode).then(([langPack1, langPack2, localLangPack1, localLangPack2, countries, _]) => { + export function getLangPack(langCode: string, web?: boolean) { + setLangCode(langCode); + return loadLangPack(langCode, web).then(([langPack1, langPack2, localLangPack1, localLangPack2, countries, _]) => { let strings: LangPackString[] = []; [localLangPack1, localLangPack2].forEach((l) => { formatLocalStrings(l.default as any, strings); }); - strings = strings.concat(langPack1.strings); - - for(const string of langPack2.strings) { - strings.push(string); - } + strings = strings.concat(...[langPack1.strings, langPack2.strings].filter(Boolean)); langPack1.strings = strings; langPack1.countries = countries; @@ -266,10 +269,18 @@ namespace I18n { })(); export function applyLangPack(langPack: LangPackDifference) { - if(langPack.lang_code !== lastRequestedLangCode) { + const currentLangCode = lastRequestedLangCode; + if(langPack.lang_code !== currentLangCode) { return; } + try { + pluralRules = new Intl.PluralRules(lastRequestedNormalizedLangCode); + } catch(err) { + console.error('pluralRules error', err); + pluralRules = new Intl.PluralRules(lastRequestedNormalizedLangCode.split('-', 1)[0]); + } + try { pluralRules = new Intl.PluralRules(langPack.lang_code); } catch(err) { @@ -299,9 +310,9 @@ namespace I18n { }); } - if(lastAppliedLangCode !== langPack.lang_code) { - rootScope.dispatchEvent('language_change', langPack.lang_code); - lastAppliedLangCode = langPack.lang_code; + if(lastAppliedLangCode !== currentLangCode) { + rootScope.dispatchEvent('language_change', currentLangCode); + lastAppliedLangCode = currentLangCode; cachedDateTimeFormats.clear(); updateAmPm(); } @@ -508,7 +519,8 @@ namespace I18n { const json = JSON.stringify(options); let dateTimeFormat = cachedDateTimeFormats.get(json); if(!dateTimeFormat) { - cachedDateTimeFormats.set(json, dateTimeFormat = new Intl.DateTimeFormat(lastRequestedLangCode + '-u-hc-' + timeFormat, options)); + dateTimeFormat = new Intl.DateTimeFormat(lastRequestedNormalizedLangCode + '-u-hc-' + timeFormat, options); + cachedDateTimeFormats.set(json, dateTimeFormat); } return dateTimeFormat; diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 00f25707..76a2247c 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]; @@ -97,7 +97,6 @@ export default class VideoPlayer extends ControlsHover { }).finally(() => { // due to autoplay, play will not call this.wrapper.classList.toggle('is-playing', !this.video.paused); }); - // (this.wrapper.querySelector('.toggle') as HTMLButtonElement).click(); } } @@ -127,15 +126,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 +169,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 +215,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/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index a7fba6ec..37fdaf53 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -17,16 +17,14 @@ import isObject from '../../helpers/object/isObject'; import gzipUncompress from '../../helpers/gzipUncompress'; import bigInt from 'big-integer'; import ulongFromInts from '../../helpers/long/ulongFromInts'; +import { safeBigInt } from '../../helpers/bigInt/bigIntConstants'; +import { bigIntToSigned, bigIntToUnsigned } from '../../helpers/bigInt/bigIntConversion'; const boolFalse = +Schema.API.constructors.find((c) => c.predicate === 'boolFalse').id; const boolTrue = +Schema.API.constructors.find((c) => c.predicate === 'boolTrue').id; const vector = +Schema.API.constructors.find((c) => c.predicate === 'vector').id; const gzipPacked = +Schema.MTProto.constructors.find((c) => c.predicate === 'gzip_packed').id; -const safeBigInt = bigInt(Number.MAX_SAFE_INTEGER); -const ulongBigInt = bigInt(bigInt[2]).pow(64); -const longBigInt = ulongBigInt.divide(bigInt[2]); - class TLSerialization { private maxLength = 2048; // 2Kb private offset = 0; // in bytes @@ -151,11 +149,7 @@ class TLSerialization { } } - let _bigInt = bigInt(sLong as string); - if(_bigInt.isNegative()) { // make it unsigned - _bigInt = ulongBigInt.add(_bigInt); - } - + const _bigInt = bigIntToUnsigned(bigInt(sLong as string)); const {quotient, remainder} = _bigInt.divmod(0x100000000); const high = quotient.toJSNumber(); const low = remainder.toJSNumber(); @@ -504,8 +498,8 @@ class TLDeserialization { const iHigh = this.readInt((field || '') + ':long[high]'); let ulong = ulongFromInts(iHigh, iLow); - if(/* !unsigned && */!this.mtproto && ulong.greater(longBigInt)) { // make it signed - ulong = ulong.minus(ulongBigInt); + if(/* !unsigned && */!this.mtproto) { // make it signed + ulong = bigIntToSigned(ulong); } if(!this.mtproto) { diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index 1fbee8ef..a0ec4865 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -18,6 +18,7 @@ import parseEntities from './parseEntities'; import setBlankToAnchor from './setBlankToAnchor'; import wrapUrl from './wrapUrl'; import EMOJI_VERSIONS_SUPPORTED from '../../environment/emojiVersionsSupport'; +import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; /** * * Expecting correctly sorted nested entities (RichTextProcessor.sortEntities) @@ -400,6 +401,8 @@ export default function wrapRichText(text: string, options: Partial<{ usedText = true; container.append(element); fragment.append(container); + + container[`on${CLICK_EVENT_NAME}`] = (window as any).onSpoilerClick; } break; @@ -419,7 +422,7 @@ export default function wrapRichText(text: string, options: Partial<{ (lastElement || fragment).append(element); } - while(nextEntity && nextEntity.offset < (endOffset - 1)) { + while(nextEntity && nextEntity.offset < endOffset) { ++nasty.i; (element || fragment).append(wrapRichText(nasty.text, { diff --git a/src/lib/rlottie/queryableWorker.ts b/src/lib/rlottie/queryableWorker.ts index 10d5f6ed..3b700a86 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/src/lib/rootScope.ts b/src/lib/rootScope.ts index 9c755f9f..8abbec02 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -16,6 +16,7 @@ import type {AppManagers} from './appManagers/managers'; import type {State} from '../config/state'; import type {Progress} from './appManagers/appDownloadManager'; import type {CallId} from './appManagers/appCallsManager'; +import type {MyDocument} from './appManagers/appDocsManager'; import {NULL_PEER_ID, UserAuth} from './mtproto/mtproto_config'; import EventListenerBase from '../helpers/eventListenerBase'; import {MOUNT_CLASS_TO} from '../config/debug'; @@ -86,6 +87,7 @@ export type BroadcastEvents = { 'stickers_installed': StickerSet.stickerSet, 'stickers_deleted': StickerSet.stickerSet, + 'stickers_recent': MyDocument[], 'state_cleared': void, 'state_synchronized': ChatId | void, @@ -139,7 +141,7 @@ export type BroadcastEvents = { 'logging_out': void, - 'payment_sent': {peerId: PeerId, mid: number}, + 'payment_sent': {peerId: PeerId, mid: number, receiptMessage: Message.messageService} 'premium_toggle': boolean, diff --git a/src/scripts/out/langPack.strings b/src/scripts/out/langPack.strings index 35bcdfa2..75f3b3ec 100644 --- a/src/scripts/out/langPack.strings +++ b/src/scripts/out/langPack.strings @@ -48,6 +48,8 @@ "Contacts.Count_other" = "%d contacts"; "Deactivated.Title" = "Too many tabs..."; "Deactivated.Subtitle" = "Telegram supports only one active tab with the app.\nClick anywhere to continue using this tab."; +"Deactivated.Version.Title" = "WebK has updated..."; +"Deactivated.Version.Subtitle" = "Another tab is running a newer version of Telegram.\nClick anywhere to reload this tab."; "General.Keyboard" = "Keyboard"; "General.SendShortcut.Enter" = "Send by Enter"; "General.SendShortcut.CtrlEnter" = "Send by %s + Enter"; @@ -90,7 +92,6 @@ "Popup.Unpin.HideTitle" = "Hide pinned messages"; "Popup.Unpin.HideDescription" = "Do you want to hide the pinned message bar? It wil stay hidden until a new message is pinned."; "Popup.Unpin.Hide" = "Hide"; -"TwoStepAuth.InvalidPassword" = "Invalid password"; "TwoStepAuth.EmailCodeChangeEmail" = "Change Email"; "MarkupTooltip.LinkPlaceholder" = "Enter URL..."; "MediaViewer.Context.Download" = "Download"; @@ -109,6 +110,13 @@ "PushNotification.Action.Settings.Mobile" = "Alerts settings"; "PushNotification.Message.NoPreview" = "You have a new message"; "LogOut.Description" = "Are you sure you want to log out?\n\nNote that you can seamlessly use Telegram on all your devices at once."; +"VoiceChat.DiscussionGroup" = "discussion group"; +"PaymentInfo.CVV" = "CVV Code"; +"PaymentInfo.Card.Title" = "Enter your card information"; +"PaymentInfo.Billing.Title" = "Enter your billing address"; +"PaymentInfo.Done" = "PROCEED TO CHECKOUT"; +"PaymentCard.Error.Invalid" = "Invalid card number"; +"PaymentCard.Error.Incomplete" = "Incomplete card number"; "AccDescrEditing" = "Editing"; "ActionCreateChannel" = "Channel created"; "ActionCreateGroup" = "un1 created the group"; @@ -599,6 +607,67 @@ "ResetAutomaticMediaDownloadAlertTitle" = "Reset settings"; "ResetAutomaticMediaDownloadAlert" = "Are you sure you want to reset auto-download settings?"; "Reset" = "Reset"; +"SendMessageAsTitle" = "Send message as..."; +"Devices" = "Devices"; +"LanguageName" = "English"; +"EditCantEditPermissionsPublic" = "This permission is not available in public groups."; +"VoipUserMicrophoneIsOff" = "%s's microphone is off"; +"VoipUserCameraIsOff" = "%s's camera is off"; +"PrivacyPhoneInfo4" = "This public link opens a chat with you:"; +"ReportChatIllegalDrugs" = "Illegal Drugs"; +"ReportChatPersonalDetails" = "Personal Details"; +"VoipPeerIncompatible" = "**%1$s**'s app is using an incompatible protocol. They need to update their app before you can call them."; +"ScamMessage" = "SCAM"; +"FakeMessage" = "FAKE"; +"TextCopied" = "Text copied to clipboard"; +"PaymentInvoice" = "INVOICE"; +"PaymentTestInvoice" = "TEST INVOICE"; +"PaymentReceipt" = "Receipt"; +"PaymentSuccessfullyPaid" = "You successfully transferred %1$s to %2$s for %3$s"; +"PaymentSuccessfullyPaidNoItem" = "You successfully transferred %1$s to %2$s"; +"PaymentCheckout" = "Checkout"; +"PaymentTransactionTotal" = "Total"; +"PaymentTip" = "Tip"; +"PaymentTipOptional" = "Tip (Optional)"; +"PaymentCheckoutPay" = "PAY %1$s"; +"PaymentCheckoutMethod" = "Payment method"; +"PaymentCheckoutProvider" = "Payment provider"; +"PaymentCardNumber" = "Card Number"; +"PaymentCardSavePaymentInformation" = "Save Payment Information"; +"PaymentCardInfo" = "Payment info"; +"PaymentCardSavePaymentInformationInfoLine1" = "You can save your payment info for future use. It will be stored directly with the payment provider. Telegram has no access to your credit card data."; +"Done" = "Done"; +"PaymentShippingMethod" = "Shipping methods"; +"PaymentNoShippingMethod" = "Sorry, it is not possible to deliver to your address."; +"PaymentShippingInfo" = "Shipping Information"; +"PaymentShippingAddress" = "Shipping address"; +"PaymentShippingAddress1Placeholder" = "Address 1 (Street)"; +"PaymentShippingAddress2Placeholder" = "Address 2 (Street)"; +"PaymentShippingCityPlaceholder" = "City"; +"PaymentShippingStatePlaceholder" = "State"; +"PaymentShippingCountry" = "Country"; +"PaymentShippingZipPlaceholder" = "Postcode"; +"PaymentShippingReceiver" = "Receiver"; +"PaymentShippingName" = "Full Name"; +"PaymentShippingEmailPlaceholder" = "Email"; +"PaymentCheckoutPhoneNumber" = "Phone number"; +"PaymentCheckoutShippingMethod" = "Shipping method"; +"PaymentShippingSave" = "Save Shipping Information"; +"PaymentShippingSaveInfo" = "You can save your shipping info for future use."; +"PaymentInfoHint" = "You paid **%1$s** for **%2$s**."; +"PrivacyPayments" = "Payments"; +"PrivacyPaymentsClearInfo" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data."; +"PrivacyPaymentsClear" = "Clear Payment and Shipping Info"; +"PrivacyPaymentsClearAlertTitle" = "Clear payment info"; +"PrivacyPaymentsClearAlertText" = "Are you sure you want to clear your payment and shipping info?"; +"PrivacyPaymentsPaymentInfoCleared" = "Payment info cleared."; +"PrivacyPaymentsShippingInfoCleared" = "Shipping info cleared."; +"PrivacyPaymentsPaymentShippingCleared" = "Payment and shipping info cleared."; +"PrivacyClearShipping" = "Shipping info"; +"PrivacyClearPayment" = "Payment info"; +"Clear" = "Clear"; +"Save" = "Save"; +"PaymentCheckoutName" = "Name"; "AccountSettings.Filters" = "Chat Folders"; "AccountSettings.Notifications" = "Notifications and Sounds"; "AccountSettings.PrivacyAndSecurity" = "Privacy and Security"; @@ -705,6 +774,7 @@ "Chat.Send.WithoutSound" = "Send Without Sound"; "Chat.Send.SetReminder" = "Set a Reminder"; "Chat.Send.ScheduledMessage" = "Schedule Message"; +"Chat.SendAs.PersonalAccount" = "personal account"; "Chat.UnpinAllMessagesConfirmation_one" = "Do you want to unpin %d message in this chat?"; "Chat.UnpinAllMessagesConfirmation_other" = "Do you want to unpin all %d messages in this chat?"; "Chat.Message.Ad.Text" = "Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.\n\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:\n\n%@\n\nSponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\n\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate – together."; @@ -713,6 +783,12 @@ "Chat.Message.ViewBot" = "VIEW BOT"; "Chat.Message.ViewGroup" = "VIEW GROUP"; "Chat.Message.Sponsored.What" = "What are sponsored messages?"; +"Checkout.2FA.Text" = "Saving payment details is only available with 2-Step Verification."; +"Checkout.NewCard.CardholderNamePlaceholder" = "Cardholder Name"; +"Checkout.PasswordEntry.Title" = "Payment Confirmation"; +"Checkout.PasswordEntry.Pay" = "Pay"; +"Checkout.PasswordEntry.Text" = "Your card %@ is on file. To pay with this card, please enter your 2-Step-Verification password."; +"Checkout.WebConfirmation.Title" = "Complete Payment"; "ChatList.Context.Mute" = "Mute"; "ChatList.Context.Unmute" = "Unmute"; "ChatList.Context.Pin" = "Pin"; @@ -769,6 +845,7 @@ "Emoji.TravelAndPlaces" = "Travel & Places"; "Emoji.Objects" = "Objects"; "Emoji.Flags" = "Flags"; +"Error.AnError" = "An error occurred. Please try again later."; "FileSize.B" = "%@ B"; "FileSize.KB" = "%@ KB"; "FileSize.MB" = "%@ MB"; @@ -781,6 +858,7 @@ "Message.Context.Pin" = "Pin"; "Message.Context.Unpin" = "Unpin"; "Message.Context.Goto" = "Show Message"; +"Message.ReplyActionButtonShowReceipt" = "Show Receipt"; "MessageContext.CopyMessageLink1" = "Copy Message Link"; "Modal.Send" = "Send"; "NewPoll.Anonymous" = "Anonymous Voting"; @@ -890,6 +968,7 @@ "GeneralSettings.EmojiPrediction" = "Suggest Emoji"; "GroupPermission.Delete" = "Delete Exception"; "Search.Confirm.ClearHistory" = "Are you sure you want to clear your search history?"; +"SecureId.Identity.Placeholder.ExpiryDate" = "Expiry Date"; "Separator.ShowMore" = "show more"; "Separator.ShowLess" = "show less"; "ScheduleController.at" = "at"; @@ -946,7 +1025,6 @@ "VoiceChat.RemovePeer.Confirm" = "Are you sure you want to remove %1$@ from the group?"; "VoiceChat.RemovePeer.Confirm.OK" = "Remove"; "Login.Title" = "Sign in to Telegram"; -"Login.CountrySelectorLabel" = "Country"; "Login.PhoneLabel" = "Phone Number"; "Login.PhoneLabelInvalid" = "Phone Number Invalid"; "Login.KeepSigned" = "Keep me signed in"; @@ -965,6 +1043,7 @@ "FirstName" = "First name (required)"; "LastName" = "Last name (optional)"; "StartMessaging" = "Start Messaging"; +"Country" = "Country"; "Contacts.PhoneNumber.Placeholder" = "Phone Number"; "Login.Next" = "Next"; "Login.ContinueOnLanguage" = "Continue in English"; diff --git a/src/scss/components/_global.scss b/src/scss/components/_global.scss index d78621ec..835eb861 100644 --- a/src/scss/components/_global.scss +++ b/src/scss/components/_global.scss @@ -164,6 +164,10 @@ Utility Classes white-space: pre-wrap !important; } +.no-wrap { + white-space: nowrap !important; +} + .no-border-radius { border-radius: 0 !important; } diff --git a/src/scss/fonts/_roboto.scss b/src/scss/fonts/_roboto.scss index 41a73dd2..fe3b886c 100644 --- a/src/scss/fonts/_roboto.scss +++ b/src/scss/fonts/_roboto.scss @@ -57,3 +57,35 @@ src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2'); unicode-range:U + 0000-00FF, U + 0131, U + 0152-0153, U + 02BB-02BC, U + 02C6, U + 02DA, U + 02DC, U + 2000-206F, U + 2074, U + 20AC, U + 2122, U + 2191, U + 2193, U + 2212, U + 2215, U + FEFF, U + FFFD } + +// * fix bold formatting + +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fABc4AMP6lbBP.woff2) format('woff2'); + unicode-range:U + 0400-045F, U + 0490-0491, U + 04B0-04B1, U + 2116 +} + +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fChc4AMP6lbBP.woff2) format('woff2'); + unicode-range:U + 0100-024F, U + 0259, U + 1E00-1EFF, U + 2020, U + 20A0-20AB, U + 20AD-20CF, U + 2113, U + 2C60-2C7F, U + A720-A7FF +} + +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2'); + unicode-range:U + 0000-00FF, U + 0131, U + 0152-0153, U + 02BB-02BC, U + 02C6, U + 02DA, U + 02DC, U + 2000-206F, U + 2074, U + 20AC, U + 2122, U + 2191, U + 2193, U + 2212, U + 2215, U + FEFF, U + FFFD +} diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 5ab93280..bbc0649d 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -2799,6 +2799,7 @@ $bubble-beside-button-width: 38px; overflow: hidden; min-height: 2.5rem; display: flex; + border-radius: .375rem; &:last-child { border-bottom-left-radius: $border-radius-big; @@ -2808,7 +2809,7 @@ $bubble-beside-button-width: 38px; &-button { padding: .5625rem 0; - border-radius: 6px; + border-radius: inherit; z-index: 2; font-size: .875rem; user-select: none; diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index 132b71e4..d023b494 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -388,7 +388,7 @@ video::-webkit-media-controls-enclosure { } &.is-focused .progress-line__filled:not(.progress-line__loaded):after { - transform: translateX(calc(var(--thumb-size) / 2)) scale(1.25); + transform: translateX(calc(var(--thumb-size) / 2)) scale(1.125); } &__loaded, &:before { diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index 39cafd39..840149e2 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -5,6 +5,7 @@ */ .emoji-dropdown { + --menu-height: 3.0625rem; display: flex/* !important */; flex-direction: column; width: 100%; @@ -41,16 +42,16 @@ } } - /* @include respond-to(handhelds) { - width: calc(100% + 1rem); - margin-left: -.5rem; - } */ - .emoji-container { width: 100%; max-width: 100%; overflow: hidden; height: 100%; + + .menu-horizontal-div { + z-index: 4; + background-color: var(--surface-color); + } } .emoji-tabs { @@ -62,34 +63,43 @@ &-search { position: absolute; left: 0; - margin-left: 4px !important; + margin-left: .5rem !important; } &-delete { position: absolute; right: 0; - margin-right: 4px !important; + margin-right: .5rem !important; } + + .menu-horizontal-div-item { + margin: 0 1rem; + } } + + .category-title { + font-size: var(--font-size-14); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-14); + color: var(--secondary-text-color); + padding: .8125rem .875rem .6875rem; + width: 100%; + position: relative; + + .btn-icon { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + font-size: 1.25rem; + z-index: 1; + pointer-events: all; + } + } .tabs-container { - /* width: 300%; */ height: 100%; - .category-title { - //position: sticky; - top: 0; - //font-size: .85rem; - font-size: 14px; - font-weight: var(--font-weight-bold); - color: var(--secondary-text-color); - //background: linear-gradient(to bottom,#fff 0,rgba(255,255,255,.9) 60%,rgba(255,255,255,0) 100%); - z-index: 2; - //padding: .53333rem 6PX .66667rem; - padding: 12px 6px 6px 6px; - width: 100%; - } - .sticky_sentinel { &--top { top: 0; @@ -110,173 +120,79 @@ position: relative; background-color: var(--surface-color); } - - .scrollable { - padding: 0 10px; - } } - .emoji-padding.active { + .emoji-padding { + .super-emojis { + padding: 0 .5rem; + } + @include respond-to(handhelds) { - .menu-horizontal-div .menu-horizontal-div-item { + .menu-horizontal-div-item { flex: unset; padding: 0; } - - .category-items { - grid-template-columns: repeat(auto-fill, 40px); - - > span { - width: 40px; - height: 40px; - justify-self: center; - } - } - - .category-title { - padding: 12px 6px 6px 10px; - } - - .scrollable { - padding: 0; - } - - .emoji-category .category-items { - grid-column-gap: unset; - } } } - .emoji-padding, - .stickers-padding { - .menu-horizontal-div { - // padding: 2px; - z-index: 4; - background-color: var(--surface-color); - - .menu-horizontal-div-item { - margin: 0; - } - } - } - .emoji-category { - //padding-top: 1px; - position: relative; - margin: 0 -.125rem; - - /* &:first-child { - //padding-top: 5px; - } */ - - /* &::after { - content: ""; - flex: auto; - } */ - } - - .sticker-category { position: relative; - - .category-title { - cursor: pointer; - } - - &.stickers-recent { - .category-title { - pointer-events: none; - } - } - - /* &::after { - content: ""; - flex: auto; - } */ - - /* &.not-full::after { - content: ""; - flex: auto; - } */ - - .category-items { - width: 100%; - display: grid; - grid-template-columns: repeat(auto-fill, var(--esg-sticker-size)); // 64px - grid-column-gap: 1px; - justify-content: space-between; - - - } } - #content-stickers { - .scrollable { - padding: 0px 5px 0; - } - } - .menu-horizontal-div { width: 100%; - height: 48px; + height: var(--menu-height); + min-height: var(--menu-height); + align-items: center; - .menu-horizontal-div-item { + &-item { font-size: 1.5rem; - margin: 0 12px; - width: 48px; - height: 48px; - line-height: 48px; + width: 2.5rem; + height: 2.5rem; + line-height: 2.5rem; display: flex; align-items: center; flex: 0 0 auto; + padding: 0; } } - + .stickers-padding { - &.active { - .scrollable { - padding: 0; - //box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21); - } + .category-title { + cursor: pointer; + } - .menu-horizontal-div { - .menu-horizontal-div-item { - height: 48px; - width: 48px; - padding: 0; - margin-right: 1px; - margin-left: 1px; - } - } + .category-items { + padding: 0 .3125rem; } .menu-wrapper { padding: 0; - height: 48px; + height: var(--menu-height); max-width: 100%; position: relative; + border-bottom: 1px solid var(--border-color); + background-color: var(--surface-color); } - - .menu-horizontal-div-item { - /* width: calc(100% / 7); */ - flex: 0 0 auto; - - &.active { - &:not(.tgico-recent) { - background-color: var(--light-secondary-text-color); - } - } - - > canvas, > img { - //padding: .75rem; - padding: 8px; - max-width: 100%; - max-height: 100%; - } - > canvas { - width: 100%; - height: 100%; - } - } + .menu-horizontal-div { + &-item { + flex: 0 0 auto; + padding: .25rem; + margin: 0 .3125rem; + + &-padding { + width: 100%; + height: 100%; + position: relative; + } + + &.active { + &:not(.tgico-recent) { + background-color: var(--light-secondary-text-color); + } + } + } + } } } diff --git a/src/scss/partials/popups/_call.scss b/src/scss/partials/popups/_call.scss index ab42559b..1269e9b2 100644 --- a/src/scss/partials/popups/_call.scss +++ b/src/scss/partials/popups/_call.scss @@ -10,6 +10,7 @@ #{$parent} { &-header { width: 100%; + justify-content: space-between; } &-title { diff --git a/src/scss/partials/popups/_forward.scss b/src/scss/partials/popups/_forward.scss index 616fe769..1d21f26d 100644 --- a/src/scss/partials/popups/_forward.scss +++ b/src/scss/partials/popups/_forward.scss @@ -29,6 +29,7 @@ &-title { flex-grow: 1; + padding: 0; } } diff --git a/src/scss/partials/popups/_groupCall.scss b/src/scss/partials/popups/_groupCall.scss index b59056ca..f6c6b66a 100644 --- a/src/scss/partials/popups/_groupCall.scss +++ b/src/scss/partials/popups/_groupCall.scss @@ -74,6 +74,7 @@ &-title, &-subtitle { + font-size: var(--font-size-16); line-height: var(--line-height); @include text-overflow(); diff --git a/src/scss/partials/popups/_payment.scss b/src/scss/partials/popups/_payment.scss index 78425ccf..c0ce8914 100644 --- a/src/scss/partials/popups/_payment.scss +++ b/src/scss/partials/popups/_payment.scss @@ -84,7 +84,8 @@ .payment-verification { width: 100%; - min-height: 30rem; + height: 40rem; + max-height: 100%; border: none; flex: 1 1 auto; } diff --git a/src/scss/partials/popups/_popup.scss b/src/scss/partials/popups/_popup.scss index 7a6a9160..d985b230 100644 --- a/src/scss/partials/popups/_popup.scss +++ b/src/scss/partials/popups/_popup.scss @@ -55,7 +55,7 @@ &-title { flex: 1; - padding: 0 2rem 0 1.5rem; + padding: 0 1rem 0 1.5rem; margin: 0; font-size: 1.25rem; font-weight: var(--font-weight-bold); diff --git a/src/scss/style.scss b/src/scss/style.scss index e615624a..7f4b33e5 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -542,9 +542,9 @@ input, [contenteditable=true] { background-color: transparent; } -[contenteditable] [style*="bold"] { - font-weight: var(--font-weight-bold) !important; -} +// [contenteditable] [style*="bold"] { +// font-weight: var(--font-weight-bold) !important; +// } input, textarea { -webkit-appearance: none; @@ -969,13 +969,13 @@ img.emoji { width: 100%; display: grid; grid-template-columns: repeat(auto-fill, var(--esg-sticker-size)); // 64px - grid-column-gap: 1px; + // grid-column-gap: 1px; justify-content: space-between; } .super-sticker { @include hover-background-effect() { - border-radius: 10px; + border-radius: $border-radius-medium; } /* &:nth-child(5n+5) { @@ -1269,6 +1269,16 @@ middle-ellipsis-element { // } // } +.media-container-contain { + position: relative; + + .media-photo { + object-fit: contain; + max-width: 100%; + max-height: 100%; + } +} + .media-container-cover { position: relative; diff --git a/webpack.common.js b/webpack.common.js index 17325bc8..a89a9536 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -37,7 +37,7 @@ const opts = { 'version': 3, 'ifdef-verbose': devMode, // add this for verbose output 'ifdef-triple-slash': false, // add this to use double slash comment instead of default triple slash - 'ifdef-fill-with-blanks': true, // add this to remove code with blank spaces instead of "//" comments + 'ifdef-fill-with-blanks': true // add this to remove code with blank spaces instead of "//" comments }; const domain = 'yourdomain.com'; @@ -45,17 +45,17 @@ const localIp = '192.168.92.78'; const middleware = (req, res, next) => { let IP = ''; - if (req.headers['cf-connecting-ip']) { + if(req.headers['cf-connecting-ip']) { IP = req.headers['cf-connecting-ip']; } else { IP = req.connection.remoteAddress.split(':').pop(); } - if (!allowedIPs.includes(IP) && !/^192\.168\.\d{1,3}\.\d{1,3}$/.test(IP)) { + if(!allowedIPs.includes(IP) && !/^192\.168\.\d{1,3}\.\d{1,3}$/.test(IP)) { console.log('Bad IP connecting: ' + IP, req.url); res.status(404).send('Nothing interesting here.'); } else { - if (req.url.indexOf('/assets/') !== 0) { + if(req.url.indexOf('/assets/') !== 0) { console.log(req.url, IP); } @@ -83,8 +83,8 @@ module.exports = { { loader: 'css-loader', options: { - url: false, - }, + url: false + } }, devMode ? undefined : MediaQueryPlugin.loader, @@ -100,18 +100,18 @@ module.exports = { // Webpack 5 postcssOptions: { plugins: [ - postcssPresetEnv(), - ], - }, - }, + postcssPresetEnv() + ] + } + } }, { loader: 'sass-loader', options: { - sourceMap: devMode, - }, - }, - ].filter(Boolean), + sourceMap: devMode + } + } + ].filter(Boolean) }, // { // test: /\.worker\.ts$/i, @@ -125,26 +125,26 @@ module.exports = { use: [ // { loader: 'babel-loader', options: require('./babel.config') }, 'ts-loader', - {loader: 'ifdef-loader', options: opts}, + {loader: 'ifdef-loader', options: opts} ], - exclude: /node_modules/, + exclude: /node_modules/ }, { test: /\.hbs$/, loader: 'handlebars-loader', options: { - helperDirs: __dirname + '/handlebarsHelpers', - }, + helperDirs: __dirname + '/handlebarsHelpers' + } // loader: 'handlebars-loader?helperDirs[]=' + __dirname + '/handlebarsHelpers', // use: [ // 'handlebars-loader' // ] - }, - ], + } + ] }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js'] }, entry: './src/index.ts', @@ -166,8 +166,8 @@ module.exports = { // Webpack 5 clean: { - keep: keepAsset, - }, + keep: keepAsset + } }, devServer: { @@ -188,10 +188,10 @@ module.exports = { http2: useLocalNotLocal ? true : (useLocal ? undefined : true), https: useLocal ? undefined : { key: fs.readFileSync(__dirname + '/certs/server-key.pem', 'utf8'), - cert: fs.readFileSync(__dirname + '/certs/server-cert.pem', 'utf8'), + cert: fs.readFileSync(__dirname + '/certs/server-cert.pem', 'utf8') }, allowedHosts: useLocal ? undefined : [ - domain, + domain ], host: useLocalNotLocal ? localIp : (useLocal ? undefined : '0.0.0.0'), // host: domain, // '0.0.0.0' @@ -207,8 +207,8 @@ module.exports = { }, client: { overlay: true, - progress: false, - }, + progress: false + } }, plugins: [ @@ -216,7 +216,7 @@ module.exports = { analyzerMode: 'static', openAnalyzer: false, generateStatsFile: false, - defaultSizes: 'gzip', + defaultSizes: 'gzip' }), new Dotenv(), @@ -260,36 +260,36 @@ module.exports = { keepClosingSlash: true, minifyJS: true, minifyCSS: true, - minifyURLs: true, + minifyURLs: true }, chunks: 'all', - excludeChunks: [], + excludeChunks: [] }), new HtmlWebpackInjectPreload({ files: [ { match: /(mtproto).*\.js$/, - attributes: {rel: 'modulepreload'}, - }, - ], + attributes: {rel: 'modulepreload'} + } + ] }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: '[name].[contenthash].css', - chunkFilename: '[id].[contenthash].css', + chunkFilename: '[id].[contenthash].css' }), new MediaQueryPlugin({ include: [ - 'style', + 'style' ], queries: { 'only screen and (max-width: 720px)': 'mobile', - 'only screen and (min-width: 721px)': 'desktop', - }, + 'only screen and (min-width: 721px)': 'desktop' + } }), new RetryChunkLoadPlugin({ @@ -301,13 +301,13 @@ module.exports = { // optional value to set the amount of time in milliseconds before trying to load the chunk again. Default is 0 retryDelay: 3000, // optional value to set the maximum number of retries to load the chunk. Default is 1 - maxRetries: 999999, + maxRetries: 999999 // optional list of chunks to which retry script should be injected // if not set will add retry script to all chunks that have webpack script loading // chunks: ['chunkName'], // optional code to be executed in the browser context if after all retries chunk is not loaded. // if not set - nothing will happen and error will be returned to the chunk loader. // lastResortScript: "window.location.href='/500.html';", - }), - ].filter(Boolean), + }) + ].filter(Boolean) };