Add server side aggregated events

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2021-12-08 21:23:18 +05:30
parent 0d12c64c47
commit dde022d179
3 changed files with 152 additions and 111 deletions

View file

@ -223,16 +223,38 @@ MessageEdit.propTypes = {
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
}; };
function MessageReactionGroup({ children }) { function getMyEmojiEvent(emojiKey, eventId, roomTimeline) {
return ( const mx = initMatrix.matrixClient;
<div className="message__reactions text text-b3 noselect"> const rEvents = roomTimeline.reactionTimeline.get(eventId);
{ children } let rEvent = null;
</div> 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) { function genReactionMsg(userIds, reaction) {
const genLessContText = (text) => <span style={{ opacity: '.6' }}>{text}</span>; const genLessContText = (text) => <span style={{ opacity: '.6' }}>{text}</span>;
@ -254,12 +276,12 @@ function genReactionMsg(userIds, reaction) {
} }
function MessageReaction({ function MessageReaction({
reaction, users, isActive, onClick, reaction, count, users, isActive, onClick,
}) { }) {
return ( return (
<Tooltip <Tooltip
className="msg__reaction-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 <button
onClick={onClick} onClick={onClick}
@ -267,18 +289,96 @@ function MessageReaction({
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`} className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
> >
{ twemojify(reaction, { className: 'react-emoji' }) } { 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> </button>
</Tooltip> </Tooltip>
); );
} }
MessageReaction.propTypes = { MessageReaction.propTypes = {
reaction: PropTypes.node.isRequired, reaction: PropTypes.node.isRequired,
count: PropTypes.number.isRequired,
users: PropTypes.arrayOf(PropTypes.string).isRequired, users: PropTypes.arrayOf(PropTypes.string).isRequired,
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.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 }) { function MessageOptions({ children }) {
return ( return (
<div className="message__options"> <div className="message__options">
@ -367,37 +467,6 @@ function genMediaContent(mE) {
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>; 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) { function getEditedBody(editedMEvent) {
const newContent = editedMEvent.getContent()['m.new_content']; const newContent = editedMEvent.getContent()['m.new_content'];
@ -438,8 +507,9 @@ function Message({
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel; const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel); const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
let [reactions, isCustomHTML] = [null, content.format === 'org.matrix.custom.html']; let isCustomHTML = content.format === 'org.matrix.custom.html';
const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)]; const isEdited = editedTimeline.has(eventId);
const haveReactions = reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation');
const isReply = !!mEvent.replyEventId; const isReply = !!mEvent.replyEventId;
let customHTML = isCustomHTML ? content.formatted_body : null; let customHTML = isCustomHTML ? content.formatted_body : null;
@ -450,39 +520,6 @@ function Message({
if (typeof body !== 'string') return null; 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) { if (isReply) {
body = parseReply(body)?.body ?? body; body = parseReply(body)?.body ?? body;
} }
@ -528,29 +565,7 @@ function Message({
/> />
)} )}
{haveReactions && ( {haveReactions && (
<MessageReactionGroup> <MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
{
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>
)} )}
{!isEditing && ( {!isEditing && (
<MessageOptions> <MessageOptions>

View file

@ -446,19 +446,43 @@ function useEventArrive(roomTimeline, readEventStore) {
readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); readEventStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
return; 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(); roomTimeline.markAllAsRead();
} }
}; };
const handleEvent = (event) => { const handleEvent = (event) => {
const tLength = roomTimeline.timeline.length; const tLength = roomTimeline.timeline.length;
if (roomTimeline.isServingLiveTimeline() const isUserViewingLive = (
roomTimeline.isServingLiveTimeline()
&& limit.getEndIndex() >= tLength - 1 && limit.getEndIndex() >= tLength - 1
&& timelineScroll.bottom < SCROLL_TRIGGER_POS) { && timelineScroll.bottom < SCROLL_TRIGGER_POS
);
if (isUserViewingLive) {
limit.setFrom(tLength - limit.getMaxEvents()); limit.setFrom(tLength - limit.getMaxEvents());
sendReadReceipt(event); sendReadReceipt(event);
setEvent(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);
} }
}; };

View file

@ -18,9 +18,12 @@ function getRelateToId(mEvent) {
function addToMap(myMap, mEvent) { function addToMap(myMap, mEvent) {
const relateToId = getRelateToId(mEvent); const relateToId = getRelateToId(mEvent);
if (relateToId === null) return null; if (relateToId === null) return null;
const mEventId = mEvent.getId();
if (typeof myMap.get(relateToId) === 'undefined') myMap.set(relateToId, []); 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; return mEvent;
} }
@ -101,10 +104,6 @@ class RoomTimeline extends EventEmitter {
clearLocalTimelines() { clearLocalTimelines() {
this.timeline = []; 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) { addToTimeline(mEvent) {
@ -295,8 +294,11 @@ class RoomTimeline extends EventEmitter {
if (this.isOngoingPagination) return; if (this.isOngoingPagination) return;
// User is currently viewing the old events probably // User is currently viewing the old events probably
// no need to add this event and emit changes. // no need to add new event and emit changes.
if (this.isServingLiveTimeline() === false) return; // only add reactions and edited messages
if (this.isServingLiveTimeline() === false) {
if (!isReaction(event) && !isEdited(event)) return;
}
// We only process live events here // We only process live events here
if (!data.liveEvent) return; if (!data.liveEvent) return;