diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg
new file mode 100644
index 0000000..984be62
--- /dev/null
+++ b/public/res/ic/outlined/bin.svg
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg
new file mode 100644
index 0000000..c4cacef
--- /dev/null
+++ b/public/res/ic/outlined/emoji-add.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx
index 5d6b575..1e169bd 100644
--- a/src/app/molecules/message/Message.jsx
+++ b/src/app/molecules/message/Message.jsx
@@ -113,7 +113,7 @@ function MessageContent({ content, isMarkdown, isEdited }) {
{ isMarkdown ? genMarkdown(content) : linkifyContent(content) }
- { isEdited && (edited)}
+ { isEdited && (edited)}
);
}
@@ -139,15 +139,19 @@ MessageReactionGroup.propTypes = {
};
function genReactionMsg(userIds, reaction) {
- let msg = '';
+ const genLessContText = (text) => {text};
+ 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 (
+
+ {children}
+
+ );
+}
+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}
);
@@ -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,
};
diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss
index f8a4108..73484cf 100644
--- a/src/app/molecules/message/Message.scss
+++ b/src/app/molecules/message/Message.scss
@@ -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 {
@@ -106,6 +107,12 @@
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;
@@ -144,6 +151,9 @@
& a {
word-break: break-all;
}
+ &-edited {
+ color: var(--tc-surface-low);
+ }
}
.message__reactions {
display: flex;
@@ -205,6 +215,22 @@
}
}
}
+.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 {
diff --git a/src/app/organisms/channel/ChannelViewContent.jsx b/src/app/organisms/channel/ChannelViewContent.jsx
index 4476b43..737cbaa 100644
--- a/src/app/organisms/channel/ChannelViewContent.jsx
+++ b/src/app/organisms/channel/ChannelViewContent.jsx
@@ -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({
}
);
+ const userOptions = (
+
+ {
+ viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
+ }}
+ src={ReplyArrowIC}
+ size="extra-small"
+ tooltip="Reply"
+ />
+
+
+ );
const myMessageEl = (
);
diff --git a/src/app/organisms/channel/ChannelViewInput.jsx b/src/app/organisms/channel/ChannelViewInput.jsx
index e3c90da..63f4730 100644
--- a/src/app/organisms/channel/ChannelViewInput.jsx
+++ b/src/app/organisms/channel/ChannelViewInput.jsx
@@ -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 (
+
+ {
+ roomsInput.cancelReplyTo(roomId);
+ setReplyTo(null);
+ }}
+ src={CrossIC}
+ tooltip="Cancel reply"
+ size="extra-small"
+ />
+
+
+ );
+ }
+
return (
<>
+ { replyTo !== null && attachReply()}
{ attachment !== null && attachFile() }