diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss
index 69109a5..1a35234 100644
--- a/src/app/molecules/message/Message.scss
+++ b/src/app/molecules/message/Message.scss
@@ -166,6 +166,16 @@
}
.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;
diff --git a/src/app/organisms/channel/ChannelViewContent.jsx b/src/app/organisms/channel/ChannelViewContent.jsx
index 7a8a2a3..2725f0e 100644
--- a/src/app/organisms/channel/ChannelViewContent.jsx
+++ b/src/app/organisms/channel/ChannelViewContent.jsx
@@ -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 (
+ <>
+
+
+
+ >
+ );
+}
+
+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 (
+
+ );
+ case 'm.image':
+ return (
+
+ );
+ case 'm.audio':
+ return (
+
+ );
+ case 'm.video':
+ if (typeof thumbnailMXC === 'undefined') {
+ thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
+ }
+ return (
+
+ );
+ default:
+ return 'Unable to attach media file!';
+ }
+}
+
+function genChannelIntro(mEvent, roomTimeline) {
+ const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
+ return (
+
+ );
+}
+
+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 : (
+
+ );
+ const userHeader = isContentOnly ? null : (
+
+ );
+ const userReply = reply === null ? null : (
+
+ );
+ const userContent = (
+
+ );
+ const userReactions = reactions === null ? null : (
+
+ {
+ reactions.map((reaction) => (
+ {
+ toggleEmoji(roomId, mEvent.getId(), reaction.key, roomTimeline);
+ }}
+ />
+ ))
+ }
+ pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
+ src={EmojiAddIC}
+ size="extra-small"
+ tooltip="Add reaction"
+ />
+
+ );
+ const userOptions = (
+
+ pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
+ src={EmojiAddIC}
+ size="extra-small"
+ tooltip="Add reaction"
+ />
+ {
+ 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()) && (
+ {
+ if (window.confirm('Are you sure you want to delete this event')) {
+ redactEvent(roomId, mEvent.getId());
+ }
+ }}
+ src={BinIC}
+ size="extra-small"
+ tooltip="Delete"
+ />
+ )}
+
+ );
+
+ const myMessageEl = (
+
+ );
+ 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 (
-
- );
- case 'm.image':
- return (
-
- );
- case 'm.audio':
- return (
-
- );
- case 'm.video':
- if (typeof thumbnailMXC === 'undefined') {
- thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
- }
- return (
-
- );
- 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 (
-
- );
- }
+ 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 : (
-
- );
- const userHeader = isContentOnly ? null : (
-
- );
- const userReply = reply === null ? null : (
-
- );
- const userContent = (
-
- );
- const userReactions = reactions === null ? null : (
-
- {
- reactions.map((reaction) => (
- alert('Sending reactions is yet to be implemented.')}
- />
- ))
- }
-
- );
- const userOptions = (
-
- {
- 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()) && (
- {
- if (window.confirm('Are you sure you want to delete this event')) {
- redact(roomId, mEvent.getId());
- }
- }}
- src={BinIC}
- size="extra-small"
- tooltip="Delete"
- />
- )}
-
- );
-
- const myMessageEl = (
-
- );
-
+ const messageComp = genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent);
prevMEvent = mEvent;
- return myMessageEl;
+ return (
+
+ {divider}
+ {messageComp}
+
+ );
}
+
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 (
- {
- roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && (
- <>
-
-
-
- >
- )
- }
- {
- roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && (
-
- )
- }
+ { roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
+ { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genChannelIntro(undefined, roomTimeline)}
{ roomTimeline.timeline.map(renderMessage) }
@@ -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,
};
diff --git a/src/app/organisms/channel/ChannelViewInput.jsx b/src/app/organisms/channel/ChannelViewInput.jsx
index 12b6e6b..d22cd46 100644
--- a/src/app/organisms/channel/ChannelViewInput.jsx
+++ b/src/app/organisms/channel/ChannelViewInput.jsx
@@ -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({
{
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);
}}
diff --git a/src/app/organisms/emoji-board/EmojiBoardOpener.jsx b/src/app/organisms/emoji-board/EmojiBoardOpener.jsx
index a832832..b2ecc0c 100644
--- a/src/app/organisms/emoji-board/EmojiBoardOpener.jsx
+++ b/src/app/organisms/emoji-board/EmojiBoardOpener.jsx
@@ -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,
diff --git a/src/client/action/room.js b/src/client/action/room.js
index 5bd6777..ecf58a7 100644
--- a/src/client/action/room.js
+++ b/src/client/action/room.js
@@ -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
index 0000000..8297bf0
--- /dev/null
+++ b/src/client/action/roomTimeline.js
@@ -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,
+};