From 285e56f23359804c277a8a09bcacf50a006fd3af Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Mon, 14 Dec 2020 00:28:17 +0200 Subject: [PATCH] Handle RTL on search inputs Debounce pinned message animation Debounce lazy load queue --- src/components/appSearch.ts | 4 +- src/components/chat/bubbles.ts | 16 ++- src/components/chat/pinnedMessage.ts | 24 +++- src/components/chat/search.ts | 17 ++- src/components/emoticonsDropdown/index.ts | 5 +- src/components/inputField.ts | 48 ++++--- .../{searchInput.ts => inputSearch.ts} | 26 +++- src/components/lazyLoadQueue.ts | 11 +- src/components/scrollable.ts | 2 + src/components/sidebarLeft/index.ts | 18 +-- src/components/sidebarLeft/tabs/contacts.ts | 10 +- src/components/sidebarRight/index.ts | 9 +- src/components/sidebarRight/tabs/gifs.ts | 14 +- src/components/sidebarRight/tabs/search.ts | 10 +- src/components/sidebarRight/tabs/stickers.ts | 14 +- src/components/wrappers.ts | 4 +- src/helpers/schedulers.ts | 125 ++++++++++++++++++ src/index.hbs | 2 +- src/lib/mtproto/apiFileManager.ts | 4 +- src/lib/mtproto/tl_utils.ts | 12 +- src/pages/pageSignQR.ts | 3 +- src/scss/partials/_chatPinned.scss | 29 ++++ src/scss/partials/_chatlist.scss | 67 ---------- src/scss/partials/_input.scss | 83 +++++++++++- src/scss/style.scss | 13 +- src/types.d.ts | 6 + 26 files changed, 406 insertions(+), 170 deletions(-) rename src/components/{searchInput.ts => inputSearch.ts} (71%) create mode 100644 src/helpers/schedulers.ts diff --git a/src/components/appSearch.ts b/src/components/appSearch.ts index 2c6b56a9..fbf18b6a 100644 --- a/src/components/appSearch.ts +++ b/src/components/appSearch.ts @@ -6,7 +6,7 @@ import appPeersManager from '../lib/appManagers/appPeersManager'; import appMessagesManager from "../lib/appManagers/appMessagesManager"; import { formatPhoneNumber } from "./misc"; import appChatsManager from "../lib/appManagers/appChatsManager"; -import SearchInput from "./searchInput"; +import InputSearch from "./inputSearch"; import rootScope from "../lib/rootScope"; import { escapeRegExp } from "../helpers/string"; import searchIndexManager from "../lib/searchIndexManager"; @@ -81,7 +81,7 @@ export default class AppSearch { private scrollable: Scrollable; - constructor(public container: HTMLElement, public searchInput: SearchInput, public searchGroups: {[group in SearchGroupType]: SearchGroup}, public onSearch?: (count: number) => void) { + constructor(public container: HTMLElement, public searchInput: InputSearch, public searchGroups: {[group in SearchGroupType]: SearchGroup}, public onSearch?: (count: number) => void) { this.scrollable = new Scrollable(this.container); this.listsContainer = this.scrollable.container as HTMLDivElement; for(let i in this.searchGroups) { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 5f190a41..ac6faa30 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -35,6 +35,7 @@ import LazyLoadQueue from "../lazyLoadQueue"; import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; +import { pause } from "../../helpers/schedulers"; const IGNORE_ACTIONS = ['messageActionHistoryClear']; @@ -1122,10 +1123,13 @@ export default class ChatBubbles { if(!bubble?.parentElement) { bubble = this.findNextMountedBubbleByMsgId(lastMsgId); } - - this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */); - if(!forwardingUnread) { - this.highlightBubble(bubble); + + // ! sometimes there can be no bubble + if(bubble) { + this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */); + if(!forwardingUnread) { + this.highlightBubble(bubble); + } } } else { this.scrollable.scrollTop = this.scrollable.scrollHeight; @@ -1263,7 +1267,9 @@ export default class ChatBubbles { } }); - resolve(); + //setTimeout(() => { + resolve(); + //}, 500); this.messagesQueuePromise = null; }, reject); }, 0); diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index b4221374..d5cedf3f 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -11,6 +11,7 @@ import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; import ButtonIcon from "../buttonIcon"; +import { debounce } from "../../helpers/schedulers"; class AnimatedSuper { static DURATION = 200; @@ -94,6 +95,17 @@ class AnimatedSuper { row.element.classList.toggle('is-hiding', false); previousRow && previousRow.element.classList.toggle('is-hiding', true); + /* const height = row.element.getBoundingClientRect().height; + row.element.style.transform = `translateY(${fromTop ? height * -1 : height}px)`; + if(previousRow) { + previousRow.element.style.transform = `translateY(${fromTop ? height : height * -1}px)`; + } */ + + /* row.element.style.setProperty('--height', row.element.getBoundingClientRect().height + 'px'); + if(previousRow) { + previousRow.element.style.setProperty('--height', previousRow.element.getBoundingClientRect().height + 'px'); + } */ + this.clearRows(index); } } @@ -227,6 +239,8 @@ export default class ChatPinnedMessage { public getCurrentIndexPromise: Promise = null; public btnOpen: HTMLButtonElement; + public setPinnedMessage: () => void; + constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { this.listenerSetter = new ListenerSetter(); @@ -290,6 +304,10 @@ export default class ChatPinnedMessage { this.pinnedMessageContainer.toggle(this.hidden = true); } }); + + // * 200 - no lags + // * 100 - need test + this.setPinnedMessage = debounce(() => this._setPinnedMessage(), 100, true, true); } public destroy() { @@ -300,6 +318,8 @@ export default class ChatPinnedMessage { } public setCorrectIndex(lastScrollDirection?: number) { + //return; + if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) { return; } @@ -315,6 +335,8 @@ export default class ChatPinnedMessage { el = findUpClassName(el, 'bubble'); if(!el) return; + //return; + const mid = el.dataset.mid; if(el && mid !== undefined) { this.chat.log('[PM]: setCorrectIndex will test mid:', mid); @@ -517,7 +539,7 @@ export default class ChatPinnedMessage { /* || (!this.chatAudio.divAndCaption.container.classList.contains('hide') && to == ScreenSize.medium) */); } - public setPinnedMessage() { + public _setPinnedMessage() { /////this.log('setting pinned message', message); //return; /* const promise: Promise = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve(); diff --git a/src/components/chat/search.ts b/src/components/chat/search.ts index 8193ab80..1456e11e 100644 --- a/src/components/chat/search.ts +++ b/src/components/chat/search.ts @@ -1,16 +1,15 @@ import type ChatTopbar from "./topbar"; -import rootScope from "../../lib/rootScope"; import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom"; import AppSearch, { SearchGroup } from "../appSearch"; import PopupDatePicker from "../popupDatepicker"; import { ripple } from "../ripple"; -import SearchInput from "../searchInput"; +import InputSearch from "../inputSearch"; import type Chat from "./chat"; export default class ChatSearch { private element: HTMLElement; private backBtn: HTMLElement; - private searchInput: SearchInput; + private inputSearch: InputSearch; private results: HTMLElement; @@ -39,7 +38,7 @@ export default class ChatSearch { this.backBtn.addEventListener('click', () => { this.topbar.container.classList.remove('hide-pinned'); this.element.remove(); - this.searchInput.remove(); + this.inputSearch.remove(); this.results.remove(); this.footer.remove(); this.footer.removeEventListener('click', this.onFooterClick); @@ -50,7 +49,7 @@ export default class ChatSearch { this.chat.bubbles.bubblesContainer.classList.remove('search-results-active'); }, {once: true}); - this.searchInput = new SearchInput('Search'); + this.inputSearch = new InputSearch('Search'); // Results this.results = document.createElement('div'); @@ -59,13 +58,13 @@ export default class ChatSearch { this.searchGroup = new SearchGroup('', 'messages', undefined, '', false); this.searchGroup.list.addEventListener('click', this.onResultsClick); - this.appSearch = new AppSearch(this.results, this.searchInput, { + this.appSearch = new AppSearch(this.results, this.inputSearch, { messages: this.searchGroup }, (count) => { this.foundCount = count; if(!this.foundCount) { - this.foundCountEl.innerText = this.searchInput.value ? 'No results' : ''; + this.foundCountEl.innerText = this.inputSearch.value ? 'No results' : ''; this.results.classList.remove('active'); this.chat.bubbles.bubblesContainer.classList.remove('search-results-active'); this.upBtn.setAttribute('disabled', 'true'); @@ -113,12 +112,12 @@ export default class ChatSearch { this.topbar.container.parentElement.insertBefore(this.footer, chat.input.chatInput); // Append container - this.element.append(this.backBtn, this.searchInput.container); + this.element.append(this.backBtn, this.inputSearch.container); this.topbar.container.classList.add('hide-pinned'); this.topbar.container.parentElement.append(this.element); - this.searchInput.input.focus(); + this.inputSearch.input.focus(); } onDateClick = (e: MouseEvent) => { diff --git a/src/components/emoticonsDropdown/index.ts b/src/components/emoticonsDropdown/index.ts index 56133471..63c5c443 100644 --- a/src/components/emoticonsDropdown/index.ts +++ b/src/components/emoticonsDropdown/index.ts @@ -13,6 +13,7 @@ import StickyIntersector from "../stickyIntersector"; import EmojiTab from "./tabs/emoji"; import GifsTab from "./tabs/gifs"; import StickersTab from "./tabs/stickers"; +import { pause } from "../../helpers/schedulers"; export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; @@ -211,9 +212,7 @@ export class EmoticonsDropdown { appImManager.chat.input.saveScroll(); // @ts-ignore document.activeElement.blur(); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); + await pause(100); } } diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 711e1eee..3fde922f 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -34,6 +34,22 @@ let init = () => { init = null; }; +const checkAndSetRTL = (input: HTMLElement) => { + //const isEmpty = isInputEmpty(input); + //console.log('input', isEmpty); + + //const char = [...getRichValue(input)][0]; + const char = (input instanceof HTMLInputElement ? input.value : input.innerText)[0]; + let direction = 'ltr'; + if(char && checkRTL(char)) { + direction = 'rtl'; + } + + //console.log('RTL', direction, char); + + input.style.direction = direction; +}; + const InputField = (options: { placeholder?: string, label?: string, @@ -51,6 +67,7 @@ const InputField = (options: { const {placeholder, label, maxLength, showLengthOn, name, plainText} = options; + let input: HTMLElement; if(!plainText) { if(init) { init(); @@ -61,21 +78,9 @@ const InputField = (options: { ${label ? `` : ''} `; - const input = div.firstElementChild as HTMLElement; - const observer = new MutationObserver((mutationsList, observer) => { - //const isEmpty = isInputEmpty(input); - //console.log('input', isEmpty); - - //const char = [...getRichValue(input)][0]; - const char = input.innerText[0]; - let direction = 'ltr'; - if(char && checkRTL(char)) { - direction = 'rtl'; - } - - //console.log('RTL', direction, char); - - input.style.direction = direction; + input = div.firstElementChild as HTMLElement; + const observer = new MutationObserver(() => { + checkAndSetRTL(input); if(processInput) { processInput(); @@ -86,21 +91,23 @@ const InputField = (options: { observer.observe(input, {characterData: true, childList: true, subtree: true}); } else { div.innerHTML = ` - + ${label ? `` : ''} `; + + input = div.firstElementChild as HTMLElement; + input.addEventListener('input', () => checkAndSetRTL(input)); } let processInput: () => void; if(maxLength) { - const input = div.firstElementChild as HTMLInputElement; const labelEl = div.lastElementChild as HTMLLabelElement; let showingLength = false; processInput = () => { const wasError = input.classList.contains('error'); // * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol - const inputLength = plainText ? input.value.length : [...getRichValue(input)].length; + const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input)].length; const diff = maxLength - inputLength; const isError = diff < 0; input.classList.toggle('error', isError); @@ -117,7 +124,10 @@ const InputField = (options: { input.addEventListener('input', processInput); } - return {container: div, input: div.firstElementChild as HTMLInputElement}; + return { + container: div, + input: div.firstElementChild as HTMLInputElement + }; }; export default InputField; \ No newline at end of file diff --git a/src/components/searchInput.ts b/src/components/inputSearch.ts similarity index 71% rename from src/components/searchInput.ts rename to src/components/inputSearch.ts index f02fea68..2cedc8ff 100644 --- a/src/components/searchInput.ts +++ b/src/components/inputSearch.ts @@ -1,4 +1,7 @@ -export default class SearchInput { +//import { getRichValue } from "../helpers/dom"; +import InputField from "./inputField"; + +export default class InputSearch { public container: HTMLElement; public input: HTMLInputElement; public clearBtn: HTMLElement; @@ -8,15 +11,19 @@ export default class SearchInput { public onChange: (value: string) => void; constructor(placeholder: string, onChange?: (value: string) => void) { - this.container = document.createElement('div'); + const inputField = InputField({ + placeholder, + plainText: true + }); + + this.container = inputField.container; + this.container.classList.remove('input-field'); this.container.classList.add('input-search'); this.onChange = onChange; - this.input = document.createElement('input'); - this.input.type = 'text'; - this.input.placeholder = placeholder; - this.input.autocomplete = Math.random().toString(36).substring(7); + this.input = inputField.input; + this.input.classList.add('input-search-input'); const searchIcon = document.createElement('span'); searchIcon.classList.add('tgico', 'tgico-search'); @@ -33,7 +40,7 @@ export default class SearchInput { onInput = () => { if(!this.onChange) return; - let value = this.input.value; + let value = this.value; //this.input.classList.toggle('is-empty', !value.trim()); @@ -53,12 +60,17 @@ export default class SearchInput { get value() { return this.input.value; + //return getRichValue(this.input); } set value(value: string) { + //this.input.innerHTML = value; this.input.value = value; this.prevValue = value; clearTimeout(this.timeout); + + const event = new Event('input', {bubbles: true, cancelable: true}); + this.input.dispatchEvent(event); } public remove() { diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index e9233e24..389bcb65 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -1,3 +1,4 @@ +import { debounce } from "../helpers/schedulers"; import { logger, LogLevels } from "../lib/logger"; import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector"; @@ -22,8 +23,10 @@ export class LazyLoadQueueBase { protected unlockResolve: () => void = null; protected log = logger('LL', LogLevels.error); + protected processQueue: () => void; constructor(protected parallelLimit = PARALLEL_LIMIT) { + this.processQueue = debounce(() => this._processQueue(), 20, false, true); } public clear() { @@ -58,7 +61,7 @@ export class LazyLoadQueueBase { this.processQueue(); } - public async processItem(item: LazyLoadElementBase) { + protected async processItem(item: LazyLoadElementBase) { if(this.lockPromise) { return; } @@ -96,7 +99,7 @@ export class LazyLoadQueueBase { this.processQueue(); } - public async processQueue(item?: LazyLoadElementBase) { + protected _processQueue(item?: LazyLoadElementBase) { if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return; do { @@ -236,9 +239,9 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector { if(!inserted) return false; this.intersector.observe(el.div); - if(el.wasSeen) { + /* if(el.wasSeen) { this.processQueue(el); - } else if(!el.hasOwnProperty('wasSeen')) { + } else */if(!el.hasOwnProperty('wasSeen')) { el.wasSeen = false; } diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 7f179e65..6d3f3a0d 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -152,6 +152,8 @@ export default class Scrollable extends ScrollableBase { //this.log('onScroll call', this.onScrollMeasure); //} + //return; + if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp && !this.onAdditionalScroll)) return; this.onScrollMeasure = window.requestAnimationFrame(() => { this.onScrollMeasure = 0; diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 973fd2d4..d998a3c7 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -13,7 +13,7 @@ import AppSearch, { SearchGroup } from "../appSearch"; import "../avatar"; import { parseMenuButtonsTo } from "../misc"; import { ScrollableX } from "../scrollable"; -import SearchInput from "../searchInput"; +import InputSearch from "../inputSearch"; import SidebarSlider from "../slider"; import { TransitionSlider } from "../transition"; import AppAddMembersTab from "./tabs/addMembers"; @@ -81,7 +81,7 @@ export class AppSidebarLeft extends SidebarSlider { private backBtn: HTMLButtonElement; private searchContainer: HTMLDivElement; //private searchInput = document.getElementById('global-search') as HTMLInputElement; - private searchInput: SearchInput; + private inputSearch: InputSearch; private menuEl: HTMLElement; private buttons: { @@ -140,9 +140,9 @@ export class AppSidebarLeft extends SidebarSlider { //this._selectTab(0); // make first tab as default - this.searchInput = new SearchInput('Telegram Search'); + this.inputSearch = new InputSearch('Telegram Search'); const sidebarHeader = this.sidebarEl.querySelector('.item-main .sidebar-header'); - sidebarHeader.append(this.searchInput.container); + sidebarHeader.append(this.inputSearch.container); this.toolsBtn = this.sidebarEl.querySelector('.sidebar-tools-button') as HTMLButtonElement; this.backBtn = this.sidebarEl.querySelector('.sidebar-back-button') as HTMLButtonElement; @@ -161,7 +161,7 @@ export class AppSidebarLeft extends SidebarSlider { this.menuEl = this.toolsBtn.querySelector('.btn-menu'); this.newBtnMenu = this.sidebarEl.querySelector('#new-menu'); - this.searchInput.input.addEventListener('focus', () => { + this.inputSearch.input.addEventListener('focus', () => { this.searchGroups = { //saved: new SearchGroup('', 'contacts'), contacts: new SearchGroup('Chats', 'contacts'), @@ -171,8 +171,8 @@ export class AppSidebarLeft extends SidebarSlider { recent: new SearchGroup('Recent', 'contacts', false, 'search-group-recent') }; - this.globalSearch = new AppSearch(this.searchContainer, this.searchInput, this.searchGroups, (count) => { - if(!count && !this.searchInput.value.trim()) { + this.globalSearch = new AppSearch(this.searchContainer, this.inputSearch, this.searchGroups, (count) => { + if(!count && !this.inputSearch.value.trim()) { this.globalSearch.reset(); this.searchGroups.people.toggle(); this.renderRecentSearch(); @@ -263,7 +263,7 @@ export class AppSidebarLeft extends SidebarSlider { }; let firstTime = true; - this.searchInput.input.addEventListener('focus', onFocus); + this.inputSearch.input.addEventListener('focus', onFocus); onFocus(); this.backBtn.addEventListener('click', (e) => { @@ -339,7 +339,7 @@ export class AppSidebarLeft extends SidebarSlider { this.recentSearchLoaded = true; } - if(this.searchInput.value.trim()) { + if(this.inputSearch.value.trim()) { return; } diff --git a/src/components/sidebarLeft/tabs/contacts.ts b/src/components/sidebarLeft/tabs/contacts.ts index ddf14b72..5ecc1f36 100644 --- a/src/components/sidebarLeft/tabs/contacts.ts +++ b/src/components/sidebarLeft/tabs/contacts.ts @@ -5,7 +5,7 @@ import appUsersManager from "../../../lib/appManagers/appUsersManager"; import appPhotosManager from "../../../lib/appManagers/appPhotosManager"; import appSidebarLeft, { AppSidebarLeft } from ".."; import rootScope from "../../../lib/rootScope"; -import SearchInput from "../../searchInput"; +import InputSearch from "../../inputSearch"; // TODO: поиск по людям глобальный, если не нашло в контактах никого @@ -15,7 +15,7 @@ export default class AppContactsTab implements SliderTab { private scrollable: Scrollable; private promise: Promise; - private searchInput: SearchInput; + private inputSearch: InputSearch; init() { this.container = document.getElementById('contacts-container'); @@ -24,12 +24,12 @@ export default class AppContactsTab implements SliderTab { appDialogsManager.setListClickListener(this.list); this.scrollable = new Scrollable(this.list.parentElement); - this.searchInput = new SearchInput('Search', (value) => { + this.inputSearch = new InputSearch('Search', (value) => { this.list.innerHTML = ''; this.openContacts(value); }); - this.container.firstElementChild.append(this.searchInput.container); + this.container.firstElementChild.append(this.inputSearch.container); // preload contacts // appUsersManager.getContacts(); @@ -43,7 +43,7 @@ export default class AppContactsTab implements SliderTab { public onCloseAfterTimeout() { this.list.innerHTML = ''; - this.searchInput.value = ''; + this.inputSearch.value = ''; } public openContacts(query?: string) { diff --git a/src/components/sidebarRight/index.ts b/src/components/sidebarRight/index.ts index d8af3b33..0cf6467a 100644 --- a/src/components/sidebarRight/index.ts +++ b/src/components/sidebarRight/index.ts @@ -8,6 +8,7 @@ import AppPrivateSearchTab from "./tabs/search"; import AppSharedMediaTab from "./tabs/sharedMedia"; //import AppForwardTab from "./tabs/forward"; import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; +import { pause } from "../../helpers/schedulers"; export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown'; @@ -111,14 +112,10 @@ export class AppSidebarRight extends SidebarSlider { //if(mediaSizes.isMobile) { //appImManager._selectTab(active ? 1 : 2); appImManager.selectTab(active ? 1 : 2); - return new Promise(resolve => { - setTimeout(resolve, mediaSizes.isMobile ? 250 : 200); // delay of slider animation - }); + return pause(mediaSizes.isMobile ? 250 : 200); // delay of slider animation //} - return new Promise(resolve => { - setTimeout(resolve, 200); // delay for third column open - }); + return pause(200); // delay for third column open //return Promise.resolve(); /* return new Promise((resolve, reject) => { diff --git a/src/components/sidebarRight/tabs/gifs.ts b/src/components/sidebarRight/tabs/gifs.ts index dca94c7b..b3f6cba0 100644 --- a/src/components/sidebarRight/tabs/gifs.ts +++ b/src/components/sidebarRight/tabs/gifs.ts @@ -1,5 +1,5 @@ import { SliderTab } from "../../slider"; -import SearchInput from "../../searchInput"; +import InputSearch from "../../inputSearch"; import Scrollable from "../../scrollable"; import animationIntersector from "../../animationIntersector"; import appSidebarRight, { AppSidebarRight } from ".."; @@ -18,7 +18,7 @@ export default class AppGifsTab implements SliderTab { private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement; private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement; //private input = this.container.querySelector('#stickers-search') as HTMLInputElement; - private searchInput: SearchInput; + private inputSearch: InputSearch; private gifsDiv = this.contentDiv.firstElementChild as HTMLDivElement; private scrollable: Scrollable; @@ -35,14 +35,14 @@ export default class AppGifsTab implements SliderTab { this.masonry = new GifsMasonry(this.gifsDiv, ANIMATIONGROUP, this.scrollable); - this.searchInput = new SearchInput('Search GIFs', (value) => { + this.inputSearch = new InputSearch('Search GIFs', (value) => { this.reset(); this.search(value); }); this.gifsDiv.addEventListener('click', this.onGifsClick); - this.backBtn.parentElement.append(this.searchInput.container); + this.backBtn.parentElement.append(this.inputSearch.container); } onGifsClick = (e: MouseEvent) => { @@ -66,7 +66,7 @@ export default class AppGifsTab implements SliderTab { public onCloseAfterTimeout() { this.reset(); this.gifsDiv.innerHTML = ''; - this.searchInput.value = ''; + this.inputSearch.value = ''; animationIntersector.checkAnimations(undefined, ANIMATIONGROUP); } @@ -86,7 +86,7 @@ export default class AppGifsTab implements SliderTab { this.reset(); this.scrollable.onScrolledBottom = () => { - this.search(this.searchInput.value, false); + this.search(this.inputSearch.value, false); }; }); } @@ -102,7 +102,7 @@ export default class AppGifsTab implements SliderTab { this.searchPromise = appInlineBotsManager.getInlineResults(0, this.gifBotPeerId, query, this.nextOffset); const { results, next_offset } = await this.searchPromise; - if(this.searchInput.value != query) { + if(this.inputSearch.value != query) { return; } diff --git a/src/components/sidebarRight/tabs/search.ts b/src/components/sidebarRight/tabs/search.ts index 1676be19..cd6955ab 100644 --- a/src/components/sidebarRight/tabs/search.ts +++ b/src/components/sidebarRight/tabs/search.ts @@ -1,13 +1,13 @@ import appSidebarRight, { AppSidebarRight } from ".."; import AppSearch, { SearchGroup } from "../../appSearch"; -import SearchInput from "../../searchInput"; +import InputSearch from "../../inputSearch"; import { SliderTab } from "../../slider"; export default class AppPrivateSearchTab implements SliderTab { public container: HTMLElement; public closeBtn: HTMLElement; - private searchInput: SearchInput; + private inputSearch: InputSearch; private appSearch: AppSearch; private peerId = 0; @@ -24,9 +24,9 @@ export default class AppPrivateSearchTab implements SliderTab { public init() { this.container = document.getElementById('search-private-container'); this.closeBtn = this.container.querySelector('.sidebar-close-button'); - this.searchInput = new SearchInput('Search'); - this.closeBtn.parentElement.append(this.searchInput.container); - this.appSearch = new AppSearch(this.container.querySelector('.chatlist-container'), this.searchInput, { + this.inputSearch = new InputSearch('Search'); + this.closeBtn.parentElement.append(this.inputSearch.container); + this.appSearch = new AppSearch(this.container.querySelector('.chatlist-container'), this.inputSearch, { messages: new SearchGroup('Private Search', 'messages') }); } diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index dfcd24ec..6189e22b 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -1,5 +1,5 @@ import { SliderTab } from "../../slider"; -import SearchInput from "../../searchInput"; +import InputSearch from "../../inputSearch"; import Scrollable from "../../scrollable"; import LazyLoadQueue from "../../lazyLoadQueue"; import { findUpClassName } from "../../../helpers/dom"; @@ -17,7 +17,7 @@ export default class AppStickersTab implements SliderTab { private contentDiv = this.container.querySelector('.sidebar-content') as HTMLDivElement; private backBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement; //private input = this.container.querySelector('#stickers-search') as HTMLInputElement; - private searchInput: SearchInput; + private inputSearch: InputSearch; private setsDiv = this.contentDiv.firstElementChild as HTMLDivElement; private scrollable: Scrollable; private lazyLoadQueue: LazyLoadQueue; @@ -27,11 +27,11 @@ export default class AppStickersTab implements SliderTab { this.lazyLoadQueue = new LazyLoadQueue(); - this.searchInput = new SearchInput('Search Stickers', (value) => { + this.inputSearch = new InputSearch('Search Stickers', (value) => { this.search(value); }); - this.backBtn.parentElement.append(this.searchInput.container); + this.backBtn.parentElement.append(this.inputSearch.container); this.setsDiv.addEventListener('click', (e) => { const sticker = findUpClassName(e.target, 'sticker-set-sticker'); @@ -76,7 +76,7 @@ export default class AppStickersTab implements SliderTab { public onCloseAfterTimeout() { this.setsDiv.innerHTML = ''; - this.searchInput.value = ''; + this.inputSearch.value = ''; animationIntersector.checkAnimations(undefined, 'STICKERS-SEARCH'); } @@ -188,7 +188,7 @@ export default class AppStickersTab implements SliderTab { public renderFeatured() { return appStickersManager.getFeaturedStickers().then(coveredSets => { - if(this.searchInput.value) { + if(this.inputSearch.value) { return; } @@ -225,7 +225,7 @@ export default class AppStickersTab implements SliderTab { } return appStickersManager.searchStickerSets(query, false).then(coveredSets => { - if(this.searchInput.value != query) { + if(this.inputSearch.value != query) { return; } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index fe3d15ec..45b2aa81 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -572,7 +572,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT }); }; - return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load: load, wasSeen: true}), Promise.resolve()); + return cacheContext.downloaded || !lazyLoadQueue ? load() : (lazyLoadQueue.push({div: container, load/* : load, wasSeen: true */}), Promise.resolve()); } export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: { @@ -813,7 +813,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o } }; - return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load, wasSeen: group == 'chat' && stickerType != 2}), Promise.resolve()) : load(); + return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load/* , wasSeen: group == 'chat' && stickerType != 2 */}), Promise.resolve()) : load(); } export function wrapReply(title: string, subtitle: string, message?: any) { diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts new file mode 100644 index 00000000..8d1a5e7d --- /dev/null +++ b/src/helpers/schedulers.ts @@ -0,0 +1,125 @@ +// * Jolly Cobra's schedulers +import { AnyToVoidFunction } from "../types"; + +//type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow; + +export function debounce( + fn: F, + ms: number, + shouldRunFirst = true, + shouldRunLast = true, +) { + let waitingTimeout: number | null = null; + + return (...args: Parameters) => { + if(waitingTimeout) { + clearTimeout(waitingTimeout); + waitingTimeout = null; + } else if(shouldRunFirst) { + // @ts-ignore + fn(...args); + } + + waitingTimeout = window.setTimeout(() => { + if(shouldRunLast) { + // @ts-ignore + fn(...args); + } + + waitingTimeout = null; + }, ms); + }; +} + +/* export function throttle( + fn: F, + ms: number, + shouldRunFirst = true, +) { + let interval: number | null = null; + let isPending: boolean; + let args: Parameters; + + return (..._args: Parameters) => { + isPending = true; + args = _args; + + if (!interval) { + if (shouldRunFirst) { + isPending = false; + // @ts-ignore + fn(...args); + } + + interval = window.setInterval(() => { + if (!isPending) { + window.clearInterval(interval!); + interval = null; + return; + } + + isPending = false; + // @ts-ignore + fn(...args); + }, ms); + } + }; +} */ + +/* export function throttleWithRaf(fn: F) { + return throttleWith(fastRaf, fn); +} + +export function throttleWithTickEnd(fn: F) { + return throttleWith(onTickEnd, fn); +} + +export function throttleWithNow(fn: F) { + return throttleWith(runNow, fn); +} + +export function throttleWith(schedulerFn: Scheduler, fn: F) { + let waiting = false; + let args: Parameters; + + return (..._args: Parameters) => { + args = _args; + + if (!waiting) { + waiting = true; + + schedulerFn(() => { + waiting = false; + // @ts-ignore + fn(...args); + }); + } + }; +} + +export function onTickEnd(cb: NoneToVoidFunction) { + Promise.resolve().then(cb); +} + +function runNow(fn: NoneToVoidFunction) { + fn(); +} */ + +export const pause = (ms: number) => new Promise((resolve) => { + setTimeout(resolve, ms); +}); + +/* let fastRafCallbacks: NoneToVoidFunction[] | undefined; +export function fastRaf(callback: NoneToVoidFunction) { + if (!fastRafCallbacks) { + fastRafCallbacks = [callback]; + + requestAnimationFrame(() => { + const currentCallbacks = fastRafCallbacks!; + fastRafCallbacks = undefined; + currentCallbacks.forEach((cb) => cb()); + }); + } else { + fastRafCallbacks.push(callback); + } +} */ diff --git a/src/index.hbs b/src/index.hbs index 218780eb..30c27e95 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -5,7 +5,7 @@ Telegram Web - + diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 553212f7..97abc947 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -210,9 +210,9 @@ export class ApiFileManager { this.log('downloadFile', fileName, size, location, options.mimeType, process); - if(options.queueId) { + /* if(options.queueId) { this.log.error('downloadFile queueId:', fileName, options.queueId); - } + } */ if(cachedPromise) { //this.log('downloadFile cachedPromise'); diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index e28fb014..65200b93 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -699,7 +699,17 @@ class TLDeserialization { } if(!constructorData) { - throw new Error('Constructor not found: ' + constructor + ' ' + this.fetchInt() + ' ' + this.fetchInt() + ' ' + field); + console.error('Constructor not found:', constructor); + + let int1: number, int2: number; + try { + int1 = this.fetchInt(field); + int2 = this.fetchInt(field); + } catch(err) { + + } + + throw new Error('Constructor not found: ' + constructor + ' ' + int1 + ' ' + int2 + ' ' + field); } } diff --git a/src/pages/pageSignQR.ts b/src/pages/pageSignQR.ts index 0c135675..b9e86ad5 100644 --- a/src/pages/pageSignQR.ts +++ b/src/pages/pageSignQR.ts @@ -8,6 +8,7 @@ import { App } from '../lib/mtproto/mtproto_config'; import serverTimeManager from '../lib/mtproto/serverTimeManager'; import { AuthAuthorization, AuthLoginToken } from '../layer'; import { bytesCmp, bytesToBase64 } from '../helpers/bytes'; +import { pause } from '../helpers/schedulers'; let onFirstMount = async() => { const pageElement = page.pageEl; @@ -102,7 +103,7 @@ let onFirstMount = async() => { let timestamp = Date.now() / 1000; let diff = loginToken.expires - timestamp - serverTimeManager.serverTimeOffset; - await new Promise((resolve, reject) => setTimeout(resolve, diff > 5 ? 5e3 : 1e3 * diff | 0)); + await pause(diff > 5 ? 5e3 : 1e3 * diff | 0); } catch(err) { switch(err.type) { case 'SESSION_PASSWORD_NEEDED': diff --git a/src/scss/partials/_chatPinned.scss b/src/scss/partials/_chatPinned.scss index 8390f6cf..cc6b8074 100644 --- a/src/scss/partials/_chatPinned.scss +++ b/src/scss/partials/_chatPinned.scss @@ -337,6 +337,35 @@ } } + .animated-super-row { + --translateY: 16px; + } + + .pinned-message-media { + --translateY: 32px; + } + + /* .animated-super-row.is-hiding { + &.from-top { + transform: translateY(-16px); + } + + &.from-bottom { + transform: translateY(16px); + } + } + + .pinned-message-media.is-hiding { + &.from-top { + transform: translateY(-32px); + } + + &.from-bottom { + transform: translateY(32px); + } + } */ + + &.hide ~ .tgico-pinlist, &:not(.is-many) ~ .tgico-pinlist { display: none; } diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 63f37f81..5bb37486 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -13,73 +13,6 @@ } } - .input-search { - position: relative; - width: 100%; - //Vozmojno nado budet vernut margin-left: 22px;, tak kak eto vrode v levom bare tak po verstke, a v pravom bare dlya mobili nado 16, gde stiker seti - margin-left: 22px; - margin-right: 4px; - - @include respond-to(handhelds) { - margin-left: 16px; - } - - input { - --border-width: 1px; - background-color: var(--color-gray-hover); - height: 40px; - border-radius: 22px; - border: var(--border-width) solid transparent; - box-sizing: border-box; - padding: 0px calc(1.5rem - var(--border-width)) 0 calc(42px - var(--border-width)); - transition: background-color .15s ease-in-out, border-color .15s ease-in-out; - width: 100%; - font-size: 16px; - - &:hover { - border-color: var(--color-gray); - } - - &:focus { - --border-width: 2px; - background-color: transparent; - border-color: $button-primary-background; - - & + .tgico { - color: $button-primary-background; - opacity: 1; - } - } - } - - .tgico { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - text-align: center; - font-size: 24px; - color: $color-gray; - opacity: .6; - transition: all .15s ease-out; - - &:before { - vertical-align: middle; - } - } - - .tgico-close { - left: auto; - right: 0px; - top: 48%; - } - - //input.is-empty ~ .tgico-close { - input:placeholder-shown ~ .tgico-close { - display: none; - } - } - ul { margin: 0; //padding: 0 .5rem; diff --git a/src/scss/partials/_input.scss b/src/scss/partials/_input.scss index 04e1c06b..9a19b66b 100644 --- a/src/scss/partials/_input.scss +++ b/src/scss/partials/_input.scss @@ -47,15 +47,17 @@ } input, &-input { + --height: 54px; + --padding: 1rem; --border-width: 1px; --border-width-top: 2px; border: var(--border-width) solid #DADCE0; border-radius: $border-radius-medium; //padding: 0 1rem; - padding: calc(1rem - var(--border-width-top)) calc(1rem - var(--border-width)); + padding: calc(var(--padding) - var(--border-width-top)) calc(var(--padding) - var(--border-width)); box-sizing: border-box; width: 100%; - min-height: 54px; + min-height: var(--height); transition: .2s border-color; position: relative; z-index: 1; @@ -126,7 +128,7 @@ transform: none; padding: 0 5px; left: .75rem; - font-size: 0.75rem!important; + font-size: .75rem!important; //color: #666; opacity: 1; } @@ -143,11 +145,11 @@ } :-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: #a2acb4; + color: #909192; } ::-ms-input-placeholder { /* Microsoft Edge */ - color: #a2acb4; + color: #909192; } input:focus, button:focus { @@ -180,4 +182,75 @@ input:focus, button:focus { 100% { transform: translateX(0); } +} + +.input-search { + position: relative; + width: 100%; + //Vozmojno nado budet vernut margin-left: 22px;, tak kak eto vrode v levom bare tak po verstke, a v pravom bare dlya mobili nado 16, gde stiker seti + margin-left: 22px; + margin-right: 4px; + overflow: hidden; + + @include respond-to(handhelds) { + margin-left: 16px; + } + + &-input { + --height: 40px; + background-color: var(--color-gray-hover); + padding: 0px calc(42px - var(--border-width)); + height: var(--height); + max-height: var(--height); + //line-height: calc(var(--height) + 2px - var(--border-width) * 2); + border-radius: 22px; + transition: background-color .2s ease-in-out, border-color .2s ease-in-out; + border-color: transparent; + + &:hover { + border-color: var(--color-gray); + } + + &:focus { + --border-width: 2px; + background-color: transparent; + border-color: $button-primary-background; + + & + .tgico { + color: $button-primary-background; + opacity: 1; + } + } + + /* &:empty:before { + color: #909192 !important; + } */ + + /* &:empty ~ .tgico-close, */&:placeholder-shown ~ .tgico-close { + display: none; + } + } + + .tgico { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + text-align: center; + font-size: 24px; + color: $color-gray; + opacity: .6; + transition: all .2s ease-out; + + &:before { + vertical-align: middle; + } + } + + .tgico-close { + left: auto; + right: 0px; + top: 48%; + z-index: 1; + } } \ No newline at end of file diff --git a/src/scss/style.scss b/src/scss/style.scss index a17152c4..3e19e70b 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -870,6 +870,7 @@ img.emoji { .animated-super { &-row { + --translateY: 100%; position: absolute; left: 0; top: 0; @@ -877,15 +878,23 @@ img.emoji { bottom: 0; transition: transform var(--pm-transition), opacity var(--pm-transition); + /* &:not(.is-hiding) { + transform: none !important; + } */ + &.is-hiding { opacity: 0; &.from-top { - transform: translateY(-100%); + transform: translate3d(0, calc(var(--translateY) * -1), 0); + //transform: translateY(calc(var(--translateY) * -1)); + //transform: translateY(-100%); } &.from-bottom { - transform: translateY(100%); + transform: translate3d(0, var(--translateY), 0); + //transform: translateY(var(--translateY)); + //transform: translateY(100%); } /* &.backwards { diff --git a/src/types.d.ts b/src/types.d.ts index 68f7d112..996927f1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -35,6 +35,12 @@ export type Modify = Omit & R; export type ArgumentTypes = F extends (...args: infer A) => any ? A : never; +export type AnyLiteral = Record; +export type AnyClass = new (...args: any[]) => any; +export type AnyFunction = (...args: any) => any; +export type AnyToVoidFunction = (...args: any) => void; +export type NoneToVoidFunction = () => void; + export type AuthState = AuthState.signIn | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn; export namespace AuthState { export type signIn = {