refactor(react): chat messages content

This commit is contained in:
Pavel Prichodko 2022-03-31 15:05:03 +02:00
parent aa0dd52252
commit 66ecfa407e
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
17 changed files with 825 additions and 6 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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()

View File

@ -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 (
<SkeletonWrapper>
<CommunitySkeleton />
</SkeletonWrapper>
)
}
return (
<>
<button className={className} onClick={() => setModal(true)}>
<CommunityIdentity subtitle={`${communityData.members} members`} />
</button>
</>
)
}
const SkeletonWrapper = styled.div`
margin-bottom: 16px;
`

View File

@ -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'

View File

@ -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'

View File

@ -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 {

View File

@ -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 (
<Wrapper>
<IconButton label="Add file">
<ImageIcon />
</IconButton>
<Bubble>
{state.message && <InputReply reply={state.message} />}
<InputWrapper>
<Input placeholder="Message" />
<Flex>
<IconButton label="Pick emoji">
<EmojiIcon />
</IconButton>
<IconButton label="Pick sticker">
<StickerIcon />
</IconButton>
<IconButton label="Pick gif">
<GifIcon />
</IconButton>
</Flex>
</InputWrapper>
</Bubble>
</Wrapper>
)
}
interface InputReplyProps {
reply: Message
}
const InputReply = ({ reply }: InputReplyProps) => {
const { dispatch } = useChatState()
return (
<Reply>
<Flex align="center" justify="between">
<Flex gap={1}>
<Icon hidden>
<ReplyIcon />
</Icon>
<Text size="13" weight="500" truncate={false}>
vitalik.eth
</Text>
</Flex>
<IconButton
label="Cancel reply"
onClick={() => dispatch({ type: 'CANCEL_REPLY' })}
>
<CrossIcon />
</IconButton>
</Flex>
{reply.type === 'text' && (
<Flex>
<Text size="13" truncate={false}>
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.
</Text>
</Flex>
)}
{reply.type === 'image' && (
<Image
src="https://images.unsplash.com/photo-1647531041383-fe7103712f16?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80"
width={56}
height={56}
fit="cover"
radius="bubble"
alt="message"
/>
)}
</Reply>
)
}
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;',
})

View File

@ -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<HTMLAnchorElement>) => {
const { onClick } = props
return (
<a
{...props}
href="https://specs.status.im/spec/"
onClick={e => {
onClick?.(e)
e.preventDefault()
}}
>
https://specs.status.im/spec/
</a>
)
}
export const ChatMessage = (props: Props) => {
const { reply, image, mention } = props
const { dispatch } = useChatState()
return (
<>
{reply && <MessageReply reply={reply} />}
<ContextMenuTrigger>
<Wrapper mention={mention}>
<div>
<DropdownMenuTrigger>
<button type="button">
<Avatar size={44} />
</button>
<DropdownMenu>
<div>
<Avatar size={36} />
<Text>simon.eth</Text>
</div>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<BellIcon />}>
View Profile
</DropdownMenu.Item>
<DropdownMenu.Item icon={<BellIcon />}>
Send Message
</DropdownMenu.Item>
<DropdownMenu.Item icon={<BellIcon />}>
Verify Identity
</DropdownMenu.Item>
<DropdownMenu.Item icon={<BellIcon />}>
Send Contact Request
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<BellIcon />} danger>
Mark as Untrustworthy
</DropdownMenu.Item>
</DropdownMenu>
</DropdownMenuTrigger>
</div>
<div style={{ flex: 1 }}>
<Flex>
<Text color="primary" weight="500">
carmen
</Text>
<Text size="10" color="gray">
10:00
</Text>
</Flex>
<Text>
My first hoya{' '}
<AlertDialogTrigger>
<MessageLink href="https://specs.status.im/spec">
https://specs.status.im/spec
</MessageLink>
<AlertDialog
title="Are you sure you want to visit this website?"
description="https://specs.status.im/spec"
actionLabel="Yes, take me there"
/>
</AlertDialogTrigger>{' '}
bloom has started to develop alongside my first aphid issue 😩
</Text>
{image && (
<Flex gap={1} css={{ paddingTop: '$1' }}>
<Image
width={147}
alt="message"
height={196}
src="https://images.unsplash.com/photo-1648492678772-8b52fe36cebc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3570&q=80"
radius="bubble"
/>
</Flex>
)}
</div>
<Actions>
<Tooltip label="React">
<IconButton label="Pick reaction" intent="info" color="gray">
<ReactionIcon />
</IconButton>
</Tooltip>
<Tooltip label="Reply">
<IconButton
label="Reply to message"
intent="info"
color="gray"
onClick={() =>
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 😩',
},
})
}
>
<ReplyIcon />
</IconButton>
</Tooltip>
<Tooltip label="Edit">
<IconButton label="Edit message" intent="info" color="gray">
<PencilIcon />
</IconButton>
</Tooltip>
<Tooltip label="Pin">
<IconButton label="Pin message" intent="info" color="gray">
<PinIcon />
</IconButton>
</Tooltip>
<Tooltip label="Delete">
<IconButton label="Delete message" intent="danger" color="gray">
<TrashIcon />
</IconButton>
</Tooltip>
</Actions>
</Wrapper>
<ContextMenu>
<ContextMenu.Item>Reply</ContextMenu.Item>
<ContextMenu.Item>Pin</ContextMenu.Item>
</ContextMenu>
</ContextMenuTrigger>
</>
)
}
const MessageReply = ({
reply,
}: {
reply: 'text' | 'image' | 'image-text'
}) => {
return (
<Reply>
<Flex gap="1" align="center">
<Avatar size={20} />
<Text color="gray" size="13" weight="500">
vitalik.eth
</Text>
</Flex>
{reply === 'text' && (
<Flex>
<Text color="gray" size="13" truncate={false}>
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.
</Text>
</Flex>
)}
{reply === 'image' && (
<Image
src="https://images.unsplash.com/photo-1647531041383-fe7103712f16?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80"
width={56}
height={56}
fit="cover"
radius="1"
alt="message"
/>
)}
{reply === 'image-text' && (
<Box>
<Text color="gray" size="13" truncate={false}>
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.
</Text>
<Image
src="https://images.unsplash.com/photo-1647531041383-fe7103712f16?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80"
width={56}
height={56}
fit="cover"
radius="1"
alt="message"
/>
</Box>
)}
</Reply>
)
}
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',
},
})

View File

@ -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 (
<Flex direction="column" gap="3" align="center" css={{ marginBottom: 50 }}>
<Avatar size={120} />
<Heading>general</Heading>
<Text>Welcome to the beginning of the #general channel!</Text>
</Flex>
)
}
const Content = () => {
return (
<div style={{ flex: 1, overflowY: 'auto' }}>
<EmptyChat />
<ChatMessage reply="text" />
<ChatMessage />
<ChatMessage reply="image" />
<ChatMessage image />
<ChatMessage reply="image-text" />
<ChatMessage />
<ChatMessage />
<ChatMessage mention />
<ChatMessage />
<ChatMessage />
<ChatMessage />
<ChatMessage />
</div>
)
}
export const Chat = () => {
const { state } = useAppState()
// TODO: Update condition based on a chat type
const enableMembers = true
const showMembers = enableMembers && state.showMembers
return (
<ChatProvider>
<Wrapper>
<Main>
<Navbar enableMembers={enableMembers} />
<Content />
<ChatInput />
</Main>
{showMembers && <MemberSidebar />}
</Wrapper>
</ChatProvider>
)
}
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',
})

View File

@ -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<string, { type: 'channel' | 'group-chat' | 'chat' }> = {
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 (
<Flex align="center" gap="2">
<Avatar size={36} />
<div>
<Text>#general</Text>
<Text size={12} color="gray">
2 pinned messages | General discussions about CryptoKitties.
</Text>
</div>
</Flex>
)
}
if (chat.type == 'group-chat') {
return (
<Flex align="center" gap="2">
<Avatar size={36} />
<div>
<Text>Climate Change</Text>
<Text size={12} color="gray">
3 pinned messages | 5 members
</Text>
</div>
</Flex>
)
}
return (
<Flex align="center" gap="2">
<Avatar size={36} />
<div>
<Text>pvl.eth</Text>
<Text size={12} color="gray">
0x63FaC9201494f0bd17B9892B9fae4d52fe3BD377
</Text>
</div>
</Flex>
)
}
const renderMenuItems = () => {
if (chat.type === 'channel') {
return (
<>
<DropdownMenu.TriggerItem label="Mute Channel" icon={<BellIcon />}>
<DropdownMenu.Item>For 15 min</DropdownMenu.Item>
<DropdownMenu.Item>For 1 hour</DropdownMenu.Item>
<DropdownMenu.Item>For 8 hours</DropdownMenu.Item>
<DropdownMenu.Item>For 24 hours</DropdownMenu.Item>
<DropdownMenu.Item>Until I turn it back on</DropdownMenu.Item>
</DropdownMenu.TriggerItem>
<DropdownMenu.Item icon={<BellIcon />}>
Mark as Read
</DropdownMenu.Item>
</>
)
}
return (
<>
{chat.type === 'chat' && (
<DropdownMenu.Item icon={<BellIcon />}>
View Profile
</DropdownMenu.Item>
)}
{chat.type === 'group-chat' && (
<DropdownMenu.Item icon={<BellIcon />}>Edit Group</DropdownMenu.Item>
)}
<DropdownMenu.Separator />
{chat.type === 'group-chat' && (
<DropdownMenu.Item icon={<BellIcon />}>
Customize Chat
</DropdownMenu.Item>
)}
<DropdownMenu.TriggerItem label="Mute Chat" icon={<BellIcon />}>
<DropdownMenu.Item>For 15 min</DropdownMenu.Item>
<DropdownMenu.Item>For 1 hour</DropdownMenu.Item>
<DropdownMenu.Item>For 8 hours</DropdownMenu.Item>
<DropdownMenu.Item>For 24 hours</DropdownMenu.Item>
<DropdownMenu.Item>Until I turn it back on</DropdownMenu.Item>
</DropdownMenu.TriggerItem>
<DropdownMenu.Item icon={<BellIcon />}>Mark as Read</DropdownMenu.Item>
<DropdownMenu.TriggerItem label="Fetch Messages" icon={<BellIcon />}>
<DropdownMenu.Item>Last 24 hours</DropdownMenu.Item>
<DropdownMenu.Item>Last 2 days</DropdownMenu.Item>
<DropdownMenu.Item>Last 3 days</DropdownMenu.Item>
<DropdownMenu.Item>Last 7 days</DropdownMenu.Item>
</DropdownMenu.TriggerItem>
<DropdownMenu.Separator />
{chat.type === 'chat' && (
<DropdownMenu.Item icon={<BellIcon />} danger>
Delete Chat
</DropdownMenu.Item>
)}
{chat.type === 'group-chat' && (
<DropdownMenu.Item icon={<BellIcon />} danger>
Leave Chat
</DropdownMenu.Item>
)}
</>
)
}
return (
<NavbarWrapper>
{renderIdentity()}
<Flex gap="2" align="center">
{enableMembers && (
<IconButton
label="Toggle Members"
active={state.showMembers}
onClick={() => dispatch({ type: 'TOGGLE_MEMBERS' })}
>
<GroupIcon />
</IconButton>
)}
<DropdownMenuTrigger>
<IconButton label="Options">
<DotsIcon />
</IconButton>
<DropdownMenu align="end">{renderMenuItems()}</DropdownMenu>
</DropdownMenuTrigger>
<Separator orientation="vertical" css={{ height: 24 }} />
<IconButton label="Show Activity Center">
<BellIcon />
</IconButton>
</Flex>
</NavbarWrapper>
)
}
const NavbarWrapper = styled('div', {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 16px',
})

View File

@ -0,0 +1,62 @@
import React, { createContext, useContext, useMemo, useReducer } from 'react'
import type { Dispatch, Reducer } from 'react'
type Context = {
state: State
dispatch: Dispatch<Action>
}
const ChatContext = createContext<Context | undefined>(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> = (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 <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
}
export const useChatState = () => {
const context = useContext(ChatContext)
if (!context) {
throw new Error('useChatState must be used within a ChatProvider')
}
return context
}