Skip to content

Commit 06d6ee6

Browse files
committed
refactor(chat): optimize ChatRow rendering and user info handling
1 parent 60d7246 commit 06d6ee6

File tree

2 files changed

+95
-100
lines changed

2 files changed

+95
-100
lines changed

webview-ui/src/hooks/__tests__/useZgsmUserInfo.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("useZgsmUserInfo", () => {
115115
consoleSpy.mockRestore()
116116
})
117117

118-
it("应该在登出时发送遥测事件", () => {
118+
it("应该在登出时发送遥测事件", async () => {
119119
// 创建一个有效的JWT token,确保能够正确解析
120120
const mockPayload = {
121121
id: "user123",
@@ -127,10 +127,15 @@ describe("useZgsmUserInfo", () => {
127127
zgsmAccessToken: validToken,
128128
}
129129

130-
const { rerender } = renderHook(({ config }) => useZgsmUserInfo(config), {
130+
const { rerender, result } = renderHook(({ config }) => useZgsmUserInfo(config), {
131131
initialProps: { config: apiConfiguration },
132132
})
133133

134+
// 等待初始认证状态设置完成
135+
await waitFor(() => {
136+
expect(result.current.isAuthenticated).toBe(true)
137+
})
138+
134139
// 模拟登出 - 移除token
135140
rerender({ config: {} as ProviderSettings })
136141

Lines changed: 88 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState, useMemo, useCallback } from "react"
1+
import { useEffect, useState, useRef } from "react"
22
import axios from "axios"
33
import type { ProviderSettings, ZgsmUserInfo } from "@roo-code/types"
44
import { TelemetryEventName } from "@roo-code/types"
@@ -20,7 +20,7 @@ function parseJwt(token: string) {
2020
throw new Error("Invalid JWT")
2121
}
2222
const payload = parts[1]
23-
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/")) // base64url → base64 → decode
23+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"))
2424
return JSON.parse(decoded)
2525
}
2626

@@ -42,7 +42,7 @@ async function hashToken(token: string): Promise<string> {
4242
export async function imageUrlToBase64(url: string): Promise<string | null> {
4343
try {
4444
const response = await axios.get(url, {
45-
responseType: "blob", // Key! Ensure axios returns Blob
45+
responseType: "blob",
4646
})
4747

4848
const blob = response.data as Blob
@@ -51,114 +51,104 @@ export async function imageUrlToBase64(url: string): Promise<string | null> {
5151
const reader = new FileReader()
5252
reader.onloadend = () => resolve(reader.result as string)
5353
reader.onerror = () => reject("Failed to convert blob to base64")
54-
reader.readAsDataURL(blob) // Automatically adds data:image/png;base64,...
54+
reader.readAsDataURL(blob)
5555
})
5656
} catch (error) {
5757
console.error("Failed to convert image to base64", error)
5858
return null
5959
}
6060
}
6161

62-
/**
63-
* 用于管理ZGSM用户信息的自定义Hook
64-
* 提供用户信息解析、头像处理、token哈希等功能
65-
* 优化版本:使用 useMemo 缓存昂贵的计算操作,避免滚动时重复计算
66-
*/
6762
export function useZgsmUserInfo(tokenOrConfig?: string | ProviderSettings): ZgsmUserData {
68-
const [logoPic, setLogoPic] = useState("")
69-
const [hash, setHash] = useState("")
70-
const wasAuthenticatedRef = useRef(false)
71-
72-
// 提取实际的 token 值
73-
const token = useMemo(() => {
74-
if (typeof tokenOrConfig === "string") {
75-
return tokenOrConfig
76-
}
77-
return tokenOrConfig?.zgsmAccessToken
78-
}, [tokenOrConfig])
63+
const [data, setData] = useState<ZgsmUserData>({
64+
userInfo: null,
65+
logoPic: "",
66+
hash: "",
67+
isAuthenticated: false,
68+
})
69+
70+
const cacheRef = useRef<{
71+
token?: string
72+
result?: ZgsmUserData
73+
isProcessing: boolean
74+
}>({ isProcessing: false })
7975

80-
// 使用 useMemo 缓存 JWT 解析结果,只有当 token 真正变化时才重新解析
81-
const parsedJwt = useMemo(() => {
82-
if (!token) return null
83-
84-
try {
85-
return parseJwt(token)
86-
} catch (error) {
87-
console.error("Failed to parse JWT token:", error)
88-
return null
89-
}
90-
}, [token])
91-
92-
// 使用 useMemo 缓存用户基本信息,只有当解析结果变化时才重新计算
93-
const userInfo = useMemo((): ZgsmUserInfo | null => {
94-
if (!parsedJwt) return null
95-
96-
return {
97-
id: parsedJwt.id,
98-
name: parsedJwt?.properties?.oauth_GitHub_username || parsedJwt.id,
99-
picture: undefined,
100-
email: parsedJwt.email,
101-
phone: parsedJwt.phone,
102-
organizationName: parsedJwt.organizationName,
103-
organizationImageUrl: parsedJwt.organizationImageUrl,
104-
}
105-
}, [parsedJwt])
106-
107-
// 使用 useCallback 缓存头像处理函数
108-
const processAvatar = useCallback(async (avatarUrl: string) => {
109-
try {
110-
const base64 = await imageUrlToBase64(avatarUrl)
111-
if (base64) {
112-
setLogoPic(base64)
113-
}
114-
} catch (error) {
115-
console.error("Failed to process avatar:", error)
116-
setLogoPic("")
76+
useEffect(() => {
77+
const token = typeof tokenOrConfig === "string" ? tokenOrConfig : tokenOrConfig?.zgsmAccessToken
78+
79+
if (!token) {
80+
setData((prevData) => {
81+
if (prevData.isAuthenticated) {
82+
telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS)
83+
}
84+
85+
return {
86+
userInfo: null,
87+
logoPic: "",
88+
hash: "",
89+
isAuthenticated: false,
90+
}
91+
})
92+
93+
cacheRef.current = { isProcessing: false }
94+
return
11795
}
118-
}, [])
119-
120-
// 使用 useCallback 缓存哈希计算函数
121-
const processTokenHash = useCallback(async (tokenValue: string) => {
122-
try {
123-
const result = await hashToken(tokenValue)
124-
setHash(result)
125-
} catch (error) {
126-
console.error("Failed to hash token:", error)
127-
setHash("")
96+
97+
if (cacheRef.current.token === token && cacheRef.current.result) {
98+
setData(cacheRef.current.result)
99+
return
128100
}
129-
}, [])
130101

131-
// 处理副作用:头像和哈希计算
132-
useEffect(() => {
133-
if (token && parsedJwt) {
134-
wasAuthenticatedRef.current = true
135-
136-
// 处理头像
137-
if (parsedJwt.avatar) {
138-
processAvatar(parsedJwt.avatar)
139-
} else {
140-
setLogoPic("")
102+
if (cacheRef.current.isProcessing) return
103+
cacheRef.current.isProcessing = true
104+
105+
const processToken = async () => {
106+
try {
107+
const parsedJwt = parseJwt(token)
108+
109+
const userInfo: ZgsmUserInfo = {
110+
id: parsedJwt.id,
111+
name: parsedJwt?.properties?.oauth_GitHub_username || parsedJwt.id,
112+
picture: undefined,
113+
email: parsedJwt.email,
114+
phone: parsedJwt.phone,
115+
organizationName: parsedJwt.organizationName,
116+
organizationImageUrl: parsedJwt.organizationImageUrl,
117+
}
118+
119+
const [logoPic, hash] = await Promise.all([
120+
parsedJwt.avatar ? imageUrlToBase64(parsedJwt.avatar) : Promise.resolve(""),
121+
hashToken(token),
122+
])
123+
124+
const result: ZgsmUserData = {
125+
userInfo,
126+
logoPic: logoPic || "",
127+
hash,
128+
isAuthenticated: true,
129+
}
130+
131+
cacheRef.current = {
132+
token,
133+
result,
134+
isProcessing: false,
135+
}
136+
setData(result)
137+
} catch (error) {
138+
console.error("Failed to parse JWT token:", error)
139+
const errorResult: ZgsmUserData = {
140+
userInfo: null,
141+
logoPic: "",
142+
hash: "",
143+
isAuthenticated: true,
144+
}
145+
cacheRef.current.isProcessing = false
146+
setData(errorResult)
141147
}
142-
143-
// 计算token哈希
144-
processTokenHash(token)
145-
} else if (wasAuthenticatedRef.current && !token) {
146-
// 检测到登出
147-
telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS)
148-
wasAuthenticatedRef.current = false
149-
setLogoPic("")
150-
setHash("")
151-
} else if (!token) {
152-
// 确保在没有token时清空状态
153-
setLogoPic("")
154-
setHash("")
155148
}
156-
}, [token, parsedJwt, processAvatar, processTokenHash])
157149

158-
return {
159-
userInfo,
160-
logoPic,
161-
hash,
162-
isAuthenticated: !!token,
163-
}
150+
processToken()
151+
}, [tokenOrConfig])
152+
153+
return data
164154
}

0 commit comments

Comments
 (0)