From c029423bf9e553cd9000f547f6c7cd28da05896e Mon Sep 17 00:00:00 2001 From: molvqingtai Date: Mon, 7 Aug 2023 00:24:00 +0800 Subject: [PATCH] feat: store message records --- .eslintrc | 3 +- package.json | 2 + pnpm-lock.yaml | 21 +++- src/components/MessageItem.tsx | 2 +- src/constants/index.ts | 2 + src/domain/MessageInput.ts | 2 + src/domain/MessageList.ts | 186 +++++++++++++++++++-------------- src/domain/externs/Storage.ts | 21 ++++ src/domain/modules/Input.ts | 18 ++-- src/impl/Storage.ts | 11 ++ src/main.tsx | 2 + src/types/index.d.ts | 11 ++ src/views/Footer/index.tsx | 32 +++--- src/views/Main/index.tsx | 3 +- 14 files changed, 203 insertions(+), 113 deletions(-) create mode 100644 src/domain/externs/Storage.ts create mode 100644 src/impl/Storage.ts create mode 100644 src/types/index.d.ts diff --git a/.eslintrc b/.eslintrc index 95a26bc..33c234e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,6 +40,7 @@ "@typescript-eslint/strict-boolean-expressions": "off", "@typescript-eslint/no-floating-promises": "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" } } diff --git a/package.json b/package.json index e876ec5..acfabbc 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,9 @@ "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "date-fns": "^2.30.0", + "idb-keyval": "^6.2.1", "lucide-react": "^0.263.0", + "mem": "^9.0.2", "nanoid": "^4.0.2", "peerjs": "^1.4.7", "react-markdown": "^8.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ae463b..f629992 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,15 @@ dependencies: date-fns: specifier: ^2.30.0 version: 2.30.0 + idb-keyval: + specifier: ^6.2.1 + version: 6.2.1 lucide-react: specifier: ^0.263.0 version: 0.263.0(react@18.2.0) + mem: + specifier: ^9.0.2 + version: 9.0.2 nanoid: specifier: ^4.0.2 version: 4.0.2 @@ -4294,6 +4300,10 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false + /idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -5091,7 +5101,6 @@ packages: engines: {node: '>=6'} dependencies: p-defer: 1.0.0 - dev: true /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -5255,6 +5264,14 @@ packages: p-is-promise: 2.1.0 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: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -5559,7 +5576,6 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - dev: true /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -5994,7 +6010,6 @@ packages: /p-defer@1.0.0: resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} engines: {node: '>=4'} - dev: true /p-is-promise@2.1.0: resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} diff --git a/src/components/MessageItem.tsx b/src/components/MessageItem.tsx index 53edb4d..886b8ff 100644 --- a/src/components/MessageItem.tsx +++ b/src/components/MessageItem.tsx @@ -4,7 +4,7 @@ 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' +import { type Message } from '@/types' import { Markdown } from '@/components/ui/Markdown' export interface MessageItemProps { diff --git a/src/constants/index.ts b/src/constants/index.ts index 3bf7ffd..6a8096b 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -17,3 +17,5 @@ export const BREAKPOINTS = { } as const export const MESSAGE_MAX_LENGTH = 500 as const + +export const STORAGE_NAME = 'WEB_CHAT' as const diff --git a/src/domain/MessageInput.ts b/src/domain/MessageInput.ts index a533903..ba9477c 100644 --- a/src/domain/MessageInput.ts +++ b/src/domain/MessageInput.ts @@ -1,6 +1,8 @@ import { Remesh } from 'remesh' import InputModule from './modules/Input' +export const MESSAGE_INPUT_STORAGE_KEY = 'MESSAGE_INPUT' + const MessageInputDomain = Remesh.domain({ name: 'MessageInputDomain', impl: (domain) => { diff --git a/src/domain/MessageList.ts b/src/domain/MessageList.ts index 6f1dcc2..b7e6141 100644 --- a/src/domain/MessageList.ts +++ b/src/domain/MessageList.ts @@ -1,102 +1,130 @@ import { Remesh } from 'remesh' import { ListModule } from 'remesh/modules/list' import { nanoid } from 'nanoid' +import { from, map, tap, merge } from 'rxjs' +import mem from 'mem' +import Storage from './externs/Storage' export interface Message { id: string - body: string - username: string - avatar: string - date: number - likeChecked: boolean - hateChecked: boolean - likeCount: number - hateCount: number + [key: string]: any } -const MessageListDomain = Remesh.domain({ - name: 'MessageListDomain', - impl: (domain) => { - const MessageListModule = ListModule(domain, { - name: 'MessageListModule', - key: (message) => message.id - }) +const MessageListDomain = () => + Remesh.domain({ + name: 'MessageListDomain', + impl: (domain) => { + const storage = domain.getExtern(Storage) + const storageKey = `${storage.name}.MESSAGE_LIST` - const ListQuery = MessageListModule.query.ItemListQuery + const MessageListModule = ListModule(domain, { + name: 'MessageListModule', + key: (message) => message.id + }) - const ItemQuery = MessageListModule.query.ItemQuery + const ListQuery = MessageListModule.query.ItemListQuery - const ChangeEvent = domain.event({ - name: 'MessageList.ChangeEvent', - impl: ({ get }) => { - return get(ListQuery()) - } - }) + const ItemQuery = MessageListModule.query.ItemQuery - const CreateEvent = domain.event({ - name: 'MessageList.CreateEvent' - }) + const ChangeListEvent = domain.event({ + name: 'MessageList.ChangeListEvent', + impl: ({ get }) => { + return get(ListQuery()) + } + }) - const CreateCommand = domain.command({ - name: 'MessageList.CreateCommand', - impl: (_, message: Omit) => { - const id = nanoid() - return [MessageListModule.command.AddItemCommand({ ...message, id }), CreateEvent(), ChangeEvent()] - } - }) + const CreateItemEvent = domain.event({ + name: 'MessageList.CreateItemEvent' + }) - const UpdateEvent = domain.event({ - name: 'MessageList.UpdateEvent' - }) + const CreateItemCommand = domain.command({ + name: 'MessageList.CreateItemCommand', + impl: (_, message: Omit) => { + const newMessage = { ...message, id: nanoid() } as T + return [MessageListModule.command.AddItemCommand(newMessage), CreateItemEvent(newMessage), ChangeListEvent()] + } + }) - const UpdateCommand = domain.command({ - name: 'MessageList.UpdateCommand', - impl: (_, message: Message) => { - return [MessageListModule.command.UpdateItemCommand(message), UpdateEvent(), ChangeEvent()] - } - }) + const UpdateItemEvent = domain.event({ + name: 'MessageList.UpdateItemEvent' + }) - const DeleteEvent = domain.event({ - name: 'MessageList.DeleteEvent' - }) + const UpdateItemCommand = domain.command({ + name: 'MessageList.UpdateItemCommand', + impl: (_, message: T) => { + return [MessageListModule.command.UpdateItemCommand(message), UpdateItemEvent(message), ChangeListEvent()] + } + }) - const DeleteCommand = domain.command({ - name: 'MessageList.DeleteCommand', - impl: (_, id: string) => { - return [MessageListModule.command.DeleteItemCommand(id), DeleteEvent(), ChangeEvent()] - } - }) + const DeleteItemEvent = domain.event({ + name: 'MessageList.DeleteItemEvent' + }) - const ClearEvent = domain.event({ - name: 'MessageList.ClearEvent' - }) + const DeleteItemCommand = domain.command({ + name: 'MessageList.DeleteItemCommand', + impl: (_, id: string) => { + return [MessageListModule.command.DeleteItemCommand(id), DeleteItemEvent(id), ChangeListEvent()] + } + }) - const ClearCommand = domain.command({ - name: 'MessageList.ClearCommand', - impl: () => { - return [MessageListModule.command.SetListCommand([]), ClearEvent(), ChangeEvent()] - } - }) + const ClearListEvent = domain.event({ + name: 'MessageList.ClearListEvent' + }) - return { - query: { - ItemQuery, - ListQuery - }, - command: { - CreateCommand, - UpdateCommand, - DeleteCommand, - ClearCommand - }, - event: { - CreateEvent, - UpdateEvent, - DeleteEvent, - ClearEvent + const ClearListCommand = domain.command({ + name: 'MessageList.ClearListCommand', + impl: () => { + return [MessageListModule.command.DeleteAllCommand(), ClearListEvent(), ChangeListEvent()] + } + }) + + const InitListEvent = domain.event({ + 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(storageKey)).pipe(map((messages) => InitListCommand(messages ?? []))) + } + }) + + domain.effect({ + name: 'FormStateToStorageEffect', + impl: ({ fromEvent }) => { + const createItem$ = fromEvent(ChangeListEvent).pipe( + tap(async (messages) => await storage.set(storageKey, messages)) + ) + return merge(createItem$).pipe(map(() => null)) + } + }) + + return { + query: { + ItemQuery, + ListQuery + }, + command: { + CreateItemCommand, + UpdateItemCommand, + DeleteItemCommand, + ClearListCommand + }, + event: { + CreateItemEvent, + UpdateItemEvent, + DeleteItemEvent, + ClearListEvent + } } } - } -}) + })() -export default MessageListDomain +export default mem(MessageListDomain) diff --git a/src/domain/externs/Storage.ts b/src/domain/externs/Storage.ts new file mode 100644 index 0000000..705f8df --- /dev/null +++ b/src/domain/externs/Storage.ts @@ -0,0 +1,21 @@ +import { Remesh } from 'remesh' + +export interface Storage { + name: string + get: (key: string) => Promise + set: (key: string, value: T) => Promise +} + +const StorageExtern = Remesh.extern({ + default: { + name: 'STORAGE', + get: async () => { + throw new Error('"get" not implemented') + }, + set: async () => { + throw new Error('"set" not implemented') + } + } +}) + +export default StorageExtern diff --git a/src/domain/modules/Input.ts b/src/domain/modules/Input.ts index c5970f7..ea0f103 100644 --- a/src/domain/modules/Input.ts +++ b/src/domain/modules/Input.ts @@ -19,31 +19,25 @@ const InputModule = (domain: RemeshDomainContext, options: InputModuleOptions) = } }) - const InputEvent = domain.event({ - name: `${options.name}.InputEvent`, - impl: ({ get }) => { - return get(ValueState()) - } + const InputEvent = domain.event({ + name: `${options.name}.InputEvent` }) const InputCommand = domain.command({ name: `${options.name}.InputCommand`, impl: (_, value: string) => { - return [ValueState().new(value), InputEvent()] + return [ValueState().new(value), InputEvent(value)] } }) - const ChangeEvent = domain.event({ - name: `${options.name}.ChangeEvent`, - impl: ({ get }) => { - return get(ValueState()) - } + const ChangeEvent = domain.event({ + name: `${options.name}.ChangeEvent` }) const ChangeCommand = domain.command({ name: `${options.name}.ChangeCommand`, impl: (_, value: string) => { - return [ValueState().new(value), ChangeEvent()] + return [ValueState().new(value), ChangeEvent(value)] } }) diff --git a/src/impl/Storage.ts b/src/impl/Storage.ts new file mode 100644 index 0000000..8718342 --- /dev/null +++ b/src/impl/Storage.ts @@ -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 diff --git a/src/main.tsx b/src/main.tsx index cbaea9e..6133007 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,10 +4,12 @@ import { RemeshLogger } from 'remesh-logger' import { Remesh } from 'remesh' import App from './App' import createShadowRoot from './createShadowRoot' +import StorageImpl from './impl/Storage' import style from './index.css?inline' void (async () => { const store = Remesh.store({ + externs: [StorageImpl], inspectors: [RemeshLogger()] }) diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..4d12e2d --- /dev/null +++ b/src/types/index.d.ts @@ -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 +} diff --git a/src/views/Footer/index.tsx b/src/views/Footer/index.tsx index ca31a3d..345f9d8 100644 --- a/src/views/Footer/index.tsx +++ b/src/views/Footer/index.tsx @@ -6,39 +6,39 @@ import MessageInput from '@/components/MessageInput' import MessageInputDomain from '@/domain/MessageInput' import MessageListDomain from '@/domain/MessageList' import { MESSAGE_MAX_LENGTH } from '@/constants' +import { type Message } from '@/types' const Footer: FC = () => { const send = useRemeshSend() - const messageListDomain = useRemeshDomain(MessageListDomain()) + const messageListDomain = useRemeshDomain(MessageListDomain()) const messageInputDomain = useRemeshDomain(MessageInputDomain()) - - const message = 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 = { + username: '墨绿青苔', + avatar: 'https://avatars.githubusercontent.com/u/10251037?v=4', + body: text, + date: Date.now(), + likeChecked: false, + likeCount: 0, + hateChecked: false, + hateCount: 0 + } + 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(messageListDomain.command.CreateItemCommand(message)) send(messageInputDomain.command.ClearCommand()) } return (
{ - const messageListDomain = useRemeshDomain(MessageListDomain()) + const messageListDomain = useRemeshDomain(MessageListDomain()) const messageList = useRemeshQuery(messageListDomain.query.ListQuery()) return (