Add server side aggregated events
authorAjay Bura <ajbura@gmail.com>
Wed, 8 Dec 2021 15:53:18 +0000 (21:23 +0530)
committerAjay Bura <ajbura@gmail.com>
Wed, 8 Dec 2021 15:53:18 +0000 (21:23 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/molecules/message/Message.jsx
src/app/organisms/room/RoomViewContent.jsx
src/client/state/RoomTimeline.js

index 9fbd8ffffc207cb755025394a605d1f1faeb88c0..b17cb33722fce8a2c4d720e2544a1680804a6b26 100644 (file)
@@ -223,16 +223,38 @@ MessageEdit.propTypes = {
   onCancel: PropTypes.func.isRequired,
 };
 
-function MessageReactionGroup({ children }) {
-  return (
-    <div className="message__reactions text text-b3 noselect">
-      { children }
-    </div>
-  );
+function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
+  const mx = initMatrix.matrixClient;
+  const rEvents = roomTimeline.reactionTimeline.get(eventId);
+  let rEvent = null;
+  rEvents?.find((rE) => {
+    if (rE.getRelation() === null) return false;
+    if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
+      rEvent = rE;
+      return true;
+    }
+    return false;
+  });
+  return rEvent;
+}
+
+function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
+  const myAlreadyReactEvent = getMyEmojiEvent(emojiKey, eventId, roomTimeline);
+  if (myAlreadyReactEvent) {
+    const rId = myAlreadyReactEvent.getId();
+    if (rId.startsWith('~')) return;
+    redactEvent(roomId, rId);
+    return;
+  }
+  sendReaction(roomId, eventId, emojiKey);
+}
+
+function pickEmoji(e, roomId, eventId, roomTimeline) {
+  openEmojiBoard(getEventCords(e), (emoji) => {
+    toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
+    e.target.click();
+  });
 }
-MessageReactionGroup.propTypes = {
-  children: PropTypes.node.isRequired,
-};
 
 function genReactionMsg(userIds, reaction) {
   const genLessContText = (text) => <span style={{ opacity: '.6' }}>{text}</span>;
@@ -254,12 +276,12 @@ function genReactionMsg(userIds, reaction) {
 }
 
 function MessageReaction({
-  reaction, users, isActive, onClick,
+  reaction, count, users, isActive, onClick,
 }) {
   return (
     <Tooltip
       className="msg__reaction-tooltip"
-      content={<Text variant="b2">{genReactionMsg(users, reaction)}</Text>}
+      content={<Text variant="b2">{users.length > 0 ? genReactionMsg(users, reaction) : 'Unable to load who has reacted'}</Text>}
     >
       <button
         onClick={onClick}
@@ -267,18 +289,96 @@ function MessageReaction({
         className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
       >
         { twemojify(reaction, { className: 'react-emoji' }) }
-        <Text variant="b3" className="msg__reaction-count">{users.length}</Text>
+        <Text variant="b3" className="msg__reaction-count">{count}</Text>
       </button>
     </Tooltip>
   );
 }
 MessageReaction.propTypes = {
   reaction: PropTypes.node.isRequired,
+  count: PropTypes.number.isRequired,
   users: PropTypes.arrayOf(PropTypes.string).isRequired,
   isActive: PropTypes.bool.isRequired,
   onClick: PropTypes.func.isRequired,
 };
 
+function MessageReactionGroup({ roomTimeline, mEvent }) {
+  const { roomId, reactionTimeline } = roomTimeline;
+  const eventId = mEvent.getId();
+  const mx = initMatrix.matrixClient;
+  const reactions = {};
+
+  const eventReactions = reactionTimeline.get(eventId);
+  const addReaction = (key, count, senderId, isActive) => {
+    let reaction = reactions[key];
+    if (reaction === undefined) {
+      reaction = {
+        count: 0,
+        users: [],
+        isActive: false,
+      };
+    }
+    if (count) {
+      reaction.count = count;
+    } else {
+      reaction.users.push(senderId);
+      reaction.count = reaction.users.length;
+      reaction.isActive = isActive;
+    }
+
+    reactions[key] = reaction;
+  };
+  if (eventReactions) {
+    eventReactions.forEach((rEvent) => {
+      if (rEvent.getRelation() === null) return;
+      const reaction = rEvent.getRelation();
+      const senderId = rEvent.getSender();
+      const isActive = senderId === mx.getUserId();
+
+      addReaction(reaction.key, undefined, senderId, isActive);
+    });
+  } else {
+    // Use aggregated reactions
+    const aggregatedReaction = mEvent.getServerAggregatedRelation('m.annotation')?.chunk;
+    if (!aggregatedReaction) return null;
+    aggregatedReaction.forEach((reaction) => {
+      if (reaction.type !== 'm.reaction') return;
+      addReaction(reaction.key, reaction.count, undefined, false);
+    });
+  }
+
+  return (
+    <div className="message__reactions text text-b3 noselect">
+      {
+        Object.keys(reactions).map((key) => (
+          <MessageReaction
+            key={key}
+            reaction={key}
+            count={reactions[key].count}
+            users={reactions[key].users}
+            isActive={reactions[key].isActive}
+            onClick={() => {
+              toggleEmoji(roomId, eventId, key, roomTimeline);
+            }}
+          />
+        ))
+      }
+      <IconButton
+        onClick={(e) => {
+          pickEmoji(e, roomId, eventId, roomTimeline);
+        }}
+        src={EmojiAddIC}
+        size="extra-small"
+        tooltip="Add reaction"
+      />
+    </div>
+  );
+}
+MessageReactionGroup.propTypes = {
+  roomTimeline: PropTypes.shape({}).isRequired,
+  mEvent: PropTypes.shape({}).isRequired,
+};
+
 function MessageOptions({ children }) {
   return (
     <div className="message__options">
@@ -367,37 +467,6 @@ function genMediaContent(mE) {
       return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
   }
 }
-function getMyEmojiEventId(emojiKey, eventId, roomTimeline) {
-  const mx = initMatrix.matrixClient;
-  const rEvents = roomTimeline.reactionTimeline.get(eventId);
-  let rEventId = null;
-  rEvents?.find((rE) => {
-    if (rE.getRelation() === null) return false;
-    if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
-      rEventId = rE.getId();
-      return true;
-    }
-    return false;
-  });
-  return rEventId;
-}
-
-function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
-  const myAlreadyReactEventId = getMyEmojiEventId(emojiKey, eventId, roomTimeline);
-  if (typeof myAlreadyReactEventId === 'string') {
-    if (myAlreadyReactEventId.indexOf('~') === 0) return;
-    redactEvent(roomId, myAlreadyReactEventId);
-    return;
-  }
-  sendReaction(roomId, eventId, emojiKey);
-}
-
-function pickEmoji(e, roomId, eventId, roomTimeline) {
-  openEmojiBoard(getEventCords(e), (emoji) => {
-    toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
-    e.target.click();
-  });
-}
 
 function getEditedBody(editedMEvent) {
   const newContent = editedMEvent.getContent()['m.new_content'];
@@ -438,8 +507,9 @@ function Message({
   const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
   const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
 
-  let [reactions, isCustomHTML] = [null, content.format === 'org.matrix.custom.html'];
-  const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)];
+  let isCustomHTML = content.format === 'org.matrix.custom.html';
+  const isEdited = editedTimeline.has(eventId);
+  const haveReactions = reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation');
   const isReply = !!mEvent.replyEventId;
   let customHTML = isCustomHTML ? content.formatted_body : null;
 
@@ -450,39 +520,6 @@ function Message({
     if (typeof body !== 'string') return null;
   }
 
-  if (haveReactions) {
-    reactions = [];
-    reactionTimeline.get(eventId).forEach((rEvent) => {
-      if (rEvent.getRelation() === null) return;
-      function alreadyHaveThisReaction(rE) {
-        for (let i = 0; i < reactions.length; i += 1) {
-          if (reactions[i].key === rE.getRelation().key) return true;
-        }
-        return false;
-      }
-      if (alreadyHaveThisReaction(rEvent)) {
-        for (let i = 0; i < reactions.length; i += 1) {
-          if (reactions[i].key === rEvent.getRelation().key) {
-            reactions[i].users.push(rEvent.getSender());
-            if (reactions[i].isActive !== true) {
-              const myUserId = mx.getUserId();
-              reactions[i].isActive = rEvent.getSender() === myUserId;
-              if (reactions[i].isActive) reactions[i].id = rEvent.getId();
-            }
-            break;
-          }
-        }
-      } else {
-        reactions.push({
-          id: rEvent.getId(),
-          key: rEvent.getRelation().key,
-          users: [rEvent.getSender()],
-          isActive: (rEvent.getSender() === mx.getUserId()),
-        });
-      }
-    });
-  }
-
   if (isReply) {
     body = parseReply(body)?.body ?? body;
   }
@@ -528,29 +565,7 @@ function Message({
           />
         )}
         {haveReactions && (
-          <MessageReactionGroup>
-            {
-              reactions.map((reaction) => (
-                <MessageReaction
-                  key={reaction.id}
-                  reaction={reaction.key}
-                  users={reaction.users}
-                  isActive={reaction.isActive}
-                  onClick={() => {
-                    toggleEmoji(roomId, eventId, reaction.key, roomTimeline);
-                  }}
-                />
-              ))
-            }
-            <IconButton
-              onClick={(e) => {
-                pickEmoji(e, roomId, eventId, roomTimeline);
-              }}
-              src={EmojiAddIC}
-              size="extra-small"
-              tooltip="Add reaction"
-            />
-          </MessageReactionGroup>
+          <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
         )}
         {!isEditing && (
           <MessageOptions>
index 1fa0ab284853de7d316c1c136833fb13b3299f3c..0a5e9c65673f4196e12e6306fd4c9c816f3d2834 100644 (file)
@@ -446,19 +446,43 @@ function useEventArrive(roomTimeline, readEventStore) {
         readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
         return;
       }
-      if (readUpToEvent?.getId() !== readUpToId) {
+      const isUnreadMsg = readUpToEvent?.getId() === readUpToId;
+      if (!isUnreadMsg) {
+        roomTimeline.markAllAsRead();
+      }
+      const { timeline } = roomTimeline;
+      const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToEvent?.getId();
+      if (unreadMsgIsLast) {
         roomTimeline.markAllAsRead();
       }
     };
 
     const handleEvent = (event) => {
       const tLength = roomTimeline.timeline.length;
-      if (roomTimeline.isServingLiveTimeline()
+      const isUserViewingLive = (
+        roomTimeline.isServingLiveTimeline()
         && limit.getEndIndex() >= tLength - 1
-        && timelineScroll.bottom < SCROLL_TRIGGER_POS) {
+        && timelineScroll.bottom < SCROLL_TRIGGER_POS
+      );
+      if (isUserViewingLive) {
         limit.setFrom(tLength - limit.getMaxEvents());
         sendReadReceipt(event);
         setEvent(event);
+        return;
+      }
+      const isRelates = (event.getType() === 'm.reaction' || event.getRelation()?.rel_type === 'm.replace');
+      if (isRelates) {
+        setEvent(event);
+        return;
+      }
+      const isUserDitchedLive = (
+        roomTimeline.isServingLiveTimeline()
+        && limit.getEndIndex() >= tLength - 1
+      );
+      if (isUserDitchedLive) {
+        // This stateUpdate will help to put the
+        // loading msg placeholder at bottom
+        setEvent(event);
       }
     };
 
index 1b7eec695cd825bb71f9d62f3e55b64528c18324..ea7376ad05c364f153fd087038ccbb9fd32fb66c 100644 (file)
@@ -18,9 +18,12 @@ function getRelateToId(mEvent) {
 function addToMap(myMap, mEvent) {
   const relateToId = getRelateToId(mEvent);
   if (relateToId === null) return null;
+  const mEventId = mEvent.getId();
 
   if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []);
-  myMap.get(relateToId).push(mEvent);
+  const mEvents = myMap.get(relateToId);
+  if (mEvents.find((ev) => ev.getId() === mEventId)) return mEvent;
+  mEvents.push(mEvent);
   return mEvent;
 }
 
@@ -101,10 +104,6 @@ class RoomTimeline extends EventEmitter {
 
   clearLocalTimelines() {
     this.timeline = [];
-
-    // TODO: don't clear these timeline cause there data can be used in other timeline
-    this.reactionTimeline.clear();
-    this.editedTimeline.clear();
   }
 
   addToTimeline(mEvent) {
@@ -295,8 +294,11 @@ class RoomTimeline extends EventEmitter {
       if (this.isOngoingPagination) return;
 
       // User is currently viewing the old events probably
-      // no need to add this event and emit changes.
-      if (this.isServingLiveTimeline() === false) return;
+      // no need to add new event and emit changes.
+      // only add reactions and edited messages
+      if (this.isServingLiveTimeline() === false) {
+        if (!isReaction(event) && !isEdited(event)) return;
+      }
 
       // We only process live events here
       if (!data.liveEvent) return;