added support for sending reaction
authorunknown <ajbura@gmail.com>
Sun, 15 Aug 2021 08:29:09 +0000 (13:59 +0530)
committerunknown <ajbura@gmail.com>
Sun, 15 Aug 2021 08:29:09 +0000 (13:59 +0530)
src/app/molecules/message/Message.scss
src/app/organisms/channel/ChannelViewContent.jsx
src/app/organisms/channel/ChannelViewInput.jsx
src/app/organisms/emoji-board/EmojiBoardOpener.jsx
src/client/action/room.js
src/client/action/roomTimeline.js [new file with mode: 0644]

index 69109a58b83faea2f8b3e18c7d132193dbef2a30..1a35234e9834ad0d0b32a160fd3b54bdcc062bd0 100644 (file)
 }
 .message__reactions {
   display: flex;
+  flex-wrap: wrap;
+
+  & .ic-btn-surface {
+    display: none;
+    padding: var(--sp-ultra-tight);
+    margin-top: var(--sp-extra-tight);
+  }
+  &:hover .ic-btn-surface {
+    display: block;
+  }
 }
 .msg__reaction {
   margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0;
index 7a8a2a317f91317d309f64cb9b96688417f75235..2725f0e261cbef392513e788c7df9ad7e2b32abb 100644 (file)
@@ -7,10 +7,11 @@ import dateFormat from 'dateformat';
 
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
-import { redact } from '../../../client/action/room';
+import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
 import { getUsername, doesRoomHaveUnread } from '../../../util/matrixUtil';
 import colorMXID from '../../../util/colorMXID';
 import { diffMinutes, isNotInSameDay } from '../../../util/common';
+import { openEmojiBoard } from '../../../client/action/navigation';
 
 import Divider from '../../atoms/divider/Divider';
 import Avatar from '../../atoms/avatar/Avatar';
@@ -30,12 +31,325 @@ import ChannelIntro from '../../molecules/channel-intro/ChannelIntro';
 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 BinIC from '../../../../public/res/ic/outlined/bin.svg';
 
 import { parseReply, parseTimelineChange } from './common';
 
 const MAX_MSG_DIFF_MINUTES = 5;
 
+function genPlaceholders() {
+  return (
+    <>
+      <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
+      <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
+      <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
+    </>
+  );
+}
+
+function isMedia(mE) {
+  return (
+    mE.getContent()?.msgtype === 'm.file'
+    || mE.getContent()?.msgtype === 'm.image'
+    || mE.getContent()?.msgtype === 'm.audio'
+    || mE.getContent()?.msgtype === 'm.video'
+  );
+}
+
+function genMediaContent(mE) {
+  const mx = initMatrix.matrixClient;
+  const mContent = mE.getContent();
+  let mediaMXC = mContent.url;
+  let thumbnailMXC = mContent?.info?.thumbnail_url;
+  const isEncryptedFile = typeof mediaMXC === 'undefined';
+  if (isEncryptedFile) mediaMXC = mContent.file.url;
+
+  switch (mE.getContent()?.msgtype) {
+    case 'm.file':
+      return (
+        <Media.File
+          name={mContent.body}
+          link={mx.mxcUrlToHttp(mediaMXC)}
+          file={mContent.file}
+          type={mContent.info.mimetype}
+        />
+      );
+    case 'm.image':
+      return (
+        <Media.Image
+          name={mContent.body}
+          width={mContent.info.w || null}
+          height={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}
+        />
+      );
+    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={mContent.info.w || null}
+          height={mContent.info.h || null}
+          file={isEncryptedFile ? mContent.file : null}
+          type={mContent.info.mimetype}
+        />
+      );
+    default:
+      return 'Unable to attach media file!';
+  }
+}
+
+function genChannelIntro(mEvent, roomTimeline) {
+  const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
+  return (
+    <ChannelIntro
+      key={mEvent ? mEvent.getId() : Math.random().toString(20).substr(2, 6)}
+      avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
+      name={roomTimeline.room.name}
+      heading={`Welcome to ${roomTimeline.room.name}`}
+      desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
+      time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
+    />
+  );
+}
+
+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({
+    x: e.detail ? e.clientX : '50%',
+    y: e.detail ? e.clientY : '50%',
+    detail: e.detail,
+  }, (emoji) => {
+    toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
+    e.target.click();
+  });
+}
+
+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 username = getUsername(parsedContent.userId);
+      reply = {
+        userId: parsedContent.userId,
+        color: colorMXID(parsedContent.userId),
+        to: username,
+        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
+      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={() => {
+              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"
+      />
+      {(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,
@@ -121,86 +435,7 @@ function ChannelViewContent({
 
   let prevMEvent = null;
   function renderMessage(mEvent) {
-    function isMedia(mE) {
-      return (
-        mE.getContent()?.msgtype === 'm.file'
-        || mE.getContent()?.msgtype === 'm.image'
-        || mE.getContent()?.msgtype === 'm.audio'
-        || mE.getContent()?.msgtype === 'm.video'
-      );
-    }
-    function genMediaContent(mE) {
-      const mContent = mE.getContent();
-      let mediaMXC = mContent.url;
-      let thumbnailMXC = mContent?.info?.thumbnail_url;
-      const isEncryptedFile = typeof mediaMXC === 'undefined';
-      if (isEncryptedFile) mediaMXC = mContent.file.url;
-
-      switch (mE.getContent()?.msgtype) {
-        case 'm.file':
-          return (
-            <Media.File
-              name={mContent.body}
-              link={mx.mxcUrlToHttp(mediaMXC)}
-              file={mContent.file}
-              type={mContent.info.mimetype}
-            />
-          );
-        case 'm.image':
-          return (
-            <Media.Image
-              name={mContent.body}
-              width={mContent.info.w || null}
-              height={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}
-            />
-          );
-        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={mContent.info.w || null}
-              height={mContent.info.h || null}
-              file={isEncryptedFile ? mContent.file : null}
-              type={mContent.info.mimetype}
-            />
-          );
-        default:
-          return 'Unable to attach media file!';
-      }
-    }
-
-    if (mEvent.getType() === 'm.room.create') {
-      const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
-      return (
-        <ChannelIntro
-          key={mEvent.getId()}
-          avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
-          name={roomTimeline.room.name}
-          heading={`Welcome to ${roomTimeline.room.name}`}
-          desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
-          time={`Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}`}
-        />
-      );
-    }
+    if (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline);
     if (
       mEvent.getType() !== 'm.room.message'
       && mEvent.getType() !== 'm.room.encrypted'
@@ -217,173 +452,16 @@ function ChannelViewContent({
     }
 
     if (mEvent.getType() !== 'm.room.member') {
-      const isContentOnly = (
-        prevMEvent !== null
-        && prevMEvent.getType() !== 'm.room.member'
-        && diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
-        && prevMEvent.getSender() === mEvent.getSender()
-      );
-
-      const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
-      const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
-
-      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 username = getUsername(parsedContent.userId);
-          reply = {
-            userId: parsedContent.userId,
-            color: colorMXID(parsedContent.userId),
-            to: username,
-            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;
-                }
-                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
-          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 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"
-          />
-          {(canIRedact || mEvent.getSender() === mx.getUserId()) && (
-            <IconButton
-              onClick={() => {
-                if (window.confirm('Are you sure you want to delete this event')) {
-                  redact(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}
-        />
-      );
-
+      const messageComp = genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent);
       prevMEvent = mEvent;
-      return myMessageEl;
+      return (
+        <React.Fragment key={`box-${mEvent.getId()}`}>
+          {divider}
+          {messageComp}
+        </React.Fragment>
+      );
     }
+
     prevMEvent = mEvent;
     const timelineChange = parseTimelineChange(mEvent);
     if (timelineChange === null) return null;
@@ -400,30 +478,11 @@ function ChannelViewContent({
     );
   }
 
-  const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
   return (
     <div className="channel-view__content">
       <div className="timeline__wrapper">
-        {
-          roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && (
-            <>
-              <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
-              <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
-              <PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
-            </>
-          )
-        }
-        {
-          roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && (
-            <ChannelIntro
-              key={Math.random().toString(20).substr(2, 6)}
-              avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
-              name={roomTimeline.room.name}
-              heading={`Welcome to ${roomTimeline.room.name}`}
-              desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
-            />
-          )
-        }
+        { roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
+        { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genChannelIntro(undefined, roomTimeline)}
         { roomTimeline.timeline.map(renderMessage) }
       </div>
     </div>
@@ -432,14 +491,7 @@ function ChannelViewContent({
 ChannelViewContent.propTypes = {
   roomId: PropTypes.string.isRequired,
   roomTimeline: PropTypes.shape({}).isRequired,
-  timelineScroll: PropTypes.shape({
-    reachBottom: PropTypes.func,
-    autoReachBottom: PropTypes.func,
-    tryRestoringScroll: PropTypes.func,
-    enableSmoothScroll: PropTypes.func,
-    disableSmoothScroll: PropTypes.func,
-    isScrollable: PropTypes.func,
-  }).isRequired,
+  timelineScroll: PropTypes.shape({}).isRequired,
   viewEvent: PropTypes.shape({}).isRequired,
 };
 
index 12b6e6be797176e630aeaafda8a3c0ed4d4cf06a..d22cd464789ef0d7833def72a0ca5a1ff6849872 100644 (file)
@@ -16,10 +16,8 @@ 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';
 import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
@@ -303,9 +301,9 @@ function ChannelViewInput({
           <IconButton
             onClick={(e) => {
               openEmojiBoard({
-                x: e.detail ? e.clientX + 40 : '10%',
-                y: e.detail ? e.clientY - 240 : 300,
-                isReverse: !e.detail,
+                x: '10%',
+                y: 300,
+                isReverse: true,
                 detail: e.detail,
               }, addEmoji);
             }}
index a8328324054abb6f1926272b155941dbba60de91..b2ecc0c43f611d620d5ee18e69d5c40c163a665f 100644 (file)
@@ -61,8 +61,9 @@ function EmojiBoardOpener() {
           onClick={toggleMenu}
           type="button"
           style={{
-            width: '0',
-            height: '0',
+            width: '32px',
+            height: '32px',
+            backgroundColor: 'transparent',
             position: 'absolute',
             top: 0,
             left: 0,
index 5bd6777a5cb24fa7fc2439ddddbf3d85caf07725..ecf58a79d3c398540cf4bed2e46889b4885c68e8 100644 (file)
@@ -189,19 +189,7 @@ async function invite(roomId, userId) {
   }
 }
 
-async function redact(roomId, eventId, reason) {
-  const mx = initMatrix.matrixClient;
-
-  try {
-    await mx.redactEvent(roomId, eventId, undefined, typeof reason === 'undefined' ? undefined : { reason });
-    return true;
-  } catch (e) {
-    throw new Error(e);
-  }
-}
-
 export {
   join, leave,
   create, invite,
-  redact,
 };
diff --git a/src/client/action/roomTimeline.js b/src/client/action/roomTimeline.js
new file mode 100644 (file)
index 0000000..8297bf0
--- /dev/null
@@ -0,0 +1,33 @@
+import initMatrix from '../initMatrix';
+
+async function redactEvent(roomId, eventId, reason) {
+  const mx = initMatrix.matrixClient;
+
+  try {
+    await mx.redactEvent(roomId, eventId, undefined, typeof reason === 'undefined' ? undefined : { reason });
+    return true;
+  } catch (e) {
+    throw new Error(e);
+  }
+}
+
+async function sendReaction(roomId, toEventId, reaction) {
+  const mx = initMatrix.matrixClient;
+
+  try {
+    await mx.sendEvent(roomId, 'm.reaction', {
+      'm.relates_to': {
+        event_id: toEventId,
+        key: reaction,
+        rel_type: 'm.annotation',
+      },
+    });
+  } catch (e) {
+    throw new Error(e);
+  }
+}
+
+export {
+  redactEvent,
+  sendReaction,
+};