Refectored Message comp
authorAjay Bura <ajbura@gmail.com>
Sat, 20 Nov 2021 07:59:32 +0000 (13:29 +0530)
committerAjay Bura <ajbura@gmail.com>
Sat, 20 Nov 2021 07:59:32 +0000 (13:29 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/atoms/divider/Divider.jsx
src/app/molecules/message/Message.jsx
src/app/molecules/message/Message.scss
src/app/molecules/message/TimelineChange.jsx
src/app/organisms/room/RoomViewContent.jsx
src/app/organisms/room/RoomViewInput.jsx
src/app/organisms/room/common.jsx
src/client/action/navigation.js
src/client/state/RoomsInput.js
src/client/state/cons.js
src/client/state/navigation.js

index 479fcec77f4719cf911b785a5377e72e33e7434c..b524ec2df6df405e2f6c3696240d7ad1fec8a2e4 100644 (file)
@@ -8,21 +8,18 @@ function Divider({ text, variant }) {
   const dividerClass = ` divider--${variant}`;
   return (
     <div className={`divider${dividerClass}`}>
-      {text !== false && <Text className="divider__text" variant="b3">{text}</Text>}
+      {text !== null && <Text className="divider__text" variant="b3">{text}</Text>}
     </div>
   );
 }
 
 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']),
 };
 
index 2baacfacd7ec1be8564723133323a4e277f3b3c4..60c00eb1c3facc6f73ecbf0a2e1c65ef59910d3a 100644 (file)
@@ -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() {
       </div>
       <div className="ph-msg__main-container">
         <div className="ph-msg__header" />
-        <div className="ph-msg__content">
+        <div className="ph-msg__body">
           <div />
           <div />
           <div />
@@ -88,13 +107,13 @@ MessageHeader.propTypes = {
   time: PropTypes.string.isRequired,
 };
 
-function MessageReply({ name, color, content }) {
+function MessageReply({ name, color, body }) {
   return (
     <div className="message__reply">
       <Text variant="b2">
         <RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
         <span style={{ color }}>{name}</span>
-        <>{` ${content}`}</>
+        <>{` ${body}`}</>
       </Text>
     </div>
   );
@@ -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 (
-    <div className="message__content">
+    <div className="message__body">
       <div className="text text-b1">
         { msgType === 'm.emote' && `* ${senderName} ` }
-        { isMarkdown ? genMarkdown(content) : linkifyContent(content) }
+        { isCustomHTML ? genMarkdown(body) : linkifyContent(body) }
       </div>
-      { isEdited && <Text className="message__content-edited" variant="b3">(edited)</Text>}
+      { isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
     </div>
   );
 }
-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 }) {
       <Input
         forwardRef={editInputRef}
         onKeyDown={handleKeyDown}
-        value={content}
+        value={body}
         placeholder="Edit message"
         required
         resizable
@@ -163,7 +182,7 @@ function MessageEdit({ content, onSave, onCancel }) {
   );
 }
 MessageEdit.propTypes = {
-  content: PropTypes.string.isRequired,
+  body: PropTypes.string.isRequired,
   onSave: PropTypes.func.isRequired,
   onCancel: PropTypes.func.isRequired,
 };
@@ -235,72 +254,366 @@ MessageOptions.propTypes = {
   children: PropTypes.node.isRequired,
 };
 
-function Message({
-  avatar, header, reply, content, editContent, reactions, options,
-  msgType,
-}) {
-  const className = [
-    'message',
-    header === null ? ' message--content-only' : ' message--full',
-  ];
+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 <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
+
+  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 <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
+
+  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 (
+        <Media.File
+          name={mContent.body}
+          link={mx.mxcUrlToHttp(mediaMXC)}
+          type={mContent.info?.mimetype}
+          file={mContent.file || null}
+        />
+      );
+    case 'm.image':
+      return (
+        <Media.Image
+          name={mContent.body}
+          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
+          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
+          link={mx.mxcUrlToHttp(mediaMXC)}
+          file={isEncryptedFile ? mContent.file : null}
+          type={mContent.info?.mimetype}
+        />
+      );
+    case 'm.audio':
+      return (
+        <Media.Audio
+          name={mContent.body}
+          link={mx.mxcUrlToHttp(mediaMXC)}
+          type={mContent.info?.mimetype}
+          file={mContent.file || null}
+        />
+      );
+    case 'm.video':
+      if (typeof thumbnailMXC === 'undefined') {
+        thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
+      }
+      return (
+        <Media.Video
+          name={mContent.body}
+          link={mx.mxcUrlToHttp(mediaMXC)}
+          thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
+          thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
+          thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
+          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
+          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
+          file={isEncryptedFile ? mContent.file : null}
+          type={mContent.info?.mimetype}
+        />
+      );
     default:
+      return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
+  }
+}
+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 (
     <div className={className.join(' ')}>
       <div className="message__avatar-container">
-        {avatar !== null && avatar}
+        {!isBodyOnly && (
+          <button type="button" onClick={() => openProfileViewer(senderId, roomId)}>
+            <Avatar imageSrc={avatarSrc} text={username} bgColor={mxidColor} size="small" />
+          </button>
+        )}
       </div>
       <div className="message__main-container">
-        {header !== null && header}
-        {reply !== null && reply}
-        {content !== null && content}
-        {editContent !== null && editContent}
-        {reactions !== null && reactions}
-        {options !== null && options}
+        {!isBodyOnly && (
+          <MessageHeader userId={senderId} name={username} color={mxidColor} time={time} />
+        )}
+        {reply !== null && (
+          <MessageReply name={reply.to} color={reply.color} body={reply.body} />
+        )}
+        {!isEditing && (
+          <MessageBody
+            senderName={username}
+            isCustomHTML={isCustomHTML}
+            body={isMedia(mEvent) ? genMediaContent(mEvent) : body}
+            msgType={msgType}
+            isEdited={isEdited}
+          />
+        )}
+        {isEditing && (
+          <MessageEdit
+            body={body}
+            onSave={(newBody) => {
+              if (newBody !== body) {
+                initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
+              }
+              setIsEditing(false);
+            }}
+            onCancel={() => setIsEditing(false)}
+          />
+        )}
+        {haveReactions && (
+          <MessageReactionGroup>
+            {
+              reactions.map((reaction) => (
+                <MessageReaction
+                  key={reaction.id}
+                  reaction={reaction.key}
+                  users={reaction.users}
+                  isActive={reaction.isActive}
+                  onClick={() => {
+                    toggleEmoji(roomId, eventId, reaction.key, roomTimeline);
+                  }}
+                />
+              ))
+            }
+            <IconButton
+              onClick={(e) => {
+                pickEmoji(e, roomId, eventId, roomTimeline);
+              }}
+              src={EmojiAddIC}
+              size="extra-small"
+              tooltip="Add reaction"
+            />
+          </MessageReactionGroup>
+        )}
+        {!isEditing && (
+          <MessageOptions>
+            <IconButton
+              onClick={(e) => pickEmoji(e, roomId, eventId, roomTimeline)}
+              src={EmojiAddIC}
+              size="extra-small"
+              tooltip="Add reaction"
+            />
+            <IconButton
+              onClick={() => replyTo(senderId, eventId, body)}
+              src={ReplyArrowIC}
+              size="extra-small"
+              tooltip="Reply"
+            />
+            {(senderId === mx.getUserId() && !isMedia(mEvent)) && (
+              <IconButton
+                onClick={() => setIsEditing(true)}
+                src={PencilIC}
+                size="extra-small"
+                tooltip="Edit"
+              />
+            )}
+            <ContextMenu
+              content={() => (
+                <>
+                  <MenuHeader>Options</MenuHeader>
+                  <MenuItem
+                    iconSrc={TickMarkIC}
+                    onClick={() => openReadReceipts(roomId, eventId)}
+                  >
+                    Read receipts
+                  </MenuItem>
+                  {(canIRedact || senderId === mx.getUserId()) && (
+                    <>
+                      <MenuBorder />
+                      <MenuItem
+                        variant="danger"
+                        iconSrc={BinIC}
+                        onClick={() => {
+                          if (window.confirm('Are you sure you want to delete this event')) {
+                            redactEvent(roomId, eventId);
+                          }
+                        }}
+                      >
+                        Delete
+                      </MenuItem>
+                    </>
+                  )}
+                </>
+              )}
+              render={(toggleMenu) => (
+                <IconButton
+                  onClick={toggleMenu}
+                  src={VerticalMenuIC}
+                  size="extra-small"
+                  tooltip="Options"
+                />
+              )}
+            />
+          </MessageOptions>
+        )}
       </div>
     </div>
   );
 }
 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 };
index f523ed61dd03337d3069fbe7d5e6a58d3c78be42..3c12c923a71c405a83c32ee9d71213058ad352f1 100644 (file)
@@ -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);
       }
     }
   }
-  &__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);
     height: 14px;
   }
 }
-.message__content {
+.message__body {
   word-break: break-word;
   
   & > .text > * {
 }
 
 // markdown formating
-.message__content {
+.message__body {
   & h1,
   & h2 {
     color: var(--tc-surface-high);
 }
 
 .message.message--type-emote {
-  .message__content {
+  .message__body {
     font-style: italic;
 
     // Remove blockness of first `<p>` so that markdown emotes stay on one line.
index 3ebc9103c9d8090463ad3b774f47d008e1f66039..2ef28c116a9b2a58c0678da2fe817163337a9af1 100644 (file)
@@ -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 }) {
       <div className="timeline-change__content">
         <Text variant="b2">
           {content}
-          {/* <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify> */}
         </Text>
       </div>
       <div className="timeline-change__time">
index 51d69b07fc96cb4caa1ab7f795a073dd8bc73303..74783279f2df5303680fcba839e057a51045893f 100644 (file)
@@ -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 <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-
-  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 <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-
-  let msgType = mE.getContent()?.msgtype;
-  if (mE.getType() === 'm.sticker') msgType = 'm.image';
-
-  switch (msgType) {
-    case 'm.file':
-      return (
-        <Media.File
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          type={mContent.info?.mimetype}
-          file={mContent.file || null}
-        />
-      );
-    case 'm.image':
-      return (
-        <Media.Image
-          name={mContent.body}
-          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
-          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          file={isEncryptedFile ? mContent.file : null}
-          type={mContent.info?.mimetype}
-        />
-      );
-    case 'm.audio':
-      return (
-        <Media.Audio
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          type={mContent.info?.mimetype}
-          file={mContent.file || null}
-        />
-      );
-    case 'm.video':
-      if (typeof thumbnailMXC === 'undefined') {
-        thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
-      }
-      return (
-        <Media.Video
-          name={mContent.body}
-          link={mx.mxcUrlToHttp(mediaMXC)}
-          thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
-          thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
-          thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
-          width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
-          height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
-          file={isEncryptedFile ? mContent.file : null}
-          type={mContent.info?.mimetype}
-        />
-      );
-    default:
-      return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
-  }
-}
-
 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 : (
-      <button type="button" onClick={() => openProfileViewer(mEvent.sender.userId, roomId)}>
-        <Avatar
-          imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
-          text={getUsernameOfRoomMember(mEvent.sender)}
-          bgColor={senderMXIDColor}
-          size="small"
-        />
-      </button>
-    );
-    const userHeader = isContentOnly ? null : (
-      <MessageHeader
-        userId={mEvent.sender.userId}
-        name={getUsernameOfRoomMember(mEvent.sender)}
-        color={senderMXIDColor}
-        time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
-      />
-    );
-    const userReply = reply === null ? null : (
-      <MessageReply
-        name={reply.to}
-        color={reply.color}
-        content={reply.content}
-      />
-    );
-    const userContent = (
-      <MessageContent
-        senderName={getUsernameOfRoomMember(mEvent.sender)}
-        isMarkdown={isMarkdown}
-        content={isMedia(mEvent) ? genMediaContent(mEvent) : content}
-        msgType={msgType}
-        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={() => {
-                toggleEmoji(roomId, mEvent.getId(), reaction.key, roomTimeline);
-              }}
-            />
-          ))
-        }
-        <IconButton
-          onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
-          src={EmojiAddIC}
-          size="extra-small"
-          tooltip="Add reaction"
-        />
-      </MessageReactionGroup>
-    );
-    const userOptions = (
-      <MessageOptions>
-        <IconButton
-          onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
-          src={EmojiAddIC}
-          size="extra-small"
-          tooltip="Add reaction"
-        />
-        <IconButton
-          onClick={() => {
-            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)) && (
-          <IconButton
-            onClick={() => setEditEvent(mEvent)}
-            src={PencilIC}
-            size="extra-small"
-            tooltip="Edit"
-          />
-        )}
-        <ContextMenu
-          content={() => (
-            <>
-              <MenuHeader>Options</MenuHeader>
-              <MenuItem
-                iconSrc={EmojiAddIC}
-                onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
-              >
-                Add reaction
-              </MenuItem>
-              <MenuItem
-                iconSrc={ReplyArrowIC}
-                onClick={() => {
-                  viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
-                }}
-              >
-                Reply
-              </MenuItem>
-              {(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
-                <MenuItem iconSrc={PencilIC} onClick={() => setEditEvent(mEvent)}>Edit</MenuItem>
-              )}
-              <MenuItem
-                iconSrc={TickMarkIC}
-                onClick={() => openReadReceipts(roomId, mEvent.getId())}
-              >
-                Read receipts
-              </MenuItem>
-              {(canIRedact || mEvent.getSender() === mx.getUserId()) && (
-                <>
-                  <MenuBorder />
-                  <MenuItem
-                    variant="danger"
-                    iconSrc={BinIC}
-                    onClick={() => {
-                      if (window.confirm('Are you sure you want to delete this event')) {
-                        redactEvent(roomId, mEvent.getId());
-                      }
-                    }}
-                  >
-                    Delete
-                  </MenuItem>
-                </>
-              )}
-            </>
-          )}
-          render={(toggleMenu) => (
-            <IconButton
-              onClick={toggleMenu}
-              src={VerticalMenuIC}
-              size="extra-small"
-              tooltip="Options"
-            />
-          )}
-        />
-      </MessageOptions>
-    );
-
-    const isEditingEvent = editEvent?.getId() === mEvent.getId();
-    const myMessageEl = (
-      <Message
-        key={mEvent.getId()}
-        avatar={userAvatar}
-        header={userHeader}
-        reply={userReply}
-        content={editEvent !== null && isEditingEvent ? null : userContent}
-        msgType={msgType}
-        editContent={editEvent !== null && isEditingEvent ? (
-          <MessageEdit
-            content={content}
-            onSave={(newBody) => {
-              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 = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
+      DividerComp = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
     }
+    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 (
         <React.Fragment key={`box-${mEvent.getId()}`}>
-          {divider}
-          {messageComp}
+          {DividerComp}
+          <TimelineChange
+            key={mEvent.getId()}
+            variant={timelineChange.variant}
+            content={timelineChange.content}
+            time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
+          />
         </React.Fragment>
       );
     }
-
-    prevMEvent = mEvent;
-    const timelineChange = parseTimelineChange(mEvent);
-    if (timelineChange === null) return false;
     return (
       <React.Fragment key={`box-${mEvent.getId()}`}>
-        {divider}
-        <TimelineChange
-          key={mEvent.getId()}
-          variant={timelineChange.variant}
-          content={timelineChange.content}
-          time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
-        />
+        {DividerComp}
+        <Message mEvent={mEvent} isBodyOnly={isContentOnly} roomTimeline={roomTimeline} />
       </React.Fragment>
     );
   }
@@ -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;
index 5b18fcb9dcf992ad92e65f000e3a4621aec57406..0f1ca9448940fd3933e57f082a45b371fa857fcd 100644 (file)
@@ -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}
         />
       </div>
     );
index e25eabb4ef787eb6bdc13558b79ff9099dd1a09e..05f5d503ec7a80fba0f87b51843add4c485a44ad 100644 (file)
@@ -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,
 };
index 66179289cc9be99be5812a804bdf186af682af37..0f9f4c1dddc77c49190b7493c7f9615e05cc1aa7 100644 (file)
@@ -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,
 };
index d1100cd95bd74457c534a44c42be7764e8d874ce..a8ee805916f37fa843e91849e15648354589ea05 100644 (file)
@@ -95,13 +95,13 @@ function getFormattedBody(markdown) {
 function getReplyFormattedBody(roomId, reply) {
   const replyToLink = `<a href="https://matrix.to/#/${roomId}/${reply.eventId}">In reply to</a>`;
   const userLink = `<a href="https://matrix.to/#/${reply.userId}">${reply.userId}</a>`;
-  const formattedReply = getFormattedBody(reply.content.replaceAll('\n', '\n> '));
+  const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> '));
   return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedReply}</blockquote></mx-reply>`;
 }
 
 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'] || {};
index 412cbb70eb61306aa62805156d8676431507ea6e..6443df008b3af451009c9f9c172cb026ae0620bf 100644 (file)
@@ -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',
index 76cecadcc46a95884b666b6d19f6dda4d09af5e6..d52d96e1dab692599400a037c94a588e9f277f78 100644 (file)
@@ -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]?.();
   }