diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index a6f8d3a2..3c2583b4 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -43,7 +43,7 @@ import PollElement from "../poll"; import AudioElement from "../audio"; import { ChatInvite, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReactionCount, ReplyMarkup, SponsoredMessage, Update, WebPage } from "../../layer"; import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; -import { FocusDirection } from "../../helpers/fastSmoothScroll"; +import { FocusDirection, ScrollStartCallbackDimensions } from "../../helpers/fastSmoothScroll"; import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation } from "../../hooks/useHeavyAnimationCheck"; import { fastRaf, fastRafPromise } from "../../helpers/schedulers"; import { deferredPromise } from "../../helpers/cancellablePromise"; @@ -119,6 +119,7 @@ type GenerateLocalMessageType = IsService extends true ? Message.mess const SPONSORED_MESSAGE_ID_OFFSET = 1; const STICKY_OFFSET = 3; +const SCROLLED_DOWN_THRESHOLD = 300; export default class ChatBubbles { public bubblesContainer: HTMLDivElement; @@ -207,7 +208,8 @@ export default class ChatBubbles { private hoverBubble: HTMLElement; private hoverReaction: HTMLElement; private sliceViewportDebounced: DebounceReturnType; - resizeObserver: ResizeObserver; + private resizeObserver: ResizeObserver; + private willScrollOnLoad: boolean; // private reactions: Map; @@ -1758,21 +1760,38 @@ export default class ChatBubbles { } } - public onScroll = () => { + public onScroll = (ignoreHeavyAnimation?: boolean, scrollDimensions?: ScrollStartCallbackDimensions) => { //return; - - // * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз - if(this.isHeavyAnimationInProgress && this.scrolledDown) { + + if(this.isHeavyAnimationInProgress) { if(this.sliceViewportDebounced) { this.sliceViewportDebounced.clearTimeout(); } - - return; + + // * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз + if(this.scrolledDown && !ignoreHeavyAnimation) { + return; + } + } else { + if(this.chat.topbar.pinnedMessage) { + this.chat.topbar.pinnedMessage.setCorrectIndexThrottled(this.scrollable.lastScrollDirection); + } + + if(this.sliceViewportDebounced) { + this.sliceViewportDebounced(); + } + + this.setStickyDateManually(); } + //lottieLoader.checkAnimations(false, 'chat'); - const distanceToEnd = this.scrollable.getDistanceToEnd(); - if(/* !IS_TOUCH_SUPPORTED && */this.scrollable.lastScrollDirection !== 0 && distanceToEnd > 0) { + if(scrollDimensions && scrollDimensions.distanceToEnd < SCROLLED_DOWN_THRESHOLD && this.scrolledDown) { + return; + } + + const distanceToEnd = scrollDimensions?.distanceToEnd ?? this.scrollable.getDistanceToEnd(); + if(/* !IS_TOUCH_SUPPORTED && */(this.scrollable.lastScrollDirection !== 0 && distanceToEnd > 0) || scrollDimensions) { if(this.isScrollingTimeout) { clearTimeout(this.isScrollingTimeout); } else if(!this.chatInner.classList.contains('is-scrolling')) { @@ -1782,26 +1801,16 @@ export default class ChatBubbles { this.isScrollingTimeout = window.setTimeout(() => { this.chatInner.classList.remove('is-scrolling'); this.isScrollingTimeout = 0; - }, 1350); + }, 1350 + (scrollDimensions?.duration ?? 0)); } - if(distanceToEnd < 300 && (this.scrollable.loadedAll.bottom || this.chat.setPeerPromise || !this.peerId)) { + if(distanceToEnd < SCROLLED_DOWN_THRESHOLD && (this.scrollable.loadedAll.bottom || this.chat.setPeerPromise || !this.peerId)) { this.bubblesContainer.classList.add('scrolled-down'); this.scrolledDown = true; } else if(this.bubblesContainer.classList.contains('scrolled-down')) { this.bubblesContainer.classList.remove('scrolled-down'); this.scrolledDown = false; } - - if(this.chat.topbar.pinnedMessage) { - this.chat.topbar.pinnedMessage.setCorrectIndexThrottled(this.scrollable.lastScrollDirection); - } - - if(this.sliceViewportDebounced) { - this.sliceViewportDebounced(); - } - - this.setStickyDateManually(); }; public setScroll() { @@ -1917,6 +1926,7 @@ export default class ChatBubbles { return; } + this.scrollable.ignoreNextScrollEvent(); if(permanent && this.chat.selection.isSelecting) { this.chat.selection.deleteSelectedMids(this.peerId, mids); } @@ -2080,7 +2090,11 @@ export default class ChatBubbles { const diff = rowsWrapperHeight - 54; return rect.height + diff; */ } : undefined, - fallbackToElementStartWhenCentering + fallbackToElementStartWhenCentering, + startCallback: (dimensions) => { + // this.onScroll(true, this.scrolledDown && dimensions.distanceToEnd <= SCROLLED_DOWN_THRESHOLD ? undefined : dimensions); + this.onScroll(true, dimensions); + } }); // fix flickering date when opening unread chat and focusing message @@ -2409,7 +2423,8 @@ export default class ChatBubbles { this.chat.dispatchEvent('setPeer', lastMsgId, false); } else if(topMessage && !isJump) { //this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight); - scrollable.setScrollTopSilently(scrollable.scrollHeight); + // scrollable.setScrollTopSilently(scrollable.scrollHeight); + this.scrollToEnd(); this.chat.dispatchEvent('setPeer', lastMsgId, true); } @@ -2473,6 +2488,12 @@ export default class ChatBubbles { this.lazyLoadQueue.lock(); + const haveToScrollToBubble = (topMessage && (isJump || samePeer)) || isTarget; + const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); + const scrollFromDown = !fromUp && samePeer; + const scrollFromUp = !scrollFromDown && fromUp/* && (samePeer || forwardingUnread) */; + this.willScrollOnLoad = scrollFromDown || scrollFromUp; + let result: ReturnType; if(!savedPosition) { result = this.getHistory(lastMsgId, true, isJump, additionMsgId); @@ -2547,11 +2568,10 @@ export default class ChatBubbles { const distance = savedPosition.top - top; scrollable.scrollTop += distance; } */ - } else if((topMessage && isJump) || isTarget) { - const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); - if(!fromUp && samePeer) { + } else if(haveToScrollToBubble) { + if(scrollFromDown) { scrollable.setScrollTopSilently(99999); - } else if(fromUp/* && (samePeer || forwardingUnread) */) { + } else if(scrollFromUp) { scrollable.setScrollTopSilently(0); } @@ -2564,7 +2584,7 @@ export default class ChatBubbles { // ! sometimes there can be no bubble if(bubble) { this.scrollToBubble(bubble, followingUnread ? 'start' : 'center', !samePeer ? FocusDirection.Static : undefined); - if(!followingUnread) { + if(!followingUnread && isTarget) { this.highlightBubble(bubble); } } @@ -3971,7 +3991,7 @@ export default class ChatBubbles { if(this.getRenderedLength() && !this.chat.setPeerPromise) { const viewportSlice = this.getViewportSlice(); - this.deleteViewportSlice(viewportSlice); + this.deleteViewportSlice(viewportSlice, true); } scrollSaver.save(); @@ -4032,12 +4052,20 @@ export default class ChatBubbles { const dateMessage = this.dateMessages[timestamp]; dateMessage.div.classList.add('is-sticky'); } */ - + const middleware = this.getMiddleware(); - setTimeout(() => { + const callback = () => { if(!middleware()) return; this.bubblesContainer.classList.add('has-sticky-dates'); - }, 600); + }; + + if(this.willScrollOnLoad) { + callback(); + } else { + setTimeout(callback, 600); + } + } else { + this.willScrollOnLoad = undefined; } } @@ -4489,15 +4517,33 @@ export default class ChatBubbles { }); } - public deleteViewportSlice(slice: ReturnType) { + public deleteViewportSlice(slice: ReturnType, ignoreScrollSaving?: boolean) { + // return; + const {invisibleTop, invisibleBottom} = slice; const invisible = invisibleTop.concat(invisibleBottom); + if(!invisible.length) { + return; + } if(invisibleTop.length) this.setLoaded('top', false); if(invisibleBottom.length) this.setLoaded('bottom', false); const mids = invisible.map(({element}) => +element.dataset.mid); + + let scrollSaver: ScrollSaver; + if(!!invisibleTop.length !== !!invisibleBottom.length && !ignoreScrollSaving) { + scrollSaver = new ScrollSaver(this.scrollable, !!invisibleTop.length); + scrollSaver.save(); + } + this.deleteMessagesByIds(mids, false, true); + + if(scrollSaver) { + scrollSaver.restore(); + } else if(invisibleTop.length) { + this.scrollable.lastScrollPosition = this.scrollable.scrollTop; + } } public sliceViewport(ignoreHeavyAnimation?: boolean) { @@ -4511,7 +4557,7 @@ export default class ChatBubbles { const slice = this.getViewportSlice(); // if(IS_SAFARI) slice.invisibleTop = []; this.deleteViewportSlice(slice); - // scrollSaver.restore(false); + // scrollSaver.restore(); } private setLoaded(side: SliceSides, value: boolean, checkPlaceholders = true) { @@ -4641,7 +4687,7 @@ export default class ChatBubbles { const isBroadcast = this.appPeersManager.isBroadcast(peerId); //console.time('appImManager call getHistory'); - const pageCount = Math.min(30, windowSize.height / 48/* * 1.25 */ | 0); + const pageCount = Math.min(30, windowSize.height / 40/* * 1.25 */ | 0); //const loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount; const realLoadCount = isBroadcast ? 20 : (Object.keys(this.bubbles).length > 0 ? Math.max(35, pageCount) : pageCount); //const realLoadCount = pageCount;//const realLoadCount = 50; @@ -4753,7 +4799,11 @@ export default class ChatBubbles { } if(justLoad) { - this.scrollable.onScroll(); // нужно делать из-за ранней прогрузки + // нужно делать из-за ранней прогрузки + this.scrollable.onScroll(); + // fastRaf(() => { + // this.scrollable.checkForTriggers(); + // }); return true; } //console.timeEnd('appImManager call getHistory'); @@ -4772,6 +4822,8 @@ export default class ChatBubbles { cached = false; promise = processPromise(result); } else if(justLoad) { + // нужно делать из-за ранней прогрузки + this.scrollable.onScroll(); return null; } else { cached = true; diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 6694cf65..2ad18203 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -179,7 +179,8 @@ export class ScrollableBase { this.lastScrollDirection = this.lastScrollPosition === scrollPosition ? 0 : (this.lastScrollPosition < scrollPosition ? 1 : -1); // * 1 - bottom, -1 - top this.lastScrollPosition = scrollPosition; - if(this.onAdditionalScroll && this.lastScrollDirection !== 0) { + // lastScrollDirection check is useless here, every callback should decide on its own + if(this.onAdditionalScroll/* && this.lastScrollDirection !== 0 */) { this.onAdditionalScroll(); } @@ -280,6 +281,12 @@ export default class Scrollable extends ScrollableBase { public setScrollTopSilently(value: number) { this.lastScrollPosition = value; + this.ignoreNextScrollEvent(); + + this.scrollTop = value; + } + + public ignoreNextScrollEvent() { if(this.removeHeavyAnimationListener) { this.removeScrollListener(); this.container.addEventListener('scroll', (e) => { @@ -287,8 +294,6 @@ export default class Scrollable extends ScrollableBase { this.addScrollListener(); }, {capture: true, passive: false, once: true}); } - - this.scrollTop = value; } get scrollHeight() { diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts index 629c30eb..2d015d24 100644 --- a/src/helpers/fastSmoothScroll.ts +++ b/src/helpers/fastSmoothScroll.ts @@ -24,6 +24,15 @@ export enum FocusDirection { }; export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number; +export type ScrollStartCallbackDimensions = { + scrollSize: number, + scrollPosition: number, + distanceToEnd: number, + path: number, + duration: number, + containerRect: DOMRect, + elementRect: DOMRect, +}; export type ScrollOptions = { container: HTMLElement, @@ -35,7 +44,8 @@ export type ScrollOptions = { forceDuration?: number, axis?: 'x' | 'y', getNormalSize?: ScrollGetNormalSizeCallback, - fallbackToElementStartWhenCentering?: HTMLElement + fallbackToElementStartWhenCentering?: HTMLElement, + startCallback?: (dimensions: ScrollStartCallbackDimensions) => void }; export default function fastSmoothScroll(options: ScrollOptions) { @@ -266,6 +276,19 @@ function scrollWithJs(options: ScrollOptions): Promise { }); */ + if(options.startCallback) { + const distanceToEnd = scrollSize - Math.round(target + container[axis === 'y' ? 'offsetHeight' : 'offsetWidth']); + options.startCallback({ + scrollSize, + scrollPosition, + distanceToEnd, + path, + duration, + containerRect, + elementRect + }); + } + return animateSingle(tick, container); }