feat: store message records

This commit is contained in:
molvqingtai 2023-08-07 00:24:00 +08:00
parent 1212f3811f
commit c029423bf9
14 changed files with 203 additions and 113 deletions

View file

@ -40,6 +40,7 @@
"@typescript-eslint/strict-boolean-expressions": "off", "@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-misused-promises": "off" "@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/consistent-type-assertions": "off"
} }
} }

View file

@ -94,7 +94,9 @@
"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",
"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-markdown": "^8.0.7",

View file

@ -35,9 +35,15 @@ dependencies:
date-fns: date-fns:
specifier: ^2.30.0 specifier: ^2.30.0
version: 2.30.0 version: 2.30.0
idb-keyval:
specifier: ^6.2.1
version: 6.2.1
lucide-react: lucide-react:
specifier: ^0.263.0 specifier: ^0.263.0
version: 0.263.0(react@18.2.0) version: 0.263.0(react@18.2.0)
mem:
specifier: ^9.0.2
version: 9.0.2
nanoid: nanoid:
specifier: ^4.0.2 specifier: ^4.0.2
version: 4.0.2 version: 4.0.2
@ -4294,6 +4300,10 @@ packages:
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
dev: false dev: false
/idb-keyval@6.2.1:
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
dev: false
/ieee754@1.2.1: /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true dev: true
@ -5091,7 +5101,6 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dependencies: dependencies:
p-defer: 1.0.0 p-defer: 1.0.0
dev: true
/map-obj@1.0.1: /map-obj@1.0.1:
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
@ -5255,6 +5264,14 @@ packages:
p-is-promise: 2.1.0 p-is-promise: 2.1.0
dev: true dev: true
/mem@9.0.2:
resolution: {integrity: sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A==}
engines: {node: '>=12.20'}
dependencies:
map-age-cleaner: 0.1.3
mimic-fn: 4.0.0
dev: false
/memorystream@0.3.1: /memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@ -5559,7 +5576,6 @@ packages:
/mimic-fn@4.0.0: /mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true
/mimic-response@3.1.0: /mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
@ -5994,7 +6010,6 @@ packages:
/p-defer@1.0.0: /p-defer@1.0.0:
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true
/p-is-promise@2.1.0: /p-is-promise@2.1.0:
resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==}

View file

@ -4,7 +4,7 @@ import { FrownIcon, ThumbsUpIcon } from 'lucide-react'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
import LikeButton from '@/components/LikeButton' import LikeButton from '@/components/LikeButton'
import { type Message } from '@/domain/MessageList' import { type Message } from '@/types'
import { Markdown } from '@/components/ui/Markdown' import { Markdown } from '@/components/ui/Markdown'
export interface MessageItemProps { export interface MessageItemProps {

View file

@ -17,3 +17,5 @@ export const BREAKPOINTS = {
} as const } as const
export const MESSAGE_MAX_LENGTH = 500 as const export const MESSAGE_MAX_LENGTH = 500 as const
export const STORAGE_NAME = 'WEB_CHAT' as const

View file

@ -1,6 +1,8 @@
import { Remesh } from 'remesh' import { Remesh } from 'remesh'
import InputModule from './modules/Input' import InputModule from './modules/Input'
export const MESSAGE_INPUT_STORAGE_KEY = 'MESSAGE_INPUT'
const MessageInputDomain = Remesh.domain({ const MessageInputDomain = Remesh.domain({
name: 'MessageInputDomain', name: 'MessageInputDomain',
impl: (domain) => { impl: (domain) => {

View file

@ -1,23 +1,23 @@
import { Remesh } from 'remesh' 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 mem from 'mem'
import Storage from './externs/Storage'
export interface Message { export interface Message {
id: string id: string
body: string [key: string]: any
username: string
avatar: string
date: number
likeChecked: boolean
hateChecked: boolean
likeCount: number
hateCount: number
} }
const MessageListDomain = Remesh.domain({ const MessageListDomain = <T extends Message>() =>
Remesh.domain({
name: 'MessageListDomain', name: 'MessageListDomain',
impl: (domain) => { impl: (domain) => {
const MessageListModule = ListModule<Message>(domain, { const storage = domain.getExtern(Storage)
const storageKey = `${storage.name}.MESSAGE_LIST`
const MessageListModule = ListModule<T>(domain, {
name: 'MessageListModule', name: 'MessageListModule',
key: (message) => message.id key: (message) => message.id
}) })
@ -26,55 +26,83 @@ const MessageListDomain = Remesh.domain({
const ItemQuery = MessageListModule.query.ItemQuery const ItemQuery = MessageListModule.query.ItemQuery
const ChangeEvent = domain.event({ const ChangeListEvent = domain.event({
name: 'MessageList.ChangeEvent', name: 'MessageList.ChangeListEvent',
impl: ({ get }) => { impl: ({ get }) => {
return get(ListQuery()) return get(ListQuery())
} }
}) })
const CreateEvent = domain.event({ const CreateItemEvent = domain.event<T>({
name: 'MessageList.CreateEvent' name: 'MessageList.CreateItemEvent'
}) })
const CreateCommand = domain.command({ const CreateItemCommand = domain.command({
name: 'MessageList.CreateCommand', name: 'MessageList.CreateItemCommand',
impl: (_, message: Omit<Message, 'id'>) => { impl: (_, message: Omit<T, 'id'>) => {
const id = nanoid() const newMessage = { ...message, id: nanoid() } as T
return [MessageListModule.command.AddItemCommand({ ...message, id }), CreateEvent(), ChangeEvent()] return [MessageListModule.command.AddItemCommand(newMessage), CreateItemEvent(newMessage), ChangeListEvent()]
} }
}) })
const UpdateEvent = domain.event({ const UpdateItemEvent = domain.event<T>({
name: 'MessageList.UpdateEvent' name: 'MessageList.UpdateItemEvent'
}) })
const UpdateCommand = domain.command({ const UpdateItemCommand = domain.command({
name: 'MessageList.UpdateCommand', name: 'MessageList.UpdateItemCommand',
impl: (_, message: Message) => { impl: (_, message: T) => {
return [MessageListModule.command.UpdateItemCommand(message), UpdateEvent(), ChangeEvent()] return [MessageListModule.command.UpdateItemCommand(message), UpdateItemEvent(message), ChangeListEvent()]
} }
}) })
const DeleteEvent = domain.event({ const DeleteItemEvent = domain.event<string>({
name: 'MessageList.DeleteEvent' name: 'MessageList.DeleteItemEvent'
}) })
const DeleteCommand = domain.command({ const DeleteItemCommand = domain.command({
name: 'MessageList.DeleteCommand', name: 'MessageList.DeleteItemCommand',
impl: (_, id: string) => { impl: (_, id: string) => {
return [MessageListModule.command.DeleteItemCommand(id), DeleteEvent(), ChangeEvent()] return [MessageListModule.command.DeleteItemCommand(id), DeleteItemEvent(id), ChangeListEvent()]
} }
}) })
const ClearEvent = domain.event({ const ClearListEvent = domain.event({
name: 'MessageList.ClearEvent' name: 'MessageList.ClearListEvent'
}) })
const ClearCommand = domain.command({ const ClearListCommand = domain.command({
name: 'MessageList.ClearCommand', name: 'MessageList.ClearListCommand',
impl: () => { impl: () => {
return [MessageListModule.command.SetListCommand([]), ClearEvent(), ChangeEvent()] return [MessageListModule.command.DeleteAllCommand(), ClearListEvent(), ChangeListEvent()]
}
})
const InitListEvent = domain.event<T[]>({
name: 'MessageList.InitListEvent'
})
const InitListCommand = domain.command({
name: 'MessageList.InitListCommand',
impl: (_, messages: T[]) => {
return [MessageListModule.command.SetListCommand(messages), InitListEvent(messages)]
}
})
domain.effect({
name: 'FormStorageToStateEffect',
impl: () => {
return from(storage.get<T[]>(storageKey)).pipe(map((messages) => InitListCommand(messages ?? [])))
}
})
domain.effect({
name: 'FormStateToStorageEffect',
impl: ({ fromEvent }) => {
const createItem$ = fromEvent(ChangeListEvent).pipe(
tap(async (messages) => await storage.set<T[]>(storageKey, messages))
)
return merge(createItem$).pipe(map(() => null))
} }
}) })
@ -84,19 +112,19 @@ const MessageListDomain = Remesh.domain({
ListQuery ListQuery
}, },
command: { command: {
CreateCommand, CreateItemCommand,
UpdateCommand, UpdateItemCommand,
DeleteCommand, DeleteItemCommand,
ClearCommand ClearListCommand
}, },
event: { event: {
CreateEvent, CreateItemEvent,
UpdateEvent, UpdateItemEvent,
DeleteEvent, DeleteItemEvent,
ClearEvent ClearListEvent
} }
} }
} }
}) })()
export default MessageListDomain export default mem(MessageListDomain)

View file

@ -0,0 +1,21 @@
import { Remesh } from 'remesh'
export interface Storage {
name: string
get: <T>(key: string) => Promise<T | undefined>
set: <T>(key: string, value: T) => Promise<void>
}
const StorageExtern = Remesh.extern<Storage>({
default: {
name: 'STORAGE',
get: async () => {
throw new Error('"get" not implemented')
},
set: async () => {
throw new Error('"set" not implemented')
}
}
})
export default StorageExtern

View file

@ -19,31 +19,25 @@ const InputModule = (domain: RemeshDomainContext, options: InputModuleOptions) =
} }
}) })
const InputEvent = domain.event({ const InputEvent = domain.event<string>({
name: `${options.name}.InputEvent`, name: `${options.name}.InputEvent`
impl: ({ get }) => {
return get(ValueState())
}
}) })
const InputCommand = domain.command({ const InputCommand = domain.command({
name: `${options.name}.InputCommand`, name: `${options.name}.InputCommand`,
impl: (_, value: string) => { impl: (_, value: string) => {
return [ValueState().new(value), InputEvent()] return [ValueState().new(value), InputEvent(value)]
} }
}) })
const ChangeEvent = domain.event({ const ChangeEvent = domain.event<string>({
name: `${options.name}.ChangeEvent`, name: `${options.name}.ChangeEvent`
impl: ({ get }) => {
return get(ValueState())
}
}) })
const ChangeCommand = domain.command({ const ChangeCommand = domain.command({
name: `${options.name}.ChangeCommand`, name: `${options.name}.ChangeCommand`,
impl: (_, value: string) => { impl: (_, value: string) => {
return [ValueState().new(value), ChangeEvent()] return [ValueState().new(value), ChangeEvent(value)]
} }
}) })

11
src/impl/Storage.ts Normal file
View file

@ -0,0 +1,11 @@
import { get, set } from 'idb-keyval'
import StorageExtern from '@/domain/externs/Storage'
import { STORAGE_NAME } from '@/constants'
const StorageImpl = StorageExtern.impl({
name: STORAGE_NAME,
get,
set
})
export default StorageImpl

View file

@ -4,10 +4,12 @@ import { RemeshLogger } from 'remesh-logger'
import { Remesh } from 'remesh' import { Remesh } from 'remesh'
import App from './App' import App from './App'
import createShadowRoot from './createShadowRoot' import createShadowRoot from './createShadowRoot'
import StorageImpl from './impl/Storage'
import style from './index.css?inline' import style from './index.css?inline'
void (async () => { void (async () => {
const store = Remesh.store({ const store = Remesh.store({
externs: [StorageImpl],
inspectors: [RemeshLogger()] inspectors: [RemeshLogger()]
}) })

11
src/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
export interface Message {
id: string
body: string
username: string
avatar: string
date: number
likeChecked: boolean
hateChecked: boolean
likeCount: number
hateCount: number
}

View file

@ -6,39 +6,39 @@ 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()) const messageListDomain = useRemeshDomain(MessageListDomain<Message>())
const messageInputDomain = useRemeshDomain(MessageInputDomain()) const messageInputDomain = useRemeshDomain(MessageInputDomain())
const text = useRemeshQuery(messageInputDomain.query.MessageQuery())
const message = useRemeshQuery(messageInputDomain.query.MessageQuery())
const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery()) const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery())
const handleInput = (value: string) => { const handleInput = (value: string) => {
send(messageInputDomain.command.InputCommand(value)) send(messageInputDomain.command.InputCommand(value))
} }
const handleSend = () => { const message = {
send(
messageListDomain.command.CreateCommand({
username: '墨绿青苔', username: '墨绿青苔',
avatar: 'https://avatars.githubusercontent.com/u/10251037?v=4', avatar: 'https://avatars.githubusercontent.com/u/10251037?v=4',
body: message, body: text,
date: Date.now(), date: Date.now(),
likeChecked: false, likeChecked: false,
likeCount: 0, likeCount: 0,
hateChecked: false, hateChecked: false,
hateCount: 0 hateCount: 0
}) }
)
const handleSend = () => {
send(messageListDomain.command.CreateItemCommand(message))
send(messageInputDomain.command.ClearCommand()) send(messageInputDomain.command.ClearCommand())
} }
return ( return (
<div className="grid gap-y-2 p-4"> <div className="grid gap-y-2 p-4">
<MessageInput <MessageInput
value={message} value={text}
preview={isPreview} preview={isPreview}
onEnter={handleSend} onEnter={handleSend}
onInput={handleInput} onInput={handleInput}

View file

@ -3,9 +3,10 @@ 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()) const messageListDomain = useRemeshDomain(MessageListDomain<Message>())
const messageList = useRemeshQuery(messageListDomain.query.ListQuery()) const messageList = useRemeshQuery(messageListDomain.query.ListQuery())
return ( return (