added support for msg editing [#40]
authorunknown <ajbura@gmail.com>
Fri, 20 Aug 2021 13:42:57 +0000 (19:12 +0530)
committerunknown <ajbura@gmail.com>
Fri, 20 Aug 2021 13:42:57 +0000 (19:12 +0530)
public/res/ic/outlined/pencil.svg [new file with mode: 0644]
src/app/atoms/context-menu/ContextMenu.scss
src/app/organisms/channel/ChannelViewContent.jsx
src/client/state/RoomsInput.js

diff --git a/public/res/ic/outlined/pencil.svg b/public/res/ic/outlined/pencil.svg
new file mode 100644 (file)
index 0000000..1b8ac24
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path d="M20.6,5.6l-2.2-2.2C18,3,17.5,2.8,17,2.8S16,3,15.6,3.4L3,16v5h5L20.6,8.4C21.4,7.6,21.4,6.4,20.6,5.6z M7.2,19H5v-2.2
+       l9.2-9.2l2.2,2.2L7.2,19z M15.6,6.2L17,4.8c0,0,0,0,0,0L19.2,7l-1.4,1.4L15.6,6.2z"/>
+</svg>
index 82a645bc7537cac3c3f24c6943c0b647edc70629..fd6ca0770814e48426d98b058bee80af1f143b90 100644 (file)
@@ -44,6 +44,7 @@
     justify-content: start;
     border-radius: 0;
     box-shadow: none;
+    white-space: nowrap;
 
     .text:first-child {
       margin: {
index 276f55ef5db606bd5295cc0b4a4b2aa688521e77..f01e8b2d3d318f379e710e4ab98be9ca9b15a0a6 100644 (file)
@@ -16,11 +16,13 @@ import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigat
 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,
@@ -32,6 +34,8 @@ 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';
 
@@ -182,193 +186,6 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
   });
 }
 
-function genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent) {
-  const mx = initMatrix.matrixClient;
-  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'
-    && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
-    && prevMEvent.getSender() === mEvent.getSender()
-  );
-
-  let content = mEvent.getContent().body;
-  if (typeof content === 'undefined') return null;
-  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 ID = parsedContent.userId || c.getUserIdsWithDisplayName(parsedContent.displayName)[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 : (
-    <Avatar
-      imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
-      text={getUsername(mEvent.sender.userId).slice(0, 1)}
-      bgColor={senderMXIDColor}
-      size="small"
-    />
-  );
-  const userHeader = isContentOnly ? null : (
-    <MessageHeader
-      userId={mEvent.sender.userId}
-      name={getUsername(mEvent.sender.userId)}
-      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
-      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={() => {
-              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"
-      />
-      <IconButton
-        onClick={() => openReadReceipts(roomId, mEvent.getId())}
-        src={TickMarkIC}
-        size="extra-small"
-        tooltip="Read receipts"
-      />
-      {(canIRedact || mEvent.getSender() === mx.getUserId()) && (
-        <IconButton
-          onClick={() => {
-            if (window.confirm('Are you sure you want to delete this event')) {
-              redactEvent(roomId, mEvent.getId());
-            }
-          }}
-          src={BinIC}
-          size="extra-small"
-          tooltip="Delete"
-        />
-      )}
-    </MessageOptions>
-  );
-
-  const myMessageEl = (
-    <Message
-      key={mEvent.getId()}
-      avatar={userAvatar}
-      header={userHeader}
-      reply={userReply}
-      content={userContent}
-      reactions={userReactions}
-      options={userOptions}
-    />
-  );
-  return myMessageEl;
-}
-
 let wasAtBottom = true;
 function ChannelViewContent({
   roomId, roomTimeline, timelineScroll, viewEvent,
@@ -376,6 +193,7 @@ function ChannelViewContent({
   const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
   const [onStateUpdate, updateState] = useState(null);
   const [onPagination, setOnPagination] = useState(null);
+  const [editEvent, setEditEvent] = useState(null);
   const mx = initMatrix.matrixClient;
 
   function autoLoadTimeline() {
@@ -453,6 +271,250 @@ function ChannelViewContent({
   }, [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'
+      && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
+      && prevMEvent.getSender() === mEvent.getSender()
+    );
+
+    let content = mEvent.getContent().body;
+    if (typeof content === 'undefined') return null;
+    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 : (
+      <Avatar
+        imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
+        text={getUsername(mEvent.sender.userId).slice(0, 1)}
+        bgColor={senderMXIDColor}
+        size="small"
+      />
+    );
+    const userHeader = isContentOnly ? null : (
+      <MessageHeader
+        userId={mEvent.sender.userId}
+        name={getUsername(mEvent.sender.userId)}
+        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
+        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={() => {
+                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 reaciton
+              </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}
+        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 (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline);
     if (
@@ -472,7 +534,7 @@ function ChannelViewContent({
     }
 
     if (mEvent.getType() !== 'm.room.member') {
-      const messageComp = genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent);
+      const messageComp = genMessage(mEvent);
       prevMEvent = mEvent;
       return (
         <React.Fragment key={`box-${mEvent.getId()}`}>
index a9216195fa747db576567a7769a892b840445950..d1100cd95bd74457c534a44c42be7764e8d874ce 100644 (file)
@@ -86,7 +86,10 @@ function getFormattedBody(markdown) {
     extensions: [gfm()],
     htmlExtensions: [gfmHtml],
   });
-  return result;
+  const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
+  if (bodyParts === null) return result;
+  if (bodyParts[2].indexOf('</p>') >= 0) return result;
+  return bodyParts[2];
 }
 
 function getReplyFormattedBody(roomId, reply) {
@@ -212,8 +215,11 @@ class RoomsInput extends EventEmitter {
         msgtype: 'm.text',
       };
       if (settings.isMarkdown) {
-        content.format = 'org.matrix.custom.html';
-        content.formatted_body = getFormattedBody(input.message);
+        const formattedBody = getFormattedBody(input.message);
+        if (formattedBody !== input.message) {
+          content.format = 'org.matrix.custom.html';
+          content.formatted_body = formattedBody;
+        }
       }
       if (typeof input.replyTo !== 'undefined') {
         content = bindReplyToContent(roomId, input.replyTo, content);
@@ -326,6 +332,45 @@ class RoomsInput extends EventEmitter {
     }
     return { url };
   }
+
+  async sendEditedMessage(roomId, mEvent, editedBody) {
+    const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
+
+    const content = {
+      body: ` * ${editedBody}`,
+      msgtype: 'm.text',
+      'm.new_content': {
+        body: editedBody,
+        msgtype: 'm.text',
+      },
+      'm.relates_to': {
+        event_id: mEvent.getId(),
+        rel_type: 'm.replace',
+      },
+    };
+    if (settings.isMarkdown) {
+      const formattedBody = getFormattedBody(editedBody);
+      if (formattedBody !== editedBody) {
+        content.formatted_body = ` * ${formattedBody}`;
+        content.format = 'org.matrix.custom.html';
+        content['m.new_content'].formatted_body = formattedBody;
+        content['m.new_content'].format = 'org.matrix.custom.html';
+      }
+    }
+    if (isReply) {
+      const evBody = mEvent.getContent().body;
+      const replyHead = evBody.slice(0, evBody.indexOf('\n\n'));
+      const evFBody = mEvent.getContent().formatted_body;
+      const fReplyHead = evFBody.slice(0, evFBody.indexOf('</mx-reply>'));
+
+      content.format = 'org.matrix.custom.html';
+      content.formatted_body = `${fReplyHead}</mx-reply>${(content.formatted_body || content.body)}`;
+
+      content.body = `${replyHead}\n\n${content.body}`;
+    }
+
+    this.matrixClient.sendMessage(roomId, content);
+  }
 }
 
 export default RoomsInput;