diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index ec8a340b..1a33fd27 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -32,7 +32,7 @@ export class AnimationIntersector { continue; } - const player = this.byGroups[group].find(p => p.el == target); + const player = this.byGroups[group].find(p => p.el === target); if(player) { if(entry.isIntersecting) { this.visible.add(player); @@ -58,7 +58,7 @@ export class AnimationIntersector { const found: AnimationItem[] = []; for(const group in this.byGroups) { for(const player of this.byGroups[group]) { - if(player.el == element) { + if(player.el === element) { found.push(player); } } @@ -80,7 +80,7 @@ export class AnimationIntersector { } for(const group in this.byGroups) { - this.byGroups[group].findAndSplice(p => p == player); + this.byGroups[group].findAndSplice(p => p === player); } this.observer.unobserve(el); @@ -132,13 +132,17 @@ export class AnimationIntersector { return; } - if(blurred) { + if(blurred || (this.onlyOnePlayableGroup && this.onlyOnePlayableGroup !== group)) { if(!animation.paused) { //console.warn('pause animation:', animation); animation.pause(); } - } else if(animation.paused && this.visible.has(player) && animation.autoplay && (!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup == group)) { - //console.warn('play animation:', animation); + } else if(animation.paused && + this.visible.has(player) && + animation.autoplay && + (!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) + ) { + console.warn('play animation:', animation); animation.play(); } } diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 8ca6a7fa..da943dc3 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -7,6 +7,7 @@ import appUsersManager from "../lib/appManagers/appUsersManager"; import rootScope from "../lib/rootScope"; import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom"; import Scrollable from "./scrollable"; +import { FocusDirection } from "../helpers/fastSmoothScroll"; type PeerType = 'contacts' | 'dialogs'; @@ -38,11 +39,31 @@ export default class AppSelectPeers { private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts']: true}> = {}; private renderedPeerIds: Set = new Set(); + + private appendTo: HTMLElement; + private onChange: (length: number) => void; + private peerType: PeerType[] = ['dialogs']; + private renderResultsFunc?: (peerIds: number[]) => void; + private chatRightsAction?: ChatRights; + private multiSelect = true; - constructor(private appendTo: HTMLElement, private onChange?: (length: number) => void, private peerType: PeerType[] = ['dialogs'], onFirstRender?: () => void, private renderResultsFunc?: (peerIds: number[]) => void, private chatRightsAction?: ChatRights, private multiSelect = true) { + constructor(options: { + appendTo: AppSelectPeers['appendTo'], + onChange?: AppSelectPeers['onChange'], + peerType?: AppSelectPeers['peerType'], + onFirstRender?: () => void, + renderResultsFunc?: AppSelectPeers['renderResultsFunc'], + chatRightsAction?: AppSelectPeers['chatRightsAction'], + multiSelect?: AppSelectPeers['multiSelect'] + }) { + for(let i in options) { + // @ts-ignore + this[i] = options[i]; + } + this.container.classList.add('selector'); - const f = (renderResultsFunc || this.renderResults).bind(this); + const f = (this.renderResultsFunc || this.renderResults).bind(this); this.renderResultsFunc = (peerIds: number[]) => { peerIds = peerIds.filter(peerId => { const notRendered = !this.renderedPeerIds.has(peerId); @@ -55,10 +76,10 @@ export default class AppSelectPeers { this.input = document.createElement('input'); this.input.classList.add('selector-search-input'); - this.input.placeholder = !peerType.includes('dialogs') ? 'Add People...' : 'Select chat'; + this.input.placeholder = !this.peerType.includes('dialogs') ? 'Add People...' : 'Select chat'; this.input.type = 'text'; - if(multiSelect) { + if(this.multiSelect) { let topContainer = document.createElement('div'); topContainer.classList.add('selector-search-container'); @@ -151,14 +172,14 @@ export default class AppSelectPeers { }; this.container.append(this.chatsContainer); - appendTo.append(this.container); + this.appendTo.append(this.container); // WARNING TIMEOUT setTimeout(() => { let getResultsPromise = this.getMoreResults() as Promise; - if(onFirstRender) { + if(options.onFirstRender) { getResultsPromise.then(() => { - onFirstRender(); + options.onFirstRender(); }); } }, 0); @@ -346,7 +367,7 @@ export default class AppSelectPeers { }); } - public add(peerId: any, title?: string) { + public add(peerId: any, title?: string, scroll = true) { //console.trace('add'); this.selected.add(peerId); @@ -380,9 +401,12 @@ export default class AppSelectPeers { this.selectedContainer.insertBefore(div, this.input); //this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight; - this.selectedScrollable.scrollTo(this.selectedScrollable.scrollHeight, 'top', true, true); this.onChange && this.onChange(this.selected.size); - + + if(scroll) { + this.selectedScrollable.scrollIntoViewNew(this.input, 'center'); + } + return div; } @@ -410,4 +434,12 @@ export default class AppSelectPeers { public getSelected() { return [...this.selected]; } + + public addInitial(values: any[]) { + values.forEach(value => { + this.add(value, undefined, false); + }); + + this.selectedScrollable.scrollIntoViewNew(this.input, 'center', undefined, undefined, FocusDirection.Static); + } } \ No newline at end of file diff --git a/src/components/chat/bubbleGroups.ts b/src/components/chat/bubbleGroups.ts index f66f692f..6721f4da 100644 --- a/src/components/chat/bubbleGroups.ts +++ b/src/components/chat/bubbleGroups.ts @@ -28,6 +28,8 @@ export default class BubbleGroups { } addBubble(bubble: HTMLDivElement, message: MyMessage, reverse: boolean) { + //return; + const timestamp = message.date; const mid = message.mid; let fromId = message.fromId; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 48b75d53..34b842c4 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -39,6 +39,9 @@ import PollElement from "../poll"; import AudioElement from "../audio"; import { Message, MessageEntity, MessageReplies, MessageReplyHeader } from "../../layer"; import { DEBUG, MOUNT_CLASS_TO, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; +import { FocusDirection } from "../../helpers/fastSmoothScroll"; +import useHeavyAnimationCheck, { getHeavyAnimationPromise } from "../../hooks/useHeavyAnimationCheck"; +import { fastRaf } from "../../helpers/schedulers"; const IGNORE_ACTIONS = ['messageActionHistoryClear']; @@ -105,6 +108,9 @@ export default class ChatBubbles { public replyFollowHistory: number[] = []; + public isHeavyAnimationInProgress = false; + public scrollingToNewBubble: HTMLElement; + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { //this.chat.log.error('Bubbles construction'); @@ -146,6 +152,10 @@ export default class ChatBubbles { this.bubbleGroups.addBubble(bubble, message, false); + if(this.scrollingToNewBubble) { + this.scrollToNewLastBubble(); + } + //this.renderMessage(message, false, false, bubble); } }); @@ -341,6 +351,12 @@ export default class ChatBubbles { } } }); + + useHeavyAnimationCheck(() => { + this.isHeavyAnimationInProgress = true; + }, () => { + this.isHeavyAnimationInProgress = false; + }, this.listenerSetter); } public constructPeerHelpers() { @@ -789,7 +805,15 @@ export default class ChatBubbles { public loadMoreHistory(top: boolean, justLoad = false) { //this.log('loadMoreHistory', top); - if(!this.peerId || /* TEST_SCROLL || */ this.chat.setPeerPromise || (top && this.getHistoryTopPromise) || (!top && this.getHistoryBottomPromise)) return; + if(!this.peerId || + /* TEST_SCROLL || */ + this.chat.setPeerPromise || + this.isHeavyAnimationInProgress || + (top && this.getHistoryTopPromise) || + (!top && this.getHistoryBottomPromise) + ) { + return; + } // warning, если иды только отрицательные то вниз не попадёт (хотя мб и так не попадёт) const history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b); @@ -817,8 +841,10 @@ export default class ChatBubbles { } public onScroll = () => { + //return; + // * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз - if(this.scrollable.scrollLocked && this.scrolledDown) return; + if(this.isHeavyAnimationInProgress && this.scrolledDown) return; //lottieLoader.checkAnimations(false, 'chat'); if(!isTouchSupported) { @@ -947,7 +973,7 @@ export default class ChatBubbles { public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) { if(!this.scrolledAllDown) { // seems search active or sliced - this.log('seems search is active, skipping render:', mids); + this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids); return; } @@ -960,25 +986,15 @@ export default class ChatBubbles { } mids = mids.filter(mid => !this.bubbles[mid]); - mids.forEach((mid: number) => { - const message = this.chat.getMessage(mid); - - /////////this.log('got new message to append:', message); - - //this.unreaded.push(msgID); - this.renderMessage(message); - }); - - //if(scrolledDown) this.scrollable.scrollTop = this.scrollable.scrollHeight; - if(this.messagesQueuePromise && scrolledDown/* && false */) { - if(this.scrollable.isScrolledDown && !this.scrollable.scrollLocked) { - //this.log('renderNewMessagesByIDs: messagesQueuePromise before will set prev max'); - this.scrollable.scrollTo(this.scrollable.scrollHeight - 1, 'top', false, true); - } - - this.messagesQueuePromise.then(() => { + const promise = this.performHistoryResult(mids, false, true); + if(scrolledDown) { + promise.then(() => { //this.log('renderNewMessagesByIDs: messagesQueuePromise after', this.scrollable.isScrolledDown); - this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true); + //this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true, 5000); + //const bubble = this.bubbles[Math.max(...mids)]; + this.scrollToNewLastBubble(); + + //this.scrollable.scrollIntoViewNew(this.chatInner, 'end'); /* setTimeout(() => { this.log('messagesQueuePromise afterafter:', this.chatInner.childElementCount, this.scrollable.scrollHeight); @@ -987,6 +1003,17 @@ export default class ChatBubbles { } } + public scrollToNewLastBubble() { + const bubble = this.chatInner.lastElementChild.lastElementChild as HTMLElement; + this.log('scrollToNewLastBubble: will scroll into view:', bubble); + if(bubble) { + this.scrollingToNewBubble = bubble; + this.scrollable.scrollIntoViewNew(bubble, 'end').then(() => { + this.scrollingToNewBubble = null; + }); + } + } + public highlightBubble(element: HTMLElement) { const datasetKey = 'highlightTimeout'; if(element.dataset[datasetKey]) { @@ -1168,7 +1195,7 @@ export default class ChatBubbles { const mounted = this.getMountedBubble(lastMsgId); if(mounted) { if(isTarget) { - this.scrollable.scrollIntoView(mounted.bubble); + this.scrollable.scrollIntoViewNew(mounted.bubble, 'center'); this.highlightBubble(mounted.bubble); this.chat.setListenerResult('setPeer', lastMsgId, false); } else if(topMessage && !isJump) { @@ -1269,11 +1296,6 @@ export default class ChatBubbles { //if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { if((topMessage && isJump) || isTarget) { - if(this.scrollable.scrollLocked) { - clearTimeout(this.scrollable.scrollLocked); - this.scrollable.scrollLocked = 0; - } - const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); const forwardingUnread = historyStorage.readMaxId === lastMsgId && !isTarget; if(!fromUp && (samePeer || forwardingUnread)) { @@ -1290,7 +1312,7 @@ export default class ChatBubbles { // ! sometimes there can be no bubble if(bubble) { - this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */); + this.scrollable.scrollIntoViewNew(bubble, forwardingUnread ? 'start' : 'center', undefined, undefined, !samePeer ? FocusDirection.Static : undefined); if(!forwardingUnread) { this.highlightBubble(bubble); } @@ -1395,43 +1417,50 @@ export default class ChatBubbles { this.messagesQueue.push({message, bubble, reverse, promises}); - if(!this.messagesQueuePromise) { - this.messagesQueuePromise = new Promise((resolve, reject) => { - setTimeout(() => { - const chatInner = this.chatInner; - const queue = this.messagesQueue.slice(); - this.messagesQueue.length = 0; + this.setMessagesQueuePromise(); + } - const promises = queue.reduce((acc, {promises}) => acc.concat(promises), []); + public setMessagesQueuePromise() { + if(this.messagesQueuePromise) return; - // * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался - if(this.scrollable.scrollLocked) { - promises.push(this.scrollable.scrollLockedPromise); + this.messagesQueuePromise = new Promise((resolve, reject) => { + setTimeout(() => { + const chatInner = this.chatInner; + const queue = this.messagesQueue.slice(); + this.messagesQueue.length = 0; + + const promises = queue.reduce((acc, {promises}) => acc.concat(promises), []); + + // * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался + // * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера + // promises.push(getHeavyAnimationPromise()); + + //this.log('promises to call', promises, queue); + Promise.all(promises).then(() => { + if(this.chatInner != chatInner) { + //this.log.warn('chatInner changed!', this.chatInner, chatInner); + return reject('chatInner changed!'); } - //this.log('promises to call', promises, queue); - Promise.all(promises).then(() => { - if(this.chatInner != chatInner) { - //this.log.warn('chatInner changed!', this.chatInner, chatInner); - return reject('chatInner changed!'); - } + if(this.messagesQueueOnRender) { + this.messagesQueueOnRender(); + } - if(this.messagesQueueOnRender) { - this.messagesQueueOnRender(); - } + queue.forEach(({message, bubble, reverse}) => { + this.setBubblePosition(bubble, message, reverse); + }); - queue.forEach(({message, bubble, reverse}) => { - this.setBubblePosition(bubble, message, reverse); - }); + //setTimeout(() => { + resolve(); + //}, 500); + this.messagesQueuePromise = null; - //setTimeout(() => { - resolve(); - //}, 500); - this.messagesQueuePromise = null; - }, reject); - }, 0); - }); - } + if(this.messagesQueue.length) { + this.setMessagesQueuePromise(); + } + }, reject); + }, 0); + }); } public setBubblePosition(bubble: HTMLElement, message: any, reverse: boolean) { @@ -2259,14 +2288,14 @@ export default class ChatBubbles { return new Promise((resolve, reject) => { //await new Promise((resolve) => setTimeout(resolve, 1e3)); - //this.log('performHistoryResult: will render some messages:', history.length); + this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress); const method = (reverse ? history.shift : history.pop).bind(history); //const padding = 10000; - const realLength = this.scrollable.container.childElementCount; + //const realLength = this.scrollable.container.childElementCount; let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */; - if(realLength > 0 && (reverse || isSafari)) { // for safari need set when scrolling bottom too + //if(realLength > 0/* && (reverse || isSafari) */) { // for safari need set when scrolling bottom too this.messagesQueueOnRender = () => { const {scrollTop, scrollHeight} = this.scrollable; @@ -2284,7 +2313,7 @@ export default class ChatBubbles { //this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop); this.messagesQueueOnRender = undefined; }; - } + //} while(history.length) { let message = this.chat.getMessage(method()); @@ -2297,28 +2326,29 @@ export default class ChatBubbles { if(previousScrollHeightMinusTop !== undefined) { /* const scrollHeight = this.scrollable.scrollHeight; const addedHeight = scrollHeight - previousScrollHeight; - + this.chatInner.style.paddingTop = (10000 - addedHeight) + 'px'; */ /* const scrollHeight = this.scrollable.scrollHeight; const addedHeight = scrollHeight - previousScrollHeight; - + this.chatInner.style.paddingTop = (padding - addedHeight) + 'px'; - + //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; const newScrollTop = reverse ? scrollHeight - addedHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; this.log('performHistoryResult: will set scrollTop', - previousScrollHeightMinusTop, this.scrollable.scrollHeight, - newScrollTop, this.scrollable.container.clientHeight); */ + previousScrollHeightMinusTop, this.scrollable.scrollHeight, + newScrollTop, this.scrollable.container.clientHeight); */ //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; const newScrollTop = reverse ? this.scrollable.scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; - + + this.log('performHistoryResult: will set up scrollTop:', newScrollTop, this.isHeavyAnimationInProgress); // touchSupport for safari iOS isTouchSupported && isApple && (this.scrollable.container.style.overflow = 'hidden'); this.scrollable.scrollTop = newScrollTop; //this.scrollable.scrollTop = this.scrollable.scrollHeight; isTouchSupported && isApple && (this.scrollable.container.style.overflow = ''); - //this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop); + this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.isHeavyAnimationInProgress); } resolve(true); diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index 39e42ad1..56713e65 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -64,7 +64,7 @@ export namespace MessageRender { message: any, messageDiv: HTMLElement }) => { - const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big'); + const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big') && !bubble.classList.contains('round'); const repliesFooter = new RepliesElement(); repliesFooter.message = message; repliesFooter.type = isFooter ? 'footer' : 'beside'; diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index e860b71b..62a47544 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -12,6 +12,7 @@ import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; import ButtonIcon from "../buttonIcon"; import { debounce } from "../../helpers/schedulers"; +import { getHeavyAnimationPromise } from "../../hooks/useHeavyAnimationCheck"; class AnimatedSuper { static DURATION = 200; @@ -514,7 +515,8 @@ export default class ChatPinnedMessage { await setPeerPromise; } - await this.chat.bubbles.scrollable.scrollLockedPromise; + //await this.chat.bubbles.scrollable.scrollLockedPromise; + await getHeavyAnimationPromise(); if(this.getCurrentIndexPromise) { await this.getCurrentIndexPromise; diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index ff79c115..b2df550f 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -2,6 +2,8 @@ import { findUpTag, whichChild } from "../helpers/dom"; import { TransitionSlider } from "./transition"; import { ScrollableX } from "./scrollable"; import rootScope from "../lib/rootScope"; +import { fastRaf } from "../helpers/schedulers"; +import { FocusDirection } from "../helpers/fastSmoothScroll"; export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement) => void, onTransitionEnd?: () => void, transitionTime = 250, scrollableX?: ScrollableX) { const selectTab = TransitionSlider(content, tabs || content.dataset.slider == 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd); @@ -23,7 +25,7 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? if(onClick) onClick(id, tabContent); if(scrollableX) { - scrollableX.scrollIntoView(target.parentElement.children[id] as HTMLElement, true, transitionTime); + scrollableX.scrollIntoViewNew(target.parentElement.children[id] as HTMLElement, 'center', undefined, undefined, animate ? undefined : FocusDirection.Static, transitionTime, 'x'); } if(!rootScope.settings.animationsEnabled) { @@ -35,32 +37,41 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? } const prev = tabs.querySelector(tagName.toLowerCase() + '.active') as HTMLElement; - prev && prev.classList.remove('active'); + fastRaf(() => { + prev && prev.classList.remove('active'); + }); + + const prevId = selectTab.prevId; // stripe from ZINCHUK - if(useStripe && selectTab.prevId !== -1 && animate) { - const indicator = target.querySelector('i')!; - const currentIndicator = target.parentElement.children[selectTab.prevId].querySelector('i')!; - - currentIndicator.classList.remove('animate'); - indicator.classList.remove('animate'); + if(useStripe && prevId !== -1 && animate) { + fastRaf(() => { + const indicator = target.querySelector('i')!; + const currentIndicator = target.parentElement.children[prevId].querySelector('i')!; + + currentIndicator.classList.remove('animate'); + indicator.classList.remove('animate'); + + // We move and resize our indicator so it repeats the position and size of the previous one. + const shiftLeft = currentIndicator.parentElement.parentElement.offsetLeft - indicator.parentElement.parentElement.offsetLeft; + const scaleFactor = currentIndicator.clientWidth / indicator.clientWidth; + indicator.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`; - // We move and resize our indicator so it repeats the position and size of the previous one. - const shiftLeft = currentIndicator.parentElement.parentElement.offsetLeft - indicator.parentElement.parentElement.offsetLeft; - const scaleFactor = currentIndicator.clientWidth / indicator.clientWidth; - indicator.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`; - - //console.log(`translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`); - - requestAnimationFrame(() => { - // Now we remove the transform to let it animate to its own position and size. - indicator.classList.add('animate'); - indicator.style.transform = 'none'; + //console.log(`translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`); + + requestAnimationFrame(() => { + // Now we remove the transform to let it animate to its own position and size. + indicator.classList.add('animate'); + indicator.style.transform = 'none'; + }); }); } // stripe END - target.classList.add('active'); + fastRaf(() => { + target.classList.add('active'); + }); + selectTab(id, animate); }; diff --git a/src/components/popups/createPoll.ts b/src/components/popups/createPoll.ts index cc90a0bf..37c9c44a 100644 --- a/src/components/popups/createPoll.ts +++ b/src/components/popups/createPoll.ts @@ -318,7 +318,7 @@ export default class PopupCreatePoll extends PopupElement { this.questions.append(radioField.label); - this.scrollable.scrollIntoView(this.questions.lastElementChild as HTMLElement, true); + this.scrollable.scrollIntoViewNew(this.questions.lastElementChild as HTMLElement, 'center'); //this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true); } } \ No newline at end of file diff --git a/src/components/popups/forward.ts b/src/components/popups/forward.ts index ba1607f3..ce7fe5e5 100644 --- a/src/components/popups/forward.ts +++ b/src/components/popups/forward.ts @@ -12,24 +12,31 @@ export default class PopupForward extends PopupElement { if(onClose) this.onClose = onClose; - this.selector = new AppSelectPeers(this.body, async() => { - const peerId = this.selector.getSelected()[0]; - this.btnClose.click(); - - this.selector = null; - - await (onSelect ? onSelect() || Promise.resolve() : Promise.resolve()); - - appImManager.setInnerPeer(peerId); - appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice()); - }, ['dialogs', 'contacts'], () => { - this.show(); - this.selector.checkForTriggers(); // ! due to zero height before mounting - - if(!isTouchSupported) { - this.selector.input.focus(); - } - }, null, 'send', false); + this.selector = new AppSelectPeers({ + appendTo: this.body, + onChange: async() => { + const peerId = this.selector.getSelected()[0]; + this.btnClose.click(); + + this.selector = null; + + await (onSelect ? onSelect() || Promise.resolve() : Promise.resolve()); + + appImManager.setInnerPeer(peerId); + appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice()); + }, + peerType: ['dialogs', 'contacts'], + onFirstRender: () => { + this.show(); + this.selector.checkForTriggers(); // ! due to zero height before mounting + + if(!isTouchSupported) { + this.selector.input.focus(); + } + }, + chatRightsAction: 'send', + multiSelect: false + }); //this.scrollable = new Scrollable(this.body); diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index f6bdf888..96ef6865 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -1,10 +1,8 @@ -import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; +import { CancellablePromise } from "../helpers/cancellablePromise"; import { isTouchSupported } from "../helpers/touchSupport"; import { logger, LogLevels } from "../lib/logger"; -import smoothscroll, { SCROLL_TIME, SmoothScrollToOptions } from '../vendor/smoothscroll'; -import rootScope from "../lib/rootScope"; -(window as any).__forceSmoothScrollPolyfill__ = true; -smoothscroll(); +import fastSmoothScroll from "../helpers/fastSmoothScroll"; +import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck"; /* var el = $0; var height = 0; @@ -51,11 +49,10 @@ const scrollsIntersector = new IntersectionObserver(entries => { export class ScrollableBase { protected log: ReturnType; + public onScrollMeasure: number = 0; protected onScroll: () => void; - public getScrollValue: () => number; - public scrollLocked = 0; - public scrollLockedPromise: CancellablePromise = Promise.resolve(); + public isHeavyAnimationInProgress: boolean; constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) { this.container.classList.add('scrollable'); @@ -73,55 +70,24 @@ export class ScrollableBase { protected setListeners() { window.addEventListener('resize', this.onScroll, {passive: true}); this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); + + useHeavyAnimationCheck(() => { + this.isHeavyAnimationInProgress = true; + + if(this.onScrollMeasure) { + window.cancelAnimationFrame(this.onScrollMeasure); + } + }, () => { + this.isHeavyAnimationInProgress = false; + this.onScroll(); + }); } public append(element: HTMLElement) { this.container.append(element); } - public scrollTo(value: number, side: 'top' | 'left', smooth = true, important = false, scrollTime = SCROLL_TIME) { - if(this.scrollLocked && !important) return; - - const scrollValue = this.getScrollValue(); - if(scrollValue == Math.floor(value)) { - return; - } - - if(!rootScope.settings.animationsEnabled) { - smooth = false; - scrollTime = 0; - } - - const wasLocked = !!this.scrollLocked; - if(wasLocked) clearTimeout(this.scrollLocked); - if(smooth) { - if(!wasLocked) { - this.scrollLockedPromise = deferredPromise(); - } - - this.scrollLocked = window.setTimeout(() => { - this.scrollLocked = 0; - this.scrollLockedPromise.resolve(); - //this.onScroll(); - this.container.dispatchEvent(new CustomEvent('scroll')); - }, scrollTime); - } else if(wasLocked) { - this.scrollLockedPromise.resolve(); - } - - const options: SmoothScrollToOptions = { - behavior: smooth ? 'smooth' : 'auto', - scrollTime - }; - - options[side] = value; - - this.container.scrollTo(options as any); - - if(!smooth) { - this.container.dispatchEvent(new CustomEvent('scroll')); - } - } + public scrollIntoViewNew = fastSmoothScroll.bind(this, this.container); } export type SliceSides = 'top' | 'bottom'; @@ -134,8 +100,6 @@ export default class Scrollable extends ScrollableBase { public onAdditionalScroll: () => void = null; public onScrolledTop: () => void = null; public onScrolledBottom: () => void = null; - - public onScrollMeasure: number = null; public lastScrollTop: number = 0; public lastScrollDirection: number = 0; @@ -168,8 +132,16 @@ export default class Scrollable extends ScrollableBase { //return; + if(this.isHeavyAnimationInProgress) { + if(this.onScrollMeasure) { + window.cancelAnimationFrame(this.onScrollMeasure); + } + + return; + } + //if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp && !this.onAdditionalScroll)) return; - if((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); this.onScrollMeasure = window.requestAnimationFrame(() => { this.onScrollMeasure = 0; @@ -189,7 +161,7 @@ export default class Scrollable extends ScrollableBase { }; public checkForTriggers = () => { - if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; + if((!this.onScrolledTop && !this.onScrolledBottom) || this.isHeavyAnimationInProgress) return; const scrollHeight = this.container.scrollHeight; if(!scrollHeight) { // незачем вызывать триггеры если блок пустой или не виден @@ -219,66 +191,6 @@ export default class Scrollable extends ScrollableBase { (this.splitUp || this.padding || this.container).append(...elements); } - public scrollIntoView(element: HTMLElement, smooth = true) { - if(element.parentElement && !this.scrollLocked) { - const isFirstUnread = element.classList.contains('is-first-unread'); - - let offset = element.getBoundingClientRect().top - this.container.getBoundingClientRect().top; - offset = this.scrollTop + offset; - - if(!smooth && isFirstUnread) { - this.scrollTo(offset, 'top', false); - return; - } - - const clientHeight = this.container.clientHeight; - const height = element.scrollHeight; - - const d = height >= clientHeight ? 0 : (clientHeight - height) / 2; - offset -= d; - - this.scrollTo(offset, 'top', smooth); - } - } - - public getScrollValue = () => { - return this.scrollTop; - }; - - /* public slice(side: SliceSides, safeCount: number) { - //const isOtherSideLoaded = this.loadedAll[side == 'top' ? 'bottom' : 'top']; - //const multiplier = 2 - +isOtherSideLoaded; - const multiplier = 2; - safeCount *= multiplier; - - const length = this.splitUp.childElementCount; - - if(length <= safeCount) { - return []; - } - - const children = Array.from(this.splitUp.children) as HTMLElement[]; - const sliced = side == 'top' ? children.slice(0, length - safeCount) : children.slice(safeCount); - for(const el of sliced) { - el.remove(); - } - - this.log.error('slice', side, length, sliced.length, this.splitUp.childElementCount); - - if(sliced.length) { - this.loadedAll[side] = false; - } - - // * fix instant load of cutted side - if(side == 'top') { - this.lastScrollTop = 0; - } else { - this.lastScrollTop = this.scrollHeight + this.container.clientHeight; - } - - return sliced; - } */ - get isScrolledDown() { return this.scrollHeight - Math.round(this.scrollTop + this.container.offsetHeight) <= 1; } @@ -326,26 +238,5 @@ export class ScrollableX extends ScrollableBase { this.container.attachEvent("onmousewheel", scrollHorizontally); } } - - this.setListeners(); - } - - public scrollIntoView(element: HTMLElement, smooth = true, scrollTime?: number) { - if(element.parentElement && !this.scrollLocked) { - let offset = element.getBoundingClientRect().left - this.container.getBoundingClientRect().left; - offset = this.getScrollValue() + offset; - - const clientWidth = this.container.clientWidth; - const width = element.scrollWidth; - - const d = width >= clientWidth ? 0 : (clientWidth - width) / 2; - offset -= d; - - this.scrollTo(offset, 'left', smooth, undefined, scrollTime); - } } - - public getScrollValue = () => { - return this.container.scrollLeft; - }; } diff --git a/src/components/sidebarLeft/tabs/addMembers.ts b/src/components/sidebarLeft/tabs/addMembers.ts index 1f36f6dc..418728b0 100644 --- a/src/components/sidebarLeft/tabs/addMembers.ts +++ b/src/components/sidebarLeft/tabs/addMembers.ts @@ -2,6 +2,7 @@ import SidebarSlider, { SliderSuperTab } from "../../slider"; import AppSelectPeers from "../../appSelectPeers"; import { putPreloader } from "../../misc"; import Button from "../../button"; +import { fastRaf } from "../../../helpers/schedulers"; export default class AppAddMembersTab extends SliderSuperTab { private nextBtn: HTMLButtonElement; @@ -67,14 +68,18 @@ export default class AppAddMembersTab extends SliderSuperTab { this.skippable = options.skippable; this.onCloseAfterTimeout(); - this.selector = new AppSelectPeers(this.content, this.skippable ? null : (length) => { - this.nextBtn.classList.toggle('is-visible', !!length); - }, ['contacts']); + this.selector = new AppSelectPeers({ + appendTo: this.content, + onChange: this.skippable ? null : (length) => { + this.nextBtn.classList.toggle('is-visible', !!length); + }, + peerType: ['contacts'] + }); this.selector.input.placeholder = options.placeholder; if(options.selectedPeerIds) { - options.selectedPeerIds.forEach(peerId => { - this.selector.add(peerId); + fastRaf(() => { + this.selector.addInitial(options.selectedPeerIds); }); } diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index a151bd67..efa18941 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -8,6 +8,8 @@ import { MyDialogFilter as DialogFilter } from "../../../lib/storages/filters"; import rootScope from "../../../lib/rootScope"; import { copy } from "../../../helpers/object"; import ButtonIcon from "../../buttonIcon"; +import { FocusDirection } from "../../../helpers/fastSmoothScroll"; +import { fastRaf } from "../../../helpers/schedulers"; export default class AppIncludedChatsTab extends SliderSuperTab { private confirmBtn: HTMLElement; @@ -190,13 +192,18 @@ export default class AppIncludedChatsTab extends SliderSuperTab { const selectedPeers = (this.type === 'included' ? filter.include_peers : filter.exclude_peers).slice(); - this.selector = new AppSelectPeers(this.container, this.onSelectChange, ['dialogs'], null, this.renderResults); + this.selector = new AppSelectPeers({ + appendTo: this.container, + onChange: this.onSelectChange, + peerType: ['dialogs'], + renderResultsFunc: this.renderResults + }); this.selector.selected = new Set(selectedPeers); this.selector.input.placeholder = 'Search'; const _add = this.selector.add.bind(this.selector); - this.selector.add = (peerId, title) => { - const div = _add(peerId, details[peerId]?.text); + this.selector.add = (peerId, title, scroll) => { + const div = _add(peerId, details[peerId]?.text, scroll); if(details[peerId]) { div.querySelector('avatar-element').classList.add('tgico-' + details[peerId].ico); } @@ -205,8 +212,8 @@ export default class AppIncludedChatsTab extends SliderSuperTab { this.selector.list.parentElement.insertBefore(fragment, this.selector.list); - selectedPeers.forEach(peerId => { - this.selector.add(peerId); + fastRaf(() => { + this.selector.addInitial(selectedPeers); }); for(const flag in filter.pFlags) { @@ -215,11 +222,6 @@ export default class AppIncludedChatsTab extends SliderSuperTab { (categories.querySelector(`[data-peerId="${flag}"]`) as HTMLElement).click(); } } - - // ! потому что onOpen срабатывает раньше, чем блок отрисовывается, и высоты нет - setTimeout(() => { - this.selector.selectedScrollable.scrollTo(this.selector.selectedScrollable.scrollHeight, 'top', false, true); - }, 0); } onSelectChange = (length: number) => { diff --git a/src/components/sidebarRight/tabs/forward.ts b/src/components/sidebarRight/tabs/forward.ts index 135318a3..e1d7c200 100644 --- a/src/components/sidebarRight/tabs/forward.ts +++ b/src/components/sidebarRight/tabs/forward.ts @@ -71,19 +71,25 @@ export default class AppForwardTab implements SliderTab { this.cleanup(); this.mids = ids; - this.selector = new AppSelectPeers(this.container, (length) => { - this.sendBtn.classList.toggle('is-visible', !!length); - }, ['dialogs', 'contacts'], () => { - //console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount); - - // !!!!!!!!!! UNCOMMENT BELOW IF NEED TO USE THIS CLASS - ////////////////////////////////////////appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.forward); - appSidebarRight.toggleSidebar(true).then(() => { - if(this.selector) { - this.selector.checkForTriggers(); - } - }); - document.body.classList.add('is-forward-active'); - }, null, 'send'); + this.selector = new AppSelectPeers({ + appendTo: this.container, + onChange: (length) => { + this.sendBtn.classList.toggle('is-visible', !!length); + }, + peerType: ['dialogs', 'contacts'], + onFirstRender: () => { + //console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount); + + // !!!!!!!!!! UNCOMMENT BELOW IF NEED TO USE THIS CLASS + ////////////////////////////////////////appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.forward); + appSidebarRight.toggleSidebar(true).then(() => { + if(this.selector) { + this.selector.checkForTriggers(); + } + }); + document.body.classList.add('is-forward-active'); + }, + chatRightsAction: 'send' + }); } } diff --git a/src/helpers/animation.ts b/src/helpers/animation.ts new file mode 100644 index 00000000..adf9db82 --- /dev/null +++ b/src/helpers/animation.ts @@ -0,0 +1,52 @@ +// * Jolly Cobra's animation.ts + +import { fastRaf } from './schedulers'; +import { CancellablePromise, deferredPromise } from './cancellablePromise'; + +interface AnimationInstance { + isCancelled: boolean; + deferred: CancellablePromise +} + +type AnimationInstanceKey = any; +const instances: Map = new Map(); + +export function getAnimationInstance(key: AnimationInstanceKey) { + return instances.get(key); +} + +export function cancelAnimationByKey(key: AnimationInstanceKey) { + const instance = getAnimationInstance(key); + if(instance) { + instance.isCancelled = true; + instances.delete(key); + } +} + +export function animateSingle(tick: Function, key: AnimationInstanceKey, instance?: AnimationInstance) { + if(!instance) { + cancelAnimationByKey(key); + instance = { isCancelled: false, deferred: deferredPromise() }; + instances.set(key, instance); + } + + fastRaf(() => { + if(instance.isCancelled) return; + + if(tick()) { + animateSingle(tick, key, instance); + } else { + instance.deferred.resolve(); + } + }); + + return instance.deferred; +} + +export function animate(tick: Function) { + fastRaf(() => { + if(tick()) { + animate(tick); + } + }); +} diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts new file mode 100644 index 00000000..b090724c --- /dev/null +++ b/src/helpers/fastSmoothScroll.ts @@ -0,0 +1,149 @@ +// * Jolly Cobra's fastSmoothScroll slightly patched + +import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck'; +import { fastRaf } from './schedulers'; +import { animateSingle, cancelAnimationByKey } from './animation'; +import rootScope from '../lib/rootScope'; + +const MAX_DISTANCE = 1500; +const MIN_JS_DURATION = 250; +const MAX_JS_DURATION = 600; + +export enum FocusDirection { + Up, + Down, + Static, +}; + +export default function fastSmoothScroll( + container: HTMLElement, + element: HTMLElement, + position: ScrollLogicalPosition, + margin = 0, + maxDistance = MAX_DISTANCE, + forceDirection?: FocusDirection, + forceDuration?: number, + axis: 'x' | 'y' = 'y' +) { + //return; + + if(!rootScope.settings.animationsEnabled) { + forceDirection = FocusDirection.Static; + } + + if(forceDirection === FocusDirection.Static) { + forceDuration = 0; + return scrollWithJs(container, element, position, margin, forceDuration, axis); + /* return Promise.resolve(); + + element.scrollIntoView({ block: position }); + + cancelAnimationByKey(container); + return Promise.resolve(); */ + } + + if(axis === 'y') { + const elementRect = element.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + const offsetTop = elementRect.top - containerRect.top; + if(forceDirection === undefined) { + if(offsetTop < -maxDistance) { + container.scrollTop += (offsetTop + maxDistance); + } else if(offsetTop > maxDistance) { + container.scrollTop += (offsetTop - maxDistance); + } + } else if(forceDirection === FocusDirection.Up) { // * not tested yet + container.scrollTop = offsetTop + container.scrollTop + maxDistance; + } else if(forceDirection === FocusDirection.Down) { // * not tested yet + container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance); + } + } + + const promise = new Promise((resolve) => { + fastRaf(() => { + scrollWithJs(container, element, position, margin, forceDuration, axis) + .then(resolve); + }); + }); + + return dispatchHeavyAnimationEvent(promise); +} + +function scrollWithJs( + container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, forceDuration?: number, axis: 'x' | 'y' = 'y' +) { + const rectStartKey = axis === 'y' ? 'top' : 'left'; + const rectEndKey = axis === 'y' ? 'bottom' : 'right'; + const sizeKey = axis === 'y' ? 'height' : 'width'; + const scrollSizeKey = axis === 'y' ? 'scrollHeight' : 'scrollWidth'; + const scrollPositionKey = axis === 'y' ? 'scrollTop' : 'scrollLeft'; + + //const { offsetTop: elementTop, offsetHeight: elementHeight } = element; + const elementRect = element.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + const elementPosition = elementRect[rectStartKey] - containerRect[rectStartKey]; + const elementSize = element[scrollSizeKey]; // margin is exclusive in DOMRect + + const containerSize = containerRect[sizeKey]; + + const scrollPosition = container[scrollPositionKey]; + const scrollSize = container[scrollSizeKey]; + + let path!: number; + + switch(position) { + case 'start': + path = (elementPosition - margin) - scrollPosition; + break; + case 'end': + //path = (elementTop + elementHeight + margin) - containerHeight; + path = elementRect[rectEndKey] + (elementSize - elementRect[sizeKey]) - containerRect[rectEndKey]; + break; + // 'nearest' is not supported yet + case 'nearest': + case 'center': + path = elementSize < containerSize + ? (elementPosition + elementSize / 2) - (containerSize / 2) + : elementPosition - margin; + break; + } + + // console.log('scrollWithJs: will scroll path:', path, element); + + if(path < 0) { + const remainingPath = -scrollPosition; + path = Math.max(path, remainingPath); + } else if(path > 0) { + const remainingPath = scrollSize - (scrollPosition + containerSize); + path = Math.min(path, remainingPath); + } + + const target = container[scrollPositionKey] + path; + const duration = forceDuration ?? ( + MIN_JS_DURATION + (Math.abs(path) / MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION) + ); + const startAt = Date.now(); + + const tick = () => { + const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; + + const currentPath = path * (1 - transition(t)); + container[scrollPositionKey] = Math.round(target - currentPath); + + return t < 1; + }; + + if(!duration) { + cancelAnimationByKey(container); + tick(); + return Promise.resolve(); + } + + return animateSingle(tick, container); +} + +function transition(t: number) { + return 1 - ((1 - t) ** 3.5); +} diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index 8d1a5e7d..76e6920c 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -1,5 +1,5 @@ // * Jolly Cobra's schedulers -import { AnyToVoidFunction } from "../types"; +import { AnyToVoidFunction, NoneToVoidFunction } from "../types"; //type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow; @@ -109,9 +109,9 @@ export const pause = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); -/* let fastRafCallbacks: NoneToVoidFunction[] | undefined; +let fastRafCallbacks: NoneToVoidFunction[] | undefined; export function fastRaf(callback: NoneToVoidFunction) { - if (!fastRafCallbacks) { + if(!fastRafCallbacks) { fastRafCallbacks = [callback]; requestAnimationFrame(() => { @@ -122,4 +122,4 @@ export function fastRaf(callback: NoneToVoidFunction) { } else { fastRafCallbacks.push(callback); } -} */ +} diff --git a/src/hooks/useHeavyAnimationCheck.ts b/src/hooks/useHeavyAnimationCheck.ts new file mode 100644 index 00000000..98aa8ba0 --- /dev/null +++ b/src/hooks/useHeavyAnimationCheck.ts @@ -0,0 +1,59 @@ +// * Jolly Cobra's useHeavyAnimationCheck.ts + +//import { useEffect } from '../lib/teact/teact'; +import { AnyToVoidFunction } from '../types'; +import ListenerSetter from '../helpers/listenerSetter'; +import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise'; + +const ANIMATION_START_EVENT = 'event-heavy-animation-start'; +const ANIMATION_END_EVENT = 'event-heavy-animation-end'; + +let isAnimating = false; +let heavyAnimationPromise: CancellablePromise = Promise.resolve(); +let lastAnimationPromise: Promise; + +export const dispatchHeavyAnimationEvent = (promise: Promise) => { + if(!isAnimating) { + heavyAnimationPromise = deferredPromise(); + } + + document.dispatchEvent(new Event(ANIMATION_START_EVENT)); + isAnimating = true; + lastAnimationPromise = promise; + + promise.then(() => { + if(lastAnimationPromise !== promise) { + return; + } + + isAnimating = false; + document.dispatchEvent(new Event(ANIMATION_END_EVENT)); + heavyAnimationPromise.resolve(); + }); + + return heavyAnimationPromise; +}; + +export const getHeavyAnimationPromise = () => heavyAnimationPromise; + +export default ( + handleAnimationStart: AnyToVoidFunction, + handleAnimationEnd: AnyToVoidFunction, + listenerSetter?: ListenerSetter +) => { + //useEffect(() => { + if(isAnimating) { + handleAnimationStart(); + } + + const add = listenerSetter ? listenerSetter.add.bind(listenerSetter, document) : document.addEventListener.bind(document); + const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, document) : document.removeEventListener.bind(document); + add(ANIMATION_START_EVENT, handleAnimationStart); + add(ANIMATION_END_EVENT, handleAnimationEnd); + + return () => { + remove(ANIMATION_END_EVENT, handleAnimationEnd); + remove(ANIMATION_START_EVENT, handleAnimationStart); + }; + //}, [handleAnimationEnd, handleAnimationStart]); +}; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 46f1c250..b15bfbff 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -29,6 +29,7 @@ import SetTransition from '../../components/singleTransition'; import ChatDragAndDrop from '../../components/chat/dragAndDrop'; import { debounce } from '../../helpers/schedulers'; import lottieLoader from '../lottieLoader'; +import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; //console.log('appImManager included33!'); @@ -140,6 +141,14 @@ export class AppImManager { this.setSettings(); rootScope.on('settings_updated', () => this.setSettings()); + + useHeavyAnimationCheck(() => { + animationIntersector.setOnlyOnePlayableGroup('lock'); + animationIntersector.checkAnimations(true); + }, () => { + animationIntersector.setOnlyOnePlayableGroup(''); + animationIntersector.checkAnimations(false); + }); } private setSettings() { diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 308bed95..ccdf1812 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -58,10 +58,9 @@ $bubble-margin: .25rem; } .bubble { - padding-top: $bubble-margin; position: relative; z-index: 1; - margin: 0 auto; + margin: 0 auto $bubble-margin; user-select: none; &.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & { @@ -70,7 +69,7 @@ $bubble-margin: .25rem; left: -50%; /* top: 0; bottom: 0; */ - top: #{$bubble-margin / 2}; + top: -#{$bubble-margin / 2}; bottom: -#{$bubble-margin / 2}; content: " "; z-index: 1; @@ -369,7 +368,11 @@ $bubble-margin: .25rem; } */ &.is-group-last { - padding-bottom: $bubble-margin; + margin-bottom: #{$bubble-margin * 2}; + + &:before, &:after { + bottom: -#{$bubble-margin}; + } > .bubble-select-checkbox { bottom: 8px; @@ -382,6 +385,12 @@ $bubble-margin: .25rem; } } + &.is-group-first { + &:before, &:after { + top: -#{$bubble-margin}; + } + } + &:not(.forwarded) { &:not(.is-group-first) { .bubble__container > .name, .document-wrapper > .name { @@ -1141,7 +1150,7 @@ $bubble-margin: .25rem; &:first-of-type { .document-selection { - top: -2px; // * padding inner + half padding outer + top: -#{$bubble-margin / 2}; // * padding inner + half padding outer } .document-wrapper { @@ -1153,7 +1162,7 @@ $bubble-margin: .25rem; &:last-of-type { .document-selection { - bottom: -2px; + bottom: -#{$bubble-margin / 2}; } .document-wrapper { @@ -1175,7 +1184,7 @@ $bubble-margin: .25rem; &.is-group-last .document-container { &:last-of-type { .document-selection { - bottom: -6px; + bottom: -$bubble-margin; } } } @@ -1264,8 +1273,10 @@ $bubble-margin: .25rem; bottom: 55px; } - &.sticker .message { - bottom: 0; + &.sticker, &.with-replies.round, &.emoji-big { + .message { + bottom: 0; + } } } @@ -1568,7 +1579,7 @@ $bubble-margin: .25rem; // * fix scroll with only 1 bubble .bubbles-date-group:last-of-type { .bubble:last-of-type { - margin-bottom: 2px; + margin-bottom: $bubble-margin; /* &:after, .document-container:last-of-type .document-selection { bottom: 0 !important; } */