Browse Source

Fix jumping chat while scroll animation is active

Fix scroll to top position
master
morethanwords 4 years ago
parent
commit
51d35d71e3
  1. 2
      src/components/animationIntersector.ts
  2. 86
      src/components/chat/bubbles.ts
  3. 5
      src/helpers/animation.ts
  4. 2
      src/helpers/fastSmoothScroll.ts
  5. 36
      src/hooks/useHeavyAnimationCheck.ts
  6. 6
      src/scss/partials/_chatBubble.scss

2
src/components/animationIntersector.ts

@ -142,7 +142,7 @@ export class AnimationIntersector {
animation.autoplay && animation.autoplay &&
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) (!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group)
) { ) {
console.warn('play animation:', animation); //console.warn('play animation:', animation);
animation.play(); animation.play();
} }
} }

86
src/components/chat/bubbles.ts

@ -40,8 +40,9 @@ 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 { FocusDirection } from "../../helpers/fastSmoothScroll";
import useHeavyAnimationCheck, { getHeavyAnimationPromise } from "../../hooks/useHeavyAnimationCheck"; import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent } from "../../hooks/useHeavyAnimationCheck";
import { fastRaf } from "../../helpers/schedulers"; import { fastRaf } from "../../helpers/schedulers";
import { deferredPromise, CancellablePromise } from "../../helpers/cancellablePromise";
const IGNORE_ACTIONS = ['messageActionHistoryClear']; const IGNORE_ACTIONS = ['messageActionHistoryClear'];
@ -295,30 +296,35 @@ export default class ChatBubbles {
this.listenerSetter.add(rootScope, 'messages_downloaded', (e) => { this.listenerSetter.add(rootScope, 'messages_downloaded', (e) => {
const {peerId, mids} = e.detail; const {peerId, mids} = e.detail;
(mids as number[]).forEach(mid => { const middleware = this.getMiddleware();
/* const promise = (this.scrollable.scrollLocked && this.scrollable.scrollLockedPromise) || Promise.resolve(); getHeavyAnimationPromise().then(() => {
promise.then(() => { if(!middleware()) return;
}); */ (mids as number[]).forEach(mid => {
this.needUpdate.forEachReverse((obj, idx) => { /* const promise = (this.scrollable.scrollLocked && this.scrollable.scrollLockedPromise) || Promise.resolve();
if(obj.replyMid === mid, obj.replyToPeerId === peerId) { promise.then(() => {
const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0];
}); */
//this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]); this.needUpdate.forEachReverse((obj, idx) => {
const bubble = this.bubbles[mid]; if(obj.replyMid === mid, obj.replyToPeerId === peerId) {
if(!bubble) return; const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0];
const message = this.chat.getMessage(mid); //this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]);
const bubble = this.bubbles[mid];
const repliedMessage = this.appMessagesManager.getMessageByPeer(obj.replyToPeerId, replyMid); if(!bubble) return;
if(repliedMessage.deleted) { // ! чтобы не пыталось бесконечно загрузить удалённое сообщение
delete message.reply_to_mid; // ! WARNING! const message = this.chat.getMessage(mid);
const repliedMessage = this.appMessagesManager.getMessageByPeer(obj.replyToPeerId, replyMid);
if(repliedMessage.deleted) { // ! чтобы не пыталось бесконечно загрузить удалённое сообщение
delete message.reply_to_mid; // ! WARNING!
}
this.renderMessage(message, true, false, bubble, false);
//this.renderMessage(message, true, true, bubble, false);
} }
});
this.renderMessage(message, true, false, bubble, false);
//this.renderMessage(message, true, true, bubble, false);
}
}); });
}); });
}); });
@ -1421,7 +1427,7 @@ export default class ChatBubbles {
} }
public setMessagesQueuePromise() { public setMessagesQueuePromise() {
if(this.messagesQueuePromise) return; if(this.messagesQueuePromise || !this.messagesQueue.length) return;
this.messagesQueuePromise = new Promise((resolve, reject) => { this.messagesQueuePromise = new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
@ -1525,7 +1531,7 @@ export default class ChatBubbles {
const peerId = this.peerId; const peerId = this.peerId;
// * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right) // * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right)
const our = message.fromId == rootScope.myId || (message.pFlags.out && this.appPeersManager.isMegagroup(this.peerId)); const our = message.fromId === rootScope.myId || (message.pFlags.out && this.appPeersManager.isMegagroup(this.peerId));
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.classList.add('message'); messageDiv.classList.add('message');
@ -1553,7 +1559,7 @@ export default class ChatBubbles {
} }
} }
} else { } else {
const save = ['is-highlighted']; const save = ['is-highlighted', 'zoom-fade'];
const wasClassNames = bubble.className.split(' '); const wasClassNames = bubble.className.split(' ');
const classNames = ['bubble'].concat(save.filter(c => wasClassNames.includes(c))); const classNames = ['bubble'].concat(save.filter(c => wasClassNames.includes(c)));
bubble.className = classNames.join(' '); bubble.className = classNames.join(' ');
@ -1561,9 +1567,11 @@ export default class ChatBubbles {
bubbleContainer = bubble.lastElementChild as HTMLDivElement; bubbleContainer = bubble.lastElementChild as HTMLDivElement;
bubbleContainer.innerHTML = ''; bubbleContainer.innerHTML = '';
//bubbleContainer.style.marginBottom = ''; //bubbleContainer.style.marginBottom = '';
const animationDelay = bubbleContainer.style.animationDelay;
bubbleContainer.style.cssText = ''; bubbleContainer.style.cssText = '';
bubbleContainer.style.animationDelay = animationDelay;
if(bubble == this.firstUnreadBubble) { if(bubble === this.firstUnreadBubble) {
bubble.classList.add('is-first-unread'); bubble.classList.add('is-first-unread');
} }
@ -2514,7 +2522,9 @@ export default class ChatBubbles {
////console.timeEnd('render history total'); ////console.timeEnd('render history total');
return this.performHistoryResult(result.history || [], reverse, isBackLimit, !isFirstMessageRender && additionMsgId); return getHeavyAnimationPromise().then(() => {
return this.performHistoryResult(result.history || [], reverse, isBackLimit, !isFirstMessageRender && additionMsgId);
});
}, (err) => { }, (err) => {
this.log.error('getHistory error:', err); this.log.error('getHistory error:', err);
return false; return false;
@ -2533,7 +2543,9 @@ export default class ChatBubbles {
cached = true; cached = true;
this.log('getHistory cached result by maxId:', maxId, reverse, isBackLimit, result, peerId, justLoad); this.log('getHistory cached result by maxId:', maxId, reverse, isBackLimit, result, peerId, justLoad);
processResult(result); processResult(result);
promise = this.performHistoryResult(result.history || [], reverse, isBackLimit, !isFirstMessageRender && additionMsgId); promise = getHeavyAnimationPromise().then(() => {
return this.performHistoryResult((result as HistoryResult).history || [], reverse, isBackLimit, !isFirstMessageRender && additionMsgId);
});
//return (reverse ? this.getHistoryTopPromise = promise : this.getHistoryBottomPromise = promise); //return (reverse ? this.getHistoryTopPromise = promise : this.getHistoryBottomPromise = promise);
//return this.performHistoryResult(result.history || [], reverse, isBackLimit, additionMsgID, true); //return this.performHistoryResult(result.history || [], reverse, isBackLimit, additionMsgID, true);
} }
@ -2544,21 +2556,33 @@ export default class ChatBubbles {
waitPromise.then(() => { waitPromise.then(() => {
if(rootScope.settings.animationsEnabled) { if(rootScope.settings.animationsEnabled) {
const mids = getObjectKeysAndSort(this.bubbles, 'desc').filter(mid => !additionMsgIds.includes(mid)); const mids = getObjectKeysAndSort(this.bubbles, 'desc').filter(mid => !additionMsgIds.includes(mid));
const animationPromise = deferredPromise<void>();
let lastMsDelay = 0;
mids.forEach((mid, idx) => { mids.forEach((mid, idx) => {
const bubble = this.bubbles[mid]; const bubble = this.bubbles[mid];
lastMsDelay = ((idx || 0.1) * 10);
//if(idx || isSafari) { //if(idx || isSafari) {
// ! 0.1 = 1ms задержка для Safari, без этого первое сообщение над самым нижним может появиться позже другого с animation-delay, LOL ! // ! 0.1 = 1ms задержка для Safari, без этого первое сообщение над самым нижним может появиться позже другого с animation-delay, LOL !
bubble.style.animationDelay = ((idx || 0.1) * 10) + 'ms'; bubble.style.animationDelay = lastMsDelay + 'ms';
//} //}
bubble.classList.add('zoom-fade'); bubble.classList.add('zoom-fade');
bubble.addEventListener('animationend', () => { bubble.addEventListener('animationend', () => {
bubble.style.animationDelay = ''; bubble.style.animationDelay = '';
bubble.classList.remove('zoom-fade'); bubble.classList.remove('zoom-fade');
if(idx === (mids.length - 1)) {
animationPromise.resolve();
}
}, {once: true}); }, {once: true});
//this.log('supa', bubble); //this.log('supa', bubble);
}); });
if(mids.length) {
dispatchHeavyAnimationEvent(animationPromise, lastMsDelay);
}
} }
setTimeout(() => { setTimeout(() => {

5
src/helpers/animation.ts

@ -19,6 +19,7 @@ export function cancelAnimationByKey(key: AnimationInstanceKey) {
const instance = getAnimationInstance(key); const instance = getAnimationInstance(key);
if(instance) { if(instance) {
instance.isCancelled = true; instance.isCancelled = true;
instance.deferred.resolve();
instances.delete(key); instances.delete(key);
} }
} }
@ -31,7 +32,9 @@ export function animateSingle(tick: Function, key: AnimationInstanceKey, instanc
} }
fastRaf(() => { fastRaf(() => {
if(instance.isCancelled) return; if(instance.isCancelled) {
return;
}
if(tick()) { if(tick()) {
animateSingle(tick, key, instance); animateSingle(tick, key, instance);

2
src/helpers/fastSmoothScroll.ts

@ -95,7 +95,7 @@ function scrollWithJs(
switch(position) { switch(position) {
case 'start': case 'start':
path = (elementPosition - margin) - scrollPosition; path = elementPosition - margin;
break; break;
case 'end': case 'end':
//path = (elementTop + elementHeight + margin) - containerHeight; //path = (elementTop + elementHeight + margin) - containerHeight;

36
src/hooks/useHeavyAnimationCheck.ts

@ -4,31 +4,43 @@
import { AnyToVoidFunction } from '../types'; import { AnyToVoidFunction } from '../types';
import ListenerSetter from '../helpers/listenerSetter'; import ListenerSetter from '../helpers/listenerSetter';
import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise'; import { CancellablePromise, deferredPromise } from '../helpers/cancellablePromise';
import { pause } from '../helpers/schedulers';
const ANIMATION_START_EVENT = 'event-heavy-animation-start'; const ANIMATION_START_EVENT = 'event-heavy-animation-start';
const ANIMATION_END_EVENT = 'event-heavy-animation-end'; const ANIMATION_END_EVENT = 'event-heavy-animation-end';
let isAnimating = false; let isAnimating = false;
let heavyAnimationPromise: CancellablePromise<void> = Promise.resolve(); let heavyAnimationPromise: CancellablePromise<void> = Promise.resolve();
let lastAnimationPromise: Promise<any>; let promisesInQueue = 0;
export const dispatchHeavyAnimationEvent = (promise: Promise<any>) => { export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: number) => {
if(!isAnimating) { if(!isAnimating) {
heavyAnimationPromise = deferredPromise<void>(); heavyAnimationPromise = deferredPromise<void>();
document.dispatchEvent(new Event(ANIMATION_START_EVENT));
isAnimating = true;
console.log('dispatchHeavyAnimationEvent: start');
} }
++promisesInQueue;
console.log('dispatchHeavyAnimationEvent: attach promise, length:', promisesInQueue);
document.dispatchEvent(new Event(ANIMATION_START_EVENT)); const promises = [
isAnimating = true; timeout !== undefined ? pause(timeout) : undefined,
lastAnimationPromise = promise; promise.finally(() => {})
].filter(Boolean);
promise.then(() => { const perf = performance.now();
if(lastAnimationPromise !== promise) { Promise.race(promises).then(() => {
return; --promisesInQueue;
} console.log('dispatchHeavyAnimationEvent: promise end, length:', promisesInQueue, performance.now() - perf);
if(!promisesInQueue) {
isAnimating = false;
promisesInQueue = 0;
document.dispatchEvent(new Event(ANIMATION_END_EVENT));
heavyAnimationPromise.resolve();
isAnimating = false; console.log('dispatchHeavyAnimationEvent: end');
document.dispatchEvent(new Event(ANIMATION_END_EVENT)); }
heavyAnimationPromise.resolve();
}); });
return heavyAnimationPromise; return heavyAnimationPromise;

6
src/scss/partials/_chatBubble.scss

@ -126,7 +126,7 @@ $bubble-margin: .25rem;
&.is-highlighted, &.is-selected { &.is-highlighted, &.is-selected {
&:after { &:after {
top: #{2rem + $bubble-margin} !important; top: calc(#{$bubble-margin / 2} + 30px);
} }
} }
} }
@ -370,7 +370,7 @@ $bubble-margin: .25rem;
&.is-group-last { &.is-group-last {
margin-bottom: #{$bubble-margin * 2}; margin-bottom: #{$bubble-margin * 2};
&:before, &:after { &:after {
bottom: -#{$bubble-margin}; bottom: -#{$bubble-margin};
} }
@ -386,7 +386,7 @@ $bubble-margin: .25rem;
} }
&.is-group-first { &.is-group-first {
&:before, &:after { &:after {
top: -#{$bubble-margin}; top: -#{$bubble-margin};
} }
} }

Loading…
Cancel
Save