Skip to content

Commit 7dc7d7d

Browse files
committed
feat: activity
Signed-off-by: Innei <[email protected]>
1 parent 22f4020 commit 7dc7d7d

32 files changed

+201
-23
lines changed

public/apps/arc.png

4.25 KB
Loading

public/apps/chrome.png

48.2 KB
Loading

public/apps/chrome_canary.png

50.5 KB
Loading

public/apps/code.png

47.2 KB
Loading

public/apps/discord.png

5.05 KB
Loading

public/apps/figma.png

7.78 KB
Loading

public/apps/finder.png

5.52 KB
Loading

public/apps/iterm2.png

10.8 KB
Loading

public/apps/linear.png

6.07 KB
Loading

public/apps/mail.png

5.66 KB
Loading

public/apps/music.png

5.17 KB
Loading

public/apps/netease.png

30.4 KB
Loading

public/apps/qq.png

41 KB
Loading

public/apps/qq_music.png

46 KB
Loading

public/apps/safari.png

8.11 KB
Loading

public/apps/slack.png

4.57 KB
Loading

public/apps/telegram.png

4.72 KB
Loading

public/apps/webstorm.png

6.09 KB
Loading

public/apps/wechat.png

29.9 KB
Loading

public/apps/xcode.png

79.1 KB
Loading

src/app/common/deleted/page.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Link from 'next/link'
2+
3+
import { NormalContainer } from '~/components/layout/container/Normal'
4+
import { StyledButton } from '~/components/ui/button'
5+
6+
export default async function PageDeleted() {
7+
return (
8+
<NormalContainer>
9+
<div className="mt-[250px] flex h-full flex-col items-center justify-center">
10+
<h1 className="text-4xl font-bold">此页面已被删除</h1>
11+
12+
<StyledButton className="mt-8">
13+
<Link href="/">返回首页</Link>
14+
</StyledButton>
15+
</div>
16+
</NormalContainer>
17+
)
18+
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Loading } from '~/components/ui/loading'
22

3-
export default () => (
4-
<div className="absolute inset-0 flex h-[120px] center">
5-
<Loading useDefaultLoadingText />
6-
</div>
7-
)
3+
export default function Load() {
4+
return (
5+
<div className="flex h-[240px] w-full center">
6+
<Loading useDefaultLoadingText />
7+
</div>
8+
)
9+
}

src/components/layout/header/Header.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { memo } from 'react'
33
import { OnlyMobile } from '~/components/ui/viewport/OnlyMobile'
44
import { clsxm } from '~/utils/helper'
55

6+
import { Activity } from './internal/Activity'
67
import { AnimatedLogo } from './internal/AnimatedLogo'
78
import { BluredBackground } from './internal/BluredBackground'
89
import styles from './internal/grid.module.css'
@@ -41,6 +42,7 @@ const MemoedHeader = memo(() => {
4142

4243
<HeaderLogoArea>
4344
<AnimatedLogo />
45+
<Activity />
4446
{/* <SiteOwnerAvatar className="absolute bottom-[10px] right-[2px] hidden lg:inline-block" /> */}
4547
<OnlyMobile>
4648
<HeaderMeta />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use client'
2+
3+
import { useQuery } from '@tanstack/react-query'
4+
import React, { useState } from 'react'
5+
import { m } from 'framer-motion'
6+
import Image from 'next/image'
7+
import type { RequestError } from '@mx-space/api-client'
8+
9+
import { apiClient } from '~/utils/request'
10+
11+
// autocorrect: false
12+
const appLabels: { [app: string]: string } = {
13+
Slack: 'slack',
14+
Arc: 'arc',
15+
Code: 'code',
16+
WebStorm: 'webstorm',
17+
Linear: 'linear',
18+
Figma: 'figma',
19+
Telegram: 'telegram',
20+
WeChat: 'wechat',
21+
Discord: 'discord',
22+
Mail: 'mail',
23+
Safari: 'safari',
24+
Music: 'music',
25+
Finder: 'finder',
26+
Messages: 'messages',
27+
QQ: 'qq',
28+
Chrome: 'chrome',
29+
'Chrome Canary': 'chrome_canary',
30+
QQ音乐: 'qq_music',
31+
NeteaseMusic: 'netease',
32+
iTerm2: 'iterm2',
33+
Xcode: 'xcode',
34+
}
35+
// autocorrect: true
36+
export function Activity() {
37+
const [isEnabled, setIsEnabled] = useState(true)
38+
39+
const { data: processName } = useQuery(
40+
['activity'],
41+
async () => {
42+
return await apiClient.proxy.fn.ps.update
43+
.post<string>()
44+
.catch((err: RequestError) => {
45+
err.status === 404 && setIsEnabled(false)
46+
return ''
47+
})
48+
},
49+
{
50+
refetchInterval: 5000,
51+
retry: false,
52+
enabled: isEnabled,
53+
},
54+
)
55+
56+
if (!processName) {
57+
return null
58+
}
59+
if (!appLabels[processName]) {
60+
console.log('Not collected process name: ', processName)
61+
return null
62+
}
63+
64+
return (
65+
<div className="pointer-events-auto relative bottom-0 right-[-25px] top-0 flex items-center md:absolute">
66+
<m.div
67+
className="absolute left-1 top-1 h-6 w-6 select-none rounded-[6px] bg-zinc-500/10 dark:bg-zinc-200/10"
68+
animate={{ opacity: [0, 0.65, 0], scale: [1, 1.4, 1] }}
69+
transition={{
70+
duration: 1.5,
71+
repeat: Infinity,
72+
}}
73+
/>
74+
<Image
75+
width={32}
76+
height={32}
77+
src={`/apps/${appLabels[processName]}.png`}
78+
alt={processName}
79+
priority
80+
fetchPriority="high"
81+
unoptimized
82+
className="pointer-events-none select-none"
83+
/>
84+
</div>
85+
)
86+
}

src/components/layout/header/internal/AnimatedLogo.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const AnimatedLogo = () => {
4949
<AnimatePresence>
5050
{!shouldShowMeta && (
5151
<m.div
52+
layout
5253
initial={{ opacity: 0 }}
5354
animate={{ opacity: 1 }}
5455
exit={{ opacity: 0 }}

src/components/layout/header/internal/HeaderContent.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ const AnimatedMenu: Component = ({ children }) => {
8484

8585
const ForDesktop: Component<{
8686
shouldHideNavBg?: boolean
87-
}> = ({ className, shouldHideNavBg }) => {
87+
animatedIcon?: boolean
88+
}> = ({ className, shouldHideNavBg, animatedIcon = true }) => {
8889
const mouseX = useMotionValue(0)
8990
const mouseY = useMotionValue(0)
9091
const radius = useMotionValue(0)
@@ -130,6 +131,7 @@ const ForDesktop: Component<{
130131
section.subMenu?.findIndex((item) => item.path === pathname) || -1
131132
return (
132133
<HeaderMenuItem
134+
iconLayout={animatedIcon}
133135
section={section}
134136
key={section.path}
135137
subItemActive={section.subMenu?.[subItemActive]}
@@ -151,7 +153,8 @@ const HeaderMenuItem = memo<{
151153
section: IHeaderMenu
152154
isActive: boolean
153155
subItemActive?: IHeaderMenu
154-
}>(({ section, isActive, subItemActive }) => {
156+
iconLayout?: boolean
157+
}>(({ section, isActive, subItemActive, iconLayout }) => {
155158
const href = section.path
156159

157160
return (
@@ -164,7 +167,7 @@ const HeaderMenuItem = memo<{
164167
<span className="relative flex items-center">
165168
{isActive && (
166169
<m.span
167-
layoutId="header-menu-icon"
170+
layoutId={iconLayout ? 'header-menu-icon' : undefined}
168171
className={clsxm('mr-2 flex items-center')}
169172
>
170173
{subItemActive?.icon ?? section.icon}

src/components/layout/header/internal/SiteOwnerAvatar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const SiteOwnerAvatar: Component = ({ className }) => {
1616
className,
1717
)}
1818
>
19-
<Image src={avatar} alt="Site Owner Avatar" width={25} height={25} />
19+
<Image src={avatar} alt="Site Owner Avatar" width={40} height={40} />
2020
</div>
2121
)
2222
}

src/lib/route-builder.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export enum Routes {
2020

2121
Projects = '/projects',
2222
Project = '/projects/',
23+
24+
PageDeletd = '/common/deleted',
2325
}
2426

2527
type Noop = never
@@ -75,12 +77,12 @@ export type RouteParams<T extends Routes> = T extends Routes.Home
7577
? OnlySlug
7678
: T extends Routes.Project
7779
? OnlyId
78-
: never
80+
: {}
7981

80-
export const routeBuilder = <T extends Routes>(
82+
export function routeBuilder<T extends Routes>(
8183
route: T,
8284
params: RouteParams<typeof route>,
83-
) => {
85+
) {
8486
let href: string = route
8587
switch (route) {
8688
case Routes.Note: {
@@ -123,11 +125,6 @@ export const routeBuilder = <T extends Routes>(
123125
href += p.id
124126
break
125127
}
126-
case Routes.NoteTopics:
127-
case Routes.Notes:
128-
case Routes.Login: {
129-
break
130-
}
131128
}
132129
return href
133130
}

src/providers/root/socket-provider.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
'use client'
22

3-
import { useEffect } from 'react'
3+
import { useEffect, useRef } from 'react'
4+
import { useRouter } from 'next/navigation'
45

56
export const SocketContainer: Component = () => {
7+
const connectOnce = useRef(false)
8+
const router = useRouter()
69
useEffect(() => {
10+
if (connectOnce.current) return
711
import('~/socket').then((module) => {
812
const { socketClient } = module
13+
connectOnce.current = true
14+
socketClient.setRouter(router)
915
socketClient.initIO()
1016
})
1117
}, [])

src/socket/handler.ts

+59-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
import type { NoteModel } from '@mx-space/api-client'
2-
import type { EventTypes } from '~/types/events'
1+
import type { NoteModel, PostModel } from '@mx-space/api-client'
2+
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context'
33

44
import { setOnlineCount } from '~/atoms'
5+
import { routeBuilder, Routes } from '~/lib/route-builder'
56
import { toast } from '~/lib/toast'
67
import {
78
getCurrentNoteData,
89
setCurrentNoteData,
910
} from '~/providers/note/CurrentNoteDataProvider'
11+
import {
12+
getCurrentPageData,
13+
setCurrentPageData,
14+
} from '~/providers/page/CurrentPageDataProvider'
15+
import {
16+
getCurrentPostData,
17+
setCurrentPostData,
18+
} from '~/providers/post/CurrentPostDataProvider'
19+
import { EventTypes } from '~/types/events'
1020
import { isDev } from '~/utils/env'
1121

12-
export const eventHandler = (type: EventTypes, data: any) => {
22+
export const eventHandler = (
23+
type: EventTypes,
24+
data: any,
25+
router: AppRouterInstance,
26+
) => {
1327
switch (type) {
1428
case 'VISITOR_ONLINE':
1529
case 'VISITOR_OFFLINE': {
@@ -18,6 +32,28 @@ export const eventHandler = (type: EventTypes, data: any) => {
1832
break
1933
}
2034

35+
case EventTypes.POST_UPDATE: {
36+
const post = data as PostModel
37+
if (getCurrentPostData()?.id === post.id) {
38+
setCurrentPostData((draft) => {
39+
const nextPost = { ...data }
40+
Reflect.deleteProperty(nextPost, 'category')
41+
Object.assign(draft, nextPost)
42+
})
43+
toast('文章已更新')
44+
}
45+
break
46+
}
47+
48+
case EventTypes.POST_DELETE: {
49+
const post = data as PostModel
50+
if (getCurrentPostData()?.id === post.id) {
51+
router.push(routeBuilder(Routes.PageDeletd, {}))
52+
toast.error('文章已删除')
53+
}
54+
break
55+
}
56+
2157
case 'NOTE_UPDATE': {
2258
const note = data as NoteModel
2359
if (getCurrentNoteData()?.data.id === note.id) {
@@ -29,6 +65,26 @@ export const eventHandler = (type: EventTypes, data: any) => {
2965
break
3066
}
3167

68+
case 'NOTE_DELETE': {
69+
const note = data as NoteModel
70+
if (getCurrentNoteData()?.data.id === note.id) {
71+
router.push(routeBuilder(Routes.PageDeletd, {}))
72+
toast.error('手记已删除')
73+
}
74+
break
75+
}
76+
77+
case EventTypes.PAGE_UPDATED: {
78+
const { slug } = data
79+
if (getCurrentPageData()?.slug === slug) {
80+
setCurrentPageData((draft) => {
81+
Object.assign(draft, data)
82+
})
83+
toast('页面已更新')
84+
}
85+
break
86+
}
87+
3288
default: {
3389
if (isDev) {
3490
console.log(type, data)

src/socket/socket-client.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { io } from 'socket.io-client'
22
import type { EventTypes } from '~/types/events'
3+
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context'
34
import type { Socket } from 'socket.io-client'
45

56
import { simpleCamelcaseKeys as camelcaseKeys } from '@mx-space/api-client'
@@ -12,6 +13,8 @@ import { eventHandler } from './handler'
1213
class SocketClient {
1314
public socket!: Socket
1415

16+
private router: AppRouterInstance
17+
1518
constructor() {
1619
this.socket = io(`${GATEWAY_URL}/web`, {
1720
timeout: 10000,
@@ -21,6 +24,10 @@ class SocketClient {
2124
transports: ['websocket'],
2225
})
2326
}
27+
28+
setRouter(router: AppRouterInstance) {
29+
this.router = router
30+
}
2431
initIO() {
2532
if (!this.socket) {
2633
return
@@ -51,7 +58,7 @@ class SocketClient {
5158

5259
window.dispatchEvent(new CustomEvent(type, { detail: data }))
5360

54-
eventHandler(type, data)
61+
eventHandler(type, data, this.router)
5562
}
5663
emit(event: EventTypes, payload: any) {
5764
return new Promise((resolve) => {

src/types/events.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { NoteModel } from '@mx-space/api-client'
22

3-
export enum EventTypes {
3+
export const enum EventTypes {
44
GATEWAY_CONNECT = 'GATEWAY_CONNECT',
55
GATEWAY_DISCONNECT = 'GATEWAY_DISCONNECT',
66

0 commit comments

Comments
 (0)