chore(input): use tiptap editer
This commit is contained in:
parent
1ca18504c4
commit
4394cc781a
11 changed files with 736 additions and 947 deletions
|
@ -91,18 +91,21 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.0.4",
|
"@radix-ui/react-scroll-area": "^1.0.4",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@tiptap/extension-character-count": "^2.1.8",
|
||||||
|
"@tiptap/extension-highlight": "^2.1.7",
|
||||||
|
"@tiptap/extension-typography": "^2.1.7",
|
||||||
|
"@tiptap/pm": "^2.1.7",
|
||||||
|
"@tiptap/react": "^2.1.7",
|
||||||
|
"@tiptap/starter-kit": "^2.1.7",
|
||||||
"class-variance-authority": "^0.6.1",
|
"class-variance-authority": "^0.6.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-react": "^0.263.0",
|
"lucide-react": "^0.263.0",
|
||||||
"mem": "^9.0.2",
|
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"peerjs": "^1.4.7",
|
"peerjs": "^1.4.7",
|
||||||
"react-markdown": "^8.0.7",
|
|
||||||
"react-nice-avatar": "^1.4.1",
|
"react-nice-avatar": "^1.4.1",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"remark-gfm": "^3.0.1",
|
|
||||||
"remesh": "^4.2.0",
|
"remesh": "^4.2.0",
|
||||||
"remesh-logger": "^4.1.0",
|
"remesh-logger": "^4.1.0",
|
||||||
"remesh-react": "^4.1.0",
|
"remesh-react": "^4.1.0",
|
||||||
|
|
1232
pnpm-lock.yaml
1232
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -23,14 +23,14 @@ const LikeButton: FC<LikeButtonProps> & { Icon: FC<LikeButtonIconProps> } = ({
|
||||||
onChange,
|
onChange,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
onClick?.(e)
|
onClick?.(e)
|
||||||
onChange?.(!checked, checked ? count - 1 : count + 1)
|
onChange?.(!checked, checked ? count - 1 : count + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOnClick}
|
onClick={handleClick}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid items-center overflow-hidden rounded-full leading-none transition-all',
|
'grid items-center overflow-hidden rounded-full leading-none transition-all',
|
||||||
|
|
|
@ -1,52 +1,65 @@
|
||||||
import { type FC, type ChangeEvent, type KeyboardEvent } from 'react'
|
import { type FC } from 'react'
|
||||||
|
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
import { Markdown } from '@/components/ui/Markdown'
|
import Typography from '@tiptap/extension-typography'
|
||||||
|
import { EditorContent, Extension, useEditor } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import CharacterCount from '@tiptap/extension-character-count'
|
||||||
import { cn } from '@/utils'
|
import { cn } from '@/utils'
|
||||||
|
|
||||||
export interface MessageInputProps {
|
export interface MessageInputProps {
|
||||||
value?: string
|
value?: string
|
||||||
className?: string
|
className?: string
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
preview?: boolean
|
enterClear?: boolean
|
||||||
onInput?: (value: string) => void
|
onInput?: (value: string) => void
|
||||||
onEnter?: (value: string) => void
|
onEnter?: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageInput: FC<MessageInputProps> = ({ value = '', className, maxLength = 500, onInput, onEnter, preview }) => {
|
const MessageInput: FC<MessageInputProps> = ({
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
value = '',
|
||||||
if (e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) {
|
className,
|
||||||
e.preventDefault()
|
maxLength = 500,
|
||||||
onEnter?.(value)
|
enterClear = false,
|
||||||
|
onInput,
|
||||||
|
onEnter
|
||||||
|
}) => {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
Highlight,
|
||||||
|
Typography,
|
||||||
|
CharacterCount.configure({
|
||||||
|
limit: maxLength
|
||||||
|
}),
|
||||||
|
Extension.create({
|
||||||
|
addKeyboardShortcuts: () => ({
|
||||||
|
Enter: ({ editor }) => {
|
||||||
|
onEnter?.(editor.getHTML())
|
||||||
|
enterClear && editor.commands.clearContent()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: cn(
|
||||||
|
'prose prose-sm prose-slate box-border text-sm break-words max-h-28 overflow-y-auto overflow-x-hidden min-h-[60px] w-full rounded-lg border border-input bg-gray-50 px-3 py-2 pb-5 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 2xl:max-h-4'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate({ editor }) {
|
||||||
|
onInput?.(editor.getHTML())
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
const handleInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
onInput?.(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
{preview ? (
|
<EditorContent editor={editor} />
|
||||||
<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={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={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={value}
|
|
||||||
placeholder="Type your message here."
|
|
||||||
onInput={handleInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400">
|
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400">
|
||||||
{value?.length ?? 0}/{maxLength}
|
{editor?.storage.characterCount.characters()}/{maxLength}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
|
||||||
|
|
||||||
import LikeButton from '@/components/LikeButton'
|
import LikeButton from '@/components/LikeButton'
|
||||||
import { type Message } from '@/types'
|
import { type Message } from '@/types'
|
||||||
import { Markdown } from '@/components/ui/Markdown'
|
|
||||||
|
|
||||||
export interface MessageItemProps {
|
export interface MessageItemProps {
|
||||||
data: Message
|
data: Message
|
||||||
|
@ -39,9 +38,7 @@ const MessageItem: FC<MessageItemProps> = ({ data }) => {
|
||||||
<div className="text-xs text-slate-400">{formatData.date}</div>
|
<div className="text-xs text-slate-400">{formatData.date}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="pb-2">
|
<div className="prose prose-sm prose-slate pb-2" dangerouslySetInnerHTML={{ __html: formatData.body }}></div>
|
||||||
<Markdown>{formatData.body}</Markdown>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-flow-col justify-end gap-x-2 leading-none">
|
<div className="grid grid-flow-col justify-end gap-x-2 leading-none">
|
||||||
<LikeButton
|
<LikeButton
|
||||||
checked={formatData.likeChecked}
|
checked={formatData.likeChecked}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { type FC } from 'react'
|
|
||||||
import ReactMarkdown from 'react-markdown'
|
|
||||||
import remarkGfm from 'remark-gfm'
|
|
||||||
import { cn } from '@/utils'
|
|
||||||
|
|
||||||
export interface MarkdownProps {
|
|
||||||
children?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Markdown: FC<MarkdownProps> = ({ children = '', className }) => {
|
|
||||||
return (
|
|
||||||
<ReactMarkdown
|
|
||||||
components={{
|
|
||||||
h1: ({ className, ...props }) => <h1 className={cn('mb-2 mt-0 text-2xl', className)} {...props} />,
|
|
||||||
h2: ({ className, ...props }) => <h2 className={cn('mb-2 mt-0', className)} {...props} />,
|
|
||||||
img: ({ className, alt, ...props }) => (
|
|
||||||
<img className={cn('my-2 max-w-[50%] rounded-md border', className)} alt={alt} {...props} />
|
|
||||||
),
|
|
||||||
ul: ({ className, ...props }) => {
|
|
||||||
Reflect.deleteProperty(props, 'ordered')
|
|
||||||
return <ul className={cn('text-sm [&:not([depth="0"])]:my-0 ', className)} {...props} />
|
|
||||||
},
|
|
||||||
input: ({ className, ...props }) => <input className={cn('my-0', className)} {...props} />,
|
|
||||||
table: ({ className, ...props }) => (
|
|
||||||
<div className="my-4 w-full overflow-y-auto">
|
|
||||||
<table className={cn('my-0 w-full rounded-md', className)} {...props} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
tr: ({ className, ...props }) => {
|
|
||||||
// fix: spell it as lowercase `isheader` warning
|
|
||||||
Reflect.deleteProperty(props, 'isHeader')
|
|
||||||
return <tr className={cn('m-0 border-t p-0 even:bg-muted', className)} {...props} />
|
|
||||||
},
|
|
||||||
th: ({ className, ...props }) => {
|
|
||||||
// fix: spell it as lowercase `isheader` warning
|
|
||||||
Reflect.deleteProperty(props, 'isHeader')
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
className={cn(
|
|
||||||
'border px-3 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
td: ({ className, ...props }) => {
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
className={cn(
|
|
||||||
'border px-3 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
className={cn(className, 'prose prose-sm prose-slate')}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ReactMarkdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Markdown.displayName = 'Markdown'
|
|
||||||
|
|
||||||
export { Markdown }
|
|
|
@ -1,21 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/utils/index'
|
|
||||||
|
|
||||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
className={cn(
|
|
||||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Textarea.displayName = 'Textarea'
|
|
||||||
|
|
||||||
export { Textarea }
|
|
|
@ -12,39 +12,6 @@ const MessageInputDomain = Remesh.domain({
|
||||||
|
|
||||||
const MessageQuery = MessageInputModule.query.ValueQuery
|
const MessageQuery = MessageInputModule.query.ValueQuery
|
||||||
|
|
||||||
const PreviewState = domain.state({
|
|
||||||
name: 'MessageInput.PreviewState',
|
|
||||||
default: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const PreviewQuery = domain.query({
|
|
||||||
name: 'MessageInput.PreviewQuery',
|
|
||||||
impl: ({ get }) => {
|
|
||||||
return get(PreviewState())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const PreviewCommand = domain.command({
|
|
||||||
name: 'MessageInput.PreviewCommand',
|
|
||||||
impl: (_, value: boolean) => {
|
|
||||||
return PreviewState().new(value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const EnterEvent = domain.event({
|
|
||||||
name: 'MessageInput.EnterEvent',
|
|
||||||
impl: ({ get }) => {
|
|
||||||
return get(MessageInputModule.query.ValueQuery())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const EnterCommand = domain.command({
|
|
||||||
name: 'MessageInput.EnterCommand',
|
|
||||||
impl: () => {
|
|
||||||
return EnterEvent()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const ClearCommand = domain.command({
|
const ClearCommand = domain.command({
|
||||||
name: 'MessageInput.ClearCommand',
|
name: 'MessageInput.ClearCommand',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
|
@ -54,18 +21,14 @@ const MessageInputDomain = Remesh.domain({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
MessageQuery,
|
MessageQuery
|
||||||
PreviewQuery
|
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
...MessageInputModule.command,
|
...MessageInputModule.command,
|
||||||
EnterCommand,
|
ClearCommand
|
||||||
ClearCommand,
|
|
||||||
PreviewCommand
|
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
...MessageInputModule.event,
|
...MessageInputModule.event
|
||||||
EnterEvent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,129 +2,125 @@ import { Remesh } from 'remesh'
|
||||||
import { ListModule } from 'remesh/modules/list'
|
import { ListModule } from 'remesh/modules/list'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { from, map, tap, merge } from 'rxjs'
|
import { from, map, tap, merge } from 'rxjs'
|
||||||
import mem from 'mem'
|
|
||||||
import Storage from './externs/Storage'
|
import Storage from './externs/Storage'
|
||||||
|
|
||||||
export interface Message {
|
import { type Message } from '@/types'
|
||||||
id: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageListDomain = <T extends Message>() =>
|
const MessageListDomain = Remesh.domain({
|
||||||
Remesh.domain({
|
name: 'MessageListDomain',
|
||||||
name: 'MessageListDomain',
|
impl: (domain) => {
|
||||||
impl: (domain) => {
|
const storage = domain.getExtern(Storage)
|
||||||
const storage = domain.getExtern(Storage)
|
const storageKey = `${storage.name}.MESSAGE_LIST`
|
||||||
const storageKey = `${storage.name}.MESSAGE_LIST`
|
|
||||||
|
|
||||||
const MessageListModule = ListModule<T>(domain, {
|
const MessageListModule = ListModule<Message>(domain, {
|
||||||
name: 'MessageListModule',
|
name: 'MessageListModule',
|
||||||
key: (message) => message.id
|
key: (message) => message.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const ListQuery = MessageListModule.query.ItemListQuery
|
const ListQuery = MessageListModule.query.ItemListQuery
|
||||||
|
|
||||||
const ItemQuery = MessageListModule.query.ItemQuery
|
const ItemQuery = MessageListModule.query.ItemQuery
|
||||||
|
|
||||||
const ChangeListEvent = domain.event({
|
const ChangeListEvent = domain.event({
|
||||||
name: 'MessageList.ChangeListEvent',
|
name: 'MessageList.ChangeListEvent',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
return get(ListQuery())
|
return get(ListQuery())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const CreateItemEvent = domain.event<T>({
|
const CreateItemEvent = domain.event<Message>({
|
||||||
name: 'MessageList.CreateItemEvent'
|
name: 'MessageList.CreateItemEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const CreateItemCommand = domain.command({
|
const CreateItemCommand = domain.command({
|
||||||
name: 'MessageList.CreateItemCommand',
|
name: 'MessageList.CreateItemCommand',
|
||||||
impl: (_, message: Omit<T, 'id'>) => {
|
impl: (_, message: Omit<Message, 'id'>) => {
|
||||||
const newMessage = { ...message, id: nanoid() } as T
|
const newMessage = { ...message, id: nanoid() }
|
||||||
return [MessageListModule.command.AddItemCommand(newMessage), CreateItemEvent(newMessage), ChangeListEvent()]
|
return [MessageListModule.command.AddItemCommand(newMessage), CreateItemEvent(newMessage), ChangeListEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const UpdateItemEvent = domain.event<T>({
|
const UpdateItemEvent = domain.event<Message>({
|
||||||
name: 'MessageList.UpdateItemEvent'
|
name: 'MessageList.UpdateItemEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const UpdateItemCommand = domain.command({
|
const UpdateItemCommand = domain.command({
|
||||||
name: 'MessageList.UpdateItemCommand',
|
name: 'MessageList.UpdateItemCommand',
|
||||||
impl: (_, message: T) => {
|
impl: (_, message: Message) => {
|
||||||
return [MessageListModule.command.UpdateItemCommand(message), UpdateItemEvent(message), ChangeListEvent()]
|
return [MessageListModule.command.UpdateItemCommand(message), UpdateItemEvent(message), ChangeListEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const DeleteItemEvent = domain.event<string>({
|
const DeleteItemEvent = domain.event<string>({
|
||||||
name: 'MessageList.DeleteItemEvent'
|
name: 'MessageList.DeleteItemEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const DeleteItemCommand = domain.command({
|
const DeleteItemCommand = domain.command({
|
||||||
name: 'MessageList.DeleteItemCommand',
|
name: 'MessageList.DeleteItemCommand',
|
||||||
impl: (_, id: string) => {
|
impl: (_, id: string) => {
|
||||||
return [MessageListModule.command.DeleteItemCommand(id), DeleteItemEvent(id), ChangeListEvent()]
|
return [MessageListModule.command.DeleteItemCommand(id), DeleteItemEvent(id), ChangeListEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const ClearListEvent = domain.event({
|
const ClearListEvent = domain.event({
|
||||||
name: 'MessageList.ClearListEvent'
|
name: 'MessageList.ClearListEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const ClearListCommand = domain.command({
|
const ClearListCommand = domain.command({
|
||||||
name: 'MessageList.ClearListCommand',
|
name: 'MessageList.ClearListCommand',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
return [MessageListModule.command.DeleteAllCommand(), ClearListEvent(), ChangeListEvent()]
|
return [MessageListModule.command.DeleteAllCommand(), ClearListEvent(), ChangeListEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const InitListEvent = domain.event<T[]>({
|
const InitListEvent = domain.event<Message[]>({
|
||||||
name: 'MessageList.InitListEvent'
|
name: 'MessageList.InitListEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const InitListCommand = domain.command({
|
const InitListCommand = domain.command({
|
||||||
name: 'MessageList.InitListCommand',
|
name: 'MessageList.InitListCommand',
|
||||||
impl: (_, messages: T[]) => {
|
impl: (_, messages: Message[]) => {
|
||||||
return [MessageListModule.command.SetListCommand(messages), InitListEvent(messages)]
|
return [MessageListModule.command.SetListCommand(messages), InitListEvent(messages)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'FormStorageToStateEffect',
|
name: 'FormStorageToStateEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
return from(storage.get<T[]>(storageKey)).pipe(map((messages) => InitListCommand(messages ?? [])))
|
return from(storage.get<Message[]>(storageKey)).pipe(map((messages) => InitListCommand(messages ?? [])))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'FormStateToStorageEffect',
|
name: 'FormStateToStorageEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const createItem$ = fromEvent(ChangeListEvent).pipe(
|
const createItem$ = fromEvent(ChangeListEvent).pipe(
|
||||||
tap(async (messages) => await storage.set<T[]>(storageKey, messages))
|
tap(async (messages) => await storage.set<Message[]>(storageKey, messages))
|
||||||
)
|
)
|
||||||
return merge(createItem$).pipe(map(() => null))
|
return merge(createItem$).pipe(map(() => null))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
ItemQuery,
|
ItemQuery,
|
||||||
ListQuery
|
ListQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
CreateItemCommand,
|
CreateItemCommand,
|
||||||
UpdateItemCommand,
|
UpdateItemCommand,
|
||||||
DeleteItemCommand,
|
DeleteItemCommand,
|
||||||
ClearListCommand
|
ClearListCommand
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
CreateItemEvent,
|
CreateItemEvent,
|
||||||
UpdateItemEvent,
|
UpdateItemEvent,
|
||||||
DeleteItemEvent,
|
DeleteItemEvent,
|
||||||
ClearListEvent
|
ClearListEvent
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default mem(MessageListDomain)
|
export default MessageListDomain
|
||||||
|
|
|
@ -6,18 +6,12 @@ import MessageInput from '@/components/MessageInput'
|
||||||
import MessageInputDomain from '@/domain/MessageInput'
|
import MessageInputDomain from '@/domain/MessageInput'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
import { MESSAGE_MAX_LENGTH } from '@/constants'
|
import { MESSAGE_MAX_LENGTH } from '@/constants'
|
||||||
import { type Message } from '@/types'
|
|
||||||
|
|
||||||
const Footer: FC = () => {
|
const Footer: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain<Message>())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
||||||
const text = useRemeshQuery(messageInputDomain.query.MessageQuery())
|
const text = useRemeshQuery(messageInputDomain.query.MessageQuery())
|
||||||
const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery())
|
|
||||||
|
|
||||||
const handleInput = (value: string) => {
|
|
||||||
send(messageInputDomain.command.InputCommand(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
username: '墨绿青苔',
|
username: '墨绿青苔',
|
||||||
|
@ -30,16 +24,19 @@ const Footer: FC = () => {
|
||||||
hateCount: 0
|
hateCount: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleInput = (value: string) => {
|
||||||
|
send(messageInputDomain.command.InputCommand(value))
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
send(messageListDomain.command.CreateItemCommand(message))
|
send(messageListDomain.command.CreateItemCommand(message))
|
||||||
send(messageInputDomain.command.ClearCommand())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-y-2 px-4 pb-4">
|
<div className="grid gap-y-2 px-4 pb-4">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
value={text}
|
value={text}
|
||||||
preview={isPreview}
|
enterClear={true}
|
||||||
onEnter={handleSend}
|
onEnter={handleSend}
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
maxLength={MESSAGE_MAX_LENGTH}
|
maxLength={MESSAGE_MAX_LENGTH}
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
||||||
import MessageList from '@/components/MessageList'
|
import MessageList from '@/components/MessageList'
|
||||||
import MessageItem from '@/components/MessageItem'
|
import MessageItem from '@/components/MessageItem'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
import { type Message } from '@/types'
|
|
||||||
|
|
||||||
const Main: FC = () => {
|
const Main: FC = () => {
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain<Message>())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
const messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in a new issue