Browse Source

Fix smoothness of sending animation

Fix comments button for round video
master
morethanwords 4 years ago
parent
commit
fec0fdf72f
  1. 16
      src/components/animationIntersector.ts
  2. 50
      src/components/appSelectPeers.ts
  3. 2
      src/components/chat/bubbleGroups.ts
  4. 106
      src/components/chat/bubbles.ts
  5. 2
      src/components/chat/messageRender.ts
  6. 4
      src/components/chat/pinnedMessage.ts
  7. 17
      src/components/horizontalMenu.ts
  8. 2
      src/components/popups/createPoll.ts
  9. 13
      src/components/popups/forward.ts
  10. 161
      src/components/scrollable.ts
  11. 13
      src/components/sidebarLeft/tabs/addMembers.ts
  12. 22
      src/components/sidebarLeft/tabs/includedChats.ts
  13. 12
      src/components/sidebarRight/tabs/forward.ts
  14. 52
      src/helpers/animation.ts
  15. 149
      src/helpers/fastSmoothScroll.ts
  16. 6
      src/helpers/schedulers.ts
  17. 59
      src/hooks/useHeavyAnimationCheck.ts
  18. 9
      src/lib/appManagers/appImManager.ts
  19. 29
      src/scss/partials/_chatBubble.scss

16
src/components/animationIntersector.ts

@ -32,7 +32,7 @@ export class AnimationIntersector {
continue; continue;
} }
const player = this.byGroups[group].find(p => p.el == target); const player = this.byGroups[group].find(p => p.el === target);
if(player) { if(player) {
if(entry.isIntersecting) { if(entry.isIntersecting) {
this.visible.add(player); this.visible.add(player);
@ -58,7 +58,7 @@ export class AnimationIntersector {
const found: AnimationItem[] = []; const found: AnimationItem[] = [];
for(const group in this.byGroups) { for(const group in this.byGroups) {
for(const player of this.byGroups[group]) { for(const player of this.byGroups[group]) {
if(player.el == element) { if(player.el === element) {
found.push(player); found.push(player);
} }
} }
@ -80,7 +80,7 @@ export class AnimationIntersector {
} }
for(const group in this.byGroups) { for(const group in this.byGroups) {
this.byGroups[group].findAndSplice(p => p == player); this.byGroups[group].findAndSplice(p => p === player);
} }
this.observer.unobserve(el); this.observer.unobserve(el);
@ -132,13 +132,17 @@ export class AnimationIntersector {
return; return;
} }
if(blurred) { if(blurred || (this.onlyOnePlayableGroup && this.onlyOnePlayableGroup !== group)) {
if(!animation.paused) { if(!animation.paused) {
//console.warn('pause animation:', animation); //console.warn('pause animation:', animation);
animation.pause(); animation.pause();
} }
} else if(animation.paused && this.visible.has(player) && animation.autoplay && (!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup == group)) { } else if(animation.paused &&
//console.warn('play animation:', animation); this.visible.has(player) &&
animation.autoplay &&
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group)
) {
console.warn('play animation:', animation);
animation.play(); animation.play();
} }
} }

50
src/components/appSelectPeers.ts

@ -7,6 +7,7 @@ import appUsersManager from "../lib/appManagers/appUsersManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom"; import { cancelEvent, findUpAttribute, findUpClassName } from "../helpers/dom";
import Scrollable from "./scrollable"; import Scrollable from "./scrollable";
import { FocusDirection } from "../helpers/fastSmoothScroll";
type PeerType = 'contacts' | 'dialogs'; type PeerType = 'contacts' | 'dialogs';
@ -39,10 +40,30 @@ export default class AppSelectPeers {
private renderedPeerIds: Set<number> = new Set(); private renderedPeerIds: Set<number> = new Set();
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) { private appendTo: HTMLElement;
private onChange: (length: number) => void;
private peerType: PeerType[] = ['dialogs'];
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'); this.container.classList.add('selector');
const f = (renderResultsFunc || this.renderResults).bind(this); const f = (this.renderResultsFunc || this.renderResults).bind(this);
this.renderResultsFunc = (peerIds: number[]) => { this.renderResultsFunc = (peerIds: number[]) => {
peerIds = peerIds.filter(peerId => { peerIds = peerIds.filter(peerId => {
const notRendered = !this.renderedPeerIds.has(peerId); const notRendered = !this.renderedPeerIds.has(peerId);
@ -55,10 +76,10 @@ export default class AppSelectPeers {
this.input = document.createElement('input'); this.input = document.createElement('input');
this.input.classList.add('selector-search-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'; this.input.type = 'text';
if(multiSelect) { if(this.multiSelect) {
let topContainer = document.createElement('div'); let topContainer = document.createElement('div');
topContainer.classList.add('selector-search-container'); topContainer.classList.add('selector-search-container');
@ -151,14 +172,14 @@ export default class AppSelectPeers {
}; };
this.container.append(this.chatsContainer); this.container.append(this.chatsContainer);
appendTo.append(this.container); this.appendTo.append(this.container);
// WARNING TIMEOUT // WARNING TIMEOUT
setTimeout(() => { setTimeout(() => {
let getResultsPromise = this.getMoreResults() as Promise<any>; let getResultsPromise = this.getMoreResults() as Promise<any>;
if(onFirstRender) { if(options.onFirstRender) {
getResultsPromise.then(() => { getResultsPromise.then(() => {
onFirstRender(); options.onFirstRender();
}); });
} }
}, 0); }, 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'); //console.trace('add');
this.selected.add(peerId); this.selected.add(peerId);
@ -380,9 +401,12 @@ export default class AppSelectPeers {
this.selectedContainer.insertBefore(div, this.input); this.selectedContainer.insertBefore(div, this.input);
//this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight; //this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight;
this.selectedScrollable.scrollTo(this.selectedScrollable.scrollHeight, 'top', true, true);
this.onChange && this.onChange(this.selected.size); this.onChange && this.onChange(this.selected.size);
if(scroll) {
this.selectedScrollable.scrollIntoViewNew(this.input, 'center');
}
return div; return div;
} }
@ -410,4 +434,12 @@ export default class AppSelectPeers {
public getSelected() { public getSelected() {
return [...this.selected]; 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);
}
} }

2
src/components/chat/bubbleGroups.ts

@ -28,6 +28,8 @@ export default class BubbleGroups {
} }
addBubble(bubble: HTMLDivElement, message: MyMessage, reverse: boolean) { addBubble(bubble: HTMLDivElement, message: MyMessage, reverse: boolean) {
//return;
const timestamp = message.date; const timestamp = message.date;
const mid = message.mid; const mid = message.mid;
let fromId = message.fromId; let fromId = message.fromId;

106
src/components/chat/bubbles.ts

@ -39,6 +39,9 @@ import PollElement from "../poll";
import AudioElement from "../audio"; import AudioElement from "../audio";
import { Message, MessageEntity, MessageReplies, MessageReplyHeader } from "../../layer"; import { Message, MessageEntity, MessageReplies, MessageReplyHeader } from "../../layer";
import { DEBUG, MOUNT_CLASS_TO, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; 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']; const IGNORE_ACTIONS = ['messageActionHistoryClear'];
@ -105,6 +108,9 @@ export default class ChatBubbles {
public replyFollowHistory: number[] = []; 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) { 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'); //this.chat.log.error('Bubbles construction');
@ -146,6 +152,10 @@ export default class ChatBubbles {
this.bubbleGroups.addBubble(bubble, message, false); this.bubbleGroups.addBubble(bubble, message, false);
if(this.scrollingToNewBubble) {
this.scrollToNewLastBubble();
}
//this.renderMessage(message, false, false, bubble); //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() { public constructPeerHelpers() {
@ -789,7 +805,15 @@ export default class ChatBubbles {
public loadMoreHistory(top: boolean, justLoad = false) { public loadMoreHistory(top: boolean, justLoad = false) {
//this.log('loadMoreHistory', top); //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, если иды только отрицательные то вниз не попадёт (хотя мб и так не попадёт) // warning, если иды только отрицательные то вниз не попадёт (хотя мб и так не попадёт)
const history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b); const history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b);
@ -817,8 +841,10 @@ export default class ChatBubbles {
} }
public onScroll = () => { public onScroll = () => {
//return;
// * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз // * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз
if(this.scrollable.scrollLocked && this.scrolledDown) return; if(this.isHeavyAnimationInProgress && this.scrolledDown) return;
//lottieLoader.checkAnimations(false, 'chat'); //lottieLoader.checkAnimations(false, 'chat');
if(!isTouchSupported) { if(!isTouchSupported) {
@ -947,7 +973,7 @@ export default class ChatBubbles {
public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) { public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) {
if(!this.scrolledAllDown) { // seems search active or sliced 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; return;
} }
@ -960,25 +986,15 @@ export default class ChatBubbles {
} }
mids = mids.filter(mid => !this.bubbles[mid]); mids = mids.filter(mid => !this.bubbles[mid]);
mids.forEach((mid: number) => { const promise = this.performHistoryResult(mids, false, true);
const message = this.chat.getMessage(mid); if(scrolledDown) {
promise.then(() => {
/////////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(() => {
//this.log('renderNewMessagesByIDs: messagesQueuePromise after', this.scrollable.isScrolledDown); //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(() => { /* setTimeout(() => {
this.log('messagesQueuePromise afterafter:', this.chatInner.childElementCount, this.scrollable.scrollHeight); 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) { public highlightBubble(element: HTMLElement) {
const datasetKey = 'highlightTimeout'; const datasetKey = 'highlightTimeout';
if(element.dataset[datasetKey]) { if(element.dataset[datasetKey]) {
@ -1168,7 +1195,7 @@ export default class ChatBubbles {
const mounted = this.getMountedBubble(lastMsgId); const mounted = this.getMountedBubble(lastMsgId);
if(mounted) { if(mounted) {
if(isTarget) { if(isTarget) {
this.scrollable.scrollIntoView(mounted.bubble); this.scrollable.scrollIntoViewNew(mounted.bubble, 'center');
this.highlightBubble(mounted.bubble); this.highlightBubble(mounted.bubble);
this.chat.setListenerResult('setPeer', lastMsgId, false); this.chat.setListenerResult('setPeer', lastMsgId, false);
} else if(topMessage && !isJump) { } else if(topMessage && !isJump) {
@ -1269,11 +1296,6 @@ export default class ChatBubbles {
//if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { //if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
if((topMessage && isJump) || isTarget) { 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 fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0);
const forwardingUnread = historyStorage.readMaxId === lastMsgId && !isTarget; const forwardingUnread = historyStorage.readMaxId === lastMsgId && !isTarget;
if(!fromUp && (samePeer || forwardingUnread)) { if(!fromUp && (samePeer || forwardingUnread)) {
@ -1290,7 +1312,7 @@ export default class ChatBubbles {
// ! sometimes there can be no bubble // ! sometimes there can be no bubble
if(bubble) { if(bubble) {
this.scrollable.scrollIntoView(bubble, samePeer/* , fromUp */); this.scrollable.scrollIntoViewNew(bubble, forwardingUnread ? 'start' : 'center', undefined, undefined, !samePeer ? FocusDirection.Static : undefined);
if(!forwardingUnread) { if(!forwardingUnread) {
this.highlightBubble(bubble); this.highlightBubble(bubble);
} }
@ -1395,7 +1417,12 @@ export default class ChatBubbles {
this.messagesQueue.push({message, bubble, reverse, promises}); this.messagesQueue.push({message, bubble, reverse, promises});
if(!this.messagesQueuePromise) { this.setMessagesQueuePromise();
}
public setMessagesQueuePromise() {
if(this.messagesQueuePromise) return;
this.messagesQueuePromise = new Promise((resolve, reject) => { this.messagesQueuePromise = new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
const chatInner = this.chatInner; const chatInner = this.chatInner;
@ -1405,9 +1432,8 @@ export default class ChatBubbles {
const promises = queue.reduce((acc, {promises}) => acc.concat(promises), []); const promises = queue.reduce((acc, {promises}) => acc.concat(promises), []);
// * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался // * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался
if(this.scrollable.scrollLocked) { // * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера
promises.push(this.scrollable.scrollLockedPromise); // promises.push(getHeavyAnimationPromise());
}
//this.log('promises to call', promises, queue); //this.log('promises to call', promises, queue);
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
@ -1428,11 +1454,14 @@ export default class ChatBubbles {
resolve(); resolve();
//}, 500); //}, 500);
this.messagesQueuePromise = null; this.messagesQueuePromise = null;
if(this.messagesQueue.length) {
this.setMessagesQueuePromise();
}
}, reject); }, reject);
}, 0); }, 0);
}); });
} }
}
public setBubblePosition(bubble: HTMLElement, message: any, reverse: boolean) { public setBubblePosition(bubble: HTMLElement, message: any, reverse: boolean) {
const dateMessage = this.getDateContainerByMessage(message, reverse); const dateMessage = this.getDateContainerByMessage(message, reverse);
@ -2259,14 +2288,14 @@ export default class ChatBubbles {
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
//await new Promise((resolve) => setTimeout(resolve, 1e3)); //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 method = (reverse ? history.shift : history.pop).bind(history);
//const padding = 10000; //const padding = 10000;
const realLength = this.scrollable.container.childElementCount; //const realLength = this.scrollable.container.childElementCount;
let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */; 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 = () => { this.messagesQueueOnRender = () => {
const {scrollTop, scrollHeight} = this.scrollable; const {scrollTop, scrollHeight} = this.scrollable;
@ -2284,7 +2313,7 @@ export default class ChatBubbles {
//this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop); //this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop);
this.messagesQueueOnRender = undefined; this.messagesQueueOnRender = undefined;
}; };
} //}
while(history.length) { while(history.length) {
let message = this.chat.getMessage(method()); let message = this.chat.getMessage(method());
@ -2312,13 +2341,14 @@ export default class ChatBubbles {
//const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop;
const newScrollTop = reverse ? this.scrollable.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 // touchSupport for safari iOS
isTouchSupported && isApple && (this.scrollable.container.style.overflow = 'hidden'); isTouchSupported && isApple && (this.scrollable.container.style.overflow = 'hidden');
this.scrollable.scrollTop = newScrollTop; this.scrollable.scrollTop = newScrollTop;
//this.scrollable.scrollTop = this.scrollable.scrollHeight; //this.scrollable.scrollTop = this.scrollable.scrollHeight;
isTouchSupported && isApple && (this.scrollable.container.style.overflow = ''); 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); resolve(true);

2
src/components/chat/messageRender.ts

@ -64,7 +64,7 @@ export namespace MessageRender {
message: any, message: any,
messageDiv: HTMLElement 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(); const repliesFooter = new RepliesElement();
repliesFooter.message = message; repliesFooter.message = message;
repliesFooter.type = isFooter ? 'footer' : 'beside'; repliesFooter.type = isFooter ? 'footer' : 'beside';

4
src/components/chat/pinnedMessage.ts

@ -12,6 +12,7 @@ import Chat from "./chat";
import ListenerSetter from "../../helpers/listenerSetter"; import ListenerSetter from "../../helpers/listenerSetter";
import ButtonIcon from "../buttonIcon"; import ButtonIcon from "../buttonIcon";
import { debounce } from "../../helpers/schedulers"; import { debounce } from "../../helpers/schedulers";
import { getHeavyAnimationPromise } from "../../hooks/useHeavyAnimationCheck";
class AnimatedSuper { class AnimatedSuper {
static DURATION = 200; static DURATION = 200;
@ -514,7 +515,8 @@ export default class ChatPinnedMessage {
await setPeerPromise; await setPeerPromise;
} }
await this.chat.bubbles.scrollable.scrollLockedPromise; //await this.chat.bubbles.scrollable.scrollLockedPromise;
await getHeavyAnimationPromise();
if(this.getCurrentIndexPromise) { if(this.getCurrentIndexPromise) {
await this.getCurrentIndexPromise; await this.getCurrentIndexPromise;

17
src/components/horizontalMenu.ts

@ -2,6 +2,8 @@ import { findUpTag, whichChild } from "../helpers/dom";
import { TransitionSlider } from "./transition"; import { TransitionSlider } from "./transition";
import { ScrollableX } from "./scrollable"; import { ScrollableX } from "./scrollable";
import rootScope from "../lib/rootScope"; 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) { 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); 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(onClick) onClick(id, tabContent);
if(scrollableX) { 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) { if(!rootScope.settings.animationsEnabled) {
@ -35,12 +37,17 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
} }
const prev = tabs.querySelector(tagName.toLowerCase() + '.active') as HTMLElement; const prev = tabs.querySelector(tagName.toLowerCase() + '.active') as HTMLElement;
fastRaf(() => {
prev && prev.classList.remove('active'); prev && prev.classList.remove('active');
});
const prevId = selectTab.prevId;
// stripe from ZINCHUK // stripe from ZINCHUK
if(useStripe && selectTab.prevId !== -1 && animate) { if(useStripe && prevId !== -1 && animate) {
fastRaf(() => {
const indicator = target.querySelector('i')!; const indicator = target.querySelector('i')!;
const currentIndicator = target.parentElement.children[selectTab.prevId].querySelector('i')!; const currentIndicator = target.parentElement.children[prevId].querySelector('i')!;
currentIndicator.classList.remove('animate'); currentIndicator.classList.remove('animate');
indicator.classList.remove('animate'); indicator.classList.remove('animate');
@ -57,10 +64,14 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
indicator.classList.add('animate'); indicator.classList.add('animate');
indicator.style.transform = 'none'; indicator.style.transform = 'none';
}); });
});
} }
// stripe END // stripe END
fastRaf(() => {
target.classList.add('active'); target.classList.add('active');
});
selectTab(id, animate); selectTab(id, animate);
}; };

2
src/components/popups/createPoll.ts

@ -318,7 +318,7 @@ export default class PopupCreatePoll extends PopupElement {
this.questions.append(radioField.label); 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); //this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true);
} }
} }

13
src/components/popups/forward.ts

@ -12,7 +12,9 @@ export default class PopupForward extends PopupElement {
if(onClose) this.onClose = onClose; if(onClose) this.onClose = onClose;
this.selector = new AppSelectPeers(this.body, async() => { this.selector = new AppSelectPeers({
appendTo: this.body,
onChange: async() => {
const peerId = this.selector.getSelected()[0]; const peerId = this.selector.getSelected()[0];
this.btnClose.click(); this.btnClose.click();
@ -22,14 +24,19 @@ export default class PopupForward extends PopupElement {
appImManager.setInnerPeer(peerId); appImManager.setInnerPeer(peerId);
appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice()); appImManager.chat.input.initMessagesForward(fromPeerId, mids.slice());
}, ['dialogs', 'contacts'], () => { },
peerType: ['dialogs', 'contacts'],
onFirstRender: () => {
this.show(); this.show();
this.selector.checkForTriggers(); // ! due to zero height before mounting this.selector.checkForTriggers(); // ! due to zero height before mounting
if(!isTouchSupported) { if(!isTouchSupported) {
this.selector.input.focus(); this.selector.input.focus();
} }
}, null, 'send', false); },
chatRightsAction: 'send',
multiSelect: false
});
//this.scrollable = new Scrollable(this.body); //this.scrollable = new Scrollable(this.body);

161
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 { isTouchSupported } from "../helpers/touchSupport";
import { logger, LogLevels } from "../lib/logger"; import { logger, LogLevels } from "../lib/logger";
import smoothscroll, { SCROLL_TIME, SmoothScrollToOptions } from '../vendor/smoothscroll'; import fastSmoothScroll from "../helpers/fastSmoothScroll";
import rootScope from "../lib/rootScope"; import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck";
(window as any).__forceSmoothScrollPolyfill__ = true;
smoothscroll();
/* /*
var el = $0; var el = $0;
var height = 0; var height = 0;
@ -51,11 +49,10 @@ const scrollsIntersector = new IntersectionObserver(entries => {
export class ScrollableBase { export class ScrollableBase {
protected log: ReturnType<typeof logger>; protected log: ReturnType<typeof logger>;
public onScrollMeasure: number = 0;
protected onScroll: () => void; protected onScroll: () => void;
public getScrollValue: () => number;
public scrollLocked = 0; public isHeavyAnimationInProgress: boolean;
public scrollLockedPromise: CancellablePromise<void> = Promise.resolve();
constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) { constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
this.container.classList.add('scrollable'); this.container.classList.add('scrollable');
@ -73,55 +70,24 @@ export class ScrollableBase {
protected setListeners() { protected setListeners() {
window.addEventListener('resize', this.onScroll, {passive: true}); window.addEventListener('resize', this.onScroll, {passive: true});
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
}
public append(element: HTMLElement) {
this.container.append(element);
}
public scrollTo(value: number, side: 'top' | 'left', smooth = true, important = false, scrollTime = SCROLL_TIME) { useHeavyAnimationCheck(() => {
if(this.scrollLocked && !important) return; this.isHeavyAnimationInProgress = true;
const scrollValue = this.getScrollValue(); if(this.onScrollMeasure) {
if(scrollValue == Math.floor(value)) { window.cancelAnimationFrame(this.onScrollMeasure);
return;
} }
}, () => {
if(!rootScope.settings.animationsEnabled) { this.isHeavyAnimationInProgress = false;
smooth = false; this.onScroll();
scrollTime = 0; });
} }
const wasLocked = !!this.scrollLocked; public append(element: HTMLElement) {
if(wasLocked) clearTimeout(this.scrollLocked); this.container.append(element);
if(smooth) {
if(!wasLocked) {
this.scrollLockedPromise = deferredPromise<void>();
} }
this.scrollLocked = window.setTimeout(() => { public scrollIntoViewNew = fastSmoothScroll.bind(this, this.container);
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'));
}
}
} }
export type SliceSides = 'top' | 'bottom'; export type SliceSides = 'top' | 'bottom';
@ -135,8 +101,6 @@ export default class Scrollable extends ScrollableBase {
public onScrolledTop: () => void = null; public onScrolledTop: () => void = null;
public onScrolledBottom: () => void = null; public onScrolledBottom: () => void = null;
public onScrollMeasure: number = null;
public lastScrollTop: number = 0; public lastScrollTop: number = 0;
public lastScrollDirection: number = 0; public lastScrollDirection: number = 0;
@ -168,8 +132,16 @@ export default class Scrollable extends ScrollableBase {
//return; //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.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); if(this.onScrollMeasure) window.cancelAnimationFrame(this.onScrollMeasure);
this.onScrollMeasure = window.requestAnimationFrame(() => { this.onScrollMeasure = window.requestAnimationFrame(() => {
this.onScrollMeasure = 0; this.onScrollMeasure = 0;
@ -189,7 +161,7 @@ export default class Scrollable extends ScrollableBase {
}; };
public checkForTriggers = () => { public checkForTriggers = () => {
if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; if((!this.onScrolledTop && !this.onScrolledBottom) || this.isHeavyAnimationInProgress) return;
const scrollHeight = this.container.scrollHeight; const scrollHeight = this.container.scrollHeight;
if(!scrollHeight) { // незачем вызывать триггеры если блок пустой или не виден if(!scrollHeight) { // незачем вызывать триггеры если блок пустой или не виден
@ -219,66 +191,6 @@ export default class Scrollable extends ScrollableBase {
(this.splitUp || this.padding || this.container).append(...elements); (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() { get isScrolledDown() {
return this.scrollHeight - Math.round(this.scrollTop + this.container.offsetHeight) <= 1; 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.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;
};
} }

13
src/components/sidebarLeft/tabs/addMembers.ts

@ -2,6 +2,7 @@ import SidebarSlider, { SliderSuperTab } from "../../slider";
import AppSelectPeers from "../../appSelectPeers"; import AppSelectPeers from "../../appSelectPeers";
import { putPreloader } from "../../misc"; import { putPreloader } from "../../misc";
import Button from "../../button"; import Button from "../../button";
import { fastRaf } from "../../../helpers/schedulers";
export default class AppAddMembersTab extends SliderSuperTab { export default class AppAddMembersTab extends SliderSuperTab {
private nextBtn: HTMLButtonElement; private nextBtn: HTMLButtonElement;
@ -67,14 +68,18 @@ export default class AppAddMembersTab extends SliderSuperTab {
this.skippable = options.skippable; this.skippable = options.skippable;
this.onCloseAfterTimeout(); this.onCloseAfterTimeout();
this.selector = new AppSelectPeers(this.content, this.skippable ? null : (length) => { this.selector = new AppSelectPeers({
appendTo: this.content,
onChange: this.skippable ? null : (length) => {
this.nextBtn.classList.toggle('is-visible', !!length); this.nextBtn.classList.toggle('is-visible', !!length);
}, ['contacts']); },
peerType: ['contacts']
});
this.selector.input.placeholder = options.placeholder; this.selector.input.placeholder = options.placeholder;
if(options.selectedPeerIds) { if(options.selectedPeerIds) {
options.selectedPeerIds.forEach(peerId => { fastRaf(() => {
this.selector.add(peerId); this.selector.addInitial(options.selectedPeerIds);
}); });
} }

22
src/components/sidebarLeft/tabs/includedChats.ts

@ -8,6 +8,8 @@ import { MyDialogFilter as DialogFilter } from "../../../lib/storages/filters";
import rootScope from "../../../lib/rootScope"; import rootScope from "../../../lib/rootScope";
import { copy } from "../../../helpers/object"; import { copy } from "../../../helpers/object";
import ButtonIcon from "../../buttonIcon"; import ButtonIcon from "../../buttonIcon";
import { FocusDirection } from "../../../helpers/fastSmoothScroll";
import { fastRaf } from "../../../helpers/schedulers";
export default class AppIncludedChatsTab extends SliderSuperTab { export default class AppIncludedChatsTab extends SliderSuperTab {
private confirmBtn: HTMLElement; 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(); 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.selected = new Set(selectedPeers);
this.selector.input.placeholder = 'Search'; this.selector.input.placeholder = 'Search';
const _add = this.selector.add.bind(this.selector); const _add = this.selector.add.bind(this.selector);
this.selector.add = (peerId, title) => { this.selector.add = (peerId, title, scroll) => {
const div = _add(peerId, details[peerId]?.text); const div = _add(peerId, details[peerId]?.text, scroll);
if(details[peerId]) { if(details[peerId]) {
div.querySelector('avatar-element').classList.add('tgico-' + details[peerId].ico); 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); this.selector.list.parentElement.insertBefore(fragment, this.selector.list);
selectedPeers.forEach(peerId => { fastRaf(() => {
this.selector.add(peerId); this.selector.addInitial(selectedPeers);
}); });
for(const flag in filter.pFlags) { for(const flag in filter.pFlags) {
@ -215,11 +222,6 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
(categories.querySelector(`[data-peerId="${flag}"]`) as HTMLElement).click(); (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) => { onSelectChange = (length: number) => {

12
src/components/sidebarRight/tabs/forward.ts

@ -71,9 +71,13 @@ export default class AppForwardTab implements SliderTab {
this.cleanup(); this.cleanup();
this.mids = ids; this.mids = ids;
this.selector = new AppSelectPeers(this.container, (length) => { this.selector = new AppSelectPeers({
appendTo: this.container,
onChange: (length) => {
this.sendBtn.classList.toggle('is-visible', !!length); this.sendBtn.classList.toggle('is-visible', !!length);
}, ['dialogs', 'contacts'], () => { },
peerType: ['dialogs', 'contacts'],
onFirstRender: () => {
//console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount); //console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount);
// !!!!!!!!!! UNCOMMENT BELOW IF NEED TO USE THIS CLASS // !!!!!!!!!! UNCOMMENT BELOW IF NEED TO USE THIS CLASS
@ -84,6 +88,8 @@ export default class AppForwardTab implements SliderTab {
} }
}); });
document.body.classList.add('is-forward-active'); document.body.classList.add('is-forward-active');
}, null, 'send'); },
chatRightsAction: 'send'
});
} }
} }

52
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<void>
}
type AnimationInstanceKey = any;
const instances: Map<AnimationInstanceKey, AnimationInstance> = 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<void>() };
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);
}
});
}

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

6
src/helpers/schedulers.ts

@ -1,5 +1,5 @@
// * Jolly Cobra's schedulers // * Jolly Cobra's schedulers
import { AnyToVoidFunction } from "../types"; import { AnyToVoidFunction, NoneToVoidFunction } from "../types";
//type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow; //type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow;
@ -109,7 +109,7 @@ export const pause = (ms: number) => new Promise((resolve) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
/* let fastRafCallbacks: NoneToVoidFunction[] | undefined; let fastRafCallbacks: NoneToVoidFunction[] | undefined;
export function fastRaf(callback: NoneToVoidFunction) { export function fastRaf(callback: NoneToVoidFunction) {
if(!fastRafCallbacks) { if(!fastRafCallbacks) {
fastRafCallbacks = [callback]; fastRafCallbacks = [callback];
@ -122,4 +122,4 @@ export function fastRaf(callback: NoneToVoidFunction) {
} else { } else {
fastRafCallbacks.push(callback); fastRafCallbacks.push(callback);
} }
} */ }

59
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<void> = Promise.resolve();
let lastAnimationPromise: Promise<any>;
export const dispatchHeavyAnimationEvent = (promise: Promise<any>) => {
if(!isAnimating) {
heavyAnimationPromise = deferredPromise<void>();
}
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]);
};

9
src/lib/appManagers/appImManager.ts

@ -29,6 +29,7 @@ import SetTransition from '../../components/singleTransition';
import ChatDragAndDrop from '../../components/chat/dragAndDrop'; import ChatDragAndDrop from '../../components/chat/dragAndDrop';
import { debounce } from '../../helpers/schedulers'; import { debounce } from '../../helpers/schedulers';
import lottieLoader from '../lottieLoader'; import lottieLoader from '../lottieLoader';
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
//console.log('appImManager included33!'); //console.log('appImManager included33!');
@ -140,6 +141,14 @@ export class AppImManager {
this.setSettings(); this.setSettings();
rootScope.on('settings_updated', () => this.setSettings()); rootScope.on('settings_updated', () => this.setSettings());
useHeavyAnimationCheck(() => {
animationIntersector.setOnlyOnePlayableGroup('lock');
animationIntersector.checkAnimations(true);
}, () => {
animationIntersector.setOnlyOnePlayableGroup('');
animationIntersector.checkAnimations(false);
});
} }
private setSettings() { private setSettings() {

29
src/scss/partials/_chatBubble.scss

@ -58,10 +58,9 @@ $bubble-margin: .25rem;
} }
.bubble { .bubble {
padding-top: $bubble-margin;
position: relative; position: relative;
z-index: 1; z-index: 1;
margin: 0 auto; margin: 0 auto $bubble-margin;
user-select: none; user-select: none;
&.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & { &.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & {
@ -70,7 +69,7 @@ $bubble-margin: .25rem;
left: -50%; left: -50%;
/* top: 0; /* top: 0;
bottom: 0; */ bottom: 0; */
top: #{$bubble-margin / 2}; top: -#{$bubble-margin / 2};
bottom: -#{$bubble-margin / 2}; bottom: -#{$bubble-margin / 2};
content: " "; content: " ";
z-index: 1; z-index: 1;
@ -369,7 +368,11 @@ $bubble-margin: .25rem;
} */ } */
&.is-group-last { &.is-group-last {
padding-bottom: $bubble-margin; margin-bottom: #{$bubble-margin * 2};
&:before, &:after {
bottom: -#{$bubble-margin};
}
> .bubble-select-checkbox { > .bubble-select-checkbox {
bottom: 8px; bottom: 8px;
@ -382,6 +385,12 @@ $bubble-margin: .25rem;
} }
} }
&.is-group-first {
&:before, &:after {
top: -#{$bubble-margin};
}
}
&:not(.forwarded) { &:not(.forwarded) {
&:not(.is-group-first) { &:not(.is-group-first) {
.bubble__container > .name, .document-wrapper > .name { .bubble__container > .name, .document-wrapper > .name {
@ -1141,7 +1150,7 @@ $bubble-margin: .25rem;
&:first-of-type { &:first-of-type {
.document-selection { .document-selection {
top: -2px; // * padding inner + half padding outer top: -#{$bubble-margin / 2}; // * padding inner + half padding outer
} }
.document-wrapper { .document-wrapper {
@ -1153,7 +1162,7 @@ $bubble-margin: .25rem;
&:last-of-type { &:last-of-type {
.document-selection { .document-selection {
bottom: -2px; bottom: -#{$bubble-margin / 2};
} }
.document-wrapper { .document-wrapper {
@ -1175,7 +1184,7 @@ $bubble-margin: .25rem;
&.is-group-last .document-container { &.is-group-last .document-container {
&:last-of-type { &:last-of-type {
.document-selection { .document-selection {
bottom: -6px; bottom: -$bubble-margin;
} }
} }
} }
@ -1264,10 +1273,12 @@ $bubble-margin: .25rem;
bottom: 55px; bottom: 55px;
} }
&.sticker .message { &.sticker, &.with-replies.round, &.emoji-big {
.message {
bottom: 0; bottom: 0;
} }
} }
}
&.with-replies .attachment { &.with-replies .attachment {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
@ -1568,7 +1579,7 @@ $bubble-margin: .25rem;
// * fix scroll with only 1 bubble // * fix scroll with only 1 bubble
.bubbles-date-group:last-of-type { .bubbles-date-group:last-of-type {
.bubble:last-of-type { .bubble:last-of-type {
margin-bottom: 2px; margin-bottom: $bubble-margin;
/* &:after, .document-container:last-of-type .document-selection { /* &:after, .document-container:last-of-type .document-selection {
bottom: 0 !important; bottom: 0 !important;
} */ } */

Loading…
Cancel
Save