Refactor chat messages (#156)

This commit is contained in:
Szymon Szlachtowicz 2021-12-14 00:55:09 +01:00 committed by GitHub
parent 7d90ad9ae4
commit 15e5731167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 504 additions and 484 deletions

View File

@ -11,17 +11,6 @@ import { equalDate } from "../utils/equalDate";
import { DownloadButton } from "./Buttons/DownloadButton";
import { buttonStyles } from "./Buttons/buttonStyle";
import { Mention } from "./Chat/ChatMessageContent";
import {
ContentWrapper,
DateSeparator,
MessageHeaderWrapper,
MessageOuterWrapper,
MessageText,
TimeWrapper,
UserAddress,
UserName,
UserNameWrapper,
} from "./Chat/ChatMessages";
import { Logo } from "./CommunityIdentity";
import { ContactMenu } from "./Form/ContactMenu";
import { Tooltip } from "./Form/Tooltip";
@ -37,6 +26,17 @@ import { ReplyIcon } from "./Icons/ReplyActivityIcon";
import { ShowIcon } from "./Icons/ShowIcon";
import { UntrustworthIcon } from "./Icons/UntrustworthIcon";
import { UserIcon } from "./Icons/UserIcon";
import {
ContentWrapper,
DateSeparator,
MessageHeaderWrapper,
MessageOuterWrapper,
MessageText,
TimeWrapper,
UserAddress,
UserName,
UserNameWrapper,
} from "./Messages/Styles";
import { ProfileModalName } from "./Modals/ProfileModal";
import { textMediumStyles, textSmallStyles } from "./Text";

View File

@ -4,13 +4,13 @@ import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useNarrow } from "../../contexts/narrowProvider";
import { Reply } from "../../hooks/useReply";
import { MessagesList } from "../Messages/MessagesList";
import { NarrowChannels } from "../NarrowMode/NarrowChannels";
import { NarrowMembers } from "../NarrowMode/NarrowMembers";
import { LoadingSkeleton } from "../Skeleton/LoadingSkeleton";
import { ChatCreation } from "./ChatCreation";
import { ChatInput } from "./ChatInput";
import { ChatMessages } from "./ChatMessages";
import { ChatTopbar } from "./ChatTopbar";
export enum ChatBodyState {
@ -71,7 +71,7 @@ export function ChatBody({ onClick, showMembers }: ChatBodyProps) {
{showState === ChatBodyState.Chat && (
<>
{messenger && communityData ? (
<ChatMessages setReply={setReply} />
<MessagesList setReply={setReply} />
) : (
<LoadingSkeleton />
)}

View File

@ -1,470 +0,0 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { utils } from "status-communities/dist/cjs";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useChatScrollHandle } from "../../hooks/useChatScrollHandle";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils";
import { EmptyChannel } from "../Channels/EmptyChannel";
import { ContactMenu } from "../Form/ContactMenu";
import { Icon } from "../Icons/Icon";
import { LoadingIcon } from "../Icons/LoadingIcon";
import { QuoteSvg } from "../Icons/QuoteIcon";
import { ReactionSvg } from "../Icons/ReactionIcon";
import { ReplySvg } from "../Icons/ReplyIcon";
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
import { UserIcon } from "../Icons/UserIcon";
import { LinkModal, LinkModalName } from "../Modals/LinkModal";
import { PictureModal, PictureModalName } from "../Modals/PictureModal";
import { textMediumStyles, textSmallStyles } from "../Text";
import { ReplyOn, ReplyTo } from "./ChatInput";
import { ChatMessageContent } from "./ChatMessageContent";
const today = new Date();
function calcHeight(quote: ChatMessage) {
if (quote.image && quote.content) {
return 88;
} else if (quote.image && !quote.content) {
return 68;
} else {
return 25;
}
}
type MessageQuoteProps = {
quote: ChatMessage | undefined;
};
function MessageQuote({ quote }: MessageQuoteProps) {
const quoteClick = useCallback(() => {
if (quote) {
const quoteDiv = document.getElementById(quote.id);
if (quoteDiv) {
quoteDiv.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
quoteDiv.style.background = "lightblue";
quoteDiv.style.transition = "background-color 1000ms linear";
window.setTimeout(() => {
quoteDiv.style.background = "";
window.setTimeout(() => {
quoteDiv.style.transition = "";
}, 1000);
}, 1000);
}
}
}, [quote]);
if (quote) {
return (
<QuoteWrapper onClick={quoteClick}>
<QuoteSvg width={22} height={calcHeight(quote)} />
<QuoteSender>
{" "}
<UserIcon memberView={true} /> {quote.sender}
</QuoteSender>
<Quote>{quote.content}</Quote>
{quote.image && <QuoteImage src={quote.image} />}
</QuoteWrapper>
);
}
return null;
}
type ChatUiMessageProps = {
idx: number;
message: ChatMessage;
channel: ChannelData;
prevMessage: ChatMessage;
setImage: (img: string) => void;
setLink: (link: string) => void;
setReply: (val: Reply | undefined) => void;
quote?: ChatMessage;
};
function ChatUiMessage({
message,
channel,
idx,
prevMessage,
setImage,
setLink,
setReply,
quote,
}: ChatUiMessageProps) {
const { contacts } = useMessengerContext();
const { setActivities } = useActivities();
const identity = useIdentity();
const contact = useMemo(
() => contacts[message.sender],
[message.sender, contacts]
);
const [showMenu, setShowMenu] = useState(false);
const [mentioned, setMentioned] = useState(false);
useEffect(() => {
if (mentioned)
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "mention",
date: message.date,
user: message.sender,
message: message,
channel: channel,
},
]);
if (quote && quote.sender === utils.bufToHex(identity.publicKey))
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "reply",
date: message.date,
user: message.sender,
message: message,
channel: channel,
quote: quote,
},
]);
}, [mentioned, message, quote]);
return (
<MessageOuterWrapper>
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
<DateSeparator>
{equalDate(message.date, today)
? "Today"
: message.date.toLocaleDateString()}
</DateSeparator>
)}
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
<MessageQuote quote={quote} />
<UserMessageWrapper>
<Icon
onClick={() => {
setShowMenu((e) => !e);
}}
>
{showMenu && (
<ContactMenu id={message.sender} setShowMenu={setShowMenu} />
)}
<UserIcon />
</Icon>
<ContentWrapper>
<MessageHeaderWrapper>
<UserNameWrapper>
<UserName>
{" "}
{contact.customName ?? message.sender.slice(0, 10)}
</UserName>
{contact.customName && (
<UserAddress>
{message.sender.slice(0, 5)}...{message.sender.slice(-3)}
</UserAddress>
)}
{contact.isUntrustworthy && <UntrustworthIcon />}
</UserNameWrapper>
<TimeWrapper>{message.date.toLocaleString()}</TimeWrapper>
</MessageHeaderWrapper>
<MessageText>
<ChatMessageContent
message={message}
setImage={setImage}
setLinkOpen={setLink}
setMentioned={setMentioned}
/>
</MessageText>
</ContentWrapper>
</UserMessageWrapper>
<Reactions>
<ReactionBtn>
<ReactionSvg />
</ReactionBtn>
<ReactionBtn
onClick={() =>
setReply({
sender: message.sender,
content: message.content,
image: message.image,
id: message.id,
})
}
>
<ReplySvg width={22} height={22} />
</ReactionBtn>
</Reactions>
</MessageWrapper>
</MessageOuterWrapper>
);
}
interface ChatMessagesProps {
setReply: (val: Reply | undefined) => void;
}
export function ChatMessages({ setReply }: ChatMessagesProps) {
const { messages, activeChannel, contacts } = useMessengerContext();
const ref = useRef<HTMLHeadingElement>(null);
const loadingMessages = useChatScrollHandle(messages, ref, activeChannel);
const shownMessages = useMemo(
() =>
messages.filter(
(message) => !contacts?.[message.sender]?.blocked ?? true
),
[contacts, messages, messages.length]
);
const [image, setImage] = useState("");
const [link, setLink] = useState("");
const { setModal: setPictureModal, isVisible: showPictureModal } =
useModal(PictureModalName);
const { setModal: setLinkModal, isVisible: showLinkModal } =
useModal(LinkModalName);
useEffect(() => (!image ? undefined : setPictureModal(true)), [image]);
useEffect(() => (!link ? undefined : setLinkModal(true)), [link]);
useEffect(
() => (!showPictureModal ? setImage("") : undefined),
[showPictureModal]
);
useEffect(() => (!showLinkModal ? setLink("") : undefined), [showLinkModal]);
return (
<MessagesWrapper ref={ref}>
<PictureModal image={image} />
<LinkModal link={link} />
<EmptyChannel channel={activeChannel} />
{loadingMessages && (
<LoadingWrapper>
<LoadingIcon className="message" />
</LoadingWrapper>
)}
{shownMessages.map((message, idx) => (
<ChatUiMessage
key={message.id}
message={message}
channel={activeChannel}
idx={idx}
prevMessage={shownMessages[idx - 1]}
setLink={setLink}
setImage={setImage}
setReply={setReply}
quote={shownMessages.find((msg) => msg.id == message?.responseTo)}
/>
))}
</MessagesWrapper>
);
}
const MessagesWrapper = styled.div`
display: flex;
flex-direction: column;
height: calc(100% - 44px);
overflow: auto;
padding: 8px 0;
&::-webkit-scrollbar {
width: 0;
}
`;
export const MessageWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
padding: 8px 16px;
border-left: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&:hover {
background: ${({ theme }) => theme.inputColor};
border-color: ${({ theme }) => theme.inputColor};
}
&:hover > div {
visibility: visible;
}
&.mention {
background: ${({ theme }) => theme.mentionBg};
border-color: ${({ theme }) => theme.mentionColor};
}
&.mention:hover {
background: ${({ theme }) => theme.mentionBgHover};
border-color: ${({ theme }) => theme.mentionColor};
}
`;
export const MessageOuterWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
position: relative;
`;
const UserMessageWrapper = styled.div`
width: 100%;
display: flex;
`;
export const DateSeparator = styled.div`
width: 100%;
display: flex;
flex: 1;
height: 100%;
text-align: center;
justify-content: center;
align-items: center;
font-family: "Inter";
font-style: normal;
font-weight: 500;
color: #939ba1;
margin-top: 16px;
margin-bottom: 16px;
${textSmallStyles}
`;
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: 8px;
`;
export const MessageHeaderWrapper = styled.div`
display: flex;
align-items: center;
`;
export const UserNameWrapper = styled.div`
display: flex;
align-items: center;
`;
export const UserName = styled.p`
font-weight: 500;
color: ${({ theme }) => theme.tertiary};
margin-right: 4px;
${textMediumStyles}
`;
export const UserAddress = styled.p`
font-size: 10px;
line-height: 14px;
letter-spacing: 0.2px;
color: ${({ theme }) => theme.secondary};
position: relative;
padding-right: 8px;
&:after {
content: "";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: ${({ theme }) => theme.secondary};
}
`;
export const TimeWrapper = styled.div`
font-size: 10px;
line-height: 14px;
letter-spacing: 0.2px;
text-transform: uppercase;
color: ${({ theme }) => theme.secondary};
margin-left: 4px;
`;
export const MessageText = styled.div`
overflow-wrap: anywhere;
width: 100%;
white-space: pre-wrap;
color: ${({ theme }) => theme.primary};
`;
const LoadingWrapper = styled.div`
display: flex;
align-self: center;
align-items: center;
justify-content: center;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
`;
const Reactions = styled.div`
display: flex;
position: absolute;
right: 20px;
top: -18px;
box-shadow: 0px 4px 12px rgba(0, 34, 51, 0.08);
border-radius: 8px;
background: ${({ theme }) => theme.bodyBackgroundColor};
visibility: hidden;
`;
const ReactionBtn = styled.button`
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
align-self: center;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&:hover > svg {
fill: ${({ theme }) => theme.tertiary};
}
`;
const QuoteWrapper = styled.div`
display: flex;
flex-direction: column;
padding-left: 48px;
position: relative;
`;
const QuoteSender = styled(ReplyTo)`
color: ${({ theme }) => theme.secondary};
`;
const Quote = styled(ReplyOn)`
color: ${({ theme }) => theme.secondary};
`;
const QuoteImage = styled.img`
width: 56px;
height: 56px;
border-radius: 4px;
margin-top: 4px;
`;

View File

@ -5,7 +5,6 @@ import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
import { UserAddress } from "../Chat/ChatMessages";
import { AddContactSvg } from "../Icons/AddContactIcon";
import { BlockSvg } from "../Icons/BlockIcon";
import { ChatSvg } from "../Icons/ChatIcon";
@ -14,6 +13,7 @@ import { ProfileSvg } from "../Icons/ProfileIcon";
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
import { UserIcon } from "../Icons/UserIcon";
import { WarningSvg } from "../Icons/WarningIcon";
import { UserAddress } from "../Messages/Styles";
import { ProfileModalName } from "../Modals/ProfileModal";
import { textMediumStyles } from "../Text";

View File

@ -0,0 +1,81 @@
import React, { useCallback } from "react";
import styled from "styled-components";
import { ChatMessage } from "../../models/ChatMessage";
import { ReplyOn, ReplyTo } from "../Chat/ChatInput";
import { QuoteSvg } from "../Icons/QuoteIcon";
import { UserIcon } from "../Icons/UserIcon";
function calcHeight(quote: ChatMessage) {
if (quote.image && quote.content) {
return 88;
} else if (quote.image && !quote.content) {
return 68;
} else {
return 25;
}
}
type MessageQuoteProps = {
quote: ChatMessage | undefined;
};
export function MessageQuote({ quote }: MessageQuoteProps) {
const quoteClick = useCallback(() => {
if (quote) {
const quoteDiv = document.getElementById(quote.id);
if (quoteDiv) {
quoteDiv.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
quoteDiv.style.background = "lightblue";
quoteDiv.style.transition = "background-color 1000ms linear";
window.setTimeout(() => {
quoteDiv.style.background = "";
window.setTimeout(() => {
quoteDiv.style.transition = "";
}, 1000);
}, 1000);
}
}
}, [quote]);
if (quote) {
return (
<QuoteWrapper onClick={quoteClick}>
<QuoteSvg width={22} height={calcHeight(quote)} />
<QuoteSender>
{" "}
<UserIcon memberView={true} /> {quote.sender}
</QuoteSender>
<Quote>{quote.content}</Quote>
{quote.image && <QuoteImage src={quote.image} />}
</QuoteWrapper>
);
}
return null;
}
const QuoteWrapper = styled.div`
display: flex;
flex-direction: column;
padding-left: 48px;
position: relative;
`;
const QuoteSender = styled(ReplyTo)`
color: ${({ theme }) => theme.secondary};
`;
const Quote = styled(ReplyOn)`
color: ${({ theme }) => theme.secondary};
`;
const QuoteImage = styled.img`
width: 56px;
height: 56px;
border-radius: 4px;
margin-top: 4px;
`;

View File

@ -0,0 +1,95 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useChatScrollHandle } from "../../hooks/useChatScrollHandle";
import { Reply } from "../../hooks/useReply";
import { EmptyChannel } from "../Channels/EmptyChannel";
import { LoadingIcon } from "../Icons/LoadingIcon";
import { LinkModal, LinkModalName } from "../Modals/LinkModal";
import { PictureModal, PictureModalName } from "../Modals/PictureModal";
import { UiMessage } from "./UiMessage";
interface MessagesListProps {
setReply: (val: Reply | undefined) => void;
}
export function MessagesList({ setReply }: MessagesListProps) {
const { messages, activeChannel, contacts } = useMessengerContext();
const ref = useRef<HTMLHeadingElement>(null);
const loadingMessages = useChatScrollHandle(messages, ref, activeChannel);
const shownMessages = useMemo(
() =>
messages.filter(
(message) => !contacts?.[message.sender]?.blocked ?? true
),
[contacts, messages, messages.length]
);
const [image, setImage] = useState("");
const [link, setLink] = useState("");
const { setModal: setPictureModal, isVisible: showPictureModal } =
useModal(PictureModalName);
const { setModal: setLinkModal, isVisible: showLinkModal } =
useModal(LinkModalName);
useEffect(() => (!image ? undefined : setPictureModal(true)), [image]);
useEffect(() => (!link ? undefined : setLinkModal(true)), [link]);
useEffect(
() => (!showPictureModal ? setImage("") : undefined),
[showPictureModal]
);
useEffect(() => (!showLinkModal ? setLink("") : undefined), [showLinkModal]);
return (
<MessagesWrapper ref={ref}>
<PictureModal image={image} />
<LinkModal link={link} />
<EmptyChannel channel={activeChannel} />
{loadingMessages && (
<LoadingWrapper>
<LoadingIcon className="message" />
</LoadingWrapper>
)}
{shownMessages.map((message, idx) => (
<UiMessage
key={message.id}
message={message}
channel={activeChannel}
idx={idx}
prevMessage={shownMessages[idx - 1]}
setLink={setLink}
setImage={setImage}
setReply={setReply}
quote={shownMessages.find((msg) => msg.id == message?.responseTo)}
/>
))}
</MessagesWrapper>
);
}
const LoadingWrapper = styled.div`
display: flex;
align-self: center;
align-items: center;
justify-content: center;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
`;
const MessagesWrapper = styled.div`
display: flex;
flex-direction: column;
height: calc(100% - 44px);
overflow: auto;
padding: 8px 0;
&::-webkit-scrollbar {
width: 0;
}
`;

View File

@ -0,0 +1,117 @@
import styled from "styled-components";
import { textMediumStyles, textSmallStyles } from "../Text";
export const MessageWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
padding: 8px 16px;
border-left: 2px solid ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&:hover {
background: ${({ theme }) => theme.inputColor};
border-color: ${({ theme }) => theme.inputColor};
}
&:hover > div {
visibility: visible;
}
&.mention {
background: ${({ theme }) => theme.mentionBg};
border-color: ${({ theme }) => theme.mentionColor};
}
&.mention:hover {
background: ${({ theme }) => theme.mentionBgHover};
border-color: ${({ theme }) => theme.mentionColor};
}
`;
export const MessageOuterWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
position: relative;
`;
export const DateSeparator = styled.div`
width: 100%;
display: flex;
flex: 1;
height: 100%;
text-align: center;
justify-content: center;
align-items: center;
font-family: "Inter";
font-style: normal;
font-weight: 500;
color: #939ba1;
margin-top: 16px;
margin-bottom: 16px;
${textSmallStyles}
`;
export const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: 8px;
`;
export const MessageHeaderWrapper = styled.div`
display: flex;
align-items: center;
`;
export const UserNameWrapper = styled.div`
display: flex;
align-items: center;
`;
export const UserName = styled.p`
font-weight: 500;
color: ${({ theme }) => theme.tertiary};
margin-right: 4px;
${textMediumStyles}
`;
export const UserAddress = styled.p`
font-size: 10px;
line-height: 14px;
letter-spacing: 0.2px;
color: ${({ theme }) => theme.secondary};
position: relative;
padding-right: 8px;
&:after {
content: "";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: ${({ theme }) => theme.secondary};
}
`;
export const TimeWrapper = styled.div`
font-size: 10px;
line-height: 14px;
letter-spacing: 0.2px;
text-transform: uppercase;
color: ${({ theme }) => theme.secondary};
margin-left: 4px;
`;
export const MessageText = styled.div`
overflow-wrap: anywhere;
width: 100%;
white-space: pre-wrap;
color: ${({ theme }) => theme.primary};
`;

View File

@ -0,0 +1,197 @@
import React, { useEffect, useMemo, useState } from "react";
import { utils } from "status-communities/dist/cjs";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils";
import { ChatMessageContent } from "../Chat/ChatMessageContent";
import { ContactMenu } from "../Form/ContactMenu";
import { Icon } from "../Icons/Icon";
import { ReactionSvg } from "../Icons/ReactionIcon";
import { ReplySvg } from "../Icons/ReplyIcon";
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
import { UserIcon } from "../Icons/UserIcon";
import { MessageQuote } from "./MessageQuote";
import {
ContentWrapper,
DateSeparator,
MessageHeaderWrapper,
MessageOuterWrapper,
MessageText,
MessageWrapper,
TimeWrapper,
UserAddress,
UserName,
UserNameWrapper,
} from "./Styles";
type UiMessageProps = {
idx: number;
message: ChatMessage;
channel: ChannelData;
prevMessage: ChatMessage;
setImage: (img: string) => void;
setLink: (link: string) => void;
setReply: (val: Reply | undefined) => void;
quote?: ChatMessage;
};
export function UiMessage({
message,
channel,
idx,
prevMessage,
setImage,
setLink,
setReply,
quote,
}: UiMessageProps) {
const today = new Date();
const { contacts } = useMessengerContext();
const { setActivities } = useActivities();
const identity = useIdentity();
const contact = useMemo(
() => contacts[message.sender],
[message.sender, contacts]
);
const [showMenu, setShowMenu] = useState(false);
const [mentioned, setMentioned] = useState(false);
useEffect(() => {
if (mentioned)
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "mention",
date: message.date,
user: message.sender,
message: message,
channel: channel,
},
]);
if (quote && quote.sender === utils.bufToHex(identity.publicKey))
setActivities((prev) => [
...prev,
{
id: message.date.getTime().toString() + message.content,
type: "reply",
date: message.date,
user: message.sender,
message: message,
channel: channel,
quote: quote,
},
]);
}, [mentioned, message, quote]);
return (
<MessageOuterWrapper>
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
<DateSeparator>
{equalDate(message.date, today)
? "Today"
: message.date.toLocaleDateString()}
</DateSeparator>
)}
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
<MessageQuote quote={quote} />
<UserMessageWrapper>
<Icon
onClick={() => {
setShowMenu((e) => !e);
}}
>
{showMenu && (
<ContactMenu id={message.sender} setShowMenu={setShowMenu} />
)}
<UserIcon />
</Icon>
<ContentWrapper>
<MessageHeaderWrapper>
<UserNameWrapper>
<UserName>
{" "}
{contact.customName ?? message.sender.slice(0, 10)}
</UserName>
{contact.customName && (
<UserAddress>
{message.sender.slice(0, 5)}...{message.sender.slice(-3)}
</UserAddress>
)}
{contact.isUntrustworthy && <UntrustworthIcon />}
</UserNameWrapper>
<TimeWrapper>{message.date.toLocaleString()}</TimeWrapper>
</MessageHeaderWrapper>
<MessageText>
<ChatMessageContent
message={message}
setImage={setImage}
setLinkOpen={setLink}
setMentioned={setMentioned}
/>
</MessageText>
</ContentWrapper>
</UserMessageWrapper>
<Reactions>
<ReactionBtn>
<ReactionSvg />
</ReactionBtn>
<ReactionBtn
onClick={() =>
setReply({
sender: message.sender,
content: message.content,
image: message.image,
id: message.id,
})
}
>
<ReplySvg width={22} height={22} />
</ReactionBtn>
</Reactions>
</MessageWrapper>
</MessageOuterWrapper>
);
}
const UserMessageWrapper = styled.div`
width: 100%;
display: flex;
`;
const Reactions = styled.div`
display: flex;
position: absolute;
right: 20px;
top: -18px;
box-shadow: 0px 4px 12px rgba(0, 34, 51, 0.08);
border-radius: 8px;
background: ${({ theme }) => theme.bodyBackgroundColor};
visibility: hidden;
`;
const ReactionBtn = styled.button`
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
align-self: center;
&:hover {
background: ${({ theme }) => theme.buttonBgHover};
}
&:hover > svg {
fill: ${({ theme }) => theme.tertiary};
}
`;