Fix new message do not appear sometimes (#185)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
714929c72f
commit
82948c1f55
3 changed files with 259 additions and 207 deletions
39
src/app/organisms/room/EventLimit.js
Normal file
39
src/app/organisms/room/EventLimit.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
class EventLimit {
|
||||||
|
constructor() {
|
||||||
|
this._from = 0;
|
||||||
|
|
||||||
|
this.SMALLEST_EVT_HEIGHT = 32;
|
||||||
|
this.PAGES_COUNT = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxEvents() {
|
||||||
|
return Math.round(document.body.clientHeight / this.SMALLEST_EVT_HEIGHT) * this.PAGES_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
get from() {
|
||||||
|
return this._from;
|
||||||
|
}
|
||||||
|
|
||||||
|
get end() {
|
||||||
|
return this._from + this.maxEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxEvents(maxEvents) {
|
||||||
|
this.maxEvents = maxEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFrom(from) {
|
||||||
|
this._from = from < 0 ? 0 : from;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate(backwards, limit, timelineLength) {
|
||||||
|
this._from = backwards ? this._from - limit : this._from + limit;
|
||||||
|
|
||||||
|
if (!backwards && this.end > timelineLength) {
|
||||||
|
this._from = timelineLength - this.maxEvents;
|
||||||
|
}
|
||||||
|
if (this._from < 0) this._from = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventLimit;
|
|
@ -7,16 +7,13 @@ import React, {
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewContent.scss';
|
import './RoomViewContent.scss';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
import {
|
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
|
||||||
diffMinutes, isInSameDay, Throttle, getScrollInfo,
|
|
||||||
} from '../../../util/common';
|
|
||||||
|
|
||||||
import Divider from '../../atoms/divider/Divider';
|
import Divider from '../../atoms/divider/Divider';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
|
@ -27,17 +24,15 @@ import TimelineChange from '../../molecules/message/TimelineChange';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { parseTimelineChange } from './common';
|
import { parseTimelineChange } from './common';
|
||||||
|
import TimelineScroll from './TimelineScroll';
|
||||||
|
import EventLimit from './EventLimit';
|
||||||
|
|
||||||
const DEFAULT_MAX_EVENTS = 50;
|
|
||||||
const PAG_LIMIT = 30;
|
const PAG_LIMIT = 30;
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
const PLACEHOLDER_COUNT = 2;
|
const PLACEHOLDER_COUNT = 2;
|
||||||
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
const PLACEHOLDERS_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||||
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
const SCROLL_TRIGGER_POS = PLACEHOLDERS_HEIGHT * 4;
|
||||||
|
|
||||||
const SMALLEST_MSG_HEIGHT = 32;
|
|
||||||
const PAGES_COUNT = 4;
|
|
||||||
|
|
||||||
function loadingMsgPlaceholders(key, count = 2) {
|
function loadingMsgPlaceholders(key, count = 2) {
|
||||||
const pl = [];
|
const pl = [];
|
||||||
const genPlaceholders = () => {
|
const genPlaceholders = () => {
|
||||||
|
@ -124,178 +119,7 @@ function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineScroll extends EventEmitter {
|
function useTimeline(roomTimeline, eventId, readEventStore, eventLimitRef) {
|
||||||
constructor(target) {
|
|
||||||
super();
|
|
||||||
if (target === null) {
|
|
||||||
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
|
||||||
}
|
|
||||||
this.scroll = target;
|
|
||||||
|
|
||||||
this.backwards = false;
|
|
||||||
this.inTopHalf = false;
|
|
||||||
this.maxEvents = DEFAULT_MAX_EVENTS;
|
|
||||||
|
|
||||||
this.isScrollable = false;
|
|
||||||
this.top = 0;
|
|
||||||
this.bottom = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.viewHeight = 0;
|
|
||||||
|
|
||||||
this.topMsg = null;
|
|
||||||
this.bottomMsg = null;
|
|
||||||
this.diff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, maxScrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore scroll using previous calc by this._updateTopBottomMsg() and this._calcDiff.
|
|
||||||
tryRestoringScroll() {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
|
|
||||||
let scrollTop = 0;
|
|
||||||
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
|
||||||
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
|
||||||
else scrollTop = ot - this.diff;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, scrollTop);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToIndex(index, offset = 0) {
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const offsetTop = msgs[index]?.offsetTop;
|
|
||||||
|
|
||||||
if (offsetTop === undefined) return;
|
|
||||||
// if msg is already in visible are we don't need to scroll to that
|
|
||||||
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
|
||||||
const to = offsetTop - offset;
|
|
||||||
|
|
||||||
this._scrollTo(scrollInfo, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
_scrollTo(scrollInfo, scrollTop) {
|
|
||||||
this.scroll.scrollTop = scrollTop;
|
|
||||||
|
|
||||||
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
|
||||||
// so here we flag that the upcoming 'onscroll' event is
|
|
||||||
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
|
||||||
// only if it's changes.
|
|
||||||
// by doing so we prevent this._updateCalc() from calc again.
|
|
||||||
if (scrollTop !== this.top) {
|
|
||||||
this.scrolledByCode = true;
|
|
||||||
}
|
|
||||||
const sInfo = { ...scrollInfo };
|
|
||||||
|
|
||||||
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
|
||||||
|
|
||||||
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
|
||||||
this._updateCalc(sInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we maintain reference of top and bottom messages
|
|
||||||
// to restore the scroll position when
|
|
||||||
// messages gets removed from either end and added to other.
|
|
||||||
_updateTopBottomMsg() {
|
|
||||||
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
|
||||||
const lMsgIndex = msgs.length - 1;
|
|
||||||
|
|
||||||
this.topMsg = msgs[0]?.className === 'ph-msg'
|
|
||||||
? msgs[PLACEHOLDER_COUNT]
|
|
||||||
: msgs[0];
|
|
||||||
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
|
||||||
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
|
||||||
: msgs[lMsgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// we calculate the difference between first/last message and current scrollTop.
|
|
||||||
// if we are going above we calc diff between first and scrollTop
|
|
||||||
// else otherwise.
|
|
||||||
// NOTE: This will help to restore the scroll when msgs get's removed
|
|
||||||
// from one end and added to other end
|
|
||||||
_calcDiff(scrollInfo) {
|
|
||||||
if (!this.topMsg || !this.bottomMsg) return 0;
|
|
||||||
if (this.inTopHalf) {
|
|
||||||
return this.topMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
return this.bottomMsg.offsetTop - scrollInfo.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_calcMaxEvents(scrollInfo) {
|
|
||||||
return Math.round(scrollInfo.viewHeight / SMALLEST_MSG_HEIGHT) * PAGES_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateCalc(scrollInfo) {
|
|
||||||
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
|
||||||
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
|
||||||
const lastMiddle = this.top + halfViewHeight;
|
|
||||||
|
|
||||||
this.backwards = scrollMiddle < lastMiddle;
|
|
||||||
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
|
||||||
|
|
||||||
this.isScrollable = scrollInfo.isScrollable;
|
|
||||||
this.top = scrollInfo.top;
|
|
||||||
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
|
||||||
this.height = scrollInfo.height;
|
|
||||||
|
|
||||||
// only calculate maxEvents if viewHeight change
|
|
||||||
if (this.viewHeight !== scrollInfo.viewHeight) {
|
|
||||||
this.maxEvents = this._calcMaxEvents(scrollInfo);
|
|
||||||
this.viewHeight = scrollInfo.viewHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateTopBottomMsg();
|
|
||||||
this.diff = this._calcDiff(scrollInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcScroll() {
|
|
||||||
if (this.scrolledByCode) {
|
|
||||||
this.scrolledByCode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollInfo = getScrollInfo(this.scroll);
|
|
||||||
this._updateCalc(scrollInfo);
|
|
||||||
|
|
||||||
this.emit('scroll', this.backwards);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timelineScroll = null;
|
|
||||||
let jumpToItemIndex = -1;
|
|
||||||
const throttle = new Throttle();
|
|
||||||
const limit = {
|
|
||||||
from: 0,
|
|
||||||
getMaxEvents() {
|
|
||||||
return timelineScroll?.maxEvents ?? DEFAULT_MAX_EVENTS;
|
|
||||||
},
|
|
||||||
getEndIndex() {
|
|
||||||
return this.from + this.getMaxEvents();
|
|
||||||
},
|
|
||||||
calcNextFrom(backwards, tLength) {
|
|
||||||
let newFrom = backwards ? this.from - PAG_LIMIT : this.from + PAG_LIMIT;
|
|
||||||
if (!backwards && newFrom + this.getMaxEvents() > tLength) {
|
|
||||||
newFrom = tLength - this.getMaxEvents();
|
|
||||||
}
|
|
||||||
if (newFrom < 0) newFrom = 0;
|
|
||||||
return newFrom;
|
|
||||||
},
|
|
||||||
setFrom(from) {
|
|
||||||
if (from < 0) {
|
|
||||||
this.from = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.from = from;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function useTimeline(roomTimeline, eventId, readEventStore) {
|
|
||||||
const [timelineInfo, setTimelineInfo] = useState(null);
|
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||||
|
|
||||||
const setEventTimeline = async (eId) => {
|
const setEventTimeline = async (eId) => {
|
||||||
|
@ -309,6 +133,7 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
const initTimeline = (eId) => {
|
const initTimeline = (eId) => {
|
||||||
// NOTICE: eId can be id of readUpto, reply or specific event.
|
// NOTICE: eId can be id of readUpto, reply or specific event.
|
||||||
// readUpTo: when user click jump to unread message button.
|
// readUpTo: when user click jump to unread message button.
|
||||||
|
@ -331,9 +156,9 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusEventIndex > -1) {
|
if (focusEventIndex > -1) {
|
||||||
limit.setFrom(focusEventIndex - Math.round(limit.getMaxEvents() / 2));
|
limit.setFrom(focusEventIndex - Math.round(limit.maxEvents / 2));
|
||||||
} else {
|
} else {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
}
|
}
|
||||||
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null });
|
||||||
};
|
};
|
||||||
|
@ -350,17 +175,24 @@ function useTimeline(roomTimeline, eventId, readEventStore) {
|
||||||
return timelineInfo;
|
return timelineInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
function usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readEventStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnPagination = (backwards, loaded) => {
|
const handleOnPagination = (backwards, loaded) => {
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (loaded === 0) return;
|
if (loaded === 0) return;
|
||||||
if (!readEventStore.getItem()) {
|
if (!readEventStore.getItem()) {
|
||||||
const readUpToId = roomTimeline.getReadUpToEventId();
|
const readUpToId = roomTimeline.getReadUpToEventId();
|
||||||
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
|
||||||
}
|
}
|
||||||
limit.setFrom(limit.calcNextFrom(backwards, roomTimeline.timeline.length));
|
limit.paginate(backwards, PAG_LIMIT, roomTimeline.timeline.length);
|
||||||
setTimeout(() => setInfo({
|
setTimeout(() => setInfo({
|
||||||
backwards,
|
backwards,
|
||||||
loaded,
|
loaded,
|
||||||
|
@ -373,13 +205,15 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const autoPaginate = useCallback(async () => {
|
const autoPaginate = useCallback(async () => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (roomTimeline.isOngoingPagination) return;
|
if (roomTimeline.isOngoingPagination) return;
|
||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
|
|
||||||
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.getEndIndex() < tLength) {
|
if (limit.end < tLength) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(false, tLength));
|
limit.paginate(false, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateForward()) {
|
} else if (roomTimeline.canPaginateForward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
|
@ -390,7 +224,7 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
||||||
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
||||||
if (limit.from > 0) {
|
if (limit.from > 0) {
|
||||||
// paginate from memory
|
// paginate from memory
|
||||||
limit.setFrom(limit.calcNextFrom(true, tLength));
|
limit.paginate(true, PAG_LIMIT, tLength);
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
} else if (roomTimeline.canPaginateBackward()) {
|
} else if (roomTimeline.canPaginateBackward()) {
|
||||||
// paginate from server.
|
// paginate from server.
|
||||||
|
@ -402,13 +236,22 @@ function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) {
|
||||||
return [info, autoPaginate];
|
return [info, autoPaginate];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdateLimit) {
|
function useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readEventStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
) {
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// emit event to toggle scrollToBottom button visibility
|
// emit event to toggle scrollToBottom button visibility
|
||||||
const isAtBottom = (
|
const isAtBottom = (
|
||||||
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()
|
||||||
&& limit.getEndIndex() >= roomTimeline.timeline.length
|
&& limit.end >= roomTimeline.timeline.length
|
||||||
);
|
);
|
||||||
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
|
||||||
if (isAtBottom && readEventStore.getItem()) {
|
if (isAtBottom && readEventStore.getItem()) {
|
||||||
|
@ -419,11 +262,13 @@ function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdate
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const handleScrollToLive = useCallback(() => {
|
const handleScrollToLive = useCallback(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
if (readEventStore.getItem()) {
|
if (readEventStore.getItem()) {
|
||||||
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
requestAnimationFrame(() => roomTimeline.markAllAsRead());
|
||||||
}
|
}
|
||||||
if (roomTimeline.isServingLiveTimeline()) {
|
if (roomTimeline.isServingLiveTimeline()) {
|
||||||
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
|
||||||
timelineScroll.scrollToBottom();
|
timelineScroll.scrollToBottom();
|
||||||
forceUpdateLimit();
|
forceUpdateLimit();
|
||||||
return;
|
return;
|
||||||
|
@ -434,10 +279,13 @@ function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdate
|
||||||
return [handleScroll, handleScrollToLive];
|
return [handleScroll, handleScrollToLive];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventArrive(roomTimeline, readEventStore) {
|
function useEventArrive(roomTimeline, readEventStore, timelineScrollRef, eventLimitRef) {
|
||||||
const myUserId = initMatrix.matrixClient.getUserId();
|
const myUserId = initMatrix.matrixClient.getUserId();
|
||||||
const [newEvent, setEvent] = useState(null);
|
const [newEvent, setEvent] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
const sendReadReceipt = (event) => {
|
const sendReadReceipt = (event) => {
|
||||||
if (event.isSending()) return;
|
if (event.isSending()) return;
|
||||||
if (myUserId === event.getSender()) {
|
if (myUserId === event.getSender()) {
|
||||||
|
@ -470,11 +318,11 @@ function useEventArrive(roomTimeline, readEventStore) {
|
||||||
const tLength = roomTimeline.timeline.length;
|
const tLength = roomTimeline.timeline.length;
|
||||||
const isUserViewingLive = (
|
const isUserViewingLive = (
|
||||||
roomTimeline.isServingLiveTimeline()
|
roomTimeline.isServingLiveTimeline()
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
&& limit.end >= tLength - 1
|
||||||
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
|
&& timelineScroll.bottom < SCROLL_TRIGGER_POS
|
||||||
);
|
);
|
||||||
if (isUserViewingLive) {
|
if (isUserViewingLive) {
|
||||||
limit.setFrom(tLength - limit.getMaxEvents());
|
limit.setFrom(tLength - limit.maxEvents);
|
||||||
sendReadReceipt(event);
|
sendReadReceipt(event);
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
return;
|
return;
|
||||||
|
@ -486,7 +334,7 @@ function useEventArrive(roomTimeline, readEventStore) {
|
||||||
}
|
}
|
||||||
const isUserDitchedLive = (
|
const isUserDitchedLive = (
|
||||||
roomTimeline.isServingLiveTimeline()
|
roomTimeline.isServingLiveTimeline()
|
||||||
&& limit.getEndIndex() >= tLength - 1
|
&& limit.end >= tLength - 1
|
||||||
);
|
);
|
||||||
if (isUserDitchedLive) {
|
if (isUserDitchedLive) {
|
||||||
// This stateUpdate will help to put the
|
// This stateUpdate will help to put the
|
||||||
|
@ -506,6 +354,7 @@ function useEventArrive(roomTimeline, readEventStore) {
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
if (timelineScroll.bottom < 16
|
if (timelineScroll.bottom < 16
|
||||||
&& !roomTimeline.canPaginateForward()
|
&& !roomTimeline.canPaginateForward()
|
||||||
|
@ -517,27 +366,49 @@ function useEventArrive(roomTimeline, readEventStore) {
|
||||||
}, [newEvent, roomTimeline]);
|
}, [newEvent, roomTimeline]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jumpToItemIndex = -1;
|
||||||
|
|
||||||
function RoomViewContent({ eventId, roomTimeline }) {
|
function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
|
const [throttle] = useState(new Throttle());
|
||||||
|
|
||||||
const timelineSVRef = useRef(null);
|
const timelineSVRef = useRef(null);
|
||||||
|
const timelineScrollRef = useRef(null);
|
||||||
|
const eventLimitRef = useRef(null);
|
||||||
|
|
||||||
const readEventStore = useStore(roomTimeline);
|
const readEventStore = useStore(roomTimeline);
|
||||||
const timelineInfo = useTimeline(roomTimeline, eventId, readEventStore);
|
|
||||||
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||||
const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, readEventStore, forceUpdateLimit);
|
|
||||||
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
const timelineInfo = useTimeline(roomTimeline, eventId, readEventStore, eventLimitRef);
|
||||||
roomTimeline, autoPaginate, readEventStore, forceUpdateLimit,
|
const [paginateInfo, autoPaginate] = usePaginate(
|
||||||
|
roomTimeline,
|
||||||
|
readEventStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
);
|
);
|
||||||
useEventArrive(roomTimeline, readEventStore);
|
const [handleScroll, handleScrollToLive] = useHandleScroll(
|
||||||
|
roomTimeline,
|
||||||
|
autoPaginate,
|
||||||
|
readEventStore,
|
||||||
|
forceUpdateLimit,
|
||||||
|
timelineScrollRef,
|
||||||
|
eventLimitRef,
|
||||||
|
);
|
||||||
|
useEventArrive(roomTimeline, readEventStore, timelineScrollRef, eventLimitRef);
|
||||||
|
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!roomTimeline.initialized) {
|
if (!roomTimeline.initialized) {
|
||||||
timelineScroll = new TimelineScroll(timelineSVRef.current);
|
timelineScrollRef.current = new TimelineScroll(timelineSVRef.current);
|
||||||
|
eventLimitRef.current = new EventLimit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// when active timeline changes
|
// when active timeline changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return undefined;
|
if (!roomTimeline.initialized) return undefined;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
|
|
||||||
if (timeline.length > 0) {
|
if (timeline.length > 0) {
|
||||||
if (jumpToItemIndex === -1) {
|
if (jumpToItemIndex === -1) {
|
||||||
|
@ -555,11 +426,9 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
}
|
}
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
|
|
||||||
timelineScroll.on('scroll', handleScroll);
|
|
||||||
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
return () => {
|
return () => {
|
||||||
if (timelineSVRef.current === null) return;
|
if (timelineSVRef.current === null) return;
|
||||||
timelineScroll.removeListener('scroll', handleScroll);
|
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive);
|
||||||
};
|
};
|
||||||
}, [timelineInfo]);
|
}, [timelineInfo]);
|
||||||
|
@ -567,6 +436,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
// when paginating from server
|
// when paginating from server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
}, [paginateInfo]);
|
}, [paginateInfo]);
|
||||||
|
@ -574,17 +444,24 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
// when paginating locally
|
// when paginating locally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
|
const timelineScroll = timelineScrollRef.current;
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
}, [onLimitUpdate]);
|
}, [onLimitUpdate]);
|
||||||
|
|
||||||
const handleTimelineScroll = (event) => {
|
const handleTimelineScroll = (event) => {
|
||||||
const { target } = event;
|
const timelineScroll = timelineScrollRef.current;
|
||||||
if (!target) return;
|
if (!event.target) return;
|
||||||
throttle._(() => timelineScroll?.calcScroll(), 400)(target);
|
|
||||||
|
throttle._(() => {
|
||||||
|
const backwards = timelineScroll?.calcScroll();
|
||||||
|
if (typeof backwards !== 'boolean') return;
|
||||||
|
handleScroll(backwards);
|
||||||
|
}, 200)();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTimeline = () => {
|
const renderTimeline = () => {
|
||||||
const tl = [];
|
const tl = [];
|
||||||
|
const limit = eventLimitRef.current;
|
||||||
|
|
||||||
let itemCountIndex = 0;
|
let itemCountIndex = 0;
|
||||||
jumpToItemIndex = -1;
|
jumpToItemIndex = -1;
|
||||||
|
@ -595,7 +472,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||||
itemCountIndex += PLACEHOLDER_COUNT;
|
itemCountIndex += PLACEHOLDER_COUNT;
|
||||||
}
|
}
|
||||||
for (let i = limit.from; i < limit.getEndIndex(); i += 1) {
|
for (let i = limit.from; i < limit.end; i += 1) {
|
||||||
if (i >= timeline.length) break;
|
if (i >= timeline.length) break;
|
||||||
const mEvent = timeline[i];
|
const mEvent = timeline[i];
|
||||||
const prevMEvent = timeline[i - 1] ?? null;
|
const prevMEvent = timeline[i - 1] ?? null;
|
||||||
|
@ -637,7 +514,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
|
||||||
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
|
||||||
itemCountIndex += 1;
|
itemCountIndex += 1;
|
||||||
}
|
}
|
||||||
if (roomTimeline.canPaginateForward() || limit.getEndIndex() < timeline.length) {
|
if (roomTimeline.canPaginateForward() || limit.end < timeline.length) {
|
||||||
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
136
src/app/organisms/room/TimelineScroll.js
Normal file
136
src/app/organisms/room/TimelineScroll.js
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { getScrollInfo } from '../../../util/common';
|
||||||
|
|
||||||
|
class TimelineScroll {
|
||||||
|
constructor(target) {
|
||||||
|
if (target === null) {
|
||||||
|
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
|
||||||
|
}
|
||||||
|
this.scroll = target;
|
||||||
|
|
||||||
|
this.backwards = false;
|
||||||
|
this.inTopHalf = false;
|
||||||
|
|
||||||
|
this.isScrollable = false;
|
||||||
|
this.top = 0;
|
||||||
|
this.bottom = 0;
|
||||||
|
this.height = 0;
|
||||||
|
this.viewHeight = 0;
|
||||||
|
|
||||||
|
this.topMsg = null;
|
||||||
|
this.bottomMsg = null;
|
||||||
|
this.diff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, maxScrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use previous calc by this._updateTopBottomMsg() & this._calcDiff.
|
||||||
|
tryRestoringScroll() {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
|
||||||
|
let scrollTop = 0;
|
||||||
|
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
|
||||||
|
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
|
||||||
|
else scrollTop = ot - this.diff;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToIndex(index, offset = 0) {
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||||
|
const offsetTop = msgs[index]?.offsetTop;
|
||||||
|
|
||||||
|
if (offsetTop === undefined) return;
|
||||||
|
// if msg is already in visible are we don't need to scroll to that
|
||||||
|
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
|
||||||
|
const to = offsetTop - offset;
|
||||||
|
|
||||||
|
this._scrollTo(scrollInfo, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollTo(scrollInfo, scrollTop) {
|
||||||
|
this.scroll.scrollTop = scrollTop;
|
||||||
|
|
||||||
|
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
|
||||||
|
// so here we flag that the upcoming 'onscroll' event is
|
||||||
|
// emitted as side effect of assigning 'this.scroll.scrollTop' above
|
||||||
|
// only if it's changes.
|
||||||
|
// by doing so we prevent this._updateCalc() from calc again.
|
||||||
|
if (scrollTop !== this.top) {
|
||||||
|
this.scrolledByCode = true;
|
||||||
|
}
|
||||||
|
const sInfo = { ...scrollInfo };
|
||||||
|
|
||||||
|
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
|
||||||
|
this._updateCalc(sInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we maintain reference of top and bottom messages
|
||||||
|
// to restore the scroll position when
|
||||||
|
// messages gets removed from either end and added to other.
|
||||||
|
_updateTopBottomMsg() {
|
||||||
|
const msgs = this.scroll.lastElementChild.lastElementChild.children;
|
||||||
|
const lMsgIndex = msgs.length - 1;
|
||||||
|
|
||||||
|
// TODO: classname 'ph-msg' prevent this class from being used
|
||||||
|
const PLACEHOLDER_COUNT = 2;
|
||||||
|
this.topMsg = msgs[0]?.className === 'ph-msg'
|
||||||
|
? msgs[PLACEHOLDER_COUNT]
|
||||||
|
: msgs[0];
|
||||||
|
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
|
||||||
|
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
|
||||||
|
: msgs[lMsgIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we calculate the difference between first/last message and current scrollTop.
|
||||||
|
// if we are going above we calc diff between first and scrollTop
|
||||||
|
// else otherwise.
|
||||||
|
// NOTE: This will help to restore the scroll when msgs get's removed
|
||||||
|
// from one end and added to other end
|
||||||
|
_calcDiff(scrollInfo) {
|
||||||
|
if (!this.topMsg || !this.bottomMsg) return 0;
|
||||||
|
if (this.inTopHalf) {
|
||||||
|
return this.topMsg.offsetTop - scrollInfo.top;
|
||||||
|
}
|
||||||
|
return this.bottomMsg.offsetTop - scrollInfo.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCalc(scrollInfo) {
|
||||||
|
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
|
||||||
|
const scrollMiddle = scrollInfo.top + halfViewHeight;
|
||||||
|
const lastMiddle = this.top + halfViewHeight;
|
||||||
|
|
||||||
|
this.backwards = scrollMiddle < lastMiddle;
|
||||||
|
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
|
||||||
|
|
||||||
|
this.isScrollable = scrollInfo.isScrollable;
|
||||||
|
this.top = scrollInfo.top;
|
||||||
|
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
|
||||||
|
this.height = scrollInfo.height;
|
||||||
|
this.viewHeight = scrollInfo.viewHeight;
|
||||||
|
|
||||||
|
this._updateTopBottomMsg();
|
||||||
|
this.diff = this._calcDiff(scrollInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcScroll() {
|
||||||
|
if (this.scrolledByCode) {
|
||||||
|
this.scrolledByCode = false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollInfo = getScrollInfo(this.scroll);
|
||||||
|
this._updateCalc(scrollInfo);
|
||||||
|
|
||||||
|
return this.backwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimelineScroll;
|
Loading…
Reference in a new issue