From 6338c79af096acc2050eb72bb42e44935b66e159 Mon Sep 17 00:00:00 2001 From: Maria Rushkova <66270386+mrushkova@users.noreply.github.com> Date: Mon, 6 Dec 2021 15:02:17 +0100 Subject: [PATCH] Reply (#146) --- .../src/components/Chat/ChatBody.tsx | 9 +- .../src/components/Chat/ChatInput.tsx | 153 +++++++++++----- .../src/components/Chat/ChatMessages.tsx | 167 ++++++++++++++---- .../src/components/Icons/ClearIcon.tsx | 4 + .../src/components/Icons/QuoteIcon.tsx | 37 ++++ .../src/components/Icons/ReactionIcon.tsx | 51 ++++++ .../src/components/Icons/ReplyIcon.tsx | 30 ++++ packages/react-chat/src/hooks/useReply.ts | 4 + packages/react-chat/src/models/ChatMessage.ts | 13 +- 9 files changed, 386 insertions(+), 82 deletions(-) create mode 100644 packages/react-chat/src/components/Icons/QuoteIcon.tsx create mode 100644 packages/react-chat/src/components/Icons/ReactionIcon.tsx create mode 100644 packages/react-chat/src/components/Icons/ReplyIcon.tsx create mode 100644 packages/react-chat/src/hooks/useReply.ts diff --git a/packages/react-chat/src/components/Chat/ChatBody.tsx b/packages/react-chat/src/components/Chat/ChatBody.tsx index d0c2923e..6f3a0bed 100644 --- a/packages/react-chat/src/components/Chat/ChatBody.tsx +++ b/packages/react-chat/src/components/Chat/ChatBody.tsx @@ -3,6 +3,7 @@ import styled from "styled-components"; import { useMessengerContext } from "../../contexts/messengerProvider"; import { useNarrow } from "../../contexts/narrowProvider"; +import { Reply } from "../../hooks/useReply"; import { Channel } from "../Channels/Channel"; import { Community } from "../Community"; import { ChannelMenu } from "../Form/ChannelMenu"; @@ -53,6 +54,8 @@ export function ChatBody({ onClick, showMembers }: ChatBodyProps) { } }, [narrow]); + const [reply, setReply] = useState(undefined); + return ( {editGroup && communityData ? ( @@ -118,11 +121,11 @@ export function ChatBody({ onClick, showMembers }: ChatBodyProps) { {showState === ChatBodyState.Chat && ( <> {messenger && communityData ? ( - + ) : ( )} - + )} @@ -142,7 +145,7 @@ export function ChatBody({ onClick, showMembers }: ChatBodyProps) { ) : ( <> - + )} diff --git a/packages/react-chat/src/components/Chat/ChatInput.tsx b/packages/react-chat/src/components/Chat/ChatInput.tsx index 9f5fe26a..9729db58 100644 --- a/packages/react-chat/src/components/Chat/ChatInput.tsx +++ b/packages/react-chat/src/components/Chat/ChatInput.tsx @@ -11,18 +11,26 @@ import styled, { useTheme } from "styled-components"; import { useMessengerContext } from "../../contexts/messengerProvider"; import { useModal } from "../../contexts/modalProvider"; import { useLow } from "../../contexts/narrowProvider"; +import { Reply } from "../../hooks/useReply"; import { lightTheme, Theme } from "../../styles/themes"; import { uintToImgUrl } from "../../utils/uintToImgUrl"; +import { ClearSvg } from "../Icons/ClearIcon"; import { EmojiIcon } from "../Icons/EmojiIcon"; import { GifIcon } from "../Icons/GifIcon"; import { PictureIcon } from "../Icons/PictureIcon"; +import { ReplySvg } from "../Icons/ReplyIcon"; import { StickerIcon } from "../Icons/StickerIcon"; import "emoji-mart/css/emoji-mart.css"; import { SizeLimitModal, SizeLimitModalName } from "../Modals/SizeLimitModal"; import { SearchBlock } from "../SearchBlock"; -import { textMediumStyles } from "../Text"; +import { textMediumStyles, textSmallStyles } from "../Text"; -export function ChatInput() { +interface ChatInputProps { + reply: Reply | undefined; + setReply: (val: Reply | undefined) => void; +} + +export function ChatInput({ reply, setReply }: ChatInputProps) { const { sendMessage } = useMessengerContext(); const theme = useTheme() as Theme; const [content, setContent] = useState(""); @@ -107,6 +115,7 @@ export function ChatInput() { inputRef.current.innerHTML = ""; } setContent(""); + setReply(undefined); } }, [content, imageUint] @@ -239,46 +248,65 @@ export function ChatInput() { }} /> - - - {image && ( - setImageUint(undefined)} /> - )} - - {query && ( - + {reply && ( + + + {" "} + {" "} + {reply.sender} + + {reply.content} + setReply(undefined)}> + {" "} + + + + )} + + + {image && ( + setImageUint(undefined)} + /> + )} + - )} - - - { - e.stopPropagation(); - setShowEmoji(!showEmoji); - }} - > - - - - - - - - - - + {query && ( + + )} + + + { + e.stopPropagation(); + setShowEmoji(!showEmoji); + }} + > + + + + + + + + + + + ); } @@ -297,6 +325,17 @@ const View = styled.div` position: relative; `; +const InputArea = styled.div` + position: relative; + display: flex; + flex-direction: column; + width: 100%; + max-height: 438px; + padding: 2px; + background: ${({ theme }) => theme.inputColor}; + border-radius: 16px 16px 4px 16px; +`; + const Row = styled.div` position: relative; display: flex; @@ -393,3 +432,35 @@ const ChatButton = styled.button` width: 32px; height: 32px; `; + +const CloseButton = styled(ChatButton)` + position: absolute; + top: 0; + right: 0; +`; +const ReplyWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + max-height: 96px; + padding: 6px 12px; + background: rgba(0, 0, 0, 0.1); + color: ${({ theme }) => theme.primary}; + border-radius: 14px 14px 4px 14px; + position: relative; + + ${textSmallStyles}; +`; + +export const ReplyTo = styled.div` + display: flex; + align-items: center; + font-weight: 500; +`; + +export const ReplyOn = styled.div` + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; diff --git a/packages/react-chat/src/components/Chat/ChatMessages.tsx b/packages/react-chat/src/components/Chat/ChatMessages.tsx index 39d41053..0992a399 100644 --- a/packages/react-chat/src/components/Chat/ChatMessages.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessages.tsx @@ -4,17 +4,22 @@ 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 { ChatMessage } from "../../models/ChatMessage"; import { equalDate } from "../../utils"; import { EmptyChannel } from "../Channels/EmptyChannel"; import { ContactMenu } from "../Form/ContactMenu"; 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(); @@ -25,6 +30,7 @@ type ChatUiMessageProps = { prevMessage: ChatMessage; setImage: (img: string) => void; setLink: (link: string) => void; + setReply: (val: Reply | undefined) => void; }; function ChatUiMessage({ @@ -33,6 +39,7 @@ function ChatUiMessage({ prevMessage, setImage, setLink, + setReply, }: ChatUiMessageProps) { const { contacts } = useMessengerContext(); const contact = useMemo( @@ -51,49 +58,78 @@ function ChatUiMessage({ : message.date.toLocaleDateString()} )} - - { - setShowMenu((e) => !e); - }} - > - {showMenu && ( - - )} - - - - - - - {" "} - {contact.customName ?? message.sender.slice(0, 10)} - - {contact.customName && ( - - {message.sender.slice(0, 5)}...{message.sender.slice(-3)} - - )} - {contact.isUntrustworthy && } - - {message.date.toLocaleString()} - - - - - + + {message.quote && ( + + + + {" "} + {message.quote.author} + + {message.quote.content} + + )} + + { + setShowMenu((e) => !e); + }} + > + {showMenu && ( + + )} + + + + + + + + {" "} + {contact.customName ?? message.sender.slice(0, 10)} + + {contact.customName && ( + + {message.sender.slice(0, 5)}...{message.sender.slice(-3)} + + )} + {contact.isUntrustworthy && } + + {message.date.toLocaleString()} + + + + + + + + + + + + setReply({ sender: message.sender, content: message.content }) + } + > + + + ); } -export function ChatMessages() { +interface ChatMessagesProps { + setReply: (val: Reply | undefined) => void; +} + +export function ChatMessages({ setReply }: ChatMessagesProps) { const { messages, activeChannel, contacts } = useMessengerContext(); const ref = useRef(null); const loadingMessages = useChatScrollHandle(messages, ref, activeChannel); @@ -141,6 +177,7 @@ export function ChatMessages() { prevMessage={shownMessages[idx - 1]} setLink={setLink} setImage={setImage} + setReply={setReply} /> ))} @@ -162,14 +199,20 @@ const MessagesWrapper = styled.div` 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}; @@ -185,6 +228,12 @@ const MessageOuterWrapper = styled.div` width: 100%; display: flex; flex-direction: column; + position: relative; +`; + +const UserMessageWrapper = styled.div` + width: 100%; + display: flex; `; const DateSeparator = styled.div` @@ -288,3 +337,47 @@ const LoadingWrapper = styled.div` 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 QuoteAuthor = styled(ReplyTo)` + color: ${({ theme }) => theme.secondary}; +`; + +const Quote = styled(ReplyOn)` + color: ${({ theme }) => theme.secondary}; +`; diff --git a/packages/react-chat/src/components/Icons/ClearIcon.tsx b/packages/react-chat/src/components/Icons/ClearIcon.tsx index 27d6e293..e31f9407 100644 --- a/packages/react-chat/src/components/Icons/ClearIcon.tsx +++ b/packages/react-chat/src/components/Icons/ClearIcon.tsx @@ -39,6 +39,10 @@ const Icon = styled(ClearSvg)` fill: ${({ theme }) => theme.secondary}; } + &.input { + fill: ${({ theme }) => theme.primary}; + } + &.profile > path { fill: ${({ theme }) => theme.bodyBackgroundColor}; } diff --git a/packages/react-chat/src/components/Icons/QuoteIcon.tsx b/packages/react-chat/src/components/Icons/QuoteIcon.tsx new file mode 100644 index 00000000..af683958 --- /dev/null +++ b/packages/react-chat/src/components/Icons/QuoteIcon.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import styled from "styled-components"; + +type QuoteProps = { + width: number; + height: number; +}; + +export function QuoteSvg({ width, height }: QuoteProps) { + return ( + + + + ); +} + +const Icon = styled.svg` + & > path { + stroke: ${({ theme }) => theme.secondary}; + } + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); +`; diff --git a/packages/react-chat/src/components/Icons/ReactionIcon.tsx b/packages/react-chat/src/components/Icons/ReactionIcon.tsx new file mode 100644 index 00000000..22703b88 --- /dev/null +++ b/packages/react-chat/src/components/Icons/ReactionIcon.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import styled from "styled-components"; + +type ReactionProps = { + className?: string; +}; + +export function ReactionSvg({ className }: ReactionProps) { + return ( + + + + + + + + + + + + + + + ); +} + +const Icon = styled.svg` + fill: ${({ theme }) => theme.secondary}; + + &:hover { + fill: ${({ theme }) => theme.tertiary}; + } +`; diff --git a/packages/react-chat/src/components/Icons/ReplyIcon.tsx b/packages/react-chat/src/components/Icons/ReplyIcon.tsx new file mode 100644 index 00000000..f53887d8 --- /dev/null +++ b/packages/react-chat/src/components/Icons/ReplyIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import styled from "styled-components"; + +type ReplyProps = { + width: number; + height: number; + className?: string; +}; + +export function ReplySvg({ width, height, className }: ReplyProps) { + return ( + + + + ); +} + +const Icon = styled.svg` + fill: ${({ theme }) => theme.secondary}; + + &.input { + fill: ${({ theme }) => theme.primary}; + } +`; diff --git a/packages/react-chat/src/hooks/useReply.ts b/packages/react-chat/src/hooks/useReply.ts new file mode 100644 index 00000000..8143355d --- /dev/null +++ b/packages/react-chat/src/hooks/useReply.ts @@ -0,0 +1,4 @@ +export type Reply = { + sender: string; + content: string; +}; diff --git a/packages/react-chat/src/models/ChatMessage.ts b/packages/react-chat/src/models/ChatMessage.ts index e202812c..e2238f33 100644 --- a/packages/react-chat/src/models/ChatMessage.ts +++ b/packages/react-chat/src/models/ChatMessage.ts @@ -7,12 +7,23 @@ export class ChatMessage { date: Date; sender: string; image?: string; + quote?: { + author: string; + content: string; + }; - constructor(content: string, date: Date, sender: string, image?: string) { + constructor( + content: string, + date: Date, + sender: string, + image?: string, + quote?: { author: string; content: string } + ) { this.content = content; this.date = date; this.sender = sender; this.image = image; + this.quote = quote; } public static fromMetadataMessage(