From 97d645f1ea5c65ed0c71db0c5874f3c3589b1e1e Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Mon, 28 Mar 2022 18:16:12 +0300 Subject: [PATCH] Fix jumping scroll again --- git-serve-server.js | 15 ++- src/components/animationIntersector.ts | 18 ++- src/components/chat/bubbles.ts | 127 ++++++++++--------- src/components/chat/pinnedMessage.ts | 2 +- src/components/wrappers.ts | 16 ++- src/lib/appManagers/appAvatarsManager.ts | 2 + src/lib/appManagers/appMessagesManager.ts | 136 +++++++++++++++++---- src/lib/appManagers/appReactionsManager.ts | 2 +- src/lib/rootScope.ts | 4 +- 9 files changed, 224 insertions(+), 98 deletions(-) diff --git a/git-serve-server.js b/git-serve-server.js index 3a51bb1a..d3f41d4c 100644 --- a/git-serve-server.js +++ b/git-serve-server.js @@ -40,20 +40,31 @@ for(const name of Object.keys(nets)) { } const useHttp = false; -const server = useHttp ? http : https; +const transport = useHttp ? http : https; let options = {}; if(!useHttp) { options.key = fs.readFileSync(__dirname + '/certs/server-key.pem'); options.cert = fs.readFileSync(__dirname + '/certs/server-cert.pem'); } +console.log(results); + const port = 3000; const protocol = useHttp ? 'http' : 'https'; console.log('Listening port:', port); function createServer(host) { - server.createServer(options, app).listen(port, host, () => { + const server = transport.createServer(options, app); + server.listen(port, host, () => { console.log('Host:', `${protocol}://${host || 'localhost'}:${port}/`); }); + + server.on('error', (e) => { + // @ts-ignore + if(e.code === 'EADDRINUSE') { + console.log('Address in use:', host); + server.close(); + } + }); } for(const name in results) { diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index 371d2998..5a10e05b 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -47,14 +47,24 @@ export class AnimationIntersector { if(entry.isIntersecting) { this.visible.add(player); this.checkAnimation(player, false); + + /* if(animation instanceof HTMLVideoElement && animation.dataset.src) { + animation.src = animation.dataset.src; + animation.load(); + } */ } else { this.visible.delete(player); this.checkAnimation(player, true); - - if(player.animation instanceof RLottiePlayer/* && player.animation.cachingDelta === 2 */) { + + const animation = player.animation; + if(animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) { //console.warn('will clear cache', player); - player.animation.clearCache(); - } + animation.clearCache(); + }/* else if(animation instanceof HTMLVideoElement && animation.src) { + animation.dataset.src = animation.src; + animation.src = ''; + animation.load(); + } */ } break; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 4a2fae69..580d0870 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -208,8 +208,6 @@ export default class ChatBubbles { private hoverBubble: HTMLElement; private hoverReaction: HTMLElement; - private onUpdateScrollSaver: ScrollSaver; - // private reactions: Map; constructor( @@ -431,8 +429,10 @@ export default class ChatBubbles { const updatePosition = this.chat.type === 'scheduled'; - this.saveOnUpdateScroll(); + const scrollSaver = new ScrollSaver(this.scrollable, true); + scrollSaver.save(); this.safeRenderMessage(mounted.message, true, false, mounted.bubble, updatePosition); + scrollSaver.restore(); if(updatePosition) { (this.messagesQueuePromise || Promise.resolve()).then(() => { @@ -461,43 +461,41 @@ export default class ChatBubbles { this.appendReactionsElementToBubble(bubble, message, changedResults); }); - this.listenerSetter.add(rootScope)('message_reactions', ({message, changedResults}) => { - if(this.peerId !== message.peerId) { - return; - } + this.listenerSetter.add(rootScope)('messages_reactions', (arr) => { + let scrollSaver: ScrollSaver; - const bubble = this.getBubbleByMessage(message); - if(!bubble) { - return; - } - - this.saveOnUpdateScroll(); + for(const {message, changedResults} of arr) { + if(this.peerId !== message.peerId) { + return; + } + + const bubble = this.getBubbleByMessage(message); + if(!bubble) { + return; + } - const key = message.peerId + '_' + message.mid; - const set = REACTIONS_ELEMENTS.get(key); - if(set) { - for(const element of set) { - element.update(message, changedResults); + if(!scrollSaver) { + scrollSaver = new ScrollSaver(this.scrollable, true); + scrollSaver.save(); } - } else { - rootScope.dispatchEvent('missed_reactions_element', {message, changedResults}); + + const key = message.peerId + '_' + message.mid; + const set = REACTIONS_ELEMENTS.get(key); + if(set) { + for(const element of set) { + element.update(message, changedResults); + } + } else { + rootScope.dispatchEvent('missed_reactions_element', {message, changedResults}); + } + } + + if(scrollSaver) { + scrollSaver.restore(); } }); } - /* this.listenerSetter.add(rootScope)('message_reactions', ({peerId, mid}) => { - if(this.peerId !== peerId) { - return; - } - - const reactionsElement = this.reactions.get(mid); - if(!reactionsElement) { - return; - } - - - }); */ - this.listenerSetter.add(rootScope)('album_edit', ({peerId, groupId, deletedMids}) => { //fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited if(peerId !== this.peerId) return; @@ -767,24 +765,35 @@ export default class ChatBubbles { } }); - this.listenerSetter.add(rootScope)('message_views', ({peerId, views, mid}) => { - if(this.peerId !== peerId) return; - + this.listenerSetter.add(rootScope)('messages_views', (arr) => { fastRaf(() => { - const bubble = this.bubbles[mid]; - if(!bubble) return; + let scrollSaver: ScrollSaver; + for(const {peerId, views, mid} of arr) { + if(this.peerId !== peerId) return; - const postViewsElements = Array.from(bubble.querySelectorAll('.post-views')) as HTMLElement[]; - if(postViewsElements.length) { - const str = formatNumber(views, 1); - let different = false; - postViewsElements.forEach(postViews => { - if(different || postViews.innerHTML !== str) { - this.saveOnUpdateScroll(); - different = true; - postViews.innerHTML = str; - } - }); + const bubble = this.bubbles[mid]; + if(!bubble) return; + + const postViewsElements = Array.from(bubble.querySelectorAll('.post-views')) as HTMLElement[]; + if(postViewsElements.length) { + const str = formatNumber(views, 1); + let different = false; + postViewsElements.forEach(postViews => { + if(different || postViews.innerHTML !== str) { + if(!scrollSaver) { + scrollSaver = new ScrollSaver(this.scrollable, true); + scrollSaver.save(); + } + + different = true; + postViews.innerHTML = str; + } + }); + } + } + + if(scrollSaver) { + scrollSaver.restore(); } }); }); @@ -928,18 +937,6 @@ export default class ChatBubbles { } } - private saveOnUpdateScroll() { - if(!this.onUpdateScrollSaver) { - this.onUpdateScrollSaver = new ScrollSaver(this.scrollable, true); - setTimeout(() => { - this.onUpdateScrollSaver.restore(); - this.onUpdateScrollSaver = undefined; - }, 0); - - this.onUpdateScrollSaver.save(); - } - } - private onBubblesMouseMove = (e: MouseEvent) => { const content = findUpClassName(e.target, 'bubble-content'); if(content && !this.chat.selection.isSelecting) { @@ -1048,6 +1045,8 @@ export default class ChatBubbles { }; public setStickyDateManually() { + // return; + const timestamps = Object.keys(this.dateMessages).map(k => +k).sort((a, b) => b - a); let lastVisible: HTMLElement; @@ -1716,7 +1715,7 @@ export default class ChatBubbles { //lottieLoader.checkAnimations(false, 'chat'); const distanceToEnd = this.scrollable.getDistanceToEnd(); - if(!IS_TOUCH_SUPPORTED && this.scrollable.lastScrollDirection !== 0 && distanceToEnd > 0) { + if(/* !IS_TOUCH_SUPPORTED && */this.scrollable.lastScrollDirection !== 0 && distanceToEnd > 0) { if(this.isScrollingTimeout) { clearTimeout(this.isScrollingTimeout); } else if(!this.chatInner.classList.contains('is-scrolling')) { @@ -1767,7 +1766,7 @@ export default class ChatBubbles { this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false); //this.scrollable.attachSentinels(undefined, 300); - if(IS_TOUCH_SUPPORTED) { + if(IS_TOUCH_SUPPORTED && false) { this.scrollable.container.addEventListener('touchmove', () => { if(this.isScrollingTimeout) { clearTimeout(this.isScrollingTimeout); @@ -3782,7 +3781,7 @@ export default class ChatBubbles { } private appendReactionsElementToBubble(bubble: HTMLElement, message: Message.message, changedResults?: ReactionCount[]) { - if(this.peerId.isUser()) { + if(this.peerId.isUser()/* || true */) { return; } diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index e8da3ad2..07e76e10 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -343,7 +343,7 @@ export default class ChatPinnedMessage { public setCorrectIndex(lastScrollDirection?: number) { if(this.isStatic) return; - //return; + // return; if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) { return; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index cebdc57b..4352378d 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -451,13 +451,23 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai }); if(doc.type === 'video') { - video.addEventListener('timeupdate', () => { + const onTimeUpdate = () => { + if(!video.videoWidth) { + return; + } + spanTime.innerText = toHHMMSS(video.duration - video.currentTime, false); - }); + }; + + const throttledTimeUpdate = throttleWithRaf(onTimeUpdate); + + video.addEventListener('timeupdate', throttledTimeUpdate); if(spanPlay) { video.addEventListener('timeupdate', () => { - spanPlay.remove(); + sequentialDom.mutateElement(spanPlay, () => { + spanPlay.remove(); + }); }, {once: true}); } } diff --git a/src/lib/appManagers/appAvatarsManager.ts b/src/lib/appManagers/appAvatarsManager.ts index d4d92056..ae0288c7 100644 --- a/src/lib/appManagers/appAvatarsManager.ts +++ b/src/lib/appManagers/appAvatarsManager.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import { MOUNT_CLASS_TO } from "../../config/debug"; import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl"; import replaceContent from "../../helpers/dom/replaceContent"; import sequentialDom from "../../helpers/sequentialDom"; @@ -220,4 +221,5 @@ export class AppAvatarsManager { } const appAvatarsManager = new AppAvatarsManager(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appAvatarsManager = appAvatarsManager); export default appAvatarsManager; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index d3f5df75..2e6db9b0 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -15,8 +15,8 @@ import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePr import { formatDateAccordingToTodayNew, formatTime, tsNow } from "../../helpers/date"; import { createPosterForVideo } from "../../helpers/files"; import { randomLong } from "../../helpers/random"; -import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, ReactionCount, MessagePeerReaction, MessagesSearchCounter, Peer } from "../../layer"; -import { InvokeApiOptions } from "../../types"; +import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, ReactionCount, MessagePeerReaction, MessagesSearchCounter, Peer, MessageReactions } from "../../layer"; +import { ArgumentTypes, InvokeApiOptions } from "../../types"; import I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack"; import { logger, LogTypes } from "../logger"; import type { ApiFileManager } from '../mtproto/apiFileManager'; @@ -70,6 +70,7 @@ import deepEqual from "../../helpers/object/deepEqual"; import escapeRegExp from "../../helpers/string/escapeRegExp"; import limitSymbols from "../../helpers/string/limitSymbols"; import splitStringByLength from "../../helpers/string/splitStringByLength"; +import debounce from "../../helpers/schedulers/debounce"; //console.trace('include'); // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках @@ -128,6 +129,13 @@ type PendingAfterMsg = Partial; +type MapValueType = A extends Map ? V : never; + +export type BatchUpdates = { + 'messages_reactions': AppMessagesManager['batchUpdateReactions'], + 'messages_views': AppMessagesManager['batchUpdateViews'] +}; + export class AppMessagesManager { private messagesStorageByPeerId: {[peerId: string]: MessagesStorage}; public groupedMessagesStorage: {[groupId: string]: MessagesStorage}; // will be used for albums @@ -214,6 +222,14 @@ export class AppMessagesManager { private unreadMentions: {[peerId: PeerId]: SlicedArray} = {}; private goToNextMentionPromises: {[peerId: PeerId]: Promise} = {}; + + private batchUpdates: { + [k in keyof BatchUpdates]?: { + callback: BatchUpdates[k], + batch: ArgumentTypes[0] + } + } = {}; + private batchUpdatesDebounced: () => Promise; constructor() { this.clear(); @@ -345,6 +361,20 @@ export class AppMessagesManager { this.maxSeenId = state.maxSeenMsgId; } }); + + this.batchUpdatesDebounced = debounce(() => { + for(const event in this.batchUpdates) { + const details = this.batchUpdates[event as keyof BatchUpdates]; + delete this.batchUpdates[event as keyof BatchUpdates]; + + // @ts-ignore + const result = details.callback(details.batch); + if(result && (!(result instanceof Array) || result.length)) { + // @ts-ignore + rootScope.dispatchEvent(event as keyof BatchUpdates, result); + } + } + }, 33, false, true); } public clear() { @@ -4700,26 +4730,10 @@ export class AppMessagesManager { } } - const results = reactions?.results ?? []; - const previousResults = message.reactions?.results ?? []; - const changedResults = results.filter(reactionCount => { - const previousReactionCount = previousResults.find(_reactionCount => _reactionCount.reaction === reactionCount.reaction); - return ( - message.pFlags.out && ( - !previousReactionCount || - reactionCount.count > previousReactionCount.count - ) - ) || ( - reactionCount.pFlags.chosen && ( - !previousReactionCount || - !previousReactionCount.pFlags.chosen - ) - ); - }); - message.reactions = reactions; - rootScope.dispatchEvent('message_reactions', {message, changedResults}); + const key = message.peerId + '_' + message.mid; + this.pushBatchUpdate('messages_reactions', this.batchUpdateReactions, key, () => copy(message.reactions)); if(!update.local) { this.setDialogToStateIfMessageIsTop(message); @@ -5094,7 +5108,7 @@ export class AppMessagesManager { const message: Message.message = this.getMessageByPeer(peerId, mid); if(!message.deleted && message.views !== undefined && message.views < views) { message.views = views; - rootScope.dispatchEvent('message_views', {peerId, mid, views}); + this.pushBatchUpdate('messages_views', this.batchUpdateViews, message.peerId + '_' + message.mid); this.setDialogToStateIfMessageIsTop(message); } }; @@ -6283,6 +6297,86 @@ export class AppMessagesManager { public canForward(message: Message.message | Message.messageService) { return !(message as Message.message).pFlags.noforwards && !appPeersManager.noForwards(message.peerId); } + + private pushBatchUpdate( + event: E, + callback: C, + key: string, + getElementCallback?: () => MapValueType[0]> + ) { + let details = this.batchUpdates[event]; + if(!details) { + // @ts-ignore + details = this.batchUpdates[event] = { + callback, + batch: new Map() + }; + } + + if(!details.batch.has(key)) { + // @ts-ignore + details.batch.set(key, getElementCallback ? getElementCallback() : undefined); + this.batchUpdatesDebounced(); + } + } + + private getMessagesFromMap>(map: T) { + const newMap: Map> = new Map(); + for(const [key, value] of map) { + const [peerIdStr, mid] = key.split('_'); + const message: Message.message | Message.messageEmpty = this.getMessageByPeer(peerIdStr.toPeerId(), +mid); + if(message._ === 'messageEmpty') { + continue; + } + + newMap.set(message, value); + } + + return newMap; + } + + private batchUpdateViews = (batch: Map) => { + const toDispatch: {peerId: PeerId, mid: number, views: number}[] = []; + + const map = this.getMessagesFromMap(batch); + for(const [message] of map) { + toDispatch.push({ + peerId: message.peerId, + mid: message.mid, + views: message.views + }) + } + + return toDispatch; + }; + + private batchUpdateReactions = (batch: Map) => { + const toDispatch: {message: Message.message, changedResults: ReactionCount.reactionCount[]}[] = []; + + const map = this.getMessagesFromMap(batch); + for(const [message, reactions] of map) { + const results = reactions?.results ?? []; + const previousResults = message.reactions?.results ?? []; + const changedResults = results.filter(reactionCount => { + const previousReactionCount = previousResults.find(_reactionCount => _reactionCount.reaction === reactionCount.reaction); + return ( + message.pFlags.out && ( + !previousReactionCount || + reactionCount.count > previousReactionCount.count + ) + ) || ( + reactionCount.pFlags.chosen && ( + !previousReactionCount || + !previousReactionCount.pFlags.chosen + ) + ); + }); + + toDispatch.push({message, changedResults}); + } + + return toDispatch; + }; } const appMessagesManager = new AppMessagesManager(); diff --git a/src/lib/appManagers/appReactionsManager.ts b/src/lib/appManagers/appReactionsManager.ts index ecfc616a..e569ff45 100644 --- a/src/lib/appManagers/appReactionsManager.ts +++ b/src/lib/appManagers/appReactionsManager.ts @@ -320,7 +320,7 @@ export class AppReactionsManager { if(onlyLocal) { message.reactions = reactions; - rootScope.dispatchEvent('message_reactions', {message, changedResults: []}); + rootScope.dispatchEvent('messages_reactions', [{message, changedResults: []}]); return Promise.resolve(); } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index ccfa2a0e..7cc62154 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -76,9 +76,9 @@ export type BroadcastEvents = { //'history_request': void, 'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number}, - 'message_views': {peerId: PeerId, mid: number, views: number}, 'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number, message: MyMessage}, - 'message_reactions': {message: Message.message, changedResults: ReactionCount[]}, + 'messages_views': {peerId: PeerId, mid: number, views: number}[], + 'messages_reactions': {message: Message.message, changedResults: ReactionCount[]}[], 'messages_pending': void, 'messages_read': void, 'messages_downloaded': {peerId: PeerId, mids: number[]},