2021-08-20 15:42:07 +02:00
|
|
|
import React, { useRef } from 'react';
|
2021-07-28 15:15:52 +02:00
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import './Message.scss';
|
|
|
|
|
|
|
|
import Linkify from 'linkifyjs/react';
|
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
|
import gfm from 'remark-gfm';
|
|
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
|
|
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
2021-08-10 13:28:44 +02:00
|
|
|
import parse from 'html-react-parser';
|
|
|
|
import twemoji from 'twemoji';
|
|
|
|
import { getUsername } from '../../../util/matrixUtil';
|
2021-07-28 15:15:52 +02:00
|
|
|
|
|
|
|
import Text from '../../atoms/text/Text';
|
|
|
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
2021-08-20 15:42:07 +02:00
|
|
|
import Button from '../../atoms/button/Button';
|
2021-08-10 13:28:44 +02:00
|
|
|
import Tooltip from '../../atoms/tooltip/Tooltip';
|
2021-08-20 15:42:07 +02:00
|
|
|
import Input from '../../atoms/input/Input';
|
2021-07-28 15:15:52 +02:00
|
|
|
|
|
|
|
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
|
|
|
|
|
|
|
const components = {
|
|
|
|
code({
|
|
|
|
// eslint-disable-next-line react/prop-types
|
|
|
|
inline, className, children,
|
|
|
|
}) {
|
|
|
|
const match = /language-(\w+)/.exec(className || '');
|
|
|
|
return !inline && match ? (
|
|
|
|
<SyntaxHighlighter
|
|
|
|
style={coy}
|
|
|
|
language={match[1]}
|
|
|
|
PreTag="div"
|
|
|
|
showLineNumbers
|
|
|
|
>
|
|
|
|
{String(children).replace(/\n$/, '')}
|
|
|
|
</SyntaxHighlighter>
|
|
|
|
) : (
|
|
|
|
<code className={className}>{String(children)}</code>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
function linkifyContent(content) {
|
|
|
|
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
|
|
|
|
}
|
|
|
|
function genMarkdown(content) {
|
|
|
|
return <ReactMarkdown remarkPlugins={[gfm]} components={components} linkTarget="_blank">{content}</ReactMarkdown>;
|
|
|
|
}
|
|
|
|
|
|
|
|
function PlaceholderMessage() {
|
|
|
|
return (
|
|
|
|
<div className="ph-msg">
|
|
|
|
<div className="ph-msg__avatar-container">
|
|
|
|
<div className="ph-msg__avatar" />
|
|
|
|
</div>
|
|
|
|
<div className="ph-msg__main-container">
|
|
|
|
<div className="ph-msg__header" />
|
|
|
|
<div className="ph-msg__content">
|
|
|
|
<div />
|
|
|
|
<div />
|
|
|
|
<div />
|
|
|
|
<div />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-10 13:28:44 +02:00
|
|
|
function MessageHeader({
|
|
|
|
userId, name, color, time,
|
|
|
|
}) {
|
|
|
|
return (
|
|
|
|
<div className="message__header">
|
|
|
|
<div style={{ color }} className="message__profile">
|
|
|
|
<Text variant="b1">{name}</Text>
|
2021-08-11 10:28:53 +02:00
|
|
|
<Text variant="b1">{userId}</Text>
|
2021-08-10 13:28:44 +02:00
|
|
|
</div>
|
|
|
|
<div className="message__time">
|
|
|
|
<Text variant="b3">{time}</Text>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageHeader.propTypes = {
|
|
|
|
userId: PropTypes.string.isRequired,
|
|
|
|
name: PropTypes.string.isRequired,
|
|
|
|
color: PropTypes.string.isRequired,
|
|
|
|
time: PropTypes.string.isRequired,
|
|
|
|
};
|
|
|
|
|
2021-08-18 12:21:57 +02:00
|
|
|
function MessageReply({ name, color, content }) {
|
2021-08-10 13:28:44 +02:00
|
|
|
return (
|
|
|
|
<div className="message__reply">
|
|
|
|
<Text variant="b2">
|
|
|
|
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
|
|
|
|
<span style={{ color }}>{name}</span>
|
|
|
|
<>{` ${content}`}</>
|
|
|
|
</Text>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageReply.propTypes = {
|
|
|
|
name: PropTypes.string.isRequired,
|
|
|
|
color: PropTypes.string.isRequired,
|
|
|
|
content: PropTypes.string.isRequired,
|
|
|
|
};
|
|
|
|
|
|
|
|
function MessageContent({ content, isMarkdown, isEdited }) {
|
|
|
|
return (
|
|
|
|
<div className="message__content">
|
|
|
|
<div className="text text-b1">
|
|
|
|
{ isMarkdown ? genMarkdown(content) : linkifyContent(content) }
|
|
|
|
</div>
|
2021-08-11 09:59:01 +02:00
|
|
|
{ isEdited && <Text className="message__content-edited" variant="b3">(edited)</Text>}
|
2021-08-10 13:28:44 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageContent.defaultProps = {
|
|
|
|
isMarkdown: false,
|
|
|
|
isEdited: false,
|
|
|
|
};
|
|
|
|
MessageContent.propTypes = {
|
|
|
|
content: PropTypes.node.isRequired,
|
|
|
|
isMarkdown: PropTypes.bool,
|
|
|
|
isEdited: PropTypes.bool,
|
|
|
|
};
|
|
|
|
|
2021-08-20 15:42:07 +02:00
|
|
|
function MessageEdit({ content, onSave, onCancel }) {
|
|
|
|
const editInputRef = useRef(null);
|
|
|
|
return (
|
|
|
|
<form className="message__edit" onSubmit={(e) => { e.preventDefault(); onSave(editInputRef.current.value); }}>
|
|
|
|
<Input
|
|
|
|
forwardRef={editInputRef}
|
|
|
|
value={content}
|
|
|
|
placeholder="Edit message"
|
|
|
|
required
|
|
|
|
resizable
|
|
|
|
/>
|
|
|
|
<div className="message__edit-btns">
|
|
|
|
<Button type="submit" variant="primary">Save</Button>
|
|
|
|
<Button onClick={onCancel}>Cancel</Button>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageEdit.propTypes = {
|
|
|
|
content: PropTypes.string.isRequired,
|
|
|
|
onSave: PropTypes.func.isRequired,
|
|
|
|
onCancel: PropTypes.func.isRequired,
|
|
|
|
};
|
|
|
|
|
2021-08-10 13:28:44 +02:00
|
|
|
function MessageReactionGroup({ children }) {
|
|
|
|
return (
|
|
|
|
<div className="message__reactions text text-b3 noselect">
|
|
|
|
{ children }
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageReactionGroup.propTypes = {
|
|
|
|
children: PropTypes.node.isRequired,
|
|
|
|
};
|
|
|
|
|
|
|
|
function genReactionMsg(userIds, reaction) {
|
2021-08-11 09:59:01 +02:00
|
|
|
const genLessContText = (text) => <span style={{ opacity: '.6' }}>{text}</span>;
|
|
|
|
let msg = <></>;
|
2021-08-10 13:28:44 +02:00
|
|
|
userIds.forEach((userId, index) => {
|
2021-08-11 09:59:01 +02:00
|
|
|
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)}</>;
|
2021-08-10 13:28:44 +02:00
|
|
|
});
|
|
|
|
return (
|
|
|
|
<>
|
2021-08-11 09:59:01 +02:00
|
|
|
{msg}
|
|
|
|
{genLessContText(' reacted with')}
|
2021-08-10 13:28:44 +02:00
|
|
|
{parse(twemoji.parse(reaction))}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function MessageReaction({
|
|
|
|
reaction, users, isActive, onClick,
|
|
|
|
}) {
|
|
|
|
return (
|
|
|
|
<Tooltip
|
|
|
|
className="msg__reaction-tooltip"
|
|
|
|
content={<Text variant="b2">{genReactionMsg(users, reaction)}</Text>}
|
|
|
|
>
|
|
|
|
<button
|
|
|
|
onClick={onClick}
|
|
|
|
type="button"
|
|
|
|
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
|
|
|
|
>
|
|
|
|
{ parse(twemoji.parse(reaction)) }
|
|
|
|
<Text variant="b3" className="msg__reaction-count">{users.length}</Text>
|
|
|
|
</button>
|
|
|
|
</Tooltip>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageReaction.propTypes = {
|
|
|
|
reaction: PropTypes.node.isRequired,
|
|
|
|
users: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
|
|
isActive: PropTypes.bool.isRequired,
|
|
|
|
onClick: PropTypes.func.isRequired,
|
|
|
|
};
|
|
|
|
|
2021-08-11 09:59:01 +02:00
|
|
|
function MessageOptions({ children }) {
|
|
|
|
return (
|
|
|
|
<div className="message__options">
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
MessageOptions.propTypes = {
|
|
|
|
children: PropTypes.node.isRequired,
|
|
|
|
};
|
|
|
|
|
2021-07-28 15:15:52 +02:00
|
|
|
function Message({
|
2021-08-20 15:42:07 +02:00
|
|
|
avatar, header, reply, content, editContent, reactions, options,
|
2021-07-28 15:15:52 +02:00
|
|
|
}) {
|
2021-08-10 13:28:44 +02:00
|
|
|
const msgClass = header === null ? ' message--content-only' : ' message--full';
|
2021-07-28 15:15:52 +02:00
|
|
|
return (
|
2021-08-10 13:28:44 +02:00
|
|
|
<div className={`message${msgClass}`}>
|
2021-07-28 15:15:52 +02:00
|
|
|
<div className="message__avatar-container">
|
2021-08-10 13:28:44 +02:00
|
|
|
{avatar !== null && avatar}
|
2021-07-28 15:15:52 +02:00
|
|
|
</div>
|
|
|
|
<div className="message__main-container">
|
2021-08-10 13:28:44 +02:00
|
|
|
{header !== null && header}
|
|
|
|
{reply !== null && reply}
|
2021-08-20 15:42:07 +02:00
|
|
|
{content !== null && content}
|
|
|
|
{editContent !== null && editContent}
|
2021-08-10 13:28:44 +02:00
|
|
|
{reactions !== null && reactions}
|
2021-08-11 09:59:01 +02:00
|
|
|
{options !== null && options}
|
2021-07-28 15:15:52 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Message.defaultProps = {
|
2021-08-10 13:28:44 +02:00
|
|
|
avatar: null,
|
|
|
|
header: null,
|
2021-07-28 15:15:52 +02:00
|
|
|
reply: null,
|
2021-08-20 15:42:07 +02:00
|
|
|
content: null,
|
|
|
|
editContent: null,
|
2021-07-28 15:15:52 +02:00
|
|
|
reactions: null,
|
2021-08-11 09:59:01 +02:00
|
|
|
options: null,
|
2021-07-28 15:15:52 +02:00
|
|
|
};
|
|
|
|
Message.propTypes = {
|
2021-08-10 13:28:44 +02:00
|
|
|
avatar: PropTypes.node,
|
|
|
|
header: PropTypes.node,
|
|
|
|
reply: PropTypes.node,
|
2021-08-20 15:42:07 +02:00
|
|
|
content: PropTypes.node,
|
|
|
|
editContent: PropTypes.node,
|
2021-08-10 13:28:44 +02:00
|
|
|
reactions: PropTypes.node,
|
2021-08-11 09:59:01 +02:00
|
|
|
options: PropTypes.node,
|
2021-07-28 15:15:52 +02:00
|
|
|
};
|
|
|
|
|
2021-08-10 13:28:44 +02:00
|
|
|
export {
|
|
|
|
Message,
|
|
|
|
MessageHeader,
|
|
|
|
MessageReply,
|
|
|
|
MessageContent,
|
2021-08-20 15:42:07 +02:00
|
|
|
MessageEdit,
|
2021-08-10 13:28:44 +02:00
|
|
|
MessageReactionGroup,
|
|
|
|
MessageReaction,
|
2021-08-11 09:59:01 +02:00
|
|
|
MessageOptions,
|
2021-08-10 13:28:44 +02:00
|
|
|
PlaceholderMessage,
|
|
|
|
};
|