diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index ad32b0cd..5d6b575e 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -7,10 +7,14 @@ import ReactMarkdown from 'react-markdown'; import gfm from 'remark-gfm'; 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 Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import Avatar from '../../atoms/avatar/Avatar'; +import Tooltip from '../../atoms/tooltip/Tooltip'; import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; @@ -61,89 +65,158 @@ function PlaceholderMessage() { ); } -function Message({ - color, avatarSrc, name, content, - time, markdown, contentOnly, reply, - edited, reactions, +function MessageHeader({ + userId, name, color, time, }) { - const msgClass = contentOnly ? 'message--content-only' : 'message--full'; return ( -
-
- {!contentOnly && } +
+
+ {name}
-
- { !contentOnly && ( -
-
- {name} -
-
- {time} -
-
- )} -
- { reply !== null && ( -
- - - {reply.to} - <>{` ${reply.content}`} - -
- )} -
- { markdown ? genMarkdown(content) : linkifyContent(content) } -
- { edited && (edited)} - { reactions && ( -
- { - reactions.map((reaction) => ( - - )) - } -
- )} -
+
+ {time}
); } +MessageHeader.propTypes = { + userId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + time: PropTypes.string.isRequired, +}; +function MessageReply({ + userId, name, color, content, +}) { + return ( +
+ + + {name} + <>{` ${content}`} + +
+ ); +} + +MessageReply.propTypes = { + userId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, +}; + +function MessageContent({ content, isMarkdown, isEdited }) { + return ( +
+
+ { isMarkdown ? genMarkdown(content) : linkifyContent(content) } +
+ { isEdited && (edited)} +
+ ); +} +MessageContent.defaultProps = { + isMarkdown: false, + isEdited: false, +}; +MessageContent.propTypes = { + content: PropTypes.node.isRequired, + isMarkdown: PropTypes.bool, + isEdited: PropTypes.bool, +}; + +function MessageReactionGroup({ children }) { + return ( +
+ { children } +
+ ); +} +MessageReactionGroup.propTypes = { + children: PropTypes.node.isRequired, +}; + +function genReactionMsg(userIds, reaction) { + let msg = ''; + userIds.forEach((userId, index) => { + if (index === 0) msg += getUsername(userId); + else if (index === userIds.length - 1) msg += ` and ${getUsername(userId)}`; + else msg += `, ${getUsername(userId)}`; + }); + return ( + <> + {`${msg} reacted with`} + {parse(twemoji.parse(reaction))} + + ); +} + +function MessageReaction({ + reaction, users, isActive, onClick, +}) { + return ( + {genReactionMsg(users, reaction)}} + > + + + ); +} +MessageReaction.propTypes = { + reaction: PropTypes.node.isRequired, + users: PropTypes.arrayOf(PropTypes.string).isRequired, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, +}; + +function Message({ + avatar, header, reply, content, reactions, +}) { + const msgClass = header === null ? ' message--content-only' : ' message--full'; + return ( +
+
+ {avatar !== null && avatar} +
+
+ {header !== null && header} + {reply !== null && reply} + {content} + {reactions !== null && reactions} +
+
+ ); +} Message.defaultProps = { - color: 'var(--tc-surface-high)', - avatarSrc: null, - markdown: false, - contentOnly: false, + avatar: null, + header: null, reply: null, - edited: false, reactions: null, }; - Message.propTypes = { - color: PropTypes.string, - avatarSrc: PropTypes.string, - name: PropTypes.string.isRequired, + avatar: PropTypes.node, + header: PropTypes.node, + reply: PropTypes.node, content: PropTypes.node.isRequired, - time: PropTypes.string.isRequired, - markdown: PropTypes.bool, - contentOnly: PropTypes.bool, - reply: PropTypes.shape({ - color: PropTypes.string.isRequired, - to: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - }), - edited: PropTypes.bool, - reactions: PropTypes.arrayOf(PropTypes.exact({ - id: PropTypes.string, - key: PropTypes.string, - count: PropTypes.number, - active: PropTypes.bool, - })), + reactions: PropTypes.node, }; -export { Message as default, PlaceholderMessage }; +export { + Message, + MessageHeader, + MessageReply, + MessageContent, + MessageReactionGroup, + MessageReaction, + PlaceholderMessage, +}; diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss index a1c7bbc5..f8a4108d 100644 --- a/src/app/molecules/message/Message.scss +++ b/src/app/molecules/message/Message.scss @@ -49,24 +49,9 @@ &__avatar-container { width: var(--av-small); } - &__reply-content { - .text { - color: var(--tc-surface-low); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .ic-raw { - width: 16px; - height: 14px; - } - } &__edited { color: var(--tc-surface-low); } - &__reactions { - margin-top: var(--sp-ultra-tight); - } } .ph-msg { @@ -106,6 +91,13 @@ } } +.message__reply, +.message__content, +.message__reactions { + max-width: 640px; +} + + .message__header { display: flex; align-items: baseline; @@ -130,8 +122,19 @@ } } } +.message__reply { + .text { + color: var(--tc-surface-low); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .ic-raw { + width: 16px; + height: 14px; + } +} .message__content { - max-width: 640px; word-break: break-word; & > .text > * { @@ -142,20 +145,36 @@ word-break: break-all; } } +.message__reactions { + display: flex; +} .msg__reaction { - --reaction-height: 24px; - --reaction-padding: 6px; - --reaction-radius: calc(var(--bo-radius) / 2); + margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0; + padding: 0 var(--sp-ultra-tight); + min-height: 26px; display: inline-flex; align-items: center; color: var(--tc-surface-normal); + background-color: var(--bg-surface-low); border: 1px solid var(--bg-surface-border); - padding: 0 var(--reaction-padding); - border-radius: var(--reaction-radius); + border-radius: 4px; cursor: pointer; - height: var(--reaction-height); - margin-right: var(--sp-extra-tight); + & .emoji { + width: 14px; + height: 14px; + margin: 2px; + } + &-count { + margin: 0 var(--sp-ultra-tight); + color: var(--tc-surface-normal) + } + &-tooltip .emoji { + width: 14px; + height: 14px; + margin: 0 var(--sp-ultra-tight); + margin-bottom: -2px; + } [dir=rtl] & { margin: { @@ -188,7 +207,7 @@ } // markdown formating -.message { +.message__content { & h1, & h2 { color: var(--tc-surface-high); diff --git a/src/app/organisms/channel/ChannelViewContent.jsx b/src/app/organisms/channel/ChannelViewContent.jsx index 2fdf1e24..4476b43f 100644 --- a/src/app/organisms/channel/ChannelViewContent.jsx +++ b/src/app/organisms/channel/ChannelViewContent.jsx @@ -12,7 +12,16 @@ import colorMXID from '../../../util/colorMXID'; import { diffMinutes, isNotInSameDay } from '../../../util/common'; import Divider from '../../atoms/divider/Divider'; -import Message, { PlaceholderMessage } from '../../molecules/message/Message'; +import Avatar from '../../atoms/avatar/Avatar'; +import { + Message, + MessageHeader, + MessageReply, + MessageContent, + MessageReactionGroup, + MessageReaction, + PlaceholderMessage, +} from '../../molecules/message/Message'; import * as Media from '../../molecules/media/Media'; import ChannelIntro from '../../molecules/channel-intro/ChannelIntro'; import TimelineChange from '../../molecules/message/TimelineChange'; @@ -224,6 +233,7 @@ function ChannelViewContent({ if (parsedContent !== null) { const username = getUsername(parsedContent.userId); reply = { + userId: parsedContent.userId, color: colorMXID(parsedContent.userId), to: username, content: parsedContent.replyContent, @@ -259,9 +269,10 @@ function ChannelViewContent({ if (alreadyHaveThisReaction(rEvent)) { for (let i = 0; i < reactions.length; i += 1) { if (reactions[i].key === rEvent.getRelation().key) { - reactions[i].count += 1; - if (reactions[i].active !== true) { - reactions[i].active = rEvent.getSender() === initMatrix.matrixClient.getUserId(); + reactions[i].users.push(rEvent.getSender()); + if (reactions[i].isActive !== true) { + const myUserId = initMatrix.matrixClient.getUserId(); + reactions[i].isActive = rEvent.getSender() === myUserId; } break; } @@ -270,46 +281,70 @@ function ChannelViewContent({ reactions.push({ id: rEvent.getId(), key: rEvent.getRelation().key, - count: 1, - active: (rEvent.getSender() === initMatrix.matrixClient.getUserId()), + users: [rEvent.getSender()], + isActive: (rEvent.getSender() === initMatrix.matrixClient.getUserId()), }); } }); } + const userMXIDColor = 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) => ( + alert('Sending reactions is yet to be implemented.')} + /> + )) + } + + ); + const myMessageEl = ( - - {divider} - { isMedia(mEvent) ? ( - - ) : ( - - )} - + ); prevMEvent = mEvent;