diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 1b82cccc..b45cdc6c 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -5,11 +5,11 @@ */ import { formatDateAccordingToToday, months } from "../helpers/date"; -import { positionElementByIndex } from "../helpers/dom"; +import { positionElementByIndex, isInDOM, replaceContent } from "../helpers/dom"; import { copy, getObjectKeysAndSort, safeAssign } from "../helpers/object"; import { escapeRegExp, limitSymbols } from "../helpers/string"; import appChatsManager from "../lib/appManagers/appChatsManager"; -import appDialogsManager from "../lib/appManagers/appDialogsManager"; +import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; import appMessagesManager, { MyInputMessagesFilter, MyMessage } from "../lib/appManagers/appMessagesManager"; import appPeersManager from "../lib/appManagers/appPeersManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager"; @@ -34,6 +34,7 @@ import findUpClassName from "../helpers/dom/findUpClassName"; import { getMiddleware } from "../helpers/middleware"; import appProfileManager from "../lib/appManagers/appProfileManager"; import { ChannelParticipant, ChatFull, ChatParticipant, ChatParticipants } from "../layer"; +import SortedUserList from "./sortedUserList"; //const testScroll = false; @@ -104,6 +105,8 @@ export default class AppSearchSuper { public mediaTabsMap: Map = new Map(); + private membersList: SortedUserList; + // * arguments public mediaTabs: SearchSuperMediaTab[]; public scrollable: Scrollable; @@ -850,32 +853,24 @@ export default class AppSearchSuper { } } - let list = mediaTab.contentTab.firstElementChild as HTMLUListElement; - if(!list) { - list = appDialogsManager.createChatList(); - appDialogsManager.setListClickListener(list, undefined, undefined, true, true); - mediaTab.contentTab.append(list); + if(!this.membersList) { + this.membersList = new SortedUserList(); + mediaTab.contentTab.append(this.membersList.list); this.afterPerforming(1, mediaTab.contentTab); } participants.forEach(participant => { - let {dialog, dom} = appDialogsManager.addDialogNew({ - dialog: participant.user_id, - container: list, - drawStatus: false, - avatarSize: 48, - autonomous: true, - meAsSaved: false, - rippleEnabled: false - }); + const user = appUsersManager.getUser(participant.user_id); + if(user.pFlags.deleted) { + return; + } - let status = appUsersManager.getUserStatusString(participant.user_id); - dom.lastMessageSpan.append(status); + this.membersList.add(participant.user_id); }); }; if(appChatsManager.isChannel(id)) { - const LOAD_COUNT = 50; + const LOAD_COUNT = !this.membersList ? 50 : 200; promise = appProfileManager.getChannelParticipants(id, undefined, LOAD_COUNT, this.nextRates[mediaTab.inputFilter]).then(participants => { if(!middleware()) { return; @@ -1139,6 +1134,7 @@ export default class AppSearchSuper { this.middleware.clean(); this.cleanScrollPositions(); + this.membersList = undefined; } public cleanScrollPositions() { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 65ee1997..9b9ac36d 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -660,7 +660,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { } public init() { - const perf = performance.now(); + //const perf = performance.now(); this.container.classList.add('shared-media-container', 'profile-container'); @@ -859,7 +859,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { } }); - console.log('construct shared media time:', performance.now() - perf); + //console.log('construct shared media time:', performance.now() - perf); } public renderNewMessages(peerId: number, mids: number[]) { @@ -923,7 +923,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { } public cleanupHTML() { - const perf = performance.now(); + // const perf = performance.now(); this.profile.cleanupHTML(); this.editBtn.style.display = 'none'; @@ -932,7 +932,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { this.container.classList.toggle('can-add-members', this.searchSuper.canViewMembers() && appChatsManager.hasRights(-this.peerId, 'invite_users')); - console.log('cleanupHTML shared media time:', performance.now() - perf); + // console.log('cleanupHTML shared media time:', performance.now() - perf); } public setLoadMutex(promise: Promise) { diff --git a/src/components/sortedUserList.ts b/src/components/sortedUserList.ts new file mode 100644 index 00000000..b31f021a --- /dev/null +++ b/src/components/sortedUserList.ts @@ -0,0 +1,97 @@ +import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; +import { isInDOM, positionElementByIndex, replaceContent } from "../helpers/dom"; +import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; +import appUsersManager from "../lib/appManagers/appUsersManager"; +import { insertInDescendSortedArray, forEachReverse } from "../helpers/array"; + +type SortedUser = { + peerId: number, + status: number, + dom: DialogDom +}; +export default class SortedUserList { + public static SORT_INTERVAL = 30e3; + public list: HTMLUListElement; + public users: Map; + public sorted: Array; + + constructor() { + this.list = appDialogsManager.createChatList(); + appDialogsManager.setListClickListener(this.list, undefined, undefined, true, true); + + this.users = new Map(); + this.sorted = []; + + let timeout: number; + const doTimeout = () => { + timeout = window.setTimeout(() => { + this.updateList().then((good) => { + if(good) { + doTimeout(); + } + }); + }, SortedUserList.SORT_INTERVAL); + }; + + doTimeout(); + } + + public async updateList() { + if(!isInDOM(this.list)) { + return false; + } + + await getHeavyAnimationPromise(); + + if(!isInDOM(this.list)) { + return false; + } + + this.users.forEach(user => { + this.update(user.peerId, true); + }); + + this.sorted.forEach((sortedUser, idx) => { + positionElementByIndex(sortedUser.dom.listEl, this.list, idx); + }); + + return true; + } + + public add(peerId: number) { + if(this.users.has(peerId)) { + return; + } + + const {dom} = appDialogsManager.addDialogNew({ + dialog: peerId, + container: false, + drawStatus: false, + avatarSize: 48, + autonomous: true, + meAsSaved: false, + rippleEnabled: false + }); + + const sortedUser: SortedUser = { + peerId, + status: appUsersManager.getUserStatusForSort(peerId), + dom + }; + + this.users.set(peerId, sortedUser); + this.update(peerId); + } + + public update(peerId: number, batch = false) { + const sortedUser = this.users.get(peerId); + sortedUser.status = appUsersManager.getUserStatusForSort(peerId); + const status = appUsersManager.getUserStatusString(peerId); + replaceContent(sortedUser.dom.lastMessageSpan, status); + + const idx = insertInDescendSortedArray(this.sorted, sortedUser, 'status'); + if(!batch) { + positionElementByIndex(sortedUser.dom.listEl, this.list, idx); + } + } +} diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 2d0ddb9a..eb124b14 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -36,7 +36,7 @@ import rootScope from '../lib/rootScope'; import { onVideoLoad } from '../helpers/files'; import { animateSingle } from '../helpers/animation'; import renderImageFromUrl from '../helpers/dom/renderImageFromUrl'; -import { fastRaf } from '../helpers/schedulers'; +import sequentialDom from '../helpers/sequentialDom'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB @@ -726,22 +726,23 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT return new Promise((resolve) => { renderImageFromUrl(image, cacheContext.url || photo.url, () => { - container.append(image); + sequentialDom.mutateElement(container, () => { + container.append(image); - fastRaf(() => { resolve(); + + if(needFadeIn) { + image.addEventListener('animationend', () => { + sequentialDom.mutate(() => { + image.classList.remove('fade-in'); + + if(thumbImage) { + thumbImage.remove(); + } + }); + }, {once: true}); + } }); - //resolve(); - - if(needFadeIn) { - image.addEventListener('animationend', () => { - image.classList.remove('fade-in'); - - if(thumbImage) { - thumbImage.remove(); - } - }, {once: true}); - } }); }); }; @@ -849,8 +850,11 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o const afterRender = () => { if(!div.childElementCount) { thumbImage.classList.add('media-sticker', 'thumbnail'); - div.append(thumbImage); - loadThumbPromise.resolve(); + + sequentialDom.mutateElement(div, () => { + div.append(thumbImage); + loadThumbPromise.resolve(); + }); } }; @@ -974,17 +978,23 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o }; if(!needFadeIn) { - cb(); - } else { - animation.canvas.classList.add('fade-in'); if(element) { - element.classList.add('fade-out'); + sequentialDom.mutate(cb); } - - animation.canvas.addEventListener('animationend', () => { - animation.canvas.classList.remove('fade-in'); - cb(); - }, {once: true}); + } else { + sequentialDom.mutate(() => { + animation.canvas.classList.add('fade-in'); + if(element) { + element.classList.add('fade-out'); + } + + animation.canvas.addEventListener('animationend', () => { + sequentialDom.mutate(() => { + animation.canvas.classList.remove('fade-in'); + cb(); + }); + }, {once: true}); + }); } appDocsManager.saveLottiePreview(doc, animation.canvas, toneIndex); @@ -1025,23 +1035,23 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o if(middleware && !middleware()) return resolve(); renderImageFromUrl(image, doc.url, () => { - div.append(image); - if(thumbImage) { - thumbImage.classList.add('fade-out'); - } - - fastRaf(() => { + sequentialDom.mutateElement(div, () => { + div.append(image); + if(thumbImage) { + thumbImage.classList.add('fade-out'); + } + resolve(); + + if(needFadeIn) { + image.addEventListener('animationend', () => { + image.classList.remove('fade-in'); + if(thumbImage) { + thumbImage.remove(); + } + }, {once: true}); + } }); - - if(needFadeIn) { - image.addEventListener('animationend', () => { - image.classList.remove('fade-in'); - if(thumbImage) { - thumbImage.remove(); - } - }, {once: true}); - } }); }; diff --git a/src/helpers/array.ts b/src/helpers/array.ts index 7c644f29..dd9f222a 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -36,3 +36,31 @@ export function forEachReverse(array: Array, callback: (value: T, index?: callback(array[i], i, array); } }; + +export function insertInDescendSortedArray(array: Array, element: T, property: K, pos?: number) { + if(pos === undefined) { + pos = array.indexOf(element); + if(pos !== -1) { + array.splice(pos, 1); + } + } + + const sortProperty: number = element[property]; + const len = array.length; + if(!len || sortProperty <= array[len - 1][property]) { + return array.push(element) - 1; + } else if(sortProperty >= array[0][property]) { + array.unshift(element); + return 0; + } else { + for(let i = 0; i < len; i++) { + if(sortProperty > array[i][property]) { + array.splice(i, 0, element); + return i; + } + } + } + + console.error('wtf', array, element); + return array.indexOf(element); +} diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index aef84518..71811afa 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -405,8 +405,10 @@ export function calcImageInBox(imageW: number, imageH: number, boxW: number, box MOUNT_CLASS_TO.calcImageInBox = calcImageInBox; -export function positionElementByIndex(element: HTMLElement, container: HTMLElement, pos: number) { - const prevPos = element.parentElement === container ? whichChild(element) : -1; +export function positionElementByIndex(element: HTMLElement, container: HTMLElement, pos: number, prevPos?: number) { + if(prevPos === undefined) { + prevPos = element.parentElement === container ? whichChild(element) : -1; + } if(prevPos === pos) { return false; diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index e2ce17cc..aac57e2c 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -132,8 +132,8 @@ export function fastRaf(callback: NoneToVoidFunction) { export function doubleRaf() { return new Promise((resolve) => { - window.requestAnimationFrame(() => { - window.requestAnimationFrame(resolve); + fastRaf(() => { + fastRaf(resolve); }); }); } diff --git a/src/helpers/sequentialDom.ts b/src/helpers/sequentialDom.ts new file mode 100644 index 00000000..47f06d7e --- /dev/null +++ b/src/helpers/sequentialDom.ts @@ -0,0 +1,68 @@ +import { fastRaf } from "./schedulers"; +import { CancellablePromise, deferredPromise } from "./cancellablePromise"; +import { isInDOM } from "./dom"; +import { MOUNT_CLASS_TO } from "../config/debug"; + +class SequentialDom { + private promises: Partial<{ + read: CancellablePromise, + write: CancellablePromise + }> = {}; + private raf = fastRaf.bind(null); + private scheduled = false; + + private do(kind: keyof SequentialDom['promises'], callback?: VoidFunction) { + let promise = this.promises[kind]; + if(!promise) { + this.scheduleFlush(); + promise = this.promises[kind] = deferredPromise(); + } + + if(callback !== undefined) { + promise.then(() => callback()); + } + + return promise; + } + + public measure(callback?: VoidFunction) { + return this.do('read', callback); + } + + public mutate(callback?: VoidFunction) { + return this.do('write', callback); + } + + /** + * Will fire instantly if element is not connected + * @param element + * @param callback + */ + public mutateElement(element: HTMLElement, callback?: VoidFunction) { + const promise = isInDOM(element) ? this.mutate() : Promise.resolve(); + + if(callback !== undefined) { + promise.then(() => callback()); + } + + return promise; + } + + private scheduleFlush() { + if(!this.scheduled) { + this.scheduled = true; + + this.raf(() => { + this.promises.read && this.promises.read.resolve(); + this.promises.write && this.promises.write.resolve(); + + this.scheduled = false; + this.promises = {}; + }); + } + } +} + +const sequentialDom = new SequentialDom(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.sequentialDom = sequentialDom); +export default sequentialDom; diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 8e21dd00..47e87130 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -37,7 +37,7 @@ import PeerTitle from "../../components/peerTitle"; import { i18n } from "../langPack"; import findUpTag from "../../helpers/dom/findUpTag"; -type DialogDom = { +export type DialogDom = { avatarEl: AvatarElement, captionDiv: HTMLDivElement, titleSpan: HTMLSpanElement, @@ -1198,7 +1198,7 @@ export class AppDialogsManager { public addDialogNew(options: { dialog: Dialog | number, - container?: HTMLUListElement | Scrollable, + container?: HTMLUListElement | Scrollable | false, drawStatus?: boolean, rippleEnabled?: boolean, onlyFirstName?: boolean, @@ -1210,7 +1210,7 @@ export class AppDialogsManager { return this.addDialog(options.dialog, options.container, options.drawStatus, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous); } - public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container) { + public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable | false, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container) { let dialog: Dialog; if(typeof(_dialog) === 'number') { @@ -1230,7 +1230,7 @@ export class AppDialogsManager { const peerId: number = dialog.peerId; - if(!container) { + if(container === undefined) { if(this.doms[peerId]) return; const filter = appMessagesManager.filtersStorage.filters[this.filterId]; @@ -1350,7 +1350,7 @@ export class AppDialogsManager { } } */ const method: 'append' | 'prepend' = append ? 'append' : 'prepend'; - if(!container/* || good */) { + if(container === undefined/* || good */) { this.scroll[method](li); this.doms[dialog.peerId] = dom; @@ -1365,7 +1365,7 @@ export class AppDialogsManager { } this.setLastMessage(dialog); - } else { + } else if(container) { container[method](li); } diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 7dc945ed..8fa00580 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -363,14 +363,18 @@ export class AppUsersManager { this.userAccess[id] = accessHash; } */ - public getUserStatusForSort(status: User['status']) { + public getUserStatusForSort(status: User['status'] | number) { + if(typeof(status) === 'number') { + status = this.getUser(status).status; + } + if(status) { const expires = status._ === 'userStatusOnline' ? status.expires : (status._ === 'userStatusOffline' ? status.was_online : 0); if(expires) { return expires; } - const timeNow = tsNow(true); + /* const timeNow = tsNow(true); switch(status._) { case 'userStatusRecently': return timeNow - 86400 * 3; @@ -378,6 +382,14 @@ export class AppUsersManager { return timeNow - 86400 * 7; case 'userStatusLastMonth': return timeNow - 86400 * 30; + } */ + switch(status._) { + case 'userStatusRecently': + return 3; + case 'userStatusLastWeek': + return 2; + case 'userStatusLastMonth': + return 1; } } diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index 9f801249..5aa5ecb0 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -16,6 +16,7 @@ import type { AppMessagesManager, Dialog, MyMessage } from "../appManagers/appMe import type { AppPeersManager } from "../appManagers/appPeersManager"; import type { ServerTimeManager } from "../mtproto/serverTimeManager"; import searchIndexManager from "../searchIndexManager"; +import { insertInDescendSortedArray } from "../../helpers/array"; export default class DialogsStorage { public dialogs: {[peerId: string]: Dialog} = {}; @@ -181,20 +182,7 @@ export default class DialogsStorage { this.dialogsOffsetDate[dialog.folder_id] = offsetDate; } - const index = dialog.index; - const len = dialogs.length; - if(!len || index < dialogs[len - 1].index) { - dialogs.push(dialog); - } else if(index >= dialogs[0].index) { - dialogs.unshift(dialog); - } else { - for(let i = 0; i < len; i++) { - if(index > dialogs[i].index) { - dialogs.splice(i, 0, dialog); - break; - } - } - } + insertInDescendSortedArray(dialogs, dialog, 'index', pos); } public dropDialog(peerId: number): [Dialog, number] | [] {