added reply support
authorunknown <ajbura@gmail.com>
Wed, 11 Aug 2021 07:59:01 +0000 (13:29 +0530)
committerunknown <ajbura@gmail.com>
Wed, 11 Aug 2021 07:59:01 +0000 (13:29 +0530)
public/res/ic/outlined/bin.svg [new file with mode: 0644]
public/res/ic/outlined/emoji-add.svg [new file with mode: 0644]
src/app/molecules/message/Message.jsx
src/app/molecules/message/Message.scss
src/app/organisms/channel/ChannelViewContent.jsx
src/app/organisms/channel/ChannelViewInput.jsx
src/app/organisms/channel/ChannelViewInput.scss
src/client/state/RoomsInput.js
src/index.scss

diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg
new file mode 100644 (file)
index 0000000..984be62
--- /dev/null
@@ -0,0 +1,18 @@
+<?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">
+<g>
+       <g>
+               <g>
+                       <rect x="9" y="8" width="2" height="8"/>
+               </g>
+               <g>
+                       <rect x="13" y="8" width="2" height="8"/>
+               </g>
+       </g>
+       <path d="M21,3h-5l-1.4-1.4C14.2,1.2,13.7,1,13.2,1h-2.3c-0.5,0-1,0.2-1.4,0.6L8,3H3v2h2v14c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V5h2
+               V3z M17,19H7V5h10V19z"/>
+</g>
+</svg>
diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg
new file mode 100644 (file)
index 0000000..c4cacef
--- /dev/null
@@ -0,0 +1,13 @@
+<?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">
+<g>
+       <path d="M12,20c-4.4,0-8-3.6-8-8s3.6-8,8-8V2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10h-2C20,16.4,16.4,20,12,20z"/>
+       <circle cx="9.5" cy="8.5" r="1.5"/>
+       <circle cx="14.5" cy="8.5" r="1.5"/>
+       <path d="M6,12c0,3.3,2.7,6,6,6s6-2.7,6-6h-2c0,2.2-1.8,4-4,4s-4-1.8-4-4H6z"/>
+       <polygon points="20.8,3.3 20.8,0 19.3,0 19.3,3.3 16,3.3 16,4.8 19.3,4.8 19.3,8 20.8,8 20.8,4.8 24,4.8 24,3.3    "/>
+</g>
+</svg>
index 5d6b575e7c9077914b024a1908741c9b5e07cdc7..1e169bd1dea67ba5a92d19fd06e691f8017057de 100644 (file)
@@ -113,7 +113,7 @@ function MessageContent({ content, isMarkdown, isEdited }) {
       <div className="text text-b1">
         { isMarkdown ? genMarkdown(content) : linkifyContent(content) }
       </div>
-      { isEdited && <Text className="message__edited" variant="b3">(edited)</Text>}
+      { isEdited && <Text className="message__content-edited" variant="b3">(edited)</Text>}
     </div>
   );
 }
@@ -139,15 +139,19 @@ MessageReactionGroup.propTypes = {
 };
 
 function genReactionMsg(userIds, reaction) {
-  let msg = '';
+  const genLessContText = (text) => <span style={{ opacity: '.6' }}>{text}</span>;
+  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)}`;
+    if (index === 0) msg = <>{getUsername(userId)}</>;
+    // eslint-disable-next-line react/jsx-one-expression-per-line
+    else if (index === userIds.length - 1) msg = <>{msg}{genLessContText(' and ')}{getUsername(userId)}</>;
+    // eslint-disable-next-line react/jsx-one-expression-per-line
+    else msg = <>{msg}{genLessContText(', ')}{getUsername(userId)}</>;
   });
   return (
     <>
-      {`${msg} reacted with`}
+      {msg}
+      {genLessContText(' reacted with')}
       {parse(twemoji.parse(reaction))}
     </>
   );
@@ -179,8 +183,19 @@ MessageReaction.propTypes = {
   onClick: PropTypes.func.isRequired,
 };
 
+function MessageOptions({ children }) {
+  return (
+    <div className="message__options">
+      {children}
+    </div>
+  );
+}
+MessageOptions.propTypes = {
+  children: PropTypes.node.isRequired,
+};
+
 function Message({
-  avatar, header, reply, content, reactions,
+  avatar, header, reply, content, reactions, options,
 }) {
   const msgClass = header === null ? ' message--content-only' : ' message--full';
   return (
@@ -193,6 +208,7 @@ function Message({
         {reply !== null && reply}
         {content}
         {reactions !== null && reactions}
+        {options !== null && options}
       </div>
     </div>
   );
@@ -202,6 +218,7 @@ Message.defaultProps = {
   header: null,
   reply: null,
   reactions: null,
+  options: null,
 };
 Message.propTypes = {
   avatar: PropTypes.node,
@@ -209,6 +226,7 @@ Message.propTypes = {
   reply: PropTypes.node,
   content: PropTypes.node.isRequired,
   reactions: PropTypes.node,
+  options: PropTypes.node,
 };
 
 export {
@@ -218,5 +236,6 @@ export {
   MessageContent,
   MessageReactionGroup,
   MessageReaction,
+  MessageOptions,
   PlaceholderMessage,
 };
index f8a4108dacbe2205d2798c96a7a8dd7f3fc4cd75..73484cf1381996d8d9623f3f2eea3851d902f5b1 100644 (file)
@@ -8,6 +8,9 @@
 
   &:hover {
     background-color: var(--bg-surface-hover);
+    & .message__options {
+      display: flex;
+    }
   }
 
   [dir=rtl] & {
@@ -21,8 +24,7 @@
     padding-top: 6px;
   }
   
-  &__avatar-container,
-  &__profile {
+  &__avatar-container{
     margin-right: var(--sp-tight);
 
     [dir=rtl] & {
@@ -36,6 +38,8 @@
   &__main-container {
     flex: 1;
     min-width: 0;
+
+    position: relative;
   }
 }
 
@@ -49,9 +53,6 @@
   &__avatar-container {
     width: var(--av-small);
   }
-  &__edited {
-    color: var(--tc-surface-low);
-  }
 }
 
 .ph-msg {
     flex: 1;
     min-width: 0;
     color: var(--tc-surface-high);
+    margin-right: var(--sp-tight);
+
+    [dir=rtl] & {
+      margin-left: var(--sp-tight);
+      margin-right: 0;
+    }
 
     & > .text {
       color: inherit;
   & a {
     word-break: break-all;
   }
+  &-edited {
+    color: var(--tc-surface-low);
+  }
 }
 .message__reactions {
   display: flex;
     }
   }
 }
+.message__options {
+  position: absolute;
+  top: 0;
+  right: 60px;
+  transform: translateY(-50%);
+
+  border-radius: var(--bo-radius);
+  box-shadow: var(--bs-surface-border);
+  background-color: var(--bg-surface-low);
+  display: none;
+
+  [dir=rtl] & {
+    left: 60px;
+    right: unset;
+  }
+}
 
 // markdown formating
 .message__content {
index 4476b43f737144d10ec7a6b6a47743a119efb084..737cbaac16a5bf85d8b1d03fee83bebb5b498842 100644 (file)
@@ -13,6 +13,7 @@ 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 {
   Message,
   MessageHeader,
@@ -20,12 +21,16 @@ import {
   MessageContent,
   MessageReactionGroup,
   MessageReaction,
+  MessageOptions,
   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';
 
+import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
+import BinIC from '../../../../public/res/ic/outlined/bin.svg';
+
 import { parseReply, parseTimelineChange } from './common';
 
 const MAX_MSG_DIFF_MINUTES = 5;
@@ -335,6 +340,19 @@ function ChannelViewContent({
           }
         </MessageReactionGroup>
       );
+      const userOptions = (
+        <MessageOptions>
+          <IconButton
+            onClick={() => {
+              viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
+            }}
+            src={ReplyArrowIC}
+            size="extra-small"
+            tooltip="Reply"
+          />
+          <IconButton src={BinIC} size="extra-small" tooltip="Delete" />
+        </MessageOptions>
+      );
 
       const myMessageEl = (
         <Message
@@ -344,6 +362,7 @@ function ChannelViewContent({
           reply={userReply}
           content={userContent}
           reactions={userReactions}
+          options={userOptions}
         />
       );
 
index e3c90da15fd42fc3364558c0e64b4daba2f4136f..63f473071e0bc360f68555a48cbe75aa06bc92ad 100644 (file)
@@ -9,12 +9,15 @@ import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import settings from '../../../client/state/settings';
 import { bytesToSize } from '../../../util/common';
+import { getUsername } from '../../../util/matrixUtil';
+import colorMXID from '../../../util/colorMXID';
 
 import Text from '../../atoms/text/Text';
 import RawIcon from '../../atoms/system-icons/RawIcon';
 import IconButton from '../../atoms/button/IconButton';
 import ContextMenu from '../../atoms/context-menu/ContextMenu';
 import ScrollView from '../../atoms/scroll/ScrollView';
+import { MessageReply } from '../../molecules/message/Message';
 import EmojiBoard from '../emoji-board/EmojiBoard';
 
 import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
@@ -25,6 +28,7 @@ import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
 import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
 import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
 import FileIC from '../../../../public/res/ic/outlined/file.svg';
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 
 const CMD_REGEX = /(\/|>[#*@]|:)(\S*)$/;
 let isTyping = false;
@@ -35,6 +39,7 @@ function ChannelViewInput({
 }) {
   const [attachment, setAttachment] = useState(null);
   const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
+  const [replyTo, setReplyTo] = useState(null);
 
   const textAreaRef = useRef(null);
   const inputBaseRef = useRef(null);
@@ -123,17 +128,24 @@ function ChannelViewInput({
     deactivateCmd();
   }
 
+  function setUpReply(userId, eventId, content) {
+    setReplyTo({ userId, eventId, content });
+    roomsInput.setReplyTo(roomId, { userId, eventId, content });
+  }
+
   useEffect(() => {
     roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
     roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
     roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
     viewEvent.on('cmd_error', errorCmd);
     viewEvent.on('cmd_fired', firedCmd);
+    viewEvent.on('reply_to', setUpReply);
     if (textAreaRef?.current !== null) {
       isTyping = false;
       textAreaRef.current.focus();
       textAreaRef.current.value = roomsInput.getMessage(roomId);
       setAttachment(roomsInput.getAttachment(roomId));
+      setReplyTo(roomsInput.getReplyTo(roomId));
     }
     return () => {
       roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
@@ -141,6 +153,7 @@ function ChannelViewInput({
       roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
       viewEvent.removeListener('cmd_error', errorCmd);
       viewEvent.removeListener('cmd_fired', firedCmd);
+      viewEvent.removeListener('reply_to', setUpReply);
       if (isCmdActivated) deactivateCmd();
       if (textAreaRef?.current === null) return;
 
@@ -180,6 +193,7 @@ function ChannelViewInput({
     timelineScroll.reachBottom();
     viewEvent.emit('message_sent');
     textAreaRef.current.style.height = 'unset';
+    if (replyTo !== null) setReplyTo(null);
   }
 
   function processTyping(msg) {
@@ -316,8 +330,31 @@ function ChannelViewInput({
     );
   }
 
+  function attachReply() {
+    return (
+      <div className="channel-reply">
+        <IconButton
+          onClick={() => {
+            roomsInput.cancelReplyTo(roomId);
+            setReplyTo(null);
+          }}
+          src={CrossIC}
+          tooltip="Cancel reply"
+          size="extra-small"
+        />
+        <MessageReply
+          userId={replyTo.userId}
+          name={getUsername(replyTo.userId)}
+          color={colorMXID(replyTo.userId)}
+          content={replyTo.content}
+        />
+      </div>
+    );
+  }
+
   return (
     <>
+      { replyTo !== null && attachReply()}
       { attachment !== null && attachFile() }
       <form className="channel-input" onSubmit={(e) => { e.preventDefault(); }}>
         {
index 915aa6d96c69ecf6e377e2c7d7f19d5a3328b159..46c703948df8ca4b69a5b7c7f6bebed04829988e 100644 (file)
       background-color: var(--bg-caution);
     }
   }
+}
+
+.channel-reply {
+  display: flex;
+  align-items: center;
+  background-color: var(--bg-surface-low);
+  border-bottom: 1px solid var(--bg-surface-border);
+
+  & .ic-btn-surface {
+    margin: 0 13px 0 17px;
+    border-radius: 0;
+    [dir=rtl] & {
+      margin: 0 17px 0 13px;
+    }
+  }
 }
\ No newline at end of file
index 970bd62142c54fcae1178487a3b309ee327ac381..c298ff57f44fd083a863c0cbc1fbd14ea7e6459b 100644 (file)
@@ -80,13 +80,32 @@ function getVideoThumbnail(video, width, height, mimeType) {
   });
 }
 
-function getFormatedBody(markdown) {
+function getFormattedBody(markdown) {
   const reader = new Parser();
   const writer = new HtmlRenderer();
   const parsed = reader.parse(markdown);
   return writer.render(parsed);
 }
 
+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>`;
+  return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${reply.content}</blockquote></mx-reply>`;
+}
+
+function bindReplyToContent(roomId, reply, content) {
+  const newContent = { ...content };
+  newContent.body = `> <${reply.userId}> ${reply.content}`;
+  newContent.body += `\n\n${content.body}`;
+  newContent.format = 'org.matrix.custom.html';
+  newContent['m.relates_to'] = content['m.relates_to'] || {};
+  newContent['m.relates_to']['m.in_reply_to'] = { event_id: reply.eventId };
+
+  const formattedReply = getReplyFormattedBody(roomId, reply);
+  newContent.formatted_body = formattedReply + (content.formatted_body || content.body);
+  return newContent;
+}
+
 class RoomsInput extends EventEmitter {
   constructor(mx) {
     super();
@@ -98,6 +117,7 @@ class RoomsInput extends EventEmitter {
   cleanEmptyEntry(roomId) {
     const input = this.getInput(roomId);
     const isEmpty = typeof input.attachment === 'undefined'
+      && typeof input.replyTo === 'undefined'
       && (typeof input.message === 'undefined' || input.message === '');
     if (isEmpty) {
       this.roomIdToInput.delete(roomId);
@@ -121,6 +141,25 @@ class RoomsInput extends EventEmitter {
     return input.message;
   }
 
+  setReplyTo(roomId, replyTo) {
+    const input = this.getInput(roomId);
+    input.replyTo = replyTo;
+    this.roomIdToInput.set(roomId, input);
+  }
+
+  getReplyTo(roomId) {
+    const input = this.getInput(roomId);
+    if (typeof input.replyTo === 'undefined') return null;
+    return input.replyTo;
+  }
+
+  cancelReplyTo(roomId) {
+    const input = this.getInput(roomId);
+    if (typeof input.replyTo === 'undefined') return;
+    delete input.replyTo;
+    this.roomIdToInput.set(roomId, input);
+  }
+
   setAttachment(roomId, file) {
     const input = this.getInput(roomId);
     input.attachment = {
@@ -145,13 +184,9 @@ class RoomsInput extends EventEmitter {
       this.matrixClient.cancelUpload(uploadingPromise);
       delete input.attachment.uploadingPromise;
     }
-    if (input.message) {
-      delete input.attachment;
-      delete input.isSending;
-      this.roomIdToInput.set(roomId, input);
-    } else {
-      this.roomIdToInput.delete(roomId);
-    }
+    delete input.attachment;
+    delete input.isSending;
+    this.roomIdToInput.set(roomId, input);
     this.emit(cons.events.roomsInput.ATTACHMENT_CANCELED, roomId);
   }
 
@@ -168,13 +203,16 @@ class RoomsInput extends EventEmitter {
     }
 
     if (this.getMessage(roomId).trim() !== '') {
-      const content = {
+      let content = {
         body: input.message,
         msgtype: 'm.text',
       };
       if (settings.isMarkdown) {
         content.format = 'org.matrix.custom.html';
-        content.formatted_body = getFormatedBody(input.message);
+        content.formatted_body = getFormattedBody(input.message);
+      }
+      if (typeof input.replyTo !== 'undefined') {
+        content = bindReplyToContent(roomId, input.replyTo, content);
       }
       this.matrixClient.sendMessage(roomId, content);
     }
index 38bfb762c4cc6c1f54b111c4c538acd25799703f..a3819a9563bd19137a6534ead363feae31144341 100644 (file)
 
 html {
   height: 100%;
+  overflow: hidden;
 }
 
 body {