refactor(react): chat messages content
This commit is contained in:
parent
aa0dd52252
commit
66ecfa407e
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
|
@ -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;
|
||||
`
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;',
|
||||
})
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue