From 66ecfa407e4f97d954c74a32842f61455993d349 Mon Sep 17 00:00:00 2001 From: Pavel Prichodko <14926950+prichodko@users.noreply.github.com> Date: Thu, 31 Mar 2022 15:05:03 +0200 Subject: [PATCH] refactor(react): chat messages content --- .../ActivityCenter/ActivityButton.tsx | 2 +- .../ActivityCenter/ActivityMessage.tsx | 2 +- .../{Chat => Chat-legacy}/ChatBody.tsx | 0 .../{Chat => Chat-legacy}/ChatCreation.tsx | 0 .../{Chat => Chat-legacy}/ChatInput.tsx | 0 .../ChatMessageContent.tsx | 0 .../{Chat => Chat-legacy}/ChatTopbar.tsx | 2 +- .../Chat-legacy/CommunitySidebar.tsx | 38 +++ .../{Chat => Chat-legacy}/EmojiPicker.tsx | 0 .../src/components/Messages/MessageQuote.tsx | 2 +- .../src/components/Messages/UiMessage.tsx | 2 +- .../src/components/SearchBlock.tsx | 2 +- .../src/components/chat/chat-input.tsx | 136 ++++++++ .../src/components/chat/chat-message.tsx | 323 ++++++++++++++++++ .../src/components/chat/index.tsx | 81 +++++ .../src/components/chat/navbar.tsx | 179 ++++++++++ .../src/contexts/chat-context.tsx | 62 ++++ 17 files changed, 825 insertions(+), 6 deletions(-) rename packages/status-react/src/components/{Chat => Chat-legacy}/ChatBody.tsx (100%) rename packages/status-react/src/components/{Chat => Chat-legacy}/ChatCreation.tsx (100%) rename packages/status-react/src/components/{Chat => Chat-legacy}/ChatInput.tsx (100%) rename packages/status-react/src/components/{Chat => Chat-legacy}/ChatMessageContent.tsx (100%) rename packages/status-react/src/components/{Chat => Chat-legacy}/ChatTopbar.tsx (98%) create mode 100644 packages/status-react/src/components/Chat-legacy/CommunitySidebar.tsx rename packages/status-react/src/components/{Chat => Chat-legacy}/EmojiPicker.tsx (100%) create mode 100644 packages/status-react/src/components/chat/chat-input.tsx create mode 100644 packages/status-react/src/components/chat/chat-message.tsx create mode 100644 packages/status-react/src/components/chat/index.tsx create mode 100644 packages/status-react/src/components/chat/navbar.tsx create mode 100644 packages/status-react/src/contexts/chat-context.tsx diff --git a/packages/status-react/src/components/ActivityCenter/ActivityButton.tsx b/packages/status-react/src/components/ActivityCenter/ActivityButton.tsx index 0757e009..19c0e0af 100644 --- a/packages/status-react/src/components/ActivityCenter/ActivityButton.tsx +++ b/packages/status-react/src/components/ActivityCenter/ActivityButton.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components' import { useIdentity } from '../../contexts/identityProvider' import { useActivities } from '../../hooks/useActivities' import { useClickOutside } from '../../hooks/useClickOutside' -import { TopBtn } from '../Chat/ChatTopbar' +import { TopBtn } from '../Chat-legacy/ChatTopbar' import { ActivityIcon } from '../Icons/ActivityIcon' import { ActivityCenter } from './ActivityCenter' diff --git a/packages/status-react/src/components/ActivityCenter/ActivityMessage.tsx b/packages/status-react/src/components/ActivityCenter/ActivityMessage.tsx index 255e1dd6..a9cca6d6 100644 --- a/packages/status-react/src/components/ActivityCenter/ActivityMessage.tsx +++ b/packages/status-react/src/components/ActivityCenter/ActivityMessage.tsx @@ -8,7 +8,7 @@ import { useScrollToMessage } from '../../contexts/scrollProvider' import { useClickOutside } from '../../hooks/useClickOutside' import { equalDate } from '../../utils/equalDate' import { DownloadButton } from '../Buttons/DownloadButton' -import { Mention } from '../Chat/ChatMessageContent' +import { Mention } from '../Chat-legacy/ChatMessageContent' import { Logo } from '../CommunityIdentity' import { ContactMenu } from '../Form/ContactMenu' import { Tooltip } from '../Form/Tooltip' diff --git a/packages/status-react/src/components/Chat/ChatBody.tsx b/packages/status-react/src/components/Chat-legacy/ChatBody.tsx similarity index 100% rename from packages/status-react/src/components/Chat/ChatBody.tsx rename to packages/status-react/src/components/Chat-legacy/ChatBody.tsx diff --git a/packages/status-react/src/components/Chat/ChatCreation.tsx b/packages/status-react/src/components/Chat-legacy/ChatCreation.tsx similarity index 100% rename from packages/status-react/src/components/Chat/ChatCreation.tsx rename to packages/status-react/src/components/Chat-legacy/ChatCreation.tsx diff --git a/packages/status-react/src/components/Chat/ChatInput.tsx b/packages/status-react/src/components/Chat-legacy/ChatInput.tsx similarity index 100% rename from packages/status-react/src/components/Chat/ChatInput.tsx rename to packages/status-react/src/components/Chat-legacy/ChatInput.tsx diff --git a/packages/status-react/src/components/Chat/ChatMessageContent.tsx b/packages/status-react/src/components/Chat-legacy/ChatMessageContent.tsx similarity index 100% rename from packages/status-react/src/components/Chat/ChatMessageContent.tsx rename to packages/status-react/src/components/Chat-legacy/ChatMessageContent.tsx diff --git a/packages/status-react/src/components/Chat/ChatTopbar.tsx b/packages/status-react/src/components/Chat-legacy/ChatTopbar.tsx similarity index 98% rename from packages/status-react/src/components/Chat/ChatTopbar.tsx rename to packages/status-react/src/components/Chat-legacy/ChatTopbar.tsx index 3565c8c2..54184307 100644 --- a/packages/status-react/src/components/Chat/ChatTopbar.tsx +++ b/packages/status-react/src/components/Chat-legacy/ChatTopbar.tsx @@ -5,7 +5,6 @@ import styled from 'styled-components' import { useMessengerContext } from '../../contexts/messengerProvider' import { useNarrow } from '../../contexts/narrowProvider' import { useClickOutside } from '../../hooks/useClickOutside' -import { CommunitySidebar } from '../../modules/community/CommunitySidebar' import { ActivityButton, ActivityWrapper, @@ -18,6 +17,7 @@ import { MoreIcon } from '../Icons/MoreIcon' import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton' import { Loading } from '../Skeleton/Loading' import { ChatBodyState } from './ChatBody' +import { CommunitySidebar } from './CommunitySidebar' export function ChatTopbarLoading() { const narrow = useNarrow() diff --git a/packages/status-react/src/components/Chat-legacy/CommunitySidebar.tsx b/packages/status-react/src/components/Chat-legacy/CommunitySidebar.tsx new file mode 100644 index 00000000..421add1f --- /dev/null +++ b/packages/status-react/src/components/Chat-legacy/CommunitySidebar.tsx @@ -0,0 +1,38 @@ +import React from 'react' + +import styled from 'styled-components' + +import { useMessengerContext } from '../../contexts/messengerProvider' +import { useModal } from '../../contexts/modalProvider' +import { CommunityIdentity } from '../CommunityIdentity' +import { CommunityModalName } from '../Modals/CommunityModal' +import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton' + +interface CommunityProps { + className?: string +} + +export function CommunitySidebar({ className }: CommunityProps) { + const { communityData } = useMessengerContext() + const { setModal } = useModal(CommunityModalName) + + if (!communityData) { + return ( + + + + ) + } + + return ( + <> + + + ) +} + +const SkeletonWrapper = styled.div` + margin-bottom: 16px; +` diff --git a/packages/status-react/src/components/Chat/EmojiPicker.tsx b/packages/status-react/src/components/Chat-legacy/EmojiPicker.tsx similarity index 100% rename from packages/status-react/src/components/Chat/EmojiPicker.tsx rename to packages/status-react/src/components/Chat-legacy/EmojiPicker.tsx diff --git a/packages/status-react/src/components/Messages/MessageQuote.tsx b/packages/status-react/src/components/Messages/MessageQuote.tsx index 0befd779..54aee50e 100644 --- a/packages/status-react/src/components/Messages/MessageQuote.tsx +++ b/packages/status-react/src/components/Messages/MessageQuote.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components' import { useMessengerContext } from '../../contexts/messengerProvider' import { useScrollToMessage } from '../../contexts/scrollProvider' -import { ReplyOn, ReplyTo } from '../Chat/ChatInput' +import { ReplyOn, ReplyTo } from '../Chat-legacy/ChatInput' import { QuoteSvg } from '../Icons/QuoteIcon' import { UserIcon } from '../Icons/UserIcon' diff --git a/packages/status-react/src/components/Messages/UiMessage.tsx b/packages/status-react/src/components/Messages/UiMessage.tsx index ca81411d..4895b38c 100644 --- a/packages/status-react/src/components/Messages/UiMessage.tsx +++ b/packages/status-react/src/components/Messages/UiMessage.tsx @@ -6,7 +6,7 @@ import { useIdentity } from '../../contexts/identityProvider' import { useMessengerContext } from '../../contexts/messengerProvider' import { useClickOutside } from '../../hooks/useClickOutside' import { equalDate } from '../../utils' -import { ChatMessageContent } from '../Chat/ChatMessageContent' +import { ChatMessageContent } from '../Chat-legacy/ChatMessageContent' import { ContactMenu } from '../Form/ContactMenu' import { MessageMenu } from '../Form/MessageMenu' import { UntrustworthIcon } from '../Icons/UntrustworthIcon' diff --git a/packages/status-react/src/components/SearchBlock.tsx b/packages/status-react/src/components/SearchBlock.tsx index 41b4cbb3..b032c88e 100644 --- a/packages/status-react/src/components/SearchBlock.tsx +++ b/packages/status-react/src/components/SearchBlock.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react' import styled from 'styled-components' import { useMessengerContext } from '../contexts/messengerProvider' -import { ContactsList } from './Chat/ChatCreation' +import { ContactsList } from './Chat-legacy/ChatCreation' import { Member } from './Members/Member' interface SearchBlockProps { diff --git a/packages/status-react/src/components/chat/chat-input.tsx b/packages/status-react/src/components/chat/chat-input.tsx new file mode 100644 index 00000000..b579223b --- /dev/null +++ b/packages/status-react/src/components/chat/chat-input.tsx @@ -0,0 +1,136 @@ +import React from 'react' + +import { useChatState } from '~/src/contexts/chat-context' +import { CrossIcon } from '~/src/icons/cross-icon' +import { EmojiIcon } from '~/src/icons/emoji-icon' +import { GifIcon } from '~/src/icons/gif-icon' +import { ImageIcon } from '~/src/icons/image-icon' +import { ReplyIcon } from '~/src/icons/reply-icon' +import { StickerIcon } from '~/src/icons/sticker-icon' +import { styled } from '~/src/styles/config' +import { Icon, Image } from '~/src/system' +import { Flex } from '~/src/system/flex' +import { IconButton } from '~/src/system/icon-button' +import { Text } from '~/src/system/text' + +import type { Message } from '~/src/contexts/chat-context' + +export const ChatInput = () => { + const { state } = useChatState() + + return ( + + + + + + {state.message && } + + + + + + + + + + + + + + + + + + ) +} + +interface InputReplyProps { + reply: Message +} + +const InputReply = ({ reply }: InputReplyProps) => { + const { dispatch } = useChatState() + return ( + + + + + + vitalik.eth + + + + dispatch({ type: 'CANCEL_REPLY' })} + > + + + + {reply.type === 'text' && ( + + + This a very very very very very very very very very very very very + very very very very very very very very very very very very very + very very very very long message that is going to be truncated. + + + )} + {reply.type === 'image' && ( + message + )} + + ) +} + +const Wrapper = styled('div', { + display: 'flex', + overflow: 'hidden', + alignItems: 'flex-end', + padding: '12px 8px 12px 10px', + gap: 4, +}) + +const Bubble = styled('div', { + width: '100%', + background: '#EEF2F5', + borderRadius: '16px 16px 4px 16px;', + padding: 2, + overflow: 'hidden', +}) + +const InputWrapper = styled('div', { + display: 'flex', + height: 40, + width: '100%', + alignItems: 'center', + background: '#EEF2F5', + padding: '0 0 0 12px', +}) + +const Input = styled('input', { + display: 'flex', + background: 'none', + alignItems: 'center', + width: '100%', +}) + +const Reply = styled('div', { + display: 'flex', + flexDirection: 'column', + width: '100%', + overflow: 'hidden', + padding: '6px 12px', + background: 'rgba(0, 0, 0, 0.1)', + borderRadius: '14px 14px 4px 14px;', +}) diff --git a/packages/status-react/src/components/chat/chat-message.tsx b/packages/status-react/src/components/chat/chat-message.tsx new file mode 100644 index 00000000..24b2f457 --- /dev/null +++ b/packages/status-react/src/components/chat/chat-message.tsx @@ -0,0 +1,323 @@ +import React from 'react' + +import { useChatState } from '~/src/contexts/chat-context' +import { BellIcon } from '~/src/icons/bell-icon' +import { PencilIcon } from '~/src/icons/pencil-icon' +import { PinIcon } from '~/src/icons/pin-icon' +import { ReactionIcon } from '~/src/icons/reaction-icon' +import { ReplyIcon } from '~/src/icons/reply-icon' +import { TrashIcon } from '~/src/icons/trash-icon' +import { styled } from '~/src/styles/config' +import { AlertDialog, AlertDialogTrigger, Box } from '~/src/system' +import { Avatar } from '~/src/system/avatar' +import { ContextMenu, ContextMenuTrigger } from '~/src/system/context-menu' +import { DropdownMenu, DropdownMenuTrigger } from '~/src/system/dropdown-menu' +import { Flex } from '~/src/system/flex' +import { IconButton } from '~/src/system/icon-button' +import { Image } from '~/src/system/image' +import { Text } from '~/src/system/text' +import { Tooltip } from '~/src/system/tooltip' + +interface Props { + reply?: 'text' | 'image' | 'image-text' + image?: boolean + mention?: boolean +} + +const MessageLink = (props: React.AnchorHTMLAttributes) => { + const { onClick } = props + + return ( + { + onClick?.(e) + e.preventDefault() + }} + > + https://specs.status.im/spec/ + + ) +} + +export const ChatMessage = (props: Props) => { + const { reply, image, mention } = props + + const { dispatch } = useChatState() + + return ( + <> + {reply && } + + +
+ + + +
+ + simon.eth +
+ + }> + View Profile + + }> + Send Message + + }> + Verify Identity + + }> + Send Contact Request + + + } danger> + Mark as Untrustworthy + +
+
+
+ +
+ + + carmen + + + 10:00 + + + + + My first hoya{' '} + + + https://specs.status.im/spec + + + {' '} + bloom has started to develop alongside my first aphid issue 😩 + + + {image && ( + + message + + )} +
+ + + + + + + + + + dispatch({ + type: 'SET_REPLY', + message: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + type: reply!, + text: 'bloom has started to develop alongside my first aphid issue 😩', + }, + }) + } + > + + + + + + + + + + + + + + + + + + + +
+ + Reply + Pin + +
+ + ) +} + +const MessageReply = ({ + reply, +}: { + reply: 'text' | 'image' | 'image-text' +}) => { + return ( + + + + + vitalik.eth + + + {reply === 'text' && ( + + + This a very very very very very very very very very very very very + very very very very very very very very very very very very very + very very very very long message that is going to be truncated. + + + )} + {reply === 'image' && ( + message + )} + {reply === 'image-text' && ( + + + This a very very very very very very very very very very very very + very very very very very very very very very very very very very + very very very very long message that is going to be truncated. + + message + + )} + + ) +} + +const Wrapper = styled('div', { + position: 'relative', + padding: '10px 16px', + display: 'flex', + gap: '$2', + + transitionProperty: 'background-color, border-color, color, fill, stroke', + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '100ms', + + '&:hover, &[data-open="true"]': { + background: '#EEF2F5', + // [`& ${Actions}`]: { + // marginLeft: '5px', + // }, + }, + + a: { + textDecoration: 'underline', + }, + + variants: { + mention: { + true: { + background: '$mention-4', + + '&:hover, &[data-open="true"]': { + background: '$mention-3', + // [`& ${Actions}`]: { + // marginLeft: '5px', + // }, + }, + + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + width: 3, + background: '$mention-1', + }, + }, + }, + }, +}) + +const Reply = styled('div', { + position: 'relative', + // height: 40, + marginLeft: 68, + display: 'flex', + flexDirection: 'column', + gap: '$1', + + '&::before, &::after': { + content: '""', + position: 'absolute', + '--background-accent': 'rgba(147, 155, 161, 0.4)', + '--avatar-size': '44px', + '--gutter': '8px', + '--width': '2px', + }, + + '&::before': { + display: 'block', + position: 'absolute', + top: 10, + right: 'calc(100% + 10px)', + bottom: '0', + left: 'calc(var(--avatar-size)/2*-1 + var(--gutter)*-1)', + marginRight: 'var(--reply-spacing)', + marginTop: 'calc(var(--width)*-1/2)', + marginLeft: 'calc(var(--width)*-1/2)', + marginBottom: 'calc(0.125rem - 4px)', + borderLeft: 'var(--width) solid var(--background-accent)', + borderBottom: '0 solid var(--background-accent)', + borderRight: '0 solid var(--background-accent)', + borderTop: 'var(--width) solid var(--background-accent)', + borderTopLeftRadius: '10px', + }, +}) + +const Actions = styled('div', { + position: 'absolute', + top: -18, + right: 16, + padding: 2, + boxShadow: '0px 4px 12px rgba(0, 34, 51, 0.08)', + background: '#fff', + borderRadius: 8, + display: 'none', + + ':hover > &': { + display: 'flex', + }, +}) diff --git a/packages/status-react/src/components/chat/index.tsx b/packages/status-react/src/components/chat/index.tsx new file mode 100644 index 00000000..a24a3006 --- /dev/null +++ b/packages/status-react/src/components/chat/index.tsx @@ -0,0 +1,81 @@ +import React from 'react' + +import { useAppState } from '~/src/contexts/app-context' +import { ChatProvider } from '~/src/contexts/chat-context' +import { styled } from '~/src/styles/config' +import { Avatar } from '~/src/system/avatar' +import { Flex } from '~/src/system/flex' +import { Heading } from '~/src/system/heading' +import { Text } from '~/src/system/text' + +import { MemberSidebar } from '../member-sidebar' +import { ChatInput } from './chat-input' +import { ChatMessage } from './chat-message' +import { Navbar } from './navbar' + +const EmptyChat = () => { + return ( + + + general + Welcome to the beginning of the #general channel! + + ) +} + +const Content = () => { + return ( +
+ + + + + + + + + + + + + +
+ ) +} + +export const Chat = () => { + const { state } = useAppState() + + // TODO: Update condition based on a chat type + const enableMembers = true + const showMembers = enableMembers && state.showMembers + + return ( + + +
+ + + +
+ {showMembers && } +
+
+ ) +} + +const Wrapper = styled('div', { + flex: 1, + position: 'relative', + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'stretch', + background: '#fff', +}) + +const Main = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'column', +}) diff --git a/packages/status-react/src/components/chat/navbar.tsx b/packages/status-react/src/components/chat/navbar.tsx new file mode 100644 index 00000000..0db7dc7d --- /dev/null +++ b/packages/status-react/src/components/chat/navbar.tsx @@ -0,0 +1,179 @@ +import React from 'react' + +import { useMatch } from 'react-router-dom' + +import { useAppState } from '~/src/contexts/app-context' +import { BellIcon } from '~/src/icons/bell-icon' +import { DotsIcon } from '~/src/icons/dots-icon' +import { GroupIcon } from '~/src/icons/group-icon' +import { styled } from '~/src/styles/config' +import { Separator } from '~/src/system' +import { Avatar } from '~/src/system/avatar' +import { DropdownMenu, DropdownMenuTrigger } from '~/src/system/dropdown-menu' +import { Flex } from '~/src/system/flex' +import { IconButton } from '~/src/system/icon-button' +import { Text } from '~/src/system/text' + +interface Props { + enableMembers: boolean +} + +const chats: Record = { + welcome: { type: 'channel' }, + general: { type: 'channel' }, + random: { type: 'channel' }, + 'vitalik.eth': { type: 'chat' }, + 'pvl.eth': { type: 'chat' }, + 'Climate Change': { type: 'group-chat' }, +} + +export const Navbar = (props: Props) => { + const { enableMembers } = props + + const { state, dispatch } = useAppState() + const { params } = useMatch(':id')! // eslint-disable-line @typescript-eslint/no-non-null-assertion + + const chat = chats[params.id!] + + const renderIdentity = () => { + if (chat.type == 'channel') { + return ( + + +
+ #general + + 2 pinned messages | General discussions about CryptoKitties. + +
+
+ ) + } + + if (chat.type == 'group-chat') { + return ( + + +
+ Climate Change + + 3 pinned messages | 5 members + +
+
+ ) + } + + return ( + + +
+ pvl.eth + + 0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377 + +
+
+ ) + } + + const renderMenuItems = () => { + if (chat.type === 'channel') { + return ( + <> + }> + For 15 min + For 1 hour + For 8 hours + For 24 hours + Until I turn it back on + + }> + Mark as Read + + + ) + } + return ( + <> + {chat.type === 'chat' && ( + }> + View Profile + + )} + {chat.type === 'group-chat' && ( + }>Edit Group + )} + + {chat.type === 'group-chat' && ( + }> + Customize Chat + + )} + }> + For 15 min + For 1 hour + For 8 hours + For 24 hours + Until I turn it back on + + }>Mark as Read + }> + Last 24 hours + Last 2 days + Last 3 days + Last 7 days + + + {chat.type === 'chat' && ( + } danger> + Delete Chat + + )} + {chat.type === 'group-chat' && ( + } danger> + Leave Chat + + )} + + ) + } + + return ( + + {renderIdentity()} + + + {enableMembers && ( + dispatch({ type: 'TOGGLE_MEMBERS' })} + > + + + )} + + + + + + {renderMenuItems()} + + + + + + + + + + ) +} + +const NavbarWrapper = styled('div', { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '10px 16px', +}) diff --git a/packages/status-react/src/contexts/chat-context.tsx b/packages/status-react/src/contexts/chat-context.tsx new file mode 100644 index 00000000..99d057b7 --- /dev/null +++ b/packages/status-react/src/contexts/chat-context.tsx @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useMemo, useReducer } from 'react' + +import type { Dispatch, Reducer } from 'react' + +type Context = { + state: State + dispatch: Dispatch +} + +const ChatContext = createContext(undefined) + +// TODO: Take from generated protobuf +export interface Message { + type: 'text' | 'image' | 'image-text' + text?: string +} + +interface State { + message?: Message +} + +type Action = + | { type: 'SET_REPLY'; message?: Message } + | { type: 'CANCEL_REPLY' } + +const reducer: Reducer = (state, action) => { + switch (action.type) { + case 'SET_REPLY': { + return { ...state, message: action.message } + } + case 'CANCEL_REPLY': { + return { ...state, message: undefined } + } + } +} + +const initialState: State = { + message: undefined, +} + +interface Props { + children: React.ReactNode +} + +export const ChatProvider = (props: Props) => { + const { children } = props + + const [state, dispatch] = useReducer(reducer, initialState) + const value = useMemo(() => ({ state, dispatch }), [state]) + + return {children} +} + +export const useChatState = () => { + const context = useContext(ChatContext) + + if (!context) { + throw new Error('useChatState must be used within a ChatProvider') + } + + return context +}