chore: complete message send
This commit is contained in:
parent
50c7b92e36
commit
a9c055b467
11 changed files with 214 additions and 104 deletions
|
@ -95,6 +95,7 @@
|
|||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"lucide-react": "^0.263.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"peerjs": "^1.4.7",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-nice-avatar": "^1.4.1",
|
||||
|
|
|
@ -38,6 +38,9 @@ dependencies:
|
|||
lucide-react:
|
||||
specifier: ^0.263.0
|
||||
version: 0.263.0(react@18.2.0)
|
||||
nanoid:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
peerjs:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
|
@ -5717,6 +5720,12 @@ packages:
|
|||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
/nanoid@4.0.2:
|
||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/natural-compare-lite@1.4.0:
|
||||
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
|
||||
dev: true
|
||||
|
|
|
@ -1,57 +1,52 @@
|
|||
import { type FC, type ChangeEvent, type KeyboardEvent } from 'react'
|
||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
import { Markdown } from '@/components/ui/Markdown'
|
||||
import { cn } from '@/utils'
|
||||
import MessageInputDomain from '@/domain/MessageInput'
|
||||
import { MESSAGE_MAX_LENGTH } from '@/constants'
|
||||
|
||||
export interface MessageInputProps {
|
||||
value?: string
|
||||
className?: string
|
||||
maxLength?: number
|
||||
preview?: boolean
|
||||
onInput?: (value: string) => void
|
||||
onEnter?: (value: string) => void
|
||||
}
|
||||
|
||||
const MessageInput: FC<MessageInputProps> = ({ className }) => {
|
||||
const send = useRemeshSend()
|
||||
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
||||
|
||||
const message = useRemeshQuery(messageInputDomain.query.ValueQuery())
|
||||
const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery())
|
||||
|
||||
const handleInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
send(messageInputDomain.command.InputCommand(e.target.value))
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const MessageInput: FC<MessageInputProps> = ({ value = '', className, maxLength = 500, onInput, onEnter, preview }) => {
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
send(messageInputDomain.command.EnterCommand())
|
||||
onEnter?.(value)
|
||||
}
|
||||
}
|
||||
const handleInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onInput?.(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
{isPreview ? (
|
||||
<Markdown className="max-h-32 rounded-lg border border-input bg-gray-50 2xl:max-h-40">{message}</Markdown>
|
||||
{preview ? (
|
||||
<Markdown className="max-h-32 rounded-lg border border-input bg-gray-50 2xl:max-h-40">{value}</Markdown>
|
||||
) : (
|
||||
// Hack: Auto-Growing Textarea
|
||||
<div
|
||||
data-value={message}
|
||||
data-value={value}
|
||||
className="grid after:pointer-events-none after:invisible after:col-start-1 after:col-end-2 after:row-start-1 after:row-end-2 after:box-border after:max-h-28 after:w-full after:overflow-x-hidden after:whitespace-pre-wrap after:break-words after:rounded-lg after:border after:px-3 after:py-2 after:pb-5 after:text-sm after:content-[attr(data-value)] after:2xl:max-h-40"
|
||||
>
|
||||
<Textarea
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength={MESSAGE_MAX_LENGTH}
|
||||
maxLength={maxLength}
|
||||
className="col-start-1 col-end-2 row-start-1 row-end-2 box-border max-h-28 resize-none overflow-x-hidden break-words rounded-lg bg-gray-50 pb-5 text-sm 2xl:max-h-40"
|
||||
rows={2}
|
||||
value={message}
|
||||
value={value}
|
||||
placeholder="Type your message here."
|
||||
onInput={handleInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400">
|
||||
{message?.length ?? 0}/{MESSAGE_MAX_LENGTH}
|
||||
{value?.length ?? 0}/{maxLength}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,22 +4,13 @@ import { FrownIcon, ThumbsUpIcon } from 'lucide-react'
|
|||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
|
||||
|
||||
import LikeButton from '@/components/LikeButton'
|
||||
import { type Message } from '@/domain/MessageList'
|
||||
|
||||
export interface MessageProps {
|
||||
data: {
|
||||
id: string
|
||||
body: string
|
||||
username: string
|
||||
avatar: string
|
||||
date: number
|
||||
likeChecked: boolean
|
||||
hateChecked: boolean
|
||||
likeCount: number
|
||||
hateCount: number
|
||||
}
|
||||
export interface MessageItemProps {
|
||||
data: Message
|
||||
}
|
||||
|
||||
const Message: FC<MessageProps> = ({ data }) => {
|
||||
const MessageItem: FC<MessageItemProps> = ({ data }) => {
|
||||
const [formatData, setFormatData] = useState({
|
||||
...data,
|
||||
date: format(data.date, 'yyyy/MM/dd HH:mm:ss')
|
||||
|
@ -76,5 +67,5 @@ const Message: FC<MessageProps> = ({ data }) => {
|
|||
)
|
||||
}
|
||||
|
||||
Message.displayName = 'Message'
|
||||
export default Message
|
||||
MessageItem.displayName = 'MessageItem'
|
||||
export default MessageItem
|
12
src/components/MessageList.tsx
Normal file
12
src/components/MessageList.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { type ReactElement, type FC } from 'react'
|
||||
|
||||
import { type MessageItemProps } from './MessageItem'
|
||||
|
||||
export interface MessageListProps {
|
||||
children?: Array<ReactElement<MessageItemProps>>
|
||||
}
|
||||
const MessageList: FC<MessageListProps> = ({ children }) => {
|
||||
return <div className="grid content-start overflow-y-auto p-4">{children}</div>
|
||||
}
|
||||
|
||||
export default MessageList
|
|
@ -4,10 +4,12 @@ import InputModule from './modules/Input'
|
|||
const MessageInputDomain = Remesh.domain({
|
||||
name: 'MessageInputDomain',
|
||||
impl: (domain) => {
|
||||
const inputModule = InputModule(domain, {
|
||||
name: 'MessageInput'
|
||||
const MessageInputModule = InputModule(domain, {
|
||||
name: 'MessageInputModule'
|
||||
})
|
||||
|
||||
const MessageQuery = MessageInputModule.query.ValueQuery
|
||||
|
||||
const PreviewState = domain.state({
|
||||
name: 'MessageInput.PreviewState',
|
||||
default: false
|
||||
|
@ -28,7 +30,10 @@ const MessageInputDomain = Remesh.domain({
|
|||
})
|
||||
|
||||
const EnterEvent = domain.event({
|
||||
name: 'MessageInput.EnterEvent'
|
||||
name: 'MessageInput.EnterEvent',
|
||||
impl: ({ get }) => {
|
||||
return get(MessageInputModule.query.ValueQuery())
|
||||
}
|
||||
})
|
||||
|
||||
const EnterCommand = domain.command({
|
||||
|
@ -41,23 +46,23 @@ const MessageInputDomain = Remesh.domain({
|
|||
const ClearCommand = domain.command({
|
||||
name: 'MessageInput.ClearCommand',
|
||||
impl: () => {
|
||||
return inputModule.command.InputCommand('')
|
||||
return MessageInputModule.command.InputCommand('')
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
query: {
|
||||
...inputModule.query,
|
||||
MessageQuery,
|
||||
PreviewQuery
|
||||
},
|
||||
command: {
|
||||
...inputModule.command,
|
||||
...MessageInputModule.command,
|
||||
EnterCommand,
|
||||
ClearCommand,
|
||||
PreviewCommand
|
||||
},
|
||||
event: {
|
||||
...inputModule.event,
|
||||
...MessageInputModule.event,
|
||||
EnterEvent
|
||||
}
|
||||
}
|
||||
|
|
102
src/domain/MessageList.ts
Normal file
102
src/domain/MessageList.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import { ListModule } from 'remesh/modules/list'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
body: string
|
||||
username: string
|
||||
avatar: string
|
||||
date: number
|
||||
likeChecked: boolean
|
||||
hateChecked: boolean
|
||||
likeCount: number
|
||||
hateCount: number
|
||||
}
|
||||
|
||||
const MessageListDomain = Remesh.domain({
|
||||
name: 'MessageListDomain',
|
||||
impl: (domain) => {
|
||||
const MessageListModule = ListModule<Message>(domain, {
|
||||
name: 'MessageListModule',
|
||||
key: (message) => message.id
|
||||
})
|
||||
|
||||
const ListQuery = MessageListModule.query.ItemListQuery
|
||||
|
||||
const ItemQuery = MessageListModule.query.ItemQuery
|
||||
|
||||
const ChangeEvent = domain.event({
|
||||
name: 'MessageList.ChangeEvent',
|
||||
impl: ({ get }) => {
|
||||
return get(ListQuery())
|
||||
}
|
||||
})
|
||||
|
||||
const CreateEvent = domain.event({
|
||||
name: 'MessageList.CreateEvent'
|
||||
})
|
||||
|
||||
const CreateCommand = domain.command({
|
||||
name: 'MessageList.CreateCommand',
|
||||
impl: (_, message: Omit<Message, 'id'>) => {
|
||||
const id = nanoid()
|
||||
return [MessageListModule.command.AddItemCommand({ ...message, id }), CreateEvent(), ChangeEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
const UpdateEvent = domain.event({
|
||||
name: 'MessageList.UpdateEvent'
|
||||
})
|
||||
|
||||
const UpdateCommand = domain.command({
|
||||
name: 'MessageList.UpdateCommand',
|
||||
impl: (_, message: Message) => {
|
||||
return [MessageListModule.command.UpdateItemCommand(message), UpdateEvent(), ChangeEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
const DeleteEvent = domain.event({
|
||||
name: 'MessageList.DeleteEvent'
|
||||
})
|
||||
|
||||
const DeleteCommand = domain.command({
|
||||
name: 'MessageList.DeleteCommand',
|
||||
impl: (_, id: string) => {
|
||||
return [MessageListModule.command.DeleteItemCommand(id), DeleteEvent(), ChangeEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
const ClearEvent = domain.event({
|
||||
name: 'MessageList.ClearEvent'
|
||||
})
|
||||
|
||||
const ClearCommand = domain.command({
|
||||
name: 'MessageList.ClearCommand',
|
||||
impl: () => {
|
||||
return [MessageListModule.command.SetListCommand([]), ClearEvent(), ChangeEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
query: {
|
||||
ItemQuery,
|
||||
ListQuery
|
||||
},
|
||||
command: {
|
||||
CreateCommand,
|
||||
UpdateCommand,
|
||||
DeleteCommand,
|
||||
ClearCommand
|
||||
},
|
||||
event: {
|
||||
CreateEvent,
|
||||
UpdateEvent,
|
||||
DeleteEvent,
|
||||
ClearEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default MessageListDomain
|
|
@ -1,7 +1,7 @@
|
|||
import { Remesh, type Capitalize, type RemeshDomainContext } from 'remesh'
|
||||
import { Remesh, type DomainConceptName, type RemeshDomainContext } from 'remesh'
|
||||
|
||||
export interface InputModuleOptions {
|
||||
name: Capitalize
|
||||
name: DomainConceptName<'InputModule'>
|
||||
value?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
@ -19,27 +19,31 @@ const InputModule = (domain: RemeshDomainContext, options: InputModuleOptions) =
|
|||
}
|
||||
})
|
||||
|
||||
const InputEvent = domain.event<string>({
|
||||
name: `${options.name}.InputEvent`
|
||||
const InputEvent = domain.event({
|
||||
name: `${options.name}.InputEvent`,
|
||||
impl: ({ get }) => {
|
||||
return get(ValueState())
|
||||
}
|
||||
})
|
||||
|
||||
const InputCommand = domain.command({
|
||||
name: `${options.name}.InputCommand`,
|
||||
impl: (_, value: string) => {
|
||||
InputEvent(value)
|
||||
return ValueState().new(value)
|
||||
return [ValueState().new(value), InputEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
const ChangeEvent = domain.event<string>({
|
||||
name: `${options.name}.ChangeEvent`
|
||||
const ChangeEvent = domain.event({
|
||||
name: `${options.name}.ChangeEvent`,
|
||||
impl: ({ get }) => {
|
||||
return get(ValueState())
|
||||
}
|
||||
})
|
||||
|
||||
const ChangeCommand = domain.command({
|
||||
name: `${options.name}.ChangeCommand`,
|
||||
impl: (_, value: string) => {
|
||||
ChangeEvent(value)
|
||||
return ValueState().new(value)
|
||||
return [ValueState().new(value), ChangeEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -55,27 +59,25 @@ const InputModule = (domain: RemeshDomainContext, options: InputModuleOptions) =
|
|||
}
|
||||
})
|
||||
|
||||
const FocusEvent = domain.event<boolean>({
|
||||
const FocusEvent = domain.event({
|
||||
name: `${options.name}.FocusEvent`
|
||||
})
|
||||
|
||||
const BlurEvent = domain.event<boolean>({
|
||||
const BlurEvent = domain.event({
|
||||
name: `${options.name}.BlurEvent`
|
||||
})
|
||||
|
||||
const BlurCommand = domain.command({
|
||||
name: `${options.name}.BlurCommand`,
|
||||
impl: () => {
|
||||
BlurEvent(false)
|
||||
return FocusState().new(false)
|
||||
return [FocusState().new(false), BlurEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
const FocusCommand = domain.command({
|
||||
name: `${options.name}.FocusCommand`,
|
||||
impl: () => {
|
||||
FocusEvent(true)
|
||||
return FocusState().new(true)
|
||||
return [FocusState().new(true), FocusEvent()]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMedia } from 'react-use'
|
||||
import { BREAKPOINTS } from '@/constants'
|
||||
|
||||
export function useBreakpoint() {
|
||||
const useBreakpoint = () => {
|
||||
const isSM = useMedia(`(min-width: ${BREAKPOINTS.sm})`)
|
||||
const isMD = useMedia(`(min-width: ${BREAKPOINTS.md})`)
|
||||
const isLG = useMedia(`(min-width: ${BREAKPOINTS.lg})`)
|
||||
|
@ -15,3 +15,5 @@ export function useBreakpoint() {
|
|||
is2XL
|
||||
}
|
||||
}
|
||||
|
||||
export default useBreakpoint
|
||||
|
|
|
@ -1,22 +1,42 @@
|
|||
import { type FC } from 'react'
|
||||
import { SmileIcon, CornerDownLeftIcon, ImageIcon } from 'lucide-react'
|
||||
import { useRemeshDomain, useRemeshEvent, useRemeshSend } from 'remesh-react'
|
||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import MessageInput from '@/components/MessageInput'
|
||||
import MessageInputDomain from '@/domain/MessageInput'
|
||||
import MessageListDomain from '@/domain/MessageList'
|
||||
|
||||
const Footer: FC = () => {
|
||||
const send = useRemeshSend()
|
||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
||||
|
||||
const message = useRemeshQuery(messageInputDomain.query.MessageQuery())
|
||||
const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery())
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
send(messageInputDomain.command.InputCommand(value))
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
send(
|
||||
messageListDomain.command.CreateCommand({
|
||||
username: '墨绿青苔',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/10251037?v=4',
|
||||
body: message,
|
||||
date: Date.now(),
|
||||
likeChecked: false,
|
||||
likeCount: 0,
|
||||
hateChecked: false,
|
||||
hateCount: 0
|
||||
})
|
||||
)
|
||||
send(messageInputDomain.command.ClearCommand())
|
||||
}
|
||||
useRemeshEvent(messageInputDomain.event.EnterEvent, handleSend)
|
||||
|
||||
return (
|
||||
<div className="grid gap-y-2 p-4">
|
||||
<MessageInput></MessageInput>
|
||||
<MessageInput value={message} preview={isPreview} onEnter={handleSend} onInput={handleInput}></MessageInput>
|
||||
<div className="grid grid-cols-[auto_auto_1fr] items-center justify-items-end">
|
||||
<Button variant="ghost" size="icon">
|
||||
<SmileIcon size={20} />
|
||||
|
|
|
@ -1,48 +1,19 @@
|
|||
import { type FC } from 'react'
|
||||
import Message from '@/components/Message'
|
||||
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
||||
import MessageList from '@/components/MessageList'
|
||||
import MessageItem from '@/components/MessageItem'
|
||||
import MessageListDomain from '@/domain/MessageList'
|
||||
|
||||
const Main: FC = () => {
|
||||
const messages = [
|
||||
{
|
||||
id: '1',
|
||||
body: 'Who are you?',
|
||||
username: 'molvqingtai',
|
||||
avatar: 'https://github.com/shadcn.png',
|
||||
date: Date.now(),
|
||||
likeChecked: false,
|
||||
hateChecked: false,
|
||||
likeCount: 0,
|
||||
hateCount: 0
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
body: `I'm Chinese`,
|
||||
username: 'Love XJP',
|
||||
avatar: 'https://github.com/shadcn.png',
|
||||
date: Date.now(),
|
||||
likeChecked: false,
|
||||
hateChecked: false,
|
||||
likeCount: 0,
|
||||
hateCount: 0
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
body: 'Do you like XJP?',
|
||||
username: 'molvqingtai',
|
||||
avatar: 'https://github.com/shadcn.png',
|
||||
date: Date.now(),
|
||||
likeChecked: false,
|
||||
hateChecked: true,
|
||||
likeCount: 9999,
|
||||
hateCount: 2
|
||||
}
|
||||
]
|
||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||
const messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
||||
|
||||
return (
|
||||
<div className="grid content-start overflow-y-auto p-4">
|
||||
{messages.map((message) => (
|
||||
<Message key={message.id} data={message} />
|
||||
<MessageList>
|
||||
{messageList.map((message) => (
|
||||
<MessageItem key={message.id} data={message}></MessageItem>
|
||||
))}
|
||||
</div>
|
||||
</MessageList>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue