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 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,89 +65,158 @@ 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__header">
|
||||||
<div className="message__avatar-container">
|
<div style={{ color }} className="message__profile">
|
||||||
{!contentOnly && <Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="small" />}
|
<Text variant="b1">{name}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="message__main-container">
|
<div className="message__time">
|
||||||
{ !contentOnly && (
|
<Text variant="b3">{time}</Text>
|
||||||
<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>
|
</div>
|
||||||
</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 = {
|
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,
|
||||||
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()}`}>
|
<Message
|
||||||
{divider}
|
key={mEvent.getId()}
|
||||||
{ isMedia(mEvent) ? (
|
avatar={userAvatar}
|
||||||
<Message
|
header={userHeader}
|
||||||
key={mEvent.getId()}
|
reply={userReply}
|
||||||
contentOnly={isContentOnly}
|
content={userContent}
|
||||||
markdown={isMarkdown}
|
reactions={userReactions}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
prevMEvent = mEvent;
|
prevMEvent = mEvent;
|
||||||
|
|
Loading…
Reference in a new issue