feat(react): add all chat input variants
This commit is contained in:
parent
234cf44ebc
commit
9bef44760c
|
@ -1,146 +0,0 @@
|
||||||
import React, { useEffect, useRef } 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 { Flex, Icon, IconButton, Image, Text } from '~/src/system'
|
|
||||||
|
|
||||||
import type { Message } from '~/src/contexts/chat-context'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatInput = (props: Props) => {
|
|
||||||
const { value } = props
|
|
||||||
const { state } = useChatState()
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.message) {
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}
|
|
||||||
}, [state.message])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<IconButton label="Add file">
|
|
||||||
<ImageIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Bubble>
|
|
||||||
{state.message && <InputReply reply={state.message} />}
|
|
||||||
|
|
||||||
<InputWrapper>
|
|
||||||
<Input ref={inputRef} placeholder="Message" defaultValue={value} />
|
|
||||||
<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,96 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { useChatContext } from '~/src/contexts/chat-context'
|
||||||
|
import { EmojiIcon } from '~/src/icons/emoji-icon'
|
||||||
|
import { GifIcon } from '~/src/icons/gif-icon'
|
||||||
|
import { ImageIcon } from '~/src/icons/image-icon'
|
||||||
|
import { StickerIcon } from '~/src/icons/sticker-icon'
|
||||||
|
import { styled } from '~/src/styles/config'
|
||||||
|
import { Flex, IconButton } from '~/src/system'
|
||||||
|
|
||||||
|
import { InputReply } from './input-reply'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode?: 'normal' | 'editing'
|
||||||
|
value?: string
|
||||||
|
editing?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatInput = (props: Props) => {
|
||||||
|
const { value, editing } = props
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(value ?? '')
|
||||||
|
const { state } = useChatContext()
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
state.message && !editing && inputRef.current?.focus()
|
||||||
|
}, [state.message, editing])
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<IconButton label="Add file">
|
||||||
|
<ImageIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Bubble>
|
||||||
|
{state.message && <InputReply message={state.message} />}
|
||||||
|
<InputWrapper>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder="Message"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Flex>
|
||||||
|
<IconButton label="Pick emoji">
|
||||||
|
<EmojiIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton label="Pick sticker">
|
||||||
|
<StickerIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton label="Pick gif">
|
||||||
|
<GifIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
</InputWrapper>
|
||||||
|
</Bubble>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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%',
|
||||||
|
})
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useChatContext } from '~/src/contexts/chat-context'
|
||||||
|
import { CrossIcon } from '~/src/icons/cross-icon'
|
||||||
|
import { ReplyIcon } from '~/src/icons/reply-icon'
|
||||||
|
import { styled } from '~/src/styles/config'
|
||||||
|
import { Box, Flex, Icon, IconButton, Image, Text } from '~/src/system'
|
||||||
|
|
||||||
|
import type { Message } from '~/src/protocol/use-messages'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputReply = (props: Props) => {
|
||||||
|
const { dispatch } = useChatContext()
|
||||||
|
|
||||||
|
const { message } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Flex align="center" justify="between">
|
||||||
|
<Flex gap={1}>
|
||||||
|
<Icon hidden>
|
||||||
|
<ReplyIcon />
|
||||||
|
</Icon>
|
||||||
|
<Text size="13" weight="500" truncate={false}>
|
||||||
|
{message.contact.name}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
label="Cancel reply"
|
||||||
|
onClick={() => dispatch({ type: 'CANCEL_REPLY' })}
|
||||||
|
>
|
||||||
|
<CrossIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
{message.type === 'text' && (
|
||||||
|
<Flex>
|
||||||
|
<Text size="13" truncate={false}>
|
||||||
|
{message.text}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{message.type === 'image' && (
|
||||||
|
<Image
|
||||||
|
src={message.imageUrl}
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
fit="cover"
|
||||||
|
radius="bubble"
|
||||||
|
alt="message"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{message.type === 'image-text' && (
|
||||||
|
<Box>
|
||||||
|
<Flex>
|
||||||
|
<Text size="13" truncate={false}>
|
||||||
|
{message.text}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Image
|
||||||
|
src={message.imageUrl}
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
fit="cover"
|
||||||
|
radius="bubble"
|
||||||
|
alt="message"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = 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;',
|
||||||
|
})
|
|
@ -1,19 +1,28 @@
|
||||||
import React from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { useMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import { MemberSidebar } from '~/src/components/member-sidebar'
|
import { MemberSidebar } from '~/src/components/member-sidebar'
|
||||||
import { useAppState } from '~/src/contexts/app-context'
|
import { useAppState } from '~/src/contexts/app-context'
|
||||||
import { ChatProvider } from '~/src/contexts/chat-context'
|
import { ChatProvider } from '~/src/contexts/chat-context'
|
||||||
|
import { useChat } from '~/src/protocol/use-chat'
|
||||||
|
import { useMessages } from '~/src/protocol/use-messages'
|
||||||
import { styled } from '~/src/styles/config'
|
import { styled } from '~/src/styles/config'
|
||||||
import { Avatar, Flex, Heading, Text } from '~/src/system'
|
import { Avatar, Flex, Heading, Text } from '~/src/system'
|
||||||
|
|
||||||
import { ChatInput } from './chat-input'
|
import { ChatInput } from './components/chat-input'
|
||||||
import { ChatMessage } from './chat-message'
|
import { ChatMessage } from './components/chat-message'
|
||||||
import { Navbar } from './components/navbar'
|
import { Navbar } from './components/navbar'
|
||||||
|
|
||||||
const EmptyChat = () => {
|
const ChatStart = () => {
|
||||||
|
// TODO: unify this with the useChat hook
|
||||||
|
const { params } = useMatch(':id')! // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
|
|
||||||
|
const chat = useChat(params.id!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3" align="center" css={{ marginBottom: 50 }}>
|
<Flex direction="column" gap="3" align="center" css={{ marginBottom: 50 }}>
|
||||||
<Avatar size={120} />
|
<Avatar size={120} src={chat.imageUrl} />
|
||||||
<Heading>general</Heading>
|
<Heading>general</Heading>
|
||||||
<Text>Welcome to the beginning of the #general channel!</Text>
|
<Text>Welcome to the beginning of the #general channel!</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -21,22 +30,22 @@ const EmptyChat = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = () => {
|
const Content = () => {
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
contentRef.current!.scrollTop = contentRef.current!.scrollHeight ?? 0
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const messages = useMessages()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
<ContentWrapper ref={contentRef}>
|
||||||
<EmptyChat />
|
<ChatStart />
|
||||||
<ChatMessage reply="text" />
|
{messages.map(message => (
|
||||||
<ChatMessage />
|
<ChatMessage key={message.id} message={message} />
|
||||||
<ChatMessage reply="image" />
|
))}
|
||||||
<ChatMessage image />
|
</ContentWrapper>
|
||||||
<ChatMessage reply="image-text" />
|
|
||||||
<ChatMessage />
|
|
||||||
<ChatMessage />
|
|
||||||
<ChatMessage mention />
|
|
||||||
<ChatMessage />
|
|
||||||
<ChatMessage />
|
|
||||||
<ChatMessage />
|
|
||||||
<ChatMessage />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +80,20 @@ const Wrapper = styled('div', {
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ContentWrapper = styled('div', {
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
WebkitOverflowScrolling: 'touch',
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
// scrollSnapType: 'y proximity',
|
||||||
|
|
||||||
|
// '& > div:last-child': {
|
||||||
|
// scrollSnapAlign: 'end',
|
||||||
|
// scrollMarginBlockEnd: '1px',
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
|
||||||
const Main = styled('div', {
|
const Main = styled('div', {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
Loading…
Reference in New Issue