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",
|
||||
"@perfsee/jsonr": "^1.13.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-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
@ -64,8 +65,9 @@
|
|||
"@webext-core/proxy-service": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"danmaku": "^2.0.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.7.0",
|
||||
"framer-motion": "^11.9.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.446.0",
|
||||
"nanoid": "^5.0.7",
|
||||
|
@ -86,14 +88,13 @@
|
|||
"tailwind-merge": "^2.5.2",
|
||||
"trystero": "^0.20.0",
|
||||
"type-fest": "^4.26.1",
|
||||
"unstorage": "^1.12.0",
|
||||
"valibot": "^0.42.1",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
"unstorage": "1.11.0",
|
||||
"valibot": "^0.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^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",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
|
@ -101,12 +102,11 @@
|
|||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.7.2",
|
||||
"@types/react": "^18.3.9",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.11.1",
|
||||
|
@ -122,7 +122,7 @@
|
|||
"postcss-rem-to-responsive-pixel": "^6.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^5.0.10",
|
||||
"semantic-release": "^24.1.1",
|
||||
"semantic-release": "^24.1.2",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"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 Setup from '@/app/content/views/Setup'
|
||||
import MessageListDomain from '@/domain/MessageList'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
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 LogoIcon0 from '@/assets/images/logo-0.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 { getDay } from 'date-fns'
|
||||
import DanmakuContainer from './components/DanmakuContainer'
|
||||
import DanmakuDomain from '@/domain/Danmaku'
|
||||
import { browser } from 'wxt/browser'
|
||||
|
||||
export default function App() {
|
||||
const send = useRemeshSend()
|
||||
const roomDomain = useRemeshDomain(RoomDomain())
|
||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||
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 userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.MessageListLoadIsFinishedQuery())
|
||||
|
||||
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.LoadIsFinishedQuery())
|
||||
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
||||
|
||||
const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())]
|
||||
|
@ -63,6 +68,15 @@ export default function App() {
|
|||
getAppOpenStatus()
|
||||
}, [])
|
||||
|
||||
const danmakuContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
danmakuIsEnabled && send(danmakuDomain.command.MountCommand(danmakuContainerRef.current!))
|
||||
return () => {
|
||||
danmakuIsEnabled && send(danmakuDomain.command.DestroyCommand())
|
||||
}
|
||||
}, [danmakuIsEnabled])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppContainer open={appOpen}>
|
||||
|
@ -75,6 +89,7 @@ export default function App() {
|
|||
<AppButton onClick={handleToggleApp}>
|
||||
<DayLogo className="max-h-full max-w-full"></DayLogo>
|
||||
</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 { Markdown } from '@/components/ui/Markdown'
|
||||
import { cn } from '@/utils'
|
||||
|
@ -17,7 +16,7 @@ export interface MessageInputProps {
|
|||
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) => {
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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 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 { DanmakuImpl } from '@/domain/impls/Danmaku'
|
||||
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
||||
import '@/assets/styles/tailwind.css'
|
||||
import '@/assets/styles/sonner.css'
|
||||
|
@ -22,7 +23,7 @@ export default defineContentScript({
|
|||
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*'],
|
||||
async main(ctx) {
|
||||
const store = Remesh.store({
|
||||
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl]
|
||||
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl, DanmakuImpl]
|
||||
// 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 { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import PromptItem from '../../components/PromptItem'
|
|||
import UserInfoDomain from '@/domain/UserInfo'
|
||||
import RoomDomain, { MessageType } from '@/domain/Room'
|
||||
import MessageListDomain from '@/domain/MessageList'
|
||||
import BlurFade from '@/components/magicui/blur-fade'
|
||||
import BlurFade from '@/components/magicui/BlurFade'
|
||||
|
||||
const Main: FC = () => {
|
||||
const send = useRemeshSend()
|
||||
|
|
|
@ -9,9 +9,9 @@ import { FC, useEffect, useState } from 'react'
|
|||
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
|
||||
import Timer from '@resreq/timer'
|
||||
import ExampleImage from '@/assets/images/example.jpg'
|
||||
import PulsatingButton from '@/components/magicui/pulsating-button'
|
||||
import BlurFade from '@/components/magicui/blur-fade'
|
||||
import WordPullUp from '@/components/magicui/word-pull-up'
|
||||
import PulsatingButton from '@/components/magicui/PulsatingButton'
|
||||
import BlurFade from '@/components/magicui/BlurFade'
|
||||
import WordPullUp from '@/components/magicui/WordPullUp'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const mockTextList = [
|
||||
|
@ -41,7 +41,8 @@ const generateUserInfo = async (): Promise<UserInfo> => {
|
|||
name: generateRandomName(),
|
||||
avatar: await generateRandomAvatar(MAX_AVATAR_SIZE),
|
||||
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'
|
||||
|
||||
export interface LayoutProps {
|
||||
|
|
|
@ -15,15 +15,16 @@ import { Label } from '@/components/ui/Label'
|
|||
import { RefreshCcwIcon } from 'lucide-react'
|
||||
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
||||
import ToastDomain from '@/domain/Toast'
|
||||
import BlurFade from '@/components/magicui/blur-fade'
|
||||
import debounce from './../../../utils/debounce'
|
||||
import BlurFade from '@/components/magicui/BlurFade'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
|
||||
const defaultUserInfo: UserInfo = {
|
||||
id: nanoid(),
|
||||
name: '',
|
||||
avatar: '',
|
||||
createTime: Date.now(),
|
||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system'
|
||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system',
|
||||
danmakuEnabled: true
|
||||
}
|
||||
|
||||
const formSchema = v.object({
|
||||
|
@ -49,7 +50,8 @@ const formSchema = v.object({
|
|||
themeMode: v.pipe(
|
||||
v.string(),
|
||||
v.union([v.literal('system'), v.literal('light'), v.literal('dark')], 'Please select extension theme mode.')
|
||||
)
|
||||
),
|
||||
danmakuEnabled: v.boolean()
|
||||
})
|
||||
|
||||
const ProfileForm = () => {
|
||||
|
@ -89,7 +91,7 @@ const ProfileForm = () => {
|
|||
|
||||
return (
|
||||
<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
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
|
@ -128,6 +130,30 @@ const ProfileForm = () => {
|
|||
</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
|
||||
control={form.control}
|
||||
name="themeMode"
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import { Remesh } from 'remesh'
|
||||
import { RemeshRoot } from 'remesh-react'
|
||||
import { RemeshLogger } from 'remesh-logger'
|
||||
import App from './App'
|
||||
import { BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
||||
import '@/assets/styles/tailwind.css'
|
||||
|
@ -10,8 +9,7 @@ import '@/assets/styles/tailwind.css'
|
|||
import { ToastImpl } from '@/domain/impls/Toast'
|
||||
|
||||
const store = Remesh.store({
|
||||
externs: [BrowserSyncStorageImpl, ToastImpl],
|
||||
inspectors: [RemeshLogger()]
|
||||
externs: [BrowserSyncStorageImpl, ToastImpl]
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
const MessageListLoadStatusModule = StatusModule(domain, {
|
||||
name: 'MessageListLoadStatusModule'
|
||||
const LoadStatusModule = StatusModule(domain, {
|
||||
name: 'Message.ListLoadStatusModule'
|
||||
})
|
||||
|
||||
const ListQuery = MessageListModule.query.ItemListQuery
|
||||
|
@ -58,6 +58,8 @@ const MessageListDomain = Remesh.domain({
|
|||
|
||||
const HasItemQuery = MessageListModule.query.HasItemByKeyQuery
|
||||
|
||||
const LoadIsFinishedQuery = LoadStatusModule.query.IsFinishedQuery
|
||||
|
||||
const ChangeListEvent = domain.event({
|
||||
name: 'MessageList.ChangeListEvent',
|
||||
impl: ({ get }) => {
|
||||
|
@ -144,16 +146,14 @@ const MessageListDomain = Remesh.domain({
|
|||
|
||||
storageEffect
|
||||
.set(SyncToStorageEvent)
|
||||
.get<
|
||||
Message[]
|
||||
>((value) => [SyncToStateCommand(value ?? []), MessageListLoadStatusModule.command.SetFinishedCommand()])
|
||||
.get<Message[]>((value) => [SyncToStateCommand(value ?? []), LoadStatusModule.command.SetFinishedCommand()])
|
||||
|
||||
return {
|
||||
query: {
|
||||
HasItemQuery,
|
||||
ItemQuery,
|
||||
ListQuery,
|
||||
MessageListLoadIsFinishedQuery: MessageListLoadStatusModule.query.IsFinishedQuery
|
||||
LoadIsFinishedQuery
|
||||
},
|
||||
command: {
|
||||
CreateItemCommand,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
||||
|
@ -7,7 +7,8 @@ import UserInfoDomain from '@/domain/UserInfo'
|
|||
import { desert, upsert } from '@/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
import StatusModule from '@/domain/modules/Status'
|
||||
import { ToastExtern } from './externs/Toast'
|
||||
import { ToastExtern } from '@/domain/externs/Toast'
|
||||
import DanmakuDomain from '@/domain/Danmaku'
|
||||
|
||||
export { MessageType }
|
||||
|
||||
|
@ -50,6 +51,7 @@ const RoomDomain = Remesh.domain({
|
|||
impl: (domain) => {
|
||||
const messageListDomain = domain.getDomain(MessageListDomain())
|
||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||
const danmakuDomain = domain.getDomain(DanmakuDomain())
|
||||
const peerRoom = domain.getExtern(PeerRoomExtern)
|
||||
const toast = domain.getExtern(ToastExtern)
|
||||
|
||||
|
@ -65,12 +67,12 @@ const RoomDomain = Remesh.domain({
|
|||
}
|
||||
})
|
||||
|
||||
const RoomJoinStatusModule = StatusModule(domain, {
|
||||
name: 'RoomJoinStatusModule'
|
||||
const JoinStatusModule = StatusModule(domain, {
|
||||
name: 'Room.JoinStatusModule'
|
||||
})
|
||||
|
||||
const UserListState = domain.state<RoomUser[]>({
|
||||
name: 'RoomUserListState',
|
||||
name: 'Room.UserListState',
|
||||
default: []
|
||||
})
|
||||
|
||||
|
@ -81,8 +83,10 @@ const RoomDomain = Remesh.domain({
|
|||
}
|
||||
})
|
||||
|
||||
const JoinIsFinishedQuery = JoinStatusModule.query.IsFinishedQuery
|
||||
|
||||
const JoinRoomCommand = domain.command({
|
||||
name: 'RoomJoinRoomCommand',
|
||||
name: 'Room.JoinRoomCommand',
|
||||
impl: ({ get }) => {
|
||||
peerRoom.joinRoom()
|
||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
|
@ -101,14 +105,14 @@ const RoomDomain = Remesh.domain({
|
|||
type: MessageType.Prompt,
|
||||
date: Date.now()
|
||||
}),
|
||||
RoomJoinStatusModule.command.SetFinishedCommand(),
|
||||
JoinStatusModule.command.SetFinishedCommand(),
|
||||
JoinRoomEvent(peerRoom.roomId)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const LeaveRoomCommand = domain.command({
|
||||
name: 'RoomLeaveRoomCommand',
|
||||
name: 'Room.LeaveRoomCommand',
|
||||
impl: ({ get }) => {
|
||||
peerRoom.leaveRoom()
|
||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
|
@ -126,16 +130,21 @@ const RoomDomain = Remesh.domain({
|
|||
type: 'delete',
|
||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||
}),
|
||||
RoomJoinStatusModule.command.SetInitialCommand(),
|
||||
JoinStatusModule.command.SetInitialCommand(),
|
||||
LeaveRoomEvent(peerRoom.roomId)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const SendTextMessageCommand = domain.command({
|
||||
name: 'RoomSendTextMessageCommand',
|
||||
name: 'Room.SendTextMessageCommand',
|
||||
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 = {
|
||||
id: nanoid(),
|
||||
|
@ -154,13 +163,17 @@ const RoomDomain = Remesh.domain({
|
|||
hateUsers: []
|
||||
}
|
||||
|
||||
peerRoom.sendMessage<RoomMessage>(textMessage)
|
||||
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
|
||||
peerRoom.sendMessage(textMessage)
|
||||
return [
|
||||
messageListDomain.command.CreateItemCommand(listMessage),
|
||||
danmakuEnabled ? PushDanmakuCommand(textMessage) : null,
|
||||
SendTextMessageEvent(textMessage)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const SendLikeMessageCommand = domain.command({
|
||||
name: 'RoomSendLikeMessageCommand',
|
||||
name: 'Room.SendLikeMessageCommand',
|
||||
impl: ({ get }, messageId: string) => {
|
||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
||||
|
@ -176,13 +189,13 @@ const RoomDomain = Remesh.domain({
|
|||
...localMessage,
|
||||
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
||||
}
|
||||
peerRoom.sendMessage<RoomMessage>(likeMessage)
|
||||
peerRoom.sendMessage(likeMessage)
|
||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
|
||||
}
|
||||
})
|
||||
|
||||
const SendHateMessageCommand = domain.command({
|
||||
name: 'RoomSendHateMessageCommand',
|
||||
name: 'Room.SendHateMessageCommand',
|
||||
impl: ({ get }, messageId: string) => {
|
||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage
|
||||
|
@ -198,13 +211,13 @@ const RoomDomain = Remesh.domain({
|
|||
...localMessage,
|
||||
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
||||
}
|
||||
peerRoom.sendMessage<RoomMessage>(hateMessage)
|
||||
peerRoom.sendMessage(hateMessage)
|
||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
|
||||
}
|
||||
})
|
||||
|
||||
const SendJoinMessageCommand = domain.command({
|
||||
name: 'RoomSendJoinMessageCommand',
|
||||
name: 'Room.SendJoinMessageCommand',
|
||||
impl: ({ get }, targetPeerId: string) => {
|
||||
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
|
||||
|
||||
|
@ -214,13 +227,13 @@ const RoomDomain = Remesh.domain({
|
|||
type: SendType.Join
|
||||
}
|
||||
|
||||
peerRoom.sendMessage<RoomMessage>(syncUserMessage, targetPeerId)
|
||||
peerRoom.sendMessage(syncUserMessage, targetPeerId)
|
||||
return [SendJoinMessageEvent(syncUserMessage)]
|
||||
}
|
||||
})
|
||||
|
||||
const UpdateUserListCommand = domain.command({
|
||||
name: 'RoomUpdateUserListCommand',
|
||||
name: 'Room.UpdateUserListCommand',
|
||||
impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => {
|
||||
const userList = get(UserListState())
|
||||
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>({
|
||||
name: 'RoomSendJoinMessageEvent'
|
||||
name: 'Room.SendJoinMessageEvent'
|
||||
})
|
||||
|
||||
const SendTextMessageEvent = domain.event<TextMessage>({
|
||||
name: 'RoomSendTextMessageEvent'
|
||||
name: 'Room.SendTextMessageEvent'
|
||||
})
|
||||
|
||||
const SendLikeMessageEvent = domain.event<LikeMessage>({
|
||||
name: 'RoomSendLikeMessageEvent'
|
||||
name: 'Room.SendLikeMessageEvent'
|
||||
})
|
||||
|
||||
const SendHateMessageEvent = domain.event<HateMessage>({
|
||||
name: 'RoomSendHateMessageEvent'
|
||||
name: 'Room.SendHateMessageEvent'
|
||||
})
|
||||
|
||||
const JoinRoomEvent = domain.event<string>({
|
||||
name: 'RoomJoinRoomEvent'
|
||||
name: 'Room.JoinRoomEvent'
|
||||
})
|
||||
|
||||
const LeaveRoomEvent = domain.event<string>({
|
||||
name: 'RoomLeaveRoomEvent'
|
||||
name: 'Room.LeaveRoomEvent'
|
||||
})
|
||||
|
||||
const OnMessageEvent = domain.event<RoomMessage>({
|
||||
name: 'RoomOnMessageEvent'
|
||||
name: 'Room.OnMessageEvent'
|
||||
})
|
||||
|
||||
const OnJoinRoomEvent = domain.event<string>({
|
||||
name: 'RoomOnJoinRoomEvent'
|
||||
name: 'Room.OnJoinRoomEvent'
|
||||
})
|
||||
|
||||
const OnLeaveRoomEvent = domain.event<string>({
|
||||
name: 'RoomOnLeaveRoomEvent'
|
||||
name: 'Room.OnLeaveRoomEvent'
|
||||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'RoomOnJoinRoomEffect',
|
||||
name: 'Room.OnJoinRoomEffect',
|
||||
impl: () => {
|
||||
const onJoinRoom$ = fromEventPattern<string>(peerRoom.onJoinRoom).pipe(
|
||||
mergeMap((peerId) => {
|
||||
|
@ -285,14 +312,16 @@ const RoomDomain = Remesh.domain({
|
|||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'RoomOnMessageEffect',
|
||||
name: 'Room.OnMessageEffect',
|
||||
impl: ({ get }) => {
|
||||
const onMessage$ = fromEventPattern<RoomMessage>(peerRoom.onMessage).pipe(
|
||||
mergeMap((message) => {
|
||||
// console.log('onMessage', message)
|
||||
const { danmakuEnabled } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
|
||||
const messageEvent$ = of(OnMessageEvent(message))
|
||||
|
||||
const commandEvent$ = (() => {
|
||||
const messageCommand$ = (() => {
|
||||
switch (message.type) {
|
||||
case SendType.Join: {
|
||||
const userList = get(UserListQuery())
|
||||
|
@ -326,7 +355,8 @@ const RoomDomain = Remesh.domain({
|
|||
date: Date.now(),
|
||||
likeUsers: [],
|
||||
hateUsers: []
|
||||
})
|
||||
}),
|
||||
danmakuEnabled ? PushDanmakuCommand(message) : null
|
||||
)
|
||||
case SendType.Like:
|
||||
case SendType.Hate: {
|
||||
|
@ -355,7 +385,8 @@ const RoomDomain = Remesh.domain({
|
|||
return EMPTY
|
||||
}
|
||||
})()
|
||||
return merge(messageEvent$, commandEvent$)
|
||||
|
||||
return merge(messageEvent$, messageCommand$)
|
||||
})
|
||||
)
|
||||
return onMessage$
|
||||
|
@ -363,7 +394,7 @@ const RoomDomain = Remesh.domain({
|
|||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'RoomOnLeaveRoomEffect',
|
||||
name: 'Room.OnLeaveRoomEffect',
|
||||
impl: ({ get }) => {
|
||||
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
|
||||
map((peerId) => {
|
||||
|
@ -392,7 +423,7 @@ const RoomDomain = Remesh.domain({
|
|||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'RoomOnErrorEffect',
|
||||
name: 'Room.OnErrorEffect',
|
||||
impl: () => {
|
||||
const onRoomError$ = fromEventPattern<Error>(peerRoom.onError).pipe(
|
||||
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
|
||||
domain.effect({
|
||||
name: 'RoomOnUnloadEffect',
|
||||
name: 'Room.OnUnloadEffect',
|
||||
impl: ({ get }) => {
|
||||
const beforeUnload$ = fromEvent(window, 'beforeunload').pipe(
|
||||
const beforeUnload$ = fromEvent(window, 'beforedestroy').pipe(
|
||||
map(() => {
|
||||
return get(RoomJoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null
|
||||
return get(JoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null
|
||||
})
|
||||
)
|
||||
return beforeUnload$
|
||||
|
@ -422,7 +453,7 @@ const RoomDomain = Remesh.domain({
|
|||
query: {
|
||||
PeerIdQuery,
|
||||
UserListQuery,
|
||||
RoomJoinIsFinishedQuery: RoomJoinStatusModule.query.IsFinishedQuery
|
||||
JoinIsFinishedQuery
|
||||
},
|
||||
command: {
|
||||
JoinRoomCommand,
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface UserInfo {
|
|||
avatar: string
|
||||
createTime: number
|
||||
themeMode: 'system' | 'light' | 'dark'
|
||||
danmakuEnabled: boolean
|
||||
}
|
||||
|
||||
const UserInfoDomain = Remesh.domain({
|
||||
|
@ -27,10 +28,10 @@ const UserInfoDomain = Remesh.domain({
|
|||
})
|
||||
|
||||
const UserInfoLoadStatusModule = StatusModule(domain, {
|
||||
name: 'UserInfoLoadStatusModule'
|
||||
name: 'UserInfo.LoadStatusModule'
|
||||
})
|
||||
const UserInfoSetStatusModule = StatusModule(domain, {
|
||||
name: 'UserInfoSetStatusModule'
|
||||
name: 'UserInfo.SetStatusModule'
|
||||
})
|
||||
|
||||
const UserInfoQuery = domain.query({
|
||||
|
@ -43,8 +44,6 @@ const UserInfoDomain = Remesh.domain({
|
|||
const UpdateUserInfoCommand = domain.command({
|
||||
name: 'UserInfo.UpdateUserInfoCommand',
|
||||
impl: (_, userInfo: UserInfo | null) => {
|
||||
console.log('111', userInfo)
|
||||
|
||||
return [
|
||||
UserInfoState().new(userInfo),
|
||||
UpdateUserInfoEvent(),
|
||||
|
@ -81,16 +80,16 @@ const UserInfoDomain = Remesh.domain({
|
|||
UserInfoState().new(userInfo),
|
||||
UpdateUserInfoEvent(),
|
||||
SyncToStateEvent(userInfo),
|
||||
userInfo && UserInfoSetStatusModule.command.SetFinishedCommand()
|
||||
userInfo
|
||||
? UserInfoSetStatusModule.command.SetFinishedCommand()
|
||||
: UserInfoSetStatusModule.command.SetInitialCommand()
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
storageEffect
|
||||
.set(SyncToStorageEvent)
|
||||
.get<UserInfo>((value) => {
|
||||
return [SyncToStateCommand(value), UserInfoLoadStatusModule.command.SetFinishedCommand()]
|
||||
})
|
||||
.get<UserInfo>((value) => [SyncToStateCommand(value), UserInfoLoadStatusModule.command.SetFinishedCommand()])
|
||||
.watch<UserInfo>((value) => [SyncToStateCommand(value)])
|
||||
|
||||
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'
|
||||
|
||||
export type PeerMessage = object | Blob | ArrayBuffer | ArrayBufferView
|
||||
import { RoomMessage } from '../Room'
|
||||
|
||||
export interface PeerRoom {
|
||||
readonly peerId: string
|
||||
readonly roomId: string
|
||||
joinRoom: () => PeerRoom
|
||||
sendMessage: <T extends PeerMessage>(message: T, id?: string) => PeerRoom
|
||||
onMessage: <T extends PeerMessage>(callback: (message: T) => void) => PeerRoom
|
||||
sendMessage: (message: RoomMessage, id?: string) => PeerRoom
|
||||
onMessage: (callback: (message: RoomMessage) => void) => PeerRoom
|
||||
leaveRoom: () => PeerRoom
|
||||
onJoinRoom: (callback: (id: string) => void) => PeerRoom
|
||||
onLeaveRoom: (callback: (id: string) => void) => PeerRoom
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import { type Promisable } from 'type-fest'
|
||||
|
||||
export type StorageValue = null | string | number | boolean | object
|
||||
export type WatchEvent = 'update' | 'remove'
|
||||
export type WatchCallback = (event: WatchEvent, key: string) => any
|
||||
export type Unwatch = () => Promisable<void>
|
||||
export type Unwatch = () => Promise<void>
|
||||
|
||||
export interface Storage {
|
||||
name: string
|
||||
|
@ -31,10 +30,10 @@ export const IndexDBStorageExtern = Remesh.extern<Storage>({
|
|||
clear: async () => {
|
||||
throw new Error('"clear" not implemented.')
|
||||
},
|
||||
watch: () => {
|
||||
watch: async () => {
|
||||
throw new Error('"watch" not implemented.')
|
||||
},
|
||||
unwatch: () => {
|
||||
unwatch: async () => {
|
||||
throw new Error('"unwatch" not implemented.')
|
||||
}
|
||||
}
|
||||
|
@ -55,10 +54,10 @@ export const BrowserSyncStorageExtern = Remesh.extern<Storage>({
|
|||
clear: async () => {
|
||||
throw new Error('"clear" not implemented.')
|
||||
},
|
||||
watch: () => {
|
||||
watch: async () => {
|
||||
throw new Error('"watch" not implemented.')
|
||||
},
|
||||
unwatch: () => {
|
||||
unwatch: async () => {
|
||||
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 { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom'
|
||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||
import { stringToHex } from '@/utils'
|
||||
import EventHub from '@resreq/event-hub'
|
||||
import { RoomMessage } from '../Room'
|
||||
|
||||
export interface Config {
|
||||
peerId?: string
|
||||
|
@ -44,37 +45,37 @@ class PeerRoom extends EventHub {
|
|||
return this
|
||||
}
|
||||
|
||||
sendMessage<T extends PeerMessage>(message: T, id?: string) {
|
||||
sendMessage(message: RoomMessage, id?: string) {
|
||||
if (!this.room) {
|
||||
this.once('action', () => {
|
||||
if (!this.room) {
|
||||
this.emit('error', new Error('Room not joined'))
|
||||
} else {
|
||||
const [send] = this.room.makeAction('MESSAGE')
|
||||
send(message as DataPayload, id)
|
||||
send(message as any as DataPayload, id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const [send] = this.room.makeAction('MESSAGE')
|
||||
send(message as DataPayload, id)
|
||||
send(message as any as DataPayload, id)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
|
||||
onMessage(callback: (message: RoomMessage) => void) {
|
||||
if (!this.room) {
|
||||
this.once('action', () => {
|
||||
if (!this.room) {
|
||||
this.emit('error', new Error('Room not joined'))
|
||||
} else {
|
||||
const [, on] = this.room.makeAction('MESSAGE')
|
||||
on((message) => callback(message as T))
|
||||
on((message) => callback(message as any as RoomMessage))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const [, on] = this.room.makeAction('MESSAGE')
|
||||
on((message) => callback(message as T))
|
||||
on((message) => callback(message as any as RoomMessage))
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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 { nanoid } from 'nanoid'
|
||||
import EventHub from '@resreq/event-hub'
|
||||
import { RoomMessage } from '../Room'
|
||||
export interface Config {
|
||||
peerId?: string
|
||||
roomId: string
|
||||
|
@ -43,7 +44,7 @@ class PeerRoom extends EventHub {
|
|||
return this
|
||||
}
|
||||
|
||||
sendMessage<T extends PeerMessage>(message: T, id?: string) {
|
||||
sendMessage(message: RoomMessage, id?: string) {
|
||||
if (!this.room) {
|
||||
this.once('action', () => {
|
||||
if (!this.room) {
|
||||
|
@ -58,17 +59,17 @@ class PeerRoom extends EventHub {
|
|||
return this
|
||||
}
|
||||
|
||||
onMessage<T extends PeerMessage>(callback: (message: T) => void) {
|
||||
onMessage(callback: (message: RoomMessage) => void) {
|
||||
if (!this.room) {
|
||||
this.once('action', () => {
|
||||
if (!this.room) {
|
||||
this.emit('error', new Error('Room not joined'))
|
||||
} else {
|
||||
this.room.on('message', (message) => callback(JSON.parse(message) as T))
|
||||
this.room.on('message', (message) => callback(JSON.parse(message) as RoomMessage))
|
||||
}
|
||||
})
|
||||
} 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
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import indexedDbDriver from 'unstorage/drivers/indexedb'
|
|||
import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
||||
import { STORAGE_NAME } from '@/constants/config'
|
||||
import { webExtensionDriver } from '@/utils/webExtensionDriver'
|
||||
import { browser } from 'wxt/browser'
|
||||
import { Storage } from '@/domain/externs/Storage'
|
||||
|
||||
export const indexDBStorage = createStorage({
|
||||
driver: indexedDbDriver({ base: `${STORAGE_NAME}:` })
|
||||
|
@ -18,7 +20,7 @@ export const IndexDBStorageImpl = IndexDBStorageExtern.impl({
|
|||
set: indexDBStorage.setItem,
|
||||
remove: indexDBStorage.removeItem,
|
||||
clear: indexDBStorage.clear,
|
||||
watch: indexDBStorage.watch,
|
||||
watch: indexDBStorage.watch as Storage['watch'],
|
||||
unwatch: indexDBStorage.unwatch
|
||||
})
|
||||
|
||||
|
@ -28,6 +30,26 @@ export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({
|
|||
set: browserSyncStorage.setItem,
|
||||
remove: browserSyncStorage.removeItem,
|
||||
clear: browserSyncStorage.clear,
|
||||
watch: browserSyncStorage.watch,
|
||||
watch: browserSyncStorage.watch as Storage['watch'],
|
||||
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 { defer, from, fromEventPattern, map, Observable, switchMap } from 'rxjs'
|
||||
import { type Promisable } from 'type-fest'
|
||||
import { defer, from, map, Observable, switchMap } from 'rxjs'
|
||||
|
||||
export type StorageValue = null | string | number | boolean | object
|
||||
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
|
||||
}
|
||||
import { Storage, StorageValue } from '@/domain/externs/Storage'
|
||||
|
||||
export interface Options {
|
||||
domain: RemeshDomainContext
|
||||
|
@ -60,20 +49,14 @@ export default class StorageEffect {
|
|||
this.domain.effect({
|
||||
name: 'WatchStorageToStateEffect',
|
||||
impl: () => {
|
||||
return defer(() => {
|
||||
let unwatch: Unwatch
|
||||
return new Observable<void>((observer) => {
|
||||
this.storage
|
||||
.watch(() => observer.next())
|
||||
.then((_unwatch) => {
|
||||
unwatch = _unwatch
|
||||
})
|
||||
return () => unwatch?.()
|
||||
// TODO: Report the bug to https://github.com/unjs/unstorage
|
||||
return new Observable((observer) => {
|
||||
const unwatchPromise = this.storage.watch(() => observer.next())
|
||||
return () => unwatchPromise.then((unwatch) => unwatch())
|
||||
}).pipe(
|
||||
switchMap(() => from(this.storage.get<T | null>(this.key))),
|
||||
map(callback)
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
return this
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
storageArea: 'sync' | 'local' | 'managed' | 'session'
|
||||
|
@ -67,6 +67,7 @@ export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = d
|
|||
watch(callback) {
|
||||
checkPermission()
|
||||
_listeners.add(callback)
|
||||
|
||||
if (_listeners.size === 1) {
|
||||
browser.storage[opts.storageArea].onChanged.addListener(_storageListener)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export default defineConfig({
|
|||
runner: {
|
||||
startUrls: ['https://www.example.com/']
|
||||
},
|
||||
manifest: ({ browser, manifestVersion }) => {
|
||||
manifest: ({ browser }) => {
|
||||
const common = {
|
||||
name: displayName,
|
||||
permissions: ['storage'],
|
||||
|
|
Loading…
Reference in a new issue