Parse reply using m.in_reply_to (#134)

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2021-12-03 18:30:05 +05:30
parent fb5f368894
commit 0c0a978886
2 changed files with 93 additions and 43 deletions

View file

@ -1,5 +1,5 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import React, { useState, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './Message.scss'; import './Message.scss';
@ -7,7 +7,7 @@ import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify'; import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common'; import { getEventCords } from '../../../util/common';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
@ -93,6 +93,60 @@ MessageReply.propTypes = {
body: PropTypes.string.isRequired, body: PropTypes.string.isRequired,
}; };
function MessageReplyWrapper({ roomTimeline, eventId }) {
const [reply, setReply] = useState(null);
const isMountedRef = useRef(true);
useEffect(() => {
const mx = initMatrix.matrixClient;
const timelineSet = roomTimeline.getUnfilteredTimelineSet();
const loadReply = async () => {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
const mEvent = eTimeline.getTimelineSet().findEventById(eventId);
const rawBody = mEvent.getContent().body;
const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply content ***';
setReply({
to: username,
color: colorMXID(mEvent.getSender()),
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody,
event: mEvent,
});
};
loadReply();
return () => {
isMountedRef.current = false;
};
}, []);
const focusReply = () => {
if (reply?.event.isRedacted()) return;
roomTimeline.loadEventTimeline(eventId);
};
return (
<div
className="message__reply-wrapper"
onClick={focusReply}
onKeyDown={focusReply}
role="button"
tabIndex="0"
>
{reply !== null && <MessageReply name={reply.to} color={reply.color} body={reply.body} />}
</div>
);
}
MessageReplyWrapper.propTypes = {
roomTimeline: PropTypes.shape({}).isRequired,
eventId: PropTypes.string.isRequired,
};
function MessageBody({ function MessageBody({
senderName, senderName,
body, body,
@ -126,13 +180,14 @@ function MessageBody({
MessageBody.defaultProps = { MessageBody.defaultProps = {
isCustomHTML: false, isCustomHTML: false,
isEdited: false, isEdited: false,
msgType: null,
}; };
MessageBody.propTypes = { MessageBody.propTypes = {
senderName: PropTypes.string.isRequired, senderName: PropTypes.string.isRequired,
body: PropTypes.node.isRequired, body: PropTypes.node.isRequired,
isCustomHTML: PropTypes.bool, isCustomHTML: PropTypes.bool,
isEdited: PropTypes.bool, isEdited: PropTypes.bool,
msgType: PropTypes.string.isRequired, msgType: PropTypes.string,
}; };
function MessageEdit({ body, onSave, onCancel }) { function MessageEdit({ body, onSave, onCancel }) {
@ -344,26 +399,6 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
}); });
} }
function parseReply(rawBody) {
if (rawBody.indexOf('>') !== 0) return null;
let body = rawBody.slice(rawBody.indexOf('<') + 1);
const user = body.slice(0, body.indexOf('>'));
body = body.slice(body.indexOf('>') + 2);
const replyBody = body.slice(0, body.indexOf('\n\n'));
body = body.slice(body.indexOf('\n\n') + 2);
if (user === '') return null;
const isUserId = user.match(/^@.+:.+/);
return {
userId: isUserId ? user : null,
displayName: isUserId ? null : user,
replyBody,
body,
};
}
function getEditedBody(editedMEvent) { function getEditedBody(editedMEvent) {
const newContent = editedMEvent.getContent()['m.new_content']; const newContent = editedMEvent.getContent()['m.new_content'];
if (typeof newContent === 'undefined') return [null, false, null]; if (typeof newContent === 'undefined') return [null, false, null];
@ -376,7 +411,9 @@ function getEditedBody(editedMEvent) {
return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null]; return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null];
} }
function Message({ mEvent, isBodyOnly, roomTimeline }) { function Message({
mEvent, isBodyOnly, roomTimeline, focus,
}) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
@ -385,6 +422,7 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
} = roomTimeline; } = roomTimeline;
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')]; const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
if (focus) className.push('message--focus');
const content = mEvent.getContent(); const content = mEvent.getContent();
const eventId = mEvent.getId(); const eventId = mEvent.getId();
const msgType = content?.msgtype; const msgType = content?.msgtype;
@ -401,9 +439,9 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
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 [reply, reactions, isCustomHTML] = [null, null, content.format === 'org.matrix.custom.html']; let [reactions, isCustomHTML] = [null, content.format === 'org.matrix.custom.html'];
const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)]; const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)];
const isReply = typeof content['m.relates_to']?.['m.in_reply_to'] !== 'undefined'; const isReply = !!mEvent.replyEventId;
let customHTML = isCustomHTML ? content.formatted_body : null; let customHTML = isCustomHTML ? content.formatted_body : null;
if (isEdited) { if (isEdited) {
@ -447,18 +485,7 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
} }
if (isReply) { if (isReply) {
const parsedContent = parseReply(body); body = parseReply(body)?.body ?? body;
if (parsedContent !== null) {
const c = room.currentState;
const displayNameToUserIds = c.getUserIdsWithDisplayName(parsedContent.displayName);
const ID = parsedContent.userId || displayNameToUserIds[0];
reply = {
color: colorMXID(ID || parsedContent.displayName),
to: parsedContent.displayName || getUsername(parsedContent.userId),
body: parsedContent.replyBody,
};
body = parsedContent.body;
}
} }
return ( return (
@ -474,8 +501,11 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
{!isBodyOnly && ( {!isBodyOnly && (
<MessageHeader userId={senderId} name={username} color={mxidColor} time={time} /> <MessageHeader userId={senderId} name={username} color={mxidColor} time={time} />
)} )}
{reply !== null && ( {isReply && (
<MessageReply name={reply.to} color={reply.color} body={reply.body} /> <MessageReplyWrapper
roomTimeline={roomTimeline}
eventId={mEvent.replyEventId}
/>
)} )}
{!isEditing && ( {!isEditing && (
<MessageBody <MessageBody
@ -551,7 +581,7 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
<MenuHeader>Options</MenuHeader> <MenuHeader>Options</MenuHeader>
<MenuItem <MenuItem
iconSrc={TickMarkIC} iconSrc={TickMarkIC}
onClick={() => openReadReceipts(roomId, eventId)} onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(eventId))}
> >
Read receipts Read receipts
</MenuItem> </MenuItem>
@ -590,11 +620,13 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) {
} }
Message.defaultProps = { Message.defaultProps = {
isBodyOnly: false, isBodyOnly: false,
focus: false,
}; };
Message.propTypes = { Message.propTypes = {
mEvent: PropTypes.shape({}).isRequired, mEvent: PropTypes.shape({}).isRequired,
isBodyOnly: PropTypes.bool, isBodyOnly: PropTypes.bool,
roomTimeline: PropTypes.shape({}).isRequired, roomTimeline: PropTypes.shape({}).isRequired,
focus: PropTypes.bool,
}; };
export { Message, MessageReply, PlaceholderMessage }; export { Message, MessageReply, PlaceholderMessage };

View file

@ -55,6 +55,10 @@
&__avatar-container { &__avatar-container {
width: var(--av-small); width: var(--av-small);
} }
&--focus {
box-shadow: inset 2px 0 0 var(--bg-caution);
background-color: var(--bg-caution-hover);
}
} }
.ph-msg { .ph-msg {
@ -96,6 +100,7 @@
.message__reply, .message__reply,
.message__body, .message__body,
.message__body__wrapper,
.message__edit, .message__edit,
.message__reactions { .message__reactions {
max-width: calc(100% - 88px); max-width: calc(100% - 88px);
@ -142,6 +147,19 @@
} }
} }
.message__reply { .message__reply {
&-wrapper {
min-height: 20px;
cursor: pointer;
&:empty {
border-radius: calc(var(--bo-radius) / 2);
background-color: var(--bg-surface-hover);
max-width: 200px;
cursor: auto;
}
&:hover .text {
color: var(--tc-surface-high);
}
}
.text { .text {
color: var(--tc-surface-low); color: var(--tc-surface-low);
white-space: nowrap; white-space: nowrap;
@ -288,7 +306,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
right: 60px; right: 60px;
z-index: 999; z-index: 99;
transform: translateY(-50%); transform: translateY(-50%);
border-radius: var(--bo-radius); border-radius: var(--bo-radius);
@ -323,7 +341,7 @@
line-height: var(--lh-s1); line-height: var(--lh-s1);
} }
& hr { & hr {
border-color: var(--bg-surface-border); border-color: var(--bg-divider);
} }
.text img { .text img {