Fix smoothness of sending animation
Fix comments button for round video
This commit is contained in:
parent
f60d0f82ce
commit
fec0fdf72f
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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<number> = 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<any>;
|
||||
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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<boolean>((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);
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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')!;
|
||||
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)`;
|
||||
|
||||
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)`;
|
||||
|
||||
//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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 = new AppSelectPeers({
|
||||
appendTo: this.body,
|
||||
onChange: async() => {
|
||||
const peerId = this.selector.getSelected()[0];
|
||||
this.btnClose.click();
|
||||
|
||||
this.selector = null;
|
||||
this.selector = null;
|
||||
|
||||
await (onSelect ? onSelect() || Promise.resolve() : Promise.resolve());
|
||||
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
|
||||
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();
|
||||
}
|
||||
}, null, 'send', false);
|
||||
if(!isTouchSupported) {
|
||||
this.selector.input.focus();
|
||||
}
|
||||
},
|
||||
chatRightsAction: 'send',
|
||||
multiSelect: false
|
||||
});
|
||||
|
||||
//this.scrollable = new Scrollable(this.body);
|
||||
|
||||
|
@ -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<typeof logger>;
|
||||
|
||||
public onScrollMeasure: number = 0;
|
||||
protected onScroll: () => void;
|
||||
public getScrollValue: () => number;
|
||||
|
||||
public scrollLocked = 0;
|
||||
public scrollLockedPromise: CancellablePromise<void> = 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<void>();
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
52
src/helpers/animation.ts
Normal file
52
src/helpers/animation.ts
Normal file
@ -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
Normal file
149
src/helpers/fastSmoothScroll.ts
Normal file
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
59
src/hooks/useHeavyAnimationCheck.ts
Normal file
59
src/hooks/useHeavyAnimationCheck.ts
Normal file
@ -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]);
|
||||
};
|
@ -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() {
|
||||
|
@ -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;
|
||||
} */
|
||||
|
Loading…
x
Reference in New Issue
Block a user