From f55a7f479d5bb912fed59cb6ed121e6bc4d74d10 Mon Sep 17 00:00:00 2001 From: molvqingtai Date: Fri, 1 Dec 2023 13:33:49 +0800 Subject: [PATCH] chore(form): store user info --- src/app/content/components/MessageItem.tsx | 1 - src/app/content/views/Footer/index.tsx | 1 - src/app/options/components/AvatarSelect.tsx | 10 ++- src/app/options/components/ProfileForm.tsx | 34 ++++++-- src/domain/MessageInput.ts | 2 - src/domain/MessageList.ts | 7 +- src/domain/UserInfo.ts | 94 +++++++++++++++++++++ src/types/global.d.ts | 9 +- src/utils/compressImage.ts | 16 ++-- wxt.config.ts | 3 + 10 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/domain/UserInfo.ts diff --git a/src/app/content/components/MessageItem.tsx b/src/app/content/components/MessageItem.tsx index e6e35ed..520bd38 100644 --- a/src/app/content/components/MessageItem.tsx +++ b/src/app/content/components/MessageItem.tsx @@ -5,7 +5,6 @@ import LikeButton from './LikeButton' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar' import { Markdown } from '@/components/ui/Markdown' -import { type Message } from '@/types/global' export interface MessageItemProps { data: Message diff --git a/src/app/content/views/Footer/index.tsx b/src/app/content/views/Footer/index.tsx index e66e4c3..712fac9 100644 --- a/src/app/content/views/Footer/index.tsx +++ b/src/app/content/views/Footer/index.tsx @@ -7,7 +7,6 @@ import { Button } from '@/components/ui/Button' import MessageInputDomain from '@/domain/MessageInput' import MessageListDomain from '@/domain/MessageList' import { MESSAGE_MAX_LENGTH } from '@/constants' -import { type Message } from '@/types/global' const Footer: FC = () => { const send = useRemeshSend() diff --git a/src/app/options/components/AvatarSelect.tsx b/src/app/options/components/AvatarSelect.tsx index 700686a..c2a88d7 100644 --- a/src/app/options/components/AvatarSelect.tsx +++ b/src/app/options/components/AvatarSelect.tsx @@ -9,6 +9,7 @@ export interface AvatarSelectProps { value?: string className?: string disabled?: boolean + compressSize?: number onSuccess?: (blob: Blob) => void onWarning?: (error: Error) => void onError?: (error: Error) => void @@ -16,7 +17,7 @@ export interface AvatarSelectProps { } const AvatarSelect = React.forwardRef( - ({ onChange, value, onError, onWarning, onSuccess, className, disabled }, ref) => { + ({ onChange, value, onError, onWarning, onSuccess, className, compressSize = 8 * 1024, disabled }, ref) => { const handleChange = async (e: ChangeEvent) => { const file = e.target.files?.[0] if (file) { @@ -26,8 +27,11 @@ const AvatarSelect = React.forwardRef( } try { - // Compress to 10kb - const blob = await compressImage(file, 10 * 1024) + /** + * In chrome storage.sync, each key-value pair supports a maximum storage of 8kb + * and all key-value pairs support a maximum storage of 100kb. + */ + const blob = await compressImage(file, compressSize) const reader = new FileReader() reader.onload = (e) => { onSuccess?.(blob) diff --git a/src/app/options/components/ProfileForm.tsx b/src/app/options/components/ProfileForm.tsx index d64eb95..80a6bec 100644 --- a/src/app/options/components/ProfileForm.tsx +++ b/src/app/options/components/ProfileForm.tsx @@ -3,35 +3,52 @@ import { useForm } from 'react-hook-form' import { valibotResolver } from '@hookform/resolvers/valibot' import { toast } from 'sonner' +import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react' +import { nanoid } from 'nanoid' +import { useEffect } from 'react' import AvatarSelect from './AvatarSelect' import { Button } from '@/components/ui/Button' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form' import { Input } from '@/components/ui/Input' import { Switch } from '@/components/ui/Switch' +import UserInfoDomain from '@/domain/UserInfo' + +// In chrome storage.sync, each key-value pair supports a maximum storage of 8kb +// Image is encoded as base64, and the size is increased by about 33%. +const COMPRESS_SIZE = 8 * 1024 - 8 * 1024 * 0.33 const formSchema = object({ - username: string([ + id: string(), + name: string([ toTrimmed(), minBytes(1, 'Please enter your username.'), maxBytes(20, 'Your username cannot exceed 20 bytes.') ]), - avatar: string([notLength(0, 'Please select your avatar.')]), + avatar: string([notLength(0, 'Please select your avatar.'), maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.')]), darkMode: boolean() }) const ProfileForm = () => { + const send = useRemeshSend() + const userInfoDomain = useRemeshDomain(UserInfoDomain()) + const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery()) + const form = useForm({ resolver: valibotResolver(formSchema), - defaultValues: { - username: '', + defaultValues: userInfo ?? { + id: nanoid(), + name: '', avatar: '', darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches } }) - const handleSubmit = (data: Output) => { - console.log(data) - console.log(data.avatar.length * 0.001) + useEffect(() => { + userInfo && form.reset(userInfo) + }, [userInfo, form]) + + const handleSubmit = (userInfo: Output) => { + send(userInfoDomain.command.SetUserInfoCommand(userInfo)) toast.success('Saved successfully!') } @@ -53,6 +70,7 @@ const ProfileForm = () => { { /> ( Username diff --git a/src/domain/MessageInput.ts b/src/domain/MessageInput.ts index d983b42..8dca19f 100644 --- a/src/domain/MessageInput.ts +++ b/src/domain/MessageInput.ts @@ -1,8 +1,6 @@ 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 de96f93..8eda6bd 100644 --- a/src/domain/MessageList.ts +++ b/src/domain/MessageList.ts @@ -3,13 +3,12 @@ import { ListModule } from 'remesh/modules/list' import { nanoid } from 'nanoid' import { from, map, tap, merge } from 'rxjs' import Storage from './externs/Storage' -import { type Message } from '@/types/global' const MessageListDomain = Remesh.domain({ name: 'MessageListDomain', impl: (domain) => { const storage = domain.getExtern(Storage) - const storageKey = `MESSAGE_LIST` + const storageKey = `MESSAGE_LIST` as const const MessageListModule = ListModule(domain, { name: 'MessageListModule', @@ -93,10 +92,10 @@ const MessageListDomain = Remesh.domain({ domain.effect({ name: 'FormStateToStorageEffect', impl: ({ fromEvent }) => { - const createItem$ = fromEvent(ChangeListEvent).pipe( + const changeList$ = fromEvent(ChangeListEvent).pipe( tap(async (messages) => await storage.set(storageKey, messages)) ) - return merge(createItem$).pipe(map(() => null)) + return merge(changeList$).pipe(map(() => null)) } }) diff --git a/src/domain/UserInfo.ts b/src/domain/UserInfo.ts new file mode 100644 index 0000000..ff76eef --- /dev/null +++ b/src/domain/UserInfo.ts @@ -0,0 +1,94 @@ +import { Remesh } from 'remesh' +import { forkJoin, from, map, merge, tap } from 'rxjs' +import Storage from './externs/Storage' + +const UserInfoDomain = Remesh.domain({ + name: 'UserInfoDomain', + impl: (domain) => { + const storage = domain.getExtern(Storage) + const storageKeys = { + USER_INFO_ID: 'USER_INFO_ID', + USER_INFO_NAME: 'USER_INFO_NAME', + USER_INFO_AVATAR: 'USER_INFO_AVATAR', + USER_INFO_DARK_MODE: 'USER_INFO_DARK_MODE' + } as const + + const UserInfoState = domain.state({ + name: 'UserInfo.UserInfoState', + default: null + }) + + const UserInfoQuery = domain.query({ + name: 'UserInfo.UserInfoQuery', + impl: ({ get }) => { + return get(UserInfoState()) + } + }) + + const SetUserInfoCommand = domain.command({ + name: 'UserInfo.SetUserInfoCommand', + impl: (_, userInfo: UserInfo | null) => { + return [UserInfoState().new(userInfo), ChangeUserInfoEvent()] + } + }) + + const ChangeUserInfoEvent = domain.event({ + name: 'UserInfo.ChangeUserInfoEvent', + impl: ({ get }) => { + return get(UserInfoQuery()) + } + }) + + domain.effect({ + name: 'FormStorageToStateEffect', + impl: () => { + return forkJoin({ + id: from(storage.get(storageKeys.USER_INFO_ID)), + name: from(storage.get(storageKeys.USER_INFO_NAME)), + avatar: from(storage.get(storageKeys.USER_INFO_AVATAR)), + darkMode: from(storage.get(storageKeys.USER_INFO_DARK_MODE)) + }).pipe( + map((userInfo) => { + if (userInfo.id && userInfo.name && userInfo.avatar && userInfo.darkMode) { + return SetUserInfoCommand(userInfo as UserInfo) + } else { + return SetUserInfoCommand(null) + } + }) + ) + } + }) + + domain.effect({ + name: 'FormStateToStorageEffect', + impl: ({ fromEvent }) => { + const changeUserInfo$ = fromEvent(ChangeUserInfoEvent).pipe( + tap(async (userInfo) => { + return await Promise.all([ + storage.set(storageKeys.USER_INFO_ID, userInfo?.id ?? null), + storage.set(storageKeys.USER_INFO_NAME, userInfo?.name ?? null), + storage.set(storageKeys.USER_INFO_AVATAR, userInfo?.avatar ?? null), + storage.set(storageKeys.USER_INFO_DARK_MODE, userInfo?.darkMode ?? null) + ]) + }) + ) + + return merge(changeUserInfo$).pipe(map(() => null)) + } + }) + + return { + query: { + UserInfoQuery + }, + command: { + SetUserInfoCommand + }, + event: { + ChangeUserInfoEvent + } + } + } +}) + +export default UserInfoDomain diff --git a/src/types/global.d.ts b/src/types/global.d.ts index bdba828..be7c69d 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,4 +1,4 @@ -export interface Message { +declare interface Message { id: string userId: string body: string @@ -12,3 +12,10 @@ export interface Message { hateUsers: string[] hateCount: number } + +declare interface UserInfo { + id: string + name: string + avatar: string + darkMode: boolean +} diff --git a/src/utils/compressImage.ts b/src/utils/compressImage.ts index 41302bb..79a7c64 100644 --- a/src/utils/compressImage.ts +++ b/src/utils/compressImage.ts @@ -1,6 +1,6 @@ const compress = async ( imageBitmap: ImageBitmap, - size: number, + targetSize: number, low: number, high: number, bestBlob: Blob @@ -22,21 +22,21 @@ const compress = async ( // Calculate the current size based on the current quality const currentSize = outputBlob.size - // If the current size is close to the target size, update the bestResult - if (Math.abs(currentSize - size) < Math.abs(bestBlob.size - size)) { + // If the current size is close to the target size, update the bestBlob + if (currentSize <= targetSize && Math.abs(currentSize - targetSize) < Math.abs(bestBlob.size - targetSize)) { bestBlob = outputBlob } - // If the current size is close to the target size or the range of low and high is too small, return the result - if (Math.abs(currentSize - size) < 100 || high - low < 0.01) { + // If the current size is between -1024 ~ 0, return the result + if ((currentSize - targetSize <= 0 && currentSize - targetSize >= -1024) || high - low < 0.01) { return bestBlob } // Adjust the range for recursion based on the current quality and size - if (currentSize > size) { - return await compress(imageBitmap, size, low, mid, bestBlob) + if (currentSize > targetSize) { + return await compress(imageBitmap, targetSize, low, mid, bestBlob) } else { - return await compress(imageBitmap, size, mid, high, bestBlob) + return await compress(imageBitmap, targetSize, mid, high, bestBlob) } } diff --git a/wxt.config.ts b/wxt.config.ts index 09d587a..0ac0137 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -12,6 +12,9 @@ export default defineConfig({ runner: { startUrls: ['https://www.example.com/'] }, + manifest: { + permissions: ['storage'] + }, vite: () => ({ define: { __DEV__: isDev,