From ae193a10dbbe9da05c86d5e4c56bd450f3c1c1be Mon Sep 17 00:00:00 2001 From: morethanwords Date: Mon, 21 Sep 2020 20:34:19 +0300 Subject: [PATCH] Global search 'saved messages' Fix blinking down arrown Lazy load queue fixes GIFs for Chrome --- src/components/animationIntersector.ts | 6 + src/components/appSearch.ts | 67 +++-- src/components/emoticonsDropdown/tabs/gifs.ts | 4 +- .../emoticonsDropdown/tabs/stickers.ts | 24 -- src/components/gifsMasonry.ts | 212 ++++++++++------ src/components/lazyLoadQueue.ts | 230 +++++++++++------- src/components/scrollable_new.ts | 24 +- src/components/sidebarLeft/contacts.ts | 10 +- src/components/sidebarRight/gifs.ts | 12 +- src/components/wrappers.ts | 16 +- src/lib/appManagers/AppInlineBotsManager.ts | 2 +- src/lib/appManagers/appImManager.ts | 3 +- src/lib/appManagers/appMediaViewer.ts | 10 +- src/lib/appManagers/appPeersManager.ts | 10 +- src/lib/appManagers/appSidebarLeft.ts | 1 + src/lib/appManagers/appUsersManager.ts | 4 +- 16 files changed, 403 insertions(+), 232 deletions(-) diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index 6e897163..1c8ae277 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -100,6 +100,12 @@ export class AnimationIntersector { if((destroy || (!isInDOM(el) && !this.lockedGroups[group]))/* && false */) { //console.log('destroy animation'); animation.remove(); + + if(animation instanceof HTMLVideoElement) { + animation.src = ''; + animation.load(); + } + for(const group in this.byGroups) { this.byGroups[group].findAndSplice(p => p == player); } diff --git a/src/components/appSearch.ts b/src/components/appSearch.ts index e4174aa7..414c29e4 100644 --- a/src/components/appSearch.ts +++ b/src/components/appSearch.ts @@ -4,10 +4,11 @@ import appMessagesIDsManager from "../lib/appManagers/appMessagesIDsManager"; import appUsersManager from "../lib/appManagers/appUsersManager"; import appPeersManager from '../lib/appManagers/appPeersManager'; import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { escapeRegExp } from "../lib/utils"; +import { $rootScope, escapeRegExp } from "../lib/utils"; import { formatPhoneNumber } from "./misc"; import appChatsManager from "../lib/appManagers/appChatsManager"; import SearchInput from "./searchInput"; +import { Peer } from "../layer"; export class SearchGroup { container: HTMLDivElement; @@ -48,6 +49,11 @@ export class SearchGroup { } } +/** + * * Saved будет использована только для вывода одного элемента - избранное + */ +type SearchGroupType = 'saved' | 'contacts' | 'globalContacts' | 'messages' | string; + export default class AppSearch { private minMsgID = 0; private loadedCount = -1; @@ -66,15 +72,15 @@ export default class AppSearch { private scrollable: Scrollable; - constructor(public container: HTMLElement, public searchInput: SearchInput, public searchGroups: {[group: string]: SearchGroup}, public onSearch?: (count: number) => void) { + constructor(public container: HTMLElement, public searchInput: SearchInput, 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) { - this.listsContainer.append(this.searchGroups[i].container); + this.listsContainer.append(this.searchGroups[i as SearchGroupType].container); } - if(this.searchGroups['messages']) { - this.scrollable.setVirtualContainer(this.searchGroups['messages'].list); + if(this.searchGroups.messages) { + this.scrollable.setVirtualContainer(this.searchGroups.messages.list); } this.searchInput.onChange = (value) => { @@ -92,7 +98,7 @@ export default class AppSearch { if(!this.query.trim()) return; if(!this.searchTimeout) { - this.searchTimeout = setTimeout(() => { + this.searchTimeout = window.setTimeout(() => { this.searchMore(); this.searchTimeout = 0; }, 0); @@ -114,7 +120,7 @@ export default class AppSearch { this.loadedContacts = false; for(let i in this.searchGroups) { - this.searchGroups[i].clear(); + this.searchGroups[i as SearchGroupType].clear(); } this.searchPromise = null; @@ -127,6 +133,13 @@ export default class AppSearch { this.searchInput.input.focus(); } + + private renderSaved() { + const group = this.searchGroups.contacts; + let {dialog, dom} = appDialogsManager.addDialog($rootScope.myID, group.list, false); + dom.lastMessageSpan.innerHTML = 'chat with yourself'; + group.setActive(); + } public searchMore() { if(this.searchPromise) return this.searchPromise; @@ -145,18 +158,40 @@ export default class AppSearch { const maxID = appMessagesIDsManager.getMessageIDInfo(this.minMsgID)[0] || 0; if(!this.peerID && !maxID && !this.loadedContacts) { - appUsersManager.searchContacts(query, 20).then((contacts: any) => { + let renderedSaved = false; + if('saved messages'.includes(query.toLowerCase()) + || appUsersManager.getUser($rootScope.myID).sortName.includes(query.toLowerCase())/* && this.searchGroups.hasOwnProperty('saved') */) { + this.renderSaved(); + renderedSaved = true; + } + + appUsersManager.searchContacts(query, 20).then((contacts) => { if(this.searchInput.value != query) { return; } this.loadedContacts = true; - ///////this.log('input search contacts result:', contacts); + // set saved message as first peer to render + const peer = contacts.my_results.findAndSplice(p => (p as Peer.peerUser).user_id == $rootScope.myID); + if(peer) { + contacts.my_results.unshift(peer); + } - let setResults = (results: any, group: SearchGroup, showMembersCount = false) => { - results.forEach((inputPeer: any) => { + //console.log('input search contacts result:', contacts); + + let setResults = (results: Peer[], group: SearchGroup, showMembersCount = false) => { + results.forEach((inputPeer) => { let peerID = appPeersManager.getPeerID(inputPeer); + + if(peerID == $rootScope.myID) { + if(!renderedSaved) { + this.renderSaved(); + } + + return; + } + let peer = appPeersManager.getPeer(peerID); let originalDialog = appMessagesManager.getDialogByPeerID(peerID)[0]; @@ -194,7 +229,11 @@ export default class AppSearch { }); if(results.length) group.setActive(); - else group.clear(); + else if(renderedSaved) { // удалить все пункты снизу + Array.from(group.list.children).slice(1).forEach(c => c.remove()); + } else { + group.clear(); + } }; setResults(contacts.my_results, this.searchGroups.contacts, true); @@ -209,7 +248,7 @@ export default class AppSearch { return; } - console.log('input search result:', this.peerID, query, null, maxID, 20, res); + //console.log('input search result:', this.peerID, query, null, maxID, 20, res); const {count, history, next_rate} = res; @@ -217,7 +256,7 @@ export default class AppSearch { history.shift(); } - const searchGroup = this.searchGroups['messages']; + const searchGroup = this.searchGroups.messages; searchGroup.setActive(); history.forEach((msgID: number) => { diff --git a/src/components/emoticonsDropdown/tabs/gifs.ts b/src/components/emoticonsDropdown/tabs/gifs.ts index b898cc5d..fd9b3deb 100644 --- a/src/components/emoticonsDropdown/tabs/gifs.ts +++ b/src/components/emoticonsDropdown/tabs/gifs.ts @@ -13,8 +13,8 @@ export default class GifsTab implements EmoticonsTab { const gifsContainer = this.content.firstElementChild as HTMLDivElement; gifsContainer.addEventListener('click', EmoticonsDropdown.onMediaClick); - const masonry = new GifsMasonry(gifsContainer); const scroll = new Scrollable(this.content, 'y', 'GIFS', null); + const masonry = new GifsMasonry(gifsContainer, EMOTICONSSTICKERGROUP, scroll); const preloader = putPreloader(this.content, true); apiManager.invokeApi('messages.getSavedGifs', {hash: 0}).then((res) => { @@ -24,7 +24,7 @@ export default class GifsTab implements EmoticonsTab { res.gifs.forEach((doc, idx) => { res.gifs[idx] = doc = appDocsManager.saveDoc(doc); //if(doc._ == 'documentEmpty') return; - masonry.add(doc as MyDocument, EMOTICONSSTICKERGROUP, EmoticonsDropdown.lazyLoadQueue); + //masonry.add(doc as MyDocument); }); } diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts index 905573ca..ac24b625 100644 --- a/src/components/emoticonsDropdown/tabs/stickers.ts +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -319,30 +319,6 @@ export default class StickersTab implements EmoticonsTab { } }); - /* let closed = true; - emoticonsDropdown.events.onClose.push(() => { - closed = false; - this.lazyLoadQueue.lock(); - }); - - emoticonsDropdown.events.onCloseAfter.push(() => { - const divs = this.lazyLoadQueue.intersector.getVisible(); - - for(const div of divs) { - this.processInvisibleDiv(div); - } - - closed = true; - }); - - emoticonsDropdown.events.onOpenAfter.push(() => { - if(closed) { - this.lazyLoadQueue.unlockAndRefresh(); - closed = false; - } else { - this.lazyLoadQueue.unlock(); - } - }); */ emoticonsDropdown.events.onClose.push(() => { this.lazyLoadQueue.lock(); }); diff --git a/src/components/gifsMasonry.ts b/src/components/gifsMasonry.ts index 5725ffad..31ec1acc 100644 --- a/src/components/gifsMasonry.ts +++ b/src/components/gifsMasonry.ts @@ -1,19 +1,151 @@ -import { calcImageInBox, findUpClassName } from "../lib/utils"; +import { calcImageInBox } from "../lib/utils"; import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import { wrapVideo } from "./wrappers"; import { renderImageFromUrl } from "./misc"; -import LazyLoadQueue from "./lazyLoadQueue"; +import { LazyLoadQueueRepeat2 } from "./lazyLoadQueue"; +import { CancellablePromise, deferredPromise } from "../lib/polyfill"; +import animationIntersector from "./animationIntersector"; +import Scrollable from "./scrollable_new"; const width = 400; const maxSingleWidth = width - 100; const height = 100; export default class GifsMasonry { - constructor(private element: HTMLElement) { - + public lazyLoadQueue: LazyLoadQueueRepeat2; + private scrollPromise: CancellablePromise = Promise.resolve(); + + constructor(private element: HTMLElement, private group: string, private scrollable: Scrollable) { + this.lazyLoadQueue = new LazyLoadQueueRepeat2(undefined, (target, visible) => { + if(visible) { + this.processVisibleDiv(target); + } else { + this.processInvisibleDiv(target); + } + }); + + setInterval(() => { + // @ts-ignore + const players = animationIntersector.byGroups[group]; + + if(players) { + console.log(`GIFS RENDERED IN ${group}:`, players.length, players.filter(p => !p.animation.paused).length, this.lazyLoadQueue.intersector.getVisible().length); + } + }, .25e3); + + let timeout = 0; + // memory leak + scrollable.container.addEventListener('scroll', () => { + if(timeout) { + clearTimeout(timeout); + } else { + this.scrollPromise = deferredPromise(); + //animationIntersector.checkAnimations(true, group); + } + + timeout = window.setTimeout(() => { + timeout = 0; + this.scrollPromise.resolve(); + //animationIntersector.checkAnimations(false, group); + }, 150); + }); } - public add(doc: MyDocument, group: string, lazyLoadQueue?: LazyLoadQueue) { + private processVisibleDiv = (div: HTMLElement) => { + const video = div.querySelector('video'); + if(video) { + return; + } + + const load = () => { + const docID = div.dataset.docID; + const doc = appDocsManager.getDoc(docID); + + const promise = this.scrollPromise.then(() => { + const promise = wrapVideo({ + doc, + container: div as HTMLDivElement, + lazyLoadQueue: null, + //lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, + group: this.group, + noInfo: true, + }); + + promise.finally(() => { + const video = div.querySelector('video'); + + div.style.opacity = ''; + const img = div.querySelector('img'); + img && img.classList.add('hide'); + + if(video && !video.parentElement) { + setTimeout(() => { + video.src = ''; + video.load(); + const animations = animationIntersector.getAnimations(video); + animations.forEach(item => { + animationIntersector.checkAnimation(item, true, true); + }); + }, 0); + } + + //clearTimeout(timeout); + if(!this.lazyLoadQueue.intersector.isVisible(div)) { + this.processInvisibleDiv(div); + } + }); + + return promise; + }); + + /* let timeout = window.setTimeout(() => { + console.error('processVisibleDiv timeout', div, doc); + }, 1e3); */ + + return promise; + }; + + //return load(); + + this.lazyLoadQueue.push({div, load}); + }; + + private processInvisibleDiv = async(div: HTMLElement) => { + return this.scrollPromise.then(async() => { + //return; + + if(this.lazyLoadQueue.intersector.isVisible(div)) { + return; + } + + const video = div.querySelector('video'); + const img = div.querySelector('img'); + + if(img) { + img && img.classList.remove('hide'); + + await new Promise((resolve) => { + window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)); + }); + } + + if(this.lazyLoadQueue.intersector.isVisible(div)) { + return; + } + + if(video) { + video.remove(); + video.src = ''; + video.load(); + const animations = animationIntersector.getAnimations(video); + animations.forEach(item => { + animationIntersector.checkAnimation(item, true, true); + }); + } + }); + }; + + public add(doc: MyDocument) { let gifWidth = doc.w; let gifHeight = doc.h; if(gifHeight < height) { @@ -21,8 +153,8 @@ export default class GifsMasonry { gifHeight = height; } - let willUseWidth = Math.min(maxSingleWidth, width, gifWidth); - let {w, h} = calcImageInBox(gifWidth, gifHeight, willUseWidth, height); + const willUseWidth = Math.min(maxSingleWidth, width, gifWidth); + const {w, h} = calcImageInBox(gifWidth, gifHeight, willUseWidth, height); /* wastedWidth += w; @@ -37,7 +169,7 @@ export default class GifsMasonry { //console.log('gif:', gif, w, h); - let div = document.createElement('div'); + const div = document.createElement('div'); div.classList.add('gif', 'fade-in-transition'); div.style.width = w + 'px'; div.style.opacity = '0'; @@ -46,6 +178,9 @@ export default class GifsMasonry { this.element.append(div); + //this.lazyLoadQueue.observe({div, load: this.processVisibleDiv}); + this.lazyLoadQueue.observe(div); + //let preloader = new ProgressivePreloader(div); const gotThumb = appDocsManager.getThumb(doc, false); @@ -62,72 +197,11 @@ export default class GifsMasonry { } } - let mouseOut = false; - const onMouseOver = (/* e: MouseEvent */) => { - //console.log('onMouseOver', doc.id); - //cancelEvent(e); - mouseOut = false; - - wrapVideo({ - doc, - container: div, - lazyLoadQueue, - //lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, - group, - noInfo: true, - }); - - const video = div.querySelector('video'); - video.addEventListener('canplay', () => { - div.style.opacity = ''; - if(!mouseOut) { - img && img.classList.add('hide'); - } else { - img && img.classList.remove('hide'); - if(div.lastElementChild != img) { - div.lastElementChild.remove(); - } - } - }, {once: true}); - }; - const afterRender = () => { if(img) { div.append(img); div.style.opacity = ''; } - - if(lazyLoadQueue) { - onMouseOver(); - } else { - div.addEventListener('mouseover', onMouseOver, {once: true}); - div.addEventListener('mouseout', (e) => { - const toElement = (e as any).toElement as Element; - //console.log('onMouseOut', doc.id, e); - if(findUpClassName(toElement, 'gif') == div) { - return; - } - - //cancelEvent(e); - - mouseOut = true; - - const cb = () => { - if(div.lastElementChild != img) { - div.lastElementChild.remove(); - } - - div.addEventListener('mouseover', onMouseOver, {once: true}); - }; - - img && img.classList.remove('hide'); - /* window.requestAnimationFrame(() => { - window.requestAnimationFrame(); - }); */ - if(img) window.requestAnimationFrame(() => window.requestAnimationFrame(cb)); - else cb(); - }); - } }; (gotThumb?.thumb?.url ? renderImageFromUrl(img, gotThumb.thumb.url, afterRender) : afterRender()); diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index 162845d5..734542c8 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -2,16 +2,19 @@ import { logger, LogLevels } from "../lib/logger"; import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector"; type LazyLoadElementBase = { - div: HTMLDivElement, - load: (target?: HTMLDivElement) => Promise + load: () => Promise }; -type LazyLoadElement = LazyLoadElementBase & { - wasSeen?: boolean +type LazyLoadElement = Omit & { + load: (target?: HTMLElement) => Promise, + div: HTMLElement + wasSeen?: boolean, }; +const PARALLEL_LIMIT = 5; + export class LazyLoadQueueBase { - protected lazyLoadMedia: Array = []; + protected queue: Array = []; protected inProcess: Set = new Set(); protected lockPromise: Promise = null; @@ -19,13 +22,13 @@ export class LazyLoadQueueBase { protected log = logger('LL', LogLevels.error); - constructor(protected parallelLimit = 5) { + constructor(protected parallelLimit = PARALLEL_LIMIT) { } public clear() { this.inProcess.clear(); // ацтеки забьются, будет плохо - this.lazyLoadMedia.length = 0; + this.queue.length = 0; // unreachable code /* for(let item of this.inProcess) { this.lazyLoadMedia.push(item); @@ -34,34 +37,40 @@ export class LazyLoadQueueBase { public lock() { if(this.lockPromise) return; + + const perf = performance.now(); this.lockPromise = new Promise((resolve, reject) => { this.unlockResolve = resolve; }); + + this.lockPromise.then(() => { + this.log('was locked for:', performance.now() - perf); + }); } public unlock() { if(!this.unlockResolve) return; - this.lockPromise = null; + this.unlockResolve(); - this.unlockResolve = null; + this.unlockResolve = this.lockPromise = null; + + this.processQueue(); } public async processItem(item: LazyLoadElementBase) { + if(this.lockPromise) { + return; + } + this.inProcess.add(item); this.log('will load media', this.lockPromise, item); try { - if(this.lockPromise/* && false */) { - const perf = performance.now(); - await this.lockPromise; - - this.log('waited lock:', performance.now() - perf); - } - //await new Promise((resolve) => setTimeout(resolve, 2e3)); //await new Promise((resolve, reject) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve))); - await item.load(item.div); + //await item.load(item.div); + await this.loadItem(item); } catch(err) { this.log.error('loadMediaQueue error:', err/* , item */); } @@ -70,25 +79,28 @@ export class LazyLoadQueueBase { this.log('loaded media', item); - if(this.lazyLoadMedia.length) { - this.processQueue(); - } + this.processQueue(); + } + + protected loadItem(item: LazyLoadElementBase) { + return item.load(); } protected getItem() { - return this.lazyLoadMedia.shift(); + return this.queue.shift(); } - protected addElement(el: LazyLoadElementBase) { - this.processQueue(el); + protected addElement(method: 'push' | 'unshift', el: LazyLoadElementBase) { + this.queue[method](el); + this.processQueue(); } public async processQueue(item?: LazyLoadElementBase) { - if(this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit) return; + if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return; do { if(item) { - this.lazyLoadMedia.findAndSplice(i => i == item); + this.queue.findAndSplice(i => i == item); } else { item = this.getItem(); } @@ -100,25 +112,26 @@ export class LazyLoadQueueBase { } item = null; - } while(this.inProcess.size < this.parallelLimit && this.lazyLoadMedia.length); + } while(this.inProcess.size < this.parallelLimit && this.queue.length); } public push(el: LazyLoadElementBase) { - this.lazyLoadMedia.push(el); - this.addElement(el); + this.addElement('push', el); } public unshift(el: LazyLoadElementBase) { - this.lazyLoadMedia.unshift(el); - this.addElement(el); + this.addElement('unshift', el); } } export class LazyLoadQueueIntersector extends LazyLoadQueueBase { + protected queue: Array = []; + protected inProcess: Set = new Set(); + public intersector: VisibilityIntersector; protected intersectorTimeout: number; - constructor(protected parallelLimit = 5) { + constructor(protected parallelLimit = PARALLEL_LIMIT) { super(parallelLimit); } @@ -146,6 +159,26 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase { this.intersector.refresh(); } + protected loadItem(item: LazyLoadElement) { + return item.load(item.div); + } + + protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) { + const item = this.queue.find(i => i.div == el.div); + if(item) { + return false; + } else { + for(const item of this.inProcess) { + if(item.div == el.div) { + return false; + } + } + } + + this.queue[method](el); + return true; + } + protected setProcessQueueTimeout() { if(!this.intersectorTimeout) { this.intersectorTimeout = window.setTimeout(() => { @@ -154,52 +187,6 @@ export class LazyLoadQueueIntersector extends LazyLoadQueueBase { }, 0); } } -} - -export default class LazyLoadQueue extends LazyLoadQueueIntersector { - protected lazyLoadMedia: Array = []; - protected inProcess: Set = new Set(); - - constructor(protected parallelLimit = 5) { - super(parallelLimit); - - this.intersector = new VisibilityIntersector(this.onVisibilityChange); - } - - private onVisibilityChange = (target: HTMLElement, visible: boolean) => { - if(visible) { - this.log('isIntersecting', target); - - // need for set element first if scrolled - const item = this.lazyLoadMedia.findAndSplice(i => i.div == target); - if(item) { - item.wasSeen = true; - this.lazyLoadMedia.unshift(item); - //this.processQueue(item); - } - - this.setProcessQueueTimeout(); - } - }; - - protected getItem() { - return this.lazyLoadMedia.findAndSplice(item => item.wasSeen); - } - - public async processItem(item: LazyLoadElement) { - await super.processItem(item); - this.intersector.unobserve(item.div); - } - - protected addElement(el: LazyLoadElement) { - //super.addElement(el); - if(el.wasSeen) { - super.processQueue(el); - } else { - el.wasSeen = false; - this.intersector.observe(el.div); - } - } public push(el: LazyLoadElement) { super.push(el); @@ -210,18 +197,66 @@ export default class LazyLoadQueue extends LazyLoadQueueIntersector { } } -export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { - private _lazyLoadMedia: Map = new Map(); +export default class LazyLoadQueue extends LazyLoadQueueIntersector { + constructor(protected parallelLimit = PARALLEL_LIMIT) { + super(parallelLimit); - constructor(protected parallelLimit = 5, protected onVisibilityChange?: OnVisibilityChange) { + this.intersector = new VisibilityIntersector(this.onVisibilityChange); + } + + private onVisibilityChange = (target: HTMLElement, visible: boolean) => { + if(visible) { + this.log('isIntersecting', target); + + // need for set element first if scrolled + const item = this.queue.findAndSplice(i => i.div == target); + if(item) { + item.wasSeen = true; + this.queue.unshift(item); + //this.processQueue(item); + } + + this.setProcessQueueTimeout(); + } + }; + + protected getItem() { + return this.queue.findAndSplice(item => item.wasSeen); + } + + public async processItem(item: LazyLoadElement) { + await super.processItem(item); + this.intersector.unobserve(item.div); + } + + protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) { + const inserted = super.addElement(method, el); + + if(!inserted) return false; + + this.intersector.observe(el.div); + if(el.wasSeen) { + this.processQueue(el); + } else if(!el.hasOwnProperty('wasSeen')) { + el.wasSeen = false; + } + + return true; + } +} + +export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { + private _queue: Map = new Map(); + + constructor(protected parallelLimit = PARALLEL_LIMIT, protected onVisibilityChange?: OnVisibilityChange) { super(parallelLimit); this.intersector = new VisibilityIntersector((target, visible) => { if(visible) { - const item = this.lazyLoadMedia.findAndSplice(i => i.div == target); - this.lazyLoadMedia.unshift(item || this._lazyLoadMedia.get(target)); + const item = this.queue.findAndSplice(i => i.div == target); + this.queue.unshift(item || this._queue.get(target)); } else { - this.lazyLoadMedia.findAndSplice(i => i.div == target); + this.queue.findAndSplice(i => i.div == target); } this.onVisibilityChange && this.onVisibilityChange(target, visible); @@ -229,6 +264,11 @@ export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { }); } + public clear() { + super.clear(); + this._queue.clear(); + } + /* public async processItem(item: LazyLoadElement) { //await super.processItem(item); await LazyLoadQueueBase.prototype.processItem.call(this, item); @@ -238,8 +278,28 @@ export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector { } } */ - public observe(el: LazyLoadElementBase) { - this._lazyLoadMedia.set(el.div, el); + public observe(el: LazyLoadElement) { + this._queue.set(el.div, el); this.intersector.observe(el.div); } } + +export class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector { + constructor(protected parallelLimit = PARALLEL_LIMIT, protected onVisibilityChange?: OnVisibilityChange) { + super(parallelLimit); + + this.intersector = new VisibilityIntersector((target, visible) => { + const item = this.queue.findAndSplice(i => i.div == target); + if(visible && item) { + this.queue.unshift(item); + } + + this.onVisibilityChange && this.onVisibilityChange(target, visible); + this.setProcessQueueTimeout(); + }); + } + + public observe(el: HTMLElement) { + this.intersector.observe(el); + } +} diff --git a/src/components/scrollable_new.ts b/src/components/scrollable_new.ts index abb90450..d76008db 100644 --- a/src/components/scrollable_new.ts +++ b/src/components/scrollable_new.ts @@ -1,6 +1,7 @@ import { logger, LogLevels } from "../lib/logger"; import smoothscroll from '../vendor/smoothscroll'; import { touchSupport, isSafari, mediaSizes } from "../lib/config"; +import { CancellablePromise, deferredPromise } from "../lib/polyfill"; //import { isInDOM } from "../lib/utils"; (window as any).__forceSmoothScrollPolyfill__ = true; smoothscroll.polyfill(); @@ -80,6 +81,7 @@ export default class Scrollable { private onScrolledBottomFired = false; */ public scrollLocked = 0; + public scrollLockedPromise: CancellablePromise = Promise.resolve(); public isVisible = false; private reorderTimeout: number; @@ -209,13 +211,11 @@ export default class Scrollable { throw new Error('no side for scroll'); } - const binded = this.onScroll.bind(this); - window.addEventListener('resize', () => { this.overflowContainer = mediaSizes.isMobile && false ? document.documentElement : this.container; this.onScroll(); }); - this.container.addEventListener('scroll', binded, {passive: true, capture: true}); + this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); //document.documentElement.addEventListener('scroll', binded, {passive: true, capture: true}); //window.addEventListener('scroll', binded, {passive: true, capture: true}); @@ -289,7 +289,7 @@ export default class Scrollable { this.log('setVirtualContainer:', el, this); } - public onScroll() { + public onScroll = () => { /* let scrollTop = this.scrollTop; this.lastScrollDirection = this.lastScrollTop < scrollTop; this.lastScrollTop = scrollTop; @@ -313,7 +313,7 @@ export default class Scrollable { if(this.splitUp) { clearTimeout(this.disableHoverTimeout); - this.disableHoverTimeout = setTimeout(() => { + this.disableHoverTimeout = window.setTimeout(() => { //appendTo.classList.remove('disable-hover'); this.lastScrollDirection = 0; }, 100); @@ -342,7 +342,7 @@ export default class Scrollable { this.lastScrollDirection = 0; } }); - } + }; public checkForTriggers(container: HTMLElement) { if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; @@ -369,7 +369,7 @@ export default class Scrollable { public reorder() { if(!this.splitUp || this.reorderTimeout) return; - this.reorderTimeout = setTimeout(() => { + this.reorderTimeout = window.setTimeout(() => { this.reorderTimeout = 0; (Array.from(this.splitUp.children) as HTMLElement[]).forEach((el, idx) => { @@ -466,9 +466,15 @@ export default class Scrollable { } if(this.scrollLocked) clearTimeout(this.scrollLocked); - this.scrollLocked = setTimeout(() => { + else { + this.scrollLockedPromise = deferredPromise(); + } + + this.scrollLocked = window.setTimeout(() => { this.scrollLocked = 0; - this.onScroll(); + this.scrollLockedPromise.resolve(); + //this.onScroll(); + this.container.dispatchEvent(new CustomEvent('scroll')); }, 468); this.container.scrollTo({behavior: smooth ? 'smooth' : 'auto', top}); diff --git a/src/components/sidebarLeft/contacts.ts b/src/components/sidebarLeft/contacts.ts index a37e0bc1..de4483e1 100644 --- a/src/components/sidebarLeft/contacts.ts +++ b/src/components/sidebarLeft/contacts.ts @@ -7,6 +7,8 @@ import appSidebarLeft, { AppSidebarLeft } from "../../lib/appManagers/appSidebar import { $rootScope } from "../../lib/utils"; import SearchInput from "../searchInput"; +// TODO: поиск по людям глобальный, если не нашло в контактах никого + export default class AppContactsTab implements SliderTab { private container = document.getElementById('contacts-container'); private list = this.container.querySelector('#contacts') as HTMLUListElement; @@ -58,7 +60,13 @@ export default class AppContactsTab implements SliderTab { } const contacts = [..._contacts]; - contacts.findAndSplice(u => u == $rootScope.myID); + + if(!query) { + contacts.findAndSplice(u => u == $rootScope.myID); + } + /* if(query && 'saved messages'.includes(query.toLowerCase())) { + contacts.unshift($rootScope.myID); + } */ let sorted = contacts .map(userID => { diff --git a/src/components/sidebarRight/gifs.ts b/src/components/sidebarRight/gifs.ts index f8ef7d4a..33364428 100644 --- a/src/components/sidebarRight/gifs.ts +++ b/src/components/sidebarRight/gifs.ts @@ -21,7 +21,6 @@ export default class AppGifsTab implements SliderTab { private searchInput: SearchInput; private gifsDiv = this.contentDiv.firstElementChild as HTMLDivElement; private scrollable: Scrollable; - private lazyLoadQueue: LazyLoadQueue; private nextOffset = ''; private loadedAll = false; @@ -35,9 +34,7 @@ export default class AppGifsTab implements SliderTab { this.scrollable = new Scrollable(this.contentDiv, 'y', ANIMATIONGROUP, undefined, undefined, 2); this.scrollable.setVirtualContainer(this.gifsDiv); - this.masonry = new GifsMasonry(this.gifsDiv); - - this.lazyLoadQueue = new LazyLoadQueue(); + this.masonry = new GifsMasonry(this.gifsDiv, ANIMATIONGROUP, this.scrollable); this.searchInput = new SearchInput('Search GIFs', (value) => { this.reset(); @@ -76,7 +73,7 @@ export default class AppGifsTab implements SliderTab { this.searchPromise = null; this.nextOffset = ''; this.loadedAll = false; - this.lazyLoadQueue.clear(); + this.masonry.lazyLoadQueue.clear(); } public init() { @@ -117,7 +114,7 @@ export default class AppGifsTab implements SliderTab { if(results.length) { results.forEach((result) => { if(result._ === 'botInlineMediaResult' && result.document) { - this.masonry.add(result.document as MyDocument, ANIMATIONGROUP, this.lazyLoadQueue); + this.masonry.add(result.document as MyDocument); } }); } else { @@ -125,7 +122,8 @@ export default class AppGifsTab implements SliderTab { } this.scrollable.onScroll(); - } catch (err) { + } catch(err) { + this.searchPromise = null; throw new Error(JSON.stringify(err)); } } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 7f0813be..0ee2d1eb 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -210,6 +210,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai //console.log('loaded doc:', doc, doc.url, container); + const deferred = deferredPromise(); + //if(doc.type == 'gif'/* || true */) { video.addEventListener('canplay', () => { if(img?.parentElement) { @@ -222,9 +224,16 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai if(doc.type == 'gif' && group) { animationIntersector.addAnimation(video, group); } + + // test lazyLoadQueue + //setTimeout(() => { + deferred.resolve(); + //}, 5000); }, {once: true}); //} + video.addEventListener('error', deferred.reject); + //if(doc.type != 'round') { renderImageFromUrl(video, doc.url); //} @@ -243,6 +252,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai video.dataset.overlay = '1'; new VideoPlayer(video); } + + return deferred; }; /* if(doc.size >= 20e6 && !doc.downloaded) { @@ -263,8 +274,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai return; } */ - /* doc.downloaded || */!lazyLoadQueue/* && false */ ? loadVideo() : lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */}); - return video; + return /* doc.downloaded || */!lazyLoadQueue/* && false */ ? loadVideo() : (lazyLoadQueue.push({div: container, load: loadVideo/* , wasSeen: true */}), Promise.resolve()); } export const formatDate = (timestamp: number, monthShort = false, withYear = true) => { @@ -476,7 +486,7 @@ export function wrapPhoto(photo: MyPhoto | MyDocument, message: any, container: }); }; - return cacheContext.downloaded || !lazyLoadQueue ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true}); + 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}: { diff --git a/src/lib/appManagers/AppInlineBotsManager.ts b/src/lib/appManagers/AppInlineBotsManager.ts index fe00596d..98f2f273 100644 --- a/src/lib/appManagers/AppInlineBotsManager.ts +++ b/src/lib/appManagers/AppInlineBotsManager.ts @@ -20,7 +20,7 @@ export class AppInlineBotsManager { query: query, geo_point: geo && {_: 'inputGeoPoint', lat: geo['lat'], long: geo['long']}, offset - }, {timeout: 1, stopTime: -1, noErrorBox: true}).then(botResults => { + }, {/* timeout: 1, */stopTime: -1, noErrorBox: true}).then(botResults => { const queryID = botResults.query_id; /* delete botResults._; delete botResults.flags; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 0714447b..6ece0a34 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -801,7 +801,8 @@ export class AppImManager { public onScroll(e: Event) { if(this.onScrollRAF) window.cancelAnimationFrame(this.onScrollRAF); - //if(this.scrollable.scrollLocked) return; + // * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз + if(this.scrollable.scrollLocked && this.scrolledDown) return; this.onScrollRAF = window.requestAnimationFrame(() => { //lottieLoader.checkAnimations(false, 'chat'); diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index 9f44fc35..22f2c996 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -1073,10 +1073,7 @@ export class AppMediaViewer { return promise; }; - this.lazyLoadQueue.unshift({ - div: null, - load - }); + this.lazyLoadQueue.unshift({load}); //} else createPlayer(); }); } else { @@ -1137,10 +1134,7 @@ export class AppMediaViewer { return cancellablePromise; }; - this.lazyLoadQueue.unshift({ - div: null, - load - }); + this.lazyLoadQueue.unshift({load}); }); } diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index 2897b76c..7316c19c 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -97,13 +97,11 @@ export class AppPeersManager { : appChatsManager.getChat(-peerID) } - public getPeerID(peerString: any): number { + public getPeerID(peerString: any/* Peer | number | string */): number { if(typeof(peerString) === 'number') return peerString; - else if(isObject(peerString)) { - return peerString.user_id - ? peerString.user_id - : -(peerString.channel_id || peerString.chat_id); - } else if(!peerString) return 0; + else if(isObject(peerString)) return peerString.user_id ? peerString.user_id : -(peerString.channel_id || peerString.chat_id); + else if(!peerString) return 0; + const isUser = peerString.charAt(0) == 'u'; const peerParams = peerString.substr(1).split('_'); diff --git a/src/lib/appManagers/appSidebarLeft.ts b/src/lib/appManagers/appSidebarLeft.ts index ca856e9b..09de32e1 100644 --- a/src/lib/appManagers/appSidebarLeft.ts +++ b/src/lib/appManagers/appSidebarLeft.ts @@ -86,6 +86,7 @@ export class AppSidebarLeft extends SidebarSlider { //private log = logger('SL'); private searchGroups = { + //saved: new SearchGroup('', 'contacts'), contacts: new SearchGroup('Chats', 'contacts'), globalContacts: new SearchGroup('Global Search', 'contacts'), messages: new SearchGroup('Global Search', 'messages'), diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 3962510b..049e17a0 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -179,7 +179,7 @@ export class AppUsersManager { return this.fillContacts().then(_contactsList => { let contactsList = [..._contactsList]; if(query) { - const results: any = searchIndexManager.search(query, this.contactsIndex); + const results = searchIndexManager.search(query, this.contactsIndex); const filteredContactsList = [...contactsList].filter(id => !!results[id]); contactsList = filteredContactsList; @@ -589,7 +589,7 @@ export class AppUsersManager { return apiManager.invokeApi('contacts.search', { q: query, limit - }).then((peers: any) => { + }).then((peers) => { //console.log(peers); this.saveApiUsers(peers.users); appChatsManager.saveApiChats(peers.chats);