refactored message compnonent
This commit is contained in:
parent
d0111e7741
commit
d03fc2fcf1
3 changed files with 259 additions and 132 deletions
|
@ -7,10 +7,14 @@ 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';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { getUsername } from '../../../util/matrixUtil';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||
|
||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||
|
||||
|
@ -61,89 +65,158 @@ function PlaceholderMessage() {
|
|||
);
|
||||
}
|
||||
|
||||
function Message({
|
||||
color, avatarSrc, name, content,
|
||||
time, markdown, contentOnly, reply,
|
||||
edited, reactions,
|
||||
function MessageHeader({
|
||||
userId, name, color, time,
|
||||
}) {
|
||||
const msgClass = contentOnly ? 'message--content-only' : 'message--full';
|
||||
return (
|
||||
<div className={`message ${msgClass}`}>
|
||||
<div className="message__avatar-container">
|
||||
{!contentOnly && <Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="small" />}
|
||||
<div className="message__header">
|
||||
<div style={{ color }} className="message__profile">
|
||||
<Text variant="b1">{name}</Text>
|
||||
</div>
|
||||
<div className="message__main-container">
|
||||
{ !contentOnly && (
|
||||
<div className="message__header">
|
||||
<div style={{ color }} className="message__profile">
|
||||
<Text variant="b1">{name}</Text>
|
||||
</div>
|
||||
<div className="message__time">
|
||||
<Text variant="b3">{time}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="message__content">
|
||||
{ reply !== null && (
|
||||
<div className="message__reply-content">
|
||||
<Text variant="b2">
|
||||
<RawIcon color={reply.color} size="extra-small" src={ReplyArrowIC} />
|
||||
<span style={{ color: reply.color }}>{reply.to}</span>
|
||||
<>{` ${reply.content}`}</>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="text text-b1">
|
||||
{ markdown ? genMarkdown(content) : linkifyContent(content) }
|
||||
</div>
|
||||
{ edited && <Text className="message__edited" variant="b3">(edited)</Text>}
|
||||
{ reactions && (
|
||||
<div className="message__reactions text text-b3 noselect">
|
||||
{
|
||||
reactions.map((reaction) => (
|
||||
<button key={reaction.id} onClick={() => alert('Sending reactions is yet to be implemented.')} type="button" className={`msg__reaction${reaction.active ? ' msg__reaction--active' : ''}`}>
|
||||
{`${reaction.key} ${reaction.count}`}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</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,
|
||||
};
|
||||
|
||||
function MessageReply({
|
||||
userId, name, color, content,
|
||||
}) {
|
||||
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 = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
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>
|
||||
{ isEdited && <Text className="message__edited" variant="b3">(edited)</Text>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MessageContent.defaultProps = {
|
||||
isMarkdown: false,
|
||||
isEdited: false,
|
||||
};
|
||||
MessageContent.propTypes = {
|
||||
content: PropTypes.node.isRequired,
|
||||
isMarkdown: PropTypes.bool,
|
||||
isEdited: PropTypes.bool,
|
||||
};
|
||||
|
||||
function MessageReactionGroup({ children }) {
|
||||
return (
|
||||
<div className="message__reactions text text-b3 noselect">
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
MessageReactionGroup.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function genReactionMsg(userIds, reaction) {
|
||||
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)}`;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{`${msg} reacted with`}
|
||||
{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,
|
||||
};
|
||||
|
||||
function Message({
|
||||
avatar, header, reply, content, reactions,
|
||||
}) {
|
||||
const msgClass = header === null ? ' message--content-only' : ' message--full';
|
||||
return (
|
||||
<div className={`message${msgClass}`}>
|
||||
<div className="message__avatar-container">
|
||||
{avatar !== null && avatar}
|
||||
</div>
|
||||
<div className="message__main-container">
|
||||
{header !== null && header}
|
||||
{reply !== null && reply}
|
||||
{content}
|
||||
{reactions !== null && reactions}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Message.defaultProps = {
|
||||
color: 'var(--tc-surface-high)',
|
||||
avatarSrc: null,
|
||||
markdown: false,
|
||||
contentOnly: false,
|
||||
avatar: null,
|
||||
header: null,
|
||||
reply: null,
|
||||
edited: false,
|
||||
reactions: null,
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
color: PropTypes.string,
|
||||
avatarSrc: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
avatar: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
reply: PropTypes.node,
|
||||
content: PropTypes.node.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
markdown: PropTypes.bool,
|
||||
contentOnly: PropTypes.bool,
|
||||
reply: PropTypes.shape({
|
||||
color: PropTypes.string.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
}),
|
||||
edited: PropTypes.bool,
|
||||
reactions: PropTypes.arrayOf(PropTypes.exact({
|
||||
id: PropTypes.string,
|
||||
key: PropTypes.string,
|
||||
count: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
})),
|
||||
reactions: PropTypes.node,
|
||||
};
|
||||
|
||||
export { Message as default, PlaceholderMessage };
|
||||
export {
|
||||
Message,
|
||||
MessageHeader,
|
||||
MessageReply,
|
||||
MessageContent,
|
||||
MessageReactionGroup,
|
||||
MessageReaction,
|
||||
PlaceholderMessage,
|
||||
};
|
||||
|
|
|
@ -49,24 +49,9 @@
|
|||
&__avatar-container {
|
||||
width: var(--av-small);
|
||||
}
|
||||
&__reply-content {
|
||||
.text {
|
||||
color: var(--tc-surface-low);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ic-raw {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
&__edited {
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
&__reactions {
|
||||
margin-top: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
|
||||
.ph-msg {
|
||||
|
@ -106,6 +91,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.message__reply,
|
||||
.message__content,
|
||||
.message__reactions {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
|
||||
.message__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
@ -130,8 +122,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.message__reply {
|
||||
.text {
|
||||
color: var(--tc-surface-low);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ic-raw {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
.message__content {
|
||||
max-width: 640px;
|
||||
word-break: break-word;
|
||||
|
||||
& > .text > * {
|
||||
|
@ -142,20 +145,36 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.message__reactions {
|
||||
display: flex;
|
||||
}
|
||||
.msg__reaction {
|
||||
--reaction-height: 24px;
|
||||
--reaction-padding: 6px;
|
||||
--reaction-radius: calc(var(--bo-radius) / 2);
|
||||
margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0;
|
||||
padding: 0 var(--sp-ultra-tight);
|
||||
min-height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--tc-surface-normal);
|
||||
background-color: var(--bg-surface-low);
|
||||
border: 1px solid var(--bg-surface-border);
|
||||
padding: 0 var(--reaction-padding);
|
||||
border-radius: var(--reaction-radius);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
height: var(--reaction-height);
|
||||
|
||||
margin-right: var(--sp-extra-tight);
|
||||
& .emoji {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 2px;
|
||||
}
|
||||
&-count {
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
color: var(--tc-surface-normal)
|
||||
}
|
||||
&-tooltip .emoji {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
[dir=rtl] & {
|
||||
margin: {
|
||||
|
@ -188,7 +207,7 @@
|
|||
}
|
||||
|
||||
// markdown formating
|
||||
.message {
|
||||
.message__content {
|
||||
& h1,
|
||||
& h2 {
|
||||
color: var(--tc-surface-high);
|
||||
|
|
|
@ -12,7 +12,16 @@ import colorMXID from '../../../util/colorMXID';
|
|||
import { diffMinutes, isNotInSameDay } from '../../../util/common';
|
||||
|
||||
import Divider from '../../atoms/divider/Divider';
|
||||
import Message, { PlaceholderMessage } from '../../molecules/message/Message';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import {
|
||||
Message,
|
||||
MessageHeader,
|
||||
MessageReply,
|
||||
MessageContent,
|
||||
MessageReactionGroup,
|
||||
MessageReaction,
|
||||
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';
|
||||
|
@ -224,6 +233,7 @@ function ChannelViewContent({
|
|||
if (parsedContent !== null) {
|
||||
const username = getUsername(parsedContent.userId);
|
||||
reply = {
|
||||
userId: parsedContent.userId,
|
||||
color: colorMXID(parsedContent.userId),
|
||||
to: username,
|
||||
content: parsedContent.replyContent,
|
||||
|
@ -259,9 +269,10 @@ function ChannelViewContent({
|
|||
if (alreadyHaveThisReaction(rEvent)) {
|
||||
for (let i = 0; i < reactions.length; i += 1) {
|
||||
if (reactions[i].key === rEvent.getRelation().key) {
|
||||
reactions[i].count += 1;
|
||||
if (reactions[i].active !== true) {
|
||||
reactions[i].active = rEvent.getSender() === initMatrix.matrixClient.getUserId();
|
||||
reactions[i].users.push(rEvent.getSender());
|
||||
if (reactions[i].isActive !== true) {
|
||||
const myUserId = initMatrix.matrixClient.getUserId();
|
||||
reactions[i].isActive = rEvent.getSender() === myUserId;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -270,46 +281,70 @@ function ChannelViewContent({
|
|||
reactions.push({
|
||||
id: rEvent.getId(),
|
||||
key: rEvent.getRelation().key,
|
||||
count: 1,
|
||||
active: (rEvent.getSender() === initMatrix.matrixClient.getUserId()),
|
||||
users: [rEvent.getSender()],
|
||||
isActive: (rEvent.getSender() === initMatrix.matrixClient.getUserId()),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const userMXIDColor = colorMXID(mEvent.sender.userId);
|
||||
const userAvatar = isContentOnly ? null : (
|
||||
<Avatar
|
||||
imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
|
||||
text={getUsername(mEvent.sender.userId).slice(0, 1)}
|
||||
bgColor={userMXIDColor}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
const userHeader = isContentOnly ? null : (
|
||||
<MessageHeader
|
||||
userId={mEvent.sender.userId}
|
||||
name={getUsername(mEvent.sender.userId)}
|
||||
color={userMXIDColor}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
/>
|
||||
);
|
||||
const userReply = reply === null ? null : (
|
||||
<MessageReply
|
||||
userId={reply.userId}
|
||||
name={reply.to}
|
||||
color={reply.color}
|
||||
content={reply.content}
|
||||
/>
|
||||
);
|
||||
const userContent = (
|
||||
<MessageContent
|
||||
isMarkdown={isMarkdown}
|
||||
content={isMedia(mEvent) ? genMediaContent(mEvent) : content}
|
||||
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={() => alert('Sending reactions is yet to be implemented.')}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</MessageReactionGroup>
|
||||
);
|
||||
|
||||
const myMessageEl = (
|
||||
<React.Fragment key={`box-${mEvent.getId()}`}>
|
||||
{divider}
|
||||
{ isMedia(mEvent) ? (
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
contentOnly={isContentOnly}
|
||||
markdown={isMarkdown}
|
||||
avatarSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
|
||||
color={colorMXID(mEvent.sender.userId)}
|
||||
name={getUsername(mEvent.sender.userId)}
|
||||
content={genMediaContent(mEvent)}
|
||||
reply={reply}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
edited={isEdited}
|
||||
reactions={reactions}
|
||||
/>
|
||||
) : (
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
contentOnly={isContentOnly}
|
||||
markdown={isMarkdown}
|
||||
avatarSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
|
||||
color={colorMXID(mEvent.sender.userId)}
|
||||
name={getUsername(mEvent.sender.userId)}
|
||||
content={content}
|
||||
reply={reply}
|
||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||
edited={isEdited}
|
||||
reactions={reactions}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
avatar={userAvatar}
|
||||
header={userHeader}
|
||||
reply={userReply}
|
||||
content={userContent}
|
||||
reactions={userReactions}
|
||||
/>
|
||||
);
|
||||
|
||||
prevMEvent = mEvent;
|
||||
|
|
Loading…
Reference in a new issue