Implement sending read receipt in new pagination
authorAjay Bura <ajbura@gmail.com>
Tue, 7 Dec 2021 15:34:07 +0000 (21:04 +0530)
committerAjay Bura <ajbura@gmail.com>
Tue, 7 Dec 2021 15:34:07 +0000 (21:04 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/hooks/useForceUpdate.js
src/app/organisms/room-optons/RoomOptions.jsx
src/app/organisms/room/RoomView.jsx
src/app/organisms/room/RoomViewCmdBar.jsx
src/app/organisms/room/RoomViewContent.jsx
src/app/organisms/room/RoomViewFloating.jsx
src/client/state/Notifications.js
src/client/state/RoomTimeline.js
src/client/state/cons.js

index 5ab2d94b2e57657b1432d18990aca567d56db8b1..bea9b3c782a8edcd95d82f10babb8cd2f9db794f 100644 (file)
@@ -4,5 +4,7 @@ import { useState } from 'react';
 export function useForceUpdate() {
   const [data, setData] = useState(null);
 
-  return [data, () => setData({})];
+  return [data, function forceUpdateHook() {
+    setData({});
+  }];
 }
index c95821d536c3a83c9f44c407adbd12d2b084f2f9..2616da61d406bf4216e10fe0e1cae19c1a49f5c8 100644 (file)
@@ -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) => (
         <>
           <MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
+          <MenuItem
+            iconSrc={TickMarkIC}
+            onClick={() => {
+              handleMarkAsRead(); toggleMenu();
+            }}
+          >
+            Mark as read
+          </MenuItem>
           <MenuItem
             iconSrc={AddUserIC}
             onClick={() => {
index 21e675ed081ce97d8ed64ff033c5b7d6f83af4b5..7b751275f4900fa589c86245f620aa75e294a1de 100644 (file)
@@ -24,12 +24,10 @@ function RoomView({ roomTimeline, eventId }) {
           <RoomViewContent
             eventId={eventId}
             roomTimeline={roomTimeline}
-            viewEvent={viewEvent}
           />
           <RoomViewFloating
             roomId={roomId}
             roomTimeline={roomTimeline}
-            viewEvent={viewEvent}
           />
         </div>
         <div className="room-view__sticky">
index 33aceb1aac20961bd9b6537262f1813b18505d3d..34c0701419ffa6466cc9af3895f05d6d9d4cfebd 100644 (file)
@@ -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 && (
     <TimelineChange
       variant="follow"
-      content={getUsersActionJsx(roomId, followingMembers, 'following the conversation.')}
+      content={getUsersActionJsx(roomId, filteredM, 'following the conversation.')}
       time=""
       onClick={() => openReadReceipts(roomId, followingMembers)}
     />
index d3fce29b2a5a39fc4b9a5abfbd3c379d105a30c9..700dc52deb8f40cf0a82cbd1998bb8ee440fc8d4 100644 (file)
@@ -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 <div key={mEvent.getId()} />;
     return (
       <TimelineChange
         key={mEvent.getId()}
@@ -147,7 +147,7 @@ class TimelineScroll extends EventEmitter {
 
     let scrollTop = 0;
     const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
-    if (!ot) scrollTop = this.top;
+    if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
     else scrollTop = ot - this.diff;
 
     this._scrollTo(scrollInfo, scrollTop);
@@ -255,7 +255,7 @@ class TimelineScroll extends EventEmitter {
 }
 
 let timelineScroll = null;
-let focusEventIndex = null;
+let jumpToItemIndex = -1;
 const throttle = new Throttle();
 const limit = {
   from: 0,
@@ -282,24 +282,9 @@ const limit = {
   },
 };
 
-function useTimeline(roomTimeline, eventId) {
+function useTimeline(roomTimeline, eventId, readEventStore) {
   const [timelineInfo, setTimelineInfo] = useState(null);
 
-  // TODO:
-  // open specific event.
-  // 1. readUpTo event is in specific timeline
-  // 2. readUpTo event isn't in specific timeline
-  // 3. readUpTo event is specific event
-  // open live timeline.
-  // 1. readUpTo event is in live timeline
-  // 2. readUpTo event isn't in live timeline
-  const initTimeline = (eId) => {
-    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(<Divider key={`new-${readEvent.getId()}`} variant="positive" text="Unread messages" />);
-        if (focusEventIndex === null) focusEventIndex = i + extraItemCount;
+        tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
+        itemCountIndex += 1;
+        if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
       }
       const dayDivider = prevMEvent && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate());
       if (dayDivider) {
         tl.push(<Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />);
-        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;
index 8ddcce2a496184b28ac20e32c170f19605e71f2e..fd6a13ad58c3cda19c1da74464909cbff0d8e170 100644 (file)
@@ -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"
         />
       </div>
       <div className={`room-view__typing${typingMembers.size > 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;
index 6b56757c28088dffa2ca74a461071a483ac64c68..476fd1b53ea9bdd93ee6d84f93db19a114c83639 100644 (file)
@@ -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];
index 57beac7bd8d91977ac4b643c5076cbd3c73ac707..f56c6a89d6e4930ed8a3249545b38228336208c9 100644 (file)
@@ -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) => {
index 6b01ec61011834225cf8aa163f7c5f564763cac9..869a4765f63803e4a373d2443958a13f7f371101 100644 (file)
@@ -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',