Added local timeline pagination
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
38cbb87a62
commit
25b7093302
3 changed files with 92 additions and 25 deletions
|
@ -2,7 +2,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function useForceUpdate() {
|
export function useForceUpdate() {
|
||||||
const [, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
return () => setData({});
|
return [data, () => setData({})];
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,11 @@ import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
||||||
import TimelineChange from '../../molecules/message/TimelineChange';
|
import TimelineChange from '../../molecules/message/TimelineChange';
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { parseTimelineChange } from './common';
|
import { parseTimelineChange } from './common';
|
||||||
|
|
||||||
|
const DEFAULT_MAX_EVENTS = 50;
|
||||||
|
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;
|
||||||
|
@ -118,7 +121,7 @@ class TimelineScroll extends EventEmitter {
|
||||||
|
|
||||||
this.backwards = false;
|
this.backwards = false;
|
||||||
this.inTopHalf = false;
|
this.inTopHalf = false;
|
||||||
this.maxEvents = 50;
|
this.maxEvents = DEFAULT_MAX_EVENTS;
|
||||||
|
|
||||||
this.isScrollable = false;
|
this.isScrollable = false;
|
||||||
this.top = 0;
|
this.top = 0;
|
||||||
|
@ -253,11 +256,44 @@ class TimelineScroll extends EventEmitter {
|
||||||
let timelineScroll = null;
|
let timelineScroll = null;
|
||||||
let focusEventIndex = null;
|
let focusEventIndex = null;
|
||||||
const throttle = new Throttle();
|
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) {
|
function useTimeline(roomTimeline, eventId) {
|
||||||
const [timelineInfo, setTimelineInfo] = useState(null);
|
const [timelineInfo, setTimelineInfo] = useState(null);
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// open specific event.
|
||||||
|
// 1. readUpTo event is in specific timeline
|
||||||
|
// 2. readUpTo event isn't in specific timeline
|
||||||
|
// 3. readUpTo event is specific event
|
||||||
|
// open live timeline.
|
||||||
|
// 1. readUpTo event is in live timeline
|
||||||
|
// 2. readUpTo event isn't in live timeline
|
||||||
const initTimeline = (eId) => {
|
const initTimeline = (eId) => {
|
||||||
|
limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents());
|
||||||
setTimelineInfo({
|
setTimelineInfo({
|
||||||
focusEventId: eId,
|
focusEventId: eId,
|
||||||
});
|
});
|
||||||
|
@ -279,17 +315,20 @@ function useTimeline(roomTimeline, eventId) {
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
|
||||||
roomTimeline.removeInternalListeners();
|
roomTimeline.removeInternalListeners();
|
||||||
|
limit.setFrom(0);
|
||||||
};
|
};
|
||||||
}, [roomTimeline, eventId]);
|
}, [roomTimeline, eventId]);
|
||||||
|
|
||||||
return timelineInfo;
|
return timelineInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useOnPaginate(roomTimeline) {
|
function usePaginate(roomTimeline, forceUpdateLimit) {
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnPagination = (backwards, loaded, canLoadMore) => {
|
const handleOnPagination = (backwards, loaded, canLoadMore) => {
|
||||||
|
if (loaded === 0) return;
|
||||||
|
limit.setFrom(limit.calcNextFrom(backwards, roomTimeline.timeline.length));
|
||||||
setInfo({
|
setInfo({
|
||||||
backwards,
|
backwards,
|
||||||
loaded,
|
loaded,
|
||||||
|
@ -302,28 +341,45 @@ function useOnPaginate(roomTimeline) {
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
return info;
|
const autoPaginate = useCallback(() => {
|
||||||
}
|
|
||||||
|
|
||||||
function useAutoPaginate(roomTimeline) {
|
|
||||||
return useCallback(() => {
|
|
||||||
if (roomTimeline.isOngoingPagination) return;
|
if (roomTimeline.isOngoingPagination) return;
|
||||||
|
const tLength = roomTimeline.timeline.length;
|
||||||
|
|
||||||
if (timelineScroll.bottom < SCROLL_TRIGGER_POS && roomTimeline.canPaginateForward()) {
|
if (timelineScroll.bottom < SCROLL_TRIGGER_POS) {
|
||||||
roomTimeline.paginateTimeline(false);
|
if (limit.getEndIndex() < tLength) {
|
||||||
|
// paginate from memory
|
||||||
|
limit.setFrom(limit.calcNextFrom(false, tLength));
|
||||||
|
forceUpdateLimit();
|
||||||
|
} else if (roomTimeline.canPaginateForward()) {
|
||||||
|
// paginate from server.
|
||||||
|
roomTimeline.paginateTimeline(false, PAG_LIMIT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (timelineScroll.top < SCROLL_TRIGGER_POS && roomTimeline.canPaginateBackward()) {
|
}
|
||||||
roomTimeline.paginateTimeline(true);
|
if (timelineScroll.top < SCROLL_TRIGGER_POS) {
|
||||||
|
if (limit.from > 0) {
|
||||||
|
// paginate from memory
|
||||||
|
limit.setFrom(limit.calcNextFrom(true, tLength));
|
||||||
|
forceUpdateLimit();
|
||||||
|
} else if (roomTimeline.canPaginateBackward()) {
|
||||||
|
// paginate from server.
|
||||||
|
roomTimeline.paginateTimeline(true, PAG_LIMIT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
|
return [info, autoPaginate];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHandleScroll(roomTimeline, autoPaginate, viewEvent) {
|
function useHandleScroll(roomTimeline, autoPaginate, viewEvent) {
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// emit event to toggle scrollToBottom button visibility
|
// emit event to toggle scrollToBottom button visibility
|
||||||
const isAtBottom = timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward();
|
const isAtBottom = (
|
||||||
|
timelineScroll.bottom < 16
|
||||||
|
&& !roomTimeline.canPaginateForward()
|
||||||
|
&& limit.getEndIndex() === roomTimeline.length
|
||||||
|
);
|
||||||
viewEvent.emit('at-bottom', isAtBottom);
|
viewEvent.emit('at-bottom', isAtBottom);
|
||||||
});
|
});
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
|
@ -334,6 +390,10 @@ function useEventArrive(roomTimeline) {
|
||||||
const [newEvent, setEvent] = useState(null);
|
const [newEvent, setEvent] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEvent = (event) => {
|
const handleEvent = (event) => {
|
||||||
|
const tLength = roomTimeline.timeline.length;
|
||||||
|
if (roomTimeline.isServingLiveTimeline() && tLength - 1 === limit.getEndIndex()) {
|
||||||
|
limit.setFrom(tLength - limit.getMaxEvents());
|
||||||
|
}
|
||||||
setEvent(event);
|
setEvent(event);
|
||||||
};
|
};
|
||||||
roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
|
roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent);
|
||||||
|
@ -354,10 +414,10 @@ function RoomViewContent({
|
||||||
eventId, roomTimeline, viewEvent,
|
eventId, roomTimeline, viewEvent,
|
||||||
}) {
|
}) {
|
||||||
const timelineSVRef = useRef(null);
|
const timelineSVRef = useRef(null);
|
||||||
const timelineInfo = useTimeline(roomTimeline, eventId);
|
|
||||||
const readEventStore = useStore(roomTimeline);
|
const readEventStore = useStore(roomTimeline);
|
||||||
const paginateInfo = useOnPaginate(roomTimeline);
|
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
|
||||||
const autoPaginate = useAutoPaginate(roomTimeline);
|
const timelineInfo = useTimeline(roomTimeline, eventId);
|
||||||
|
const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, forceUpdateLimit);
|
||||||
const handleScroll = useHandleScroll(roomTimeline, autoPaginate, viewEvent);
|
const handleScroll = useHandleScroll(roomTimeline, autoPaginate, viewEvent);
|
||||||
useEventArrive(roomTimeline);
|
useEventArrive(roomTimeline);
|
||||||
const { timeline } = roomTimeline;
|
const { timeline } = roomTimeline;
|
||||||
|
@ -394,15 +454,19 @@ function RoomViewContent({
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!roomTimeline.initialized) return;
|
if (!roomTimeline.initialized) return;
|
||||||
// TODO: decide is restore scroll
|
|
||||||
timelineScroll.tryRestoringScroll();
|
timelineScroll.tryRestoringScroll();
|
||||||
autoPaginate();
|
autoPaginate();
|
||||||
}, [paginateInfo]);
|
}, [paginateInfo]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!roomTimeline.initialized) return;
|
||||||
|
timelineScroll.tryRestoringScroll();
|
||||||
|
}, [onLimitUpdate]);
|
||||||
|
|
||||||
const handleTimelineScroll = (event) => {
|
const handleTimelineScroll = (event) => {
|
||||||
const { target } = event;
|
const { target } = event;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
throttle._(() => timelineScroll?.calcScroll(), 200)(target);
|
throttle._(() => timelineScroll?.calcScroll(), 400)(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReadEvent = () => {
|
const getReadEvent = () => {
|
||||||
|
@ -425,11 +489,12 @@ function RoomViewContent({
|
||||||
let extraItemCount = 0;
|
let extraItemCount = 0;
|
||||||
focusEventIndex = null;
|
focusEventIndex = null;
|
||||||
|
|
||||||
if (roomTimeline.canPaginateBackward()) {
|
if (roomTimeline.canPaginateBackward() || limit.from > 0) {
|
||||||
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT));
|
||||||
extraItemCount += PLACEHOLDER_COUNT;
|
extraItemCount += PLACEHOLDER_COUNT;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < timeline.length; i += 1) {
|
for (let i = limit.from; i < limit.getEndIndex(); i += 1) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -461,7 +526,7 @@ function RoomViewContent({
|
||||||
|
|
||||||
tl.push(renderEvent(roomTimeline, mEvent, prevMEvent, isFocus));
|
tl.push(renderEvent(roomTimeline, mEvent, prevMEvent, isFocus));
|
||||||
}
|
}
|
||||||
if (roomTimeline.canPaginateForward()) {
|
if (roomTimeline.canPaginateForward() || limit.getEndIndex() < timeline.length) {
|
||||||
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,8 +91,10 @@ class RoomTimeline extends EventEmitter {
|
||||||
|
|
||||||
clearLocalTimelines() {
|
clearLocalTimelines() {
|
||||||
this.timeline = [];
|
this.timeline = [];
|
||||||
this.reactionTimeline.clear();
|
|
||||||
this.editedTimeline.clear();
|
// TODO: don't clear these timeline cause there data can be used in other timeline
|
||||||
|
// this.reactionTimeline.clear();
|
||||||
|
// this.editedTimeline.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
addToTimeline(mEvent) {
|
addToTimeline(mEvent) {
|
||||||
|
|
Loading…
Reference in a new issue