refactored message compnonent

This commit is contained in:
unknown 2021-08-10 16:58:44 +05:30
parent d0111e7741
commit d03fc2fcf1
3 changed files with 259 additions and 132 deletions

View file

@ -7,10 +7,14 @@ import ReactMarkdown from 'react-markdown';
import gfm from 'remark-gfm'; import gfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism'; 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 Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon'; import RawIcon from '../../atoms/system-icons/RawIcon';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import Tooltip from '../../atoms/tooltip/Tooltip';
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
@ -61,19 +65,10 @@ function PlaceholderMessage() {
); );
} }
function Message({ function MessageHeader({
color, avatarSrc, name, content, userId, name, color, time,
time, markdown, contentOnly, reply,
edited, reactions,
}) { }) {
const msgClass = contentOnly ? 'message--content-only' : 'message--full';
return ( return (
<div className={`message ${msgClass}`}>
<div className="message__avatar-container">
{!contentOnly && <Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="small" />}
</div>
<div className="message__main-container">
{ !contentOnly && (
<div className="message__header"> <div className="message__header">
<div style={{ color }} className="message__profile"> <div style={{ color }} className="message__profile">
<Text variant="b1">{name}</Text> <Text variant="b1">{name}</Text>
@ -82,68 +77,146 @@ function Message({
<Text variant="b3">{time}</Text> <Text variant="b3">{time}</Text>
</div> </div>
</div> </div>
)} );
<div className="message__content"> }
{ reply !== null && ( MessageHeader.propTypes = {
<div className="message__reply-content"> 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"> <Text variant="b2">
<RawIcon color={reply.color} size="extra-small" src={ReplyArrowIC} /> <RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
<span style={{ color: reply.color }}>{reply.to}</span> <span style={{ color }}>{name}</span>
<>{` ${reply.content}`}</> <>{` ${content}`}</>
</Text> </Text>
</div> </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>
</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 = { Message.defaultProps = {
color: 'var(--tc-surface-high)', avatar: null,
avatarSrc: null, header: null,
markdown: false,
contentOnly: false,
reply: null, reply: null,
edited: false,
reactions: null, reactions: null,
}; };
Message.propTypes = { Message.propTypes = {
color: PropTypes.string, avatar: PropTypes.node,
avatarSrc: PropTypes.string, header: PropTypes.node,
name: PropTypes.string.isRequired, reply: PropTypes.node,
content: PropTypes.node.isRequired, content: PropTypes.node.isRequired,
time: PropTypes.string.isRequired, reactions: PropTypes.node,
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,
})),
}; };
export { Message as default, PlaceholderMessage }; export {
Message,
MessageHeader,
MessageReply,
MessageContent,
MessageReactionGroup,
MessageReaction,
PlaceholderMessage,
};

View file

@ -49,24 +49,9 @@
&__avatar-container { &__avatar-container {
width: var(--av-small); 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 { &__edited {
color: var(--tc-surface-low); color: var(--tc-surface-low);
} }
&__reactions {
margin-top: var(--sp-ultra-tight);
}
} }
.ph-msg { .ph-msg {
@ -106,6 +91,13 @@
} }
} }
.message__reply,
.message__content,
.message__reactions {
max-width: 640px;
}
.message__header { .message__header {
display: flex; display: flex;
align-items: baseline; 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 { .message__content {
max-width: 640px;
word-break: break-word; word-break: break-word;
& > .text > * { & > .text > * {
@ -142,20 +145,36 @@
word-break: break-all; word-break: break-all;
} }
} }
.message__reactions {
display: flex;
}
.msg__reaction { .msg__reaction {
--reaction-height: 24px; margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0;
--reaction-padding: 6px; padding: 0 var(--sp-ultra-tight);
--reaction-radius: calc(var(--bo-radius) / 2); min-height: 26px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: var(--tc-surface-normal); color: var(--tc-surface-normal);
background-color: var(--bg-surface-low);
border: 1px solid var(--bg-surface-border); border: 1px solid var(--bg-surface-border);
padding: 0 var(--reaction-padding); border-radius: 4px;
border-radius: var(--reaction-radius);
cursor: pointer; 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] & { [dir=rtl] & {
margin: { margin: {
@ -188,7 +207,7 @@
} }
// markdown formating // markdown formating
.message { .message__content {
& h1, & h1,
& h2 { & h2 {
color: var(--tc-surface-high); color: var(--tc-surface-high);

View file

@ -12,7 +12,16 @@ import colorMXID from '../../../util/colorMXID';
import { diffMinutes, isNotInSameDay } from '../../../util/common'; import { diffMinutes, isNotInSameDay } from '../../../util/common';
import Divider from '../../atoms/divider/Divider'; 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 * as Media from '../../molecules/media/Media';
import ChannelIntro from '../../molecules/channel-intro/ChannelIntro'; import ChannelIntro from '../../molecules/channel-intro/ChannelIntro';
import TimelineChange from '../../molecules/message/TimelineChange'; import TimelineChange from '../../molecules/message/TimelineChange';
@ -224,6 +233,7 @@ function ChannelViewContent({
if (parsedContent !== null) { if (parsedContent !== null) {
const username = getUsername(parsedContent.userId); const username = getUsername(parsedContent.userId);
reply = { reply = {
userId: parsedContent.userId,
color: colorMXID(parsedContent.userId), color: colorMXID(parsedContent.userId),
to: username, to: username,
content: parsedContent.replyContent, content: parsedContent.replyContent,
@ -259,9 +269,10 @@ function ChannelViewContent({
if (alreadyHaveThisReaction(rEvent)) { if (alreadyHaveThisReaction(rEvent)) {
for (let i = 0; i < reactions.length; i += 1) { for (let i = 0; i < reactions.length; i += 1) {
if (reactions[i].key === rEvent.getRelation().key) { if (reactions[i].key === rEvent.getRelation().key) {
reactions[i].count += 1; reactions[i].users.push(rEvent.getSender());
if (reactions[i].active !== true) { if (reactions[i].isActive !== true) {
reactions[i].active = rEvent.getSender() === initMatrix.matrixClient.getUserId(); const myUserId = initMatrix.matrixClient.getUserId();
reactions[i].isActive = rEvent.getSender() === myUserId;
} }
break; break;
} }
@ -270,46 +281,70 @@ function ChannelViewContent({
reactions.push({ reactions.push({
id: rEvent.getId(), id: rEvent.getId(),
key: rEvent.getRelation().key, key: rEvent.getRelation().key,
count: 1, users: [rEvent.getSender()],
active: (rEvent.getSender() === initMatrix.matrixClient.getUserId()), 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 = ( const myMessageEl = (
<React.Fragment key={`box-${mEvent.getId()}`}>
{divider}
{ isMedia(mEvent) ? (
<Message <Message
key={mEvent.getId()} key={mEvent.getId()}
contentOnly={isContentOnly} avatar={userAvatar}
markdown={isMarkdown} header={userHeader}
avatarSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')} reply={userReply}
color={colorMXID(mEvent.sender.userId)} content={userContent}
name={getUsername(mEvent.sender.userId)} reactions={userReactions}
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>
); );
prevMEvent = mEvent; prevMEvent = mEvent;