refactored message compnonent
authorunknown <ajbura@gmail.com>
Tue, 10 Aug 2021 11:28:44 +0000 (16:58 +0530)
committerunknown <ajbura@gmail.com>
Tue, 10 Aug 2021 11:28:44 +0000 (16:58 +0530)
src/app/molecules/message/Message.jsx
src/app/molecules/message/Message.scss
src/app/organisms/channel/ChannelViewContent.jsx

index ad32b0cd221e17a38049802e7c05256b2c9c4844..5d6b575e7c9077914b024a1908741c9b5e07cdc7 100644 (file)
@@ -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 MessageHeader({
+  userId, name, color, time,
+}) {
+  return (
+    <div className="message__header">
+      <div style={{ color }} className="message__profile">
+        <Text variant="b1">{name}</Text>
+      </div>
+      <div className="message__time">
+        <Text variant="b3">{time}</Text>
+      </div>
+    </div>
+  );
+}
+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 (
+    <div className="message__reply">
+      <Text variant="b2">
+        <RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
+        <span style={{ color }}>{name}</span>
+        <>{` ${content}`}</>
+      </Text>
+    </div>
+  );
+}
+
+MessageReply.propTypes = {
+  userId: PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  color: PropTypes.string.isRequired,
+  content: PropTypes.string.isRequired,
+};
+
+function MessageContent({ content, isMarkdown, isEdited }) {
+  return (
+    <div className="message__content">
+      <div className="text text-b1">
+        { isMarkdown ? genMarkdown(content) : linkifyContent(content) }
+      </div>
+      { isEdited && <Text className="message__edited" variant="b3">(edited)</Text>}
+    </div>
+  );
+}
+MessageContent.defaultProps = {
+  isMarkdown: false,
+  isEdited: false,
+};
+MessageContent.propTypes = {
+  content: PropTypes.node.isRequired,
+  isMarkdown: PropTypes.bool,
+  isEdited: PropTypes.bool,
+};
+
+function MessageReactionGroup({ children }) {
+  return (
+    <div className="message__reactions text text-b3 noselect">
+      { children }
+    </div>
+  );
+}
+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 (
+    <Tooltip
+      className="msg__reaction-tooltip"
+      content={<Text variant="b2">{genReactionMsg(users, reaction)}</Text>}
+    >
+      <button
+        onClick={onClick}
+        type="button"
+        className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
+      >
+        { parse(twemoji.parse(reaction)) }
+        <Text variant="b3" className="msg__reaction-count">{users.length}</Text>
+      </button>
+    </Tooltip>
+  );
+}
+MessageReaction.propTypes = {
+  reaction: PropTypes.node.isRequired,
+  users: PropTypes.arrayOf(PropTypes.string).isRequired,
+  isActive: PropTypes.bool.isRequired,
+  onClick: PropTypes.func.isRequired,
+};
+
 function Message({
-  color, avatarSrc, name, content,
-  time, markdown, contentOnly, reply,
-  edited, reactions,
+  avatar, header, reply, content, reactions,
 }) {
-  const msgClass = contentOnly ? 'message--content-only' : 'message--full';
+  const msgClass = header === null ? ' message--content-only' : ' message--full';
   return (
-    <div className={`message ${msgClass}`}>
+    <div className={`message${msgClass}`}>
       <div className="message__avatar-container">
-        {!contentOnly && <Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="small" />}
+        {avatar !== null && avatar}
       </div>
       <div className="message__main-container">
-        { !contentOnly && (
-          <div className="message__header">
-            <div style={{ color }} className="message__profile">
-              <Text variant="b1">{name}</Text>
-            </div>
-            <div className="message__time">
-              <Text variant="b3">{time}</Text>
-            </div>
-          </div>
-        )}
-        <div className="message__content">
-          { reply !== null && (
-            <div className="message__reply-content">
-              <Text variant="b2">
-                <RawIcon color={reply.color} size="extra-small" src={ReplyArrowIC} />
-                <span style={{ color: reply.color }}>{reply.to}</span>
-                <>{` ${reply.content}`}</>
-              </Text>
-            </div>
-          )}
-          <div className="text text-b1">
-            { markdown ? genMarkdown(content) : linkifyContent(content) }
-          </div>
-          { edited && <Text className="message__edited" variant="b3">(edited)</Text>}
-          { reactions && (
-            <div className="message__reactions text text-b3 noselect">
-              {
-                reactions.map((reaction) => (
-                  <button key={reaction.id} onClick={() => alert('Sending reactions is yet to be implemented.')} type="button" className={`msg__reaction${reaction.active ? ' msg__reaction--active' : ''}`}>
-                    {`${reaction.key} ${reaction.count}`}
-                  </button>
-                ))
-              }
-            </div>
-          )}
-        </div>
+        {header !== null && header}
+        {reply !== null && reply}
+        {content}
+        {reactions !== null && reactions}
       </div>
     </div>
   );
 }
-
 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,
+};
index a1c7bbc52f21e70f5fb0f507736ee077309e75bd..f8a4108dacbe2205d2798c96a7a8dd7f3fc4cd75 100644 (file)
   &__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 {
   }
 }
 
+.message__reply,
+.message__content,
+.message__reactions {
+  max-width: 640px;
+}
+
+
 .message__header {
   display: flex;
   align-items: baseline;
     }
   }
 }
+.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 > * {
     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: {
 }
 
 // markdown formating
-.message {
+.message__content {
   & h1,
   & h2 {
     color: var(--tc-surface-high);
index 2fdf1e241ce4e1d60e907e0f64b28cb5ab56dfac..4476b43f737144d10ec7a6b6a47743a119efb084 100644 (file)
@@ -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 : (
+        <Avatar
+          imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
+          text={getUsername(mEvent.sender.userId).slice(0, 1)}
+          bgColor={userMXIDColor}
+          size="small"
+        />
+      );
+      const userHeader = isContentOnly ? null : (
+        <MessageHeader
+          userId={mEvent.sender.userId}
+          name={getUsername(mEvent.sender.userId)}
+          color={userMXIDColor}
+          time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
+        />
+      );
+      const userReply = reply === null ? null : (
+        <MessageReply
+          userId={reply.userId}
+          name={reply.to}
+          color={reply.color}
+          content={reply.content}
+        />
+      );
+      const userContent = (
+        <MessageContent
+          isMarkdown={isMarkdown}
+          content={isMedia(mEvent) ? genMediaContent(mEvent) : content}
+          isEdited={isEdited}
+        />
+      );
+      const userReactions = reactions === null ? null : (
+        <MessageReactionGroup>
+          {
+            reactions.map((reaction) => (
+              <MessageReaction
+                key={reaction.id}
+                reaction={reaction.key}
+                users={reaction.users}
+                isActive={reaction.isActive}
+                onClick={() => alert('Sending reactions is yet to be implemented.')}
+              />
+            ))
+          }
+        </MessageReactionGroup>
+      );
+
       const myMessageEl = (
-        <React.Fragment key={`box-${mEvent.getId()}`}>
-          {divider}
-          { isMedia(mEvent) ? (
-            <Message
-              key={mEvent.getId()}
-              contentOnly={isContentOnly}
-              markdown={isMarkdown}
-              avatarSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
-              color={colorMXID(mEvent.sender.userId)}
-              name={getUsername(mEvent.sender.userId)}
-              content={genMediaContent(mEvent)}
-              reply={reply}
-              time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
-              edited={isEdited}
-              reactions={reactions}
-            />
-          ) : (
-            <Message
-              key={mEvent.getId()}
-              contentOnly={isContentOnly}
-              markdown={isMarkdown}
-              avatarSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
-              color={colorMXID(mEvent.sender.userId)}
-              name={getUsername(mEvent.sender.userId)}
-              content={content}
-              reply={reply}
-              time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
-              edited={isEdited}
-              reactions={reactions}
-            />
-          )}
-        </React.Fragment>
+        <Message
+          key={mEvent.getId()}
+          avatar={userAvatar}
+          header={userHeader}
+          reply={userReply}
+          content={userContent}
+          reactions={userReactions}
+        />
       );
 
       prevMEvent = mEvent;