chore: watch userInfo storage
This commit is contained in:
parent
511338850e
commit
968480605c
13 changed files with 172 additions and 36 deletions
|
@ -2,6 +2,6 @@
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 160,
|
"printWidth": 120,
|
||||||
"tailwindFunction": ["clsx"]
|
"tailwindFunction": ["clsx"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
|
"dev:signaling": "y-webrtc-signaling",
|
||||||
"dev:firefox": "wxt -b firefox",
|
"dev:firefox": "wxt -b firefox",
|
||||||
"build": "wxt build",
|
"build": "wxt build",
|
||||||
"build:firefox": "wxt build -b firefox",
|
"build:firefox": "wxt build -b firefox",
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default defineContentScript({
|
||||||
async main(ctx) {
|
async main(ctx) {
|
||||||
const doc = new Y.Doc()
|
const doc = new Y.Doc()
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new WebrtcProvider(__NAME__, doc)
|
new WebrtcProvider(__NAME__, doc, { signaling: ['ws://localhost:4444'] })
|
||||||
|
|
||||||
const store = Remesh.store({
|
const store = Remesh.store({
|
||||||
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, RemeshYjsExtern.impl({ doc })],
|
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, RemeshYjsExtern.impl({ doc })],
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
import { type ReactNode, type FC, useState, type MouseEvent, useRef } from 'react'
|
import { type ReactNode, type FC, useState, type MouseEvent, useRef } from 'react'
|
||||||
import { SettingsIcon, MoonIcon, SunIcon } from 'lucide-react'
|
import { SettingsIcon, MoonIcon, SunIcon } from 'lucide-react'
|
||||||
import { useClickAway } from 'react-use'
|
|
||||||
import { browser } from 'wxt/browser'
|
import { browser } from 'wxt/browser'
|
||||||
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { EVENTS } from '@/constants'
|
import { EVENTS } from '@/constants'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
|
import useClickAway from '@/hooks/useClickAway'
|
||||||
|
|
||||||
export interface AppButtonProps {
|
export interface AppButtonProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppButton: FC<AppButtonProps> = ({ children }) => {
|
const AppButton: FC<AppButtonProps> = ({ children }) => {
|
||||||
|
const send = useRemeshSend()
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
console.log(userInfo)
|
|
||||||
|
const isDarkMode =
|
||||||
|
userInfo?.themeMode === 'dark'
|
||||||
|
? true
|
||||||
|
: userInfo?.themeMode === 'light'
|
||||||
|
? false
|
||||||
|
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
@ -32,42 +40,46 @@ const AppButton: FC<AppButtonProps> = ({ children }) => {
|
||||||
['click']
|
['click']
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleToggle = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleToggleMenu = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSwitchTheme = () => {
|
||||||
|
if (userInfo) {
|
||||||
|
send(userInfoDomain.command.UpdateUserInfoCommand({ ...userInfo, themeMode: isDarkMode ? 'light' : 'dark' }))
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOpenOptionsPage = () => {
|
const handleOpenOptionsPage = () => {
|
||||||
browser.runtime.sendMessage(EVENTS.OPEN_OPTIONS_PAGE)
|
browser.runtime.sendMessage(EVENTS.OPEN_OPTIONS_PAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menuRef} className="fixed bottom-5 right-5 z-top grid select-none justify-center gap-y-3">
|
<div ref={menuRef} className="fixed bottom-5 right-5 z-top grid select-none justify-center gap-y-3">
|
||||||
<div className="grid gap-y-3" inert={!open && ''}>
|
{/* <div className="grid gap-y-3" inert={!open && ''}> */}
|
||||||
{/* <Button
|
<div className="pointer-events-none grid gap-y-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleSwitchTheme}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
data-state={open ? 'open' : 'closed'}
|
data-state={open ? 'open' : 'closed'}
|
||||||
className="h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
className="pointer-events-auto h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
||||||
>
|
>
|
||||||
<MoonIcon size={20} />
|
<MoonIcon size={20} />
|
||||||
</Button> */}
|
{/* <SunIcon size={20} /> */}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
data-state={open ? 'open' : 'closed'}
|
|
||||||
className="h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
|
||||||
>
|
|
||||||
<SunIcon size={20} />
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenOptionsPage}
|
onClick={handleOpenOptionsPage}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
data-state={open ? 'open' : 'closed'}
|
data-state={open ? 'open' : 'closed'}
|
||||||
className="h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
className="pointer-events-auto h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
||||||
>
|
>
|
||||||
<SettingsIcon size={20} />
|
<SettingsIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button onContextMenu={handleToggle} className="relative z-10 h-10 w-10 rounded-full p-0 text-xs shadow">
|
<Button onContextMenu={handleToggleMenu} className="relative z-10 h-10 w-10 rounded-full p-0 text-xs shadow">
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { type Output, object, string, minBytes, maxBytes, toTrimmed, union, literal, notLength, number } from 'valibot'
|
import { type Output, object, string, minBytes, maxBytes, toTrimmed, union, literal, notLength, number } from 'valibot'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { valibotResolver } from '@hookform/resolvers/valibot'
|
import { valibotResolver } from '@hookform/resolvers/valibot'
|
||||||
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
@ -32,7 +31,11 @@ const formSchema = object({
|
||||||
createTime: number(),
|
createTime: number(),
|
||||||
// Pure numeric strings will be converted to number
|
// Pure numeric strings will be converted to number
|
||||||
// Issues: https://github.com/unjs/unstorage/issues/277
|
// Issues: https://github.com/unjs/unstorage/issues/277
|
||||||
name: string([toTrimmed(), minBytes(1, 'Please enter your username.'), maxBytes(20, 'Your username cannot exceed 20 bytes.')]),
|
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.'), maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.')]),
|
avatar: string([notLength(0, 'Please select your avatar.'), maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.')]),
|
||||||
themeMode: union([literal('system'), literal('light'), literal('dark')], 'Please select extension theme mode.')
|
themeMode: union([literal('system'), literal('light'), literal('dark')], 'Please select extension theme mode.')
|
||||||
})
|
})
|
||||||
|
@ -47,12 +50,13 @@ const ProfileForm = () => {
|
||||||
defaultValues: userInfo ?? defaultUserInfo
|
defaultValues: userInfo ?? defaultUserInfo
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update defaultValues
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userInfo && form.reset(userInfo)
|
userInfo && form.reset(userInfo)
|
||||||
}, [userInfo, form])
|
}, [userInfo, form])
|
||||||
|
|
||||||
const handleSubmit = (userInfo: Output<typeof formSchema>) => {
|
const handleSubmit = (userInfo: Output<typeof formSchema>) => {
|
||||||
send(userInfoDomain.command.SetUserInfoCommand(userInfo))
|
send(userInfoDomain.command.UpdateUserInfoCommand(userInfo))
|
||||||
toast.success('Saved successfully!')
|
toast.success('Saved successfully!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +77,13 @@ const ProfileForm = () => {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
|
<FormItem className="absolute left-1/2 top-0 grid -translate-x-1/2 -translate-y-1/2 justify-items-center">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<AvatarSelect compressSize={COMPRESS_SIZE} onError={handleError} onWarning={handleWarning} className="shadow-lg" {...field}></AvatarSelect>
|
<AvatarSelect
|
||||||
|
compressSize={COMPRESS_SIZE}
|
||||||
|
onError={handleError}
|
||||||
|
onWarning={handleWarning}
|
||||||
|
className="shadow-lg"
|
||||||
|
{...field}
|
||||||
|
></AvatarSelect>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -115,7 +125,9 @@ const ProfileForm = () => {
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>The theme mode of the extension. If you choose the system, will follow the system theme.</FormDescription>
|
<FormDescription>
|
||||||
|
The theme mode of the extension. If you choose the system, will follow the system theme.
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => {
|
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => {
|
||||||
const shadowRoot = document.querySelector(__NAME__)!.shadowRoot! as any as HTMLElement
|
const shadowRoot = document.querySelector(__NAME__)!.shadowRoot! as unknown as HTMLElement
|
||||||
return (
|
return (
|
||||||
<PopoverPrimitive.Portal container={shadowRoot}>
|
<PopoverPrimitive.Portal container={shadowRoot}>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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 { RemeshYjs } from 'remesh-yjs'
|
||||||
import { IndexDBStorageExtern } from './externs/Storage'
|
import { IndexDBStorageExtern } from './externs/Storage'
|
||||||
|
|
||||||
const MessageListDomain = Remesh.domain({
|
const MessageListDomain = Remesh.domain({
|
||||||
|
@ -92,11 +93,24 @@ const MessageListDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'FormStateToStorageEffect',
|
name: 'FormStateToStorageEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const changeList$ = fromEvent(ChangeListEvent).pipe(tap(async (messages) => await storage.set<Message[]>(storageKey, messages)))
|
const changeList$ = fromEvent(ChangeListEvent).pipe(
|
||||||
|
tap(async (messages) => await storage.set<Message[]>(storageKey, messages))
|
||||||
|
)
|
||||||
return merge(changeList$).pipe(map(() => null))
|
return merge(changeList$).pipe(map(() => null))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
RemeshYjs(domain, {
|
||||||
|
key: 'MessageList',
|
||||||
|
dataType: 'array',
|
||||||
|
onSend: ({ get }): Message[] => {
|
||||||
|
return get(ListQuery())
|
||||||
|
},
|
||||||
|
onReceive: (_, messages: Message[]) => {
|
||||||
|
return InitListCommand(messages)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
ItemQuery,
|
ItemQuery,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { forkJoin, from, map, merge, tap } from 'rxjs'
|
import { forkJoin, from, map, merge, switchMap, tap } from 'rxjs'
|
||||||
import { BrowserSyncStorageExtern } from './externs/Storage'
|
import { BrowserSyncStorageExtern } from './externs/Storage'
|
||||||
import { isNullish } from '@/utils'
|
import { isNullish, storageToObservable } from '@/utils'
|
||||||
|
|
||||||
const UserInfoDomain = Remesh.domain({
|
const UserInfoDomain = Remesh.domain({
|
||||||
name: 'UserInfoDomain',
|
name: 'UserInfoDomain',
|
||||||
|
@ -27,8 +27,8 @@ const UserInfoDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SetUserInfoCommand = domain.command({
|
const UpdateUserInfoCommand = domain.command({
|
||||||
name: 'UserInfo.SetUserInfoCommand',
|
name: 'UserInfo.UpdateUserInfoCommand',
|
||||||
impl: (_, userInfo: UserInfo | null) => {
|
impl: (_, userInfo: UserInfo | null) => {
|
||||||
return [UserInfoState().new(userInfo), UpdateUserInfoEvent()]
|
return [UserInfoState().new(userInfo), UpdateUserInfoEvent()]
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,9 @@ const UserInfoDomain = Remesh.domain({
|
||||||
!isNullish(userInfo.createTime) &&
|
!isNullish(userInfo.createTime) &&
|
||||||
!isNullish(userInfo.themeMode)
|
!isNullish(userInfo.themeMode)
|
||||||
) {
|
) {
|
||||||
return SetUserInfoCommand(userInfo as UserInfo)
|
return UpdateUserInfoCommand(userInfo as UserInfo)
|
||||||
} else {
|
} else {
|
||||||
return SetUserInfoCommand(null)
|
return UpdateUserInfoCommand(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -77,22 +77,55 @@ const UserInfoDomain = Remesh.domain({
|
||||||
storage.set<UserInfo['id'] | null>(storageKeys.USER_INFO_ID, userInfo?.id ?? null),
|
storage.set<UserInfo['id'] | null>(storageKeys.USER_INFO_ID, userInfo?.id ?? null),
|
||||||
storage.set<UserInfo['name'] | null>(storageKeys.USER_INFO_NAME, userInfo?.name ?? null),
|
storage.set<UserInfo['name'] | null>(storageKeys.USER_INFO_NAME, userInfo?.name ?? null),
|
||||||
storage.set<UserInfo['avatar'] | null>(storageKeys.USER_INFO_AVATAR, userInfo?.avatar ?? null),
|
storage.set<UserInfo['avatar'] | null>(storageKeys.USER_INFO_AVATAR, userInfo?.avatar ?? null),
|
||||||
storage.set<UserInfo['createTime'] | null>(storageKeys.USER_INFO_CREATE_TIME, userInfo?.createTime ?? null),
|
storage.set<UserInfo['createTime'] | null>(
|
||||||
|
storageKeys.USER_INFO_CREATE_TIME,
|
||||||
|
userInfo?.createTime ?? null
|
||||||
|
),
|
||||||
storage.set<UserInfo['themeMode'] | null>(storageKeys.USER_INFO_THEME_MODE, userInfo?.themeMode ?? null)
|
storage.set<UserInfo['themeMode'] | null>(storageKeys.USER_INFO_THEME_MODE, userInfo?.themeMode ?? null)
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return merge(changeUserInfo$).pipe(map(() => null))
|
return merge(changeUserInfo$).pipe(map(() => null))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'WatchStorageEffect',
|
||||||
|
impl: () => {
|
||||||
|
return storageToObservable(storage).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
return forkJoin({
|
||||||
|
id: from(storage.get<UserInfo['id']>(storageKeys.USER_INFO_ID)),
|
||||||
|
name: from(storage.get<UserInfo['name']>(storageKeys.USER_INFO_NAME)),
|
||||||
|
avatar: from(storage.get<UserInfo['avatar']>(storageKeys.USER_INFO_AVATAR)),
|
||||||
|
createTime: from(storage.get<UserInfo['createTime']>(storageKeys.USER_INFO_CREATE_TIME)),
|
||||||
|
themeMode: from(storage.get<UserInfo['themeMode']>(storageKeys.USER_INFO_THEME_MODE))
|
||||||
|
}).pipe(
|
||||||
|
map((userInfo) => {
|
||||||
|
if (
|
||||||
|
!isNullish(userInfo.id) &&
|
||||||
|
!isNullish(userInfo.name) &&
|
||||||
|
!isNullish(userInfo.avatar) &&
|
||||||
|
!isNullish(userInfo.createTime) &&
|
||||||
|
!isNullish(userInfo.themeMode)
|
||||||
|
) {
|
||||||
|
return UpdateUserInfoCommand(userInfo as UserInfo)
|
||||||
|
} else {
|
||||||
|
return UpdateUserInfoCommand(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
UserInfoQuery
|
UserInfoQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
SetUserInfoCommand
|
UpdateUserInfoCommand
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
UpdateUserInfoEvent
|
UpdateUserInfoEvent
|
||||||
|
|
45
src/hooks/useClickAway.ts
Normal file
45
src/hooks/useClickAway.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { type RefObject, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export type Events = Array<keyof GlobalEventHandlersEventMap>
|
||||||
|
|
||||||
|
const useClickAway = <E extends Event = Event>(
|
||||||
|
ref: RefObject<HTMLElement | null>,
|
||||||
|
onClickAway: (event: E) => void,
|
||||||
|
events: Events = ['mousedown', 'touchstart']
|
||||||
|
) => {
|
||||||
|
const savedCallback = useRef(onClickAway)
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = onClickAway
|
||||||
|
}, [onClickAway])
|
||||||
|
useEffect(() => {
|
||||||
|
const { current: el } = ref
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const rootNode = el.getRootNode()
|
||||||
|
const isInShadow = rootNode instanceof ShadowRoot
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When events are captured outside the component, events that occur in shadow DOM will target the host element
|
||||||
|
* so additional event listeners need to be added for shadowDom
|
||||||
|
*
|
||||||
|
* document shadowDom target
|
||||||
|
* | | |
|
||||||
|
* |- on(document) -|- on(shadowRoot) -|
|
||||||
|
*/
|
||||||
|
const handler = (event: SafeAny) => {
|
||||||
|
!el.contains(event.target) && event.target.shadowRoot !== rootNode && savedCallback.current(event)
|
||||||
|
}
|
||||||
|
for (const eventName of events) {
|
||||||
|
document.addEventListener(eventName, handler)
|
||||||
|
isInShadow && rootNode.addEventListener(eventName, handler)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
for (const eventName of events) {
|
||||||
|
document.removeEventListener(eventName, handler)
|
||||||
|
isInShadow && rootNode.removeEventListener(eventName, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [events, ref])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useClickAway
|
5
src/types/global.d.ts
vendored
5
src/types/global.d.ts
vendored
|
@ -1,4 +1,5 @@
|
||||||
declare interface Message {
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
declare type Message = {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
body: string
|
body: string
|
||||||
|
@ -20,3 +21,5 @@ declare interface UserInfo {
|
||||||
createTime: number
|
createTime: number
|
||||||
themeMode: 'system' | 'light' | 'dark'
|
themeMode: 'system' | 'light' | 'dark'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type SafeAny = any
|
||||||
|
|
|
@ -7,3 +7,4 @@ export { default as chunk } from './chunk'
|
||||||
export { default as compressImage } from './compressImage'
|
export { default as compressImage } from './compressImage'
|
||||||
export { default as isNullish } from './isNullish'
|
export { default as isNullish } from './isNullish'
|
||||||
export { default as checkSystemDarkMode } from './checkSystemDarkMode'
|
export { default as checkSystemDarkMode } from './checkSystemDarkMode'
|
||||||
|
export { default as storageToObservable } from './storageToObservable'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
const isNullish = (value: any) => value === undefined || value === null
|
const isNullish = <T = any>(value: T) => value === undefined || value === null
|
||||||
|
|
||||||
export default isNullish
|
export default isNullish
|
||||||
|
|
15
src/utils/storageToObservable.ts
Normal file
15
src/utils/storageToObservable.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { type Storage } from '@/domain/externs/Storage'
|
||||||
|
|
||||||
|
const storageToObservable = (storage: Storage) => {
|
||||||
|
return new Observable((subscriber) => {
|
||||||
|
storage.watch((event) => {
|
||||||
|
subscriber.next(event)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
storage.unwatch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default storageToObservable
|
Loading…
Reference in a new issue