feat(chat): smooth loading with progressive session hydration

This commit is contained in:
tisonhuang
2026-03-01 18:22:51 +08:00
parent dbdb2e2959
commit 22b6a07749
7 changed files with 189 additions and 49 deletions

View File

@@ -912,6 +912,10 @@ function registerIpcHandlers() {
return chatService.getSessions() return chatService.getSessions()
}) })
ipcMain.handle('chat:getSessionStatuses', async (_, usernames: string[]) => {
return chatService.getSessionStatuses(usernames)
})
ipcMain.handle('chat:getExportTabCounts', async () => { ipcMain.handle('chat:getExportTabCounts', async () => {
return chatService.getExportTabCounts() return chatService.getExportTabCounts()
}) })

View File

@@ -130,6 +130,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
chat: { chat: {
connect: () => ipcRenderer.invoke('chat:connect'), connect: () => ipcRenderer.invoke('chat:connect'),
getSessions: () => ipcRenderer.invoke('chat:getSessions'), getSessions: () => ipcRenderer.invoke('chat:getSessions'),
getSessionStatuses: (usernames: string[]) => ipcRenderer.invoke('chat:getSessionStatuses', usernames),
getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'), getExportTabCounts: () => ipcRenderer.invoke('chat:getExportTabCounts'),
getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds), getSessionMessageCounts: (sessionIds: string[]) => ipcRenderer.invoke('chat:getSessionMessageCounts', sessionIds),
enrichSessionsContactInfo: (usernames: string[]) => enrichSessionsContactInfo: (usernames: string[]) =>

View File

@@ -201,6 +201,8 @@ class ChatService {
private sessionMessageCountHintCache = new Map<string, number>() private sessionMessageCountHintCache = new Map<string, number>()
private sessionMessageCountCacheScope = '' private sessionMessageCountCacheScope = ''
private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000 private readonly sessionMessageCountCacheTtlMs = 10 * 60 * 1000
private sessionStatusCache = new Map<string, { isFolded?: boolean; isMuted?: boolean; updatedAt: number }>()
private readonly sessionStatusCacheTtlMs = 10 * 60 * 1000
constructor() { constructor() {
this.configService = new ConfigService() this.configService = new ConfigService()
@@ -386,7 +388,7 @@ class ChatService {
return { success: false, error: `会话表异常: ${detail}${tableInfo}${tables}${columns}` } return { success: false, error: `会话表异常: ${detail}${tableInfo}${tables}${columns}` }
} }
// 转换为 ChatSession先加载缓存但不等待数据库查询) // 转换为 ChatSession先加载缓存但不等待额外状态查询)
const sessions: ChatSession[] = [] const sessions: ChatSession[] = []
const now = Date.now() const now = Date.now()
const myWxid = this.configService.get('myWxid') const myWxid = this.configService.get('myWxid')
@@ -449,7 +451,7 @@ class ChatService {
avatarUrl = cached.avatarUrl avatarUrl = cached.avatarUrl
} }
sessions.push({ const nextSession: ChatSession = {
username, username,
type: parseInt(row.type || '0', 10), type: parseInt(row.type || '0', 10),
unreadCount: parseInt(row.unread_count || row.unreadCount || row.unreadcount || '0', 10), unreadCount: parseInt(row.unread_count || row.unreadCount || row.unreadcount || '0', 10),
@@ -463,7 +465,15 @@ class ChatService {
lastMsgSender: row.last_msg_sender, lastMsgSender: row.last_msg_sender,
lastSenderDisplayName: row.last_sender_display_name, lastSenderDisplayName: row.last_sender_display_name,
selfWxid: myWxid selfWxid: myWxid
}) }
const cachedStatus = this.sessionStatusCache.get(username)
if (cachedStatus && now - cachedStatus.updatedAt <= this.sessionStatusCacheTtlMs) {
nextSession.isFolded = cachedStatus.isFolded
nextSession.isMuted = cachedStatus.isMuted
}
sessions.push(nextSession)
if (typeof messageCountHint === 'number') { if (typeof messageCountHint === 'number') {
this.sessionMessageCountHintCache.set(username, messageCountHint) this.sessionMessageCountHintCache.set(username, messageCountHint)
@@ -474,23 +484,6 @@ class ChatService {
} }
} }
// 批量拉取 extra_buffer 状态isFolded/isMuted不阻塞主流程
const allUsernames = sessions.map(s => s.username)
try {
const statusResult = await wcdbService.getContactStatus(allUsernames)
if (statusResult.success && statusResult.map) {
for (const s of sessions) {
const st = statusResult.map[s.username]
if (st) {
s.isFolded = st.isFolded
s.isMuted = st.isMuted
}
}
}
} catch {
// 状态获取失败不影响会话列表返回
}
// 不等待联系人信息加载,直接返回基础会话列表 // 不等待联系人信息加载,直接返回基础会话列表
// 前端可以异步调用 enrichSessionsWithContacts 来补充信息 // 前端可以异步调用 enrichSessionsWithContacts 来补充信息
return { success: true, sessions } return { success: true, sessions }
@@ -500,6 +493,46 @@ class ChatService {
} }
} }
async getSessionStatuses(usernames: string[]): Promise<{
success: boolean
map?: Record<string, { isFolded?: boolean; isMuted?: boolean }>
error?: string
}> {
try {
if (!Array.isArray(usernames) || usernames.length === 0) {
return { success: true, map: {} }
}
const connectResult = await this.ensureConnected()
if (!connectResult.success) {
return { success: false, error: connectResult.error }
}
const result = await wcdbService.getContactStatus(usernames)
if (!result.success || !result.map) {
return { success: false, error: result.error || '获取会话状态失败' }
}
const now = Date.now()
for (const username of usernames) {
const state = result.map[username]
if (!state) continue
this.sessionStatusCache.set(username, {
isFolded: state.isFolded,
isMuted: state.isMuted,
updatedAt: now
})
}
return {
success: true,
map: result.map as Record<string, { isFolded?: boolean; isMuted?: boolean }>
}
} catch (e) {
return { success: false, error: String(e) }
}
}
/** /**
* 异步补充会话列表的联系人信息(公开方法,供前端调用) * 异步补充会话列表的联系人信息(公开方法,供前端调用)
*/ */
@@ -1532,6 +1565,7 @@ class ChatService {
this.sessionMessageCountCacheScope = scope this.sessionMessageCountCacheScope = scope
this.sessionMessageCountCache.clear() this.sessionMessageCountCache.clear()
this.sessionMessageCountHintCache.clear() this.sessionMessageCountHintCache.clear()
this.sessionStatusCache.clear()
} }
private async collectSessionExportStats( private async collectSessionExportStats(

View File

@@ -815,6 +815,24 @@
min-width: 0; min-width: 0;
} }
.session-sync-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 999px;
background: var(--bg-primary);
color: var(--text-tertiary);
font-size: 11px;
white-space: nowrap;
border: 1px solid var(--border-color);
flex-shrink: 0;
.spin {
animation: spin 0.9s linear infinite;
}
}
.search-box { .search-box {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -1651,6 +1669,18 @@
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
} }
&.switching .message-list {
opacity: 0.42;
transform: scale(0.995);
filter: saturate(0.72) blur(1px);
pointer-events: none;
}
&.switching .loading-overlay {
background: rgba(127, 127, 127, 0.18);
backdrop-filter: blur(4px);
}
} }
.message-list { .message-list {
@@ -1666,7 +1696,7 @@
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
position: relative; position: relative;
-webkit-app-region: no-drag !important; -webkit-app-region: no-drag !important;
transition: opacity 240ms ease, transform 240ms ease; transition: opacity 240ms ease, transform 240ms ease, filter 220ms ease;
// 滚动条样式 // 滚动条样式
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -4133,7 +4163,6 @@
font-weight: 500; font-weight: 500;
} }
} }
// 消息信息弹窗 // 消息信息弹窗
.message-info-overlay { .message-info-overlay {
position: fixed; position: fixed;
@@ -4298,4 +4327,4 @@
user-select: text; user-select: text;
} }
} }
} }

View File

@@ -317,6 +317,7 @@ function ChatPage(_props: ChatPageProps) {
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false) const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图 const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
const [hasInitialMessages, setHasInitialMessages] = useState(false) const [hasInitialMessages, setHasInitialMessages] = useState(false)
const [isSessionSwitching, setIsSessionSwitching] = useState(false)
const [noMessageTable, setNoMessageTable] = useState(false) const [noMessageTable, setNoMessageTable] = useState(false)
const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null) const [fallbackDisplayName, setFallbackDisplayName] = useState<string | null>(null)
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false) const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
@@ -376,6 +377,7 @@ function ChatPage(_props: ChatPageProps) {
const sessionMapRef = useRef<Map<string, ChatSession>>(new Map()) const sessionMapRef = useRef<Map<string, ChatSession>>(new Map())
const sessionsRef = useRef<ChatSession[]>([]) const sessionsRef = useRef<ChatSession[]>([])
const currentSessionRef = useRef<string | null>(null) const currentSessionRef = useRef<string | null>(null)
const pendingSessionLoadRef = useRef<string | null>(null)
const prevSessionRef = useRef<string | null>(null) const prevSessionRef = useRef<string | null>(null)
const isLoadingMessagesRef = useRef(false) const isLoadingMessagesRef = useRef(false)
const isLoadingMoreRef = useRef(false) const isLoadingMoreRef = useRef(false)
@@ -447,10 +449,10 @@ function ChatPage(_props: ChatPageProps) {
const result = await window.electronAPI.chat.connect() const result = await window.electronAPI.chat.connect()
if (result.success) { if (result.success) {
setConnected(true) setConnected(true)
await loadSessions() const wxidPromise = window.electronAPI.config.get('myWxid')
await loadMyAvatar() await Promise.all([loadSessions(), loadMyAvatar()])
// 获取 myWxid 用于匹配个人头像 // 获取 myWxid 用于匹配个人头像
const wxid = await window.electronAPI.config.get('myWxid') const wxid = await wxidPromise
if (wxid) setMyWxid(wxid as string) if (wxid) setMyWxid(wxid as string)
} else { } else {
setConnectionError(result.error || '连接失败') setConnectionError(result.error || '连接失败')
@@ -467,6 +469,8 @@ function ChatPage(_props: ChatPageProps) {
senderAvatarLoading.clear() senderAvatarLoading.clear()
preloadImageKeysRef.current.clear() preloadImageKeysRef.current.clear()
lastPreloadSessionRef.current = null lastPreloadSessionRef.current = null
pendingSessionLoadRef.current = null
setIsSessionSwitching(false)
setSessionDetail(null) setSessionDetail(null)
setCurrentSession(null) setCurrentSession(null)
setSessions([]) setSessions([])
@@ -499,6 +503,45 @@ function ChatPage(_props: ChatPageProps) {
currentSessionRef.current = currentSessionId currentSessionRef.current = currentSessionId
}, [currentSessionId]) }, [currentSessionId])
const hydrateSessionStatuses = useCallback(async (sessionList: ChatSession[]) => {
const usernames = sessionList.map((s) => s.username).filter(Boolean)
if (usernames.length === 0) return
try {
const result = await window.electronAPI.chat.getSessionStatuses(usernames)
if (!result.success || !result.map) return
const statusMap = result.map
const { sessions: latestSessions } = useChatStore.getState()
if (!Array.isArray(latestSessions) || latestSessions.length === 0) return
let hasChanges = false
const updatedSessions = latestSessions.map((session) => {
const status = statusMap[session.username]
if (!status) return session
const nextIsFolded = status.isFolded ?? session.isFolded
const nextIsMuted = status.isMuted ?? session.isMuted
if (nextIsFolded === session.isFolded && nextIsMuted === session.isMuted) {
return session
}
hasChanges = true
return {
...session,
isFolded: nextIsFolded,
isMuted: nextIsMuted
}
})
if (hasChanges) {
setSessions(updatedSessions)
}
} catch (e) {
console.warn('会话状态补齐失败:', e)
}
}, [setSessions])
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息) // 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
const loadSessions = async (options?: { silent?: boolean }) => { const loadSessions = async (options?: { silent?: boolean }) => {
if (options?.silent) { if (options?.silent) {
@@ -518,11 +561,13 @@ function ChatPage(_props: ChatPageProps) {
setSessions(nextSessions) setSessions(nextSessions)
sessionsRef.current = nextSessions sessionsRef.current = nextSessions
void hydrateSessionStatuses(nextSessions)
// 立即启动联系人信息加载,不再延迟 500ms // 立即启动联系人信息加载,不再延迟 500ms
void enrichSessionsContactInfo(nextSessions) void enrichSessionsContactInfo(nextSessions)
} else { } else {
console.error('mergeSessions returned non-array:', nextSessions) console.error('mergeSessions returned non-array:', nextSessions)
setSessions(sessionsArray) setSessions(sessionsArray)
void hydrateSessionStatuses(sessionsArray)
void enrichSessionsContactInfo(sessionsArray) void enrichSessionsContactInfo(sessionsArray)
} }
} else if (!result.success) { } else if (!result.success) {
@@ -575,8 +620,8 @@ function ChatPage(_props: ChatPageProps) {
// 进一步减少批次大小每批3个避免DLL调用阻塞 // 批量补齐联系人,平衡吞吐和 UI 流畅性
const batchSize = 3 const batchSize = 8
let loadedCount = 0 let loadedCount = 0
for (let i = 0; i < needEnrich.length; i += batchSize) { for (let i = 0; i < needEnrich.length; i += batchSize) {
@@ -585,7 +630,7 @@ function ChatPage(_props: ChatPageProps) {
// 等待滚动结束 // 等待滚动结束
while (isScrollingRef.current && !enrichCancelledRef.current) { while (isScrollingRef.current && !enrichCancelledRef.current) {
await new Promise(resolve => setTimeout(resolve, 200)) await new Promise(resolve => setTimeout(resolve, 120))
} }
if (enrichCancelledRef.current) break if (enrichCancelledRef.current) break
} }
@@ -602,11 +647,11 @@ function ChatPage(_props: ChatPageProps) {
if ('requestIdleCallback' in window) { if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => { window.requestIdleCallback(() => {
void loadContactInfoBatch(usernames).then(() => resolve()) void loadContactInfoBatch(usernames).then(() => resolve())
}, { timeout: 2000 }) }, { timeout: 700 })
} else { } else {
setTimeout(() => { setTimeout(() => {
void loadContactInfoBatch(usernames).then(() => resolve()) void loadContactInfoBatch(usernames).then(() => resolve())
}, 300) }, 80)
} }
}) })
@@ -618,8 +663,7 @@ function ChatPage(_props: ChatPageProps) {
// 批次间延迟给UI更多时间DLL调用可能阻塞需要更长的延迟 // 批次间延迟给UI更多时间DLL调用可能阻塞需要更长的延迟
if (i + batchSize < needEnrich.length && !enrichCancelledRef.current) { if (i + batchSize < needEnrich.length && !enrichCancelledRef.current) {
// 如果不在滚动,可以延迟短一点 const delay = isScrollingRef.current ? 260 : 120
const delay = isScrollingRef.current ? 1000 : 800
await new Promise(resolve => setTimeout(resolve, delay)) await new Promise(resolve => setTimeout(resolve, delay))
} }
} }
@@ -649,17 +693,17 @@ function ChatPage(_props: ChatPageProps) {
contactUpdateTimerRef.current = null contactUpdateTimerRef.current = null
} }
// 增加防抖延迟到500ms避免在滚动时频繁更新 // 使用短防抖,让头像和昵称更快补齐但依然避免频繁重渲染
contactUpdateTimerRef.current = window.setTimeout(() => { contactUpdateTimerRef.current = window.setTimeout(() => {
const updates = contactUpdateQueueRef.current const updates = contactUpdateQueueRef.current
if (updates.size === 0) return if (updates.size === 0) return
const now = Date.now() const now = Date.now()
// 如果距离上次更新太近(小于1秒),继续延迟 // 如果距离上次更新太近(小于250ms),继续延迟
if (now - lastUpdateTimeRef.current < 1000) { if (now - lastUpdateTimeRef.current < 250) {
contactUpdateTimerRef.current = window.setTimeout(() => { contactUpdateTimerRef.current = window.setTimeout(() => {
flushContactUpdates() flushContactUpdates()
}, 1000 - (now - lastUpdateTimeRef.current)) }, 250 - (now - lastUpdateTimeRef.current))
return return
} }
@@ -696,7 +740,7 @@ function ChatPage(_props: ChatPageProps) {
updates.clear() updates.clear()
contactUpdateTimerRef.current = null contactUpdateTimerRef.current = null
}, 500) // 500ms 防抖,减少更新频率 }, 120)
}, [setSessions]) }, [setSessions])
// 加载一批联系人信息并更新会话列表(优化:使用队列批量更新) // 加载一批联系人信息并更新会话列表(优化:使用队列批量更新)
@@ -885,7 +929,8 @@ function ChatPage(_props: ChatPageProps) {
if (offset === 0) { if (offset === 0) {
setLoadingMessages(true) setLoadingMessages(true)
setMessages([]) // 切会话时保留旧内容作为过渡,避免大面积闪烁
setHasInitialMessages(true)
} else { } else {
setLoadingMore(true) setLoadingMore(true)
} }
@@ -900,6 +945,9 @@ function ChatPage(_props: ChatPageProps) {
hasMore?: boolean; hasMore?: boolean;
error?: string error?: string
} }
if (currentSessionRef.current !== sessionId) {
return
}
if (result.success && result.messages) { if (result.success && result.messages) {
if (offset === 0) { if (offset === 0) {
setMessages(result.messages) setMessages(result.messages)
@@ -996,9 +1044,16 @@ function ChatPage(_props: ChatPageProps) {
console.error('加载消息失败:', e) console.error('加载消息失败:', e)
setConnectionError('加载消息失败') setConnectionError('加载消息失败')
setHasMoreMessages(false) setHasMoreMessages(false)
if (offset === 0 && currentSessionRef.current === sessionId) {
setMessages([])
}
} finally { } finally {
setLoadingMessages(false) setLoadingMessages(false)
setLoadingMore(false) setLoadingMore(false)
if (offset === 0 && pendingSessionLoadRef.current === sessionId) {
pendingSessionLoadRef.current = null
setIsSessionSwitching(false)
}
} }
} }
@@ -1042,11 +1097,13 @@ function ChatPage(_props: ChatPageProps) {
return return
} }
if (session.username === currentSessionId) return if (session.username === currentSessionId) return
setCurrentSession(session.username) pendingSessionLoadRef.current = session.username
setIsSessionSwitching(true)
setCurrentSession(session.username, { preserveMessages: true })
setCurrentOffset(0) setCurrentOffset(0)
setJumpStartTime(0) setJumpStartTime(0)
setJumpEndTime(0) setJumpEndTime(0)
loadMessages(session.username, 0, 0, 0) void loadMessages(session.username, 0, 0, 0)
// 重置详情面板 // 重置详情面板
setSessionDetail(null) setSessionDetail(null)
if (showDetailPanel) { if (showDetailPanel) {
@@ -1368,6 +1425,10 @@ function ChatPage(_props: ChatPageProps) {
) )
}, [sessions, searchKeyword, foldedView]) }, [sessions, searchKeyword, foldedView])
const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0
const shouldShowSessionsSkeleton = isLoadingSessions && !hasSessionRecords
const isSessionListSyncing = (isLoadingSessions || isRefreshingSessions) && hasSessionRecords
// 格式化会话时间(相对时间)- 使用 useMemo 缓存,避免每次渲染都计算 // 格式化会话时间(相对时间)- 使用 useMemo 缓存,避免每次渲染都计算
const formatSessionTime = useCallback((timestamp: number): string => { const formatSessionTime = useCallback((timestamp: number): string => {
@@ -2068,6 +2129,12 @@ function ChatPage(_props: ChatPageProps) {
<button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}> <button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}>
<RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} /> <RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} />
</button> </button>
{isSessionListSyncing && (
<div className="session-sync-indicator">
<Loader2 size={12} className="spin" />
<span></span>
</div>
)}
</div> </div>
</div> </div>
{/* 折叠群 header */} {/* 折叠群 header */}
@@ -2093,7 +2160,7 @@ function ChatPage(_props: ChatPageProps) {
)} )}
{/* ... (previous content) ... */} {/* ... (previous content) ... */}
{isLoadingSessions ? ( {shouldShowSessionsSkeleton ? (
<div className="loading-sessions"> <div className="loading-sessions">
{[1, 2, 3, 4, 5].map(i => ( {[1, 2, 3, 4, 5].map(i => (
<div key={i} className="skeleton-item"> <div key={i} className="skeleton-item">
@@ -2311,11 +2378,11 @@ function ChatPage(_props: ChatPageProps) {
</div> </div>
</div> </div>
<div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'}`}> <div className={`message-content-wrapper ${hasInitialMessages ? 'loaded' : 'loading'} ${isSessionSwitching ? 'switching' : ''}`}>
{isLoadingMessages && !hasInitialMessages && ( {isLoadingMessages && (!hasInitialMessages || isSessionSwitching) && (
<div className="loading-messages loading-overlay"> <div className="loading-messages loading-overlay">
<Loader2 size={24} /> <Loader2 size={24} />
<span>...</span> <span>{isSessionSwitching ? '切换会话中...' : '加载消息中...'}</span>
</div> </div>
)} )}
<div <div

View File

@@ -32,7 +32,7 @@ export interface ChatState {
setConnectionError: (error: string | null) => void setConnectionError: (error: string | null) => void
setSessions: (sessions: ChatSession[]) => void setSessions: (sessions: ChatSession[]) => void
setFilteredSessions: (sessions: ChatSession[]) => void setFilteredSessions: (sessions: ChatSession[]) => void
setCurrentSession: (sessionId: string | null) => void setCurrentSession: (sessionId: string | null, options?: { preserveMessages?: boolean }) => void
setLoadingSessions: (loading: boolean) => void setLoadingSessions: (loading: boolean) => void
setMessages: (messages: Message[]) => void setMessages: (messages: Message[]) => void
appendMessages: (messages: Message[], prepend?: boolean) => void appendMessages: (messages: Message[], prepend?: boolean) => void
@@ -69,12 +69,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
setSessions: (sessions) => set({ sessions, filteredSessions: sessions }), setSessions: (sessions) => set({ sessions, filteredSessions: sessions }),
setFilteredSessions: (sessions) => set({ filteredSessions: sessions }), setFilteredSessions: (sessions) => set({ filteredSessions: sessions }),
setCurrentSession: (sessionId) => set({ setCurrentSession: (sessionId, options) => set((state) => ({
currentSessionId: sessionId, currentSessionId: sessionId,
messages: [], messages: options?.preserveMessages ? state.messages : [],
hasMoreMessages: true, hasMoreMessages: true,
hasMoreLater: false hasMoreLater: false
}), })),
setLoadingSessions: (loading) => set({ isLoadingSessions: loading }), setLoadingSessions: (loading) => set({ isLoadingSessions: loading }),

View File

@@ -74,6 +74,11 @@ export interface ElectronAPI {
chat: { chat: {
connect: () => Promise<{ success: boolean; error?: string }> connect: () => Promise<{ success: boolean; error?: string }>
getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }> getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }>
getSessionStatuses: (usernames: string[]) => Promise<{
success: boolean
map?: Record<string, { isFolded?: boolean; isMuted?: boolean }>
error?: string
}>
getExportTabCounts: () => Promise<{ getExportTabCounts: () => Promise<{
success: boolean success: boolean
counts?: { counts?: {