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 { useIdentity } from '../../contexts/identityProvider'
|
||||||
import { useActivities } from '../../hooks/useActivities'
|
import { useActivities } from '../../hooks/useActivities'
|
||||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||||
import { TopBtn } from '../Chat/ChatTopbar'
|
import { TopBtn } from '../Chat-legacy/ChatTopbar'
|
||||||
import { ActivityIcon } from '../Icons/ActivityIcon'
|
import { ActivityIcon } from '../Icons/ActivityIcon'
|
||||||
import { ActivityCenter } from './ActivityCenter'
|
import { ActivityCenter } from './ActivityCenter'
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useScrollToMessage } from '../../contexts/scrollProvider'
|
||||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||||
import { equalDate } from '../../utils/equalDate'
|
import { equalDate } from '../../utils/equalDate'
|
||||||
import { DownloadButton } from '../Buttons/DownloadButton'
|
import { DownloadButton } from '../Buttons/DownloadButton'
|
||||||
import { Mention } from '../Chat/ChatMessageContent'
|
import { Mention } from '../Chat-legacy/ChatMessageContent'
|
||||||
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'
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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 { useClickOutside } from '../../hooks/useClickOutside'
|
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||||
import { CommunitySidebar } from '../../modules/community/CommunitySidebar'
|
|
||||||
import {
|
import {
|
||||||
ActivityButton,
|
ActivityButton,
|
||||||
ActivityWrapper,
|
ActivityWrapper,
|
||||||
|
@ -18,6 +17,7 @@ import { MoreIcon } from '../Icons/MoreIcon'
|
||||||
import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton'
|
import { CommunitySkeleton } from '../Skeleton/CommunitySkeleton'
|
||||||
import { Loading } from '../Skeleton/Loading'
|
import { Loading } from '../Skeleton/Loading'
|
||||||
import { ChatBodyState } from './ChatBody'
|
import { ChatBodyState } from './ChatBody'
|
||||||
|
import { CommunitySidebar } from './CommunitySidebar'
|
||||||
|
|
||||||
export function ChatTopbarLoading() {
|
export function ChatTopbarLoading() {
|
||||||
const narrow = useNarrow()
|
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 { useMessengerContext } from '../../contexts/messengerProvider'
|
||||||
import { useScrollToMessage } from '../../contexts/scrollProvider'
|
import { useScrollToMessage } from '../../contexts/scrollProvider'
|
||||||
import { ReplyOn, ReplyTo } from '../Chat/ChatInput'
|
import { ReplyOn, ReplyTo } from '../Chat-legacy/ChatInput'
|
||||||
import { QuoteSvg } from '../Icons/QuoteIcon'
|
import { QuoteSvg } from '../Icons/QuoteIcon'
|
||||||
import { UserIcon } from '../Icons/UserIcon'
|
import { UserIcon } from '../Icons/UserIcon'
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useIdentity } from '../../contexts/identityProvider'
|
||||||
import { useMessengerContext } from '../../contexts/messengerProvider'
|
import { useMessengerContext } from '../../contexts/messengerProvider'
|
||||||
import { useClickOutside } from '../../hooks/useClickOutside'
|
import { useClickOutside } from '../../hooks/useClickOutside'
|
||||||
import { equalDate } from '../../utils'
|
import { equalDate } from '../../utils'
|
||||||
import { ChatMessageContent } from '../Chat/ChatMessageContent'
|
import { ChatMessageContent } from '../Chat-legacy/ChatMessageContent'
|
||||||
import { ContactMenu } from '../Form/ContactMenu'
|
import { ContactMenu } from '../Form/ContactMenu'
|
||||||
import { MessageMenu } from '../Form/MessageMenu'
|
import { MessageMenu } from '../Form/MessageMenu'
|
||||||
import { UntrustworthIcon } from '../Icons/UntrustworthIcon'
|
import { UntrustworthIcon } from '../Icons/UntrustworthIcon'
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { useMemo } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { useMessengerContext } from '../contexts/messengerProvider'
|
import { useMessengerContext } from '../contexts/messengerProvider'
|
||||||
import { ContactsList } from './Chat/ChatCreation'
|
import { ContactsList } from './Chat-legacy/ChatCreation'
|
||||||
import { Member } from './Members/Member'
|
import { Member } from './Members/Member'
|
||||||
|
|
||||||
interface SearchBlockProps {
|
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