feat(react): add all chat input variants

This commit is contained in:
Pavel Prichodko 2022-04-13 14:24:48 +02:00
parent 234cf44ebc
commit 9bef44760c
No known key found for this signature in database
GPG Key ID: 8E4C82D464215E83
4 changed files with 224 additions and 166 deletions

View File

@ -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;',
})

View File

@ -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%',
})

View File

@ -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;',
})

View File

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