feat: ranking of users supporting online websites Closes #48
This commit is contained in:
parent
00f0bd08b0
commit
d0fea9e42d
33 changed files with 1148 additions and 3036 deletions
24
package.json
24
package.json
|
@ -45,7 +45,6 @@
|
||||||
"homepage": "https://github.com/molvqingtai/WebChat",
|
"homepage": "https://github.com/molvqingtai/WebChat",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@lottiefiles/dotlottie-react": "^0.9.3",
|
|
||||||
"@perfsee/jsonr": "^1.13.0",
|
"@perfsee/jsonr": "^1.13.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
|
@ -57,7 +56,7 @@
|
||||||
"@radix-ui/react-portal": "^1.1.2",
|
"@radix-ui/react-portal": "^1.1.2",
|
||||||
"@radix-ui/react-presence": "^1.1.1",
|
"@radix-ui/react-presence": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.2.1",
|
"@radix-ui/react-radio-group": "^1.2.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@resreq/event-hub": "^1.6.0",
|
"@resreq/event-hub": "^1.6.0",
|
||||||
|
@ -70,13 +69,13 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"danmu": "^0.14.0",
|
"danmu": "^0.14.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.13",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.456.0",
|
||||||
"nanoid": "^5.0.8",
|
"nanoid": "^5.0.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
"react-virtuoso": "^4.12.0",
|
"react-virtuoso": "^4.12.0",
|
||||||
|
@ -88,7 +87,6 @@
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"trystero": "^0.20.0",
|
|
||||||
"type-fest": "^4.26.1",
|
"type-fest": "^4.26.1",
|
||||||
"unstorage": "^1.13.1",
|
"unstorage": "^1.13.1",
|
||||||
"valibot": "1.0.0-beta.0"
|
"valibot": "1.0.0-beta.0"
|
||||||
|
@ -96,18 +94,18 @@
|
||||||
"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.15.2",
|
"@eslint-react/eslint-plugin": "^1.16.1",
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"@semantic-release/exec": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/node": "^22.8.7",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/parser": "^8.12.2",
|
"@typescript-eslint/parser": "^8.14.0",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
@ -115,12 +113,12 @@
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.12.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"jiti": "^2.4.0",
|
"jiti": "^2.4.0",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.49",
|
||||||
"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": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
@ -128,7 +126,7 @@
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.12.2",
|
"typescript-eslint": "^8.14.0",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"webext-bridge": "^6.0.1",
|
"webext-bridge": "^6.0.1",
|
||||||
"wxt": "^0.19.13"
|
"wxt": "^0.19.13"
|
||||||
|
|
3118
pnpm-lock.yaml
3118
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ import Main from '@/app/content/views/Main'
|
||||||
import AppButton from '@/app/content/views/AppButton'
|
import AppButton from '@/app/content/views/AppButton'
|
||||||
import AppMain from '@/app/content/views/AppMain'
|
import AppMain from '@/app/content/views/AppMain'
|
||||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||||
import RoomDomain from '@/domain/Room'
|
import ChatRoomDomain from '@/domain/ChatRoom'
|
||||||
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'
|
||||||
|
@ -15,6 +15,7 @@ import DanmakuContainer from './components/DanmakuContainer'
|
||||||
import DanmakuDomain from '@/domain/Danmaku'
|
import DanmakuDomain from '@/domain/Danmaku'
|
||||||
import AppStatusDomain from '@/domain/AppStatus'
|
import AppStatusDomain from '@/domain/AppStatus'
|
||||||
import { checkDarkMode, cn } from '@/utils'
|
import { checkDarkMode, cn } from '@/utils'
|
||||||
|
import VirtualRoomDomain from '@/domain/VirtualRoom'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fix requestAnimationFrame error in jest
|
* Fix requestAnimationFrame error in jest
|
||||||
|
@ -27,7 +28,8 @@ if (import.meta.env.FIREFOX) {
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const chatRoomDomain = useRemeshDomain(ChatRoomDomain())
|
||||||
|
const virtualRoomDomain = useRemeshDomain(VirtualRoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const danmakuDomain = useRemeshDomain(DanmakuDomain())
|
const danmakuDomain = useRemeshDomain(DanmakuDomain())
|
||||||
|
@ -37,19 +39,32 @@ export default function App() {
|
||||||
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||||
const appStatusDomain = useRemeshDomain(AppStatusDomain())
|
const appStatusDomain = useRemeshDomain(AppStatusDomain())
|
||||||
const appStatusLoadIsFinished = useRemeshQuery(appStatusDomain.query.StatusLoadIsFinishedQuery())
|
const appStatusLoadIsFinished = useRemeshQuery(appStatusDomain.query.StatusLoadIsFinishedQuery())
|
||||||
|
const chatRoomJoinIsFinished = useRemeshQuery(chatRoomDomain.query.JoinIsFinishedQuery())
|
||||||
|
const virtualRoomJoinIsFinished = useRemeshQuery(virtualRoomDomain.query.JoinIsFinishedQuery())
|
||||||
|
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
||||||
|
|
||||||
|
const joinRoom = () => {
|
||||||
|
send(chatRoomDomain.command.JoinRoomCommand())
|
||||||
|
send(virtualRoomDomain.command.JoinRoomCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaveRoom = () => {
|
||||||
|
chatRoomJoinIsFinished && send(chatRoomDomain.command.LeaveRoomCommand())
|
||||||
|
virtualRoomJoinIsFinished && send(virtualRoomDomain.command.LeaveRoomCommand())
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messageListLoadFinished) {
|
if (messageListLoadFinished) {
|
||||||
if (userInfoSetFinished) {
|
if (userInfoSetFinished) {
|
||||||
send(roomDomain.command.JoinRoomCommand())
|
joinRoom()
|
||||||
} else {
|
} else {
|
||||||
// Clear simulated data when refreshing on the setup page
|
// Clear simulated data when refreshing on the setup page
|
||||||
send(messageListDomain.command.ClearListCommand())
|
send(messageListDomain.command.ClearListCommand())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return () => leaveRoom()
|
||||||
}, [userInfoSetFinished, messageListLoadFinished])
|
}, [userInfoSetFinished, messageListLoadFinished])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -59,6 +74,13 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, [danmakuIsEnabled])
|
}, [danmakuIsEnabled])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('beforeunload', leaveRoom)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', leaveRoom)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const themeMode =
|
const themeMode =
|
||||||
userInfo?.themeMode === 'system'
|
userInfo?.themeMode === 'system'
|
||||||
? checkDarkMode()
|
? checkDarkMode()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
|
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { TextMessage } from '@/domain/Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
import { cn } from '@/utils'
|
import { cn } from '@/utils'
|
||||||
import { AvatarImage } from '@radix-ui/react-avatar'
|
import { AvatarImage } from '@radix-ui/react-avatar'
|
||||||
import { FC, MouseEvent } from 'react'
|
import { FC, MouseEvent } from 'react'
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { LocalStorageImpl, IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/
|
||||||
import { DanmakuImpl } from '@/domain/impls/Danmaku'
|
import { DanmakuImpl } from '@/domain/impls/Danmaku'
|
||||||
import { NotificationImpl } from '@/domain/impls/Notification'
|
import { NotificationImpl } from '@/domain/impls/Notification'
|
||||||
import { ToastImpl } from '@/domain/impls/Toast'
|
import { ToastImpl } from '@/domain/impls/Toast'
|
||||||
// import { PeerRoomImpl } from '@/domain/impls/PeerRoom'
|
import { ChatRoomImpl } from '@/domain/impls/ChatRoom'
|
||||||
import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
|
import { VirtualRoomImpl } from '@/domain/impls/VirtualRoom'
|
||||||
// Remove import after merging: https://github.com/emilkowalski/sonner/pull/508
|
// Remove import after merging: https://github.com/emilkowalski/sonner/pull/508
|
||||||
import '@/assets/styles/sonner.css'
|
import '@/assets/styles/sonner.css'
|
||||||
import '@/assets/styles/overlay.css'
|
import '@/assets/styles/overlay.css'
|
||||||
|
@ -38,7 +38,8 @@ export default defineContentScript({
|
||||||
LocalStorageImpl,
|
LocalStorageImpl,
|
||||||
IndexDBStorageImpl,
|
IndexDBStorageImpl,
|
||||||
BrowserSyncStorageImpl,
|
BrowserSyncStorageImpl,
|
||||||
PeerRoomImpl,
|
ChatRoomImpl,
|
||||||
|
VirtualRoomImpl,
|
||||||
ToastImpl,
|
ToastImpl,
|
||||||
DanmakuImpl,
|
DanmakuImpl,
|
||||||
NotificationImpl
|
NotificationImpl
|
||||||
|
|
|
@ -6,7 +6,7 @@ import EmojiButton from '../../components/EmojiButton'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import MessageInputDomain from '@/domain/MessageInput'
|
import MessageInputDomain from '@/domain/MessageInput'
|
||||||
import { MESSAGE_MAX_LENGTH, WEB_RTC_MAX_MESSAGE_SIZE } from '@/constants/config'
|
import { MESSAGE_MAX_LENGTH, WEB_RTC_MAX_MESSAGE_SIZE } from '@/constants/config'
|
||||||
import RoomDomain from '@/domain/Room'
|
import ChatRoomDomain from '@/domain/ChatRoom'
|
||||||
import useCursorPosition from '@/hooks/useCursorPosition'
|
import useCursorPosition from '@/hooks/useCursorPosition'
|
||||||
import useShareRef from '@/hooks/useShareRef'
|
import useShareRef from '@/hooks/useShareRef'
|
||||||
import { Presence } from '@radix-ui/react-presence'
|
import { Presence } from '@radix-ui/react-presence'
|
||||||
|
@ -25,12 +25,12 @@ import { nanoid } from 'nanoid'
|
||||||
const Footer: FC = () => {
|
const Footer: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const toastDomain = useRemeshDomain(ToastDomain())
|
const toastDomain = useRemeshDomain(ToastDomain())
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const chatRoomDomain = useRemeshDomain(ChatRoomDomain())
|
||||||
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
||||||
const message = useRemeshQuery(messageInputDomain.query.MessageQuery())
|
const message = useRemeshQuery(messageInputDomain.query.MessageQuery())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
const userList = useRemeshQuery(roomDomain.query.UserListQuery())
|
const userList = useRemeshQuery(chatRoomDomain.query.UserListQuery())
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const { x, y, selectionStart, selectionEnd, setRef } = useCursorPosition()
|
const { x, y, selectionStart, selectionEnd, setRef } = useCursorPosition()
|
||||||
|
@ -143,7 +143,7 @@ const Footer: FC = () => {
|
||||||
return send(toastDomain.command.WarningCommand('Message size cannot exceed 256KiB.'))
|
return send(toastDomain.command.WarningCommand('Message size cannot exceed 256KiB.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
send(roomDomain.command.SendTextMessageCommand({ body: transformedMessage, atUsers }))
|
send(chatRoomDomain.command.SendTextMessageCommand({ body: transformedMessage, atUsers }))
|
||||||
send(messageInputDomain.command.ClearCommand())
|
send(messageInputDomain.command.ClearCommand())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,39 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/H
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { cn, getSiteInfo } from '@/utils'
|
import { cn, getSiteInfo } from '@/utils'
|
||||||
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
||||||
import RoomDomain from '@/domain/Room'
|
import ChatRoomDomain from '@/domain/ChatRoom'
|
||||||
|
import VirtualRoomDomain, { FromInfo, RoomUser } from '@/domain/VirtualRoom'
|
||||||
import { ScrollArea } from '@/components/ui/ScrollArea'
|
import { ScrollArea } from '@/components/ui/ScrollArea'
|
||||||
import { Virtuoso } from 'react-virtuoso'
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
|
import AvatarCircles from '@/components/magicui/AvatarCircles'
|
||||||
|
import Link from '@/components/Link'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
const siteInfo = getSiteInfo()
|
const siteInfo = getSiteInfo()
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const chatRoomDomain = useRemeshDomain(ChatRoomDomain())
|
||||||
const userList = useRemeshQuery(roomDomain.query.UserListQuery())
|
const virtualRoomDomain = useRemeshDomain(VirtualRoomDomain())
|
||||||
const onlineCount = userList.length
|
const chatUserList = useRemeshQuery(chatRoomDomain.query.UserListQuery())
|
||||||
|
const virtualUserList = useRemeshQuery(virtualRoomDomain.query.UserListQuery())
|
||||||
|
const chatOnlineCount = chatUserList.length
|
||||||
|
|
||||||
const [scrollParentRef, setScrollParentRef] = useState<HTMLDivElement | null>(null)
|
const virtualOnlineGroup = virtualUserList
|
||||||
|
.flatMap((user) => user.fromInfos.map((from) => ({ from, user })))
|
||||||
|
.reduce<(FromInfo & { users: RoomUser[] })[]>((acc, item) => {
|
||||||
|
const existSite = acc.find((group) => group.origin === item.from.origin)
|
||||||
|
if (existSite) {
|
||||||
|
const existUser = existSite.users.find((user) => user.userId === item.user.userId)
|
||||||
|
!existUser && existSite.users.push(item.user)
|
||||||
|
} else {
|
||||||
|
acc.push({ ...item.from, users: [item.user] })
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => b.users.length - a.users.length)
|
||||||
|
|
||||||
|
const [chatUserListScrollParentRef, setChatUserListScrollParentRef] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [virtualOnlineGroupScrollParentRef, setVirtualOnlineGroupScrollParentRef] = useState<HTMLDivElement | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-10 grid h-12 grid-flow-col grid-cols-[theme('spacing.20')_auto_theme('spacing.20')] items-center justify-between rounded-t-xl bg-white px-4 backdrop-blur-lg dark:bg-slate-950">
|
<div className="z-10 grid h-12 grid-flow-col grid-cols-[theme('spacing.20')_auto_theme('spacing.20')] items-center justify-between rounded-t-xl bg-white px-4 backdrop-blur-lg dark:bg-slate-950">
|
||||||
|
@ -33,23 +55,55 @@ const Header: FC = () => {
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-80 rounded-lg">
|
<HoverCardContent className="w-80 rounded-lg p-0">
|
||||||
<div className="grid grid-cols-[auto_1fr] gap-x-4">
|
<ScrollArea className="max-h-96 min-h-[72px] p-2" ref={setVirtualOnlineGroupScrollParentRef}>
|
||||||
<Avatar className="size-14">
|
<Virtuoso
|
||||||
<AvatarImage src={siteInfo.icon} alt="favicon" />
|
data={virtualOnlineGroup}
|
||||||
|
defaultItemHeight={56}
|
||||||
|
customScrollParent={virtualOnlineGroupScrollParentRef!}
|
||||||
|
itemContent={(_index, site) => (
|
||||||
|
<Link
|
||||||
|
underline={false}
|
||||||
|
href={site.origin}
|
||||||
|
className="grid cursor-pointer grid-cols-[auto_1fr] items-center gap-x-2 rounded-lg px-2 py-1.5 hover:bg-accent hover:text-accent-foreground"
|
||||||
|
>
|
||||||
|
<Avatar className="size-10">
|
||||||
|
<AvatarImage src={site.icon} alt="favicon" />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
<Globe2Icon size="100%" className="text-gray-400" />
|
<Globe2Icon size="100%" className="text-gray-400" />
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid items-center">
|
<div className="grid items-center">
|
||||||
<h4 className="truncate text-sm font-semibold">{siteInfo.title}</h4>
|
<div className="flex items-center gap-x-1 overflow-hidden">
|
||||||
{siteInfo.description && (
|
<h4 className="flex-1 truncate text-sm font-semibold">{site.hostname.replace(/^www\./i, '')}</h4>
|
||||||
<p className="line-clamp-2 max-h-8 text-xs text-slate-500 dark:text-slate-300">
|
<div className="shrink-0 text-sm">
|
||||||
{siteInfo.description}
|
<div className="flex items-center gap-x-1 text-nowrap text-xs text-slate-500">
|
||||||
</p>
|
<span className="relative flex size-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute inline-flex size-full animate-ping rounded-full opacity-75',
|
||||||
|
site.users.length > 1 ? 'bg-green-400' : 'bg-orange-400'
|
||||||
)}
|
)}
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex size-full rounded-full',
|
||||||
|
site.users.length > 1 ? 'bg-green-500' : 'bg-orange-500'
|
||||||
|
)}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<span className="dark:text-slate-50">
|
||||||
|
ONLINE {site.users.length > 99 ? '99+' : site.users.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<AvatarCircles max={9} size="xs" avatarUrls={site.users.map((user) => user.userAvatar)} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
></Virtuoso>
|
||||||
|
</ScrollArea>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
|
@ -60,26 +114,26 @@ const Header: FC = () => {
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inline-flex size-full animate-ping rounded-full opacity-75',
|
'absolute inline-flex size-full animate-ping rounded-full opacity-75',
|
||||||
onlineCount > 1 ? 'bg-green-400' : 'bg-orange-400'
|
chatOnlineCount > 1 ? 'bg-green-400' : 'bg-orange-400'
|
||||||
)}
|
)}
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex size-2 rounded-full',
|
'relative inline-flex size-full rounded-full',
|
||||||
onlineCount > 1 ? 'bg-green-500' : 'bg-orange-500'
|
chatOnlineCount > 1 ? 'bg-green-500' : 'bg-orange-500'
|
||||||
)}
|
)}
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="dark:text-slate-50">ONLINE {onlineCount > 99 ? '99+' : onlineCount}</span>
|
<span className="dark:text-slate-50">ONLINE {chatOnlineCount > 99 ? '99+' : chatOnlineCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-36 rounded-lg p-0">
|
<HoverCardContent className="w-36 rounded-lg p-0">
|
||||||
<ScrollArea className="max-h-[204px] min-h-9 p-1" ref={setScrollParentRef}>
|
<ScrollArea className="max-h-[204px] min-h-9 p-1" ref={setChatUserListScrollParentRef}>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={userList}
|
data={chatUserList}
|
||||||
defaultItemHeight={28}
|
defaultItemHeight={28}
|
||||||
customScrollParent={scrollParentRef!}
|
customScrollParent={chatUserListScrollParentRef!}
|
||||||
itemContent={(index, user) => (
|
itemContent={(index, user) => (
|
||||||
<div className={cn('flex items-center gap-x-2 rounded-md px-2 py-1.5 outline-none')}>
|
<div className={cn('flex items-center gap-x-2 rounded-md px-2 py-1.5 outline-none')}>
|
||||||
<Avatar className="size-4 shrink-0">
|
<Avatar className="size-4 shrink-0">
|
||||||
|
|
|
@ -5,13 +5,13 @@ import MessageList from '../../components/MessageList'
|
||||||
import MessageItem from '../../components/MessageItem'
|
import MessageItem from '../../components/MessageItem'
|
||||||
import PromptItem from '../../components/PromptItem'
|
import PromptItem from '../../components/PromptItem'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import RoomDomain, { MessageType } from '@/domain/Room'
|
import ChatRoomDomain, { MessageType } from '@/domain/ChatRoom'
|
||||||
import MessageListDomain from '@/domain/MessageList'
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
|
|
||||||
const Main: FC = () => {
|
const Main: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const chatRoomDomain = useRemeshDomain(ChatRoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
const _messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
const _messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
||||||
|
@ -29,11 +29,11 @@ const Main: FC = () => {
|
||||||
.toSorted((a, b) => a.sendTime - b.sendTime)
|
.toSorted((a, b) => a.sendTime - b.sendTime)
|
||||||
|
|
||||||
const handleLikeChange = (messageId: string) => {
|
const handleLikeChange = (messageId: string) => {
|
||||||
send(roomDomain.command.SendLikeMessageCommand(messageId))
|
send(chatRoomDomain.command.SendLikeMessageCommand(messageId))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleHateChange = (messageId: string) => {
|
const handleHateChange = (messageId: string) => {
|
||||||
send(roomDomain.command.SendHateMessageCommand(messageId))
|
send(chatRoomDomain.command.SendHateMessageCommand(messageId))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,11 +5,18 @@ export interface LinkProps {
|
||||||
href: string
|
href: string
|
||||||
className?: string
|
className?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
underline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Link = forwardRef<HTMLAnchorElement, LinkProps>(({ href, className, children }, ref) => {
|
const Link = forwardRef<HTMLAnchorElement, LinkProps>(({ href, className, children, underline = true }, ref) => {
|
||||||
return (
|
return (
|
||||||
<a href={href} target={href} rel="noopener noreferrer" className={cn('hover:underline', className)} ref={ref}>
|
<a
|
||||||
|
href={href}
|
||||||
|
target={href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={cn(underline && 'hover:underline', className)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
69
src/components/magicui/AvatarCircles.tsx
Normal file
69
src/components/magicui/AvatarCircles.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/index'
|
||||||
|
|
||||||
|
interface AvatarCirclesProps {
|
||||||
|
className?: string
|
||||||
|
avatarUrls: string[]
|
||||||
|
size?: VariantProps<typeof SizeVariants>['size']
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeVariants = cva('z-10 flex -space-x-4 rtl:space-x-reverse', {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default: 'h-10 min-w-10',
|
||||||
|
sm: 'h-8 min-w-8',
|
||||||
|
xs: 'h-6 min-w-6',
|
||||||
|
lg: 'h-12 min-w-12'
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const spaceVariants = cva('flex -space-x-4 rtl:space-x-reverse', {
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default: '-space-x-4',
|
||||||
|
sm: '-space-x-3',
|
||||||
|
xs: '-space-x-2',
|
||||||
|
lg: '-space-x-5'
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const AvatarCircles = ({ className, avatarUrls, size, max = 10 }: AvatarCirclesProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn(spaceVariants({ size }), className)}>
|
||||||
|
{avatarUrls.slice(0, max).map((url, index) => (
|
||||||
|
<img
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
'rounded-full border-2 border-white dark:border-slate-800 aspect-square',
|
||||||
|
SizeVariants({ size })
|
||||||
|
)}
|
||||||
|
src={url}
|
||||||
|
alt={`Avatar ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center rounded-full border-2 border-white bg-slate-600 text-center text-xs font-medium text-white dark:border-slate-800 p-1',
|
||||||
|
SizeVariants({ size }),
|
||||||
|
size === 'xs' && 'text-2xs'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
+{avatarUrls.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarCircles
|
|
@ -207,3 +207,5 @@ export const SYNC_HISTORY_MAX_DAYS = 30 as const
|
||||||
* Message max size is 256KiB; if the message is too large, it will cause the connection to drop.
|
* Message max size is 256KiB; if the message is too large, it will cause the connection to drop.
|
||||||
*/
|
*/
|
||||||
export const WEB_RTC_MAX_MESSAGE_SIZE = 262144 as const
|
export const WEB_RTC_MAX_MESSAGE_SIZE = 262144 as const
|
||||||
|
|
||||||
|
export const VIRTUAL_ROOM_ID = 'WEB_CHAT_VIRTUAL_ROOM' as const
|
||||||
|
|
|
@ -3,7 +3,7 @@ import StatusModule from './modules/Status'
|
||||||
import { LocalStorageExtern } from './externs/Storage'
|
import { LocalStorageExtern } from './externs/Storage'
|
||||||
import { APP_STATUS_STORAGE_KEY } from '@/constants/config'
|
import { APP_STATUS_STORAGE_KEY } from '@/constants/config'
|
||||||
import StorageEffect from './modules/StorageEffect'
|
import StorageEffect from './modules/StorageEffect'
|
||||||
import RoomDomain, { SendType } from './Room'
|
import ChatRoomDomain, { SendType } from '@/domain/ChatRoom'
|
||||||
import { map } from 'rxjs'
|
import { map } from 'rxjs'
|
||||||
|
|
||||||
export interface AppStatus {
|
export interface AppStatus {
|
||||||
|
@ -26,7 +26,7 @@ const AppStatusDomain = Remesh.domain({
|
||||||
extern: LocalStorageExtern,
|
extern: LocalStorageExtern,
|
||||||
key: APP_STATUS_STORAGE_KEY
|
key: APP_STATUS_STORAGE_KEY
|
||||||
})
|
})
|
||||||
const roomDomain = domain.getDomain(RoomDomain())
|
const chatRoomDomain = domain.getDomain(ChatRoomDomain())
|
||||||
|
|
||||||
const StatusLoadModule = StatusModule(domain, {
|
const StatusLoadModule = StatusModule(domain, {
|
||||||
name: 'AppStatus.LoadStatusModule'
|
name: 'AppStatus.LoadStatusModule'
|
||||||
|
@ -131,7 +131,7 @@ const AppStatusDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'OnMessageEffect',
|
name: 'OnMessageEffect',
|
||||||
impl: ({ fromEvent, get }) => {
|
impl: ({ fromEvent, get }) => {
|
||||||
const onMessage$ = fromEvent(roomDomain.event.OnMessageEvent).pipe(
|
const onMessage$ = fromEvent(chatRoomDomain.event.OnMessageEvent).pipe(
|
||||||
map((message) => {
|
map((message) => {
|
||||||
const status = get(StatusState())
|
const status = get(StatusState())
|
||||||
if (!status.open && message.type === SendType.Text) {
|
if (!status.open && message.type === SendType.Text) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { map, merge, of, EMPTY, mergeMap, fromEvent, fromEventPattern } from 'rxjs'
|
import { map, merge, of, EMPTY, mergeMap, fromEventPattern } from 'rxjs'
|
||||||
import { AtUser, NormalMessage, type MessageUser } from './MessageList'
|
import { AtUser, NormalMessage, type MessageUser } from './MessageList'
|
||||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
import { ChatRoomExtern } from '@/domain/externs/ChatRoom'
|
||||||
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import { desert, getTextByteSize, upsert } from '@/utils'
|
import { desert, getTextByteSize, upsert } from '@/utils'
|
||||||
|
@ -127,16 +127,16 @@ const RoomMessageSchema = v.union([
|
||||||
const checkMessageFormat = (message: v.InferInput<typeof RoomMessageSchema>) =>
|
const checkMessageFormat = (message: v.InferInput<typeof RoomMessageSchema>) =>
|
||||||
v.safeParse(RoomMessageSchema, message).success
|
v.safeParse(RoomMessageSchema, message).success
|
||||||
|
|
||||||
const RoomDomain = Remesh.domain({
|
const ChatRoomDomain = Remesh.domain({
|
||||||
name: 'RoomDomain',
|
name: 'ChatRoomDomain',
|
||||||
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 peerRoom = domain.getExtern(PeerRoomExtern)
|
const chatRoomExtern = domain.getExtern(ChatRoomExtern)
|
||||||
|
|
||||||
const PeerIdState = domain.state<string>({
|
const PeerIdState = domain.state<string>({
|
||||||
name: 'Room.PeerIdState',
|
name: 'Room.PeerIdState',
|
||||||
default: peerRoom.peerId
|
default: chatRoomExtern.peerId
|
||||||
})
|
})
|
||||||
|
|
||||||
const PeerIdQuery = domain.query({
|
const PeerIdQuery = domain.query({
|
||||||
|
@ -165,7 +165,7 @@ const RoomDomain = Remesh.domain({
|
||||||
const SelfUserQuery = domain.query({
|
const SelfUserQuery = domain.query({
|
||||||
name: 'Room.SelfUserQuery',
|
name: 'Room.SelfUserQuery',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
return get(UserListQuery()).find((user) => user.peerIds.includes(peerRoom.peerId))!
|
return get(UserListQuery()).find((user) => user.peerIds.includes(chatRoomExtern.peerId))!
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ const RoomDomain = Remesh.domain({
|
||||||
return [
|
return [
|
||||||
UpdateUserListCommand({
|
UpdateUserListCommand({
|
||||||
type: 'create',
|
type: 'create',
|
||||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
user: { peerId: chatRoomExtern.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||||
}),
|
}),
|
||||||
messageListDomain.command.CreateItemCommand({
|
messageListDomain.command.CreateItemCommand({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
|
@ -202,14 +202,14 @@ const RoomDomain = Remesh.domain({
|
||||||
receiveTime: Date.now()
|
receiveTime: Date.now()
|
||||||
}),
|
}),
|
||||||
JoinStatusModule.command.SetFinishedCommand(),
|
JoinStatusModule.command.SetFinishedCommand(),
|
||||||
JoinRoomEvent(peerRoom.roomId),
|
JoinRoomEvent(chatRoomExtern.roomId),
|
||||||
SelfJoinRoomEvent(peerRoom.roomId)
|
SelfJoinRoomEvent(chatRoomExtern.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
JoinRoomCommand.after(() => {
|
JoinRoomCommand.after(() => {
|
||||||
peerRoom.joinRoom()
|
chatRoomExtern.joinRoom()
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -230,17 +230,17 @@ const RoomDomain = Remesh.domain({
|
||||||
}),
|
}),
|
||||||
UpdateUserListCommand({
|
UpdateUserListCommand({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
user: { peerId: chatRoomExtern.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||||
}),
|
}),
|
||||||
JoinStatusModule.command.SetInitialCommand(),
|
JoinStatusModule.command.SetInitialCommand(),
|
||||||
LeaveRoomEvent(peerRoom.roomId),
|
LeaveRoomEvent(chatRoomExtern.roomId),
|
||||||
SelfLeaveRoomEvent(peerRoom.roomId)
|
SelfLeaveRoomEvent(chatRoomExtern.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
LeaveRoomCommand.after(() => {
|
LeaveRoomCommand.after(() => {
|
||||||
peerRoom.leaveRoom()
|
chatRoomExtern.leaveRoom()
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ const RoomDomain = Remesh.domain({
|
||||||
atUsers: typeof message === 'string' ? [] : message.atUsers
|
atUsers: typeof message === 'string' ? [] : message.atUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
peerRoom.sendMessage(textMessage)
|
chatRoomExtern.sendMessage(textMessage)
|
||||||
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
|
return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -288,7 +288,7 @@ const RoomDomain = Remesh.domain({
|
||||||
...localMessage,
|
...localMessage,
|
||||||
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage(likeMessage)
|
chatRoomExtern.sendMessage(likeMessage)
|
||||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
|
return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -309,7 +309,7 @@ const RoomDomain = Remesh.domain({
|
||||||
...localMessage,
|
...localMessage,
|
||||||
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage(hateMessage)
|
chatRoomExtern.sendMessage(hateMessage)
|
||||||
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
|
return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -323,13 +323,13 @@ const RoomDomain = Remesh.domain({
|
||||||
const syncUserMessage: SyncUserMessage = {
|
const syncUserMessage: SyncUserMessage = {
|
||||||
...self,
|
...self,
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
peerId: peerRoom.peerId,
|
peerId: chatRoomExtern.peerId,
|
||||||
sendTime: Date.now(),
|
sendTime: Date.now(),
|
||||||
lastMessageTime,
|
lastMessageTime,
|
||||||
type: SendType.SyncUser
|
type: SendType.SyncUser
|
||||||
}
|
}
|
||||||
|
|
||||||
peerRoom.sendMessage(syncUserMessage, peerId)
|
chatRoomExtern.sendMessage(syncUserMessage, peerId)
|
||||||
return [SendSyncUserMessageEvent(syncUserMessage)]
|
return [SendSyncUserMessageEvent(syncUserMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -395,7 +395,7 @@ const RoomDomain = Remesh.domain({
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return pushHistoryMessageList.map((message) => {
|
return pushHistoryMessageList.map((message) => {
|
||||||
peerRoom.sendMessage(message, peerId)
|
chatRoomExtern.sendMessage(message, peerId)
|
||||||
return SendSyncHistoryMessageEvent(message)
|
return SendSyncHistoryMessageEvent(message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -411,7 +411,7 @@ const RoomDomain = Remesh.domain({
|
||||||
UserListState().new(
|
UserListState().new(
|
||||||
upsert(
|
upsert(
|
||||||
userList,
|
userList,
|
||||||
{ ...action.user, peerIds: [...(existUser?.peerIds || []), action.user.peerId] },
|
{ ...action.user, peerIds: [...new Set(existUser?.peerIds || []), action.user.peerId] },
|
||||||
'userId'
|
'userId'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -492,10 +492,10 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Room.OnJoinRoomEffect',
|
name: 'Room.OnJoinRoomEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
const onJoinRoom$ = fromEventPattern<string>(peerRoom.onJoinRoom).pipe(
|
const onJoinRoom$ = fromEventPattern<string>(chatRoomExtern.onJoinRoom).pipe(
|
||||||
mergeMap((peerId) => {
|
mergeMap((peerId) => {
|
||||||
// console.log('onJoinRoom', peerId)
|
// console.log('onJoinRoom', peerId)
|
||||||
if (peerRoom.peerId === peerId) {
|
if (chatRoomExtern.peerId === peerId) {
|
||||||
return [OnJoinRoomEvent(peerId)]
|
return [OnJoinRoomEvent(peerId)]
|
||||||
} else {
|
} else {
|
||||||
return [SendSyncUserMessageCommand(peerId), OnJoinRoomEvent(peerId)]
|
return [SendSyncUserMessageCommand(peerId), OnJoinRoomEvent(peerId)]
|
||||||
|
@ -509,7 +509,7 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Room.OnMessageEffect',
|
name: 'Room.OnMessageEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onMessage$ = fromEventPattern<RoomMessage>(peerRoom.onMessage).pipe(
|
const onMessage$ = fromEventPattern<RoomMessage>(chatRoomExtern.onMessage).pipe(
|
||||||
mergeMap((message) => {
|
mergeMap((message) => {
|
||||||
// Filter out messages that do not conform to the format
|
// Filter out messages that do not conform to the format
|
||||||
if (!checkMessageFormat(message)) {
|
if (!checkMessageFormat(message)) {
|
||||||
|
@ -606,7 +606,7 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Room.OnLeaveRoomEffect',
|
name: 'Room.OnLeaveRoomEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
|
const onLeaveRoom$ = fromEventPattern<string>(chatRoomExtern.onLeaveRoom).pipe(
|
||||||
map((peerId) => {
|
map((peerId) => {
|
||||||
if (get(JoinStatusModule.query.IsInitialQuery())) {
|
if (get(JoinStatusModule.query.IsInitialQuery())) {
|
||||||
return null
|
return null
|
||||||
|
@ -642,7 +642,7 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Room.OnErrorEffect',
|
name: 'Room.OnErrorEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
const onRoomError$ = fromEventPattern<Error>(peerRoom.onError).pipe(
|
const onRoomError$ = fromEventPattern<Error>(chatRoomExtern.onError).pipe(
|
||||||
map((error) => {
|
map((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return OnErrorEvent(error)
|
return OnErrorEvent(error)
|
||||||
|
@ -652,18 +652,6 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
domain.effect({
|
|
||||||
name: 'Room.OnUnloadEffect',
|
|
||||||
impl: ({ get }) => {
|
|
||||||
const beforeUnload$ = fromEvent(window, 'beforeunload').pipe(
|
|
||||||
map(() => {
|
|
||||||
return get(JoinStatusModule.query.IsFinishedQuery()) ? LeaveRoomCommand() : null
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return beforeUnload$
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
PeerIdQuery,
|
PeerIdQuery,
|
||||||
|
@ -699,4 +687,4 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default RoomDomain
|
export default ChatRoomDomain
|
|
@ -1,15 +1,15 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { DanmakuExtern } from './externs/Danmaku'
|
import { DanmakuExtern } from './externs/Danmaku'
|
||||||
import RoomDomain, { TextMessage } from './Room'
|
import ChatRoomDomain, { TextMessage } from '@/domain/ChatRoom'
|
||||||
import UserInfoDomain from './UserInfo'
|
import UserInfoDomain from './UserInfo'
|
||||||
import { map, merge } from 'rxjs'
|
import { map, merge } from 'rxjs'
|
||||||
|
|
||||||
const DanmakuDomain = Remesh.domain({
|
const DanmakuDomain = Remesh.domain({
|
||||||
name: 'DanmakuDomain',
|
name: 'DanmakuDomain',
|
||||||
impl: (domain) => {
|
impl: (domain) => {
|
||||||
const danmaku = domain.getExtern(DanmakuExtern)
|
const danmakuExtern = domain.getExtern(DanmakuExtern)
|
||||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
const roomDomain = domain.getDomain(RoomDomain())
|
const chatRoomDomain = domain.getDomain(ChatRoomDomain())
|
||||||
|
|
||||||
const MountState = domain.state({
|
const MountState = domain.state({
|
||||||
name: 'Danmaku.MountState',
|
name: 'Danmaku.MountState',
|
||||||
|
@ -49,7 +49,7 @@ const DanmakuDomain = Remesh.domain({
|
||||||
const PushCommand = domain.command({
|
const PushCommand = domain.command({
|
||||||
name: 'Danmaku.PushCommand',
|
name: 'Danmaku.PushCommand',
|
||||||
impl: (_, message: TextMessage) => {
|
impl: (_, message: TextMessage) => {
|
||||||
danmaku.push(message)
|
danmakuExtern.push(message)
|
||||||
return [PushEvent(message)]
|
return [PushEvent(message)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -57,7 +57,7 @@ const DanmakuDomain = Remesh.domain({
|
||||||
const UnshiftCommand = domain.command({
|
const UnshiftCommand = domain.command({
|
||||||
name: 'Danmaku.UnshiftCommand',
|
name: 'Danmaku.UnshiftCommand',
|
||||||
impl: (_, message: TextMessage) => {
|
impl: (_, message: TextMessage) => {
|
||||||
danmaku.unshift(message)
|
danmakuExtern.unshift(message)
|
||||||
return [UnshiftEvent(message)]
|
return [UnshiftEvent(message)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -65,7 +65,7 @@ const DanmakuDomain = Remesh.domain({
|
||||||
const ClearCommand = domain.command({
|
const ClearCommand = domain.command({
|
||||||
name: 'Danmaku.ClearCommand',
|
name: 'Danmaku.ClearCommand',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
danmaku.clear()
|
danmakuExtern.clear()
|
||||||
return [ClearEvent()]
|
return [ClearEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -73,7 +73,7 @@ const DanmakuDomain = Remesh.domain({
|
||||||
const MountCommand = domain.command({
|
const MountCommand = domain.command({
|
||||||
name: 'Danmaku.ClearCommand',
|
name: 'Danmaku.ClearCommand',
|
||||||
impl: (_, container: HTMLElement) => {
|
impl: (_, container: HTMLElement) => {
|
||||||
danmaku.mount(container)
|
danmakuExtern.mount(container)
|
||||||
return [MountEvent(container)]
|
return [MountEvent(container)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -81,7 +81,7 @@ const DanmakuDomain = Remesh.domain({
|
||||||
const UnmountCommand = domain.command({
|
const UnmountCommand = domain.command({
|
||||||
name: 'Danmaku.UnmountCommand',
|
name: 'Danmaku.UnmountCommand',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
danmaku.unmount()
|
danmakuExtern.unmount()
|
||||||
return [UnmountEvent()]
|
return [UnmountEvent()]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -121,8 +121,8 @@ const DanmakuDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Danmaku.OnRoomMessageEffect',
|
name: 'Danmaku.OnRoomMessageEffect',
|
||||||
impl: ({ fromEvent, get }) => {
|
impl: ({ fromEvent, get }) => {
|
||||||
const sendTextMessage$ = fromEvent(roomDomain.event.SendTextMessageEvent)
|
const sendTextMessage$ = fromEvent(chatRoomDomain.event.SendTextMessageEvent)
|
||||||
const onTextMessage$ = fromEvent(roomDomain.event.OnTextMessageEvent)
|
const onTextMessage$ = fromEvent(chatRoomDomain.event.OnTextMessageEvent)
|
||||||
|
|
||||||
const onMessage$ = merge(sendTextMessage$, onTextMessage$).pipe(
|
const onMessage$ = merge(sendTextMessage$, onTextMessage$).pipe(
|
||||||
map((message) => {
|
map((message) => {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { NotificationExtern } from './externs/Notification'
|
import { NotificationExtern } from './externs/Notification'
|
||||||
import RoomDomain, { TextMessage } from './Room'
|
import ChatRoomDomain, { TextMessage } from '@/domain/ChatRoom'
|
||||||
import UserInfoDomain from './UserInfo'
|
import UserInfoDomain from './UserInfo'
|
||||||
import { map, merge } from 'rxjs'
|
import { map, merge } from 'rxjs'
|
||||||
|
|
||||||
const NotificationDomain = Remesh.domain({
|
const NotificationDomain = Remesh.domain({
|
||||||
name: 'NotificationDomain',
|
name: 'NotificationDomain',
|
||||||
impl: (domain) => {
|
impl: (domain) => {
|
||||||
const notification = domain.getExtern(NotificationExtern)
|
const notificationExtern = domain.getExtern(NotificationExtern)
|
||||||
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
const roomDomain = domain.getDomain(RoomDomain())
|
const chatRoomDomain = domain.getDomain(ChatRoomDomain())
|
||||||
|
|
||||||
const NotificationEnabledState = domain.state<boolean>({
|
const NotificationEnabledState = domain.state<boolean>({
|
||||||
name: 'Notification.EnabledState',
|
name: 'Notification.EnabledState',
|
||||||
|
@ -40,7 +40,7 @@ const NotificationDomain = Remesh.domain({
|
||||||
const PushCommand = domain.command({
|
const PushCommand = domain.command({
|
||||||
name: 'Notification.PushCommand',
|
name: 'Notification.PushCommand',
|
||||||
impl: (_, message: TextMessage) => {
|
impl: (_, message: TextMessage) => {
|
||||||
notification.push(message)
|
notificationExtern.push(message)
|
||||||
return [PushEvent(message)]
|
return [PushEvent(message)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -68,7 +68,7 @@ const NotificationDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Notification.OnRoomMessageEffect',
|
name: 'Notification.OnRoomMessageEffect',
|
||||||
impl: ({ fromEvent, get }) => {
|
impl: ({ fromEvent, get }) => {
|
||||||
const onTextMessage$ = fromEvent(roomDomain.event.OnTextMessageEvent)
|
const onTextMessage$ = fromEvent(chatRoomDomain.event.OnTextMessageEvent)
|
||||||
const onMessage$ = merge(onTextMessage$).pipe(
|
const onMessage$ = merge(onTextMessage$).pipe(
|
||||||
map((message) => {
|
map((message) => {
|
||||||
const notificationEnabled = get(IsEnabledQuery())
|
const notificationEnabled = get(IsEnabledQuery())
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import ToastModule from './modules/Toast'
|
import ToastModule from './modules/Toast'
|
||||||
import RoomDomain, { SendType } from './Room'
|
import ChatRoomDomain, { SendType } from './ChatRoom'
|
||||||
import { filter, map } from 'rxjs'
|
import VirtualRoomDomain from './VirtualRoom'
|
||||||
|
import { filter, map, merge } from 'rxjs'
|
||||||
|
|
||||||
const ToastDomain = Remesh.domain({
|
const ToastDomain = Remesh.domain({
|
||||||
name: 'ToastDomain',
|
name: 'ToastDomain',
|
||||||
impl: (domain) => {
|
impl: (domain) => {
|
||||||
const roomDomain = domain.getDomain(RoomDomain())
|
const chatRoomDomain = domain.getDomain(ChatRoomDomain())
|
||||||
|
const virtualRoomDomain = domain.getDomain(VirtualRoomDomain())
|
||||||
const toastModule = ToastModule(domain)
|
const toastModule = ToastModule(domain)
|
||||||
|
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Toast.OnRoomSelfJoinRoomEffect',
|
name: 'Toast.OnRoomSelfJoinRoomEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const onRoomJoin$ = fromEvent(roomDomain.event.SelfJoinRoomEvent).pipe(
|
const onRoomJoin$ = fromEvent(chatRoomDomain.event.SelfJoinRoomEvent).pipe(
|
||||||
map(() => toastModule.command.LoadingCommand({ message: 'Connected to the chat.', duration: 3000 }))
|
map(() => toastModule.command.LoadingCommand({ message: 'Connected to the chat.', duration: 3000 }))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,7 +25,10 @@ const ToastDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Toast.OnRoomErrorEffect',
|
name: 'Toast.OnRoomErrorEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const onRoomError$ = fromEvent(roomDomain.event.OnErrorEvent).pipe(
|
const onRoomError$ = merge(
|
||||||
|
fromEvent(chatRoomDomain.event.OnErrorEvent),
|
||||||
|
fromEvent(virtualRoomDomain.event.OnErrorEvent)
|
||||||
|
).pipe(
|
||||||
map((error) => {
|
map((error) => {
|
||||||
return toastModule.command.ErrorCommand(error.message)
|
return toastModule.command.ErrorCommand(error.message)
|
||||||
})
|
})
|
||||||
|
@ -36,7 +41,7 @@ const ToastDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'Toast.OnSyncHistoryEffect',
|
name: 'Toast.OnSyncHistoryEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const onSyncHistory$ = fromEvent(roomDomain.event.OnMessageEvent).pipe(
|
const onSyncHistory$ = fromEvent(chatRoomDomain.event.OnMessageEvent).pipe(
|
||||||
filter((message) => message.type === SendType.SyncHistory),
|
filter((message) => message.type === SendType.SyncHistory),
|
||||||
map(() => toastModule.command.SuccessCommand('Syncing history messages.'))
|
map(() => toastModule.command.SuccessCommand('Syncing history messages.'))
|
||||||
)
|
)
|
||||||
|
|
381
src/domain/VirtualRoom.ts
Normal file
381
src/domain/VirtualRoom.ts
Normal file
|
@ -0,0 +1,381 @@
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
import { map, merge, of, EMPTY, mergeMap, fromEventPattern } from 'rxjs'
|
||||||
|
import { type MessageUser } from './MessageList'
|
||||||
|
import { VirtualRoomExtern } from '@/domain/externs/VirtualRoom'
|
||||||
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
|
import { upsert } from '@/utils'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import StatusModule from '@/domain/modules/Status'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import getSiteInfo, { SiteInfo } from '@/utils/getSiteInfo'
|
||||||
|
|
||||||
|
export enum SendType {
|
||||||
|
SyncUser = 'SyncUser'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FromInfo extends SiteInfo {
|
||||||
|
peerId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncUserMessage extends MessageUser {
|
||||||
|
type: SendType.SyncUser
|
||||||
|
id: string
|
||||||
|
peerId: string
|
||||||
|
joinTime: number
|
||||||
|
sendTime: number
|
||||||
|
fromInfo: FromInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoomMessage = SyncUserMessage
|
||||||
|
|
||||||
|
export type RoomUser = MessageUser & { peerIds: string[]; fromInfos: FromInfo[]; joinTime: number }
|
||||||
|
|
||||||
|
const MessageUserSchema = {
|
||||||
|
userId: v.string(),
|
||||||
|
username: v.string(),
|
||||||
|
userAvatar: v.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
const FromInfoSchema = {
|
||||||
|
peerId: v.string(),
|
||||||
|
host: v.string(),
|
||||||
|
hostname: v.string(),
|
||||||
|
href: v.string(),
|
||||||
|
origin: v.string(),
|
||||||
|
title: v.string(),
|
||||||
|
icon: v.string(),
|
||||||
|
description: v.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoomMessageSchema = v.union([
|
||||||
|
v.object({
|
||||||
|
type: v.literal(SendType.SyncUser),
|
||||||
|
id: v.string(),
|
||||||
|
peerId: v.string(),
|
||||||
|
joinTime: v.number(),
|
||||||
|
sendTime: v.number(),
|
||||||
|
fromInfo: v.object(FromInfoSchema),
|
||||||
|
...MessageUserSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
// Check if the message conforms to the format
|
||||||
|
const checkMessageFormat = (message: v.InferInput<typeof RoomMessageSchema>) =>
|
||||||
|
v.safeParse(RoomMessageSchema, message).success
|
||||||
|
|
||||||
|
const VirtualRoomDomain = Remesh.domain({
|
||||||
|
name: 'VirtualRoomDomain',
|
||||||
|
impl: (domain) => {
|
||||||
|
const userInfoDomain = domain.getDomain(UserInfoDomain())
|
||||||
|
const virtualRoomExtern = domain.getExtern(VirtualRoomExtern)
|
||||||
|
|
||||||
|
const PeerIdState = domain.state<string>({
|
||||||
|
name: 'Room.PeerIdState',
|
||||||
|
default: virtualRoomExtern.peerId
|
||||||
|
})
|
||||||
|
|
||||||
|
const PeerIdQuery = domain.query({
|
||||||
|
name: 'Room.PeerIdQuery',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(PeerIdState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const JoinStatusModule = StatusModule(domain, {
|
||||||
|
name: 'Room.JoinStatusModule'
|
||||||
|
})
|
||||||
|
|
||||||
|
const UserListState = domain.state<RoomUser[]>({
|
||||||
|
name: 'Room.UserListState',
|
||||||
|
default: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const UserListQuery = domain.query({
|
||||||
|
name: 'Room.UserListQuery',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(UserListState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelfUserQuery = domain.query({
|
||||||
|
name: 'Room.SelfUserQuery',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(UserListQuery()).find((user) => user.peerIds.includes(virtualRoomExtern.peerId))!
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const JoinIsFinishedQuery = JoinStatusModule.query.IsFinishedQuery
|
||||||
|
|
||||||
|
const JoinRoomCommand = domain.command({
|
||||||
|
name: 'Room.JoinRoomCommand',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
return [
|
||||||
|
UpdateUserListCommand({
|
||||||
|
type: 'create',
|
||||||
|
user: {
|
||||||
|
peerId: virtualRoomExtern.peerId,
|
||||||
|
fromInfo: { ...getSiteInfo(), peerId: virtualRoomExtern.peerId },
|
||||||
|
joinTime: Date.now(),
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
userAvatar
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
JoinStatusModule.command.SetFinishedCommand(),
|
||||||
|
JoinRoomEvent(virtualRoomExtern.roomId),
|
||||||
|
SelfJoinRoomEvent(virtualRoomExtern.roomId)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
JoinRoomCommand.after(() => {
|
||||||
|
virtualRoomExtern.joinRoom()
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const LeaveRoomCommand = domain.command({
|
||||||
|
name: 'Room.LeaveRoomCommand',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
|
return [
|
||||||
|
UpdateUserListCommand({
|
||||||
|
type: 'delete',
|
||||||
|
user: {
|
||||||
|
peerId: virtualRoomExtern.peerId,
|
||||||
|
fromInfo: { ...getSiteInfo(), peerId: virtualRoomExtern.peerId },
|
||||||
|
joinTime: Date.now(),
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
userAvatar
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
JoinStatusModule.command.SetInitialCommand(),
|
||||||
|
LeaveRoomEvent(virtualRoomExtern.roomId),
|
||||||
|
SelfLeaveRoomEvent(virtualRoomExtern.roomId)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
LeaveRoomCommand.after(() => {
|
||||||
|
virtualRoomExtern.leaveRoom()
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const UpdateUserListCommand = domain.command({
|
||||||
|
name: 'Room.UpdateUserListCommand',
|
||||||
|
impl: (
|
||||||
|
{ get },
|
||||||
|
action: {
|
||||||
|
type: 'create' | 'delete'
|
||||||
|
user: Omit<RoomUser, 'peerIds' | 'fromInfos'> & { peerId: string; fromInfo: FromInfo }
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const userList = get(UserListState())
|
||||||
|
const existUser = userList.find((user) => user.userId === action.user.userId)
|
||||||
|
if (action.type === 'create') {
|
||||||
|
return [
|
||||||
|
UserListState().new(
|
||||||
|
upsert(
|
||||||
|
userList,
|
||||||
|
{
|
||||||
|
...action.user,
|
||||||
|
peerIds: [...new Set(existUser?.peerIds || []), action.user.peerId],
|
||||||
|
fromInfos: upsert(existUser?.fromInfos || [], action.user.fromInfo, 'peerId')
|
||||||
|
},
|
||||||
|
'userId'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
UserListState().new(
|
||||||
|
upsert(
|
||||||
|
userList,
|
||||||
|
{
|
||||||
|
...action.user,
|
||||||
|
peerIds: existUser?.peerIds?.filter((peerId) => peerId !== action.user.peerId) || [],
|
||||||
|
fromInfos: existUser?.fromInfos?.filter((fromInfo) => fromInfo.peerId !== action.user.peerId) || []
|
||||||
|
},
|
||||||
|
'userId'
|
||||||
|
).filter((user) => user.peerIds.length)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SendSyncUserMessageCommand = domain.command({
|
||||||
|
name: 'Room.SendSyncUserMessageCommand',
|
||||||
|
impl: ({ get }, peerId: string) => {
|
||||||
|
const self = get(SelfUserQuery())
|
||||||
|
|
||||||
|
const syncUserMessage: SyncUserMessage = {
|
||||||
|
...self,
|
||||||
|
id: nanoid(),
|
||||||
|
peerId: virtualRoomExtern.peerId,
|
||||||
|
sendTime: Date.now(),
|
||||||
|
fromInfo: { ...getSiteInfo(), peerId: virtualRoomExtern.peerId },
|
||||||
|
type: SendType.SyncUser
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualRoomExtern.sendMessage(syncUserMessage, peerId)
|
||||||
|
return [SendSyncUserMessageEvent(syncUserMessage)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SendSyncUserMessageEvent = domain.event<SyncUserMessage>({
|
||||||
|
name: 'Room.SendSyncUserMessageEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const JoinRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.JoinRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const LeaveRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.LeaveRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const OnMessageEvent = domain.event<RoomMessage>({
|
||||||
|
name: 'Room.OnMessageEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const OnJoinRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.OnJoinRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelfJoinRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.SelfJoinRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const OnLeaveRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.OnLeaveRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelfLeaveRoomEvent = domain.event<string>({
|
||||||
|
name: 'Room.SelfLeaveRoomEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const OnErrorEvent = domain.event<Error>({
|
||||||
|
name: 'Room.OnErrorEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'Room.OnJoinRoomEffect',
|
||||||
|
impl: () => {
|
||||||
|
const onJoinRoom$ = fromEventPattern<string>(virtualRoomExtern.onJoinRoom).pipe(
|
||||||
|
mergeMap((peerId) => {
|
||||||
|
// console.log('onJoinRoom', peerId)
|
||||||
|
if (virtualRoomExtern.peerId === peerId) {
|
||||||
|
return [OnJoinRoomEvent(peerId)]
|
||||||
|
} else {
|
||||||
|
return [SendSyncUserMessageCommand(peerId), OnJoinRoomEvent(peerId)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return onJoinRoom$
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'Room.OnMessageEffect',
|
||||||
|
impl: () => {
|
||||||
|
const onMessage$ = fromEventPattern<RoomMessage>(virtualRoomExtern.onMessage).pipe(
|
||||||
|
mergeMap((message) => {
|
||||||
|
// Filter out messages that do not conform to the format
|
||||||
|
if (!checkMessageFormat(message)) {
|
||||||
|
console.warn('Invalid message format', message)
|
||||||
|
return EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageEvent$ = of(OnMessageEvent(message))
|
||||||
|
|
||||||
|
const messageCommand$ = (() => {
|
||||||
|
switch (message.type) {
|
||||||
|
case SendType.SyncUser: {
|
||||||
|
return of(UpdateUserListCommand({ type: 'create', user: message }))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('Unsupported message type', message)
|
||||||
|
return EMPTY
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return merge(messageEvent$, messageCommand$)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return onMessage$
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'Room.OnLeaveRoomEffect',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
const onLeaveRoom$ = fromEventPattern<string>(virtualRoomExtern.onLeaveRoom).pipe(
|
||||||
|
map((peerId) => {
|
||||||
|
if (get(JoinStatusModule.query.IsInitialQuery())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// console.log('onLeaveRoom', peerId)
|
||||||
|
|
||||||
|
const existUser = get(UserListQuery()).find((user) => user.peerIds.includes(peerId))
|
||||||
|
|
||||||
|
if (existUser) {
|
||||||
|
return [
|
||||||
|
UpdateUserListCommand({
|
||||||
|
type: 'delete',
|
||||||
|
user: { ...existUser, peerId, fromInfo: { ...getSiteInfo(), peerId } }
|
||||||
|
}),
|
||||||
|
OnLeaveRoomEvent(peerId)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [OnLeaveRoomEvent(peerId)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return onLeaveRoom$
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
domain.effect({
|
||||||
|
name: 'Room.OnErrorEffect',
|
||||||
|
impl: () => {
|
||||||
|
const onRoomError$ = fromEventPattern<Error>(virtualRoomExtern.onError).pipe(
|
||||||
|
map((error) => {
|
||||||
|
console.error(error)
|
||||||
|
return OnErrorEvent(error)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return onRoomError$
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
PeerIdQuery,
|
||||||
|
UserListQuery,
|
||||||
|
JoinIsFinishedQuery
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
JoinRoomCommand,
|
||||||
|
LeaveRoomCommand,
|
||||||
|
SendSyncUserMessageCommand
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
SendSyncUserMessageEvent,
|
||||||
|
JoinRoomEvent,
|
||||||
|
SelfJoinRoomEvent,
|
||||||
|
LeaveRoomEvent,
|
||||||
|
SelfLeaveRoomEvent,
|
||||||
|
OnMessageEvent,
|
||||||
|
OnJoinRoomEvent,
|
||||||
|
OnLeaveRoomEvent,
|
||||||
|
OnErrorEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default VirtualRoomDomain
|
|
@ -1,19 +1,19 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { RoomMessage } from '../Room'
|
import { RoomMessage } from '../ChatRoom'
|
||||||
|
|
||||||
export interface PeerRoom {
|
export interface ChatRoom {
|
||||||
readonly peerId: string
|
readonly peerId: string
|
||||||
readonly roomId: string
|
readonly roomId: string
|
||||||
joinRoom: () => PeerRoom
|
joinRoom: () => ChatRoom
|
||||||
sendMessage: (message: RoomMessage, id?: string | string[]) => PeerRoom
|
sendMessage: (message: RoomMessage, id?: string | string[]) => ChatRoom
|
||||||
onMessage: (callback: (message: RoomMessage) => void) => PeerRoom
|
onMessage: (callback: (message: RoomMessage) => void) => ChatRoom
|
||||||
leaveRoom: () => PeerRoom
|
leaveRoom: () => ChatRoom
|
||||||
onJoinRoom: (callback: (id: string) => void) => PeerRoom
|
onJoinRoom: (callback: (id: string) => void) => ChatRoom
|
||||||
onLeaveRoom: (callback: (id: string) => void) => PeerRoom
|
onLeaveRoom: (callback: (id: string) => void) => ChatRoom
|
||||||
onError: (callback: (error: Error) => void) => PeerRoom
|
onError: (callback: (error: Error) => void) => ChatRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PeerRoomExtern = Remesh.extern<PeerRoom>({
|
export const ChatRoomExtern = Remesh.extern<ChatRoom>({
|
||||||
default: {
|
default: {
|
||||||
peerId: '',
|
peerId: '',
|
||||||
roomId: '',
|
roomId: '',
|
|
@ -1,5 +1,5 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { TextMessage } from '../Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
|
|
||||||
export interface Danmaku {
|
export interface Danmaku {
|
||||||
push: (message: TextMessage) => void
|
push: (message: TextMessage) => void
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { TextMessage } from '../Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
push: (message: TextMessage) => Promise<string>
|
push: (message: TextMessage) => Promise<string>
|
||||||
|
|
42
src/domain/externs/VirtualRoom.ts
Normal file
42
src/domain/externs/VirtualRoom.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
import { RoomMessage } from '@/domain/VirtualRoom'
|
||||||
|
|
||||||
|
export interface VirtualRoom {
|
||||||
|
readonly peerId: string
|
||||||
|
readonly roomId: string
|
||||||
|
joinRoom: () => VirtualRoom
|
||||||
|
sendMessage: (message: RoomMessage, id?: string | string[]) => VirtualRoom
|
||||||
|
onMessage: (callback: (message: RoomMessage) => void) => VirtualRoom
|
||||||
|
leaveRoom: () => VirtualRoom
|
||||||
|
onJoinRoom: (callback: (id: string) => void) => VirtualRoom
|
||||||
|
onLeaveRoom: (callback: (id: string) => void) => VirtualRoom
|
||||||
|
onError: (callback: (error: Error) => void) => VirtualRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VirtualRoomExtern = Remesh.extern<VirtualRoom>({
|
||||||
|
default: {
|
||||||
|
peerId: '',
|
||||||
|
roomId: '',
|
||||||
|
joinRoom: () => {
|
||||||
|
throw new Error('"joinRoom" not implemented.')
|
||||||
|
},
|
||||||
|
sendMessage: () => {
|
||||||
|
throw new Error('"sendMessage" not implemented.')
|
||||||
|
},
|
||||||
|
onMessage: () => {
|
||||||
|
throw new Error('"onMessage" not implemented.')
|
||||||
|
},
|
||||||
|
leaveRoom: () => {
|
||||||
|
throw new Error('"leaveRoom" not implemented.')
|
||||||
|
},
|
||||||
|
onJoinRoom: () => {
|
||||||
|
throw new Error('"onJoinRoom" not implemented.')
|
||||||
|
},
|
||||||
|
onLeaveRoom: () => {
|
||||||
|
throw new Error('"onLeaveRoom" not implemented.')
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
throw new Error('"onError" not implemented.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,27 +1,28 @@
|
||||||
import { Artico, Room } from '@rtco/client'
|
import { Room } from '@rtco/client'
|
||||||
|
|
||||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
import { ChatRoomExtern } from '@/domain/externs/ChatRoom'
|
||||||
import { stringToHex } from '@/utils'
|
import { stringToHex } from '@/utils'
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import EventHub from '@resreq/event-hub'
|
import EventHub from '@resreq/event-hub'
|
||||||
import { RoomMessage } from '../Room'
|
import { RoomMessage } from '@/domain/ChatRoom'
|
||||||
import { JSONR } from '@/utils'
|
import { JSONR } from '@/utils'
|
||||||
|
import Peer from './Peer'
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
peerId?: string
|
peer: Peer
|
||||||
roomId: string
|
roomId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class PeerRoom extends EventHub {
|
class ChatRoom extends EventHub {
|
||||||
|
readonly peer: Peer
|
||||||
readonly roomId: string
|
readonly roomId: string
|
||||||
private rtco?: Artico
|
|
||||||
readonly peerId: string
|
readonly peerId: string
|
||||||
private room?: Room
|
private room?: Room
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
super()
|
super()
|
||||||
|
this.peer = config.peer
|
||||||
this.roomId = config.roomId
|
this.roomId = config.roomId
|
||||||
this.peerId = config.peerId || nanoid()
|
this.peerId = config.peer.id
|
||||||
this.joinRoom = this.joinRoom.bind(this)
|
this.joinRoom = this.joinRoom.bind(this)
|
||||||
this.sendMessage = this.sendMessage.bind(this)
|
this.sendMessage = this.sendMessage.bind(this)
|
||||||
this.onMessage = this.onMessage.bind(this)
|
this.onMessage = this.onMessage.bind(this)
|
||||||
|
@ -32,17 +33,19 @@ class PeerRoom extends EventHub {
|
||||||
}
|
}
|
||||||
|
|
||||||
joinRoom() {
|
joinRoom() {
|
||||||
if (!this.rtco) {
|
|
||||||
this.rtco = new Artico({ id: this.peerId })
|
|
||||||
}
|
|
||||||
if (this.room) {
|
if (this.room) {
|
||||||
this.room = this.rtco.join(this.roomId)
|
this.room = this.peer.join(this.roomId)
|
||||||
} else {
|
} else {
|
||||||
this.rtco!.on('open', () => {
|
if (this.peer.state === 'ready') {
|
||||||
this.room = this.rtco!.join(this.roomId)
|
this.room = this.peer.join(this.roomId)
|
||||||
|
this.emit('action')
|
||||||
|
} else {
|
||||||
|
this.peer!.on('open', () => {
|
||||||
|
this.room = this.peer.join(this.roomId)
|
||||||
this.emit('action')
|
this.emit('action')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +126,7 @@ class PeerRoom extends EventHub {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
onError(callback: (error: Error) => void) {
|
onError(callback: (error: Error) => void) {
|
||||||
this.rtco?.on('error', (error) => callback(error))
|
this.peer?.on('error', (error) => callback(error))
|
||||||
this.on('error', (error: Error) => callback(error))
|
this.on('error', (error: Error) => callback(error))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -131,9 +134,9 @@ class PeerRoom extends EventHub {
|
||||||
|
|
||||||
const hostRoomId = stringToHex(document.location.host)
|
const hostRoomId = stringToHex(document.location.host)
|
||||||
|
|
||||||
const peerRoom = new PeerRoom({ roomId: hostRoomId })
|
const chatRoom = new ChatRoom({ roomId: hostRoomId, peer: Peer.createInstance() })
|
||||||
|
|
||||||
export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
|
export const ChatRoomImpl = ChatRoomExtern.impl(chatRoom)
|
||||||
|
|
||||||
// https://github.com/w3c/webextensions/issues/72
|
// https://github.com/w3c/webextensions/issues/72
|
||||||
// https://issues.chromium.org/issues/40251342
|
// https://issues.chromium.org/issues/40251342
|
|
@ -1,6 +1,6 @@
|
||||||
import { DanmakuExtern } from '@/domain/externs/Danmaku'
|
import { DanmakuExtern } from '@/domain/externs/Danmaku'
|
||||||
|
|
||||||
import { TextMessage } from '@/domain/Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import DanmakuMessage from '@/app/content/components/DanmakuMessage'
|
import DanmakuMessage from '@/app/content/components/DanmakuMessage'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NotificationExtern } from '@/domain/externs/Notification'
|
import { NotificationExtern } from '@/domain/externs/Notification'
|
||||||
import { TextMessage } from '../Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
import { EVENT } from '@/constants/event'
|
import { EVENT } from '@/constants/event'
|
||||||
import { messenger } from '@/messenger'
|
import { messenger } from '@/messenger'
|
||||||
|
|
||||||
|
|
22
src/domain/impls/Peer.ts
Normal file
22
src/domain/impls/Peer.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { Artico } from '@rtco/client'
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
peerId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Peer extends Artico {
|
||||||
|
private static instance: Peer | null = null
|
||||||
|
private constructor(config: Config = {}) {
|
||||||
|
const { peerId = nanoid() } = config
|
||||||
|
super({ id: peerId })
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createInstance(config: Config = {}) {
|
||||||
|
return (this.instance ??= new Peer(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance() {
|
||||||
|
return this.instance
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,29 @@
|
||||||
import { type DataPayload, type Room, joinRoom, selfId } from 'trystero'
|
import { Room } from '@rtco/client'
|
||||||
|
|
||||||
// import { joinRoom } from 'trystero/firebase'
|
import { VirtualRoomExtern } from '@/domain/externs/VirtualRoom'
|
||||||
|
|
||||||
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'
|
import { RoomMessage } from '@/domain/VirtualRoom'
|
||||||
|
import { JSONR } from '@/utils'
|
||||||
|
import { VIRTUAL_ROOM_ID } from '@/constants/config'
|
||||||
|
import Peer from './Peer'
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
peerId?: string
|
peer: Peer
|
||||||
roomId: string
|
roomId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class PeerRoom extends EventHub {
|
class VirtualRoom extends EventHub {
|
||||||
readonly appId: string
|
readonly peer: Peer
|
||||||
private room?: Room
|
|
||||||
readonly roomId: string
|
readonly roomId: string
|
||||||
readonly peerId: string
|
readonly peerId: string
|
||||||
|
private room?: Room
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
super()
|
super()
|
||||||
this.appId = __NAME__
|
this.peer = config.peer
|
||||||
this.roomId = config.roomId
|
this.roomId = config.roomId
|
||||||
this.peerId = selfId
|
this.peerId = config.peer.id
|
||||||
this.joinRoom = this.joinRoom.bind(this)
|
this.joinRoom = this.joinRoom.bind(this)
|
||||||
this.sendMessage = this.sendMessage.bind(this)
|
this.sendMessage = this.sendMessage.bind(this)
|
||||||
this.onMessage = this.onMessage.bind(this)
|
this.onMessage = this.onMessage.bind(this)
|
||||||
|
@ -32,16 +34,19 @@ class PeerRoom extends EventHub {
|
||||||
}
|
}
|
||||||
|
|
||||||
joinRoom() {
|
joinRoom() {
|
||||||
this.room = joinRoom({ appId: this.appId }, this.roomId)
|
if (this.room) {
|
||||||
/**
|
this.room = this.peer.join(this.roomId)
|
||||||
* If we wait to join, it will result in not being able to listen to our own join event.
|
} else {
|
||||||
* This might be related to the fact that:
|
if (this.peer.state === 'ready') {
|
||||||
* (If called more than once, only the latest callback registered is ever called.)
|
this.room = this.peer.join(this.roomId)
|
||||||
* Multiple listeners may overwrite each other.
|
|
||||||
* @see: https://github.com/dmotz/trystero?tab=readme-ov-file#onpeerjoincallback
|
|
||||||
*/
|
|
||||||
// this.room.onPeerJoin(() => this.emit('action'))
|
|
||||||
this.emit('action')
|
this.emit('action')
|
||||||
|
} else {
|
||||||
|
this.peer!.on('open', () => {
|
||||||
|
this.room = this.peer.join(this.roomId)
|
||||||
|
this.emit('action')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,15 +56,12 @@ class PeerRoom extends EventHub {
|
||||||
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')
|
this.room.send(JSONR.stringify(message)!, id)
|
||||||
send(message as any as DataPayload, id)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const [send] = this.room.makeAction('MESSAGE')
|
this.room.send(JSONR.stringify(message)!, id)
|
||||||
send(message as any as DataPayload, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,13 +71,11 @@ class PeerRoom extends EventHub {
|
||||||
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')
|
this.room.on('message', (message) => callback(JSONR.parse(message) as RoomMessage))
|
||||||
on((message) => callback(message as any as RoomMessage))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const [, on] = this.room.makeAction('MESSAGE')
|
this.room.on('message', (message) => callback(JSONR.parse(message) as RoomMessage))
|
||||||
on((message) => callback(message as any as RoomMessage))
|
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -86,15 +86,11 @@ class PeerRoom extends EventHub {
|
||||||
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.onPeerJoin((peerId) => {
|
this.room.on('join', (id) => callback(id))
|
||||||
callback(peerId)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.room.onPeerJoin((peerId) => {
|
this.room.on('join', (id) => callback(id))
|
||||||
callback(peerId)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -105,11 +101,11 @@ class PeerRoom extends EventHub {
|
||||||
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.onPeerLeave((peerId) => callback(peerId))
|
this.room.on('leave', (id) => callback(id))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.room.onPeerLeave((peerId) => callback(peerId))
|
this.room.on('leave', (id) => callback(id))
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -130,19 +126,15 @@ class PeerRoom extends EventHub {
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(callback: (error: Error) => void) {
|
onError(callback: (error: Error) => void) {
|
||||||
|
this.peer?.on('error', (error) => callback(error))
|
||||||
this.on('error', (error: Error) => callback(error))
|
this.on('error', (error: Error) => callback(error))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostRoomId = stringToHex(document.location.host)
|
const hostRoomId = stringToHex(VIRTUAL_ROOM_ID)
|
||||||
const peerRoom = new PeerRoom({ roomId: hostRoomId })
|
|
||||||
|
|
||||||
export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom)
|
const virtualRoom = new VirtualRoom({ roomId: hostRoomId, peer: Peer.createInstance() })
|
||||||
|
|
||||||
// https://github.com/w3c/webextensions/issues/72
|
export const VirtualRoomImpl = VirtualRoomExtern.impl(virtualRoom)
|
||||||
// https://issues.chromium.org/issues/40251342
|
|
||||||
// https://github.com/w3c/webrtc-extensions/issues/77
|
|
||||||
// https://github.com/aklinker1/webext-core/pull/70
|
|
|
@ -6,7 +6,7 @@ export interface ToastOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => {
|
const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name: 'MessageToastModule' }) => {
|
||||||
const toast = domain.getExtern(ToastExtern)
|
const toastExtern = domain.getExtern(ToastExtern)
|
||||||
|
|
||||||
const SuccessEvent = domain.event<number | string>({
|
const SuccessEvent = domain.event<number | string>({
|
||||||
name: `${options.name}.SuccessEvent`
|
name: `${options.name}.SuccessEvent`
|
||||||
|
@ -15,7 +15,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const SuccessCommand = domain.command({
|
const SuccessCommand = domain.command({
|
||||||
name: `${options.name}.SuccessCommand`,
|
name: `${options.name}.SuccessCommand`,
|
||||||
impl: (_, message: string | { message: string; duration?: number }) => {
|
impl: (_, message: string | { message: string; duration?: number }) => {
|
||||||
const id = toast.success(
|
const id = toastExtern.success(
|
||||||
typeof message === 'string' ? message : message.message,
|
typeof message === 'string' ? message : message.message,
|
||||||
typeof message === 'string' ? undefined : message.duration
|
typeof message === 'string' ? undefined : message.duration
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const ErrorCommand = domain.command({
|
const ErrorCommand = domain.command({
|
||||||
name: `${options.name}.ErrorCommand`,
|
name: `${options.name}.ErrorCommand`,
|
||||||
impl: (_, message: string | { message: string; duration?: number }) => {
|
impl: (_, message: string | { message: string; duration?: number }) => {
|
||||||
const id = toast.error(
|
const id = toastExtern.error(
|
||||||
typeof message === 'string' ? message : message.message,
|
typeof message === 'string' ? message : message.message,
|
||||||
typeof message === 'string' ? undefined : message.duration
|
typeof message === 'string' ? undefined : message.duration
|
||||||
)
|
)
|
||||||
|
@ -45,7 +45,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const InfoCommand = domain.command({
|
const InfoCommand = domain.command({
|
||||||
name: `${options.name}.InfoCommand`,
|
name: `${options.name}.InfoCommand`,
|
||||||
impl: (_, message: string | { message: string; duration?: number }) => {
|
impl: (_, message: string | { message: string; duration?: number }) => {
|
||||||
const id = toast.info(
|
const id = toastExtern.info(
|
||||||
typeof message === 'string' ? message : message.message,
|
typeof message === 'string' ? message : message.message,
|
||||||
typeof message === 'string' ? undefined : message.duration
|
typeof message === 'string' ? undefined : message.duration
|
||||||
)
|
)
|
||||||
|
@ -60,7 +60,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const WarningCommand = domain.command({
|
const WarningCommand = domain.command({
|
||||||
name: `${options.name}.WarningCommand`,
|
name: `${options.name}.WarningCommand`,
|
||||||
impl: (_, message: string | { message: string; duration?: number }) => {
|
impl: (_, message: string | { message: string; duration?: number }) => {
|
||||||
const id = toast.warning(
|
const id = toastExtern.warning(
|
||||||
typeof message === 'string' ? message : message.message,
|
typeof message === 'string' ? message : message.message,
|
||||||
typeof message === 'string' ? undefined : message.duration
|
typeof message === 'string' ? undefined : message.duration
|
||||||
)
|
)
|
||||||
|
@ -75,7 +75,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const LoadingCommand = domain.command({
|
const LoadingCommand = domain.command({
|
||||||
name: `${options.name}.LoadingCommand`,
|
name: `${options.name}.LoadingCommand`,
|
||||||
impl: (_, message: string | { message: string; duration?: number }) => {
|
impl: (_, message: string | { message: string; duration?: number }) => {
|
||||||
const id = toast.loading(
|
const id = toastExtern.loading(
|
||||||
typeof message === 'string' ? message : message.message,
|
typeof message === 'string' ? message : message.message,
|
||||||
typeof message === 'string' ? undefined : message.duration
|
typeof message === 'string' ? undefined : message.duration
|
||||||
)
|
)
|
||||||
|
@ -90,7 +90,7 @@ const ToastModule = (domain: RemeshDomainContext, options: ToastOptions = { name
|
||||||
const CancelCommand = domain.command({
|
const CancelCommand = domain.command({
|
||||||
name: `${options.name}.CancelCommand`,
|
name: `${options.name}.CancelCommand`,
|
||||||
impl: (_, id: number | string) => {
|
impl: (_, id: number | string) => {
|
||||||
toast.cancel(id)
|
toastExtern.cancel(id)
|
||||||
return [CancelEvent(id)]
|
return [CancelEvent(id)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { EVENT } from '@/constants/event'
|
import { EVENT } from '@/constants/event'
|
||||||
import { defineExtensionMessaging } from '@webext-core/messaging'
|
import { defineExtensionMessaging } from '@webext-core/messaging'
|
||||||
import { TextMessage } from '@/domain/Room'
|
import { TextMessage } from '@/domain/ChatRoom'
|
||||||
|
|
||||||
interface ProtocolMap {
|
interface ProtocolMap {
|
||||||
[EVENT.OPTIONS_PAGE_OPEN]: () => void
|
[EVENT.OPTIONS_PAGE_OPEN]: () => void
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { buildFullURL } from '@/utils'
|
||||||
|
|
||||||
export interface SiteInfo {
|
export interface SiteInfo {
|
||||||
host: string
|
host: string
|
||||||
hostname: string
|
hostname: string
|
||||||
|
@ -15,15 +17,17 @@ const getSiteInfo = (): SiteInfo => {
|
||||||
href: document.location.href,
|
href: document.location.href,
|
||||||
origin: document.location.origin,
|
origin: document.location.origin,
|
||||||
title:
|
title:
|
||||||
document.querySelector('meta[rel="og:title i"]')?.getAttribute('content') ??
|
document.querySelector('meta[property="og:site_name" i]')?.getAttribute('content') ??
|
||||||
document.querySelector('meta[rel="og:title i"]')?.getAttribute('content') ??
|
document.querySelector('meta[property="og:title" i]')?.getAttribute('content') ??
|
||||||
document.querySelector('meta[rel="og:site_name i"]')?.getAttribute('content') ??
|
|
||||||
document.title,
|
document.title,
|
||||||
icon:
|
icon: buildFullURL(
|
||||||
document.querySelector('meta[property="og:image" i]')?.getAttribute('href') ??
|
document.location.origin,
|
||||||
document.querySelector('link[rel="icon" i]')?.getAttribute('href') ??
|
document.querySelector('link[rel="icon" i]')?.getAttribute('href') ??
|
||||||
document.querySelector('link[rel="shortcut icon" i]')?.getAttribute('href') ??
|
document.querySelector('link[rel="shortcut icon" i]')?.getAttribute('href') ??
|
||||||
`${document.location.origin}/favicon.ico`,
|
document.querySelector('meta[property="og:image" i]')?.getAttribute('content') ??
|
||||||
|
document.querySelector('link[rel="apple-touch-icon" i]')?.getAttribute('href') ??
|
||||||
|
`/favicon.ico`
|
||||||
|
),
|
||||||
description:
|
description:
|
||||||
document.querySelector('meta[property="og:description i"]')?.getAttribute('content') ??
|
document.querySelector('meta[property="og:description i"]')?.getAttribute('content') ??
|
||||||
document.querySelector('meta[name="description" i]')?.getAttribute('content') ??
|
document.querySelector('meta[name="description" i]')?.getAttribute('content') ??
|
||||||
|
|
|
@ -17,3 +17,5 @@ export { default as getRootNode } from './getRootNode'
|
||||||
export { default as blobToBase64 } from './blobToBase64'
|
export { default as blobToBase64 } from './blobToBase64'
|
||||||
export * as JSONR from './jsonr'
|
export * as JSONR from './jsonr'
|
||||||
export { getTextByteSize } from './getTextByteSize'
|
export { getTextByteSize } from './getTextByteSize'
|
||||||
|
export { default as isEqual } from './isEqual'
|
||||||
|
export { cleanURL, isAbsoluteURL, assembleURL, buildFullURL } from './url'
|
||||||
|
|
5
src/utils/isEqual.ts
Normal file
5
src/utils/isEqual.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const isEqual = (a: object, b: object) => {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default isEqual
|
36
src/utils/url.ts
Normal file
36
src/utils/url.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export const cleanURL = (url: string) => url.replace(/([^:]\/)\/+/g, '$1').replace(/\/+$/, '')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the specified URL is absolute
|
||||||
|
* Reference: https://github.com/axios/axios/blob/v1.x/lib/helpers/isAbsoluteURL.js
|
||||||
|
*/
|
||||||
|
export const isAbsoluteURL = (url: string) => {
|
||||||
|
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
|
||||||
|
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
|
||||||
|
// by any combination of letters, digits, plus, period, or hyphen.
|
||||||
|
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add params to the URL
|
||||||
|
*/
|
||||||
|
export const assembleURL = (url: string, params: Record<string, string>) => {
|
||||||
|
return Object.entries(params)
|
||||||
|
.reduce((url, [key, value]) => {
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
return url
|
||||||
|
}, new URL(url))
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new URL by combining the baseURL with the requestedURL,
|
||||||
|
* only when the requestedURL is not already an absolute URL.
|
||||||
|
* If the requestURL is absolute, this function returns the requestedURL untouched.
|
||||||
|
*
|
||||||
|
* reference: https://github.com/axios/axios/blob/v1.x/lib/core/buildFullPath.js
|
||||||
|
*/
|
||||||
|
export const buildFullURL = (baseURL: string = '', pathURL: string = '', params: Record<string, any> = {}) => {
|
||||||
|
const url = cleanURL(isAbsoluteURL(pathURL) ? pathURL : `${baseURL}/${pathURL}`)
|
||||||
|
return assembleURL(url, params)
|
||||||
|
}
|
|
@ -11,6 +11,9 @@ export default {
|
||||||
padding: '2rem'
|
padding: '2rem'
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
fontSize: {
|
||||||
|
'2xs': '0.625rem'
|
||||||
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
infinity: 'calc(infinity)'
|
infinity: 'calc(infinity)'
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue