perf: add animation effects and add self join message
This commit is contained in:
parent
90253effa6
commit
437c234f8a
20 changed files with 367 additions and 103 deletions
|
@ -24,7 +24,7 @@ export default [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/lib/**', '**.million**']
|
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/magicui/**', '**/lib/**', '**.million**']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -33,7 +33,8 @@ export default [
|
||||||
'@typescript-eslint/no-empty-object-type': 'off',
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
'@typescript-eslint/no-unused-expressions': 'off',
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
'@eslint-react/no-array-index-key': 'off',
|
'@eslint-react/no-array-index-key': 'off',
|
||||||
'@eslint-react/hooks-extra/no-redundant-custom-hook': 'off'
|
'@eslint-react/hooks-extra/no-redundant-custom-hook': 'off',
|
||||||
|
'@eslint-react/dom/no-missing-button-type': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"homepage": "https://github.com/molvqingtai/WebChat#readme",
|
"homepage": "https://github.com/molvqingtai/WebChat#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.9.0",
|
||||||
"@perfsee/jsonr": "^1.13.0",
|
"@perfsee/jsonr": "^1.13.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^11.5.6",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lucide-react": "^0.445.0",
|
"lucide-react": "^0.445.0",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
|
@ -94,8 +96,8 @@
|
||||||
"@eslint-react/eslint-plugin": "^1.14.2",
|
"@eslint-react/eslint-plugin": "^1.14.2",
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint__js": "^8.42.3",
|
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/node": "^22.6.1",
|
"@types/node": "^22.6.1",
|
||||||
"@types/react": "^18.3.9",
|
"@types/react": "^18.3.9",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
|
|
@ -11,6 +11,9 @@ importers:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^3.9.0
|
specifier: ^3.9.0
|
||||||
version: 3.9.0(react-hook-form@7.53.0(react@18.3.1))
|
version: 3.9.0(react-hook-form@7.53.0(react@18.3.1))
|
||||||
|
'@lottiefiles/dotlottie-react':
|
||||||
|
specifier: ^0.9.0
|
||||||
|
version: 0.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@perfsee/jsonr':
|
'@perfsee/jsonr':
|
||||||
specifier: ^1.13.0
|
specifier: ^1.13.0
|
||||||
version: 1.13.0
|
version: 1.13.0
|
||||||
|
@ -68,6 +71,9 @@ importers:
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^11.5.6
|
||||||
|
version: 11.5.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
idb-keyval:
|
idb-keyval:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.1
|
version: 6.2.1
|
||||||
|
@ -1118,6 +1124,15 @@ packages:
|
||||||
'@libp2p/websockets@8.2.0':
|
'@libp2p/websockets@8.2.0':
|
||||||
resolution: {integrity: sha512-UNjqkQ8/emnYswp1ohIIuZCnhI5DlvWF9IaIND2MoTCDavi7yubWfMp8jSWBsAqPnMeLMO8MQ6YlOo4FFC104Q==}
|
resolution: {integrity: sha512-UNjqkQ8/emnYswp1ohIIuZCnhI5DlvWF9IaIND2MoTCDavi7yubWfMp8jSWBsAqPnMeLMO8MQ6YlOo4FFC104Q==}
|
||||||
|
|
||||||
|
'@lottiefiles/dotlottie-react@0.9.0':
|
||||||
|
resolution: {integrity: sha512-6vR3XA7YWWvv76TkNPKC6Hc4CU0WXJlVydYmEezgi6RZS+Au3IUsCxZBr/3Zt2JPvtS3mttvP4X7tfeTA+414g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
|
'@lottiefiles/dotlottie-web@0.34.0':
|
||||||
|
resolution: {integrity: sha512-lKayn0IaqFKcnLGxJKKaDlxwKqKBpHzZ7COMEZ0HYhd3cbI9GFJb9YDUgtdpzTH5uL9Eqnr19myWYmEXHPm9pQ==}
|
||||||
|
|
||||||
'@multiformats/dns@1.0.6':
|
'@multiformats/dns@1.0.6':
|
||||||
resolution: {integrity: sha512-nt/5UqjMPtyvkG9BQYdJ4GfLK3nMqGpFZOzf4hAmIa0sJh2LlS9YKXZ4FgwBDsaHvzZqR/rUFIywIc7pkHNNuw==}
|
resolution: {integrity: sha512-nt/5UqjMPtyvkG9BQYdJ4GfLK3nMqGpFZOzf4hAmIa0sJh2LlS9YKXZ4FgwBDsaHvzZqR/rUFIywIc7pkHNNuw==}
|
||||||
|
|
||||||
|
@ -3229,6 +3244,20 @@ packages:
|
||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
|
framer-motion@11.5.6:
|
||||||
|
resolution: {integrity: sha512-JMwUpAxv/DWgul9vPgX0ElKn0G66sUc6O9tOXsYwn3zxwvhxFljSXC0XT2QCzuTYBshwC8nyDAa1SYcV0Ldbhw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fs-constants@1.0.0:
|
fs-constants@1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
|
@ -7294,6 +7323,14 @@ snapshots:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@lottiefiles/dotlottie-react@0.9.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@lottiefiles/dotlottie-web': 0.34.0
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
'@lottiefiles/dotlottie-web@0.34.0': {}
|
||||||
|
|
||||||
'@multiformats/dns@1.0.6':
|
'@multiformats/dns@1.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/dns-packet': 5.6.5
|
'@types/dns-packet': 5.6.5
|
||||||
|
@ -9725,6 +9762,13 @@ snapshots:
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
|
framer-motion@11.5.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.7.0
|
||||||
|
optionalDependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-extra@11.2.0:
|
fs-extra@11.2.0:
|
||||||
|
|
|
@ -16,22 +16,17 @@ export default function App() {
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const roomFinished = useRemeshQuery(roomDomain.query.IsFinishedQuery())
|
const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery())
|
||||||
const userInfoFinished = useRemeshQuery(userInfoDomain.query.IsFinishedQuery())
|
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||||
|
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.MessageListLoadIsFinishedQuery())
|
||||||
|
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
||||||
|
|
||||||
const notUserInfo = userInfoFinished && !userInfo
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userInfoFinished) {
|
if (userInfoSetFinished && messageListLoadFinished) {
|
||||||
if (userInfo) {
|
send(roomDomain.command.JoinRoomCommand())
|
||||||
!roomFinished && send(roomDomain.command.JoinRoomCommand())
|
|
||||||
} else {
|
|
||||||
send(messageListDomain.command.ClearListCommand())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [userInfoFinished, userInfo, roomFinished])
|
}, [userInfoSetFinished, messageListLoadFinished])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -37,7 +37,9 @@ const MessageItem: FC<MessageItemProps> = (props) => {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="grid grid-cols-[1fr_auto] items-center gap-x-2 leading-none">
|
<div className="grid grid-cols-[1fr_auto] items-center gap-x-2 leading-none">
|
||||||
<div className="overflow-hidden text-ellipsis text-sm font-medium text-slate-600">{props.data.username}</div>
|
<div className="overflow-hidden text-ellipsis text-sm font-semibold text-slate-600">
|
||||||
|
{props.data.username}
|
||||||
|
</div>
|
||||||
<FormatDate className="text-xs text-slate-400" date={props.data.date}></FormatDate>
|
<FormatDate className="text-xs text-slate-400" date={props.data.date}></FormatDate>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -20,13 +20,14 @@ export default defineContentScript({
|
||||||
async main(ctx) {
|
async main(ctx) {
|
||||||
const store = Remesh.store({
|
const store = Remesh.store({
|
||||||
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl],
|
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl],
|
||||||
inspectors: __DEV__ ? [RemeshLogger()] : []
|
inspectors: !__DEV__ ? [RemeshLogger()] : []
|
||||||
})
|
})
|
||||||
|
|
||||||
const ui = await createShadowRootUi(ctx, {
|
const ui = await createShadowRootUi(ctx, {
|
||||||
name: __NAME__,
|
name: __NAME__,
|
||||||
position: 'inline',
|
position: 'inline',
|
||||||
anchor: 'body',
|
anchor: 'body',
|
||||||
|
isolateEvents: ['scroll', 'click'],
|
||||||
mode: __DEV__ ? 'open' : 'closed',
|
mode: __DEV__ ? 'open' : 'closed',
|
||||||
onMount: (container) => {
|
onMount: (container) => {
|
||||||
const app = createElement('<div id="app"></div>')
|
const app = createElement('<div id="app"></div>')
|
||||||
|
@ -34,11 +35,11 @@ export default defineContentScript({
|
||||||
|
|
||||||
const root = createRoot(app)
|
const root = createRoot(app)
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
// <React.StrictMode>
|
||||||
<RemeshRoot store={store}>
|
<RemeshRoot store={store}>
|
||||||
<App />
|
<App />
|
||||||
</RemeshRoot>
|
</RemeshRoot>
|
||||||
</React.StrictMode>
|
// </React.StrictMode>
|
||||||
)
|
)
|
||||||
return root
|
return root
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,7 +25,7 @@ const Header: FC = () => {
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<Button className="overflow-hidden" variant="link">
|
<Button className="overflow-hidden" variant="link">
|
||||||
<span className="truncate text-lg font-medium text-slate-600">
|
<span className="truncate text-lg font-semibold text-slate-600">
|
||||||
{siteInfo.hostname.replace(/^www\./i, '')}
|
{siteInfo.hostname.replace(/^www\./i, '')}
|
||||||
{/* {peerId} */}
|
{/* {peerId} */}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -6,13 +6,15 @@ 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 RoomDomain, { MessageType } from '@/domain/Room'
|
||||||
|
import MessageListDomain from '@/domain/MessageList'
|
||||||
|
|
||||||
const Main: FC = () => {
|
const Main: FC = () => {
|
||||||
const send = useRemeshSend()
|
const send = useRemeshSend()
|
||||||
|
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||||
const roomDomain = useRemeshDomain(RoomDomain())
|
const roomDomain = useRemeshDomain(RoomDomain())
|
||||||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||||
const _messageList = useRemeshQuery(roomDomain.query.MessageListQuery())
|
|
||||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||||
|
const _messageList = useRemeshQuery(messageListDomain.query.ListQuery())
|
||||||
const messageList = _messageList.map((message) => {
|
const messageList = _messageList.map((message) => {
|
||||||
if (message.type === MessageType.Normal) {
|
if (message.type === MessageType.Normal) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
|
||||||
import { Button } from '@/components/ui/Button'
|
|
||||||
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
||||||
import MessageListDomain, { Message, MessageType } from '@/domain/MessageList'
|
import MessageListDomain, { Message, MessageType } from '@/domain/MessageList'
|
||||||
import UserInfoDomain, { UserInfo } from '@/domain/UserInfo'
|
import UserInfoDomain, { UserInfo } from '@/domain/UserInfo'
|
||||||
|
@ -10,11 +9,14 @@ import { FC, useEffect, useState } from 'react'
|
||||||
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
|
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
|
||||||
import Timer from '@resreq/Timer'
|
import Timer from '@resreq/Timer'
|
||||||
import ExampleImage from '@/assets/images/example.jpg'
|
import ExampleImage from '@/assets/images/example.jpg'
|
||||||
|
import PulsatingButton from '@/components/magicui/pulsating-button'
|
||||||
|
import BlurFade from '@/components/magicui/blur-fade'
|
||||||
|
import WordPullUp from '@/components/magicui/word-pull-up'
|
||||||
|
|
||||||
const mockTextList = [
|
const mockTextList = [
|
||||||
`你問我支持不支持,我說我支持`,
|
`你問我支持不支持,我說我支持`,
|
||||||
`我就明確告訴你,你們啊,我感覺你們新聞界還要學習一個,你們非常熟悉西方的那一套`,
|
`我就明確告訴你,你們啊,我感覺你們新聞界還要學習一個,你們非常熟悉西方的那一套`,
|
||||||
`你們畢竟還 too young`,
|
`你們畢竟還 “too young”`,
|
||||||
`明白我的意思吧?`,
|
`明白我的意思吧?`,
|
||||||
`我告訴你們我是身經百戰了,見得多了`,
|
`我告訴你們我是身經百戰了,見得多了`,
|
||||||
`西方的那個國家我沒去過?`,
|
`西方的那個國家我沒去過?`,
|
||||||
|
@ -22,12 +24,12 @@ const mockTextList = [
|
||||||
`其實媒體呀,還是要提高自己的知識水平,識得唔識得呀?`,
|
`其實媒體呀,還是要提高自己的知識水平,識得唔識得呀?`,
|
||||||
`你們有一個好,全世界跑到什么地方,你們比其他的西方記者跑得還快`,
|
`你們有一個好,全世界跑到什么地方,你們比其他的西方記者跑得還快`,
|
||||||
`但是呢問來問去的問題呀`,
|
`但是呢問來問去的問題呀`,
|
||||||
`都 too simple sometimes naive`,
|
`都 “too simple sometimes naive”`,
|
||||||
`懂了沒啊,識得唔識得呀?`,
|
`懂了沒啊,識得唔識得呀?`,
|
||||||
`我很抱歉,我今天是作爲一個長者給你們講`,
|
`我很抱歉,我今天是作爲一個長者給你們講`,
|
||||||
`我不是新聞工作者,但是我見得太多了`,
|
`我不是新聞工作者,但是我見得太多了`,
|
||||||
`我有這個必要好告訴你們一點人生的經驗`,
|
`我有這個必要好告訴你們一點人生的經驗`,
|
||||||
`![too young too simple sometimes naive](${ExampleImage})`
|
`![ExampleImage](${ExampleImage})`
|
||||||
]
|
]
|
||||||
|
|
||||||
const generateUserInfo = async (): Promise<UserInfo> => {
|
const generateUserInfo = async (): Promise<UserInfo> => {
|
||||||
|
@ -90,19 +92,27 @@ const Setup: FC = () => {
|
||||||
send(messageListDomain.command.ClearListCommand())
|
send(messageListDomain.command.ClearListCommand())
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-50 flex rounded-xl bg-black/10 shadow-2xl backdrop-blur-sm">
|
<div className="absolute inset-0 z-50 flex rounded-xl bg-black/10 shadow-2xl backdrop-blur-sm">
|
||||||
<div className="m-auto flex flex-col items-center justify-center gap-y-8 pb-40 drop-shadow-lg">
|
<div className="m-auto flex flex-col items-center justify-center gap-y-8 pb-40 drop-shadow-lg">
|
||||||
<Avatar className="size-24 cursor-pointer border-4 border-white ">
|
<BlurFade key={userInfo?.avatar} delay={0.1} inView>
|
||||||
<AvatarImage src={userInfo?.avatar} alt="avatar" />
|
<Avatar className="size-24 cursor-pointer border-4 border-white ">
|
||||||
<AvatarFallback>
|
<AvatarImage src={userInfo?.avatar} alt="avatar" />
|
||||||
<UserIcon size={30} className="text-slate-400" />
|
<AvatarFallback>
|
||||||
</AvatarFallback>
|
<UserIcon size={30} className="text-slate-400" />
|
||||||
</Avatar>
|
</AvatarFallback>
|
||||||
<div className="text-2xl font-bold text-primary">@{userInfo?.name}</div>
|
</Avatar>
|
||||||
<Button className="rounded-full" size="lg" onClick={handleSetup}>
|
</BlurFade>
|
||||||
Start chatting
|
<div className="flex">
|
||||||
</Button>
|
<div className="text-2xl font-bold text-primary">@</div>
|
||||||
|
<WordPullUp
|
||||||
|
className="text-2xl font-bold text-primary"
|
||||||
|
key={userInfo?.name}
|
||||||
|
words={`${userInfo?.name || ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PulsatingButton onClick={handleSetup}>Start chatting</PulsatingButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { Label } from '@/components/ui/Label'
|
||||||
import { RefreshCcwIcon } from 'lucide-react'
|
import { RefreshCcwIcon } from 'lucide-react'
|
||||||
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
import { MAX_AVATAR_SIZE } from '@/constants/config'
|
||||||
import ToastDomain from '@/domain/Toast'
|
import ToastDomain from '@/domain/Toast'
|
||||||
|
import BlurFade from '@/components/magicui/blur-fade'
|
||||||
|
|
||||||
const defaultUserInfo: UserInfo = {
|
const defaultUserInfo: UserInfo = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
|
@ -94,13 +95,15 @@ const ProfileForm = () => {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="absolute inset-x-1 top-0 mx-auto grid w-fit -translate-y-1/2 justify-items-center">
|
<FormItem className="absolute inset-x-1 top-0 mx-auto grid w-fit -translate-y-1/2 justify-items-center">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<AvatarSelect
|
<BlurFade key={form.getValues().avatar} delay={0.1}>
|
||||||
compressSize={MAX_AVATAR_SIZE}
|
<AvatarSelect
|
||||||
onError={handleError}
|
compressSize={MAX_AVATAR_SIZE}
|
||||||
onWarning={handleWarning}
|
onError={handleError}
|
||||||
className="shadow-lg"
|
onWarning={handleWarning}
|
||||||
{...field}
|
className="shadow-lg"
|
||||||
></AvatarSelect>
|
{...field}
|
||||||
|
></AvatarSelect>
|
||||||
|
</BlurFade>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
61
src/components/magicui/blur-fade.tsx
Normal file
61
src/components/magicui/blur-fade.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { AnimatePresence, motion, useInView, UseInViewOptions, Variants } from 'framer-motion'
|
||||||
|
|
||||||
|
type MarginType = UseInViewOptions['margin']
|
||||||
|
|
||||||
|
interface BlurFadeProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
variant?: {
|
||||||
|
hidden: { y: number }
|
||||||
|
visible: { y: number }
|
||||||
|
}
|
||||||
|
duration?: number
|
||||||
|
delay?: number
|
||||||
|
yOffset?: number
|
||||||
|
inView?: boolean
|
||||||
|
inViewMargin?: MarginType
|
||||||
|
blur?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlurFade({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
duration = 0.4,
|
||||||
|
delay = 0,
|
||||||
|
yOffset = 6,
|
||||||
|
inView = false,
|
||||||
|
inViewMargin = '-50px',
|
||||||
|
blur = '6px'
|
||||||
|
}: BlurFadeProps) {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
||||||
|
const isInView = !inView || inViewResult
|
||||||
|
const defaultVariants: Variants = {
|
||||||
|
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||||
|
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` }
|
||||||
|
}
|
||||||
|
const combinedVariants = variant || defaultVariants
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
delay: 0.04 + delay,
|
||||||
|
duration,
|
||||||
|
ease: 'easeOut'
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
37
src/components/magicui/pulsating-button.tsx
Normal file
37
src/components/magicui/pulsating-button.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/index'
|
||||||
|
|
||||||
|
interface PulsatingButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
pulseColor?: string
|
||||||
|
duration?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PulsatingButton({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
pulseColor = '#0f172a50',
|
||||||
|
duration = '1.5s',
|
||||||
|
...props
|
||||||
|
}: PulsatingButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'relative rounded-full text-center cursor-pointer text-sm font-medium flex justify-center items-center text-primary-foreground bg-primary py-2 h-10 px-8 hover:bg-primary/90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--pulse-color': pulseColor,
|
||||||
|
'--duration': duration
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative z-10">{children}</div>
|
||||||
|
<div className="absolute left-1/2 top-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-full bg-inherit" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
53
src/components/magicui/word-pull-up.tsx
Normal file
53
src/components/magicui/word-pull-up.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/index";
|
||||||
|
|
||||||
|
interface WordPullUpProps {
|
||||||
|
words: string;
|
||||||
|
delayMultiple?: number;
|
||||||
|
wrapperFramerProps?: Variants;
|
||||||
|
framerProps?: Variants;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WordPullUp({
|
||||||
|
words,
|
||||||
|
wrapperFramerProps = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
framerProps = {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
show: { y: 0, opacity: 1 },
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
}: WordPullUpProps) {
|
||||||
|
return (
|
||||||
|
<motion.h1
|
||||||
|
variants={wrapperFramerProps}
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
className={cn(
|
||||||
|
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{words.split(" ").map((word, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
variants={framerProps}
|
||||||
|
style={{ display: "inline-block", paddingRight: "8px" }}
|
||||||
|
>
|
||||||
|
{word === "" ? <span> </span> : word}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</motion.h1>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { Remesh } from 'remesh'
|
||||||
import { ListModule } from 'remesh/modules/list'
|
import { ListModule } from 'remesh/modules/list'
|
||||||
import { IndexDBStorageExtern } from '@/domain/externs/Storage'
|
import { IndexDBStorageExtern } from '@/domain/externs/Storage'
|
||||||
import StorageEffect from '@/domain/modules/StorageEffect'
|
import StorageEffect from '@/domain/modules/StorageEffect'
|
||||||
|
import StatusModule from './modules/Status'
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
Normal = 'normal',
|
Normal = 'normal',
|
||||||
|
@ -48,6 +49,10 @@ const MessageListDomain = Remesh.domain({
|
||||||
key: (message) => message.id
|
key: (message) => message.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const MessageListLoadStatusModule = StatusModule(domain, {
|
||||||
|
name: 'MessageListLoadStatusModule'
|
||||||
|
})
|
||||||
|
|
||||||
const ListQuery = MessageListModule.query.ItemListQuery
|
const ListQuery = MessageListModule.query.ItemListQuery
|
||||||
|
|
||||||
const ItemQuery = MessageListModule.query.ItemQuery
|
const ItemQuery = MessageListModule.query.ItemQuery
|
||||||
|
@ -140,14 +145,18 @@ const MessageListDomain = Remesh.domain({
|
||||||
|
|
||||||
storageEffect
|
storageEffect
|
||||||
.set(SyncToStorageEvent)
|
.set(SyncToStorageEvent)
|
||||||
.get<Message[]>((value) => SyncToStateCommand(value ?? []))
|
.get<Message[]>((value) => [
|
||||||
|
SyncToStateCommand(value ?? []),
|
||||||
|
MessageListLoadStatusModule.command.SetFinishedCommand()
|
||||||
|
])
|
||||||
.watch<Message[]>((value) => SyncToStateCommand(value ?? []))
|
.watch<Message[]>((value) => SyncToStateCommand(value ?? []))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
HasItemQuery,
|
HasItemQuery,
|
||||||
ItemQuery,
|
ItemQuery,
|
||||||
ListQuery
|
ListQuery,
|
||||||
|
MessageListLoadIsFinishedQuery: MessageListLoadStatusModule.query.IsFinishedQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
CreateItemCommand,
|
CreateItemCommand,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Remesh } from 'remesh'
|
import { Remesh } from 'remesh'
|
||||||
import { map, merge, of, EMPTY, mergeMap } from 'rxjs'
|
import { map, merge, of, EMPTY, mergeMap, fromEvent, Observable, tap } from 'rxjs'
|
||||||
import { NormalMessage, type MessageUser } from './MessageList'
|
import { NormalMessage, type MessageUser } from './MessageList'
|
||||||
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
import { PeerRoomExtern } from '@/domain/externs/PeerRoom'
|
||||||
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
import MessageListDomain, { MessageType } from '@/domain/MessageList'
|
||||||
import UserInfoDomain from '@/domain/UserInfo'
|
import UserInfoDomain from '@/domain/UserInfo'
|
||||||
import { callbackToObservable, desert, upsert } from '@/utils'
|
import { fromEventPattern, desert, upsert } from '@/utils'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import StatusModule from '@/domain/modules/Status'
|
import StatusModule from '@/domain/modules/Status'
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@ export enum SendType {
|
||||||
Like = 'like',
|
Like = 'like',
|
||||||
Hate = 'hate',
|
Hate = 'hate',
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
UserSync = 'userSync'
|
Join = 'join'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncUserMessage extends MessageUser {
|
export interface SyncUserMessage extends MessageUser {
|
||||||
type: SendType.UserSync
|
type: SendType.Join
|
||||||
id: string
|
id: string
|
||||||
peerId: string
|
peerId: string
|
||||||
joinTime: number
|
joinTime: number
|
||||||
|
@ -63,10 +63,8 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const MessageListQuery = messageListDomain.query.ListQuery
|
const RoomJoinStatusModule = StatusModule(domain, {
|
||||||
|
name: 'RoomJoinStatusModule'
|
||||||
const RoomStatusModule = StatusModule(domain, {
|
|
||||||
name: 'Room.RoomStatusModule'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserListState = domain.state<RoomUser[]>({
|
const UserListState = domain.state<RoomUser[]>({
|
||||||
|
@ -92,7 +90,16 @@ const RoomDomain = Remesh.domain({
|
||||||
type: 'create',
|
type: 'create',
|
||||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||||
}),
|
}),
|
||||||
RoomStatusModule.command.SetFinishedCommand(),
|
messageListDomain.command.CreateItemCommand({
|
||||||
|
id: nanoid(),
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
userAvatar,
|
||||||
|
body: `"${username}" joined the chat`,
|
||||||
|
type: MessageType.Prompt,
|
||||||
|
date: Date.now()
|
||||||
|
}),
|
||||||
|
RoomJoinStatusModule.command.SetFinishedCommand(),
|
||||||
JoinRoomEvent(peerRoom.roomId)
|
JoinRoomEvent(peerRoom.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -100,16 +107,25 @@ const RoomDomain = Remesh.domain({
|
||||||
|
|
||||||
const LeaveRoomCommand = domain.command({
|
const LeaveRoomCommand = domain.command({
|
||||||
name: 'RoomLeaveRoomCommand',
|
name: 'RoomLeaveRoomCommand',
|
||||||
impl: ({ get }, roomId: string) => {
|
impl: ({ get }) => {
|
||||||
peerRoom.leaveRoom()
|
peerRoom.leaveRoom()
|
||||||
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())!
|
||||||
return [
|
return [
|
||||||
|
messageListDomain.command.CreateItemCommand({
|
||||||
|
id: nanoid(),
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
userAvatar,
|
||||||
|
body: `"${username}" left the chat`,
|
||||||
|
type: MessageType.Prompt,
|
||||||
|
date: Date.now()
|
||||||
|
}),
|
||||||
UpdateUserListCommand({
|
UpdateUserListCommand({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar }
|
||||||
}),
|
}),
|
||||||
RoomStatusModule.command.SetInitialCommand(),
|
RoomJoinStatusModule.command.SetInitialCommand(),
|
||||||
LeaveRoomEvent(roomId)
|
LeaveRoomEvent(peerRoom.roomId)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -156,8 +172,6 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
const listMessage: NormalMessage = {
|
const listMessage: NormalMessage = {
|
||||||
...localMessage,
|
...localMessage,
|
||||||
type: MessageType.Normal,
|
|
||||||
date: Date.now(),
|
|
||||||
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage<RoomMessage>(likeMessage)
|
peerRoom.sendMessage<RoomMessage>(likeMessage)
|
||||||
|
@ -180,8 +194,6 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
const listMessage: NormalMessage = {
|
const listMessage: NormalMessage = {
|
||||||
...localMessage,
|
...localMessage,
|
||||||
type: MessageType.Normal,
|
|
||||||
date: Date.now(),
|
|
||||||
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId')
|
||||||
}
|
}
|
||||||
peerRoom.sendMessage<RoomMessage>(hateMessage)
|
peerRoom.sendMessage<RoomMessage>(hateMessage)
|
||||||
|
@ -189,19 +201,19 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendUserSyncMessageCommand = domain.command({
|
const SendJoinMessageCommand = domain.command({
|
||||||
name: 'RoomSendUserSyncMessageCommand',
|
name: 'RoomSendJoinMessageCommand',
|
||||||
impl: ({ get }, targetPeerId: string) => {
|
impl: ({ get }, targetPeerId: string) => {
|
||||||
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
|
const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)!
|
||||||
|
|
||||||
const syncUserMessage: SyncUserMessage = {
|
const syncUserMessage: SyncUserMessage = {
|
||||||
...self,
|
...self,
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
type: SendType.UserSync
|
type: SendType.Join
|
||||||
}
|
}
|
||||||
|
|
||||||
peerRoom.sendMessage<RoomMessage>(syncUserMessage, targetPeerId)
|
peerRoom.sendMessage<RoomMessage>(syncUserMessage, targetPeerId)
|
||||||
return [SendUserSyncMessageEvent(syncUserMessage)]
|
return [SendJoinMessageEvent(syncUserMessage)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -217,8 +229,8 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendUserSyncMessageEvent = domain.event<SyncUserMessage>({
|
const SendJoinMessageEvent = domain.event<SyncUserMessage>({
|
||||||
name: 'RoomSendUserSyncMessageEvent'
|
name: 'RoomSendJoinMessageEvent'
|
||||||
})
|
})
|
||||||
|
|
||||||
const SendTextMessageEvent = domain.event<TextMessage>({
|
const SendTextMessageEvent = domain.event<TextMessage>({
|
||||||
|
@ -256,13 +268,13 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnJoinRoomEffect',
|
name: 'RoomOnJoinRoomEffect',
|
||||||
impl: () => {
|
impl: () => {
|
||||||
const onJoinRoom$ = callbackToObservable<string>(peerRoom.onJoinRoom).pipe(
|
const onJoinRoom$ = fromEventPattern<string>(peerRoom.onJoinRoom).pipe(
|
||||||
mergeMap((peerId) => {
|
mergeMap((peerId) => {
|
||||||
console.log('onJoinRoom', peerId)
|
// console.log('onJoinRoom', peerId)
|
||||||
if (peerRoom.peerId === peerId) {
|
if (peerRoom.peerId === peerId) {
|
||||||
return [OnJoinRoomEvent(peerId)]
|
return [OnJoinRoomEvent(peerId)]
|
||||||
} else {
|
} else {
|
||||||
return [SendUserSyncMessageCommand(peerId), OnJoinRoomEvent(peerId)]
|
return [SendJoinMessageCommand(peerId), OnJoinRoomEvent(peerId)]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -273,14 +285,14 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnMessageEffect',
|
name: 'RoomOnMessageEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onMessage$ = callbackToObservable<RoomMessage>(peerRoom.onMessage).pipe(
|
const onMessage$ = fromEventPattern<RoomMessage>(peerRoom.onMessage).pipe(
|
||||||
mergeMap((message) => {
|
mergeMap((message) => {
|
||||||
console.log('onMessage', message)
|
// console.log('onMessage', message)
|
||||||
const messageEvent$ = of(OnMessageEvent(message))
|
const messageEvent$ = of(OnMessageEvent(message))
|
||||||
|
|
||||||
const commandEvent$ = (() => {
|
const commandEvent$ = (() => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case SendType.UserSync: {
|
case SendType.Join: {
|
||||||
const userList = get(UserListQuery())
|
const userList = get(UserListQuery())
|
||||||
const selfUser = userList.find((user) => user.peerId === peerRoom.peerId)!
|
const selfUser = userList.find((user) => user.peerId === peerRoom.peerId)!
|
||||||
// If the browser has multiple tabs open, it can cause the same user to join multiple times with the same peerId but different userId
|
// If the browser has multiple tabs open, it can cause the same user to join multiple times with the same peerId but different userId
|
||||||
|
@ -351,9 +363,9 @@ const RoomDomain = Remesh.domain({
|
||||||
domain.effect({
|
domain.effect({
|
||||||
name: 'RoomOnLeaveRoomEffect',
|
name: 'RoomOnLeaveRoomEffect',
|
||||||
impl: ({ get }) => {
|
impl: ({ get }) => {
|
||||||
const onLeaveRoom$ = callbackToObservable<string>(peerRoom.onLeaveRoom).pipe(
|
const onLeaveRoom$ = fromEventPattern<string>(peerRoom.onLeaveRoom).pipe(
|
||||||
map((peerId) => {
|
map((peerId) => {
|
||||||
console.log('onLeaveRoom', peerId)
|
// console.log('onLeaveRoom', peerId)
|
||||||
const user = get(UserListQuery()).find((user) => user.peerId === peerId)
|
const user = get(UserListQuery()).find((user) => user.peerId === peerId)
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -377,12 +389,24 @@ const RoomDomain = Remesh.domain({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 以后移动到 service worker 中,无需每次刷新页面都发送离开房间的消息
|
||||||
|
domain.effect({
|
||||||
|
name: 'RoomOnUnloadEffect',
|
||||||
|
impl: () => {
|
||||||
|
const beforeUnload$ = fromEvent(window, 'beforeunload').pipe(
|
||||||
|
map(() => {
|
||||||
|
return [LeaveRoomCommand()]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return beforeUnload$
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
PeerIdQuery,
|
PeerIdQuery,
|
||||||
UserListQuery,
|
UserListQuery,
|
||||||
MessageListQuery,
|
RoomJoinIsFinishedQuery: RoomJoinStatusModule.query.IsFinishedQuery
|
||||||
...RoomStatusModule.query
|
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
JoinRoomCommand,
|
JoinRoomCommand,
|
||||||
|
@ -390,20 +414,18 @@ const RoomDomain = Remesh.domain({
|
||||||
SendTextMessageCommand,
|
SendTextMessageCommand,
|
||||||
SendLikeMessageCommand,
|
SendLikeMessageCommand,
|
||||||
SendHateMessageCommand,
|
SendHateMessageCommand,
|
||||||
SendUserSyncMessageCommand,
|
SendJoinMessageCommand
|
||||||
...RoomStatusModule.command
|
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
SendTextMessageEvent,
|
SendTextMessageEvent,
|
||||||
SendLikeMessageEvent,
|
SendLikeMessageEvent,
|
||||||
SendHateMessageEvent,
|
SendHateMessageEvent,
|
||||||
SendUserSyncMessageEvent,
|
SendJoinMessageEvent,
|
||||||
JoinRoomEvent,
|
JoinRoomEvent,
|
||||||
LeaveRoomEvent,
|
LeaveRoomEvent,
|
||||||
OnMessageEvent,
|
OnMessageEvent,
|
||||||
OnJoinRoomEvent,
|
OnJoinRoomEvent,
|
||||||
OnLeaveRoomEvent,
|
OnLeaveRoomEvent
|
||||||
...RoomStatusModule.event
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,11 @@ const UserInfoDomain = Remesh.domain({
|
||||||
default: null
|
default: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserInfoStatusModule = StatusModule(domain, {
|
const UserInfoLoadStatusModule = StatusModule(domain, {
|
||||||
name: 'UserInfo.StatusModule'
|
name: 'UserInfoLoadStatusModule'
|
||||||
|
})
|
||||||
|
const UserInfoSetStatusModule = StatusModule(domain, {
|
||||||
|
name: 'UserInfoSetStatusModule'
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserInfoQuery = domain.query({
|
const UserInfoQuery = domain.query({
|
||||||
|
@ -46,8 +49,8 @@ const UserInfoDomain = Remesh.domain({
|
||||||
UpdateUserInfoEvent(),
|
UpdateUserInfoEvent(),
|
||||||
SyncToStorageEvent(),
|
SyncToStorageEvent(),
|
||||||
userInfo
|
userInfo
|
||||||
? UserInfoStatusModule.command.SetFinishedCommand()
|
? UserInfoSetStatusModule.command.SetFinishedCommand()
|
||||||
: UserInfoStatusModule.command.SetInitialCommand()
|
: UserInfoSetStatusModule.command.SetInitialCommand()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -73,31 +76,35 @@ const UserInfoDomain = Remesh.domain({
|
||||||
const SyncToStateCommand = domain.command({
|
const SyncToStateCommand = domain.command({
|
||||||
name: 'UserInfo.SyncToStateCommand',
|
name: 'UserInfo.SyncToStateCommand',
|
||||||
impl: (_, userInfo: UserInfo | null) => {
|
impl: (_, userInfo: UserInfo | null) => {
|
||||||
return [UserInfoState().new(userInfo), UpdateUserInfoEvent(), SyncToStateEvent(userInfo)]
|
return [
|
||||||
|
UserInfoState().new(userInfo),
|
||||||
|
UpdateUserInfoEvent(),
|
||||||
|
SyncToStateEvent(userInfo),
|
||||||
|
userInfo && UserInfoSetStatusModule.command.SetFinishedCommand()
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
storageEffect
|
storageEffect
|
||||||
.set(SyncToStorageEvent)
|
.set(SyncToStorageEvent)
|
||||||
.get<UserInfo>((value) => {
|
.get<UserInfo>((value) => {
|
||||||
return [SyncToStateCommand(value), UserInfoStatusModule.command.SetFinishedCommand()]
|
return [SyncToStateCommand(value), UserInfoLoadStatusModule.command.SetFinishedCommand()]
|
||||||
})
|
})
|
||||||
.watch<UserInfo>((value) => [SyncToStateCommand(value)])
|
.watch<UserInfo>((value) => [SyncToStateCommand(value)])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
UserInfoQuery,
|
UserInfoQuery,
|
||||||
...UserInfoStatusModule.query
|
UserInfoLoadIsFinishedQuery: UserInfoLoadStatusModule.query.IsFinishedQuery,
|
||||||
|
UserInfoSetIsFinishedQuery: UserInfoSetStatusModule.query.IsFinishedQuery
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
UpdateUserInfoCommand,
|
UpdateUserInfoCommand
|
||||||
...UserInfoStatusModule.command
|
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
SyncToStateEvent,
|
SyncToStateEvent,
|
||||||
SyncToStorageEvent,
|
SyncToStorageEvent,
|
||||||
UpdateUserInfoEvent,
|
UpdateUserInfoEvent
|
||||||
...UserInfoStatusModule.event
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,12 +45,12 @@ export default class StorageEffect {
|
||||||
this.domain.effect({
|
this.domain.effect({
|
||||||
name: 'FormStateToStorageEffect',
|
name: 'FormStateToStorageEffect',
|
||||||
impl: ({ fromEvent }) => {
|
impl: ({ fromEvent }) => {
|
||||||
const changeUserInfo$ = fromEvent(event).pipe(
|
return fromEvent(event).pipe(
|
||||||
tap(async (value) => {
|
switchMap(async (value) => {
|
||||||
return await this.storage.set(this.key, value)
|
await this.storage.set(this.key, value)
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return merge(changeUserInfo$).pipe(map(() => null))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Observable } from 'rxjs'
|
||||||
|
|
||||||
export type Subscribe<T> = (callback: (event: T) => void) => void
|
export type Subscribe<T> = (callback: (event: T) => void) => void
|
||||||
|
|
||||||
const callbackToObservable = <T>(subscribe: Subscribe<T>, unsubscribe?: () => void) => {
|
const fromEventPattern = <T>(subscribe: Subscribe<T>, unsubscribe?: () => void) => {
|
||||||
return new Observable<T>((subscriber) => {
|
return new Observable<T>((subscriber) => {
|
||||||
subscribe((event: T) => {
|
subscribe((event: T) => {
|
||||||
subscriber.next(event)
|
subscriber.next(event)
|
||||||
|
@ -15,4 +15,4 @@ const callbackToObservable = <T>(subscribe: Subscribe<T>, unsubscribe?: () => vo
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default callbackToObservable
|
export default fromEventPattern
|
||||||
|
|
|
@ -5,7 +5,7 @@ export { default as getSiteInfo } from './getSiteInfo'
|
||||||
export { default as compressImage } from './compressImage'
|
export { default as compressImage } from './compressImage'
|
||||||
export { default as isNullish } from './isNullish'
|
export { default as isNullish } from './isNullish'
|
||||||
export { default as checkSystemDarkMode } from './checkSystemDarkMode'
|
export { default as checkSystemDarkMode } from './checkSystemDarkMode'
|
||||||
export { default as callbackToObservable } from './callbackToObservable'
|
export { default as fromEventPattern } from './fromEventPattern'
|
||||||
export { default as stringToHex } from './stringToHex'
|
export { default as stringToHex } from './stringToHex'
|
||||||
export { default as debounce } from './debounce'
|
export { default as debounce } from './debounce'
|
||||||
export { default as throttle } from './throttle'
|
export { default as throttle } from './throttle'
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default {
|
||||||
md: 'calc(var(--radius) - 2px)',
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
},
|
},
|
||||||
|
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: { height: '0' },
|
from: { height: '0' },
|
||||||
|
@ -68,11 +69,25 @@ export default {
|
||||||
'accordion-up': {
|
'accordion-up': {
|
||||||
from: { height: 'var(--radix-accordion-content-height)' },
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
to: { height: '0' }
|
to: { height: '0' }
|
||||||
|
},
|
||||||
|
pulse: {
|
||||||
|
'0%, 100%': { boxShadow: '0 0 0 0 var(--pulse-color)' },
|
||||||
|
'50%': { boxShadow: '0 0 0 8px var(--pulse-color)' }
|
||||||
|
},
|
||||||
|
ripple: {
|
||||||
|
'0%, 100%': {
|
||||||
|
transform: 'translate(-50%, -50%) scale(1)'
|
||||||
|
},
|
||||||
|
'50%': {
|
||||||
|
transform: 'translate(-50%, -50%) scale(0.9)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
pulse: 'pulse var(--duration) ease-out infinite',
|
||||||
|
ripple: 'ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue