Add server side aggregated events
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
0d12c64c47
commit
dde022d179
3 changed files with 152 additions and 111 deletions
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue