Skip to content

Commit 46fdc01

Browse files
committed
feat: add like top
1 parent 60be490 commit 46fdc01

File tree

9 files changed

+260
-49
lines changed

9 files changed

+260
-49
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
"title": "User List",
3030
"description": "List all users",
3131
"mode": "view"
32+
},
33+
{
34+
"name": "like-top",
35+
"title": "Like Top",
36+
"description": "Leaderboard of likes",
37+
"mode": "view"
3238
}
3339
],
3440
"scripts": {

src/actions/user.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Action, ActionPanel, Icon, showHUD } from '@raycast/api'
2+
3+
export const openProfile = (username: string) => [
4+
<Action.OpenInBrowser
5+
key="open"
6+
title="打开用户页 (macOS App)"
7+
icon="📲"
8+
url={`jike://page.jk/user/${username}`}
9+
onOpen={() => showHUD('已打开')}
10+
/>,
11+
<ActionPanel.Submenu
12+
key="openWith"
13+
title="打开用户页 (其他客户端)"
14+
icon={Icon.Window}
15+
>
16+
<Action.OpenInBrowser
17+
title="PC Web 端"
18+
url={`https://web.okjike.com/u/${username}`}
19+
onOpen={() => showHUD('已打开')}
20+
/>
21+
<Action.OpenInBrowser
22+
title="手机 Web 端"
23+
url={`https://m.okjike.com/users/${username}`}
24+
onOpen={() => showHUD('已打开')}
25+
/>
26+
</ActionPanel.Submenu>,
27+
]

src/components/user-select.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Form } from '@raycast/api'
2+
import { useUsers } from '../hooks/user'
3+
4+
export function UserSelect() {
5+
const { users } = useUsers()
6+
7+
return (
8+
<Form.Dropdown id="userId" title="用户">
9+
{users.map((user) => (
10+
<Form.Dropdown.Item
11+
key={user.userId}
12+
title={user.screenName}
13+
value={user.userId}
14+
/>
15+
))}
16+
</Form.Dropdown>
17+
)
18+
}

src/hooks/user.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useMemo, useState } from 'react'
2+
import { JikeClient } from 'jike-sdk'
3+
import { getConfig } from '../utils/config'
4+
import type { ConfigUser } from '../utils/config'
5+
6+
export function useUsers() {
7+
const [users, setUsers] = useState<ConfigUser[]>([])
8+
9+
const update = () => getConfig().then((cfg) => setUsers(cfg.users))
10+
11+
const findUser = (userId: string) =>
12+
useMemo(() => users.find((u) => u.userId === userId), [users])
13+
14+
useEffect(() => {
15+
update()
16+
}, [])
17+
18+
return {
19+
users,
20+
update,
21+
findUser,
22+
}
23+
}
24+
25+
export function useClient(user?: ConfigUser) {
26+
const client = useMemo(() => user && JikeClient.fromJSON(user), [user])
27+
28+
return {
29+
client,
30+
}
31+
}

src/like-top.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { LikeTop } from './views/like-top'
2+
3+
export default LikeTop

src/views/like-top.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
Action,
3+
ActionPanel,
4+
Form,
5+
List,
6+
Toast,
7+
showToast,
8+
useNavigation,
9+
} from '@raycast/api'
10+
import { limit } from 'jike-sdk'
11+
import { useEffect, useMemo, useState } from 'react'
12+
import { UserSelect } from '../components/user-select'
13+
import { useClient, useUsers } from '../hooks/user'
14+
import { handleError } from '../utils/errors'
15+
import { openProfile } from '../actions/user'
16+
import type { Entity, JikePostWithDetail } from 'jike-sdk'
17+
18+
export interface LikeTopForm {
19+
userId: string
20+
topCount: string
21+
postCount: string
22+
}
23+
24+
export function LikeTop() {
25+
const { push } = useNavigation()
26+
27+
const onSubmit = (form: LikeTopForm) => {
28+
push(<LikeTopResult {...form} />)
29+
}
30+
31+
return (
32+
<Form
33+
navigationTitle="点赞排行榜"
34+
actions={
35+
<ActionPanel>
36+
<Action.SubmitForm onSubmit={onSubmit} />
37+
</ActionPanel>
38+
}
39+
>
40+
<UserSelect />
41+
<Form.TextField id="topCount" title="排名数量" defaultValue="50" />
42+
<Form.TextField id="postCount" title="动态数量" defaultValue="0" />
43+
<Form.Description text="统计多少条最近发布的动态,0 为所有动态" />
44+
</Form>
45+
)
46+
}
47+
48+
function LikeTopResult({ userId, topCount, postCount }: LikeTopForm) {
49+
interface LikeStat {
50+
user: Entity.User
51+
count: number
52+
}
53+
type UserMap = Record<string, LikeStat>
54+
55+
const [loading, setLoading] = useState(false)
56+
const [posts, setPosts] = useState<JikePostWithDetail[]>([])
57+
const [likeStat, setLikeStat] = useState<LikeStat[]>([])
58+
59+
const countRanking = useMemo(
60+
() =>
61+
likeStat
62+
? [...new Set(Object.values(likeStat).map(({ count }) => count))].sort(
63+
(a, b) => b - a
64+
)
65+
: [],
66+
[likeStat]
67+
)
68+
const getRanking = (count: number) => countRanking.indexOf(count) + 1
69+
70+
const { findUser } = useUsers()
71+
const user = findUser(userId)
72+
const { client } = useClient(user)
73+
74+
const fetchResult = async (ab: AbortController) => {
75+
if (!client) return
76+
77+
setLoading(true)
78+
let toast = await showToast({
79+
title: '正在获取',
80+
message: '正在获取动态',
81+
style: Toast.Style.Animated,
82+
})
83+
try {
84+
const posts = await client.getSelf().queryPersonalUpdate({
85+
limit:
86+
+postCount > 0 ? limit.limitMaxCount(+postCount) : limit.limitNone(),
87+
})
88+
setPosts(posts)
89+
90+
const userMap: UserMap = {}
91+
for (const [i, post] of posts.entries()) {
92+
if (ab.signal.aborted) {
93+
toast = await showToast({
94+
title: '已取消',
95+
style: Toast.Style.Failure,
96+
})
97+
return
98+
}
99+
toast.message = `正在获取点赞数据 (${i + 1} / ${posts.length})`
100+
101+
const users = await post.listLikedUsers()
102+
for (const user of users) {
103+
const id = user.id
104+
if (!userMap[id]) userMap[id] = { user, count: 1 }
105+
else userMap[id].count++
106+
}
107+
}
108+
setLikeStat(
109+
Object.values(userMap)
110+
.sort((a, b) => b.count - a.count)
111+
.slice(0, +topCount)
112+
)
113+
toast.hide()
114+
} catch (err) {
115+
toast.hide()
116+
handleError(err)
117+
return
118+
} finally {
119+
setLoading(false)
120+
}
121+
}
122+
123+
useEffect(() => {
124+
const ab = new AbortController()
125+
fetchResult(ab)
126+
return () => ab.abort()
127+
}, [client, postCount, topCount])
128+
129+
const renderRanking = (rank: number) => {
130+
switch (rank) {
131+
case 1:
132+
return { icon: '🥇' }
133+
case 2:
134+
return { icon: '🥈' }
135+
case 3:
136+
return { icon: '🥉' }
137+
default:
138+
return { text: String(rank) }
139+
}
140+
}
141+
return (
142+
<List isLoading={loading}>
143+
{likeStat.map(({ user, count }) => (
144+
<List.Item
145+
key={user.id}
146+
icon={user.avatarImage.thumbnailUrl}
147+
title={user.screenName}
148+
subtitle={`点赞 ${count} 次,${((count / posts.length) * 100).toFixed(
149+
2
150+
)}%`}
151+
accessories={[renderRanking(getRanking(count))]}
152+
actions={
153+
<ActionPanel>
154+
{...openProfile(user.username)}
155+
<Action.CopyToClipboard
156+
title="复制昵称"
157+
content={user.screenName}
158+
/>
159+
</ActionPanel>
160+
}
161+
/>
162+
))}
163+
</List>
164+
)
165+
}

src/views/post.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,13 @@ import {
77
useNavigation,
88
} from '@raycast/api'
99
import { ApiOptions, JikeClient } from 'jike-sdk'
10-
import { useEffect, useState } from 'react'
11-
import { getConfig } from '../utils/config'
1210
import { handleError } from '../utils/errors'
13-
import type { ConfigUser } from '../utils/config'
11+
import { useUsers } from '../hooks/user'
12+
import { UserSelect } from '../components/user-select'
1413

1514
export function Post() {
1615
const { pop } = useNavigation()
17-
const [users, setUsers] = useState<ConfigUser[]>([])
18-
19-
useEffect(() => {
20-
getConfig().then((cfg) => setUsers(cfg.users))
21-
}, [])
16+
const { findUser } = useUsers()
2217

2318
const submit = async ({
2419
userId,
@@ -35,7 +30,7 @@ export function Post() {
3530
return
3631
}
3732

38-
const user = users.find((u) => u.userId === userId)!
33+
const user = findUser(userId)!
3934
const client = JikeClient.fromJSON(user)
4035
try {
4136
await client.createPost(ApiOptions.PostType.ORIGINAL, content)
@@ -60,15 +55,7 @@ export function Post() {
6055
</ActionPanel>
6156
}
6257
>
63-
<Form.Dropdown id="userId" title="用户">
64-
{users.map((user) => (
65-
<Form.Dropdown.Item
66-
key={user.userId}
67-
title={user.screenName}
68-
value={user.userId}
69-
/>
70-
))}
71-
</Form.Dropdown>
58+
<UserSelect />
7259

7360
<Form.TextArea
7461
id="content"

src/views/user-detail.tsx

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
List,
77
Toast,
88
confirmAlert,
9-
showHUD,
109
showToast,
1110
} from '@raycast/api'
1211
import { JikeClient } from 'jike-sdk'
1312
import { handleError } from '../utils/errors'
1413
import { updateConfig } from '../utils/config'
1514
import { findUser } from '../utils/user'
15+
import { openProfile } from '../actions/user'
1616
import type { ConfigUser } from '../utils/config'
1717
import type { ReactNode } from 'react'
1818
import type { Entity } from 'jike-sdk'
@@ -69,30 +69,7 @@ export function UserDetail({ user, actions, onRefresh }: UserDetailProps) {
6969
}
7070

7171
const itemActions = (user: ConfigUser) => [
72-
<Action.OpenInBrowser
73-
key="open"
74-
title="打开用户页 (macOS App)"
75-
icon="📲"
76-
url={`jike://page.jk/user/${user.username}`}
77-
onOpen={() => showHUD('已打开')}
78-
/>,
79-
80-
<ActionPanel.Submenu
81-
key="openWith"
82-
title="打开用户页 (通过其他客户端)"
83-
icon={Icon.Window}
84-
>
85-
<Action.OpenInBrowser
86-
title="PC Web 端"
87-
url={`https://web.okjike.com/u/${user.username}`}
88-
onOpen={() => showHUD('已打开')}
89-
/>
90-
<Action.OpenInBrowser
91-
title="手机 Web 端"
92-
url={`https://m.okjike.com/users/${user.username}`}
93-
onOpen={() => showHUD('已打开')}
94-
/>
95-
</ActionPanel.Submenu>,
72+
...openProfile(user.username),
9673

9774
<Action
9875
key="refresh"

src/views/user-list.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { useEffect, useMemo, useState } from 'react'
22
import { Action, ActionPanel, Icon, List } from '@raycast/api'
3-
import { getConfig } from '../utils/config'
3+
import { useUsers } from '../hooks/user'
44
import { UserDetail } from './user-detail'
55
import { Login } from './login'
6-
import type { ConfigUser } from '../utils/config'
76

87
export function UserList() {
9-
const [users, setUsers] = useState<ConfigUser[]>([])
8+
const { users, update } = useUsers()
109
const [loading, setLoading] = useState(false)
1110

1211
const refreshList = async () => {
1312
setLoading(true)
14-
await getConfig()
15-
.then((config) => setUsers(config.users))
16-
.finally(() => setLoading(false))
13+
await update().finally(() => setLoading(false))
1714
}
1815

1916
useEffect(() => {

0 commit comments

Comments
 (0)