added reply support

This commit is contained in:
unknown 2021-08-11 13:29:01 +05:30
parent 109e2fa82d
commit 717ffe560f
9 changed files with 208 additions and 22 deletions

View file

@ -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>

After

Width:  |  Height:  |  Size: 735 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 839 B

View 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,
};

View 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 {
@ -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 {

View 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}
/>
);

View 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(); }}>
{

View file

@ -99,3 +99,18 @@
}
}
}
.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;
}
}
}

View 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);
}
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);
}

View file

@ -221,6 +221,7 @@
html {
height: 100%;
overflow: hidden;
}
body {