& button {
cursor: pointer;
+ display: flex;
}
[dir=rtl] & {
import EventEmitter from 'events';
import RoomTimeline from '../../../client/state/RoomTimeline';
+import { Debounce, getScrollInfo } from '../../../util/common';
import ScrollView from '../../atoms/scroll/ScrollView';
import RoomViewInput from './RoomViewInput';
import RoomViewCmdBar from './RoomViewCmdBar';
-import { scrollToBottom, isAtBottom, autoScrollToBottom } from './common';
-
const viewEvent = new EventEmitter();
-let lastScrollTop = 0;
-let lastScrollHeight = 0;
-let isReachedBottom = true;
-let isReachedTop = false;
function RoomView({ roomId }) {
const [roomTimeline, updateRoomTimeline] = useState(null);
+ const [debounce] = useState(new Debounce());
const timelineSVRef = useRef(null);
useEffect(() => {
roomTimeline?.removeInternalListeners();
updateRoomTimeline(new RoomTimeline(roomId));
- isReachedBottom = true;
- isReachedTop = false;
}, [roomId]);
const timelineScroll = {
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() {
- autoScrollToBottom(timelineSVRef);
+ if (timelineScroll.position === 'BOTTOM') timelineScroll.reachBottom();
},
tryRestoringScroll() {
+ timelineScroll.isOngoing = true;
const sv = timelineSVRef.current;
- const { scrollHeight } = sv;
-
- if (lastScrollHeight === scrollHeight) return;
-
- if (lastScrollHeight < scrollHeight) {
- sv.scrollTop = lastScrollTop + (scrollHeight - lastScrollHeight);
- } else {
- timelineScroll.reachBottom();
+ const {
+ lastTopMsg, lastBottomMsg,
+ diff, isInTopHalf, lastTop,
+ } = timelineScroll;
+
+ if (lastTopMsg === null) {
+ sv.scrollTop = sv.scrollHeight;
+ return;
}
+
+ const ot = isInTopHalf ? lastTopMsg?.offsetTop : lastBottomMsg?.offsetTop;
+ if (!ot) sv.scrollTop = lastTop;
+ else sv.scrollTop = ot - diff;
},
- enableSmoothScroll() {
- timelineSVRef.current.style.scrollBehavior = 'smooth';
- },
- disableSmoothScroll() {
- timelineSVRef.current.style.scrollBehavior = 'auto';
- },
- isScrollable() {
- const oHeight = timelineSVRef.current.offsetHeight;
- const sHeight = timelineSVRef.current.scrollHeight;
- if (sHeight > oHeight) return true;
- return false;
- },
+ position: 'BOTTOM',
+ isScrollable: false,
+ isInTopHalf: false,
+ maxEvents: 50,
+ lastTop: 0,
+ lastHeight: 0,
+ lastViewHeight: 0,
+ lastTopMsg: null,
+ lastBottomMsg: null,
+ diff: 0,
+ isOngoing: false,
};
- function onTimelineScroll(e) {
- const { scrollTop, scrollHeight, offsetHeight } = e.target;
- const scrollBottom = scrollTop + offsetHeight;
- 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);
+ const calcScroll = (target) => {
+ if (timelineScroll.isOngoing) {
+ timelineScroll.isOngoing = false;
+ return;
}
- if (isReachedBottom && !isAtBottom(timelineSVRef)) {
- isReachedBottom = false;
- viewEvent.emit('toggle-reached-bottom', false);
+ const PLACEHOLDER_COUNT = 2;
+ const PLACEHOLDER_HEIGHT = 96 * PLACEHOLDER_COUNT;
+ const SMALLEST_MSG_HEIGHT = 32;
+ const scroll = getScrollInfo(target);
+
+ const isPaginateBack = scroll.top < PLACEHOLDER_HEIGHT;
+ const isPaginateForward = scroll.bottom > (scroll.height - PLACEHOLDER_HEIGHT);
+ 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;
}
- // TOP of timeline
- if (scrollTop < topPagKeyPoint && isReachedTop === false) {
- isReachedTop = true;
- viewEvent.emit('reached-top');
- return;
+ 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;
}
- isReachedTop = false;
- // BOTTOM of timeline
- if (scrollBottom > bottomPagKeyPoint) {
- // TODO:
+ 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 (
<div className="room-view">
<RoomViewHeader roomId={roomId} />
<div className="room-view__content-wrapper">
<div className="room-view__scrollable">
- <ScrollView onScroll={onTimelineScroll} ref={timelineSVRef} autoHide>
+ <ScrollView onScroll={handleTimelineScroll} ref={timelineSVRef} autoHide>
{roomTimeline !== null && (
<RoomViewContent
roomId={roomId}
<RoomViewFloating
roomId={roomId}
roomTimeline={roomTimeline}
- timelineScroll={timelineScroll}
viewEvent={viewEvent}
/>
)}
};
}, [roomTimeline]);
- const lastMEvent = roomTimeline.timeline[roomTimeline.timeline.length - 1];
+ const { timeline } = roomTimeline.room;
+ const lastMEvent = timeline[timeline.length - 1];
return followingMembers.length !== 0 && (
<TimelineChange
variant="follow"
const MAX_MSG_DIFF_MINUTES = 5;
-function genPlaceholders() {
+function genPlaceholders(key) {
return (
- <>
- <PlaceholderMessage key="placeholder-1" />
- <PlaceholderMessage key="placeholder-2" />
- <PlaceholderMessage key="placeholder-3" />
- </>
+ <React.Fragment key={`placeholder-container${key}`}>
+ <PlaceholderMessage key={`placeholder-1${key}`} />
+ <PlaceholderMessage key={`placeholder-2${key}`} />
+ </React.Fragment>
);
}
});
}
-let wasAtBottom = true;
+const scroll = {
+ from: 0,
+ limit: 0,
+ getEndIndex() {
+ return (this.from + this.limit);
+ },
+ isNewEvent: false,
+};
function RoomViewContent({
roomId, roomTimeline, timelineScroll, viewEvent,
}) {
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
const [onStateUpdate, updateState] = useState(null);
- const [onPagination, setOnPagination] = useState(null);
const [editEvent, setEditEvent] = useState(null);
const mx = initMatrix.matrixClient;
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() {
- if (timelineScroll.isScrollable() === true) return;
+ if (timelineScroll.isScrollable === true) return;
roomTimeline.paginateBack();
}
function trySendingReadReceipt() {
- const { room, timeline } = roomTimeline;
+ const { timeline } = roomTimeline.room;
if (
- (noti.doesRoomHaveUnread(room) || noti.hasNoti(roomId))
+ (noti.doesRoomHaveUnread(roomTimeline.room) || noti.hasNoti(roomId))
&& timeline.length !== 0) {
mx.sendReadReceipt(timeline[timeline.length - 1]);
}
}
- function onReachedTop() {
- if (roomTimeline.isOngoingPagination || isReachedTimelineEnd) return;
- roomTimeline.paginateBack();
- }
- function toggleOnReachedBottom(isBottom) {
- wasAtBottom = isBottom;
- if (!isBottom) return;
- trySendingReadReceipt();
- }
+ const getNewFrom = (position) => {
+ let newFrom = scroll.from;
+ const tSize = roomTimeline.timeline.size;
+ const doPaginate = tSize > timelineScroll.maxEvents;
+ if (!doPaginate || scroll.from < 0) newFrom = 0;
+ const newEventCount = Math.round(timelineScroll.maxEvents / 2);
+ scroll.limit = timelineScroll.maxEvents;
- const updatePAG = (canPagMore) => {
- if (!canPagMore) {
- setIsReachedTimelineEnd(true);
- } else {
- setOnPagination({});
- autoLoadTimeline();
+ if (position === 'TOP' && doPaginate) newFrom -= newEventCount;
+ if (position === 'BOTTOM' && doPaginate) newFrom += newEventCount;
+
+ if (newFrom >= tSize || scroll.getEndIndex() >= tSize) newFrom = tSize - scroll.limit - 1;
+ if (newFrom < 0) newFrom = 0;
+ 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
const updateRT = () => {
- if (wasAtBottom) {
+ if (timelineScroll.position === 'BOTTOM') {
trySendingReadReceipt();
+ scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
+ if (scroll.from < 0) scroll.from = 0;
+ scroll.isNewEvent = true;
}
updateState({});
};
+ const handleScrollToLive = () => {
+ scroll.from = roomTimeline.timeline.size - scroll.limit - 1;
+ if (scroll.from < 0) scroll.from = 0;
+ scroll.isNewEvent = true;
+ updateState({});
+ };
+
useEffect(() => {
- setIsReachedTimelineEnd(false);
- wasAtBottom = true;
+ trySendingReadReceipt();
+ return () => {
+ setIsReachedTimelineEnd(false);
+ scroll.limit = 0;
+ };
}, [roomId]);
- useEffect(() => trySendingReadReceipt(), [roomTimeline]);
// init room setup completed.
// listen for future. setup stateUpdate listener.
useEffect(() => {
roomTimeline.on(cons.events.roomTimeline.EVENT, updateRT);
roomTimeline.on(cons.events.roomTimeline.PAGINATED, updatePAG);
- viewEvent.on('reached-top', onReachedTop);
- viewEvent.on('toggle-reached-bottom', toggleOnReachedBottom);
+ viewEvent.on('timeline-scroll', handleTimelineScroll);
+ viewEvent.on('scroll-to-live', handleScrollToLive);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, updateRT);
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, updatePAG);
- viewEvent.removeListener('reached-top', onReachedTop);
- viewEvent.removeListener('toggle-reached-bottom', toggleOnReachedBottom);
+ viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
+ viewEvent.removeListener('scroll-to-live', handleScrollToLive);
};
- }, [roomTimeline, isReachedTimelineEnd, onPagination]);
+ }, [roomTimeline, isReachedTimelineEnd]);
useLayoutEffect(() => {
timelineScroll.reachBottom();
autoLoadTimeline();
+ trySendingReadReceipt();
}, [roomTimeline]);
useLayoutEffect(() => {
- if (onPagination === null) return;
- timelineScroll.tryRestoringScroll();
- }, [onPagination]);
-
- useEffect(() => {
- if (onStateUpdate === null) return;
- if (wasAtBottom) timelineScroll.reachBottom();
+ if (onStateUpdate === null || scroll.isNewEvent) {
+ scroll.isNewEvent = false;
+ timelineScroll.reachBottom();
+ return;
+ }
+ if (timelineScroll.isScrollable) {
+ timelineScroll.tryRestoringScroll();
+ } else {
+ timelineScroll.reachBottom();
+ autoLoadTimeline();
+ }
}, [onStateUpdate]);
let prevMEvent = null;
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 isContentOnly = (
}
function renderMessage(mEvent) {
- if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
- if (
- mEvent.getType() !== 'm.room.message'
- && mEvent.getType() !== 'm.room.encrypted'
- && mEvent.getType() !== 'm.room.member'
- && mEvent.getType() !== 'm.sticker'
- ) return false;
+ if (!cons.supportEventTypes.includes(mEvent.getType())) return false;
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
-
- // ignore if message is deleted
if (mEvent.isRedacted()) return false;
+ if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
+
let divider = null;
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
divider = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
prevMEvent = mEvent;
const timelineChange = parseTimelineChange(mEvent);
- if (timelineChange === null) return null;
+ if (timelineChange === null) return false;
return (
<React.Fragment key={`box-${mEvent.getId()}`}>
{divider}
);
}
+ 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 (
<div className="room-view__content">
<div className="timeline__wrapper">
- { roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
- { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genRoomIntro(undefined, roomTimeline)}
- { roomTimeline.timeline.map(renderMessage) }
+ { renderTimeline() }
</div>
</div>
);
import { getUsersActionJsx } from './common';
function RoomViewFloating({
- roomId, roomTimeline, timelineScroll, viewEvent,
+ roomId, roomTimeline, viewEvent,
}) {
const [reachedBottom, setReachedBottom] = useState(true);
const [typingMembers, setTypingMembers] = useState(new Set());
function updateTyping(members) {
setTypingMembers(members);
}
+ const handleTimelineScroll = (position) => {
+ setReachedBottom(position === 'BOTTOM');
+ };
useEffect(() => {
setReachedBottom(true);
setTypingMembers(new Set());
- viewEvent.on('toggle-reached-bottom', setReachedBottom);
- return () => viewEvent.removeListener('toggle-reached-bottom', setReachedBottom);
+ viewEvent.on('timeline-scroll', handleTimelineScroll);
+ return () => viewEvent.removeListener('timeline-scroll', handleTimelineScroll);
}, [roomId]);
useEffect(() => {
<div className={`room-view__STB${reachedBottom ? '' : ' room-view__STB--open'}`}>
<IconButton
onClick={() => {
- timelineScroll.enableSmoothScroll();
- timelineScroll.reachBottom();
- timelineScroll.disableSmoothScroll();
+ viewEvent.emit('scroll-to-live');
+ setReachedBottom(true);
}}
src={ChevronBottomIC}
tooltip="Scroll to Bottom"
RoomViewFloating.propTypes = {
roomId: PropTypes.string.isRequired,
roomTimeline: PropTypes.shape({}).isRequired,
- timelineScroll: PropTypes.shape({
- reachBottom: PropTypes.func,
- }).isRequired,
viewEvent: PropTypes.shape({}).isRequired,
};
focusInput();
textAreaRef.current.value = roomsInput.getMessage(roomId);
- timelineScroll.reachBottom();
+ viewEvent.emit('scroll-to-live');
viewEvent.emit('message_sent');
textAreaRef.current.style.height = 'unset';
if (replyTo !== null) setReplyTo(null);
RoomViewInput.propTypes = {
roomId: PropTypes.string.isRequired,
roomTimeline: PropTypes.shape({}).isRequired,
- timelineScroll: PropTypes.shape({
- reachBottom: PropTypes.func,
- autoReachBottom: PropTypes.func,
- tryRestoringScroll: PropTypes.func,
- enableSmoothScroll: PropTypes.func,
- disableSmoothScroll: PropTypes.func,
- }).isRequired,
+ timelineScroll: PropTypes.shape({}).isRequired,
viewEvent: PropTypes.shape({}).isRequired,
};
}
}
-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 {
getTimelineJSXMessages,
getUsersActionJsx,
parseReply,
parseTimelineChange,
- scrollToBottom,
- isAtBottom,
- autoScrollToBottom,
};
import EventEmitter from 'events';
import * as sdk from 'matrix-js-sdk';
+import { logger } from 'matrix-js-sdk/lib/logger';
import { secret } from './state/auth';
import RoomList from './state/RoomList';
global.Olm = require('@matrix-org/olm');
+logger.disableAll();
+
class InitMatrix extends EventEmitter {
async init() {
await this.startClient();
constructor(roomList) {
super();
+ this.supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
this.matrixClient = roomList.matrixClient;
this.roomList = roomList;
doesRoomHaveUnread(room) {
const userId = this.matrixClient.getUserId();
const readUpToId = room.getEventReadUpTo(userId);
- const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
if (room.timeline.length
&& room.timeline[room.timeline.length - 1].sender
if (event.getId() === readUpToId) return false;
- if (supportEvents.includes(event.getType())) {
+ if (this.supportEvents.includes(event.getType())) {
return true;
}
}
_listenEvents() {
this.matrixClient.on('Room.timeline', (mEvent, room) => {
- const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
- if (!supportEvents.includes(mEvent.getType())) return;
+ if (!this.supportEvents.includes(mEvent.getType())) return;
const lastTimelineEvent = room.timeline[room.timeline.length - 1];
if (lastTimelineEvent.getId() !== mEvent.getId()) return;
import initMatrix from '../initMatrix';
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 {
constructor(roomId) {
super();
this.matrixClient = initMatrix.matrixClient;
this.roomId = roomId;
this.room = this.matrixClient.getRoom(roomId);
- this.timeline = this.room.timeline;
- this.editedTimeline = this.getEditedTimeline();
- this.reactionTimeline = this.getReactionTimeline();
+
+ this.timeline = new Map();
+ this.editedTimeline = new Map();
+ this.reactionTimeline = new Map();
+
this.isOngoingPagination = false;
this.ongoingDecryptionCount = 0;
this.typingMembers = new Set();
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.isOngoingPagination) return;
- this.emit(cons.events.roomTimeline.EVENT);
- };
- this._listenRedaction = (event, room) => {
- if (room.roomId !== this.roomId) return;
+ this.addToTimeline(event);
this.emit(cons.events.roomTimeline.EVENT);
};
this._listenDecryptEvent = (event) => {
if (event.getRoomId() !== this.roomId) return;
- if (this.ongoingDecryptionCount > 0) this.ongoingDecryptionCount -= 1;
- this.timeline = this.room.timeline;
+ if (this.ongoingDecryptionCount > 0) {
+ 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);
};
if (room.roomId !== this.roomId) return;
const receiptContent = event.getContent();
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 lastEventRecipt = receiptContent[lastEventId];
if (typeof lastEventRecipt === 'undefined') return;
window.selectedRoom = this;
if (this.isEncryptedRoom()) this.room.decryptAllEvents();
+ this._populateTimelines();
}
isEncryptedRoom() {
return this.matrixClient.isRoomEncrypted(this.roomId);
}
- // eslint-disable-next-line class-methods-use-this
- isEdited(mEvent) {
- return mEvent.getRelation()?.rel_type === 'm.replace';
+ addToTimeline(mEvent) {
+ if (isReaction(mEvent)) {
+ 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
- getRelateToId(mEvent) {
- const relation = mEvent.getRelation();
- return relation && relation.event_id;
- }
-
- 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;
+ _populateTimelines() {
+ this.timeline.clear();
+ this.reactionTimeline.clear();
+ this.editedTimeline.clear();
+ this.room.timeline.forEach((mEvent) => this.addToTimeline(mEvent));
}
paginateBack() {
if (this.isOngoingPagination) return;
this.isOngoingPagination = true;
+ const oldSize = this.timeline.size;
const MSG_LIMIT = 30;
this.matrixClient.scrollback(this.room, MSG_LIMIT).then(async (room) => {
if (room.oldState.paginationToken === null) {
// We have reached start of the timeline
this.isOngoingPagination = false;
if (this.isEncryptedRoom()) await this.room.decryptAllEvents();
- this.emit(cons.events.roomTimeline.PAGINATED, false);
+ this.emit(cons.events.roomTimeline.PAGINATED, false, 0);
return;
}
- this.editedTimeline = this.getEditedTimeline();
- this.reactionTimeline = this.getReactionTimeline();
+ this._populateTimelines();
+ const loaded = this.timeline.size - oldSize;
- this.isOngoingPagination = false;
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);
});
}
HOME: 'home',
DIRECTS: 'dm',
},
+ supportEventTypes: ['m.room.create', 'm.room.message', 'm.room.encrypted', 'm.room.member', 'm.sticker'],
notifs: {
DEFAULT: 'default',
ALL_MESSAGES: 'all_messages',
const urlParams = new URLSearchParams(queryString);
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;
+}