Merge branch 'develop'
This commit is contained in:
commit
a3ac1092f9
24 changed files with 8195 additions and 9699 deletions
10
README.md
10
README.md
|
@ -10,7 +10,9 @@
|
|||
|
||||
This is an anonymous chat browser extension that is decentralized and serverless, utilizing WebRTC for end-to-end encrypted communication. It prioritizes privacy, with all data stored locally.
|
||||
|
||||
The aim is to add chat room functionality to any website, enabling real-time messaging anytime, anywhere.
|
||||
The aim is to add chat room functionality to any website, you'll never feel alone again.
|
||||
|
||||
|
||||
|
||||
### Install
|
||||
|
||||
|
@ -27,6 +29,12 @@ The aim is to add chat room functionality to any website, enabling real-time mes
|
|||
- Enable "Developer mode"
|
||||
- Click "Load unpacked" and select the folder you just extracted
|
||||
|
||||
### Use
|
||||
|
||||
After installing the extension, you'll see a ghost icon in the bottom-right corner of any website. Click it, and you'll be able to chat happily with others on the same site!
|
||||
|
||||
|
||||
|
||||
### Video
|
||||
|
||||
https://github.com/user-attachments/assets/34890975-5926-4e38-9a5f-34a28e17ff36
|
||||
|
|
11
package.json
11
package.json
|
@ -62,14 +62,15 @@
|
|||
"@resreq/timer": "^1.1.6",
|
||||
"@rtco/client": "^0.2.17",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@webext-core/messaging": "^1.4.0",
|
||||
"@webext-core/proxy-service": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"danmu": "^0.12.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.11.7",
|
||||
"framer-motion": "^11.11.8",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.451.0",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
|
@ -94,17 +95,17 @@
|
|||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@eslint-react/eslint-plugin": "^1.14.3",
|
||||
"@eslint-react/eslint-plugin": "^1.15.0",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
|
17476
pnpm-lock.yaml
17476
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,17 +1,64 @@
|
|||
import { EVENT } from '@/constants/event'
|
||||
import { browser } from 'wxt/browser'
|
||||
import { messenger } from '@/messenger'
|
||||
import { browser, Tabs } from 'wxt/browser'
|
||||
import { defineBackground } from 'wxt/sandbox'
|
||||
|
||||
export default defineBackground({
|
||||
// Set manifest options
|
||||
persistent: true,
|
||||
type: 'module',
|
||||
|
||||
main() {
|
||||
browser.runtime.onMessage.addListener(async (event: EVENT) => {
|
||||
if (event === EVENT.OPTIONS_PAGE_OPEN) {
|
||||
browser.runtime.openOptionsPage()
|
||||
const historyNotificationTabs = new Map<string, Tabs.Tab>()
|
||||
messenger.onMessage(EVENT.OPTIONS_PAGE_OPEN, () => {
|
||||
browser.runtime.openOptionsPage()
|
||||
})
|
||||
|
||||
messenger.onMessage(EVENT.NOTIFICATION_PUSH, async ({ data: message, sender }) => {
|
||||
// Check if there is an active tab on the same site
|
||||
const tabs = await browser.tabs.query({ active: true })
|
||||
const hasActiveSomeSiteTab = tabs.some((tab) => {
|
||||
return new URL(tab.url!).origin === new URL(sender.tab!.url!).origin
|
||||
})
|
||||
|
||||
if (hasActiveSomeSiteTab) return
|
||||
|
||||
browser.notifications.create(message.id, {
|
||||
type: 'basic',
|
||||
iconUrl: message.userAvatar,
|
||||
title: message.username,
|
||||
message: message.body,
|
||||
contextMessage: sender.tab!.url!
|
||||
})
|
||||
historyNotificationTabs.set(message.id, sender.tab!)
|
||||
})
|
||||
messenger.onMessage(EVENT.NOTIFICATION_CLEAR, async ({ data: id }) => {
|
||||
browser.notifications.clear(id)
|
||||
})
|
||||
|
||||
browser.notifications.onButtonClicked.addListener(async (id) => {
|
||||
const fromTab = historyNotificationTabs.get(id)
|
||||
if (fromTab?.id) {
|
||||
try {
|
||||
const tab = await browser.tabs.get(fromTab.id)
|
||||
browser.tabs.update(tab.id, { active: true })
|
||||
} catch {
|
||||
browser.tabs.create({ url: fromTab.url })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
browser.notifications.onClicked.addListener(async (id) => {
|
||||
const fromTab = historyNotificationTabs.get(id)
|
||||
if (fromTab?.id) {
|
||||
try {
|
||||
const tab = await browser.tabs.get(fromTab.id)
|
||||
browser.tabs.update(tab.id, { active: true })
|
||||
} catch {
|
||||
browser.tabs.create({ url: fromTab.url })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
browser.notifications.onClosed.addListener(async (id) => {
|
||||
historyNotificationTabs.delete(id)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Remesh } from 'remesh'
|
||||
import { RemeshRoot } from 'remesh-react'
|
||||
import { RemeshRoot, RemeshScope } from 'remesh-react'
|
||||
import { RemeshLogger } from 'remesh-logger'
|
||||
import { defineContentScript } from 'wxt/sandbox'
|
||||
import { createShadowRootUi } from 'wxt/client'
|
||||
|
@ -9,21 +9,31 @@ import { createShadowRootUi } from 'wxt/client'
|
|||
import App from './App'
|
||||
import { LocalStorageImpl, IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
|
||||
import { DanmakuImpl } from '@/domain/impls/Danmaku'
|
||||
import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
||||
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
||||
import { NotificationImpl } from '@/domain/impls/Notification'
|
||||
import { ToastImpl } from '@/domain/impls/Toast'
|
||||
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
||||
import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
||||
import '@/assets/styles/tailwind.css'
|
||||
import '@/assets/styles/sonner.css'
|
||||
import { createElement } from '@/utils'
|
||||
import { ToastImpl } from '@/domain/impls/Toast'
|
||||
import NotificationDomain from '@/domain/Notification'
|
||||
|
||||
export default defineContentScript({
|
||||
cssInjectionMode: 'ui',
|
||||
runAt: 'document_end',
|
||||
matches: ['https://*/*'],
|
||||
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*'],
|
||||
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*', '*://*.csdn.net/*', '*://*.csdn.com/*'],
|
||||
async main(ctx) {
|
||||
const store = Remesh.store({
|
||||
externs: [LocalStorageImpl, IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl, DanmakuImpl]
|
||||
externs: [
|
||||
LocalStorageImpl,
|
||||
IndexDBStorageImpl,
|
||||
BrowserSyncStorageImpl,
|
||||
PeerRoomImpl,
|
||||
ToastImpl,
|
||||
DanmakuImpl,
|
||||
NotificationImpl
|
||||
]
|
||||
// inspectors: __DEV__ ? [RemeshLogger()] : []
|
||||
})
|
||||
|
||||
|
@ -42,7 +52,9 @@ export default defineContentScript({
|
|||
root.render(
|
||||
<React.StrictMode>
|
||||
<RemeshRoot store={store}>
|
||||
<App />
|
||||
<RemeshScope domains={[NotificationDomain()]}>
|
||||
<App />
|
||||
</RemeshScope>
|
||||
</RemeshRoot>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
|
@ -19,6 +19,7 @@ import LogoIcon5 from '@/assets/images/logo-5.svg'
|
|||
import LogoIcon6 from '@/assets/images/logo-6.svg'
|
||||
import AppStatusDomain from '@/domain/AppStatus'
|
||||
import { getDay } from 'date-fns'
|
||||
import { messenger } from '@/messenger'
|
||||
|
||||
const AppButton: FC = () => {
|
||||
const send = useRemeshSend()
|
||||
|
@ -56,7 +57,7 @@ const AppButton: FC = () => {
|
|||
}
|
||||
|
||||
const handleOpenOptionsPage = () => {
|
||||
browser.runtime.sendMessage(EVENT.OPTIONS_PAGE_OPEN)
|
||||
messenger.sendMessage(EVENT.OPTIONS_PAGE_OPEN, undefined)
|
||||
}
|
||||
|
||||
const handleToggleApp = () => {
|
||||
|
|
|
@ -69,10 +69,10 @@ const Header: FC = () => {
|
|||
</div>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-44 rounded-lg p-2">
|
||||
<HoverCardContent className="w-44 rounded-lg px-0 py-2">
|
||||
<ScrollArea className="max-h-80">
|
||||
{userList.map((user) => (
|
||||
<div className="flex items-center gap-x-2 p-2 [content-visibility:auto]" key={user.userId}>
|
||||
<div className="flex items-center gap-x-2 px-4 py-2 [content-visibility:auto]" key={user.userId}>
|
||||
<Avatar className="size-6 shrink-0">
|
||||
<AvatarImage src={user.userAvatar} alt="avatar" />
|
||||
<AvatarFallback>{user.username.at(0)}</AvatarFallback>
|
||||
|
|
|
@ -42,7 +42,8 @@ const generateUserInfo = async (): Promise<UserInfo> => {
|
|||
avatar: await generateRandomAvatar(MAX_AVATAR_SIZE),
|
||||
createTime: Date.now(),
|
||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system',
|
||||
danmakuEnabled: true
|
||||
danmakuEnabled: true,
|
||||
notificationEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import Link from '@/components/Link'
|
|||
|
||||
const BadgeList: FC = () => {
|
||||
return (
|
||||
<div className="fixed inset-x-1 bottom-6 mx-auto flex w-fit">
|
||||
<div className="fixed inset-x-1 bottom-4 mx-auto flex w-fit">
|
||||
<Button asChild size="lg" variant="ghost" className="rounded-full px-3 text-xl font-semibold text-primary">
|
||||
<Link href="https://github.com/molvqingtai/WebChat">
|
||||
<GitHubLogoIcon className="mr-1 size-6"></GitHubLogoIcon>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
|
|||
import { Label } from '@/components/ui/Label'
|
||||
import { RefreshCcwIcon } from 'lucide-react'
|
||||
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
||||
import ToastDomain from '@/domain/Toast'
|
||||
import { ToastImpl } from '@/domain/impls/Toast'
|
||||
import BlurFade from '@/components/magicui/BlurFade'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import Link from '@/components/Link'
|
||||
|
@ -25,7 +25,8 @@ const defaultUserInfo: UserInfo = {
|
|||
avatar: '',
|
||||
createTime: Date.now(),
|
||||
themeMode: checkSystemDarkMode() ? 'dark' : 'system',
|
||||
danmakuEnabled: true
|
||||
danmakuEnabled: true,
|
||||
notificationEnabled: false
|
||||
}
|
||||
|
||||
const formSchema = v.object({
|
||||
|
@ -52,12 +53,13 @@ const formSchema = v.object({
|
|||
v.string(),
|
||||
v.union([v.literal('system'), v.literal('light'), v.literal('dark')], 'Please select extension theme mode.')
|
||||
),
|
||||
danmakuEnabled: v.boolean()
|
||||
danmakuEnabled: v.boolean(),
|
||||
notificationEnabled: v.boolean()
|
||||
})
|
||||
|
||||
const ProfileForm = () => {
|
||||
const send = useRemeshSend()
|
||||
const toastDomain = useRemeshDomain(ToastDomain())
|
||||
const toast = ToastImpl.value
|
||||
|
||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||
|
@ -74,15 +76,15 @@ const ProfileForm = () => {
|
|||
|
||||
const handleSubmit = (userInfo: UserInfo) => {
|
||||
send(userInfoDomain.command.UpdateUserInfoCommand(userInfo))
|
||||
send(toastDomain.command.SuccessCommand('Saved successfully!'))
|
||||
toast.success('Saved successfully!')
|
||||
}
|
||||
|
||||
const handleWarning = (error: Error) => {
|
||||
send(toastDomain.command.WarningCommand(error.message))
|
||||
toast.warning(error.message)
|
||||
}
|
||||
|
||||
const handleError = (error: Error) => {
|
||||
send(toastDomain.command.ErrorCommand(error.message))
|
||||
toast.error(error.message)
|
||||
}
|
||||
|
||||
const handleRefreshAvatar = async () => {
|
||||
|
@ -163,7 +165,7 @@ const ProfileForm = () => {
|
|||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enabling this will display messages scrolling on the website.
|
||||
Enabling this option will display scrolling messages on the website.
|
||||
<Link className="ml-2 text-primary" href="https://en.wikipedia.org/wiki/Danmaku_subtitling">
|
||||
Wikipedia
|
||||
</Link>
|
||||
|
@ -172,6 +174,30 @@ const ProfileForm = () => {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{/* <FormLabel>Username</FormLabel> */}
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
defaultChecked={false}
|
||||
id="notification-enabled"
|
||||
onCheckedChange={field.onChange}
|
||||
checked={field.value}
|
||||
/>
|
||||
<FormLabel className="cursor-pointer" htmlFor="notification-enabled">
|
||||
Enable Notification
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>Enabling this option will display desktop notifications for messages.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="themeMode"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export enum EVENT {
|
||||
OPTIONS_PAGE_OPEN = `WEB_CHAT_OPTIONS_PAGE_OPEN`,
|
||||
APP_OPEN = 'WEB_CHAT_APP_OPEN'
|
||||
APP_OPEN = 'WEB_CHAT_APP_OPEN',
|
||||
NOTIFICATION_PUSH = 'WEB_CHAT_NOTIFICATION_PUSH',
|
||||
NOTIFICATION_CLEAR = 'WEB_CHAT_NOTIFICATION_CLEAR'
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import { DanmakuExtern } from './externs/Danmaku'
|
||||
import { TextMessage } from './Room'
|
||||
import RoomDomain, { TextMessage } from './Room'
|
||||
import UserInfoDomain from './UserInfo'
|
||||
import { map } from 'rxjs'
|
||||
import { map, merge, of } from 'rxjs'
|
||||
|
||||
const DanmakuDomain = Remesh.domain({
|
||||
name: 'DanmakuDomain',
|
||||
impl: (domain) => {
|
||||
const danmaku = domain.getExtern(DanmakuExtern)
|
||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||
const roomDomain = domain.getDomain(RoomDomain())
|
||||
|
||||
const MountState = domain.state({
|
||||
name: 'Danmaku.MountState',
|
||||
|
@ -117,6 +118,22 @@ const DanmakuDomain = Remesh.domain({
|
|||
}
|
||||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'Danmaku.OnRoomMessageEffect',
|
||||
impl: ({ fromEvent, get }) => {
|
||||
const sendTextMessage$ = fromEvent(roomDomain.event.SendTextMessageEvent)
|
||||
const onTextMessage$ = fromEvent(roomDomain.event.OnTextMessageEvent)
|
||||
|
||||
const onMessage$ = merge(sendTextMessage$, onTextMessage$).pipe(
|
||||
map((message) => {
|
||||
const danmakuEnabled = get(IsEnabledQuery())
|
||||
return danmakuEnabled ? PushCommand(message) : null
|
||||
})
|
||||
)
|
||||
return onMessage$
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
query: {
|
||||
IsMountedQuery,
|
||||
|
|
101
src/domain/Notification.ts
Normal file
101
src/domain/Notification.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import { NotificationExtern } from './externs/Notification'
|
||||
import RoomDomain, { TextMessage } from './Room'
|
||||
import UserInfoDomain from './UserInfo'
|
||||
import { map, merge } from 'rxjs'
|
||||
|
||||
const NotificationDomain = Remesh.domain({
|
||||
name: 'NotificationDomain',
|
||||
impl: (domain) => {
|
||||
const notification = domain.getExtern(NotificationExtern)
|
||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||
const roomDomain = domain.getDomain(RoomDomain())
|
||||
|
||||
const NotificationEnabledState = domain.state<boolean>({
|
||||
name: 'Notification.EnabledState',
|
||||
default: false
|
||||
})
|
||||
|
||||
const IsEnabledQuery = domain.query({
|
||||
name: 'Notification.IsOpenQuery',
|
||||
impl: ({ get }) => {
|
||||
return get(NotificationEnabledState())
|
||||
}
|
||||
})
|
||||
|
||||
const EnableCommand = domain.command({
|
||||
name: 'Notification.EnableCommand',
|
||||
impl: () => {
|
||||
return NotificationEnabledState().new(true)
|
||||
}
|
||||
})
|
||||
|
||||
const DisableCommand = domain.command({
|
||||
name: 'Notification.DisableCommand',
|
||||
impl: () => {
|
||||
return NotificationEnabledState().new(false)
|
||||
}
|
||||
})
|
||||
|
||||
const PushCommand = domain.command({
|
||||
name: 'Notification.PushCommand',
|
||||
impl: (_, message: TextMessage) => {
|
||||
notification.push(message)
|
||||
return [PushEvent(message)]
|
||||
}
|
||||
})
|
||||
|
||||
const PushEvent = domain.event<TextMessage>({
|
||||
name: 'Notification.PushEvent'
|
||||
})
|
||||
|
||||
const ClearEvent = domain.event<string>({
|
||||
name: 'Notification.ClearEvent'
|
||||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'Notification.OnUserInfoEffect',
|
||||
impl: ({ fromEvent }) => {
|
||||
const onUserInfo$ = fromEvent(userInfoDomain.event.UpdateUserInfoEvent)
|
||||
return onUserInfo$.pipe(
|
||||
map((userInfo) => {
|
||||
return userInfo?.notificationEnabled ? EnableCommand() : DisableCommand()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'Notification.OnRoomMessageEffect',
|
||||
impl: ({ fromEvent, get }) => {
|
||||
const onTextMessage$ = fromEvent(roomDomain.event.OnTextMessageEvent)
|
||||
|
||||
const onMessage$ = merge(onTextMessage$).pipe(
|
||||
map((message) => {
|
||||
const notificationEnabled = get(IsEnabledQuery())
|
||||
return notificationEnabled ? PushCommand(message) : null
|
||||
})
|
||||
)
|
||||
|
||||
return onMessage$
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
query: {
|
||||
IsEnabledQuery
|
||||
},
|
||||
command: {
|
||||
EnableCommand,
|
||||
DisableCommand,
|
||||
PushCommand
|
||||
},
|
||||
event: {
|
||||
PushEvent,
|
||||
ClearEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default NotificationDomain
|
|
@ -7,8 +7,6 @@ import UserInfoDomain from '@/domain/UserInfo'
|
|||
import { desert, upsert } from '@/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
import StatusModule from '@/domain/modules/Status'
|
||||
import { ToastExtern } from '@/domain/externs/Toast'
|
||||
import DanmakuDomain from '@/domain/Danmaku'
|
||||
|
||||
export { MessageType }
|
||||
|
||||
|
@ -51,9 +49,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)
|
||||
|
||||
const PeerIdState = domain.state<string>({
|
||||
name: 'Room.PeerIdState',
|
||||
|
@ -139,12 +135,7 @@ const RoomDomain = Remesh.domain({
|
|||
const SendTextMessageCommand = domain.command({
|
||||
name: 'Room.SendTextMessageCommand',
|
||||
impl: ({ get }, message: string) => {
|
||||
const {
|
||||
id: userId,
|
||||
name: username,
|
||||
avatar: userAvatar,
|
||||
danmakuEnabled
|
||||
} = get(userInfoDomain.query.UserInfoQuery())!
|
||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||
|
||||
const textMessage: TextMessage = {
|
||||
id: nanoid(),
|
||||
|
@ -164,11 +155,7 @@ const RoomDomain = Remesh.domain({
|
|||
}
|
||||
|
||||
peerRoom.sendMessage(textMessage)
|
||||
return [
|
||||
messageListDomain.command.CreateItemCommand(listMessage),
|
||||
danmakuEnabled ? PushDanmakuCommand(textMessage) : null,
|
||||
SendTextMessageEvent(textMessage)
|
||||
]
|
||||
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -244,20 +231,6 @@ 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: 'Room.SendJoinMessageEvent'
|
||||
})
|
||||
|
@ -286,6 +259,10 @@ const RoomDomain = Remesh.domain({
|
|||
name: 'Room.OnMessageEvent'
|
||||
})
|
||||
|
||||
const OnTextMessageEvent = domain.event<TextMessage>({
|
||||
name: 'Room.OnTextMessageEvent'
|
||||
})
|
||||
|
||||
const OnJoinRoomEvent = domain.event<string>({
|
||||
name: 'Room.OnJoinRoomEvent'
|
||||
})
|
||||
|
@ -294,6 +271,10 @@ const RoomDomain = Remesh.domain({
|
|||
name: 'Room.OnLeaveRoomEvent'
|
||||
})
|
||||
|
||||
const OnErrorEvent = domain.event<Error>({
|
||||
name: 'Room.OnErrorEvent'
|
||||
})
|
||||
|
||||
domain.effect({
|
||||
name: 'Room.OnJoinRoomEffect',
|
||||
impl: () => {
|
||||
|
@ -317,10 +298,11 @@ const RoomDomain = Remesh.domain({
|
|||
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 textMessageEvent$ = of(message.type === SendType.Text ? OnTextMessageEvent(message) : null)
|
||||
|
||||
const messageCommand$ = (() => {
|
||||
switch (message.type) {
|
||||
case SendType.Join: {
|
||||
|
@ -355,8 +337,7 @@ const RoomDomain = Remesh.domain({
|
|||
date: Date.now(),
|
||||
likeUsers: [],
|
||||
hateUsers: []
|
||||
}),
|
||||
danmakuEnabled ? PushDanmakuCommand(message) : null
|
||||
})
|
||||
)
|
||||
case SendType.Like:
|
||||
case SendType.Hate: {
|
||||
|
@ -386,7 +367,7 @@ const RoomDomain = Remesh.domain({
|
|||
}
|
||||
})()
|
||||
|
||||
return merge(messageEvent$, messageCommand$)
|
||||
return merge(messageEvent$, textMessageEvent$, messageCommand$)
|
||||
})
|
||||
)
|
||||
return onMessage$
|
||||
|
@ -428,8 +409,7 @@ const RoomDomain = Remesh.domain({
|
|||
const onRoomError$ = fromEventPattern<Error>(peerRoom.onError).pipe(
|
||||
map((error) => {
|
||||
console.error(error)
|
||||
toast.error(error.message)
|
||||
return null
|
||||
return OnErrorEvent(error)
|
||||
})
|
||||
)
|
||||
return onRoomError$
|
||||
|
@ -471,8 +451,10 @@ const RoomDomain = Remesh.domain({
|
|||
JoinRoomEvent,
|
||||
LeaveRoomEvent,
|
||||
OnMessageEvent,
|
||||
OnTextMessageEvent,
|
||||
OnJoinRoomEvent,
|
||||
OnLeaveRoomEvent
|
||||
OnLeaveRoomEvent,
|
||||
OnErrorEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import ToastModule from './modules/Toast'
|
||||
import RoomDomain from './Room'
|
||||
import { map, merge } from 'rxjs'
|
||||
|
||||
const ToastDomain = Remesh.domain({
|
||||
name: 'ToastDomain',
|
||||
impl: (domain) => {
|
||||
return ToastModule(domain)
|
||||
const roomDomain = domain.getDomain(RoomDomain())
|
||||
const toastModule = ToastModule(domain)
|
||||
domain.effect({
|
||||
name: 'Toast.OnRoomErrorEffect',
|
||||
impl: ({ fromEvent }) => {
|
||||
const onRoomError$ = fromEvent(roomDomain.event.OnErrorEvent)
|
||||
|
||||
const onError$ = merge(onRoomError$).pipe(
|
||||
map((error) => {
|
||||
return toastModule.command.ErrorCommand(error.message)
|
||||
})
|
||||
)
|
||||
|
||||
return onError$
|
||||
}
|
||||
})
|
||||
return toastModule
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ export interface UserInfo {
|
|||
createTime: number
|
||||
themeMode: 'system' | 'light' | 'dark'
|
||||
danmakuEnabled: boolean
|
||||
notificationEnabled: boolean
|
||||
}
|
||||
|
||||
const UserInfoDomain = Remesh.domain({
|
||||
|
|
14
src/domain/externs/Notification.ts
Normal file
14
src/domain/externs/Notification.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Remesh } from 'remesh'
|
||||
import { TextMessage } from '../Room'
|
||||
|
||||
export interface Notification {
|
||||
push: (message: TextMessage) => Promise<string>
|
||||
}
|
||||
|
||||
export const NotificationExtern = Remesh.extern<Notification>({
|
||||
default: {
|
||||
push: () => {
|
||||
throw new Error('"push" not implemented.')
|
||||
}
|
||||
}
|
||||
})
|
|
@ -15,7 +15,7 @@ export class Danmaku {
|
|||
private manager?: Manager<TextMessage>
|
||||
constructor() {
|
||||
this.manager = create<TextMessage>({
|
||||
durationRange: [10000, 13000],
|
||||
durationRange: [7000, 10000],
|
||||
plugin: {
|
||||
$createNode(manager) {
|
||||
if (!manager.node) return
|
||||
|
|
15
src/domain/impls/Notification.ts
Normal file
15
src/domain/impls/Notification.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { NotificationExtern } from '@/domain/externs/Notification'
|
||||
import { TextMessage } from '../Room'
|
||||
import { EVENT } from '@/constants/event'
|
||||
import { messenger } from '@/messenger'
|
||||
|
||||
class Notification {
|
||||
messages: TextMessage[] = []
|
||||
async push(message: TextMessage) {
|
||||
await messenger.sendMessage(EVENT.NOTIFICATION_PUSH, message)
|
||||
this.messages.push(message)
|
||||
return message.id
|
||||
}
|
||||
}
|
||||
|
||||
export const NotificationImpl = NotificationExtern.impl(new Notification())
|
|
@ -145,3 +145,4 @@ export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
|
|||
// https://github.com/w3c/webextensions/issues/72
|
||||
// https://issues.chromium.org/issues/40251342
|
||||
// https://github.com/w3c/webrtc-extensions/issues/77
|
||||
// https://github.com/aklinker1/webext-core/pull/70
|
||||
|
|
|
@ -136,3 +136,4 @@ export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
|
|||
// https://github.com/w3c/webextensions/issues/72
|
||||
// https://issues.chromium.org/issues/40251342
|
||||
// https://github.com/w3c/webrtc-extensions/issues/77
|
||||
// https://github.com/aklinker1/webext-core/pull/70
|
||||
|
|
11
src/messenger/index.ts
Normal file
11
src/messenger/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { EVENT } from '@/constants/event'
|
||||
import { defineExtensionMessaging } from '@webext-core/messaging'
|
||||
import { TextMessage } from '@/domain/Room'
|
||||
|
||||
interface ProtocolMap {
|
||||
[EVENT.OPTIONS_PAGE_OPEN]: () => void
|
||||
[EVENT.NOTIFICATION_PUSH]: (message: TextMessage) => void
|
||||
[EVENT.NOTIFICATION_CLEAR]: (id: string) => void
|
||||
}
|
||||
|
||||
export const messenger = defineExtensionMessaging<ProtocolMap>()
|
9
src/utils/asyncMap.ts
Normal file
9
src/utils/asyncMap.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
const asyncMap = async <T = any, U = any>(list: T[], run: (arg: T, index: number, list: T[]) => Promise<U>) => {
|
||||
const task: U[] = []
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
task.push(await run(list[index], index, list))
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
export default asyncMap
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
|||
manifest: ({ browser }) => {
|
||||
const common = {
|
||||
name: displayName,
|
||||
permissions: ['storage'],
|
||||
permissions: ['storage', 'notifications'],
|
||||
homepage_url: homepage,
|
||||
icons: {
|
||||
'16': 'logo.png',
|
||||
|
|
Loading…
Reference in a new issue