From c1e3645d573b28e899a592a9dd797e4ddc7ec0e0 Mon Sep 17 00:00:00 2001 From: Ajay Bura Date: Tue, 7 Dec 2021 21:04:07 +0530 Subject: [PATCH] Implement sending read receipt in new pagination Signed-off-by: Ajay Bura --- src/app/hooks/useForceUpdate.js | 4 +- src/app/organisms/room-optons/RoomOptions.jsx | 17 ++ src/app/organisms/room/RoomView.jsx | 2 - src/app/organisms/room/RoomViewCmdBar.jsx | 14 +- src/app/organisms/room/RoomViewContent.jsx | 208 +++++++++++------- src/app/organisms/room/RoomViewFloating.jsx | 42 ++-- src/client/state/Notifications.js | 25 ++- src/client/state/RoomTimeline.js | 60 +++-- src/client/state/cons.js | 4 + 9 files changed, 244 insertions(+), 132 deletions(-) diff --git a/src/app/hooks/useForceUpdate.js b/src/app/hooks/useForceUpdate.js index 5ab2d94..bea9b3c 100644 --- a/src/app/hooks/useForceUpdate.js +++ b/src/app/hooks/useForceUpdate.js @@ -4,5 +4,7 @@ import { useState } from 'react'; export function useForceUpdate() { const [data, setData] = useState(null); - return [data, () => setData({})]; + return [data, function forceUpdateHook() { + setData({}); + }]; } diff --git a/src/app/organisms/room-optons/RoomOptions.jsx b/src/app/organisms/room-optons/RoomOptions.jsx index c95821d..2616da6 100644 --- a/src/app/organisms/room-optons/RoomOptions.jsx +++ b/src/app/organisms/room-optons/RoomOptions.jsx @@ -11,6 +11,7 @@ import * as roomActions from '../../../client/action/room'; import ContextMenu, { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; +import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import BellIC from '../../../../public/res/ic/outlined/bell.svg'; import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg'; import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg'; @@ -148,6 +149,14 @@ function RoomOptions() { }; }, []); + const handleMarkAsRead = () => { + const mx = initMatrix.matrixClient; + const room = mx.getRoom(roomId); + if (!room) return; + const events = room.getLiveTimeline().getEvents(); + mx.sendReadReceipt(events[events.length - 1]); + }; + const handleInviteClick = () => openInviteUser(roomId); const handleLeaveClick = (toggleMenu) => { if (confirm('Are you really want to leave this room?')) { @@ -169,6 +178,14 @@ function RoomOptions() { content={(toggleMenu) => ( <> {twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)} + { + handleMarkAsRead(); toggleMenu(); + }} + > + Mark as read + { diff --git a/src/app/organisms/room/RoomView.jsx b/src/app/organisms/room/RoomView.jsx index 21e675e..7b75127 100644 --- a/src/app/organisms/room/RoomView.jsx +++ b/src/app/organisms/room/RoomView.jsx @@ -24,12 +24,10 @@ function RoomView({ roomTimeline, eventId }) {
diff --git a/src/app/organisms/room/RoomViewCmdBar.jsx b/src/app/organisms/room/RoomViewCmdBar.jsx index 33aceb1..34c0701 100644 --- a/src/app/organisms/room/RoomViewCmdBar.jsx +++ b/src/app/organisms/room/RoomViewCmdBar.jsx @@ -122,15 +122,14 @@ function ViewCmd() { function FollowingMembers({ roomId, roomTimeline, viewEvent }) { const [followingMembers, setFollowingMembers] = useState([]); const mx = initMatrix.matrixClient; + const myUserId = mx.getUserId(); const handleOnMessageSent = () => setFollowingMembers([]); - const updateFollowingMembers = () => { - const myUserId = mx.getUserId(); - setFollowingMembers(roomTimeline.getLiveReaders().filter((userId) => userId !== myUserId)); - }; - useEffect(() => { + const updateFollowingMembers = () => { + setFollowingMembers(roomTimeline.getLiveReaders()); + }; updateFollowingMembers(); roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers); viewEvent.on('message_sent', handleOnMessageSent); @@ -140,10 +139,11 @@ function FollowingMembers({ roomId, roomTimeline, viewEvent }) { }; }, [roomTimeline]); - return followingMembers.length !== 0 && ( + const filteredM = followingMembers.filter((userId) => userId !== myUserId); + return filteredM.length !== 0 && ( openReadReceipts(roomId, followingMembers)} /> diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index d3fce29..700dc52 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -90,7 +90,7 @@ function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) { if (mEvent.getType() === 'm.room.member') { const timelineChange = parseTimelineChange(mEvent); - if (timelineChange === null) return false; + if (timelineChange === null) return
; return ( { - limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents()); - setTimelineInfo({ - focusEventId: eId, - }); - }; - const setEventTimeline = async (eId) => { if (typeof eId === 'string') { const isLoaded = await roomTimeline.loadEventTimeline(eId); @@ -311,6 +296,35 @@ function useTimeline(roomTimeline, eventId) { }; useEffect(() => { + const initTimeline = (eId) => { + // NOTICE: eId can be id of readUpto, reply or specific event. + // readUpTo: when user click jump to unread message button. + // reply: when user click reply from timeline. + // specific event when user open a link of event. behave same as ^^^^ + const readUpToId = roomTimeline.getReadUpToEventId(); + let focusEventIndex = -1; + const isSpecificEvent = eId && eId !== readUpToId; + + if (isSpecificEvent) { + focusEventIndex = roomTimeline.getEventIndex(eId); + } else if (!readEventStore.getItem()) { + // either opening live timeline or jump to unread. + focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId); + if (roomTimeline.hasEventInTimeline(readUpToId)) { + readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); + } + } else { + focusEventIndex = roomTimeline.getUnreadEventIndex(readEventStore.getItem().getId()); + } + + if (focusEventIndex > -1) { + limit.setFrom(focusEventIndex - Math.round(limit.getMaxEvents() / 2)); + } else { + limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents()); + } + setTimelineInfo({ focusEventId: isSpecificEvent ? eId : null }); + }; + roomTimeline.on(cons.events.roomTimeline.READY, initTimeline); setEventTimeline(eventId); return () => { @@ -323,12 +337,16 @@ function useTimeline(roomTimeline, eventId) { return timelineInfo; } -function usePaginate(roomTimeline, forceUpdateLimit) { +function usePaginate(roomTimeline, readEventStore, forceUpdateLimit) { const [info, setInfo] = useState(null); useEffect(() => { const handleOnPagination = (backwards, loaded, canLoadMore) => { if (loaded === 0) return; + if (!readEventStore.getItem()) { + const readUpToId = roomTimeline.getReadUpToEventId(); + readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); + } limit.setFrom(limit.calcNextFrom(backwards, roomTimeline.timeline.length)); setInfo({ backwards, @@ -372,96 +390,147 @@ function usePaginate(roomTimeline, forceUpdateLimit) { return [info, autoPaginate]; } -function useHandleScroll(roomTimeline, autoPaginate, viewEvent) { - return useCallback(() => { +function useHandleScroll(roomTimeline, autoPaginate, readEventStore, forceUpdateLimit) { + const handleScroll = useCallback(() => { requestAnimationFrame(() => { // emit event to toggle scrollToBottom button visibility const isAtBottom = ( - timelineScroll.bottom < 16 - && !roomTimeline.canPaginateForward() - && limit.getEndIndex() === roomTimeline.length + timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() + && limit.getEndIndex() >= roomTimeline.timeline.length ); - viewEvent.emit('at-bottom', isAtBottom); + roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom); + if (isAtBottom && readEventStore.getItem()) { + requestAnimationFrame(() => roomTimeline.markAllAsRead()); + } }); autoPaginate(); }, [roomTimeline]); + + const handleScrollToLive = useCallback(() => { + if (readEventStore.getItem()) { + requestAnimationFrame(() => roomTimeline.markAllAsRead()); + } + if (roomTimeline.isServingLiveTimeline()) { + limit.setFrom(roomTimeline.timeline.length - limit.getMaxEvents()); + timelineScroll.scrollToBottom(); + forceUpdateLimit(); + return; + } + roomTimeline.loadLiveTimeline(); + }, [roomTimeline]); + + return [handleScroll, handleScrollToLive]; } -function useEventArrive(roomTimeline) { +function useEventArrive(roomTimeline, readEventStore) { + const myUserId = initMatrix.matrixClient.getUserId(); const [newEvent, setEvent] = useState(null); useEffect(() => { + const sendReadReceipt = (event) => { + if (event.isSending()) return; + if (myUserId === event.getSender()) { + roomTimeline.markAllAsRead(); + return; + } + const readUpToEvent = readEventStore.getItem(); + const readUpToId = roomTimeline.getReadUpToEventId(); + + // if user doesn't have focus on app don't mark messages as read. + if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) { + if (readUpToEvent === readUpToId) return; + readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); + return; + } + if (readUpToEvent?.getId() !== readUpToId) { + roomTimeline.markAllAsRead(); + } + }; + const handleEvent = (event) => { const tLength = roomTimeline.timeline.length; - if (roomTimeline.isServingLiveTimeline() && tLength - 1 === limit.getEndIndex()) { + if (roomTimeline.isServingLiveTimeline() + && limit.getEndIndex() >= tLength - 1 + && timelineScroll.bottom < SCROLL_TRIGGER_POS) { limit.setFrom(tLength - limit.getMaxEvents()); + sendReadReceipt(event); + setEvent(event); } - setEvent(event); }; + + const handleEventRedact = (event) => setEvent(event); + roomTimeline.on(cons.events.roomTimeline.EVENT, handleEvent); + roomTimeline.on(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact); return () => { roomTimeline.removeListener(cons.events.roomTimeline.EVENT, handleEvent); + roomTimeline.removeListener(cons.events.roomTimeline.EVENT_REDACTED, handleEventRedact); }; }, [roomTimeline]); useEffect(() => { if (!roomTimeline.initialized) return; - if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) { + if (timelineScroll.bottom < 16 + && !roomTimeline.canPaginateForward() + && document.visibilityState === 'visible') { timelineScroll.scrollToBottom(); } }, [newEvent, roomTimeline]); } -function RoomViewContent({ - eventId, roomTimeline, viewEvent, -}) { +function RoomViewContent({ eventId, roomTimeline }) { const timelineSVRef = useRef(null); const readEventStore = useStore(roomTimeline); - const timelineInfo = useTimeline(roomTimeline, eventId); + const timelineInfo = useTimeline(roomTimeline, eventId, readEventStore); const [onLimitUpdate, forceUpdateLimit] = useForceUpdate(); - const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, forceUpdateLimit); - const handleScroll = useHandleScroll(roomTimeline, autoPaginate, viewEvent); - useEventArrive(roomTimeline); + const [paginateInfo, autoPaginate] = usePaginate(roomTimeline, readEventStore, forceUpdateLimit); + const [handleScroll, handleScrollToLive] = useHandleScroll( + roomTimeline, autoPaginate, readEventStore, forceUpdateLimit, + ); + useEventArrive(roomTimeline, readEventStore); const { timeline } = roomTimeline; - const handleScrollToLive = useCallback(() => { - if (roomTimeline.isServingLiveTimeline()) { - timelineScroll.scrollToBottom(); - return; - } - roomTimeline.loadLiveTimeline(); - }, [roomTimeline]); - useLayoutEffect(() => { if (!roomTimeline.initialized) { timelineScroll = new TimelineScroll(timelineSVRef.current); } }); + // when active timeline changes useEffect(() => { if (!roomTimeline.initialized) return undefined; if (timeline.length > 0) { - if (focusEventIndex === null) timelineScroll.scrollToBottom(); - else timelineScroll.scrollToIndex(focusEventIndex, 80); - focusEventIndex = null; + if (jumpToItemIndex === -1) { + timelineScroll.scrollToBottom(); + } else { + timelineScroll.scrollToIndex(jumpToItemIndex, 80); + } + if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) { + if (readEventStore.getItem()?.getId() === roomTimeline.getReadUpToEventId()) { + requestAnimationFrame(() => roomTimeline.markAllAsRead()); + } + } + jumpToItemIndex = -1; } autoPaginate(); timelineScroll.on('scroll', handleScroll); - viewEvent.on('scroll-to-live', handleScrollToLive); + roomTimeline.on(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive); return () => { if (timelineSVRef.current === null) return; timelineScroll.removeListener('scroll', handleScroll); - viewEvent.removeListener('scroll-to-live', handleScrollToLive); + roomTimeline.removeListener(cons.events.roomTimeline.SCROLL_TO_LIVE, handleScrollToLive); }; }, [timelineInfo]); + // when paginating from server useEffect(() => { if (!roomTimeline.initialized) return; timelineScroll.tryRestoringScroll(); autoPaginate(); }, [paginateInfo]); + // when paginating locally useEffect(() => { if (!roomTimeline.initialized) return; timelineScroll.tryRestoringScroll(); @@ -473,29 +542,16 @@ function RoomViewContent({ throttle._(() => timelineScroll?.calcScroll(), 400)(target); }; - const getReadEvent = () => { - const readEventId = roomTimeline.getReadUpToEventId(); - if (readEventStore.getItem()?.getId() === readEventId) { - return readEventStore.getItem(); - } - if (roomTimeline.hasEventInActiveTimeline(readEventId)) { - return readEventStore.setItem( - roomTimeline.findEventByIdInTimelineSet(readEventId), - ); - } - return readEventStore.setItem(null); - }; - const renderTimeline = () => { const tl = []; - const readEvent = getReadEvent(); - let extraItemCount = 0; - focusEventIndex = null; + let itemCountIndex = 0; + jumpToItemIndex = -1; + const readEvent = readEventStore.getItem(); if (roomTimeline.canPaginateBackward() || limit.from > 0) { tl.push(loadingMsgPlaceholders(1, PLACEHOLDER_COUNT)); - extraItemCount += PLACEHOLDER_COUNT; + itemCountIndex += PLACEHOLDER_COUNT; } for (let i = limit.from; i < limit.getEndIndex(); i += 1) { if (i >= timeline.length) break; @@ -505,30 +561,35 @@ function RoomViewContent({ if (i === 0 && !roomTimeline.canPaginateBackward()) { if (mEvent.getType() === 'm.room.create') { tl.push(genRoomIntro(mEvent, roomTimeline)); + itemCountIndex += 1; // eslint-disable-next-line no-continue continue; } else { tl.push(genRoomIntro(undefined, roomTimeline)); - extraItemCount += 1; + itemCountIndex += 1; } } + const unreadDivider = (readEvent && prevMEvent?.getTs() <= readEvent.getTs() && readEvent.getTs() < mEvent.getTs()); if (unreadDivider) { - tl.push(); - if (focusEventIndex === null) focusEventIndex = i + extraItemCount; + tl.push(); + itemCountIndex += 1; + if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex; } const dayDivider = prevMEvent && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate()); if (dayDivider) { tl.push(); - extraItemCount += 1; + itemCountIndex += 1; } + const focusId = timelineInfo.focusEventId; - const isFocus = focusId === mEvent.getId() && focusId !== readEvent?.getId(); - if (isFocus) focusEventIndex = i + extraItemCount; + const isFocus = focusId === mEvent.getId(); + if (isFocus) jumpToItemIndex = itemCountIndex; tl.push(renderEvent(roomTimeline, mEvent, prevMEvent, isFocus)); + itemCountIndex += 1; } if (roomTimeline.canPaginateForward() || limit.getEndIndex() < timeline.length) { tl.push(loadingMsgPlaceholders(2, PLACEHOLDER_COUNT)); @@ -554,7 +615,6 @@ RoomViewContent.defaultProps = { RoomViewContent.propTypes = { eventId: PropTypes.string, roomTimeline: PropTypes.shape({}).isRequired, - viewEvent: PropTypes.shape({}).isRequired, }; export default RoomViewContent; diff --git a/src/app/organisms/room/RoomViewFloating.jsx b/src/app/organisms/room/RoomViewFloating.jsx index 8ddcce2..fd6a13a 100644 --- a/src/app/organisms/room/RoomViewFloating.jsx +++ b/src/app/organisms/room/RoomViewFloating.jsx @@ -11,7 +11,7 @@ import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; -import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; +import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import { getUsersActionJsx } from './common'; @@ -20,28 +20,25 @@ function useJumpToEvent(roomTimeline) { const jumpToEvent = () => { roomTimeline.loadEventTimeline(eventId); - setEventId(null); }; - const cancelJumpToEvent = () => { + const cancelJumpToEvent = (mEvent) => { setEventId(null); - roomTimeline.markAsRead(); + if (!mEvent) roomTimeline.markAllAsRead(); }; - // TODO: if user reaches the unread messages with other ways - // like by paginating, or loading timeline for that event by other ways ex: clicking on reply. - // then setEventId(null); - useEffect(() => { const readEventId = roomTimeline.getReadUpToEventId(); - // we only show "Jump to unread" btn only if the event is not in live timeline. - // if event is in live timeline - // we will automatically open the timeline from that event - if (!roomTimeline.hasEventInLiveTimeline(readEventId)) { + // we only show "Jump to unread" btn only if the event is not in timeline. + // if event is in timeline + // we will automatically open the timeline from that event position + if (!readEventId.startsWith('~') && !roomTimeline.hasEventInTimeline(readEventId)) { setEventId(readEventId); } + roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, cancelJumpToEvent); return () => { + roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, cancelJumpToEvent); setEventId(null); }; }, [roomTimeline]); @@ -69,28 +66,28 @@ function useTypingMembers(roomTimeline) { return [typingMembers]; } -function useScrollToBottom(roomId, viewEvent) { +function useScrollToBottom(roomTimeline) { const [isAtBottom, setIsAtBottom] = useState(true); const handleAtBottom = (atBottom) => setIsAtBottom(atBottom); useEffect(() => { setIsAtBottom(true); - viewEvent.on('at-bottom', handleAtBottom); - return () => viewEvent.removeListener('at-bottom', handleAtBottom); - }, [roomId]); + roomTimeline.on(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom); + return () => roomTimeline.removeListener(cons.events.roomTimeline.AT_BOTTOM, handleAtBottom); + }, [roomTimeline]); return [isAtBottom, setIsAtBottom]; } function RoomViewFloating({ - roomId, roomTimeline, viewEvent, + roomId, roomTimeline, }) { - const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline, viewEvent); + const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline); const [typingMembers] = useTypingMembers(roomTimeline); - const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomId, viewEvent); + const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomTimeline); const handleScrollToBottom = () => { - viewEvent.emit('scroll-to-live'); + roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE); setIsAtBottom(true); }; @@ -104,9 +101,9 @@ function RoomViewFloating({ onClick={cancelJumpToEvent} variant="primary" size="extra-small" - src={CrossIC} + src={TickMarkIC} tooltipPlacement="bottom" - tooltip="Cancel" + tooltip="Mark as read" />
0 ? ' room-view__typing--open' : ''}`}> @@ -126,7 +123,6 @@ function RoomViewFloating({ RoomViewFloating.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, - viewEvent: PropTypes.shape({}).isRequired, }; export default RoomViewFloating; diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js index 6b56757..476fd1b 100644 --- a/src/client/state/Notifications.js +++ b/src/client/state/Notifications.js @@ -1,11 +1,21 @@ import EventEmitter from 'events'; import cons from './cons'; +function isNotifEvent(mEvent) { + const eType = mEvent.getType(); + if (!cons.supportEventTypes.includes(eType)) return false; + if (eType === 'm.room.member') return false; + + if (mEvent.isRedacted()) return false; + if (mEvent.getRelation()?.rel_type === 'm.replace') return false; + + return true; +} + class Notifications extends EventEmitter { constructor(roomList) { super(); - this.supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker']; this.matrixClient = roomList.matrixClient; this.roomList = roomList; @@ -36,21 +46,14 @@ class Notifications extends EventEmitter { const readUpToId = room.getEventReadUpTo(userId); const liveEvents = room.getLiveTimeline().getEvents(); - if (liveEvents.length - && liveEvents[liveEvents.length - 1].sender - && liveEvents[liveEvents.length - 1].sender.userId === userId - && liveEvents[liveEvents.length - 1].getType() !== 'm.room.member') { + if (liveEvents[liveEvents.length - 1]?.getSender() === userId) { return false; } for (let i = liveEvents.length - 1; i >= 0; i -= 1) { const event = liveEvents[i]; - if (event.getId() === readUpToId) return false; - - if (this.supportEvents.includes(event.getType())) { - return true; - } + if (isNotifEvent(event)) return true; } return true; } @@ -150,7 +153,7 @@ class Notifications extends EventEmitter { _listenEvents() { this.matrixClient.on('Room.timeline', (mEvent, room) => { - if (!this.supportEvents.includes(mEvent.getType())) return; + if (!isNotifEvent(mEvent)) return; const liveEvents = room.getLiveTimeline().getEvents(); const lastTimelineEvent = liveEvents[liveEvents.length - 1]; diff --git a/src/client/state/RoomTimeline.js b/src/client/state/RoomTimeline.js index 57beac7..f56c6a8 100644 --- a/src/client/state/RoomTimeline.js +++ b/src/client/state/RoomTimeline.js @@ -48,6 +48,15 @@ function iterateLinkedTimelines(timeline, backwards, callback) { } } +function isTimelineLinked(tm1, tm2) { + let tm = getFirstLinkedTimeline(tm1); + while (tm) { + if (tm === tm2) return true; + tm = tm.nextTimeline; + } + return false; +} + class RoomTimeline extends EventEmitter { constructor(roomId) { super(); @@ -93,8 +102,8 @@ class RoomTimeline extends EventEmitter { this.timeline = []; // TODO: don't clear these timeline cause there data can be used in other timeline - // this.reactionTimeline.clear(); - // this.editedTimeline.clear(); + this.reactionTimeline.clear(); + this.editedTimeline.clear(); } addToTimeline(mEvent) { @@ -197,22 +206,29 @@ class RoomTimeline extends EventEmitter { return Promise.allSettled(decryptionPromises); } - markAsRead() { + markAllAsRead() { const readEventId = this.getReadUpToEventId(); if (this.timeline.length === 0) return; const latestEvent = this.timeline[this.timeline.length - 1]; if (readEventId === latestEvent.getId()) return; this.matrixClient.sendReadReceipt(latestEvent); + this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent); } - hasEventInLiveTimeline(eventId) { - const timelineSet = this.getUnfilteredTimelineSet(); - return timelineSet.getTimelineForEvent(eventId) === this.liveTimeline; + markAsRead(eventId) { + if (this.hasEventInTimeline(eventId)) { + const mEvent = this.findEventById(eventId); + if (!mEvent) return; + this.matrixClient.sendReadReceipt(mEvent); + this.emit(cons.events.roomTimeline.MARKED_AS_READ, mEvent); + } } - hasEventInActiveTimeline(eventId) { + hasEventInTimeline(eventId, timeline = this.activeTimeline) { const timelineSet = this.getUnfilteredTimelineSet(); - return timelineSet.getTimelineForEvent(eventId) === this.activeTimeline; + const eventTimeline = timelineSet.getTimelineForEvent(eventId); + if (!eventTimeline) return false; + return isTimelineLinked(eventTimeline, timeline); } getUnfilteredTimelineSet() { @@ -242,6 +258,22 @@ class RoomTimeline extends EventEmitter { return [...new Set(readers)]; } + getUnreadEventIndex(readUpToEventId) { + if (!this.hasEventInTimeline(readUpToEventId)) return -1; + + const readUpToEvent = this.findEventByIdInTimelineSet(readUpToEventId); + if (!readUpToEvent) return -1; + const rTs = readUpToEvent.getTs(); + + const tLength = this.timeline.length; + + for (let i = 0; i < tLength; i += 1) { + const mEvent = this.timeline[i]; + if (mEvent.getTs() > rTs) return i; + } + return -1; + } + getReadUpToEventId() { return this.room.getEventReadUpTo(this.matrixClient.getUserId()); } @@ -261,7 +293,7 @@ class RoomTimeline extends EventEmitter { deleteFromTimeline(eventId) { const i = this.getEventIndex(eventId); if (i === -1) return undefined; - return this.timeline.splice(i, 1); + return this.timeline.splice(i, 1)[0]; } _listenEvents() { @@ -306,12 +338,12 @@ class RoomTimeline extends EventEmitter { this.emit(cons.events.roomTimeline.EVENT, event); }; - this._listenRedaction = (event, room) => { + this._listenRedaction = (mEvent, room) => { if (room.roomId !== this.roomId) return; - this.deleteFromTimeline(event.getId()); - this.editedTimeline.delete(event.getId()); - this.reactionTimeline.delete(event.getId()); - this.emit(cons.events.roomTimeline.EVENT); + const rEvent = this.deleteFromTimeline(mEvent.event.redacts); + this.editedTimeline.delete(mEvent.event.redacts); + this.reactionTimeline.delete(mEvent.event.redacts); + this.emit(cons.events.roomTimeline.EVENT_REDACTED, rEvent, mEvent); }; this._listenTypingEvent = (event, member) => { diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 6b01ec6..869a476 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -92,6 +92,10 @@ const cons = { PAGINATED: 'PAGINATED', TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED', LIVE_RECEIPT: 'LIVE_RECEIPT', + MARKED_AS_READ: 'MARKED_AS_READ', + EVENT_REDACTED: 'EVENT_REDACTED', + AT_BOTTOM: 'AT_BOTTOM', + SCROLL_TO_LIVE: 'SCROLL_TO_LIVE', }, roomsInput: { MESSAGE_SENT: 'MESSAGE_SENT',