diff --git a/src/app/atoms/divider/Divider.jsx b/src/app/atoms/divider/Divider.jsx index 479fcec..b524ec2 100644 --- a/src/app/atoms/divider/Divider.jsx +++ b/src/app/atoms/divider/Divider.jsx @@ -8,21 +8,18 @@ function Divider({ text, variant }) { const dividerClass = ` divider--${variant}`; return (
- {text !== false && {text}} + {text !== null && {text}}
); } Divider.defaultProps = { - text: false, + text: null, variant: 'surface', }; Divider.propTypes = { - text: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.bool, - ]), + text: PropTypes.string, variant: PropTypes.oneOf(['surface', 'primary', 'caution', 'danger']), }; diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index 2baacfa..60c00eb 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -1,4 +1,5 @@ -import React, { useRef } from 'react'; +/* eslint-disable react/prop-types */ +import React, { useState, useRef } from 'react'; import PropTypes from 'prop-types'; import './Message.scss'; @@ -9,15 +10,33 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism'; import parse from 'html-react-parser'; import twemoji from 'twemoji'; -import { getUsername } from '../../../util/matrixUtil'; +import dateFormat from 'dateformat'; + +import initMatrix from '../../../client/initMatrix'; +import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; +import colorMXID from '../../../util/colorMXID'; +import { getEventCords } from '../../../util/common'; +import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; +import { + openEmojiBoard, openProfileViewer, openReadReceipts, replyTo, +} from '../../../client/action/navigation'; import Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import Button from '../../atoms/button/Button'; import Tooltip from '../../atoms/tooltip/Tooltip'; import Input from '../../atoms/input/Input'; +import Avatar from '../../atoms/avatar/Avatar'; +import IconButton from '../../atoms/button/IconButton'; +import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu'; +import * as Media from '../media/Media'; import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; +import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg'; +import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; +import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; +import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; +import BinIC from '../../../../public/res/ic/outlined/bin.svg'; const components = { code({ @@ -55,7 +74,7 @@ function PlaceholderMessage() {
-
+
@@ -88,13 +107,13 @@ MessageHeader.propTypes = { time: PropTypes.string.isRequired, }; -function MessageReply({ name, color, content }) { +function MessageReply({ name, color, body }) { return (
{name} - <>{` ${content}`} + <>{` ${body}`}
); @@ -103,39 +122,39 @@ function MessageReply({ name, color, content }) { MessageReply.propTypes = { name: PropTypes.string.isRequired, color: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, + body: PropTypes.string.isRequired, }; -function MessageContent({ +function MessageBody({ senderName, - content, - isMarkdown, + body, + isCustomHTML, isEdited, msgType, }) { return ( -
+
{ msgType === 'm.emote' && `* ${senderName} ` } - { isMarkdown ? genMarkdown(content) : linkifyContent(content) } + { isCustomHTML ? genMarkdown(body) : linkifyContent(body) }
- { isEdited && (edited)} + { isEdited && (edited)}
); } -MessageContent.defaultProps = { - isMarkdown: false, +MessageBody.defaultProps = { + isCustomHTML: false, isEdited: false, }; -MessageContent.propTypes = { +MessageBody.propTypes = { senderName: PropTypes.string.isRequired, - content: PropTypes.node.isRequired, - isMarkdown: PropTypes.bool, + body: PropTypes.node.isRequired, + isCustomHTML: PropTypes.bool, isEdited: PropTypes.bool, msgType: PropTypes.string.isRequired, }; -function MessageEdit({ content, onSave, onCancel }) { +function MessageEdit({ body, onSave, onCancel }) { const editInputRef = useRef(null); const handleKeyDown = (e) => { @@ -150,7 +169,7 @@ function MessageEdit({ content, onSave, onCancel }) { Malformed event; + + let mediaMXC = mContent?.url; + const isEncryptedFile = typeof mediaMXC === 'undefined'; + if (isEncryptedFile) mediaMXC = mContent?.file?.url; + + let thumbnailMXC = mContent?.info?.thumbnail_url; + + if (typeof mediaMXC === 'undefined' || mediaMXC === '') return Malformed event; + + let msgType = mE.getContent()?.msgtype; + if (mE.getType() === 'm.sticker') msgType = 'm.image'; + switch (msgType) { - case 'm.text': - className.push('message--type-text'); - break; - case 'm.emote': - className.push('message--type-emote'); - break; - case 'm.notice': - className.push('message--type-notice'); - break; + case 'm.file': + return ( + + ); + case 'm.image': + return ( + + ); + case 'm.audio': + return ( + + ); + case 'm.video': + if (typeof thumbnailMXC === 'undefined') { + thumbnailMXC = mContent.info?.thumbnail_file?.url || null; + } + return ( + + ); default: + 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 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(eventId, editedTimeline) { + const editedList = editedTimeline.get(eventId); + const editedMEvent = editedList[editedList.length - 1]; + const newContent = editedMEvent.getContent()['m.new_content']; + if (typeof newContent === 'undefined') return [null, false]; + + const isCustomHTML = newContent.format === 'org.matrix.custom.html'; + const parsedContent = parseReply(newContent.body); + if (parsedContent === null) { + return [newContent.body, isCustomHTML]; + } + return [parsedContent.body, isCustomHTML]; +} + +function Message({ mEvent, isBodyOnly, roomTimeline }) { + const [isEditing, setIsEditing] = useState(false); + + const mx = initMatrix.matrixClient; + const { + room, roomId, editedTimeline, reactionTimeline, + } = roomTimeline; + + const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')]; + const content = mEvent.getWireContent(); + const eventId = mEvent.getId(); + const msgType = content?.msgtype; + const senderId = mEvent.getSender(); + const mxidColor = colorMXID(senderId); + let { body } = content; + const avatarSrc = mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop'); + const username = getUsernameOfRoomMember(mEvent.sender); + const time = `${dateFormat(mEvent.getDate(), 'hh:MM TT')}`; + + if (typeof body === 'undefined') return null; + if (msgType === 'm.emote') className.push('message--type-emote'); + + // TODO: these line can be moved to option menu + 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']; + const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)]; + const isReply = typeof content['m.relates_to']?.['m.in_reply_to'] !== 'undefined'; + + if (isEdited) { + [body, isCustomHTML] = getEditedBody(eventId, editedTimeline); + 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) { + 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; + } } return (
- {avatar !== null && avatar} + {!isBodyOnly && ( + + )}
- {header !== null && header} - {reply !== null && reply} - {content !== null && content} - {editContent !== null && editContent} - {reactions !== null && reactions} - {options !== null && options} + {!isBodyOnly && ( + + )} + {reply !== null && ( + + )} + {!isEditing && ( + + )} + {isEditing && ( + { + if (newBody !== body) { + initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody); + } + setIsEditing(false); + }} + onCancel={() => setIsEditing(false)} + /> + )} + {haveReactions && ( + + { + reactions.map((reaction) => ( + { + toggleEmoji(roomId, eventId, reaction.key, roomTimeline); + }} + /> + )) + } + { + pickEmoji(e, roomId, eventId, roomTimeline); + }} + src={EmojiAddIC} + size="extra-small" + tooltip="Add reaction" + /> + + )} + {!isEditing && ( + + pickEmoji(e, roomId, eventId, roomTimeline)} + src={EmojiAddIC} + size="extra-small" + tooltip="Add reaction" + /> + replyTo(senderId, eventId, body)} + src={ReplyArrowIC} + size="extra-small" + tooltip="Reply" + /> + {(senderId === mx.getUserId() && !isMedia(mEvent)) && ( + setIsEditing(true)} + src={PencilIC} + size="extra-small" + tooltip="Edit" + /> + )} + ( + <> + Options + openReadReceipts(roomId, eventId)} + > + Read receipts + + {(canIRedact || senderId === mx.getUserId()) && ( + <> + + { + if (window.confirm('Are you sure you want to delete this event')) { + redactEvent(roomId, eventId); + } + }} + > + Delete + + + )} + + )} + render={(toggleMenu) => ( + + )} + /> + + )}
); } Message.defaultProps = { - avatar: null, - header: null, - reply: null, - content: null, - editContent: null, - reactions: null, - options: null, - msgType: 'm.text', + isBodyOnly: false, }; Message.propTypes = { - avatar: PropTypes.node, - header: PropTypes.node, - reply: PropTypes.node, - content: PropTypes.node, - editContent: PropTypes.node, - reactions: PropTypes.node, - options: PropTypes.node, - msgType: PropTypes.string, + mEvent: PropTypes.shape({}).isRequired, + isBodyOnly: PropTypes.bool, + roomTimeline: PropTypes.shape({}).isRequired, }; -export { - Message, - MessageHeader, - MessageReply, - MessageContent, - MessageEdit, - MessageReactionGroup, - MessageReaction, - MessageOptions, - PlaceholderMessage, -}; +export { Message, MessageReply, PlaceholderMessage }; diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss index f523ed6..3c12c92 100644 --- a/src/app/molecules/message/Message.scss +++ b/src/app/molecules/message/Message.scss @@ -47,7 +47,7 @@ .message { &--full + &--full, - &--content-only + &--full, + &--body-only + &--full, & + .timeline-change, .timeline-change + & { margin-top: var(--sp-normal); @@ -66,7 +66,7 @@ } &__header, - &__content > div { + &__body > div { margin: var(--sp-ultra-tight) 0; margin-right: var(--sp-extra-tight); height: var(--fs-b1); @@ -82,20 +82,20 @@ } } } - &__content { + &__body { display: flex; flex-wrap: wrap; } - &__content > div:nth-child(1n) { + &__body > div:nth-child(1n) { max-width: 10%; } - &__content > div:nth-child(2n) { + &__body > div:nth-child(2n) { max-width: 50%; } } .message__reply, -.message__content, +.message__body, .message__edit, .message__reactions { max-width: calc(100% - 88px); @@ -153,7 +153,7 @@ height: 14px; } } -.message__content { +.message__body { word-break: break-word; & > .text > * { @@ -265,7 +265,7 @@ } // markdown formating -.message__content { +.message__body { & h1, & h2 { color: var(--tc-surface-high); @@ -403,7 +403,7 @@ } .message.message--type-emote { - .message__content { + .message__body { font-style: italic; // Remove blockness of first `

` so that markdown emotes stay on one line. diff --git a/src/app/molecules/message/TimelineChange.jsx b/src/app/molecules/message/TimelineChange.jsx index 3ebc910..2ef28c1 100644 --- a/src/app/molecules/message/TimelineChange.jsx +++ b/src/app/molecules/message/TimelineChange.jsx @@ -12,7 +12,9 @@ import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-canc import UserIC from '../../../../public/res/ic/outlined/user.svg'; import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; -function TimelineChange({ variant, content, time, onClick }) { +function TimelineChange({ + variant, content, time, onClick, +}) { let iconSrc; switch (variant) { @@ -47,7 +49,6 @@ function TimelineChange({ variant, content, time, onClick }) {

{content} - {/* {content} */}
diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index 51d69b0..7478327 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -7,39 +7,14 @@ import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; -import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; -import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; -import colorMXID from '../../../util/colorMXID'; -import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common'; -import { openEmojiBoard, openProfileViewer, openReadReceipts } from '../../../client/action/navigation'; +import { diffMinutes, isNotInSameDay } from '../../../util/common'; import Divider from '../../atoms/divider/Divider'; -import Avatar from '../../atoms/avatar/Avatar'; -import IconButton from '../../atoms/button/IconButton'; -import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu'; -import { - Message, - MessageHeader, - MessageReply, - MessageContent, - MessageEdit, - MessageReactionGroup, - MessageReaction, - MessageOptions, - PlaceholderMessage, -} from '../../molecules/message/Message'; -import * as Media from '../../molecules/media/Media'; +import { Message, PlaceholderMessage } from '../../molecules/message/Message'; import RoomIntro from '../../molecules/room-intro/RoomIntro'; import TimelineChange from '../../molecules/message/TimelineChange'; -import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; -import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg'; -import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; -import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; -import BinIC from '../../../../public/res/ic/outlined/bin.svg'; - -import { parseReply, parseTimelineChange } from './common'; +import { parseTimelineChange } from './common'; const MAX_MSG_DIFF_MINUTES = 5; @@ -52,84 +27,6 @@ function genPlaceholders(key) { ); } -function isMedia(mE) { - return ( - mE.getContent()?.msgtype === 'm.file' - || mE.getContent()?.msgtype === 'm.image' - || mE.getContent()?.msgtype === 'm.audio' - || mE.getContent()?.msgtype === 'm.video' - || mE.getType() === 'm.sticker' - ); -} - -function genMediaContent(mE) { - const mx = initMatrix.matrixClient; - const mContent = mE.getContent(); - if (!mContent || !mContent.body) return Malformed event; - - let mediaMXC = mContent?.url; - const isEncryptedFile = typeof mediaMXC === 'undefined'; - if (isEncryptedFile) mediaMXC = mContent?.file?.url; - - let thumbnailMXC = mContent?.info?.thumbnail_url; - - if (typeof mediaMXC === 'undefined' || mediaMXC === '') return Malformed event; - - let msgType = mE.getContent()?.msgtype; - if (mE.getType() === 'm.sticker') msgType = 'm.image'; - - switch (msgType) { - case 'm.file': - return ( - - ); - case 'm.image': - return ( - - ); - case 'm.audio': - return ( - - ); - case 'm.video': - if (typeof thumbnailMXC === 'undefined') { - thumbnailMXC = mContent.info?.thumbnail_file?.url || null; - } - return ( - - ); - default: - return Malformed event; - } -} - function genRoomIntro(mEvent, roomTimeline) { const mx = initMatrix.matrixClient; const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; @@ -149,38 +46,6 @@ function genRoomIntro(mEvent, roomTimeline) { ); } -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(); - }); -} - const scroll = { from: 0, limit: 0, @@ -194,9 +59,10 @@ function RoomViewContent({ }) { const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false); const [onStateUpdate, updateState] = useState(null); - const [editEvent, setEditEvent] = useState(null); + const mx = initMatrix.matrixClient; const noti = initMatrix.notifications; + if (scroll.limit === 0) { const from = roomTimeline.timeline.size - timelineScroll.maxEvents; scroll.from = (from < 0) ? 0 : from; @@ -259,7 +125,7 @@ function RoomViewContent({ if (roomTimeline.ongoingDecryptionCount === 0) updateState({}); } else setIsReachedTimelineEnd(true); }; - // force update RoomTimeline on cons.events.roomTimeline.EVENT + // force update RoomTimeline const updateRT = () => { if (timelineScroll.position === 'BOTTOM') { trySendingReadReceipt(); @@ -323,291 +189,37 @@ function RoomViewContent({ }, [onStateUpdate]); let prevMEvent = null; - function genMessage(mEvent) { - const myPowerlevel = roomTimeline.room.getMember(mx.getUserId())?.powerLevel; - const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel); - - const isContentOnly = ( - prevMEvent !== null - && prevMEvent.getType() !== 'm.room.member' + function renderMessage(mEvent) { + const isContentOnly = (prevMEvent !== null && prevMEvent.getType() !== 'm.room.member' && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES && prevMEvent.getSender() === mEvent.getSender() ); - let content = mEvent.getContent().body; - if (typeof content === 'undefined') return null; - const msgType = mEvent.getContent()?.msgtype; - let reply = null; - let reactions = null; - let isMarkdown = mEvent.getContent().format === 'org.matrix.custom.html'; - const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined'; - const isEdited = roomTimeline.editedTimeline.has(mEvent.getId()); - const haveReactions = roomTimeline.reactionTimeline.has(mEvent.getId()); - - if (isReply) { - const parsedContent = parseReply(content); - if (parsedContent !== null) { - const c = roomTimeline.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), - content: parsedContent.replyContent, - }; - content = parsedContent.content; - } - } - - if (isEdited) { - const editedList = roomTimeline.editedTimeline.get(mEvent.getId()); - const latestEdited = editedList[editedList.length - 1]; - if (typeof latestEdited.getContent()['m.new_content'] === 'undefined') return null; - const latestEditBody = latestEdited.getContent()['m.new_content'].body; - const parsedEditedContent = parseReply(latestEditBody); - isMarkdown = latestEdited.getContent()['m.new_content'].format === 'org.matrix.custom.html'; - if (parsedEditedContent === null) { - content = latestEditBody; - } else { - content = parsedEditedContent.content; - } - } - - if (haveReactions) { - reactions = []; - roomTimeline.reactionTimeline.get(mEvent.getId()).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 = initMatrix.matrixClient.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() === initMatrix.matrixClient.getUserId()), - }); - } - }); - } - - const senderMXIDColor = colorMXID(mEvent.sender.userId); - const userAvatar = isContentOnly ? null : ( - - ); - const userHeader = isContentOnly ? null : ( - - ); - const userReply = reply === null ? null : ( - - ); - const userContent = ( - - ); - const userReactions = reactions === null ? null : ( - - { - reactions.map((reaction) => ( - { - toggleEmoji(roomId, mEvent.getId(), reaction.key, roomTimeline); - }} - /> - )) - } - pickEmoji(e, roomId, mEvent.getId(), roomTimeline)} - src={EmojiAddIC} - size="extra-small" - tooltip="Add reaction" - /> - - ); - const userOptions = ( - - pickEmoji(e, roomId, mEvent.getId(), roomTimeline)} - src={EmojiAddIC} - size="extra-small" - tooltip="Add reaction" - /> - { - viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content); - }} - src={ReplyArrowIC} - size="extra-small" - tooltip="Reply" - /> - {(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && ( - setEditEvent(mEvent)} - src={PencilIC} - size="extra-small" - tooltip="Edit" - /> - )} - ( - <> - Options - pickEmoji(e, roomId, mEvent.getId(), roomTimeline)} - > - Add reaction - - { - viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content); - }} - > - Reply - - {(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && ( - setEditEvent(mEvent)}>Edit - )} - openReadReceipts(roomId, mEvent.getId())} - > - Read receipts - - {(canIRedact || mEvent.getSender() === mx.getUserId()) && ( - <> - - { - if (window.confirm('Are you sure you want to delete this event')) { - redactEvent(roomId, mEvent.getId()); - } - }} - > - Delete - - - )} - - )} - render={(toggleMenu) => ( - - )} - /> - - ); - - const isEditingEvent = editEvent?.getId() === mEvent.getId(); - const myMessageEl = ( - { - if (newBody !== content) { - initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody); - } - setEditEvent(null); - }} - onCancel={() => setEditEvent(null)} - /> - ) : null} - reactions={userReactions} - options={editEvent !== null && isEditingEvent ? null : userOptions} - /> - ); - return myMessageEl; - } - - function renderMessage(mEvent) { - if (!cons.supportEventTypes.includes(mEvent.getType())) return false; - if (mEvent.getRelation()?.rel_type === 'm.replace') return false; - if (mEvent.isRedacted()) return false; - - if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline); - - let divider = null; + let DividerComp = null; if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) { - divider = ; + DividerComp = ; } + prevMEvent = mEvent; - if (mEvent.getType() !== 'm.room.member') { - const messageComp = genMessage(mEvent); - prevMEvent = mEvent; + if (mEvent.getType() === 'm.room.member') { + const timelineChange = parseTimelineChange(mEvent); + if (timelineChange === null) return false; return ( - {divider} - {messageComp} + {DividerComp} + ); } - - prevMEvent = mEvent; - const timelineChange = parseTimelineChange(mEvent); - if (timelineChange === null) return false; return ( - {divider} - + {DividerComp} + ); } @@ -625,7 +237,8 @@ function RoomViewContent({ if (mEvent.getType() !== 'm.room.create' && !isReachedTimelineEnd) tl.push(genPlaceholders(1)); if (mEvent.getType() !== 'm.room.create' && isReachedTimelineEnd) tl.push(genRoomIntro(undefined, roomTimeline)); } - tl.push(renderMessage(mEvent)); + if (mEvent.getType() === 'm.room.create') tl.push(genRoomIntro(mEvent, roomTimeline)); + else tl.push(renderMessage(mEvent)); } i += 1; if (i > scroll.getEndIndex()) break; diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx index 5b18fcb..0f1ca94 100644 --- a/src/app/organisms/room/RoomViewInput.jsx +++ b/src/app/organisms/room/RoomViewInput.jsx @@ -9,6 +9,7 @@ import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import { openEmojiBoard } from '../../../client/action/navigation'; +import navigation from '../../../client/state/navigation'; import { bytesToSize, getEventCords } from '../../../util/common'; import { getUsername } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; @@ -152,9 +153,9 @@ function RoomViewInput({ textAreaRef.current.focus(); } - function setUpReply(userId, eventId, content) { - setReplyTo({ userId, eventId, content }); - roomsInput.setReplyTo(roomId, { userId, eventId, content }); + function setUpReply(userId, eventId, body) { + setReplyTo({ userId, eventId, body }); + roomsInput.setReplyTo(roomId, { userId, eventId, body }); focusInput(); } @@ -164,7 +165,7 @@ function RoomViewInput({ roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment); viewEvent.on('cmd_error', errorCmd); viewEvent.on('cmd_fired', firedCmd); - viewEvent.on('reply_to', setUpReply); + navigation.on(cons.events.navigation.REPLY_TO_CLICKED, setUpReply); if (textAreaRef?.current !== null) { isTyping = false; focusInput(); @@ -178,7 +179,7 @@ function RoomViewInput({ roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment); viewEvent.removeListener('cmd_error', errorCmd); viewEvent.removeListener('cmd_fired', firedCmd); - viewEvent.removeListener('reply_to', setUpReply); + navigation.removeListener(cons.events.navigation.REPLY_TO_CLICKED, setUpReply); if (isCmdActivated) deactivateCmd(); if (textAreaRef?.current === null) return; @@ -410,7 +411,7 @@ function RoomViewInput({ userId={replyTo.userId} name={getUsername(replyTo.userId)} color={colorMXID(replyTo.userId)} - content={replyTo.content} + body={replyTo.body} />
); diff --git a/src/app/organisms/room/common.jsx b/src/app/organisms/room/common.jsx index e25eabb..05f5d50 100644 --- a/src/app/organisms/room/common.jsx +++ b/src/app/organisms/room/common.jsx @@ -165,27 +165,6 @@ function getUsersActionJsx(roomId, userIds, actionStr) { return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} other are {actionStr}; } -function parseReply(rawContent) { - if (rawContent.indexOf('>') !== 0) return null; - let content = rawContent.slice(rawContent.indexOf('<') + 1); - const user = content.slice(0, content.indexOf('>')); - - content = content.slice(content.indexOf('>') + 2); - const replyContent = content.slice(0, content.indexOf('\n\n')); - content = content.slice(content.indexOf('\n\n') + 2); - - if (user === '') return null; - - const isUserId = user.match(/^@.+:.+/); - - return { - userId: isUserId ? user : null, - displayName: isUserId ? null : user, - replyContent, - content, - }; -} - function parseTimelineChange(mEvent) { const tJSXMsgs = getTimelineJSXMessages(); const makeReturnObj = (variant, content) => ({ @@ -237,6 +216,5 @@ function parseTimelineChange(mEvent) { export { getTimelineJSXMessages, getUsersActionJsx, - parseReply, parseTimelineChange, }; diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 6617928..0f9f4c1 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -87,6 +87,15 @@ function openRoomOptions(cords, roomId) { }); } +function replyTo(userId, eventId, body) { + appDispatcher.dispatch({ + type: cons.actions.navigation.CLICK_REPLY_TO, + userId, + eventId, + body, + }); +} + export { selectTab, selectSpace, @@ -100,4 +109,5 @@ export { openEmojiBoard, openReadReceipts, openRoomOptions, + replyTo, }; diff --git a/src/client/state/RoomsInput.js b/src/client/state/RoomsInput.js index d1100cd..a8ee805 100644 --- a/src/client/state/RoomsInput.js +++ b/src/client/state/RoomsInput.js @@ -95,13 +95,13 @@ function getFormattedBody(markdown) { function getReplyFormattedBody(roomId, reply) { const replyToLink = `In reply to`; const userLink = `${reply.userId}`; - const formattedReply = getFormattedBody(reply.content.replaceAll('\n', '\n> ')); + const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> ')); return `
${replyToLink}${userLink}
${formattedReply}
`; } function bindReplyToContent(roomId, reply, content) { const newContent = { ...content }; - newContent.body = `> <${reply.userId}> ${reply.content.replaceAll('\n', '\n> ')}`; + newContent.body = `> <${reply.userId}> ${reply.body.replaceAll('\n', '\n> ')}`; newContent.body += `\n\n${content.body}`; newContent.format = 'org.matrix.custom.html'; newContent['m.relates_to'] = content['m.relates_to'] || {}; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 412cbb7..6443df0 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -34,6 +34,7 @@ const cons = { OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD', OPEN_READRECEIPTS: 'OPEN_READRECEIPTS', OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS', + CLICK_REPLY_TO: 'CLICK_REPLY_TO', }, room: { JOIN: 'JOIN', @@ -65,6 +66,7 @@ const cons = { EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED', READRECEIPTS_OPENED: 'READRECEIPTS_OPENED', ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED', + REPLY_TO_CLICKED: 'REPLY_TO_CLICKED', }, roomList: { ROOMLIST_UPDATED: 'ROOMLIST_UPDATED', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 76cecad..d52d96e 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -90,6 +90,14 @@ class Navigation extends EventEmitter { action.roomId, ); }, + [cons.actions.navigation.CLICK_REPLY_TO]: () => { + this.emit( + cons.events.navigation.REPLY_TO_CLICKED, + action.userId, + action.eventId, + action.body, + ); + }, }; actions[action.type]?.(); }