diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index 4cf0635..5d0e591 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import React, { useState, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './Message.scss'; @@ -7,7 +7,7 @@ import dateFormat from 'dateformat'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; -import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; +import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; import { getEventCords } from '../../../util/common'; import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; @@ -93,6 +93,60 @@ MessageReply.propTypes = { 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 ( +
+ {reply !== null && } +
+ ); +} +MessageReplyWrapper.propTypes = { + roomTimeline: PropTypes.shape({}).isRequired, + eventId: PropTypes.string.isRequired, +}; + function MessageBody({ senderName, body, @@ -126,13 +180,14 @@ function MessageBody({ MessageBody.defaultProps = { isCustomHTML: false, isEdited: false, + msgType: null, }; MessageBody.propTypes = { senderName: PropTypes.string.isRequired, body: PropTypes.node.isRequired, isCustomHTML: PropTypes.bool, isEdited: PropTypes.bool, - msgType: PropTypes.string.isRequired, + msgType: PropTypes.string, }; 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) { const newContent = editedMEvent.getContent()['m.new_content']; if (typeof newContent === 'undefined') return [null, false, null]; @@ -376,7 +411,9 @@ function getEditedBody(editedMEvent) { 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 mx = initMatrix.matrixClient; @@ -385,6 +422,7 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) { } = roomTimeline; const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')]; + if (focus) className.push('message--focus'); const content = mEvent.getContent(); const eventId = mEvent.getId(); const msgType = content?.msgtype; @@ -401,9 +439,9 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) { const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel; 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 isReply = typeof content['m.relates_to']?.['m.in_reply_to'] !== 'undefined'; + const isReply = !!mEvent.replyEventId; let customHTML = isCustomHTML ? content.formatted_body : null; if (isEdited) { @@ -447,18 +485,7 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) { } if (isReply) { - const parsedContent = parseReply(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; - } + body = parseReply(body)?.body ?? body; } return ( @@ -474,8 +501,11 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) { {!isBodyOnly && ( )} - {reply !== null && ( - + {isReply && ( + )} {!isEditing && ( Options openReadReceipts(roomId, eventId)} + onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(eventId))} > Read receipts @@ -590,11 +620,13 @@ function Message({ mEvent, isBodyOnly, roomTimeline }) { } Message.defaultProps = { isBodyOnly: false, + focus: false, }; Message.propTypes = { mEvent: PropTypes.shape({}).isRequired, isBodyOnly: PropTypes.bool, roomTimeline: PropTypes.shape({}).isRequired, + focus: PropTypes.bool, }; export { Message, MessageReply, PlaceholderMessage }; diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss index a682418..1b7da57 100644 --- a/src/app/molecules/message/Message.scss +++ b/src/app/molecules/message/Message.scss @@ -55,6 +55,10 @@ &__avatar-container { width: var(--av-small); } + &--focus { + box-shadow: inset 2px 0 0 var(--bg-caution); + background-color: var(--bg-caution-hover); + } } .ph-msg { @@ -96,6 +100,7 @@ .message__reply, .message__body, +.message__body__wrapper, .message__edit, .message__reactions { max-width: calc(100% - 88px); @@ -142,6 +147,19 @@ } } .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 { color: var(--tc-surface-low); white-space: nowrap; @@ -288,7 +306,7 @@ position: absolute; top: 0; right: 60px; - z-index: 999; + z-index: 99; transform: translateY(-50%); border-radius: var(--bo-radius); @@ -323,7 +341,7 @@ line-height: var(--lh-s1); } & hr { - border-color: var(--bg-surface-border); + border-color: var(--bg-divider); } .text img {