Refactor chat messages (#156)
This commit is contained in:
parent
7d90ad9ae4
commit
15e5731167
|
@ -11,17 +11,6 @@ import { equalDate } from "../utils/equalDate";
|
||||||
import { DownloadButton } from "./Buttons/DownloadButton";
|
import { DownloadButton } from "./Buttons/DownloadButton";
|
||||||
import { buttonStyles } from "./Buttons/buttonStyle";
|
import { buttonStyles } from "./Buttons/buttonStyle";
|
||||||
import { Mention } from "./Chat/ChatMessageContent";
|
import { Mention } from "./Chat/ChatMessageContent";
|
||||||
import {
|
|
||||||
ContentWrapper,
|
|
||||||
DateSeparator,
|
|
||||||
MessageHeaderWrapper,
|
|
||||||
MessageOuterWrapper,
|
|
||||||
MessageText,
|
|
||||||
TimeWrapper,
|
|
||||||
UserAddress,
|
|
||||||
UserName,
|
|
||||||
UserNameWrapper,
|
|
||||||
} from "./Chat/ChatMessages";
|
|
||||||
import { Logo } from "./CommunityIdentity";
|
import { Logo } from "./CommunityIdentity";
|
||||||
import { ContactMenu } from "./Form/ContactMenu";
|
import { ContactMenu } from "./Form/ContactMenu";
|
||||||
import { Tooltip } from "./Form/Tooltip";
|
import { Tooltip } from "./Form/Tooltip";
|
||||||
|
@ -37,6 +26,17 @@ import { ReplyIcon } from "./Icons/ReplyActivityIcon";
|
||||||
import { ShowIcon } from "./Icons/ShowIcon";
|
import { ShowIcon } from "./Icons/ShowIcon";
|
||||||
import { UntrustworthIcon } from "./Icons/UntrustworthIcon";
|
import { UntrustworthIcon } from "./Icons/UntrustworthIcon";
|
||||||
import { UserIcon } from "./Icons/UserIcon";
|
import { UserIcon } from "./Icons/UserIcon";
|
||||||
|
import {
|
||||||
|
ContentWrapper,
|
||||||
|
DateSeparator,
|
||||||
|
MessageHeaderWrapper,
|
||||||
|
MessageOuterWrapper,
|
||||||
|
MessageText,
|
||||||
|
TimeWrapper,
|
||||||
|
UserAddress,
|
||||||
|
UserName,
|
||||||
|
UserNameWrapper,
|
||||||
|
} from "./Messages/Styles";
|
||||||
import { ProfileModalName } from "./Modals/ProfileModal";
|
import { ProfileModalName } from "./Modals/ProfileModal";
|
||||||
import { textMediumStyles, textSmallStyles } from "./Text";
|
import { textMediumStyles, textSmallStyles } from "./Text";
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,13 @@ import styled from "styled-components";
|
||||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||||
import { useNarrow } from "../../contexts/narrowProvider";
|
import { useNarrow } from "../../contexts/narrowProvider";
|
||||||
import { Reply } from "../../hooks/useReply";
|
import { Reply } from "../../hooks/useReply";
|
||||||
|
import { MessagesList } from "../Messages/MessagesList";
|
||||||
import { NarrowChannels } from "../NarrowMode/NarrowChannels";
|
import { NarrowChannels } from "../NarrowMode/NarrowChannels";
|
||||||
import { NarrowMembers } from "../NarrowMode/NarrowMembers";
|
import { NarrowMembers } from "../NarrowMode/NarrowMembers";
|
||||||
import { LoadingSkeleton } from "../Skeleton/LoadingSkeleton";
|
import { LoadingSkeleton } from "../Skeleton/LoadingSkeleton";
|
||||||
|
|
||||||
import { ChatCreation } from "./ChatCreation";
|
import { ChatCreation } from "./ChatCreation";
|
||||||
import { ChatInput } from "./ChatInput";
|
import { ChatInput } from "./ChatInput";
|
||||||
import { ChatMessages } from "./ChatMessages";
|
|
||||||
import { ChatTopbar } from "./ChatTopbar";
|
import { ChatTopbar } from "./ChatTopbar";
|
||||||
|
|
||||||
export enum ChatBodyState {
|
export enum ChatBodyState {
|
||||||
|
@ -71,7 +71,7 @@ export function ChatBody({ onClick, showMembers }: ChatBodyProps) {
|
||||||
{showState === ChatBodyState.Chat && (
|
{showState === ChatBodyState.Chat && (
|
||||||
<>
|
<>
|
||||||
{messenger && communityData ? (
|
{messenger && communityData ? (
|
||||||
<ChatMessages setReply={setReply} />
|
<MessagesList setReply={setReply} />
|
||||||
) : (
|
) : (
|
||||||
<LoadingSkeleton />
|
<LoadingSkeleton />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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;
|
|
||||||
`;
|
|
|
@ -5,7 +5,6 @@ import styled from "styled-components";
|
||||||
import { useIdentity } from "../../contexts/identityProvider";
|
import { useIdentity } from "../../contexts/identityProvider";
|
||||||
import { useModal } from "../../contexts/modalProvider";
|
import { useModal } from "../../contexts/modalProvider";
|
||||||
import { useManageContact } from "../../hooks/useManageContact";
|
import { useManageContact } from "../../hooks/useManageContact";
|
||||||
import { UserAddress } from "../Chat/ChatMessages";
|
|
||||||
import { AddContactSvg } from "../Icons/AddContactIcon";
|
import { AddContactSvg } from "../Icons/AddContactIcon";
|
||||||
import { BlockSvg } from "../Icons/BlockIcon";
|
import { BlockSvg } from "../Icons/BlockIcon";
|
||||||
import { ChatSvg } from "../Icons/ChatIcon";
|
import { ChatSvg } from "../Icons/ChatIcon";
|
||||||
|
@ -14,6 +13,7 @@ import { ProfileSvg } from "../Icons/ProfileIcon";
|
||||||
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
|
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
|
||||||
import { UserIcon } from "../Icons/UserIcon";
|
import { UserIcon } from "../Icons/UserIcon";
|
||||||
import { WarningSvg } from "../Icons/WarningIcon";
|
import { WarningSvg } from "../Icons/WarningIcon";
|
||||||
|
import { UserAddress } from "../Messages/Styles";
|
||||||
import { ProfileModalName } from "../Modals/ProfileModal";
|
import { ProfileModalName } from "../Modals/ProfileModal";
|
||||||
import { textMediumStyles } from "../Text";
|
import { textMediumStyles } from "../Text";
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
`;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -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};
|
||||||
|
`;
|
|
@ -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};
|
||||||
|
}
|
||||||
|
`;
|
Loading…
Reference in New Issue