Fix wrong scroll position when switching chats
Fix detecting sticky dates
This commit is contained in:
parent
41c8024956
commit
51fed91103
@ -68,7 +68,7 @@ import whichChild from "../../helpers/dom/whichChild";
|
||||
import { cancelAnimationByKey } from "../../helpers/animation";
|
||||
import assumeType from "../../helpers/assumeType";
|
||||
import { EmoticonsDropdown } from "../emoticonsDropdown";
|
||||
import debounce from "../../helpers/schedulers/debounce";
|
||||
import debounce, { DebounceReturnType } from "../../helpers/schedulers/debounce";
|
||||
import { SEND_WHEN_ONLINE_TIMESTAMP } from "../../lib/mtproto/constants";
|
||||
import windowSize from "../../helpers/windowSize";
|
||||
import { formatPhoneNumber } from "../../helpers/formatPhoneNumber";
|
||||
@ -118,6 +118,7 @@ let queueId = 0;
|
||||
type GenerateLocalMessageType<IsService> = IsService extends true ? Message.messageService : Message.message;
|
||||
|
||||
const SPONSORED_MESSAGE_ID_OFFSET = 1;
|
||||
const STICKY_OFFSET = 3;
|
||||
|
||||
export default class ChatBubbles {
|
||||
public bubblesContainer: HTMLDivElement;
|
||||
@ -205,7 +206,8 @@ export default class ChatBubbles {
|
||||
|
||||
private hoverBubble: HTMLElement;
|
||||
private hoverReaction: HTMLElement;
|
||||
private sliceViewportDebounced: () => Promise<void>;
|
||||
private sliceViewportDebounced: DebounceReturnType<ChatBubbles['sliceViewport']>;
|
||||
resizeObserver: ResizeObserver;
|
||||
|
||||
// private reactions: Map<number, ReactionsElement>;
|
||||
|
||||
@ -649,18 +651,37 @@ export default class ChatBubbles {
|
||||
});
|
||||
}
|
||||
|
||||
if(false) this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
|
||||
/* if(false) */this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
|
||||
for(const timestamp in this.dateMessages) {
|
||||
const dateMessage = this.dateMessages[timestamp];
|
||||
if(dateMessage.container === target) {
|
||||
dateMessage.div.classList.toggle('is-sticky', stuck);
|
||||
const dateBubble = dateMessage.div;
|
||||
|
||||
// dateMessage.container.classList.add('has-sticky-dates');
|
||||
|
||||
// SetTransition(dateBubble, 'kek', stuck, this.previousStickyDate ? 300 : 0);
|
||||
// if(this.previousStickyDate) {
|
||||
// dateBubble.classList.add('kek');
|
||||
// }
|
||||
|
||||
dateBubble.classList.toggle('is-sticky', stuck);
|
||||
if(stuck) {
|
||||
this.previousStickyDate = dateBubble;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.previousStickyDate) {
|
||||
// fastRaf(() => {
|
||||
// this.bubblesContainer.classList.add('has-sticky-dates');
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
if(!IS_SAFARI) {
|
||||
// this.sliceViewportDebounced = debounce(this.sliceViewport.bind(this), 100, false, true);
|
||||
this.sliceViewportDebounced = debounce(this.sliceViewport.bind(this), 100, false, true);
|
||||
}
|
||||
|
||||
let middleware: ReturnType<ChatBubbles['getMiddleware']>;
|
||||
@ -668,12 +689,20 @@ export default class ChatBubbles {
|
||||
this.isHeavyAnimationInProgress = true;
|
||||
this.lazyLoadQueue.lock();
|
||||
middleware = this.getMiddleware();
|
||||
|
||||
// if(this.sliceViewportDebounced) {
|
||||
// this.sliceViewportDebounced.clearTimeout();
|
||||
// }
|
||||
}, () => {
|
||||
this.isHeavyAnimationInProgress = false;
|
||||
|
||||
if(middleware && middleware()) {
|
||||
this.lazyLoadQueue.unlock();
|
||||
this.lazyLoadQueue.refresh();
|
||||
|
||||
// if(this.sliceViewportDebounced) {
|
||||
// this.sliceViewportDebounced();
|
||||
// }
|
||||
}
|
||||
|
||||
middleware = null;
|
||||
@ -760,7 +789,7 @@ export default class ChatBubbles {
|
||||
});
|
||||
|
||||
if(isScrolledDown) {
|
||||
this.scrollable.scrollTop = 99999;
|
||||
this.scrollable.setScrollTopSilently(99999);
|
||||
} else {
|
||||
this.performHistoryResult([], true, false, undefined);
|
||||
}
|
||||
@ -839,17 +868,24 @@ export default class ChatBubbles {
|
||||
|
||||
this.appMessagesManager.incrementMessageViews(this.peerId, mids);
|
||||
}, 1000, false, true);
|
||||
}
|
||||
|
||||
if('ResizeObserver' in window) {
|
||||
let wasHeight = this.scrollable.container.offsetHeight;
|
||||
private createResizeObserver() {
|
||||
if(!('ResizeObserver' in window) || this.resizeObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.scrollable.container;
|
||||
let wasHeight = container.offsetHeight;
|
||||
let resizing = false;
|
||||
let skip = false;
|
||||
let scrolled = 0;
|
||||
let part = 0;
|
||||
let rAF = 0;
|
||||
let skipNext = true;
|
||||
|
||||
const onResizeEnd = () => {
|
||||
const height = this.scrollable.container.offsetHeight;
|
||||
const height = container.offsetHeight;
|
||||
const isScrolledDown = this.scrollable.isScrolledDown;
|
||||
if(height !== wasHeight && (!skip || !isScrolledDown)) { // * fix opening keyboard while ESG is active, offsetHeight will change right between 'start' and this first frame
|
||||
part += wasHeight - height;
|
||||
@ -879,7 +915,12 @@ export default class ChatBubbles {
|
||||
});
|
||||
};
|
||||
|
||||
const processEntries = (entries: any) => {
|
||||
const processEntries: ResizeObserverCallback = (entries) => {
|
||||
if(skipNext) {
|
||||
skipNext = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(skip) {
|
||||
setEndRAF(false);
|
||||
return;
|
||||
@ -933,10 +974,18 @@ export default class ChatBubbles {
|
||||
wasHeight = height;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const resizeObserver = new ResizeObserver(processEntries);
|
||||
resizeObserver.observe(this.bubblesContainer);
|
||||
const resizeObserver = this.resizeObserver = new ResizeObserver(processEntries);
|
||||
resizeObserver.observe(container);
|
||||
}
|
||||
|
||||
private destroyResizeObserver() {
|
||||
const resizeObserver = this.resizeObserver;
|
||||
if(!resizeObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
resizeObserver.disconnect();
|
||||
this.resizeObserver = undefined;
|
||||
}
|
||||
|
||||
private onBubblesMouseMove = (e: MouseEvent) => {
|
||||
@ -1047,7 +1096,7 @@ export default class ChatBubbles {
|
||||
};
|
||||
|
||||
public setStickyDateManually() {
|
||||
// return;
|
||||
return;
|
||||
|
||||
const timestamps = Object.keys(this.dateMessages).map(k => +k).sort((a, b) => b - a);
|
||||
let lastVisible: HTMLElement;
|
||||
@ -1713,7 +1762,13 @@ export default class ChatBubbles {
|
||||
//return;
|
||||
|
||||
// * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз
|
||||
if(this.isHeavyAnimationInProgress && this.scrolledDown) return;
|
||||
if(this.isHeavyAnimationInProgress && this.scrolledDown) {
|
||||
if(this.sliceViewportDebounced) {
|
||||
this.sliceViewportDebounced.clearTimeout();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
//lottieLoader.checkAnimations(false, 'chat');
|
||||
|
||||
const distanceToEnd = this.scrollable.getDistanceToEnd();
|
||||
@ -1821,7 +1876,7 @@ export default class ChatBubbles {
|
||||
}
|
||||
}
|
||||
|
||||
public deleteMessagesByIds(mids: number[], permanent = true) {
|
||||
public deleteMessagesByIds(mids: number[], permanent = true, ignoreOnScroll?: boolean) {
|
||||
let deleted = false;
|
||||
mids.forEach(mid => {
|
||||
if(!(mid in this.bubbles)) return;
|
||||
@ -1868,8 +1923,11 @@ export default class ChatBubbles {
|
||||
|
||||
animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP);
|
||||
this.deleteEmptyDateGroups();
|
||||
|
||||
if(!ignoreOnScroll) {
|
||||
this.onScroll();
|
||||
}
|
||||
}
|
||||
|
||||
public renderNewMessagesByIds(mids: number[], scrolledDown?: boolean) {
|
||||
if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced
|
||||
@ -1928,7 +1986,7 @@ export default class ChatBubbles {
|
||||
this.scrollable.scrollTop += add; */
|
||||
setPaddingTo = this.chatInner;
|
||||
setPaddingTo.style.paddingTop = clientHeight + 'px';
|
||||
this.scrollable.scrollTop = scrollHeight;
|
||||
this.scrollable.setScrollTopSilently(scrollHeight);
|
||||
this.isTopPaddingSet = true;
|
||||
}
|
||||
}
|
||||
@ -1982,7 +2040,7 @@ export default class ChatBubbles {
|
||||
|
||||
let fallbackToElementStartWhenCentering: HTMLElement;
|
||||
// * if it's a start, then scroll to start of the group
|
||||
if(bubble && position !== 'end' && whichChild(bubble) === (this.stickyIntersector ? 2 : 1)/* && this.chat.setPeerPromise */) {
|
||||
if(bubble && position !== 'end' && whichChild(bubble) === (this.stickyIntersector ? STICKY_OFFSET : 1)/* && this.chat.setPeerPromise */) {
|
||||
const dateGroup = bubble.parentElement;
|
||||
// if(whichChild(dateGroup) === 0) {
|
||||
fallbackToElementStartWhenCentering = dateGroup;
|
||||
@ -2002,7 +2060,7 @@ export default class ChatBubbles {
|
||||
} */
|
||||
|
||||
const isChangingHeight = (this.chat.input.messageInput && this.chat.input.messageInput.classList.contains('is-changing-height')) || this.chat.container.classList.contains('is-toggling-helper');
|
||||
return this.scrollable.scrollIntoViewNew({
|
||||
const promise = this.scrollable.scrollIntoViewNew({
|
||||
element,
|
||||
position,
|
||||
margin,
|
||||
@ -2024,6 +2082,13 @@ export default class ChatBubbles {
|
||||
} : undefined,
|
||||
fallbackToElementStartWhenCentering
|
||||
});
|
||||
|
||||
// fix flickering date when opening unread chat and focusing message
|
||||
if(forceDirection === FocusDirection.Static) {
|
||||
this.scrollable.lastScrollPosition = this.scrollable.scrollTop;
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public scrollToEnd() {
|
||||
@ -2080,11 +2145,7 @@ export default class ChatBubbles {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
public getDateContainerByMessage(message: any, reverse: boolean) {
|
||||
const date = new Date(message.date * 1000);
|
||||
date.setHours(0, 0, 0);
|
||||
const dateTimestamp = date.getTime();
|
||||
if(!this.dateMessages[dateTimestamp]) {
|
||||
private createDateBubble(timestamp: number, date: Date = new Date(timestamp * 1000)) {
|
||||
let dateElement: HTMLElement;
|
||||
|
||||
const today = new Date();
|
||||
@ -2094,7 +2155,7 @@ export default class ChatBubbles {
|
||||
|
||||
if(today.getTime() === date.getTime()) {
|
||||
dateElement = i18n(isScheduled ? 'Chat.Date.ScheduledForToday' : 'Date.Today');
|
||||
} else if(isScheduled && message.date === SEND_WHEN_ONLINE_TIMESTAMP) {
|
||||
} else if(isScheduled && timestamp === SEND_WHEN_ONLINE_TIMESTAMP) {
|
||||
dateElement = i18n('MessageScheduledUntilOnline');
|
||||
} else {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
@ -2127,11 +2188,23 @@ export default class ChatBubbles {
|
||||
|
||||
bubbleContent.append(serviceMsg);
|
||||
bubble.append(bubbleContent);
|
||||
////////this.log('need to render date message', dateTimestamp, str);
|
||||
|
||||
return bubble;
|
||||
}
|
||||
|
||||
public getDateContainerByMessage(message: any, reverse: boolean) {
|
||||
const date = new Date(message.date * 1000);
|
||||
date.setHours(0, 0, 0);
|
||||
const dateTimestamp = date.getTime();
|
||||
if(!this.dateMessages[dateTimestamp]) {
|
||||
const bubble = this.createDateBubble(message.date, date);
|
||||
// bubble.classList.add('is-sticky');
|
||||
const fakeBubble = this.createDateBubble(message.date, date);
|
||||
fakeBubble.classList.add('is-fake');
|
||||
|
||||
const container = document.createElement('section');
|
||||
container.className = 'bubbles-date-group';
|
||||
container.append(bubble);
|
||||
container.append(bubble, fakeBubble);
|
||||
|
||||
this.dateMessages[dateTimestamp] = {
|
||||
div: bubble,
|
||||
@ -2239,6 +2312,8 @@ export default class ChatBubbles {
|
||||
this.viewsMids.clear();
|
||||
}
|
||||
|
||||
this.destroyResizeObserver();
|
||||
|
||||
this.middleware.clean();
|
||||
|
||||
this.onAnimateLadder = undefined;
|
||||
@ -2258,6 +2333,9 @@ export default class ChatBubbles {
|
||||
clearTimeout(this.isScrollingTimeout);
|
||||
this.isScrollingTimeout = 0;
|
||||
}
|
||||
|
||||
this.bubblesContainer.classList.remove('has-sticky-dates');
|
||||
this.scrollable.cancelMeasure();
|
||||
}
|
||||
|
||||
public setPeer(peerId: PeerId, lastMsgId?: number, startParam?: string): {cached?: boolean, promise: Chat['setPeerPromise']} {
|
||||
@ -2331,7 +2409,7 @@ 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.scrollTop = scrollable.scrollHeight;
|
||||
scrollable.setScrollTopSilently(scrollable.scrollHeight);
|
||||
this.chat.dispatchEvent('setPeer', lastMsgId, true);
|
||||
}
|
||||
|
||||
@ -2423,6 +2501,8 @@ export default class ChatBubbles {
|
||||
/* this.ladderDeferred && this.ladderDeferred.resolve();
|
||||
this.ladderDeferred = deferredPromise<void>(); */
|
||||
|
||||
const middleware = this.getMiddleware();
|
||||
|
||||
animationIntersector.lockGroup(CHAT_ANIMATION_GROUP);
|
||||
const setPeerPromise = promise.then(() => {
|
||||
////this.log('setPeer removing preloader');
|
||||
@ -2431,10 +2511,10 @@ export default class ChatBubbles {
|
||||
if(!samePeer) {
|
||||
this.chat.finishPeerChange(isTarget, isJump, lastMsgId, startParam); // * костыль
|
||||
}
|
||||
} else {
|
||||
this.preloader.detach();
|
||||
}
|
||||
|
||||
this.preloader.detach();
|
||||
|
||||
if(this.resolveLadderAnimation) {
|
||||
this.resolveLadderAnimation();
|
||||
this.resolveLadderAnimation = undefined;
|
||||
@ -2455,7 +2535,7 @@ export default class ChatBubbles {
|
||||
|
||||
//if(dialog && lastMsgID && lastMsgID !== topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
|
||||
if(savedPosition) {
|
||||
scrollable.scrollTop = scrollable.lastScrollPosition = savedPosition.top;
|
||||
scrollable.setScrollTopSilently(savedPosition.top);
|
||||
/* const mountedByLastMsgId = this.getMountedBubble(lastMsgId);
|
||||
let bubble: HTMLElement = mountedByLastMsgId?.bubble;
|
||||
if(!bubble?.parentElement) {
|
||||
@ -2470,9 +2550,9 @@ export default class ChatBubbles {
|
||||
} else if((topMessage && isJump) || isTarget) {
|
||||
const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0);
|
||||
if(!fromUp && samePeer) {
|
||||
scrollable.scrollTop = scrollable.lastScrollPosition = 99999;
|
||||
scrollable.setScrollTopSilently(99999);
|
||||
} else if(fromUp/* && (samePeer || forwardingUnread) */) {
|
||||
scrollable.scrollTop = scrollable.lastScrollPosition = 0;
|
||||
scrollable.setScrollTopSilently(0);
|
||||
}
|
||||
|
||||
const mountedByLastMsgId = this.getMountedBubble(lastMsgId);
|
||||
@ -2489,7 +2569,7 @@ export default class ChatBubbles {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollable.scrollTop = scrollable.lastScrollPosition = 99999;
|
||||
scrollable.setScrollTopSilently(99999);
|
||||
}
|
||||
|
||||
this.onScroll();
|
||||
@ -2605,7 +2685,10 @@ export default class ChatBubbles {
|
||||
//console.timeEnd('appImManager setPeer');
|
||||
}).catch(err => {
|
||||
this.log.error('getHistory promise error:', err);
|
||||
if(!middleware()) {
|
||||
this.preloader.detach();
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
@ -2628,6 +2711,8 @@ export default class ChatBubbles {
|
||||
|
||||
this.chatInner.classList.toggle('is-chat', this.chat.isAnyGroup());
|
||||
this.chatInner.classList.toggle('is-channel', isChannel);
|
||||
|
||||
this.createResizeObserver();
|
||||
}
|
||||
|
||||
public renderMessagesQueue(message: any, bubble: HTMLElement, reverse: boolean, promises: Promise<any>[]) {
|
||||
@ -2706,7 +2791,7 @@ export default class ChatBubbles {
|
||||
|
||||
const dateMessage = this.getDateContainerByMessage(message, reverse);
|
||||
if(this.chat.type === 'scheduled' || this.chat.type === 'pinned'/* || true */) { // ! TEMP COMMENTED
|
||||
const offset = this.stickyIntersector ? 2 : 1;
|
||||
const offset = this.stickyIntersector ? STICKY_OFFSET : 1;
|
||||
let children = Array.from(dateMessage.container.children).slice(offset) as HTMLElement[];
|
||||
let i = 0, foundMidOnSameTimestamp = 0;
|
||||
for(; i < children.length; ++i) {
|
||||
@ -2735,7 +2820,7 @@ export default class ChatBubbles {
|
||||
positionElementByIndex(bubble, dateMessage.container, index);
|
||||
} else {
|
||||
if(reverse) {
|
||||
dateMessage.container.insertBefore(bubble, dateMessage.container.children[this.stickyIntersector ? 1 : 0].nextSibling);
|
||||
dateMessage.container.insertBefore(bubble, dateMessage.container.children[this.stickyIntersector ? STICKY_OFFSET - 1 : 0].nextSibling);
|
||||
} else {
|
||||
dateMessage.container.append(bubble);
|
||||
}
|
||||
@ -3880,14 +3965,18 @@ export default class ChatBubbles {
|
||||
this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise);
|
||||
} */
|
||||
|
||||
let scrollSaver: ScrollSaver/* , viewportSlice: ReturnType<ChatBubbles['getViewportSlice']> */;
|
||||
let scrollSaver: ScrollSaver, hadScroll: boolean/* , viewportSlice: ReturnType<ChatBubbles['getViewportSlice']> */;
|
||||
this.messagesQueueOnRender = () => {
|
||||
scrollSaver = new ScrollSaver(this.scrollable, reverse);
|
||||
|
||||
if(this.getRenderedLength() && !this.chat.setPeerPromise) {
|
||||
const viewportSlice = this.getViewportSlice();
|
||||
this.deleteViewportSlice(viewportSlice);
|
||||
}
|
||||
|
||||
scrollSaver.save();
|
||||
const saved = scrollSaver.getSaved();
|
||||
hadScroll = saved.scrollHeight !== saved.clientHeight;
|
||||
};
|
||||
|
||||
if(this.needReflowScroll) {
|
||||
@ -3936,6 +4025,20 @@ export default class ChatBubbles {
|
||||
|
||||
if(scrollSaver) {
|
||||
scrollSaver.restore(history.length === 1 && !reverse ? false : true);
|
||||
|
||||
const state = scrollSaver.getSaved();
|
||||
if(state.scrollHeight !== state.clientHeight) {
|
||||
/* for(const timestamp in this.dateMessages) {
|
||||
const dateMessage = this.dateMessages[timestamp];
|
||||
dateMessage.div.classList.add('is-sticky');
|
||||
} */
|
||||
|
||||
const middleware = this.getMiddleware();
|
||||
setTimeout(() => {
|
||||
if(!middleware()) return;
|
||||
this.bubblesContainer.classList.add('has-sticky-dates');
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -4378,6 +4481,7 @@ export default class ChatBubbles {
|
||||
}
|
||||
|
||||
public getViewportSlice() {
|
||||
// this.log.trace('viewport slice');
|
||||
return getViewportSlice({
|
||||
overflowElement: this.scrollable.container,
|
||||
selector: '.bubbles-date-group .bubble:not(.is-date)',
|
||||
@ -4393,11 +4497,12 @@ export default class ChatBubbles {
|
||||
if(invisibleBottom.length) this.setLoaded('bottom', false);
|
||||
|
||||
const mids = invisible.map(({element}) => +element.dataset.mid);
|
||||
this.deleteMessagesByIds(mids, false);
|
||||
this.deleteMessagesByIds(mids, false, true);
|
||||
}
|
||||
|
||||
public sliceViewport() {
|
||||
if(IS_SAFARI) {
|
||||
public sliceViewport(ignoreHeavyAnimation?: boolean) {
|
||||
// Safari cannot reset the scroll.
|
||||
if(IS_SAFARI || (this.isHeavyAnimationInProgress && !ignoreHeavyAnimation)/* || true */) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -4765,7 +4870,7 @@ export default class ChatBubbles {
|
||||
}
|
||||
|
||||
public deleteEmptyDateGroups() {
|
||||
const mustBeCount = 1 + +!!this.stickyIntersector;
|
||||
const mustBeCount = this.stickyIntersector ? STICKY_OFFSET : 1;
|
||||
let deleted = false;
|
||||
for(const i in this.dateMessages) {
|
||||
const dateMessage = this.dateMessages[i];
|
||||
|
@ -72,7 +72,8 @@ export class ScrollableBase {
|
||||
|
||||
public scrollProperty: 'scrollTop' | 'scrollLeft';
|
||||
|
||||
private removeHeavyAnimationListener: () => void;
|
||||
protected removeHeavyAnimationListener: () => void;
|
||||
protected addedScrollListener: boolean;
|
||||
|
||||
constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
|
||||
this.container.classList.add('scrollable');
|
||||
@ -87,20 +88,38 @@ export class ScrollableBase {
|
||||
//this.onScroll();
|
||||
}
|
||||
|
||||
public addScrollListener() {
|
||||
if(this.addedScrollListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addedScrollListener = true;
|
||||
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
|
||||
}
|
||||
|
||||
public removeScrollListener() {
|
||||
if(!this.addedScrollListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addedScrollListener = false;
|
||||
this.container.removeEventListener('scroll', this.onScroll, {capture: true});
|
||||
}
|
||||
|
||||
public setListeners() {
|
||||
if(this.removeHeavyAnimationListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onScroll, {passive: true});
|
||||
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
|
||||
this.addScrollListener();
|
||||
|
||||
this.removeHeavyAnimationListener = useHeavyAnimationCheck(() => {
|
||||
this.isHeavyAnimationInProgress = true;
|
||||
|
||||
if(this.onScrollMeasure) {
|
||||
this.cancelMeasure();
|
||||
this.needCheckAfterAnimation = true;
|
||||
window.cancelAnimationFrame(this.onScrollMeasure);
|
||||
}
|
||||
}, () => {
|
||||
this.isHeavyAnimationInProgress = false;
|
||||
@ -118,9 +137,10 @@ export class ScrollableBase {
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.onScroll);
|
||||
this.container.removeEventListener('scroll', this.onScroll, {capture: true});
|
||||
this.removeScrollListener();
|
||||
|
||||
this.removeHeavyAnimationListener();
|
||||
this.removeHeavyAnimationListener = undefined;
|
||||
}
|
||||
|
||||
public append(element: HTMLElement) {
|
||||
@ -143,17 +163,15 @@ export class ScrollableBase {
|
||||
//return;
|
||||
|
||||
if(this.isHeavyAnimationInProgress) {
|
||||
if(this.onScrollMeasure) {
|
||||
window.cancelAnimationFrame(this.onScrollMeasure);
|
||||
}
|
||||
|
||||
this.cancelMeasure();
|
||||
this.needCheckAfterAnimation = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp && !this.onAdditionalScroll)) return;
|
||||
if((!this.onScrolledTop && !this.onScrolledBottom) && !this.splitUp && !this.onAdditionalScroll) return;
|
||||
if(this.onScrollMeasure) window.cancelAnimationFrame(this.onScrollMeasure);
|
||||
if(this.onScrollMeasure) return;
|
||||
// if(this.onScrollMeasure) window.cancelAnimationFrame(this.onScrollMeasure);
|
||||
this.onScrollMeasure = window.requestAnimationFrame(() => {
|
||||
this.onScrollMeasure = 0;
|
||||
|
||||
@ -170,6 +188,13 @@ export class ScrollableBase {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public cancelMeasure() {
|
||||
if(this.onScrollMeasure) {
|
||||
window.cancelAnimationFrame(this.onScrollMeasure);
|
||||
this.onScrollMeasure = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SliceSides = 'top' | 'bottom';
|
||||
@ -253,6 +278,19 @@ export default class Scrollable extends ScrollableBase {
|
||||
return this.container.scrollTop;
|
||||
}
|
||||
|
||||
public setScrollTopSilently(value: number) {
|
||||
this.lastScrollPosition = value;
|
||||
if(this.removeHeavyAnimationListener) {
|
||||
this.removeScrollListener();
|
||||
this.container.addEventListener('scroll', (e) => {
|
||||
cancelEvent(e);
|
||||
this.addScrollListener();
|
||||
}, {capture: true, passive: false, once: true});
|
||||
}
|
||||
|
||||
this.scrollTop = value;
|
||||
}
|
||||
|
||||
get scrollHeight() {
|
||||
return this.container.scrollHeight;
|
||||
}
|
||||
|
@ -41,9 +41,12 @@ export default class StickyIntersector {
|
||||
|
||||
private observeElements() {
|
||||
this.elementsObserver = new IntersectionObserver((entries) => {
|
||||
let entry = entries.filter(entry => entry.boundingClientRect.top < 0).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
const entry = entries
|
||||
.filter(entry => entry.boundingClientRect.top < entry.rootBounds.top)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0];
|
||||
if(!entry) return;
|
||||
let container = entry.isIntersecting ? entry.target : entry.target.nextElementSibling;
|
||||
|
||||
const container = entry.isIntersecting ? entry.target : entry.target.nextElementSibling;
|
||||
this.handler(true, container as HTMLElement);
|
||||
}, {root: this.container});
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
// * Jolly Cobra's fastSmoothScroll slightly patched
|
||||
|
||||
import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck';
|
||||
import { fastRaf } from './schedulers';
|
||||
import { fastRaf, fastRafPromise } from './schedulers';
|
||||
import { animateSingle, cancelAnimationByKey } from './animation';
|
||||
import rootScope from '../lib/rootScope';
|
||||
import isInDOM from './dom/isInDOM';
|
||||
@ -67,11 +67,7 @@ export default function fastSmoothScroll(options: ScrollOptions) {
|
||||
return Promise.resolve(); */
|
||||
}
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
fastRaf(() => {
|
||||
scrollWithJs(options).then(resolve);
|
||||
});
|
||||
});
|
||||
const promise = fastRafPromise().then(() => scrollWithJs(options));
|
||||
|
||||
return options.axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise;
|
||||
}
|
||||
|
@ -60,11 +60,11 @@ export function fastRafConventional(callback: NoneToVoidFunction) {
|
||||
}
|
||||
}
|
||||
|
||||
let rafPromise: Promise<DOMHighResTimeStamp>;
|
||||
let rafPromise: Promise<void>;
|
||||
export function fastRafPromise() {
|
||||
if(rafPromise) return rafPromise;
|
||||
|
||||
rafPromise = new Promise(requestAnimationFrame);
|
||||
rafPromise = new Promise<void>((resolve) => fastRaf(() => resolve()));
|
||||
rafPromise.then(() => {
|
||||
rafPromise = undefined;
|
||||
});
|
||||
|
@ -1,18 +1,37 @@
|
||||
// * Jolly Cobra's schedulers
|
||||
|
||||
import ctx from "../../environment/ctx";
|
||||
import { AnyFunction, Awaited } from "../../types";
|
||||
import noop from "../noop";
|
||||
|
||||
export type DebounceReturnType<F extends AnyFunction> = {
|
||||
(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>;
|
||||
clearTimeout(): void;
|
||||
};
|
||||
|
||||
export default function debounce<F extends AnyFunction>(
|
||||
fn: F,
|
||||
ms: number,
|
||||
shouldRunFirst = true,
|
||||
shouldRunLast = true,
|
||||
) {
|
||||
): DebounceReturnType<F> {
|
||||
let waitingTimeout: number;
|
||||
let waitingPromise: Promise<Awaited<ReturnType<F>>>, resolve: (result: any) => void, reject: () => void;
|
||||
let hadNewCall = false;
|
||||
|
||||
return (...args: Parameters<F>): typeof waitingPromise => {
|
||||
const invoke = (args: Parameters<F>) => {
|
||||
const _resolve = resolve, _reject = reject;
|
||||
try {
|
||||
const result = fn.apply(null, args);
|
||||
_resolve(result);
|
||||
} catch(err) {
|
||||
console.error('debounce error', err);
|
||||
// @ts-ignore
|
||||
_reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const debounce = (...args: Parameters<F>) => {
|
||||
if(!waitingPromise) waitingPromise = new Promise((_resolve, _reject) => (resolve = _resolve, reject = _reject));
|
||||
|
||||
if(waitingTimeout) {
|
||||
@ -21,23 +40,36 @@ export default function debounce<F extends AnyFunction>(
|
||||
reject();
|
||||
waitingPromise = new Promise((_resolve, _reject) => (resolve = _resolve, reject = _reject));
|
||||
} else if(shouldRunFirst) {
|
||||
// @ts-ignore
|
||||
resolve(fn(...args));
|
||||
invoke(args);
|
||||
hadNewCall = false;
|
||||
}
|
||||
|
||||
waitingTimeout = setTimeout(() => {
|
||||
const _waitingTimeout = ctx.setTimeout(() => {
|
||||
// will run if should run last or first but with new call
|
||||
if(shouldRunLast && (!shouldRunFirst || hadNewCall)) {
|
||||
// @ts-ignore
|
||||
resolve(fn(...args));
|
||||
invoke(args);
|
||||
}
|
||||
|
||||
// if debounce was called during invoking
|
||||
if(waitingTimeout === _waitingTimeout) {
|
||||
waitingTimeout = waitingPromise = resolve = reject = undefined;
|
||||
hadNewCall = false;
|
||||
}, ms) as any;
|
||||
}
|
||||
}, ms);
|
||||
|
||||
waitingPromise.catch(() => {});
|
||||
waitingTimeout = _waitingTimeout;
|
||||
waitingPromise.catch(noop);
|
||||
return waitingPromise;
|
||||
};
|
||||
|
||||
debounce.clearTimeout = () => {
|
||||
if(waitingTimeout) {
|
||||
ctx.clearTimeout(waitingTimeout);
|
||||
reject();
|
||||
waitingTimeout = waitingPromise = resolve = reject = undefined;
|
||||
hadNewCall = false;
|
||||
}
|
||||
};
|
||||
|
||||
return debounce;
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ import { IS_SAFARI } from "../environment/userAgent";
|
||||
import reflowScrollableElement from "./dom/reflowScrollableElement";
|
||||
|
||||
export default class ScrollSaver {
|
||||
private previousScrollHeight: number;
|
||||
private previousScrollHeightMinusTop: number/* , previousScrollHeight: number */;
|
||||
private scrollHeight: number;
|
||||
private scrollHeightMinusTop: number;
|
||||
private scrollTop: number;
|
||||
private clientHeight: number;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -28,13 +30,23 @@ export default class ScrollSaver {
|
||||
return this.scrollable.container;
|
||||
}
|
||||
|
||||
public getSaved() {
|
||||
return {
|
||||
scrollHeight: this.scrollHeight,
|
||||
scrollTop: this.scrollTop,
|
||||
clientHeight: this.clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
public save() {
|
||||
const {scrollTop, scrollHeight} = this.container;
|
||||
const {scrollTop, scrollHeight, clientHeight} = this.container;
|
||||
|
||||
//previousScrollHeight = scrollHeight;
|
||||
//previousScrollHeight = scrollHeight + padding;
|
||||
this.previousScrollHeight = scrollHeight;
|
||||
this.previousScrollHeightMinusTop = this.reverse ? scrollHeight - scrollTop : scrollTop;
|
||||
this.scrollHeight = scrollHeight;
|
||||
this.scrollTop = scrollTop;
|
||||
this.clientHeight = clientHeight;
|
||||
this.scrollHeightMinusTop = this.reverse ? scrollHeight - scrollTop : scrollTop;
|
||||
|
||||
//this.chatInner.style.paddingTop = padding + 'px';
|
||||
/* if(reverse) {
|
||||
@ -49,13 +61,18 @@ export default class ScrollSaver {
|
||||
}
|
||||
|
||||
public restore(useReflow?: boolean) {
|
||||
const {container, previousScrollHeightMinusTop, scrollable} = this;
|
||||
if(previousScrollHeightMinusTop !== undefined) {
|
||||
const {container, scrollHeightMinusTop: previousScrollHeightMinusTop, scrollable} = this;
|
||||
if(previousScrollHeightMinusTop === undefined) {
|
||||
throw new Error('scroll was not saved');
|
||||
}
|
||||
|
||||
const scrollHeight = container.scrollHeight;
|
||||
if(scrollHeight === this.previousScrollHeight) {
|
||||
if(scrollHeight === this.scrollHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollHeight = scrollHeight;
|
||||
|
||||
/* const scrollHeight = container.scrollHeight;
|
||||
const addedHeight = scrollHeight - previousScrollHeight;
|
||||
|
||||
@ -79,13 +96,10 @@ export default class ScrollSaver {
|
||||
|
||||
// touchSupport for safari iOS
|
||||
//isTouchSupported && isApple && (container.container.style.overflow = 'hidden');
|
||||
container.scrollTop = newScrollTop;
|
||||
this.scrollable.setScrollTopSilently(this.scrollTop = newScrollTop);
|
||||
//container.scrollTop = scrollHeight;
|
||||
//isTouchSupported && isApple && (container.container.style.overflow = '');
|
||||
|
||||
scrollable.lastScrollPosition = newScrollTop;
|
||||
// scrollable.lastScrollDirection = 0;
|
||||
|
||||
if(IS_SAFARI && useReflow/* && !isAppleMobile */) { // * fix blinking and jumping
|
||||
reflowScrollableElement(container);
|
||||
}
|
||||
@ -93,6 +107,7 @@ export default class ScrollSaver {
|
||||
/* if(DEBUG) {
|
||||
this.log('performHistoryResult: have set up scrollTop:', newScrollTop, container.scrollTop, container.scrollHeight, this.isHeavyAnimationInProgress);
|
||||
} */
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1135,15 +1135,12 @@ export class AppImManager {
|
||||
//if(bubble) {
|
||||
//const top = bubble.getBoundingClientRect().top;
|
||||
const chatBubbles = chat.bubbles;
|
||||
|
||||
chatBubbles.sliceViewport();
|
||||
|
||||
const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : '');
|
||||
const chatPositions = stateStorage.getFromCache('chatPositions');
|
||||
if(!(chatBubbles.scrollable.getDistanceToEnd() <= 16 && chatBubbles.scrollable.loadedAll.bottom) && chatBubbles.getRenderedLength()) {
|
||||
chatBubbles.sliceViewport(true);
|
||||
const top = chatBubbles.scrollable.scrollTop;
|
||||
|
||||
const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : '');
|
||||
|
||||
const chatPositions = stateStorage.getFromCache('chatPositions');
|
||||
if(!(chatBubbles.scrollable.getDistanceToEnd() <= 16 && chatBubbles.scrollable.loadedAll.bottom) && Object.keys(chatBubbles.bubbles).length) {
|
||||
const position = {
|
||||
mids: getObjectKeysAndSort(chatBubbles.bubbles, 'desc'),
|
||||
top
|
||||
|
@ -399,6 +399,24 @@ $bubble-beside-button-width: 38px;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.bubbles:not(.has-sticky-dates) & {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.bubbles.has-sticky-dates &.is-fake {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-fake {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 1 !important;
|
||||
transition: none !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-beside-button {
|
||||
|
Loading…
Reference in New Issue
Block a user