Sorted user list (by online time)

Sequential dom instead of simple rafs
This commit is contained in:
morethanwords 2021-04-11 03:14:57 +04:00
parent b9ca65650f
commit 41ebab81c7
11 changed files with 290 additions and 89 deletions

View File

@ -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<SearchSuperMediaType, SearchSuperMediaTab> = 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() {

View File

@ -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<any>) {

View File

@ -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<number, SortedUser>;
public sorted: Array<SortedUser>;
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);
}
}
}

View File

@ -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,24 +726,25 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
return new Promise((resolve) => {
renderImageFromUrl(image, cacheContext.url || photo.url, () => {
sequentialDom.mutateElement(container, () => {
container.append(image);
fastRaf(() => {
resolve();
});
//resolve();
if(needFadeIn) {
image.addEventListener('animationend', () => {
sequentialDom.mutate(() => {
image.classList.remove('fade-in');
if(thumbImage) {
thumbImage.remove();
}
});
}, {once: true});
}
});
});
});
};
let loadPromise: Promise<any>;
@ -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');
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();
if(element) {
sequentialDom.mutate(cb);
}
} 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,14 +1035,13 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
if(middleware && !middleware()) return resolve();
renderImageFromUrl(image, doc.url, () => {
sequentialDom.mutateElement(div, () => {
div.append(image);
if(thumbImage) {
thumbImage.classList.add('fade-out');
}
fastRaf(() => {
resolve();
});
if(needFadeIn) {
image.addEventListener('animationend', () => {
@ -1043,6 +1052,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}, {once: true});
}
});
});
};
if(doc.url) r();

View File

@ -36,3 +36,31 @@ export function forEachReverse<T>(array: Array<T>, callback: (value: T, index?:
callback(array[i], i, array);
}
};
export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K extends keyof T>(array: Array<T>, 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);
}

View File

@ -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;

View File

@ -132,8 +132,8 @@ export function fastRaf(callback: NoneToVoidFunction) {
export function doubleRaf() {
return new Promise((resolve) => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(resolve);
fastRaf(() => {
fastRaf(resolve);
});
});
}

View File

@ -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<void>,
write: CancellablePromise<void>
}> = {};
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<void>();
}
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;

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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] | [] {