From dde022d17947935fb6dee282e71b07696ffb24e3 Mon Sep 17 00:00:00 2001 From: Ajay Bura Date: Wed, 8 Dec 2021 21:23:18 +0530 Subject: [PATCH] Add server side aggregated events Signed-off-by: Ajay Bura --- src/app/molecules/message/Message.jsx | 217 +++++++++++---------- src/app/organisms/room/RoomViewContent.jsx | 30 ++- src/client/state/RoomTimeline.js | 16 +- 3 files changed, 152 insertions(+), 111 deletions(-) diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index 9fbd8ff..b17cb33 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -223,16 +223,38 @@ MessageEdit.propTypes = { onCancel: PropTypes.func.isRequired, }; -function MessageReactionGroup({ children }) { - return ( -
- { children } -
- ); +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) => {text}; @@ -254,12 +276,12 @@ function genReactionMsg(userIds, reaction) { } function MessageReaction({ - reaction, users, isActive, onClick, + reaction, count, users, isActive, onClick, }) { return ( {genReactionMsg(users, reaction)}} + content={{users.length > 0 ? genReactionMsg(users, reaction) : 'Unable to load who has reacted'}} > ); } 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 ( +
+ { + Object.keys(reactions).map((key) => ( + { + toggleEmoji(roomId, eventId, key, roomTimeline); + }} + /> + )) + } + { + pickEmoji(e, roomId, eventId, roomTimeline); + }} + src={EmojiAddIC} + size="extra-small" + tooltip="Add reaction" + /> +
+ ); +} +MessageReactionGroup.propTypes = { + roomTimeline: PropTypes.shape({}).isRequired, + mEvent: PropTypes.shape({}).isRequired, +}; + function MessageOptions({ children }) { return (
@@ -367,37 +467,6 @@ function genMediaContent(mE) { return Malformed event; } } -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 && ( - - { - reactions.map((reaction) => ( - { - toggleEmoji(roomId, eventId, reaction.key, roomTimeline); - }} - /> - )) - } - { - pickEmoji(e, roomId, eventId, roomTimeline); - }} - src={EmojiAddIC} - size="extra-small" - tooltip="Add reaction" - /> - + )} {!isEditing && ( diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index 1fa0ab2..0a5e9c6 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -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); } }; diff --git a/src/client/state/RoomTimeline.js b/src/client/state/RoomTimeline.js index 1b7eec6..ea7376a 100644 --- a/src/client/state/RoomTimeline.js +++ b/src/client/state/RoomTimeline.js @@ -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;