chore: complete message send

This commit is contained in:
molvqingtai 2023-08-03 04:28:15 +08:00
parent 50c7b92e36
commit a9c055b467
11 changed files with 214 additions and 104 deletions

View file

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

View file

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

View file

@ -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>
)

View file

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

View 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

View file

@ -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
View 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

View file

@ -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()]
}
})

View file

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

View file

@ -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} />

View file

@ -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>
)
}