Add pagination in room timeline
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
beb32755a3
commit
57697142a2
12 changed files with 305 additions and 235 deletions
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
& button {
|
& button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir=rtl] & {
|
[dir=rtl] & {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import './RoomView.scss';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
import RoomTimeline from '../../../client/state/RoomTimeline';
|
import RoomTimeline from '../../../client/state/RoomTimeline';
|
||||||
|
import { Debounce, getScrollInfo } from '../../../util/common';
|
||||||
|
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
|
|
||||||
|
@ -14,98 +15,125 @@ import RoomViewFloating from './RoomViewFloating';
|
||||||
import RoomViewInput from './RoomViewInput';
|
import RoomViewInput from './RoomViewInput';
|
||||||
import RoomViewCmdBar from './RoomViewCmdBar';
|
import RoomViewCmdBar from './RoomViewCmdBar';
|
||||||
|
|
||||||
import { scrollToBottom, isAtBottom, autoScrollToBottom } from './common';
|
|
||||||
|
|
||||||
const viewEvent = new EventEmitter();
|
const viewEvent = new EventEmitter();
|
||||||
|
|
||||||
let lastScrollTop = 0;
|
|
||||||
let lastScrollHeight = 0;
|
|
||||||
let isReachedBottom = true;
|
|
||||||
let isReachedTop = false;
|
|
||||||
function RoomView({ roomId }) {
|
function RoomView({ roomId }) {
|
||||||
const [roomTimeline, updateRoomTimeline] = useState(null);
|
const [roomTimeline, updateRoomTimeline] = useState(null);
|
||||||
|
const [debounce] = useState(new Debounce());
|
||||||
const timelineSVRef = useRef(null);
|
const timelineSVRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomTimeline?.removeInternalListeners();
|
roomTimeline?.removeInternalListeners();
|
||||||
updateRoomTimeline(new RoomTimeline(roomId));
|
updateRoomTimeline(new RoomTimeline(roomId));
|
||||||
isReachedBottom = true;
|
|
||||||
isReachedTop = false;
|
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
const timelineScroll = {
|
const timelineScroll = {
|
||||||
reachBottom() {
|
reachBottom() {
|
||||||
scrollToBottom(timelineSVRef);
|
timelineScroll.isOngoing = true;
|
||||||
|
const target = timelineSVRef?.current;
|
||||||
|
if (!target) return;
|
||||||
|
const maxScrollTop = target.scrollHeight - target.offsetHeight;
|
||||||
|
target.scrollTop = maxScrollTop;
|
||||||
|
timelineScroll.position = 'BOTTOM';
|
||||||
|
timelineScroll.isScrollable = maxScrollTop > 0;
|
||||||
|
timelineScroll.isInTopHalf = false;
|
||||||
|
timelineScroll.lastTopMsg = null;
|
||||||
|
timelineScroll.lastBottomMsg = null;
|
||||||
},
|
},
|
||||||
autoReachBottom() {
|
autoReachBottom() {
|
||||||
autoScrollToBottom(timelineSVRef);
|
if (timelineScroll.position === 'BOTTOM') timelineScroll.reachBottom();
|
||||||
},
|
},
|
||||||
tryRestoringScroll() {
|
tryRestoringScroll() {
|
||||||
|
timelineScroll.isOngoing = true;
|
||||||
const sv = timelineSVRef.current;
|
const sv = timelineSVRef.current;
|
||||||
const { scrollHeight } = sv;
|
const {
|
||||||
|
lastTopMsg, lastBottomMsg,
|
||||||
|
diff, isInTopHalf, lastTop,
|
||||||
|
} = timelineScroll;
|
||||||
|
|
||||||
if (lastScrollHeight === scrollHeight) return;
|
if (lastTopMsg === null) {
|
||||||
|
sv.scrollTop = sv.scrollHeight;
|
||||||
if (lastScrollHeight < scrollHeight) {
|
return;
|
||||||
sv.scrollTop = lastScrollTop + (scrollHeight - lastScrollHeight);
|
|
||||||
} else {
|
|
||||||
timelineScroll.reachBottom();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ot = isInTopHalf ? lastTopMsg?.offsetTop : lastBottomMsg?.offsetTop;
|
||||||
|
if (!ot) sv.scrollTop = lastTop;
|
||||||
|
else sv.scrollTop = ot - diff;
|
||||||
},
|
},
|
||||||
enableSmoothScroll() {
|
position: 'BOTTOM',
|
||||||
timelineSVRef.current.style.scrollBehavior = 'smooth';
|
isScrollable: false,
|
||||||
},
|
isInTopHalf: false,
|
||||||
disableSmoothScroll() {
|
maxEvents: 50,
|
||||||
timelineSVRef.current.style.scrollBehavior = 'auto';
|
lastTop: 0,
|
||||||
},
|
lastHeight: 0,
|
||||||
isScrollable() {
|
lastViewHeight: 0,
|
||||||
const oHeight = timelineSVRef.current.offsetHeight;
|
lastTopMsg: null,
|
||||||
const sHeight = timelineSVRef.current.scrollHeight;
|
lastBottomMsg: null,
|
||||||
if (sHeight > oHeight) return true;
|
diff: 0,
|
||||||
return false;
|
isOngoing: false,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function onTimelineScroll(e) {
|
const calcScroll = (target) => {
|
||||||
const { scrollTop, scrollHeight, offsetHeight } = e.target;
|
if (timelineScroll.isOngoing) {
|
||||||
const scrollBottom = scrollTop + offsetHeight;
|
timelineScroll.isOngoing = false;
|
||||||
lastScrollTop = scrollTop;
|
|
||||||
lastScrollHeight = scrollHeight;
|
|
||||||
|
|
||||||
const PLACEHOLDER_HEIGHT = 96;
|
|
||||||
const PLACEHOLDER_COUNT = 3;
|
|
||||||
|
|
||||||
const topPagKeyPoint = PLACEHOLDER_COUNT * PLACEHOLDER_HEIGHT;
|
|
||||||
const bottomPagKeyPoint = scrollHeight - (offsetHeight / 2);
|
|
||||||
|
|
||||||
if (!isReachedBottom && isAtBottom(timelineSVRef)) {
|
|
||||||
isReachedBottom = true;
|
|
||||||
viewEvent.emit('toggle-reached-bottom', true);
|
|
||||||
}
|
|
||||||
if (isReachedBottom && !isAtBottom(timelineSVRef)) {
|
|
||||||
isReachedBottom = false;
|
|
||||||
viewEvent.emit('toggle-reached-bottom', false);
|
|
||||||
}
|
|
||||||
// TOP of timeline
|
|
||||||
if (scrollTop < topPagKeyPoint && isReachedTop === false) {
|
|
||||||
isReachedTop = true;
|
|
||||||
viewEvent.emit('reached-top');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isReachedTop = false;
|
const PLACEHOLDER_COUNT = 2;
|
||||||
|
const PLACEHOLDER_HEIGHT = 96 * PLACEHOLDER_COUNT;
|
||||||
|
const SMALLEST_MSG_HEIGHT = 32;
|
||||||
|
const scroll = getScrollInfo(target);
|
||||||
|
|
||||||
// BOTTOM of timeline
|
const isPaginateBack = scroll.top < PLACEHOLDER_HEIGHT;
|
||||||
if (scrollBottom > bottomPagKeyPoint) {
|
const isPaginateForward = scroll.bottom > (scroll.height - PLACEHOLDER_HEIGHT);
|
||||||
// TODO:
|
timelineScroll.isInTopHalf = scroll.top + (scroll.viewHeight / 2) < scroll.height / 2;
|
||||||
|
|
||||||
|
if (timelineScroll.lastViewHeight !== scroll.viewHeight) {
|
||||||
|
timelineScroll.maxEvents = Math.round(scroll.viewHeight / SMALLEST_MSG_HEIGHT) * 3;
|
||||||
|
timelineScroll.lastViewHeight = scroll.viewHeight;
|
||||||
}
|
}
|
||||||
}
|
timelineScroll.isScrollable = scroll.isScrollable;
|
||||||
|
timelineScroll.lastTop = scroll.top;
|
||||||
|
timelineScroll.lastHeight = scroll.height;
|
||||||
|
const tChildren = target.lastElementChild.lastElementChild.children;
|
||||||
|
const lCIndex = tChildren.length - 1;
|
||||||
|
|
||||||
|
timelineScroll.lastTopMsg = tChildren[0]?.className === 'ph-msg'
|
||||||
|
? tChildren[PLACEHOLDER_COUNT]
|
||||||
|
: tChildren[0];
|
||||||
|
timelineScroll.lastBottomMsg = tChildren[lCIndex]?.className === 'ph-msg'
|
||||||
|
? tChildren[lCIndex - PLACEHOLDER_COUNT]
|
||||||
|
: tChildren[lCIndex];
|
||||||
|
|
||||||
|
if (timelineScroll.isInTopHalf && timelineScroll.lastBottomMsg) {
|
||||||
|
timelineScroll.diff = timelineScroll.lastTopMsg.offsetTop - scroll.top;
|
||||||
|
} else {
|
||||||
|
timelineScroll.diff = timelineScroll.lastBottomMsg.offsetTop - scroll.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPaginateBack) {
|
||||||
|
timelineScroll.position = 'TOP';
|
||||||
|
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
||||||
|
} else if (isPaginateForward) {
|
||||||
|
timelineScroll.position = 'BOTTOM';
|
||||||
|
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
||||||
|
} else {
|
||||||
|
timelineScroll.position = 'BETWEEN';
|
||||||
|
viewEvent.emit('timeline-scroll', timelineScroll.position);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimelineScroll = (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (!target) return;
|
||||||
|
debounce._(calcScroll, 200)(target);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room-view">
|
<div className="room-view">
|
||||||
<RoomViewHeader roomId={roomId} />
|
<RoomViewHeader roomId={roomId} />
|
||||||
<div className="room-view__content-wrapper">
|
<div className="room-view__content-wrapper">
|
||||||
<div className="room-view__scrollable">
|
<div className="room-view__scrollable">
|
||||||
<ScrollView onScroll={onTimelineScroll} ref={timelineSVRef} autoHide>
|
<ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
|
||||||
{roomTimeline !== null && (
|
{roomTimeline !== null && (
|
||||||
<RoomViewContent
|
<RoomViewContent
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
@ -119,7 +147,6 @@ function RoomView({ roomId }) {
|
||||||
<RoomViewFloating
|
<RoomViewFloating
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
roomTimeline={roomTimeline}
|
roomTimeline={roomTimeline}
|
||||||
timelineScroll={timelineScroll}
|
|
||||||
viewEvent={viewEvent}
|
viewEvent={viewEvent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -146,7 +146,8 @@ function FollowingMembers({ roomId, roomTimeline, viewEvent }) {
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
const lastMEvent = roomTimeline.timeline[roomTimeline.timeline.length - 1];
|
const { timeline } = roomTimeline.room;
|
||||||
|
const lastMEvent = timeline[timeline.length - 1];
|
||||||
return followingMembers.length !== 0 && (
|
return followingMembers.length !== 0 && (
|
||||||
<TimelineChange
|
<TimelineChange
|
||||||
variant="follow"
|
variant="follow"
|
||||||
|
|
|
@ -43,13 +43,12 @@ import { parseReply, parseTimelineChange } from './common';
|
||||||
|
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
|
|
||||||
function genPlaceholders() {
|
function genPlaceholders(key) {
|
||||||
return (
|
return (
|
||||||
<>
|
<React.Fragment key={`placeholder-container${key}`}>
|
||||||
<PlaceholderMessage key="placeholder-1" />
|
<PlaceholderMessage key={`placeholder-1${key}`} />
|
||||||
<PlaceholderMessage key="placeholder-2" />
|
<PlaceholderMessage key={`placeholder-2${key}`} />
|
||||||
<PlaceholderMessage key="placeholder-3" />
|
</React.Fragment>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,96 +181,149 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let wasAtBottom = true;
|
const scroll = {
|
||||||
|
from: 0,
|
||||||
|
limit: 0,
|
||||||
|
getEndIndex() {
|
||||||
|
return (this.from + this.limit);
|
||||||
|
},
|
||||||
|
isNewEvent: false,
|
||||||
|
};
|
||||||
function RoomViewContent({
|
function RoomViewContent({
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
roomId, roomTimeline, timelineScroll, viewEvent,
|
||||||
}) {
|
}) {
|
||||||
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
|
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
|
||||||
const [onStateUpdate, updateState] = useState(null);
|
const [onStateUpdate, updateState] = useState(null);
|
||||||
const [onPagination, setOnPagination] = useState(null);
|
|
||||||
const [editEvent, setEditEvent] = useState(null);
|
const [editEvent, setEditEvent] = useState(null);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const noti = initMatrix.notifications;
|
const noti = initMatrix.notifications;
|
||||||
|
if (scroll.limit === 0) {
|
||||||
|
const from = roomTimeline.timeline.size - timelineScroll.maxEvents;
|
||||||
|
scroll.from = (from < 0) ? 0 : from;
|
||||||
|
scroll.limit = timelineScroll.maxEvents;
|
||||||
|
}
|
||||||
|
|
||||||
function autoLoadTimeline() {
|
function autoLoadTimeline() {
|
||||||
if (timelineScroll.isScrollable() === true) return;
|
if (timelineScroll.isScrollable === true) return;
|
||||||
roomTimeline.paginateBack();
|
roomTimeline.paginateBack();
|
||||||
}
|
}
|
||||||
function trySendingReadReceipt() {
|
function trySendingReadReceipt() {
|
||||||
const { room, timeline } = roomTimeline;
|
const { timeline } = roomTimeline.room;
|
||||||
if (
|
if (
|
||||||
(noti.doesRoomHaveUnread(room) || noti.hasNoti(roomId))
|
(noti.doesRoomHaveUnread(roomTimeline.room) || noti.hasNoti(roomId))
|
||||||
&& timeline.length !== 0) {
|
&& timeline.length !== 0) {
|
||||||
mx.sendReadReceipt(timeline[timeline.length - 1]);
|
mx.sendReadReceipt(timeline[timeline.length - 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReachedTop() {
|
const getNewFrom = (position) => {
|
||||||
if (roomTimeline.isOngoingPagination || isReachedTimelineEnd) return;
|
let newFrom = scroll.from;
|
||||||
roomTimeline.paginateBack();
|
const tSize = roomTimeline.timeline.size;
|
||||||
}
|
const doPaginate = tSize > timelineScroll.maxEvents;
|
||||||
function toggleOnReachedBottom(isBottom) {
|
if (!doPaginate || scroll.from < 0) newFrom = 0;
|
||||||
wasAtBottom = isBottom;
|
const newEventCount = Math.round(timelineScroll.maxEvents / 2);
|
||||||
if (!isBottom) return;
|
scroll.limit = timelineScroll.maxEvents;
|
||||||
trySendingReadReceipt();
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePAG = (canPagMore) => {
|
if (position === 'TOP' && doPaginate) newFrom -= newEventCount;
|
||||||
if (!canPagMore) {
|
if (position === 'BOTTOM' && doPaginate) newFrom += newEventCount;
|
||||||
setIsReachedTimelineEnd(true);
|
|
||||||
} else {
|
if (newFrom >= tSize || scroll.getEndIndex() >= tSize) newFrom = tSize - scroll.limit - 1;
|
||||||
setOnPagination({});
|
if (newFrom < 0) newFrom = 0;
|
||||||
autoLoadTimeline();
|
return newFrom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimelineScroll = (position) => {
|
||||||
|
const tSize = roomTimeline.timeline.size;
|
||||||
|
if (position === 'BETWEEN') return;
|
||||||
|
if (position === 'BOTTOM' && scroll.getEndIndex() + 1 === tSize) return;
|
||||||
|
|
||||||
|
if (scroll.from === 0 && position === 'TOP') {
|
||||||
|
// Fetch back history.
|
||||||
|
if (roomTimeline.isOngoingPagination || isReachedTimelineEnd) return;
|
||||||
|
roomTimeline.paginateBack();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scroll.from = getNewFrom(position);
|
||||||
|
updateState({});
|
||||||
|
|
||||||
|
if (scroll.getEndIndex() + 1 >= tSize) {
|
||||||
|
trySendingReadReceipt();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePAG = (canPagMore, loaded) => {
|
||||||
|
if (canPagMore) {
|
||||||
|
scroll.from += loaded;
|
||||||
|
scroll.from = getNewFrom(timelineScroll.position);
|
||||||
|
if (roomTimeline.ongoingDecryptionCount === 0) updateState({});
|
||||||
|
} else setIsReachedTimelineEnd(true);
|
||||||
};
|
};
|
||||||
// force update RoomTimeline on cons.events.roomTimeline.EVENT
|
// force update RoomTimeline on cons.events.roomTimeline.EVENT
|
||||||
const updateRT = () => {
|
const updateRT = () => {
|
||||||
if (wasAtBottom) {
|
if (timelineScroll.position === 'BOTTOM') {
|
||||||
trySendingReadReceipt();
|
trySendingReadReceipt();
|
||||||
|
scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
|
||||||
|
if (scroll.from < 0) scroll.from = 0;
|
||||||
|
scroll.isNewEvent = true;
|
||||||
}
|
}
|
||||||
updateState({});
|
updateState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScrollToLive = () => {
|
||||||
|
scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
|
||||||
|
if (scroll.from < 0) scroll.from = 0;
|
||||||
|
scroll.isNewEvent = true;
|
||||||
|
updateState({});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsReachedTimelineEnd(false);
|
trySendingReadReceipt();
|
||||||
wasAtBottom = true;
|
return () => {
|
||||||
|
setIsReachedTimelineEnd(false);
|
||||||
|
scroll.limit = 0;
|
||||||
|
};
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
useEffect(() => trySendingReadReceipt(), [roomTimeline]);
|
|
||||||
|
|
||||||
// init room setup completed.
|
// init room setup completed.
|
||||||
// listen for future. setup stateUpdate listener.
|
// listen for future. setup stateUpdate listener.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.EVENT, updateRT);
|
roomTimeline.on(cons.events.roomTimeline.EVENT, updateRT);
|
||||||
roomTimeline.on(cons.events.roomTimeline.PAGINATED, updatePAG);
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, updatePAG);
|
||||||
viewEvent.on('reached-top', onReachedTop);
|
viewEvent.on('timeline-scroll', handleTimelineScroll);
|
||||||
viewEvent.on('toggle-reached-bottom', toggleOnReachedBottom);
|
viewEvent.on('scroll-to-live', handleScrollToLive);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, updateRT);
|
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, updateRT);
|
||||||
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, updatePAG);
|
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, updatePAG);
|
||||||
viewEvent.removeListener('reached-top', onReachedTop);
|
viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
|
||||||
viewEvent.removeListener('toggle-reached-bottom', toggleOnReachedBottom);
|
viewEvent.removeListener('scroll-to-live', handleScrollToLive);
|
||||||
};
|
};
|
||||||
}, [roomTimeline, isReachedTimelineEnd, onPagination]);
|
}, [roomTimeline, isReachedTimelineEnd]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
timelineScroll.reachBottom();
|
timelineScroll.reachBottom();
|
||||||
autoLoadTimeline();
|
autoLoadTimeline();
|
||||||
|
trySendingReadReceipt();
|
||||||
}, [roomTimeline]);
|
}, [roomTimeline]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (onPagination === null) return;
|
if (onStateUpdate === null || scroll.isNewEvent) {
|
||||||
timelineScroll.tryRestoringScroll();
|
scroll.isNewEvent = false;
|
||||||
}, [onPagination]);
|
timelineScroll.reachBottom();
|
||||||
|
return;
|
||||||
useEffect(() => {
|
}
|
||||||
if (onStateUpdate === null) return;
|
if (timelineScroll.isScrollable) {
|
||||||
if (wasAtBottom) timelineScroll.reachBottom();
|
timelineScroll.tryRestoringScroll();
|
||||||
|
} else {
|
||||||
|
timelineScroll.reachBottom();
|
||||||
|
autoLoadTimeline();
|
||||||
|
}
|
||||||
}, [onStateUpdate]);
|
}, [onStateUpdate]);
|
||||||
|
|
||||||
let prevMEvent = null;
|
let prevMEvent = null;
|
||||||
function genMessage(mEvent) {
|
function genMessage(mEvent) {
|
||||||
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
|
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId())?.powerLevel;
|
||||||
const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
||||||
|
|
||||||
const isContentOnly = (
|
const isContentOnly = (
|
||||||
|
@ -521,18 +573,12 @@ function RoomViewContent({
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessage(mEvent) {
|
function renderMessage(mEvent) {
|
||||||
if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
|
if (!cons.supportEventTypes.includes(mEvent.getType())) return false;
|
||||||
if (
|
|
||||||
mEvent.getType() !== 'm.room.message'
|
|
||||||
&& mEvent.getType() !== 'm.room.encrypted'
|
|
||||||
&& mEvent.getType() !== 'm.room.member'
|
|
||||||
&& mEvent.getType() !== 'm.sticker'
|
|
||||||
) return false;
|
|
||||||
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
||||||
|
|
||||||
// ignore if message is deleted
|
|
||||||
if (mEvent.isRedacted()) return false;
|
if (mEvent.isRedacted()) return false;
|
||||||
|
|
||||||
|
if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
|
||||||
|
|
||||||
let divider = null;
|
let divider = null;
|
||||||
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
|
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
|
||||||
divider = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
divider = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
||||||
|
@ -551,7 +597,7 @@ function RoomViewContent({
|
||||||
|
|
||||||
prevMEvent = mEvent;
|
prevMEvent = mEvent;
|
||||||
const timelineChange = parseTimelineChange(mEvent);
|
const timelineChange = parseTimelineChange(mEvent);
|
||||||
if (timelineChange === null) return null;
|
if (timelineChange === null) return false;
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
<React.Fragment key={`box-${mEvent.getId()}`}>
|
||||||
{divider}
|
{divider}
|
||||||
|
@ -565,12 +611,33 @@ function RoomViewContent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTimeline = () => {
|
||||||
|
const { timeline } = roomTimeline;
|
||||||
|
const tl = [];
|
||||||
|
if (timeline.size === 0) return tl;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const [, mEvent] of timeline.entries()) {
|
||||||
|
if (i >= scroll.from) {
|
||||||
|
if (i === scroll.from) {
|
||||||
|
if (mEvent.getType() !== 'm.room.create' && !isReachedTimelineEnd) tl.push(genPlaceholders(1));
|
||||||
|
if (mEvent.getType() !== 'm.room.create' && isReachedTimelineEnd) tl.push(genRoomIntro(undefined, roomTimeline));
|
||||||
|
}
|
||||||
|
tl.push(renderMessage(mEvent));
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
if (i > scroll.getEndIndex()) break;
|
||||||
|
}
|
||||||
|
if (i < timeline.size) tl.push(genPlaceholders(2));
|
||||||
|
|
||||||
|
return tl;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room-view__content">
|
<div className="room-view__content">
|
||||||
<div className="timeline__wrapper">
|
<div className="timeline__wrapper">
|
||||||
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
|
{ renderTimeline() }
|
||||||
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genRoomIntro(undefined, roomTimeline)}
|
|
||||||
{ roomTimeline.timeline.map(renderMessage) }
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
|
||||||
import { getUsersActionJsx } from './common';
|
import { getUsersActionJsx } from './common';
|
||||||
|
|
||||||
function RoomViewFloating({
|
function RoomViewFloating({
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
roomId, roomTimeline, viewEvent,
|
||||||
}) {
|
}) {
|
||||||
const [reachedBottom, setReachedBottom] = useState(true);
|
const [reachedBottom, setReachedBottom] = useState(true);
|
||||||
const [typingMembers, setTypingMembers] = useState(new Set());
|
const [typingMembers, setTypingMembers] = useState(new Set());
|
||||||
|
@ -36,12 +36,15 @@ function RoomViewFloating({
|
||||||
function updateTyping(members) {
|
function updateTyping(members) {
|
||||||
setTypingMembers(members);
|
setTypingMembers(members);
|
||||||
}
|
}
|
||||||
|
const handleTimelineScroll = (position) => {
|
||||||
|
setReachedBottom(position === 'BOTTOM');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReachedBottom(true);
|
setReachedBottom(true);
|
||||||
setTypingMembers(new Set());
|
setTypingMembers(new Set());
|
||||||
viewEvent.on('toggle-reached-bottom', setReachedBottom);
|
viewEvent.on('timeline-scroll', handleTimelineScroll);
|
||||||
return () => viewEvent.removeListener('toggle-reached-bottom', setReachedBottom);
|
return () => viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -60,9 +63,8 @@ function RoomViewFloating({
|
||||||
<div className={`room-view__STB${reachedBottom ? '' : ' room-view__STB--open'}`}>
|
<div className={`room-view__STB${reachedBottom ? '' : ' room-view__STB--open'}`}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
timelineScroll.enableSmoothScroll();
|
viewEvent.emit('scroll-to-live');
|
||||||
timelineScroll.reachBottom();
|
setReachedBottom(true);
|
||||||
timelineScroll.disableSmoothScroll();
|
|
||||||
}}
|
}}
|
||||||
src={ChevronBottomIC}
|
src={ChevronBottomIC}
|
||||||
tooltip="Scroll to Bottom"
|
tooltip="Scroll to Bottom"
|
||||||
|
@ -74,9 +76,6 @@ function RoomViewFloating({
|
||||||
RoomViewFloating.propTypes = {
|
RoomViewFloating.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
timelineScroll: PropTypes.shape({
|
|
||||||
reachBottom: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
viewEvent: PropTypes.shape({}).isRequired,
|
viewEvent: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ function RoomViewInput({
|
||||||
focusInput();
|
focusInput();
|
||||||
|
|
||||||
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
textAreaRef.current.value = roomsInput.getMessage(roomId);
|
||||||
timelineScroll.reachBottom();
|
viewEvent.emit('scroll-to-live');
|
||||||
viewEvent.emit('message_sent');
|
viewEvent.emit('message_sent');
|
||||||
textAreaRef.current.style.height = 'unset';
|
textAreaRef.current.style.height = 'unset';
|
||||||
if (replyTo !== null) setReplyTo(null);
|
if (replyTo !== null) setReplyTo(null);
|
||||||
|
@ -433,13 +433,7 @@ function RoomViewInput({
|
||||||
RoomViewInput.propTypes = {
|
RoomViewInput.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
timelineScroll: PropTypes.shape({
|
timelineScroll: PropTypes.shape({}).isRequired,
|
||||||
reachBottom: PropTypes.func,
|
|
||||||
autoReachBottom: PropTypes.func,
|
|
||||||
tryRestoringScroll: PropTypes.func,
|
|
||||||
enableSmoothScroll: PropTypes.func,
|
|
||||||
disableSmoothScroll: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
viewEvent: PropTypes.shape({}).isRequired,
|
viewEvent: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -234,39 +234,9 @@ function parseTimelineChange(mEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom(ref) {
|
|
||||||
const maxScrollTop = ref.current.scrollHeight - ref.current.offsetHeight;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
ref.current.scrollTop = maxScrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAtBottom(ref) {
|
|
||||||
const { scrollHeight, scrollTop, offsetHeight } = ref.current;
|
|
||||||
const scrollUptoBottom = scrollTop + offsetHeight;
|
|
||||||
|
|
||||||
// scroll view have to div inside div which contains messages
|
|
||||||
const lastMessage = ref.current.lastElementChild.lastElementChild.lastElementChild;
|
|
||||||
const lastChildHeight = lastMessage.offsetHeight;
|
|
||||||
|
|
||||||
// auto scroll to bottom even if user has EXTRA_SPACE left to scroll
|
|
||||||
const EXTRA_SPACE = 48;
|
|
||||||
|
|
||||||
if (scrollHeight - scrollUptoBottom <= lastChildHeight + EXTRA_SPACE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoScrollToBottom(ref) {
|
|
||||||
if (isAtBottom(ref)) scrollToBottom(ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getTimelineJSXMessages,
|
getTimelineJSXMessages,
|
||||||
getUsersActionJsx,
|
getUsersActionJsx,
|
||||||
parseReply,
|
parseReply,
|
||||||
parseTimelineChange,
|
parseTimelineChange,
|
||||||
scrollToBottom,
|
|
||||||
isAtBottom,
|
|
||||||
autoScrollToBottom,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import * as sdk from 'matrix-js-sdk';
|
import * as sdk from 'matrix-js-sdk';
|
||||||
|
import { logger } from 'matrix-js-sdk/lib/logger';
|
||||||
|
|
||||||
import { secret } from './state/auth';
|
import { secret } from './state/auth';
|
||||||
import RoomList from './state/RoomList';
|
import RoomList from './state/RoomList';
|
||||||
|
@ -8,6 +9,8 @@ import Notifications from './state/Notifications';
|
||||||
|
|
||||||
global.Olm = require('@matrix-org/olm');
|
global.Olm = require('@matrix-org/olm');
|
||||||
|
|
||||||
|
logger.disableAll();
|
||||||
|
|
||||||
class InitMatrix extends EventEmitter {
|
class InitMatrix extends EventEmitter {
|
||||||
async init() {
|
async init() {
|
||||||
await this.startClient();
|
await this.startClient();
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Notifications extends EventEmitter {
|
||||||
constructor(roomList) {
|
constructor(roomList) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
|
||||||
this.matrixClient = roomList.matrixClient;
|
this.matrixClient = roomList.matrixClient;
|
||||||
this.roomList = roomList;
|
this.roomList = roomList;
|
||||||
|
|
||||||
|
@ -33,7 +34,6 @@ class Notifications extends EventEmitter {
|
||||||
doesRoomHaveUnread(room) {
|
doesRoomHaveUnread(room) {
|
||||||
const userId = this.matrixClient.getUserId();
|
const userId = this.matrixClient.getUserId();
|
||||||
const readUpToId = room.getEventReadUpTo(userId);
|
const readUpToId = room.getEventReadUpTo(userId);
|
||||||
const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
|
|
||||||
|
|
||||||
if (room.timeline.length
|
if (room.timeline.length
|
||||||
&& room.timeline[room.timeline.length - 1].sender
|
&& room.timeline[room.timeline.length - 1].sender
|
||||||
|
@ -47,7 +47,7 @@ class Notifications extends EventEmitter {
|
||||||
|
|
||||||
if (event.getId() === readUpToId) return false;
|
if (event.getId() === readUpToId) return false;
|
||||||
|
|
||||||
if (supportEvents.includes(event.getType())) {
|
if (this.supportEvents.includes(event.getType())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,8 +149,7 @@ class Notifications extends EventEmitter {
|
||||||
|
|
||||||
_listenEvents() {
|
_listenEvents() {
|
||||||
this.matrixClient.on('Room.timeline', (mEvent, room) => {
|
this.matrixClient.on('Room.timeline', (mEvent, room) => {
|
||||||
const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
|
if (!this.supportEvents.includes(mEvent.getType())) return;
|
||||||
if (!supportEvents.includes(mEvent.getType())) return;
|
|
||||||
|
|
||||||
const lastTimelineEvent = room.timeline[room.timeline.length - 1];
|
const lastTimelineEvent = room.timeline[room.timeline.length - 1];
|
||||||
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
|
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
|
||||||
|
|
|
@ -2,15 +2,39 @@ import EventEmitter from 'events';
|
||||||
import initMatrix from '../initMatrix';
|
import initMatrix from '../initMatrix';
|
||||||
import cons from './cons';
|
import cons from './cons';
|
||||||
|
|
||||||
|
function isEdited(mEvent) {
|
||||||
|
return mEvent.getRelation()?.rel_type === 'm.replace';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReaction(mEvent) {
|
||||||
|
return mEvent.getType() === 'm.reaction';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelateToId(mEvent) {
|
||||||
|
const relation = mEvent.getRelation();
|
||||||
|
return relation && relation.event_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addToMap(myMap, mEvent) {
|
||||||
|
const relateToId = getRelateToId(mEvent);
|
||||||
|
if (relateToId === null) return null;
|
||||||
|
|
||||||
|
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
|
||||||
|
myMap.get(relateToId).push(mEvent);
|
||||||
|
return mEvent;
|
||||||
|
}
|
||||||
|
|
||||||
class RoomTimeline extends EventEmitter {
|
class RoomTimeline extends EventEmitter {
|
||||||
constructor(roomId) {
|
constructor(roomId) {
|
||||||
super();
|
super();
|
||||||
this.matrixClient = initMatrix.matrixClient;
|
this.matrixClient = initMatrix.matrixClient;
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.room = this.matrixClient.getRoom(roomId);
|
this.room = this.matrixClient.getRoom(roomId);
|
||||||
this.timeline = this.room.timeline;
|
|
||||||
this.editedTimeline = this.getEditedTimeline();
|
this.timeline = new Map();
|
||||||
this.reactionTimeline = this.getReactionTimeline();
|
this.editedTimeline = new Map();
|
||||||
|
this.reactionTimeline = new Map();
|
||||||
|
|
||||||
this.isOngoingPagination = false;
|
this.isOngoingPagination = false;
|
||||||
this.ongoingDecryptionCount = 0;
|
this.ongoingDecryptionCount = 0;
|
||||||
this.typingMembers = new Set();
|
this.typingMembers = new Set();
|
||||||
|
@ -23,31 +47,30 @@ class RoomTimeline extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeline = this.room.timeline;
|
|
||||||
if (this.isEdited(event)) {
|
|
||||||
this.addToMap(this.editedTimeline, event);
|
|
||||||
}
|
|
||||||
if (this.isReaction(event)) {
|
|
||||||
this.addToMap(this.reactionTimeline, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ongoingDecryptionCount !== 0) return;
|
if (this.ongoingDecryptionCount !== 0) return;
|
||||||
if (this.isOngoingPagination) return;
|
if (this.isOngoingPagination) return;
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
|
||||||
};
|
|
||||||
|
|
||||||
this._listenRedaction = (event, room) => {
|
this.addToTimeline(event);
|
||||||
if (room.roomId !== this.roomId) return;
|
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
this.emit(cons.events.roomTimeline.EVENT);
|
||||||
};
|
};
|
||||||
|
|
||||||
this._listenDecryptEvent = (event) => {
|
this._listenDecryptEvent = (event) => {
|
||||||
if (event.getRoomId() !== this.roomId) return;
|
if (event.getRoomId() !== this.roomId) return;
|
||||||
|
|
||||||
if (this.ongoingDecryptionCount > 0) this.ongoingDecryptionCount -= 1;
|
if (this.ongoingDecryptionCount > 0) {
|
||||||
this.timeline = this.room.timeline;
|
this.ongoingDecryptionCount -= 1;
|
||||||
|
}
|
||||||
|
if (this.ongoingDecryptionCount > 0) return;
|
||||||
|
|
||||||
if (this.ongoingDecryptionCount !== 0) return;
|
if (this.isOngoingPagination) return;
|
||||||
|
this.emit(cons.events.roomTimeline.EVENT);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._listenRedaction = (event, room) => {
|
||||||
|
if (room.roomId !== this.roomId) return;
|
||||||
|
this.timeline.delete(event.getId());
|
||||||
|
this.editedTimeline.delete(event.getId());
|
||||||
|
this.reactionTimeline.delete(event.getId());
|
||||||
this.emit(cons.events.roomTimeline.EVENT);
|
this.emit(cons.events.roomTimeline.EVENT);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,7 +86,7 @@ class RoomTimeline extends EventEmitter {
|
||||||
if (room.roomId !== this.roomId) return;
|
if (room.roomId !== this.roomId) return;
|
||||||
const receiptContent = event.getContent();
|
const receiptContent = event.getContent();
|
||||||
if (this.timeline.length === 0) return;
|
if (this.timeline.length === 0) return;
|
||||||
const tmlLastEvent = this.timeline[this.timeline.length - 1];
|
const tmlLastEvent = room.timeline[room.timeline.length - 1];
|
||||||
const lastEventId = tmlLastEvent.getId();
|
const lastEventId = tmlLastEvent.getId();
|
||||||
const lastEventRecipt = receiptContent[lastEventId];
|
const lastEventRecipt = receiptContent[lastEventId];
|
||||||
if (typeof lastEventRecipt === 'undefined') return;
|
if (typeof lastEventRecipt === 'undefined') return;
|
||||||
|
@ -82,78 +105,53 @@ class RoomTimeline extends EventEmitter {
|
||||||
window.selectedRoom = this;
|
window.selectedRoom = this;
|
||||||
|
|
||||||
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
|
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
|
||||||
|
this._populateTimelines();
|
||||||
}
|
}
|
||||||
|
|
||||||
isEncryptedRoom() {
|
isEncryptedRoom() {
|
||||||
return this.matrixClient.isRoomEncrypted(this.roomId);
|
return this.matrixClient.isRoomEncrypted(this.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
addToTimeline(mEvent) {
|
||||||
isEdited(mEvent) {
|
if (isReaction(mEvent)) {
|
||||||
return mEvent.getRelation()?.rel_type === 'm.replace';
|
addToMap(this.reactionTimeline, mEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cons.supportEventTypes.includes(mEvent.getType())) return;
|
||||||
|
if (isEdited(mEvent)) {
|
||||||
|
addToMap(this.editedTimeline, mEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timeline.set(mEvent.getId(), mEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
_populateTimelines() {
|
||||||
getRelateToId(mEvent) {
|
this.timeline.clear();
|
||||||
const relation = mEvent.getRelation();
|
this.reactionTimeline.clear();
|
||||||
return relation && relation.event_id;
|
this.editedTimeline.clear();
|
||||||
}
|
this.room.timeline.forEach((mEvent) => this.addToTimeline(mEvent));
|
||||||
|
|
||||||
addToMap(myMap, mEvent) {
|
|
||||||
const relateToId = this.getRelateToId(mEvent);
|
|
||||||
if (relateToId === null) return null;
|
|
||||||
|
|
||||||
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
|
|
||||||
myMap.get(relateToId).push(mEvent);
|
|
||||||
return mEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEditedTimeline() {
|
|
||||||
const mReplace = new Map();
|
|
||||||
this.timeline.forEach((mEvent) => {
|
|
||||||
if (this.isEdited(mEvent)) {
|
|
||||||
this.addToMap(mReplace, mEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return mReplace;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
isReaction(mEvent) {
|
|
||||||
return mEvent.getType() === 'm.reaction';
|
|
||||||
}
|
|
||||||
|
|
||||||
getReactionTimeline() {
|
|
||||||
const mReaction = new Map();
|
|
||||||
this.timeline.forEach((mEvent) => {
|
|
||||||
if (this.isReaction(mEvent)) {
|
|
||||||
this.addToMap(mReaction, mEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return mReaction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paginateBack() {
|
paginateBack() {
|
||||||
if (this.isOngoingPagination) return;
|
if (this.isOngoingPagination) return;
|
||||||
this.isOngoingPagination = true;
|
this.isOngoingPagination = true;
|
||||||
|
|
||||||
|
const oldSize = this.timeline.size;
|
||||||
const MSG_LIMIT = 30;
|
const MSG_LIMIT = 30;
|
||||||
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
|
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
|
||||||
if (room.oldState.paginationToken === null) {
|
if (room.oldState.paginationToken === null) {
|
||||||
// We have reached start of the timeline
|
// We have reached start of the timeline
|
||||||
this.isOngoingPagination = false;
|
this.isOngoingPagination = false;
|
||||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||||
this.emit(cons.events.roomTimeline.PAGINATED, false);
|
this.emit(cons.events.roomTimeline.PAGINATED, false, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.editedTimeline = this.getEditedTimeline();
|
this._populateTimelines();
|
||||||
this.reactionTimeline = this.getReactionTimeline();
|
const loaded = this.timeline.size - oldSize;
|
||||||
|
|
||||||
this.isOngoingPagination = false;
|
|
||||||
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
|
||||||
this.emit(cons.events.roomTimeline.PAGINATED, true);
|
this.isOngoingPagination = false;
|
||||||
|
this.emit(cons.events.roomTimeline.PAGINATED, true, loaded);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ const cons = {
|
||||||
HOME: 'home',
|
HOME: 'home',
|
||||||
DIRECTS: 'dm',
|
DIRECTS: 'dm',
|
||||||
},
|
},
|
||||||
|
supportEventTypes: ['m.room.create', 'm.room.message', 'm.room.encrypted', 'm.room.member', 'm.sticker'],
|
||||||
notifs: {
|
notifs: {
|
||||||
DEFAULT: 'default',
|
DEFAULT: 'default',
|
||||||
ALL_MESSAGES: 'all_messages',
|
ALL_MESSAGES: 'all_messages',
|
||||||
|
|
|
@ -84,3 +84,13 @@ export function getUrlPrams(paramName) {
|
||||||
const urlParams = new URLSearchParams(queryString);
|
const urlParams = new URLSearchParams(queryString);
|
||||||
return urlParams.get(paramName);
|
return urlParams.get(paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getScrollInfo(target) {
|
||||||
|
const scroll = {};
|
||||||
|
scroll.top = Math.round(target.scrollTop);
|
||||||
|
scroll.height = Math.round(target.scrollHeight);
|
||||||
|
scroll.viewHeight = Math.round(target.offsetHeight);
|
||||||
|
scroll.bottom = Math.round(scroll.top + scroll.viewHeight);
|
||||||
|
scroll.isScrollable = scroll.height > scroll.viewHeight;
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue