feat: support danmaku
This commit is contained in:
parent
52cd203a53
commit
999a55c65f
31 changed files with 1116 additions and 595 deletions
20
package.json
20
package.json
|
@ -48,6 +48,7 @@
|
||||||
"@lottiefiles/dotlottie-react": "^0.9.0",
|
"@lottiefiles/dotlottie-react": "^0.9.0",
|
||||||
"@perfsee/jsonr": "^1.13.0",
|
"@perfsee/jsonr": "^1.13.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
@ -64,8 +65,9 @@
|
||||||
"@webext-core/proxy-service": "^1.2.0",
|
"@webext-core/proxy-service": "^1.2.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"danmaku": "^2.0.7",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^11.7.0",
|
"framer-motion": "^11.9.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
@ -86,14 +88,13 @@
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"trystero": "^0.20.0",
|
"trystero": "^0.20.0",
|
||||||
"type-fest": "^4.26.1",
|
"type-fest": "^4.26.1",
|
||||||
"unstorage": "^1.12.0",
|
"unstorage": "1.11.0",
|
||||||
"valibot": "^0.42.1",
|
"valibot": "^0.42.1"
|
||||||
"webextension-polyfill": "^0.12.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.5.0",
|
"@commitlint/cli": "^19.5.0",
|
||||||
"@commitlint/config-conventional": "^19.5.0",
|
"@commitlint/config-conventional": "^19.5.0",
|
||||||
"@eslint-react/eslint-plugin": "^1.14.2",
|
"@eslint-react/eslint-plugin": "^1.14.3",
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
@ -101,12 +102,11 @@
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/node": "^22.7.2",
|
"@types/node": "^22.7.4",
|
||||||
"@types/react": "^18.3.9",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/webextension-polyfill": "^0.12.1",
|
|
||||||
"@typescript-eslint/parser": "^8.7.0",
|
"@typescript-eslint/parser": "^8.7.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
"postcss-rem-to-responsive-pixel": "^6.0.2",
|
"postcss-rem-to-responsive-pixel": "^6.0.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"rimraf": "^5.0.10",
|
"rimraf": "^5.0.10",
|
||||||
"semantic-release": "^24.1.1",
|
"semantic-release": "^24.1.2",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
|
|
1019
pnpm-lock.yaml
1019
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -8,9 +8,9 @@ import RoomDomain from '@/domain/Room'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import Setup from '@/app/content/views/Setup'
|
import Setup from '@/app/content/views/Setup'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'sonner'
|
||||||
import { indexDBStorage } from '@/domain/impls/Storage'
|
import { browserSyncStorage, indexDBStorage } from '@/domain/impls/Storage'
|
||||||
import { APP_OPEN_STATUS_STORAGE_KEY } from '@/constants/config'
|
import { APP_OPEN_STATUS_STORAGE_KEY } from '@/constants/config'
|
||||||
import LogoIcon0 from '@/assets/images/logo-0.svg'
|
import LogoIcon0 from '@/assets/images/logo-0.svg'
|
||||||
import LogoIcon1 from '@/assets/images/logo-1.svg'
|
import LogoIcon1 from '@/assets/images/logo-1.svg'
|
||||||
|
@ -21,16 +21,21 @@ import LogoIcon5 from '@/assets/images/logo-5.svg'
|
||||||
import LogoIcon6 from '@/assets/images/logo-6.svg'
|
import LogoIcon6 from '@/assets/images/logo-6.svg'
|
||||||
|
|
||||||
import { getDay } from 'date-fns'
|
import { getDay } from 'date-fns'
|
||||||
|
import DanmakuContainer from './components/DanmakuContainer'
|
||||||
|
import DanmakuDomain from '@/domain/Danmaku'
|
||||||
|
import { browser } from 'wxt/browser'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
|
const danmakuDomain = useRemeshDomain(DanmakuDomain())
|
||||||
|
const danmakuIsEnabled = useRemeshQuery(danmakuDomain.query.IsEnabledQuery())
|
||||||
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery())
|
const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery())
|
||||||
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||||
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.MessageListLoadIsFinishedQuery())
|
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.LoadIsFinishedQuery())
|
||||||
|
|
||||||
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
||||||
|
|
||||||
const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())]
|
const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())]
|
||||||
|
@ -63,6 +68,15 @@ export default function App() {
|
||||||
getAppOpenStatus()
|
getAppOpenStatus()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const danmakuContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
danmakuIsEnabled && send(danmakuDomain.command.MountCommand(danmakuContainerRef.current!))
|
||||||
|
return () => {
|
||||||
|
danmakuIsEnabled && send(danmakuDomain.command.DestroyCommand())
|
||||||
|
}
|
||||||
|
}, [danmakuIsEnabled])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppContainer open={appOpen}>
|
<AppContainer open={appOpen}>
|
||||||
|
@ -75,6 +89,7 @@ export default function App() {
|
||||||
<AppButton onClick={handleToggleApp}>
|
<AppButton onClick={handleToggleApp}>
|
||||||
<DayLogo className="max-h-full max-w-full"></DayLogo>
|
<DayLogo className="max-h-full max-w-full"></DayLogo>
|
||||||
</AppButton>
|
</AppButton>
|
||||||
|
<DanmakuContainer ref={danmakuContainerRef} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
19
src/app/content/components/DanmakuContainer.tsx
Normal file
19
src/app/content/components/DanmakuContainer.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { cn } from '@/utils'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
export interface DanmakuContainerProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DanmakuContainer = forwardRef<HTMLDivElement, DanmakuContainerProps>(({ className }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('fixed left-0 top-20 z-infinity w-full h-full invisible pointer-events-none shadow-md', className)}
|
||||||
|
ref={ref}
|
||||||
|
></div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
DanmakuContainer.displayName = 'DanmakuContainer'
|
||||||
|
|
||||||
|
export default DanmakuContainer
|
32
src/app/content/components/DanmakuMessage.tsx
Normal file
32
src/app/content/components/DanmakuMessage.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { TextMessage } from '@/domain/Room'
|
||||||
|
import { cn } from '@/utils'
|
||||||
|
import { AvatarImage } from '@radix-ui/react-avatar'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
export interface PromptItemProps {
|
||||||
|
data: TextMessage
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DanmakuMessage: FC<PromptItemProps> = ({ data, className }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'flex justify-center pointer-events-auto visible gap-x-2 border px-2.5 py-0.5 rounded-full bg-primary/30 text-base font-medium text-white backdrop-blur-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Avatar className="size-5">
|
||||||
|
<AvatarImage src={data.userAvatar} alt="avatar" />
|
||||||
|
<AvatarFallback>{data.username.at(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="max-w-40 overflow-hidden text-ellipsis">{data.body}</div>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DanmakuMessage.displayName = 'DanmakuMessage'
|
||||||
|
|
||||||
|
export default DanmakuMessage
|
|
@ -1,6 +1,5 @@
|
||||||
import { type ChangeEvent, type KeyboardEvent } from 'react'
|
import { forwardRef, type ChangeEvent, type KeyboardEvent } from 'react'
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { Markdown } from '@/components/ui/Markdown'
|
import { Markdown } from '@/components/ui/Markdown'
|
||||||
import { cn } from '@/utils'
|
import { cn } from '@/utils'
|
||||||
|
@ -17,7 +16,7 @@ export interface MessageInputProps {
|
||||||
onEnter?: (value: string) => void
|
onEnter?: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageInput = React.forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
({ value = '', className, maxLength = 500, onInput, onEnter, preview, autoFocus, disabled }, ref) => {
|
({ value = '', className, maxLength = 500, onInput, onEnter, preview, autoFocus, disabled }, ref) => {
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) {
|
if (e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) {
|
||||||
|
|
|
@ -7,8 +7,9 @@ import { defineContentScript } from 'wxt/sandbox'
|
||||||
import { createShadowRootUi } from 'wxt/client'
|
import { createShadowRootUi } from 'wxt/client'
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
import { IndexDBStorageImpl, BrowserSyncStorageImpl, indexDBStorage } from '@/domain/impls/Storage'
|
||||||
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
||||||
|
import { DanmakuImpl } from '@/domain/impls/Danmaku'
|
||||||
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
||||||
import '@/assets/styles/tailwind.css'
|
import '@/assets/styles/tailwind.css'
|
||||||
import '@/assets/styles/sonner.css'
|
import '@/assets/styles/sonner.css'
|
||||||
|
@ -22,7 +23,7 @@ export default defineContentScript({
|
||||||
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*'],
|
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*'],
|
||||||
async main(ctx) {
|
async main(ctx) {
|
||||||
const store = Remesh.store({
|
const store = Remesh.store({
|
||||||
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl]
|
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl, DanmakuImpl]
|
||||||
// inspectors: __DEV__ ? [RemeshLogger()] : []
|
// inspectors: __DEV__ ? [RemeshLogger()] : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type ReactNode, type FC, useState, type MouseEvent, useRef, useEffect } 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 { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import PromptItem from '../../components/PromptItem'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import RoomDomain, { MessageType } from '@/domain/Room'
|
import RoomDomain, { MessageType } from '@/domain/Room'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
import BlurFade from '@/components/magicui/blur-fade'
|
import BlurFade from '@/components/magicui/BlurFade'
|
||||||
|
|
||||||
const Main: FC = () => {
|
const Main: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
|
|
|
@ -9,9 +9,9 @@ import { FC, useEffect, useState } from 'react'
|
||||||
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
|
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
|
||||||
import Timer from '@resreq/timer'
|
import Timer from '@resreq/timer'
|
||||||
import ExampleImage from '@/assets/images/example.jpg'
|
import ExampleImage from '@/assets/images/example.jpg'
|
||||||
import PulsatingButton from '@/components/magicui/pulsating-button'
|
import PulsatingButton from '@/components/magicui/PulsatingButton'
|
||||||
import BlurFade from '@/components/magicui/blur-fade'
|
import BlurFade from '@/components/magicui/BlurFade'
|
||||||
import WordPullUp from '@/components/magicui/word-pull-up'
|
import WordPullUp from '@/components/magicui/WordPullUp'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
const mockTextList = [
|
const mockTextList = [
|
||||||
|
@ -41,7 +41,8 @@ const generateUserInfo = async (): Promise<UserInfo> => {
|
||||||
name: generateRandomName(),
|
name: generateRandomName(),
|
||||||
avatar: await generateRandomAvatar(MAX_AVATAR_SIZE),
|
avatar: await generateRandomAvatar(MAX_AVATAR_SIZE),
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system'
|
themeMode: checkSystemDarkMode() ? 'dark' : 'system',
|
||||||
|
danmakuEnabled: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Meteors from '@/components/magicui/meteors'
|
import Meteors from '@/components/magicui/Meteors'
|
||||||
import { FC, ReactNode } from 'react'
|
import { FC, ReactNode } from 'react'
|
||||||
|
|
||||||
export interface LayoutProps {
|
export interface LayoutProps {
|
||||||
|
|
|
@ -15,15 +15,16 @@ import { Label } from '@/components/ui/Label'
|
||||||
import { RefreshCcwIcon } from 'lucide-react'
|
import { RefreshCcwIcon } from 'lucide-react'
|
||||||
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
||||||
import ToastDomain from '@/domain/Toast'
|
import ToastDomain from '@/domain/Toast'
|
||||||
import BlurFade from '@/components/magicui/blur-fade'
|
import BlurFade from '@/components/magicui/BlurFade'
|
||||||
import debounce from './../../../utils/debounce'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
|
||||||
const defaultUserInfo: UserInfo = {
|
const defaultUserInfo: UserInfo = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: '',
|
name: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
createTime: Date.now(),
|
createTime: Date.now(),
|
||||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system'
|
themeMode: checkSystemDarkMode() ? 'dark' : 'system',
|
||||||
|
danmakuEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = v.object({
|
const formSchema = v.object({
|
||||||
|
@ -49,7 +50,8 @@ const formSchema = v.object({
|
||||||
themeMode: v.pipe(
|
themeMode: v.pipe(
|
||||||
v.string(),
|
v.string(),
|
||||||
v.union([v.literal('system'), v.literal('light'), v.literal('dark')], 'Please select extension theme mode.')
|
v.union([v.literal('system'), v.literal('light'), v.literal('dark')], 'Please select extension theme mode.')
|
||||||
)
|
),
|
||||||
|
danmakuEnabled: v.boolean()
|
||||||
})
|
})
|
||||||
|
|
||||||
const ProfileForm = () => {
|
const ProfileForm = () => {
|
||||||
|
@ -89,7 +91,7 @@ const ProfileForm = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} autoComplete="off" className="relative w-96 space-y-8 p-10">
|
<form onSubmit={form.handleSubmit(handleSubmit)} autoComplete="off" className="relative w-[450px] space-y-8 p-14">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="avatar"
|
name="avatar"
|
||||||
|
@ -128,6 +130,30 @@ const ProfileForm = () => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="danmakuEnabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
{/* <FormLabel>Username</FormLabel> */}
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={false}
|
||||||
|
id="enable-danmaku"
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
checked={field.value}
|
||||||
|
/>
|
||||||
|
<FormLabel className="cursor-pointer" htmlFor="enable-danmaku">
|
||||||
|
Enable Danmaku
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Danmaku messages will scroll across the screen.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="themeMode"
|
name="themeMode"
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { RemeshRoot } from 'remesh-react'
|
import { RemeshRoot } from 'remesh-react'
|
||||||
import { RemeshLogger } from 'remesh-logger'
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
import { BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
||||||
import '@/assets/styles/tailwind.css'
|
import '@/assets/styles/tailwind.css'
|
||||||
|
@ -10,8 +9,7 @@ import '@/assets/styles/tailwind.css'
|
||||||
import { ToastImpl } from '@/domain/impls/Toast'
|
import { ToastImpl } from '@/domain/impls/Toast'
|
||||||
|
|
||||||
const store = Remesh.store({
|
const store = Remesh.store({
|
||||||
externs: [BrowserSyncStorageImpl, ToastImpl],
|
externs: [BrowserSyncStorageImpl, ToastImpl]
|
||||||
inspectors: [RemeshLogger()]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
|
28
src/components/ui/checkbox.tsx
Normal file
28
src/components/ui/checkbox.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/index"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
145
src/domain/Danmaku.ts
Normal file
145
src/domain/Danmaku.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
import { DanmakuExtern } from './externs/Danmaku'
|
||||||
|
import { TextMessage } from './Room'
|
||||||
|
import UserInfoDomain from './UserInfo'
|
||||||
|
import { map } from 'rxjs'
|
||||||
|
|
||||||
|
const DanmakuDomain = Remesh.domain({
|
||||||
|
name: 'DanmakuDomain',
|
||||||
|
impl: (domain) => {
|
||||||
|
const danmaku = domain.getExtern(DanmakuExtern)
|
||||||
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
|
|
||||||
|
const MountState = domain.state({
|
||||||
|
name: 'Danmaku.MountState',
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
const DanmakuEnabledState = domain.state<boolean>({
|
||||||
|
name: 'Danmaku.EnabledState',
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const IsEnabledQuery = domain.query({
|
||||||
|
name: 'Danmaku.IsOpenQuery',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(DanmakuEnabledState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const EnableCommand = domain.command({
|
||||||
|
name: 'Danmaku.EnableCommand',
|
||||||
|
impl: () => {
|
||||||
|
return DanmakuEnabledState().new(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DisableCommand = domain.command({
|
||||||
|
name: 'Danmaku.DisableCommand',
|
||||||
|
impl: () => {
|
||||||
|
return DanmakuEnabledState().new(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const IsMountedQuery = domain.query({
|
||||||
|
name: 'Danmaku.IsMountedQuery',
|
||||||
|
impl: ({ get }) => get(MountState())
|
||||||
|
})
|
||||||
|
|
||||||
|
const PushCommand = domain.command({
|
||||||
|
name: 'Danmaku.PushCommand',
|
||||||
|
impl: (_, message: TextMessage) => {
|
||||||
|
danmaku.push(message)
|
||||||
|
return [PushEvent(message)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const UnshiftCommand = domain.command({
|
||||||
|
name: 'Danmaku.UnshiftCommand',
|
||||||
|
impl: (_, message: TextMessage) => {
|
||||||
|
danmaku.unshift(message)
|
||||||
|
return [UnshiftEvent(message)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ClearCommand = domain.command({
|
||||||
|
name: 'Danmaku.ClearCommand',
|
||||||
|
impl: () => {
|
||||||
|
danmaku.clear()
|
||||||
|
return [ClearEvent()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const MountCommand = domain.command({
|
||||||
|
name: 'Danmaku.ClearCommand',
|
||||||
|
impl: (_, container: HTMLElement) => {
|
||||||
|
danmaku.mount(container)
|
||||||
|
return [MountEvent(container)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DestroyCommand = domain.command({
|
||||||
|
name: 'Danmaku.DestroyCommand',
|
||||||
|
impl: () => {
|
||||||
|
danmaku.destroy()
|
||||||
|
return [DestroyEvent()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const PushEvent = domain.event<TextMessage>({
|
||||||
|
name: 'Danmaku.PushEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const UnshiftEvent = domain.event<TextMessage>({
|
||||||
|
name: 'Danmaku.UnshiftEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ClearEvent = domain.event({
|
||||||
|
name: 'Danmaku.ClearEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const MountEvent = domain.event<HTMLElement>({
|
||||||
|
name: 'Danmaku.MountEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const DestroyEvent = domain.event({
|
||||||
|
name: 'Danmaku.DestroyEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'Danmaku.OnUserInfoEffect',
|
||||||
|
impl: ({ fromEvent }) => {
|
||||||
|
const onUserInfo$ = fromEvent(userInfoDomain.event.UpdateUserInfoEvent)
|
||||||
|
return onUserInfo$.pipe(
|
||||||
|
map((userInfo) => {
|
||||||
|
return userInfo?.danmakuEnabled ? EnableCommand() : DisableCommand()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
IsMountedQuery,
|
||||||
|
IsEnabledQuery
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
EnableCommand,
|
||||||
|
DisableCommand,
|
||||||
|
PushCommand,
|
||||||
|
UnshiftCommand,
|
||||||
|
ClearCommand,
|
||||||
|
MountCommand,
|
||||||
|
DestroyCommand
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
PushEvent,
|
||||||
|
UnshiftEvent,
|
||||||
|
ClearEvent,
|
||||||
|
MountEvent,
|
||||||
|
DestroyEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DanmakuDomain
|
|
@ -48,8 +48,8 @@ const MessageListDomain = Remesh.domain({
|
||||||
key: (message) => message.id
|
key: (message) => message.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const MessageListLoadStatusModule = StatusModule(domain, {
|
const LoadStatusModule = StatusModule(domain, {
|
||||||
name: 'MessageListLoadStatusModule'
|
name: 'Message.ListLoadStatusModule'
|
||||||
})
|
})
|
||||||
|
|
||||||
const ListQuery = MessageListModule.query.ItemListQuery
|
const ListQuery = MessageListModule.query.ItemListQuery
|
||||||
|
@ -58,6 +58,8 @@ const MessageListDomain = Remesh.domain({
|
||||||
|
|
||||||
const HasItemQuery = MessageListModule.query.HasItemByKeyQuery
|
const HasItemQuery = MessageListModule.query.HasItemByKeyQuery
|
||||||
|
|
||||||
|
const LoadIsFinishedQuery = LoadStatusModule.query.IsFinishedQuery
|
||||||
|
|
||||||
const ChangeListEvent = domain.event({
|
const ChangeListEvent = domain.event({
|
||||||
name: 'MessageList.ChangeListEvent',
|
name: 'MessageList.ChangeListEvent',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
|
@ -144,16 +146,14 @@ const MessageListDomain = Remesh.domain({
|
||||||
|
|
||||||
storageEffect
|
storageEffect
|
||||||
.set(SyncToStorageEvent)
|
.set(SyncToStorageEvent)
|
||||||
.get<
|
.get<Message[]>((value) => [SyncToStateCommand(value ?? []), LoadStatusModule.command.SetFinishedCommand()])
|
||||||
Message[]
|
|
||||||
>((value) => [SyncToStateCommand(value ?? []), MessageListLoadStatusModule.command.SetFinishedCommand()])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
HasItemQuery,
|
HasItemQuery,
|
||||||
ItemQuery,
|
ItemQuery,
|
||||||
ListQuery,
|
ListQuery,
|
||||||
MessageListLoadIsFinishedQuery: MessageListLoadStatusModule.query.IsFinishedQuery
|
LoadIsFinishedQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
CreateItemCommand,
|
CreateItemCommand,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { map, merge, of, EMPTY, mergeMap, fromEvent, Observable, tap, fromEventPattern } from 'rxjs'
|
import { map, merge, of, EMPTY, mergeMap, fromEvent, fromEventPattern } from 'rxjs'
|
||||||
import { NormalMessage, type MessageUser } from './MessageList'
|
import { NormalMessage, type MessageUser } from './MessageList'
|
||||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||||
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
||||||
|
@ -7,7 +7,8 @@ import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import { desert, upsert } from '@/utils'
|
import { desert, upsert } from '@/utils'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import StatusModule from '@/domain/modules/Status'
|
import StatusModule from '@/domain/modules/Status'
|
||||||
import { ToastExtern } from './externs/Toast'
|
import { ToastExtern } from '@/domain/externs/Toast'
|
||||||
|
import DanmakuDomain from '@/domain/Danmaku'
|
||||||
|
|
||||||
export { MessageType }
|
export { MessageType }
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ const RoomDomain = Remesh.domain({
|
||||||
impl: (domain) => {
|
impl: (domain) => {
|
||||||
const messageListDomain = domain.getDomain(MessageListDomain())
|
const messageListDomain = domain.getDomain(MessageListDomain())
|
||||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
|
const danmakuDomain = domain.getDomain(DanmakuDomain())
|
||||||
const peerRoom = domain.getExtern(PeerRoomExtern)
|
const peerRoom = domain.getExtern(PeerRoomExtern)
|
||||||
const toast = domain.getExtern(ToastExtern)
|
const toast = domain.getExtern(ToastExtern)
|
||||||
|
|
||||||
|
@ -65,12 +67,12 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const RoomJoinStatusModule = StatusModule(domain, {
|
const JoinStatusModule = StatusModule(domain, {
|
||||||
name: 'RoomJoinStatusModule'
|
name: 'Room.JoinStatusModule'
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserListState = domain.state<RoomUser[]>({
|
const UserListState = domain.state<RoomUser[]>({
|
||||||
name: 'RoomUserListState',
|
name: 'Room.UserListState',
|
||||||
default: []
|
default: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -81,8 +83,10 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const JoinIsFinishedQuery = JoinStatusModule.query.IsFinishedQuery
|
||||||
|
|
||||||
const JoinRoomCommand = domain.command({
|
const JoinRoomCommand = domain.command({
|
||||||
name: 'RoomJoinRoomCommand',
|
name: 'Room.JoinRoomCommand',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
peerRoom.joinRoom()
|
peerRoom.joinRoom()
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
@ -101,14 +105,14 @@ const RoomDomain = Remesh.domain({
|
||||||
type: MessageType.Prompt,
|
type: MessageType.Prompt,
|
||||||
date: Date.now()
|
date: Date.now()
|
||||||
}),
|
}),
|
||||||
RoomJoinStatusModule.command.SetFinishedCommand(),
|
JoinStatusModule.command.SetFinishedCommand(),
|
||||||
JoinRoomEvent(peerRoom.roomId)
|
JoinRoomEvent(peerRoom.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const LeaveRoomCommand = domain.command({
|
const LeaveRoomCommand = domain.command({
|
||||||
name: 'RoomLeaveRoomCommand',
|
name: 'Room.LeaveRoomCommand',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
peerRoom.leaveRoom()
|
peerRoom.leaveRoom()
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
@ -126,16 +130,21 @@ const RoomDomain = Remesh.domain({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||||
}),
|
}),
|
||||||
RoomJoinStatusModule.command.SetInitialCommand(),
|
JoinStatusModule.command.SetInitialCommand(),
|
||||||
LeaveRoomEvent(peerRoom.roomId)
|
LeaveRoomEvent(peerRoom.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendTextMessageCommand = domain.command({
|
const SendTextMessageCommand = domain.command({
|
||||||
name: 'RoomSendTextMessageCommand',
|
name: 'Room.SendTextMessageCommand',
|
||||||
impl: ({ get }, message: string) => {
|
impl: ({ get }, message: string) => {
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const {
|
||||||
|
id: userId,
|
||||||
|
name: username,
|
||||||
|
avatar: userAvatar,
|
||||||
|
danmakuEnabled
|
||||||
|
} = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
|
||||||
const textMessage: TextMessage = {
|
const textMessage: TextMessage = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
|
@ -154,13 +163,17 @@ const RoomDomain = Remesh.domain({
|
||||||
hateUsers: []
|
hateUsers: []
|
||||||
}
|
}
|
||||||
|
|
||||||
peerRoom.sendMessage<RoomMessage>(textMessage)
|
peerRoom.sendMessage(textMessage)
|
||||||
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
|
return [
|
||||||
|
messageListDomain.command.CreateItemCommand(listMessage),
|
||||||
|
danmakuEnabled ? PushDanmakuCommand(textMessage) : null,
|
||||||
|
SendTextMessageEvent(textMessage)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendLikeMessageCommand = domain.command({
|
const SendLikeMessageCommand = domain.command({
|
||||||
name: 'RoomSendLikeMessageCommand',
|
name: 'Room.SendLikeMessageCommand',
|
||||||
impl: ({ get }, messageId: string) => {
|
impl: ({ get }, messageId: string) => {
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
||||||
|
@ -176,13 +189,13 @@ const RoomDomain = Remesh.domain({
|
||||||
...localMessage,
|
...localMessage,
|
||||||
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage<RoomMessage>(likeMessage)
|
peerRoom.sendMessage(likeMessage)
|
||||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
|
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendHateMessageCommand = domain.command({
|
const SendHateMessageCommand = domain.command({
|
||||||
name: 'RoomSendHateMessageCommand',
|
name: 'Room.SendHateMessageCommand',
|
||||||
impl: ({ get }, messageId: string) => {
|
impl: ({ get }, messageId: string) => {
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
||||||
|
@ -198,13 +211,13 @@ const RoomDomain = Remesh.domain({
|
||||||
...localMessage,
|
...localMessage,
|
||||||
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage<RoomMessage>(hateMessage)
|
peerRoom.sendMessage(hateMessage)
|
||||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
|
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendJoinMessageCommand = domain.command({
|
const SendJoinMessageCommand = domain.command({
|
||||||
name: 'RoomSendJoinMessageCommand',
|
name: 'Room.SendJoinMessageCommand',
|
||||||
impl: ({ get }, targetPeerId: string) => {
|
impl: ({ get }, targetPeerId: string) => {
|
||||||
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
|
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
|
||||||
|
|
||||||
|
@ -214,13 +227,13 @@ const RoomDomain = Remesh.domain({
|
||||||
type: SendType.Join
|
type: SendType.Join
|
||||||
}
|
}
|
||||||
|
|
||||||
peerRoom.sendMessage<RoomMessage>(syncUserMessage, targetPeerId)
|
peerRoom.sendMessage(syncUserMessage, targetPeerId)
|
||||||
return [SendJoinMessageEvent(syncUserMessage)]
|
return [SendJoinMessageEvent(syncUserMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const UpdateUserListCommand = domain.command({
|
const UpdateUserListCommand = domain.command({
|
||||||
name: 'RoomUpdateUserListCommand',
|
name: 'Room.UpdateUserListCommand',
|
||||||
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => {
|
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => {
|
||||||
const userList = get(UserListState())
|
const userList = get(UserListState())
|
||||||
if (action.type === 'create') {
|
if (action.type === 'create') {
|
||||||
|
@ -231,44 +244,58 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const PushDanmakuCommand = domain.command({
|
||||||
|
name: 'Room.PushDanmakuCommand',
|
||||||
|
impl: (_, message: TextMessage) => {
|
||||||
|
return [danmakuDomain.command.PushCommand(message)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// const UnshiftDanmakuCommand = domain.command({
|
||||||
|
// name: 'Room.PushDanmakuCommand',
|
||||||
|
// impl: (_, message: TextMessage) => {
|
||||||
|
// return [danmakuDomain.command.UnshiftCommand(message)]
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
const SendJoinMessageEvent = domain.event<SyncUserMessage>({
|
const SendJoinMessageEvent = domain.event<SyncUserMessage>({
|
||||||
name: 'RoomSendJoinMessageEvent'
|
name: 'Room.SendJoinMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendTextMessageEvent = domain.event<TextMessage>({
|
const SendTextMessageEvent = domain.event<TextMessage>({
|
||||||
name: 'RoomSendTextMessageEvent'
|
name: 'Room.SendTextMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendLikeMessageEvent = domain.event<LikeMessage>({
|
const SendLikeMessageEvent = domain.event<LikeMessage>({
|
||||||
name: 'RoomSendLikeMessageEvent'
|
name: 'Room.SendLikeMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendHateMessageEvent = domain.event<HateMessage>({
|
const SendHateMessageEvent = domain.event<HateMessage>({
|
||||||
name: 'RoomSendHateMessageEvent'
|
name: 'Room.SendHateMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const JoinRoomEvent = domain.event<string>({
|
const JoinRoomEvent = domain.event<string>({
|
||||||
name: 'RoomJoinRoomEvent'
|
name: 'Room.JoinRoomEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const LeaveRoomEvent = domain.event<string>({
|
const LeaveRoomEvent = domain.event<string>({
|
||||||
name: 'RoomLeaveRoomEvent'
|
name: 'Room.LeaveRoomEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const OnMessageEvent = domain.event<RoomMessage>({
|
const OnMessageEvent = domain.event<RoomMessage>({
|
||||||
name: 'RoomOnMessageEvent'
|
name: 'Room.OnMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const OnJoinRoomEvent = domain.event<string>({
|
const OnJoinRoomEvent = domain.event<string>({
|
||||||
name: 'RoomOnJoinRoomEvent'
|
name: 'Room.OnJoinRoomEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const OnLeaveRoomEvent = domain.event<string>({
|
const OnLeaveRoomEvent = domain.event<string>({
|
||||||
name: 'RoomOnLeaveRoomEvent'
|
name: 'Room.OnLeaveRoomEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnJoinRoomEffect',
|
name: 'Room.OnJoinRoomEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
const onJoinRoom$ = fromEventPattern<string>(peerRoom.onJoinRoom).pipe(
|
const onJoinRoom$ = fromEventPattern<string>(peerRoom.onJoinRoom).pipe(
|
||||||
mergeMap((peerId) => {
|
mergeMap((peerId) => {
|
||||||
|
@ -285,14 +312,16 @@ const RoomDomain = Remesh.domain({
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnMessageEffect',
|
name: 'Room.OnMessageEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onMessage$ = fromEventPattern<RoomMessage>(peerRoom.onMessage).pipe(
|
const onMessage$ = fromEventPattern<RoomMessage>(peerRoom.onMessage).pipe(
|
||||||
mergeMap((message) => {
|
mergeMap((message) => {
|
||||||
// console.log('onMessage', message)
|
// console.log('onMessage', message)
|
||||||
|
const { danmakuEnabled } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
|
||||||
const messageEvent$ = of(OnMessageEvent(message))
|
const messageEvent$ = of(OnMessageEvent(message))
|
||||||
|
|
||||||
const commandEvent$ = (() => {
|
const messageCommand$ = (() => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case SendType.Join: {
|
case SendType.Join: {
|
||||||
const userList = get(UserListQuery())
|
const userList = get(UserListQuery())
|
||||||
|
@ -326,7 +355,8 @@ const RoomDomain = Remesh.domain({
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
likeUsers: [],
|
likeUsers: [],
|
||||||
hateUsers: []
|
hateUsers: []
|
||||||
})
|
}),
|
||||||
|
danmakuEnabled ? PushDanmakuCommand(message) : null
|
||||||
)
|
)
|
||||||
case SendType.Like:
|
case SendType.Like:
|
||||||
case SendType.Hate: {
|
case SendType.Hate: {
|
||||||
|
@ -355,7 +385,8 @@ const RoomDomain = Remesh.domain({
|
||||||
return EMPTY
|
return EMPTY
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
return merge(messageEvent$, commandEvent$)
|
|
||||||
|
return merge(messageEvent$, messageCommand$)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return onMessage$
|
return onMessage$
|
||||||
|
@ -363,7 +394,7 @@ const RoomDomain = Remesh.domain({
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnLeaveRoomEffect',
|
name: 'Room.OnLeaveRoomEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
|
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
|
||||||
map((peerId) => {
|
map((peerId) => {
|
||||||
|
@ -392,7 +423,7 @@ const RoomDomain = Remesh.domain({
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnErrorEffect',
|
name: 'Room.OnErrorEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
const onRoomError$ = fromEventPattern<Error>(peerRoom.onError).pipe(
|
const onRoomError$ = fromEventPattern<Error>(peerRoom.onError).pipe(
|
||||||
map((error) => {
|
map((error) => {
|
||||||
|
@ -407,11 +438,11 @@ const RoomDomain = Remesh.domain({
|
||||||
|
|
||||||
// TODO: Move this to a service worker in the future, so we don't need to send a leave room message every time the page refreshes
|
// TODO: Move this to a service worker in the future, so we don't need to send a leave room message every time the page refreshes
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnUnloadEffect',
|
name: 'Room.OnUnloadEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const beforeUnload$ = fromEvent(window, 'beforeunload').pipe(
|
const beforeUnload$ = fromEvent(window, 'beforedestroy').pipe(
|
||||||
map(() => {
|
map(() => {
|
||||||
return get(RoomJoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null
|
return get(JoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return beforeUnload$
|
return beforeUnload$
|
||||||
|
@ -422,7 +453,7 @@ const RoomDomain = Remesh.domain({
|
||||||
query: {
|
query: {
|
||||||
PeerIdQuery,
|
PeerIdQuery,
|
||||||
UserListQuery,
|
UserListQuery,
|
||||||
RoomJoinIsFinishedQuery: RoomJoinStatusModule.query.IsFinishedQuery
|
JoinIsFinishedQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
JoinRoomCommand,
|
JoinRoomCommand,
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface UserInfo {
|
||||||
avatar: string
|
avatar: string
|
||||||
createTime: number
|
createTime: number
|
||||||
themeMode: 'system' | 'light' | 'dark'
|
themeMode: 'system' | 'light' | 'dark'
|
||||||
|
danmakuEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserInfoDomain = Remesh.domain({
|
const UserInfoDomain = Remesh.domain({
|
||||||
|
@ -27,10 +28,10 @@ const UserInfoDomain = Remesh.domain({
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserInfoLoadStatusModule = StatusModule(domain, {
|
const UserInfoLoadStatusModule = StatusModule(domain, {
|
||||||
name: 'UserInfoLoadStatusModule'
|
name: 'UserInfo.LoadStatusModule'
|
||||||
})
|
})
|
||||||
const UserInfoSetStatusModule = StatusModule(domain, {
|
const UserInfoSetStatusModule = StatusModule(domain, {
|
||||||
name: 'UserInfoSetStatusModule'
|
name: 'UserInfo.SetStatusModule'
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserInfoQuery = domain.query({
|
const UserInfoQuery = domain.query({
|
||||||
|
@ -43,8 +44,6 @@ const UserInfoDomain = Remesh.domain({
|
||||||
const UpdateUserInfoCommand = domain.command({
|
const UpdateUserInfoCommand = domain.command({
|
||||||
name: 'UserInfo.UpdateUserInfoCommand',
|
name: 'UserInfo.UpdateUserInfoCommand',
|
||||||
impl: (_, userInfo: UserInfo | null) => {
|
impl: (_, userInfo: UserInfo | null) => {
|
||||||
console.log('111', userInfo)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UserInfoState().new(userInfo),
|
UserInfoState().new(userInfo),
|
||||||
UpdateUserInfoEvent(),
|
UpdateUserInfoEvent(),
|
||||||
|
@ -81,16 +80,16 @@ const UserInfoDomain = Remesh.domain({
|
||||||
UserInfoState().new(userInfo),
|
UserInfoState().new(userInfo),
|
||||||
UpdateUserInfoEvent(),
|
UpdateUserInfoEvent(),
|
||||||
SyncToStateEvent(userInfo),
|
SyncToStateEvent(userInfo),
|
||||||
userInfo && UserInfoSetStatusModule.command.SetFinishedCommand()
|
userInfo
|
||||||
|
? UserInfoSetStatusModule.command.SetFinishedCommand()
|
||||||
|
: UserInfoSetStatusModule.command.SetInitialCommand()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
storageEffect
|
storageEffect
|
||||||
.set(SyncToStorageEvent)
|
.set(SyncToStorageEvent)
|
||||||
.get<UserInfo>((value) => {
|
.get<UserInfo>((value) => [SyncToStateCommand(value), UserInfoLoadStatusModule.command.SetFinishedCommand()])
|
||||||
return [SyncToStateCommand(value), UserInfoLoadStatusModule.command.SetFinishedCommand()]
|
|
||||||
})
|
|
||||||
.watch<UserInfo>((value) => [SyncToStateCommand(value)])
|
.watch<UserInfo>((value) => [SyncToStateCommand(value)])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
30
src/domain/externs/Danmaku.ts
Normal file
30
src/domain/externs/Danmaku.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
import { TextMessage } from '../Room'
|
||||||
|
|
||||||
|
export interface Danmaku {
|
||||||
|
push: (message: TextMessage) => void
|
||||||
|
unshift: (message: TextMessage) => void
|
||||||
|
clear: () => void
|
||||||
|
mount: (root: HTMLElement) => void
|
||||||
|
destroy: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DanmakuExtern = Remesh.extern<Danmaku>({
|
||||||
|
default: {
|
||||||
|
mount: () => {
|
||||||
|
throw new Error('"mount" not implemented.')
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
throw new Error('"destroy" not implemented.')
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
throw new Error('"clear" not implemented.')
|
||||||
|
},
|
||||||
|
push: () => {
|
||||||
|
throw new Error('"push" not implemented.')
|
||||||
|
},
|
||||||
|
unshift: () => {
|
||||||
|
throw new Error('"unshift" not implemented.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,13 +1,12 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
|
import { RoomMessage } from '../Room'
|
||||||
export type PeerMessage = object | Blob | ArrayBuffer | ArrayBufferView
|
|
||||||
|
|
||||||
export interface PeerRoom {
|
export interface PeerRoom {
|
||||||
readonly peerId: string
|
readonly peerId: string
|
||||||
readonly roomId: string
|
readonly roomId: string
|
||||||
joinRoom: () => PeerRoom
|
joinRoom: () => PeerRoom
|
||||||
sendMessage: <T extends PeerMessage>(message: T, id?: string) => PeerRoom
|
sendMessage: (message: RoomMessage, id?: string) => PeerRoom
|
||||||
onMessage: <T extends PeerMessage>(callback: (message: T) => void) => PeerRoom
|
onMessage: (callback: (message: RoomMessage) => void) => PeerRoom
|
||||||
leaveRoom: () => PeerRoom
|
leaveRoom: () => PeerRoom
|
||||||
onJoinRoom: (callback: (id: string) => void) => PeerRoom
|
onJoinRoom: (callback: (id: string) => void) => PeerRoom
|
||||||
onLeaveRoom: (callback: (id: string) => void) => PeerRoom
|
onLeaveRoom: (callback: (id: string) => void) => PeerRoom
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { type Promisable } from 'type-fest'
|
|
||||||
|
|
||||||
export type StorageValue = null | string | number | boolean | object
|
export type StorageValue = null | string | number | boolean | object
|
||||||
export type WatchEvent = 'update' | 'remove'
|
export type WatchEvent = 'update' | 'remove'
|
||||||
export type WatchCallback = (event: WatchEvent, key: string) => any
|
export type WatchCallback = (event: WatchEvent, key: string) => any
|
||||||
export type Unwatch = () => Promisable<void>
|
export type Unwatch = () => Promise<void>
|
||||||
|
|
||||||
export interface Storage {
|
export interface Storage {
|
||||||
name: string
|
name: string
|
||||||
|
@ -31,10 +30,10 @@ export const IndexDBStorageExtern = Remesh.extern<Storage>({
|
||||||
clear: async () => {
|
clear: async () => {
|
||||||
throw new Error('"clear" not implemented.')
|
throw new Error('"clear" not implemented.')
|
||||||
},
|
},
|
||||||
watch: () => {
|
watch: async () => {
|
||||||
throw new Error('"watch" not implemented.')
|
throw new Error('"watch" not implemented.')
|
||||||
},
|
},
|
||||||
unwatch: () => {
|
unwatch: async () => {
|
||||||
throw new Error('"unwatch" not implemented.')
|
throw new Error('"unwatch" not implemented.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,10 +54,10 @@ export const BrowserSyncStorageExtern = Remesh.extern<Storage>({
|
||||||
clear: async () => {
|
clear: async () => {
|
||||||
throw new Error('"clear" not implemented.')
|
throw new Error('"clear" not implemented.')
|
||||||
},
|
},
|
||||||
watch: () => {
|
watch: async () => {
|
||||||
throw new Error('"watch" not implemented.')
|
throw new Error('"watch" not implemented.')
|
||||||
},
|
},
|
||||||
unwatch: () => {
|
unwatch: async () => {
|
||||||
throw new Error('"unwatch" not implemented.')
|
throw new Error('"unwatch" not implemented.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
src/domain/impls/Danmaku.ts
Normal file
80
src/domain/impls/Danmaku.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { DanmakuExtern } from '@/domain/externs/Danmaku'
|
||||||
|
|
||||||
|
import { TextMessage } from '@/domain/Room'
|
||||||
|
import { createElement } from 'react'
|
||||||
|
import _Danmaku from 'danmaku'
|
||||||
|
import DanmakuMessage from '@/app/content/components/DanmakuMessage'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
// import { create } from 'danmaku'
|
||||||
|
// const manager = create<TextMessage>({
|
||||||
|
// trackHeight: '20%',
|
||||||
|
// plugin: {
|
||||||
|
// init(manager) {
|
||||||
|
// 'shadow shadow-slate-200 bg-slate-100'.split(' ').forEach((c) => {
|
||||||
|
// manager.container.node.classList.add(c)
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// $createNode(dm) {
|
||||||
|
// if (!dm.node) return
|
||||||
|
// createRoot(dm.node).render(createElement(DanmakuMessage, { data: dm.data }))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// manager.mount(document.body)
|
||||||
|
// manager.startPlaying()
|
||||||
|
|
||||||
|
export class Danmaku {
|
||||||
|
private container?: Element
|
||||||
|
private _danmaku?: _Danmaku
|
||||||
|
|
||||||
|
mount(container: HTMLElement) {
|
||||||
|
this.container = container
|
||||||
|
|
||||||
|
this._danmaku = new _Danmaku({
|
||||||
|
container
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (!this.container) {
|
||||||
|
throw new Error('Danmaku not mounted')
|
||||||
|
}
|
||||||
|
this._danmaku!.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
push(message: TextMessage) {
|
||||||
|
if (!this.container) {
|
||||||
|
throw new Error('Danmaku not mounted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.createElement('div')
|
||||||
|
createRoot(root).render(createElement(DanmakuMessage, { data: message }))
|
||||||
|
|
||||||
|
// Wait for React render to complete
|
||||||
|
requestIdleCallback(() => {
|
||||||
|
this._danmaku!.emit({
|
||||||
|
render() {
|
||||||
|
return root.firstElementChild! as HTMLElement
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unshift(message: TextMessage) {
|
||||||
|
if (!this.container) {
|
||||||
|
throw new Error('Danmaku not mounted')
|
||||||
|
}
|
||||||
|
// console.log(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
if (!this.container) {
|
||||||
|
throw new Error('Danmaku not mounted')
|
||||||
|
}
|
||||||
|
this._danmaku!.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DanmakuImpl = DanmakuExtern.impl(new Danmaku())
|
|
@ -2,9 +2,10 @@ import { type DataPayload, type Room, joinRoom, selfId } from 'trystero'
|
||||||
|
|
||||||
// import { joinRoom } from 'trystero/firebase'
|
// import { joinRoom } from 'trystero/firebase'
|
||||||
|
|
||||||
import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom'
|
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||||
import { stringToHex } from '@/utils'
|
import { stringToHex } from '@/utils'
|
||||||
import EventHub from '@resreq/event-hub'
|
import EventHub from '@resreq/event-hub'
|
||||||
|
import { RoomMessage } from '../Room'
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
peerId?: string
|
peerId?: string
|
||||||
|
@ -44,37 +45,37 @@ class PeerRoom extends EventHub {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage<T extends PeerMessage>(message: T, id?: string) {
|
sendMessage(message: RoomMessage, id?: string) {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.once('action', () => {
|
this.once('action', () => {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.emit('error', new Error('Room not joined'))
|
this.emit('error', new Error('Room not joined'))
|
||||||
} else {
|
} else {
|
||||||
const [send] = this.room.makeAction('MESSAGE')
|
const [send] = this.room.makeAction('MESSAGE')
|
||||||
send(message as DataPayload, id)
|
send(message as any as DataPayload, id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const [send] = this.room.makeAction('MESSAGE')
|
const [send] = this.room.makeAction('MESSAGE')
|
||||||
send(message as DataPayload, id)
|
send(message as any as DataPayload, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
|
onMessage(callback: (message: RoomMessage) => void) {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.once('action', () => {
|
this.once('action', () => {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.emit('error', new Error('Room not joined'))
|
this.emit('error', new Error('Room not joined'))
|
||||||
} else {
|
} else {
|
||||||
const [, on] = this.room.makeAction('MESSAGE')
|
const [, on] = this.room.makeAction('MESSAGE')
|
||||||
on((message) => callback(message as T))
|
on((message) => callback(message as any as RoomMessage))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const [, on] = this.room.makeAction('MESSAGE')
|
const [, on] = this.room.makeAction('MESSAGE')
|
||||||
on((message) => callback(message as T))
|
on((message) => callback(message as any as RoomMessage))
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Artico, Room } from '@rtco/client'
|
import { Artico, Room } from '@rtco/client'
|
||||||
|
|
||||||
import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom'
|
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||||
import { stringToHex } from '@/utils'
|
import { stringToHex } from '@/utils'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import EventHub from '@resreq/event-hub'
|
import EventHub from '@resreq/event-hub'
|
||||||
|
import { RoomMessage } from '../Room'
|
||||||
export interface Config {
|
export interface Config {
|
||||||
peerId?: string
|
peerId?: string
|
||||||
roomId: string
|
roomId: string
|
||||||
|
@ -43,7 +44,7 @@ class PeerRoom extends EventHub {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage<T extends PeerMessage>(message: T, id?: string) {
|
sendMessage(message: RoomMessage, id?: string) {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.once('action', () => {
|
this.once('action', () => {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
|
@ -58,17 +59,17 @@ class PeerRoom extends EventHub {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
|
onMessage(callback: (message: RoomMessage) => void) {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.once('action', () => {
|
this.once('action', () => {
|
||||||
if (!this.room) {
|
if (!this.room) {
|
||||||
this.emit('error', new Error('Room not joined'))
|
this.emit('error', new Error('Room not joined'))
|
||||||
} else {
|
} else {
|
||||||
this.room.on('message', (message) => callback(JSON.parse(message) as T))
|
this.room.on('message', (message) => callback(JSON.parse(message) as RoomMessage))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.room.on('message', (message) => callback(JSON.parse(message) as T))
|
this.room.on('message', (message) => callback(JSON.parse(message) as RoomMessage))
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import indexedDbDriver from 'unstorage/drivers/indexedb'
|
||||||
import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
||||||
import { STORAGE_NAME } from '@/constants/config'
|
import { STORAGE_NAME } from '@/constants/config'
|
||||||
import { webExtensionDriver } from '@/utils/webExtensionDriver'
|
import { webExtensionDriver } from '@/utils/webExtensionDriver'
|
||||||
|
import { browser } from 'wxt/browser'
|
||||||
|
import { Storage } from '@/domain/externs/Storage'
|
||||||
|
|
||||||
export const indexDBStorage = createStorage({
|
export const indexDBStorage = createStorage({
|
||||||
driver: indexedDbDriver({ base: `${STORAGE_NAME}:` })
|
driver: indexedDbDriver({ base: `${STORAGE_NAME}:` })
|
||||||
|
@ -18,7 +20,7 @@ export const IndexDBStorageImpl = IndexDBStorageExtern.impl({
|
||||||
set: indexDBStorage.setItem,
|
set: indexDBStorage.setItem,
|
||||||
remove: indexDBStorage.removeItem,
|
remove: indexDBStorage.removeItem,
|
||||||
clear: indexDBStorage.clear,
|
clear: indexDBStorage.clear,
|
||||||
watch: indexDBStorage.watch,
|
watch: indexDBStorage.watch as Storage['watch'],
|
||||||
unwatch: indexDBStorage.unwatch
|
unwatch: indexDBStorage.unwatch
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -28,6 +30,26 @@ export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({
|
||||||
set: browserSyncStorage.setItem,
|
set: browserSyncStorage.setItem,
|
||||||
remove: browserSyncStorage.removeItem,
|
remove: browserSyncStorage.removeItem,
|
||||||
clear: browserSyncStorage.clear,
|
clear: browserSyncStorage.clear,
|
||||||
watch: browserSyncStorage.watch,
|
watch: browserSyncStorage.watch as Storage['watch'],
|
||||||
unwatch: browserSyncStorage.unwatch
|
unwatch: browserSyncStorage.unwatch
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({
|
||||||
|
// name: STORAGE_NAME,
|
||||||
|
// get: async (key: string) => {
|
||||||
|
// const res = await browser.storage.sync.get(key)
|
||||||
|
// return res[key] ?? null
|
||||||
|
// },
|
||||||
|
// set: async (key, value) => {
|
||||||
|
// await browser.storage.sync.set({ [key]: value ?? null })
|
||||||
|
// },
|
||||||
|
// remove: browserSyncStorage.removeItem,
|
||||||
|
// clear: browserSyncStorage.clear,
|
||||||
|
// watch: async (callback) => {
|
||||||
|
// browser.storage.sync.onChanged.addListener(callback)
|
||||||
|
// return async () => {
|
||||||
|
// return browser.storage.sync.onChanged.removeListener(callback)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// unwatch: browserSyncStorage.unwatch
|
||||||
|
// })
|
||||||
|
|
|
@ -1,18 +1,7 @@
|
||||||
import { type RemeshEvent, type RemeshAction, type RemeshDomainContext, type RemeshExtern } from 'remesh'
|
import { type RemeshEvent, type RemeshAction, type RemeshDomainContext, type RemeshExtern } from 'remesh'
|
||||||
import { defer, from, fromEventPattern, map, Observable, switchMap } from 'rxjs'
|
import { defer, from, map, Observable, switchMap } from 'rxjs'
|
||||||
import { type Promisable } from 'type-fest'
|
|
||||||
|
|
||||||
export type StorageValue = null | string | number | boolean | object
|
import { Storage, StorageValue } from '@/domain/externs/Storage'
|
||||||
export type WatchEvent = 'update' | 'remove'
|
|
||||||
export type WatchCallback = (event: WatchEvent, key: string) => any
|
|
||||||
export type Unwatch = () => Promisable<void>
|
|
||||||
|
|
||||||
export interface Storage {
|
|
||||||
get: <T extends StorageValue>(key: string) => Promise<T | null>
|
|
||||||
set: <T extends StorageValue>(key: string, value: T) => Promise<void>
|
|
||||||
watch: (callback: WatchCallback) => Promise<Unwatch>
|
|
||||||
unwatch?: Unwatch
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
domain: RemeshDomainContext
|
domain: RemeshDomainContext
|
||||||
|
@ -60,20 +49,14 @@ export default class StorageEffect {
|
||||||
this.domain.effect({
|
this.domain.effect({
|
||||||
name: 'WatchStorageToStateEffect',
|
name: 'WatchStorageToStateEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
return defer(() => {
|
// TODO: Report the bug to https://github.com/unjs/unstorage
|
||||||
let unwatch: Unwatch
|
return new Observable((observer) => {
|
||||||
return new Observable<void>((observer) => {
|
const unwatchPromise = this.storage.watch(() => observer.next())
|
||||||
this.storage
|
return () => unwatchPromise.then((unwatch) => unwatch())
|
||||||
.watch(() => observer.next())
|
}).pipe(
|
||||||
.then((_unwatch) => {
|
switchMap(() => from(this.storage.get<T | null>(this.key))),
|
||||||
unwatch = _unwatch
|
map(callback)
|
||||||
})
|
)
|
||||||
return () => unwatch?.()
|
|
||||||
}).pipe(
|
|
||||||
switchMap(() => from(this.storage.get<T | null>(this.key))),
|
|
||||||
map(callback)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { type Driver, type WatchCallback, defineDriver } from 'unstorage'
|
import { type Driver, type WatchCallback, defineDriver } from 'unstorage'
|
||||||
import browser, { type Storage as BrowserStorage } from 'webextension-polyfill'
|
import { browser, type Storage as BrowserStorage } from 'wxt/browser'
|
||||||
|
|
||||||
export interface WebExtensionDriverOptions {
|
export interface WebExtensionDriverOptions {
|
||||||
storageArea: 'sync' | 'local' | 'managed' | 'session'
|
storageArea: 'sync' | 'local' | 'managed' | 'session'
|
||||||
|
@ -67,6 +67,7 @@ export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = d
|
||||||
watch(callback) {
|
watch(callback) {
|
||||||
checkPermission()
|
checkPermission()
|
||||||
_listeners.add(callback)
|
_listeners.add(callback)
|
||||||
|
|
||||||
if (_listeners.size === 1) {
|
if (_listeners.size === 1) {
|
||||||
browser.storage[opts.storageArea].onChanged.addListener(_storageListener)
|
browser.storage[opts.storageArea].onChanged.addListener(_storageListener)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default defineConfig({
|
||||||
runner: {
|
runner: {
|
||||||
startUrls: ['https://www.example.com/']
|
startUrls: ['https://www.example.com/']
|
||||||
},
|
},
|
||||||
manifest: ({ browser, manifestVersion }) => {
|
manifest: ({ browser }) => {
|
||||||
const common = {
|
const common = {
|
||||||
name: displayName,
|
name: displayName,
|
||||||
permissions: ['storage'],
|
permissions: ['storage'],
|
||||||
|
|
Loading…
Reference in a new issue