Refectored Message comp
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
f628a6c3d6
commit
7e7a5e692e
11 changed files with 453 additions and 530 deletions
|
@ -8,21 +8,18 @@ function Divider({ text, variant }) {
|
|||
const dividerClass = ` divider--${variant}`;
|
||||
return (
|
||||
<div className={`divider${dividerClass}`}>
|
||||
{text !== false && <Text className="divider__text" variant="b3">{text}</Text>}
|
||||
{text !== null && <Text className="divider__text" variant="b3">{text}</Text>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Divider.defaultProps = {
|
||||
text: false,
|
||||
text: null,
|
||||
variant: 'surface',
|
||||
};
|
||||
|
||||
Divider.propTypes = {
|
||||
text: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
text: PropTypes.string,
|
||||
variant: PropTypes.oneOf(['surface', 'primary', 'caution', 'danger']),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Message.scss';
|
||||
|
||||
|
@ -9,15 +10,33 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|||
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||
import {
|
||||
openEmojiBoard, openProfileViewer, openReadReceipts, replyTo,
|
||||
} from '../../../client/action/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
||||
import * as Media from '../media/Media';
|
||||
|
||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
||||
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
const components = {
|
||||
code({
|
||||
|
@ -55,7 +74,7 @@ function PlaceholderMessage() {
|
|||
</div>
|
||||
<div className="ph-msg__main-container">
|
||||
<div className="ph-msg__header" />
|
||||
<div className="ph-msg__content">
|
||||
<div className="ph-msg__body">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
|
@ -88,13 +107,13 @@ MessageHeader.propTypes = {
|
|||
time: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MessageReply({ name, color, content }) {
|
||||
function MessageReply({ name, color, body }) {
|
||||
return (
|
||||
<div className="message__reply">
|
||||
<Text variant="b2">
|
||||
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
||||
<span style={{ color }}>{name}</span>
|
||||
<>{` ${content}`}</>
|
||||
<>{` ${body}`}</>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
@ -103,39 +122,39 @@ function MessageReply({ name, color, content }) {
|
|||
MessageReply.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MessageContent({
|
||||
function MessageBody({
|
||||
senderName,
|
||||
content,
|
||||
isMarkdown,
|
||||
body,
|
||||
isCustomHTML,
|
||||
isEdited,
|
||||
msgType,
|
||||
}) {
|
||||
return (
|
||||
<div className="message__content">
|
||||
<div className="message__body">
|
||||
<div className="text text-b1">
|
||||
{ msgType === 'm.emote' && `* ${senderName} ` }
|
||||
{ isMarkdown ? genMarkdown(content) : linkifyContent(content) }
|
||||
{ isCustomHTML ? genMarkdown(body) : linkifyContent(body) }
|
||||
</div>
|
||||
{ isEdited && <Text className="message__content-edited" variant="b3">(edited)</Text>}
|
||||
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MessageContent.defaultProps = {
|
||||
isMarkdown: false,
|
||||
MessageBody.defaultProps = {
|
||||
isCustomHTML: false,
|
||||
isEdited: false,
|
||||
};
|
||||
MessageContent.propTypes = {
|
||||
MessageBody.propTypes = {
|
||||
senderName: PropTypes.string.isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
isMarkdown: PropTypes.bool,
|
||||
body: PropTypes.node.isRequired,
|
||||
isCustomHTML: PropTypes.bool,
|
||||
isEdited: PropTypes.bool,
|
||||
msgType: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MessageEdit({ content, onSave, onCancel }) {
|
||||
function MessageEdit({ body, onSave, onCancel }) {
|
||||
const editInputRef = useRef(null);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
|
@ -150,7 +169,7 @@ function MessageEdit({ content, onSave, onCancel }) {
|
|||
<Input
|
||||
forwardRef={editInputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={content}
|
||||
value={body}
|
||||
placeholder="Edit message"
|
||||
required
|
||||
resizable
|
||||
|
@ -163,7 +182,7 @@ function MessageEdit({ content, onSave, onCancel }) {
|
|||
);
|
||||
}
|
||||
MessageEdit.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -235,72 +254,366 @@ MessageOptions.propTypes = {
|
|||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function Message({
|
||||
avatar, header, reply, content, editContent, reactions, options,
|
||||
msgType,
|
||||
}) {
|
||||
const className = [
|
||||
'message',
|
||||
header === null ? ' message--content-only' : ' message--full',
|
||||
];
|
||||
function isMedia(mE) {
|
||||
return (
|
||||
mE.getContent()?.msgtype === 'm.file'
|
||||
|| mE.getContent()?.msgtype === 'm.image'
|
||||
|| mE.getContent()?.msgtype === 'm.audio'
|
||||
|| mE.getContent()?.msgtype === 'm.video'
|
||||
|| mE.getType() === 'm.sticker'
|
||||
);
|
||||
}
|
||||
|
||||
function genMediaContent(mE) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const mContent = mE.getContent();
|
||||
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let mediaMXC = mContent?.url;
|
||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
||||
|
||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||
|
||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let msgType = mE.getContent()?.msgtype;
|
||||
if (mE.getType() === 'm.sticker') msgType = 'm.image';
|
||||
|
||||
switch (msgType) {
|
||||
case 'm.text':
|
||||
className.push('message--type-text');
|
||||
break;
|
||||
case 'm.emote':
|
||||
className.push('message--type-emote');
|
||||
break;
|
||||
case 'm.notice':
|
||||
className.push('message--type-notice');
|
||||
break;
|
||||
case 'm.file':
|
||||
return (
|
||||
<Media.File
|
||||
name={mContent.body}
|
||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||
type={mContent.info?.mimetype}
|
||||
file={mContent.file || null}
|
||||
/>
|
||||
);
|
||||
case 'm.image':
|
||||
return (
|
||||
<Media.Image
|
||||
name={mContent.body}
|
||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||
height={typeof mContent.info?.h === 'number' ? 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 || null}
|
||||
/>
|
||||
);
|
||||
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={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
}
|
||||
}
|
||||
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(getEventCords(e), (emoji) => {
|
||||
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
||||
e.target.click();
|
||||
});
|
||||
}
|
||||
|
||||
function parseReply(rawBody) {
|
||||
if (rawBody.indexOf('>') !== 0) return null;
|
||||
let body = rawBody.slice(rawBody.indexOf('<') + 1);
|
||||
const user = body.slice(0, body.indexOf('>'));
|
||||
|
||||
body = body.slice(body.indexOf('>') + 2);
|
||||
const replyBody = body.slice(0, body.indexOf('\n\n'));
|
||||
body = body.slice(body.indexOf('\n\n') + 2);
|
||||
|
||||
if (user === '') return null;
|
||||
|
||||
const isUserId = user.match(/^@.+:.+/);
|
||||
|
||||
return {
|
||||
userId: isUserId ? user : null,
|
||||
displayName: isUserId ? null : user,
|
||||
replyBody,
|
||||
body,
|
||||
};
|
||||
}
|
||||
function getEditedBody(eventId, editedTimeline) {
|
||||
const editedList = editedTimeline.get(eventId);
|
||||
const editedMEvent = editedList[editedList.length - 1];
|
||||
const newContent = editedMEvent.getContent()['m.new_content'];
|
||||
if (typeof newContent === 'undefined') return [null, false];
|
||||
|
||||
const isCustomHTML = newContent.format === 'org.matrix.custom.html';
|
||||
const parsedContent = parseReply(newContent.body);
|
||||
if (parsedContent === null) {
|
||||
return [newContent.body, isCustomHTML];
|
||||
}
|
||||
return [parsedContent.body, isCustomHTML];
|
||||
}
|
||||
|
||||
function Message({ mEvent, isBodyOnly, roomTimeline }) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const mx = initMatrix.matrixClient;
|
||||
const {
|
||||
room, roomId, editedTimeline, reactionTimeline,
|
||||
} = roomTimeline;
|
||||
|
||||
const className = ['message', (isBodyOnly ? 'message--body-only' : 'message--full')];
|
||||
const content = mEvent.getWireContent();
|
||||
const eventId = mEvent.getId();
|
||||
const msgType = content?.msgtype;
|
||||
const senderId = mEvent.getSender();
|
||||
const mxidColor = colorMXID(senderId);
|
||||
let { body } = content;
|
||||
const avatarSrc = mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop');
|
||||
const username = getUsernameOfRoomMember(mEvent.sender);
|
||||
const time = `${dateFormat(mEvent.getDate(), 'hh:MM TT')}`;
|
||||
|
||||
if (typeof body === 'undefined') return null;
|
||||
if (msgType === 'm.emote') className.push('message--type-emote');
|
||||
|
||||
// TODO: these line can be moved to option menu
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel;
|
||||
const canIRedact = room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
||||
|
||||
let [reply, reactions, isCustomHTML] = [null, null, content.format === 'org.matrix.custom.html'];
|
||||
const [isEdited, haveReactions] = [editedTimeline.has(eventId), reactionTimeline.has(eventId)];
|
||||
const isReply = typeof content['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
|
||||
|
||||
if (isEdited) {
|
||||
[body, isCustomHTML] = getEditedBody(eventId, editedTimeline);
|
||||
if (typeof body !== 'string') return null;
|
||||
}
|
||||
|
||||
if (haveReactions) {
|
||||
reactions = [];
|
||||
reactionTimeline.get(eventId).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 = mx.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() === mx.getUserId()),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isReply) {
|
||||
const parsedContent = parseReply(body);
|
||||
if (parsedContent !== null) {
|
||||
const c = room.currentState;
|
||||
const displayNameToUserIds = c.getUserIdsWithDisplayName(parsedContent.displayName);
|
||||
const ID = parsedContent.userId || displayNameToUserIds[0];
|
||||
reply = {
|
||||
color: colorMXID(ID || parsedContent.displayName),
|
||||
to: parsedContent.displayName || getUsername(parsedContent.userId),
|
||||
body: parsedContent.replyBody,
|
||||
};
|
||||
body = parsedContent.body;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className.join(' ')}>
|
||||
<div className="message__avatar-container">
|
||||
{avatar !== null && avatar}
|
||||
{!isBodyOnly && (
|
||||
<button type="button" onClick={() => openProfileViewer(senderId, roomId)}>
|
||||
<Avatar imageSrc={avatarSrc} text={username} bgColor={mxidColor} size="small" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="message__main-container">
|
||||
{header !== null && header}
|
||||
{reply !== null && reply}
|
||||
{content !== null && content}
|
||||
{editContent !== null && editContent}
|
||||
{reactions !== null && reactions}
|
||||
{options !== null && options}
|
||||
{!isBodyOnly && (
|
||||
<MessageHeader userId={senderId} name={username} color={mxidColor} time={time} />
|
||||
)}
|
||||
{reply !== null && (
|
||||
<MessageReply name={reply.to} color={reply.color} body={reply.body} />
|
||||
)}
|
||||
{!isEditing && (
|
||||
<MessageBody
|
||||
senderName={username}
|
||||
isCustomHTML={isCustomHTML}
|
||||
body={isMedia(mEvent) ? genMediaContent(mEvent) : body}
|
||||
msgType={msgType}
|
||||
isEdited={isEdited}
|
||||
/>
|
||||
)}
|
||||
{isEditing && (
|
||||
<MessageEdit
|
||||
body={body}
|
||||
onSave={(newBody) => {
|
||||
if (newBody !== body) {
|
||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||
}
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
)}
|
||||
{haveReactions && (
|
||||
<MessageReactionGroup>
|
||||
{
|
||||
reactions.map((reaction) => (
|
||||
<MessageReaction
|
||||
key={reaction.id}
|
||||
reaction={reaction.key}
|
||||
users={reaction.users}
|
||||
isActive={reaction.isActive}
|
||||
onClick={() => {
|
||||
toggleEmoji(roomId, eventId, reaction.key, roomTimeline);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
pickEmoji(e, roomId, eventId, roomTimeline);
|
||||
}}
|
||||
src={EmojiAddIC}
|
||||
size="extra-small"
|
||||
tooltip="Add reaction"
|
||||
/>
|
||||
</MessageReactionGroup>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<MessageOptions>
|
||||
<IconButton
|
||||
onClick={(e) => pickEmoji(e, roomId, eventId, roomTimeline)}
|
||||
src={EmojiAddIC}
|
||||
size="extra-small"
|
||||
tooltip="Add reaction"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => replyTo(senderId, eventId, body)}
|
||||
src={ReplyArrowIC}
|
||||
size="extra-small"
|
||||
tooltip="Reply"
|
||||
/>
|
||||
{(senderId === mx.getUserId() && !isMedia(mEvent)) && (
|
||||
<IconButton
|
||||
onClick={() => setIsEditing(true)}
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
tooltip="Edit"
|
||||
/>
|
||||
)}
|
||||
<ContextMenu
|
||||
content={() => (
|
||||
<>
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={TickMarkIC}
|
||||
onClick={() => openReadReceipts(roomId, eventId)}
|
||||
>
|
||||
Read receipts
|
||||
</MenuItem>
|
||||
{(canIRedact || senderId === mx.getUserId()) && (
|
||||
<>
|
||||
<MenuBorder />
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
iconSrc={BinIC}
|
||||
onClick={() => {
|
||||
if (window.confirm('Are you sure you want to delete this event')) {
|
||||
redactEvent(roomId, eventId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
render={(toggleMenu) => (
|
||||
<IconButton
|
||||
onClick={toggleMenu}
|
||||
src={VerticalMenuIC}
|
||||
size="extra-small"
|
||||
tooltip="Options"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</MessageOptions>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Message.defaultProps = {
|
||||
avatar: null,
|
||||
header: null,
|
||||
reply: null,
|
||||
content: null,
|
||||
editContent: null,
|
||||
reactions: null,
|
||||
options: null,
|
||||
msgType: 'm.text',
|
||||
isBodyOnly: false,
|
||||
};
|
||||
Message.propTypes = {
|
||||
avatar: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
reply: PropTypes.node,
|
||||
content: PropTypes.node,
|
||||
editContent: PropTypes.node,
|
||||
reactions: PropTypes.node,
|
||||
options: PropTypes.node,
|
||||
msgType: PropTypes.string,
|
||||
mEvent: PropTypes.shape({}).isRequired,
|
||||
isBodyOnly: PropTypes.bool,
|
||||
roomTimeline: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export {
|
||||
Message,
|
||||
MessageHeader,
|
||||
MessageReply,
|
||||
MessageContent,
|
||||
MessageEdit,
|
||||
MessageReactionGroup,
|
||||
MessageReaction,
|
||||
MessageOptions,
|
||||
PlaceholderMessage,
|
||||
};
|
||||
export { Message, MessageReply, PlaceholderMessage };
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
.message {
|
||||
&--full + &--full,
|
||||
&--content-only + &--full,
|
||||
&--body-only + &--full,
|
||||
& + .timeline-change,
|
||||
.timeline-change + & {
|
||||
margin-top: var(--sp-normal);
|
||||
|
@ -66,7 +66,7 @@
|
|||
}
|
||||
|
||||
&__header,
|
||||
&__content > div {
|
||||
&__body > div {
|
||||
margin: var(--sp-ultra-tight) 0;
|
||||
margin-right: var(--sp-extra-tight);
|
||||
height: var(--fs-b1);
|
||||
|
@ -82,20 +82,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&__content {
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&__content > div:nth-child(1n) {
|
||||
&__body > div:nth-child(1n) {
|
||||
max-width: 10%;
|
||||
}
|
||||
&__content > div:nth-child(2n) {
|
||||
&__body > div:nth-child(2n) {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.message__reply,
|
||||
.message__content,
|
||||
.message__body,
|
||||
.message__edit,
|
||||
.message__reactions {
|
||||
max-width: calc(100% - 88px);
|
||||
|
@ -153,7 +153,7 @@
|
|||
height: 14px;
|
||||
}
|
||||
}
|
||||
.message__content {
|
||||
.message__body {
|
||||
word-break: break-word;
|
||||
|
||||
& > .text > * {
|
||||
|
@ -265,7 +265,7 @@
|
|||
}
|
||||
|
||||
// markdown formating
|
||||
.message__content {
|
||||
.message__body {
|
||||
& h1,
|
||||
& h2 {
|
||||
color: var(--tc-surface-high);
|
||||
|
@ -403,7 +403,7 @@
|
|||
}
|
||||
|
||||
.message.message--type-emote {
|
||||
.message__content {
|
||||
.message__body {
|
||||
font-style: italic;
|
||||
|
||||
// Remove blockness of first `<p>` so that markdown emotes stay on one line.
|
||||
|
|
|
@ -12,7 +12,9 @@ import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-canc
|
|||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
|
||||
function TimelineChange({ variant, content, time, onClick }) {
|
||||
function TimelineChange({
|
||||
variant, content, time, onClick,
|
||||
}) {
|
||||
let iconSrc;
|
||||
|
||||
switch (variant) {
|
||||
|
@ -47,7 +49,6 @@ function TimelineChange({ variant, content, time, onClick }) {
|
|||
<div className="timeline-change__content">
|
||||
<Text variant="b2">
|
||||
{content}
|
||||
{/* <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify> */}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="timeline-change__time">
|
||||
|
|
|
@ -7,39 +7,14 @@ import dateFormat from 'dateformat';
|
|||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common';
|
||||
import { openEmojiBoard, openProfileViewer, openReadReceipts } from '../../../client/action/navigation';
|
||||
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 ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
||||
import {
|
||||
Message,
|
||||
MessageHeader,
|
||||
MessageReply,
|
||||
MessageContent,
|
||||
MessageEdit,
|
||||
MessageReactionGroup,
|
||||
MessageReaction,
|
||||
MessageOptions,
|
||||
PlaceholderMessage,
|
||||
} from '../../molecules/message/Message';
|
||||
import * as Media from '../../molecules/media/Media';
|
||||
import { Message, PlaceholderMessage } from '../../molecules/message/Message';
|
||||
import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
||||
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 VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
import { parseReply, parseTimelineChange } from './common';
|
||||
import { parseTimelineChange } from './common';
|
||||
|
||||
const MAX_MSG_DIFF_MINUTES = 5;
|
||||
|
||||
|
@ -52,84 +27,6 @@ function genPlaceholders(key) {
|
|||
);
|
||||
}
|
||||
|
||||
function isMedia(mE) {
|
||||
return (
|
||||
mE.getContent()?.msgtype === 'm.file'
|
||||
|| mE.getContent()?.msgtype === 'm.image'
|
||||
|| mE.getContent()?.msgtype === 'm.audio'
|
||||
|| mE.getContent()?.msgtype === 'm.video'
|
||||
|| mE.getType() === 'm.sticker'
|
||||
);
|
||||
}
|
||||
|
||||
function genMediaContent(mE) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const mContent = mE.getContent();
|
||||
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let mediaMXC = mContent?.url;
|
||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
||||
|
||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||
|
||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let msgType = mE.getContent()?.msgtype;
|
||||
if (mE.getType() === 'm.sticker') msgType = 'm.image';
|
||||
|
||||
switch (msgType) {
|
||||
case 'm.file':
|
||||
return (
|
||||
<Media.File
|
||||
name={mContent.body}
|
||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||
type={mContent.info?.mimetype}
|
||||
file={mContent.file || null}
|
||||
/>
|
||||
);
|
||||
case 'm.image':
|
||||
return (
|
||||
<Media.Image
|
||||
name={mContent.body}
|
||||
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||
height={typeof mContent.info?.h === 'number' ? 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 || null}
|
||||
/>
|
||||
);
|
||||
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={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
||||
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
||||
file={isEncryptedFile ? mContent.file : null}
|
||||
type={mContent.info?.mimetype}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
}
|
||||
}
|
||||
|
||||
function genRoomIntro(mEvent, roomTimeline) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||
|
@ -149,38 +46,6 @@ function genRoomIntro(mEvent, roomTimeline) {
|
|||
);
|
||||
}
|
||||
|
||||
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(getEventCords(e), (emoji) => {
|
||||
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
||||
e.target.click();
|
||||
});
|
||||
}
|
||||
|
||||
const scroll = {
|
||||
from: 0,
|
||||
limit: 0,
|
||||
|
@ -194,9 +59,10 @@ function RoomViewContent({
|
|||
}) {
|
||||
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
|
||||
const [onStateUpdate, updateState] = useState(null);
|
||||
const [editEvent, setEditEvent] = useState(null);
|
||||
|
||||
const mx = initMatrix.matrixClient;
|
||||
const noti = initMatrix.notifications;
|
||||
|
||||
if (scroll.limit === 0) {
|
||||
const from = roomTimeline.timeline.size - timelineScroll.maxEvents;
|
||||
scroll.from = (from < 0) ? 0 : from;
|
||||
|
@ -259,7 +125,7 @@ function RoomViewContent({
|
|||
if (roomTimeline.ongoingDecryptionCount === 0) updateState({});
|
||||
} else setIsReachedTimelineEnd(true);
|
||||
};
|
||||
// force update RoomTimeline on cons.events.roomTimeline.EVENT
|
||||
// force update RoomTimeline
|
||||
const updateRT = () => {
|
||||
if (timelineScroll.position === 'BOTTOM') {
|
||||
trySendingReadReceipt();
|
||||
|
@ -323,291 +189,37 @@ function RoomViewContent({
|
|||
}, [onStateUpdate]);
|
||||
|
||||
let prevMEvent = null;
|
||||
function genMessage(mEvent) {
|
||||
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'
|
||||
function renderMessage(mEvent) {
|
||||
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;
|
||||
const msgType = mEvent.getContent()?.msgtype;
|
||||
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 c = roomTimeline.room.currentState;
|
||||
const displayNameToUserIds = c.getUserIdsWithDisplayName(parsedContent.displayName);
|
||||
const ID = parsedContent.userId || displayNameToUserIds[0];
|
||||
reply = {
|
||||
color: colorMXID(ID || parsedContent.displayName),
|
||||
to: parsedContent.displayName || getUsername(parsedContent.userId),
|
||||
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 : (
|
||||
<button type="button" onClick={() => openProfileViewer(mEvent.sender.userId, roomId)}>
|
||||
<Avatar
|
||||
imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
|
||||
text={getUsernameOfRoomMember(mEvent.sender)}
|
||||
bgColor={senderMXIDColor}
|
||||
size="small"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
const userHeader = isContentOnly ? null : (
|
||||
<MessageHeader
|
||||
userId={mEvent.sender.userId}
|
||||
name={getUsernameOfRoomMember(mEvent.sender)}
|
||||
color={senderMXIDColor}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
/>
|
||||
);
|
||||
const userReply = reply === null ? null : (
|
||||
<MessageReply
|
||||
name={reply.to}
|
||||
color={reply.color}
|
||||
content={reply.content}
|
||||
/>
|
||||
);
|
||||
const userContent = (
|
||||
<MessageContent
|
||||
senderName={getUsernameOfRoomMember(mEvent.sender)}
|
||||
isMarkdown={isMarkdown}
|
||||
content={isMedia(mEvent) ? genMediaContent(mEvent) : content}
|
||||
msgType={msgType}
|
||||
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"
|
||||
/>
|
||||
{(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
|
||||
<IconButton
|
||||
onClick={() => setEditEvent(mEvent)}
|
||||
src={PencilIC}
|
||||
size="extra-small"
|
||||
tooltip="Edit"
|
||||
/>
|
||||
)}
|
||||
<ContextMenu
|
||||
content={() => (
|
||||
<>
|
||||
<MenuHeader>Options</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={EmojiAddIC}
|
||||
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
||||
>
|
||||
Add reaction
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={ReplyArrowIC}
|
||||
onClick={() => {
|
||||
viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
|
||||
}}
|
||||
>
|
||||
Reply
|
||||
</MenuItem>
|
||||
{(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
|
||||
<MenuItem iconSrc={PencilIC} onClick={() => setEditEvent(mEvent)}>Edit</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
iconSrc={TickMarkIC}
|
||||
onClick={() => openReadReceipts(roomId, mEvent.getId())}
|
||||
>
|
||||
Read receipts
|
||||
</MenuItem>
|
||||
{(canIRedact || mEvent.getSender() === mx.getUserId()) && (
|
||||
<>
|
||||
<MenuBorder />
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
iconSrc={BinIC}
|
||||
onClick={() => {
|
||||
if (window.confirm('Are you sure you want to delete this event')) {
|
||||
redactEvent(roomId, mEvent.getId());
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
render={(toggleMenu) => (
|
||||
<IconButton
|
||||
onClick={toggleMenu}
|
||||
src={VerticalMenuIC}
|
||||
size="extra-small"
|
||||
tooltip="Options"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</MessageOptions>
|
||||
);
|
||||
|
||||
const isEditingEvent = editEvent?.getId() === mEvent.getId();
|
||||
const myMessageEl = (
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
avatar={userAvatar}
|
||||
header={userHeader}
|
||||
reply={userReply}
|
||||
content={editEvent !== null && isEditingEvent ? null : userContent}
|
||||
msgType={msgType}
|
||||
editContent={editEvent !== null && isEditingEvent ? (
|
||||
<MessageEdit
|
||||
content={content}
|
||||
onSave={(newBody) => {
|
||||
if (newBody !== content) {
|
||||
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
||||
}
|
||||
setEditEvent(null);
|
||||
}}
|
||||
onCancel={() => setEditEvent(null)}
|
||||
/>
|
||||
) : null}
|
||||
reactions={userReactions}
|
||||
options={editEvent !== null && isEditingEvent ? null : userOptions}
|
||||
/>
|
||||
);
|
||||
return myMessageEl;
|
||||
}
|
||||
|
||||
function renderMessage(mEvent) {
|
||||
if (!cons.supportEventTypes.includes(mEvent.getType())) return false;
|
||||
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
||||
if (mEvent.isRedacted()) return false;
|
||||
|
||||
if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
|
||||
|
||||
let divider = null;
|
||||
let DividerComp = null;
|
||||
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
|
||||
divider = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
||||
DividerComp = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
||||
}
|
||||
prevMEvent = mEvent;
|
||||
|
||||
if (mEvent.getType() !== 'm.room.member') {
|
||||
const messageComp = genMessage(mEvent);
|
||||
prevMEvent = mEvent;
|
||||
if (mEvent.getType() === 'm.room.member') {
|
||||
const timelineChange = parseTimelineChange(mEvent);
|
||||
if (timelineChange === null) return false;
|
||||
return (
|
||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
||||
{divider}
|
||||
{messageComp}
|
||||
{DividerComp}
|
||||
<TimelineChange
|
||||
key={mEvent.getId()}
|
||||
variant={timelineChange.variant}
|
||||
content={timelineChange.content}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
prevMEvent = mEvent;
|
||||
const timelineChange = parseTimelineChange(mEvent);
|
||||
if (timelineChange === null) return false;
|
||||
return (
|
||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
||||
{divider}
|
||||
<TimelineChange
|
||||
key={mEvent.getId()}
|
||||
variant={timelineChange.variant}
|
||||
content={timelineChange.content}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
/>
|
||||
{DividerComp}
|
||||
<Message mEvent={mEvent} isBodyOnly={isContentOnly} roomTimeline={roomTimeline} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -625,7 +237,8 @@ function RoomViewContent({
|
|||
if (mEvent.getType() !== 'm.room.create' && !isReachedTimelineEnd) tl.push(genPlaceholders(1));
|
||||
if (mEvent.getType() !== 'm.room.create' && isReachedTimelineEnd) tl.push(genRoomIntro(undefined, roomTimeline));
|
||||
}
|
||||
tl.push(renderMessage(mEvent));
|
||||
if (mEvent.getType() === 'm.room.create') tl.push(genRoomIntro(mEvent, roomTimeline));
|
||||
else tl.push(renderMessage(mEvent));
|
||||
}
|
||||
i += 1;
|
||||
if (i > scroll.getEndIndex()) break;
|
||||
|
|
|
@ -9,6 +9,7 @@ import initMatrix from '../../../client/initMatrix';
|
|||
import cons from '../../../client/state/cons';
|
||||
import settings from '../../../client/state/settings';
|
||||
import { openEmojiBoard } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { bytesToSize, getEventCords } from '../../../util/common';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
@ -152,9 +153,9 @@ function RoomViewInput({
|
|||
textAreaRef.current.focus();
|
||||
}
|
||||
|
||||
function setUpReply(userId, eventId, content) {
|
||||
setReplyTo({ userId, eventId, content });
|
||||
roomsInput.setReplyTo(roomId, { userId, eventId, content });
|
||||
function setUpReply(userId, eventId, body) {
|
||||
setReplyTo({ userId, eventId, body });
|
||||
roomsInput.setReplyTo(roomId, { userId, eventId, body });
|
||||
focusInput();
|
||||
}
|
||||
|
||||
|
@ -164,7 +165,7 @@ function RoomViewInput({
|
|||
roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
||||
viewEvent.on('cmd_error', errorCmd);
|
||||
viewEvent.on('cmd_fired', firedCmd);
|
||||
viewEvent.on('reply_to', setUpReply);
|
||||
navigation.on(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
|
||||
if (textAreaRef?.current !== null) {
|
||||
isTyping = false;
|
||||
focusInput();
|
||||
|
@ -178,7 +179,7 @@ function RoomViewInput({
|
|||
roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
||||
viewEvent.removeListener('cmd_error', errorCmd);
|
||||
viewEvent.removeListener('cmd_fired', firedCmd);
|
||||
viewEvent.removeListener('reply_to', setUpReply);
|
||||
navigation.removeListener(cons.events.navigation.REPLY_TO_CLICKED, setUpReply);
|
||||
if (isCmdActivated) deactivateCmd();
|
||||
if (textAreaRef?.current === null) return;
|
||||
|
||||
|
@ -410,7 +411,7 @@ function RoomViewInput({
|
|||
userId={replyTo.userId}
|
||||
name={getUsername(replyTo.userId)}
|
||||
color={colorMXID(replyTo.userId)}
|
||||
content={replyTo.content}
|
||||
body={replyTo.body}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -165,27 +165,6 @@ function getUsersActionJsx(roomId, userIds, actionStr) {
|
|||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} other are {actionStr}</>;
|
||||
}
|
||||
|
||||
function parseReply(rawContent) {
|
||||
if (rawContent.indexOf('>') !== 0) return null;
|
||||
let content = rawContent.slice(rawContent.indexOf('<') + 1);
|
||||
const user = content.slice(0, content.indexOf('>'));
|
||||
|
||||
content = content.slice(content.indexOf('>') + 2);
|
||||
const replyContent = content.slice(0, content.indexOf('\n\n'));
|
||||
content = content.slice(content.indexOf('\n\n') + 2);
|
||||
|
||||
if (user === '') return null;
|
||||
|
||||
const isUserId = user.match(/^@.+:.+/);
|
||||
|
||||
return {
|
||||
userId: isUserId ? user : null,
|
||||
displayName: isUserId ? null : user,
|
||||
replyContent,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
function parseTimelineChange(mEvent) {
|
||||
const tJSXMsgs = getTimelineJSXMessages();
|
||||
const makeReturnObj = (variant, content) => ({
|
||||
|
@ -237,6 +216,5 @@ function parseTimelineChange(mEvent) {
|
|||
export {
|
||||
getTimelineJSXMessages,
|
||||
getUsersActionJsx,
|
||||
parseReply,
|
||||
parseTimelineChange,
|
||||
};
|
||||
|
|
|
@ -87,6 +87,15 @@ function openRoomOptions(cords, roomId) {
|
|||
});
|
||||
}
|
||||
|
||||
function replyTo(userId, eventId, body) {
|
||||
appDispatcher.dispatch({
|
||||
type: cons.actions.navigation.CLICK_REPLY_TO,
|
||||
userId,
|
||||
eventId,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
selectTab,
|
||||
selectSpace,
|
||||
|
@ -100,4 +109,5 @@ export {
|
|||
openEmojiBoard,
|
||||
openReadReceipts,
|
||||
openRoomOptions,
|
||||
replyTo,
|
||||
};
|
||||
|
|
|
@ -95,13 +95,13 @@ function getFormattedBody(markdown) {
|
|||
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>`;
|
||||
const formattedReply = getFormattedBody(reply.content.replaceAll('\n', '\n> '));
|
||||
const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> '));
|
||||
return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedReply}</blockquote></mx-reply>`;
|
||||
}
|
||||
|
||||
function bindReplyToContent(roomId, reply, content) {
|
||||
const newContent = { ...content };
|
||||
newContent.body = `> <${reply.userId}> ${reply.content.replaceAll('\n', '\n> ')}`;
|
||||
newContent.body = `> <${reply.userId}> ${reply.body.replaceAll('\n', '\n> ')}`;
|
||||
newContent.body += `\n\n${content.body}`;
|
||||
newContent.format = 'org.matrix.custom.html';
|
||||
newContent['m.relates_to'] = content['m.relates_to'] || {};
|
||||
|
|
|
@ -34,6 +34,7 @@ const cons = {
|
|||
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
|
||||
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
|
||||
OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS',
|
||||
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
|
||||
},
|
||||
room: {
|
||||
JOIN: 'JOIN',
|
||||
|
@ -65,6 +66,7 @@ const cons = {
|
|||
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
|
||||
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
|
||||
ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED',
|
||||
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
|
||||
},
|
||||
roomList: {
|
||||
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
|
||||
|
|
|
@ -90,6 +90,14 @@ class Navigation extends EventEmitter {
|
|||
action.roomId,
|
||||
);
|
||||
},
|
||||
[cons.actions.navigation.CLICK_REPLY_TO]: () => {
|
||||
this.emit(
|
||||
cons.events.navigation.REPLY_TO_CLICKED,
|
||||
action.userId,
|
||||
action.eventId,
|
||||
action.body,
|
||||
);
|
||||
},
|
||||
};
|
||||
actions[action.type]?.();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue