feat(react): add all chat input variants
This commit is contained in:
parent
f69a37c041
commit
f309ba291b
|
@ -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 { useAppState } from '~/src/contexts/app-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 { Avatar, Flex, Heading, Text } from '~/src/system'
|
||||
|
||||
import { ChatInput } from './chat-input'
|
||||
import { ChatMessage } from './chat-message'
|
||||
import { ChatInput } from './components/chat-input'
|
||||
import { ChatMessage } from './components/chat-message'
|
||||
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 (
|
||||
<Flex direction="column" gap="3" align="center" css={{ marginBottom: 50 }}>
|
||||
<Avatar size={120} />
|
||||
<Avatar size={120} src={chat.imageUrl} />
|
||||
<Heading>general</Heading>
|
||||
<Text>Welcome to the beginning of the #general channel!</Text>
|
||||
</Flex>
|
||||
|
@ -21,22 +30,22 @@ const EmptyChat = () => {
|
|||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<ContentWrapper ref={contentRef}>
|
||||
<ChatStart />
|
||||
{messages.map(message => (
|
||||
<ChatMessage key={message.id} message={message} />
|
||||
))}
|
||||
</ContentWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -71,6 +80,20 @@ const Wrapper = styled('div', {
|
|||
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', {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
|
|
Loading…
Reference in New Issue