支持一键已读

This commit is contained in:
cc
2026-05-04 23:34:49 +08:00
parent fd0db6e306
commit c09128b83e
8 changed files with 110 additions and 0 deletions

View File

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

View File

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

View File

@@ -978,6 +978,23 @@ class ChatService {
}
}
async markAllSessionsRead(): Promise<{ success: boolean; error?: string }> {
try {
const connectResult = await this.ensureConnected()
if (!connectResult.success) {
return { success: false, error: connectResult.error }
}
const result = await wcdbService.markAllSessionsRead()
if (result.success) {
this.syntheticUnreadState.clear()
}
return result
} catch (e) {
console.error('ChatService: 一键已读失败:', e)
return { success: false, error: String(e) }
}
}
private getSessionUsername(row: Record<string, any>): string {
return String(
row.username ||

View File

@@ -35,6 +35,7 @@ export class WcdbCore {
private wcdbUpdateMessage: any = null
private wcdbDeleteMessage: any = null
private wcdbGetSessions: any = null
private wcdbMarkAllSessionsRead: any = null
private wcdbGetMessages: any = null
private wcdbGetMessageCount: any = null
private wcdbGetDisplayNames: any = null
@@ -810,6 +811,13 @@ export class WcdbCore {
// wcdb_status wcdb_get_sessions(wcdb_handle handle, char** out_json)
this.wcdbGetSessions = this.lib.func('int32 wcdb_get_sessions(int64 handle, _Out_ void** outJson)')
// wcdb_status wcdb_mark_all_sessions_read(wcdb_handle handle, char** out_error)
try {
this.wcdbMarkAllSessionsRead = this.lib.func('int32 wcdb_mark_all_sessions_read(int64 handle, _Out_ void** outError)')
} catch {
this.wcdbMarkAllSessionsRead = null
}
// wcdb_status wcdb_get_messages(wcdb_handle handle, const char* username, int32_t limit, int32_t offset, char** out_json)
this.wcdbGetMessages = this.lib.func('int32 wcdb_get_messages(int64 handle, const char* username, int32 limit, int32 offset, _Out_ void** outJson)')
@@ -1697,6 +1705,39 @@ export class WcdbCore {
}
}
async markAllSessionsRead(): Promise<{ success: boolean; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
}
if (!this.wcdbMarkAllSessionsRead) {
return { success: false, error: '当前数据服务版本不支持一键已读' }
}
try {
await new Promise(resolve => setImmediate(resolve))
const outPtr = [null as any]
const result = this.wcdbMarkAllSessionsRead(this.handle, outPtr)
let message = ''
if (outPtr[0]) {
try { message = this.koffi.decode(outPtr[0], 'char', -1) } catch { }
try { this.wcdbFreeString(outPtr[0]) } catch { }
}
await new Promise(resolve => setImmediate(resolve))
if (result !== 0) {
this.writeLog(`markAllSessionsRead failed: code=${result} error=${message}`)
return { success: false, error: message || `一键已读失败: ${result}` }
}
this.clearMediaStreamSessionCache()
this.writeLog('markAllSessionsRead ok')
return { success: true }
} catch (e) {
this.writeLog(`markAllSessionsRead exception: ${String(e)}`)
return { success: false, error: String(e) }
}
}
async getMessages(sessionId: string, limit: number, offset: number): Promise<{ success: boolean; messages?: any[]; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }

View File

@@ -204,6 +204,10 @@ export class WcdbService {
return this.callWorker('getSessions')
}
async markAllSessionsRead(): Promise<{ success: boolean; error?: string }> {
return this.callWorker('markAllSessionsRead')
}
/**
* 获取消息列表
*/

View File

@@ -50,6 +50,9 @@ if (parentPort) {
case 'getSessions':
result = await core.getSessions()
break
case 'markAllSessionsRead':
result = await core.markAllSessionsRead()
break
case 'getMessages':
result = await core.getMessages(payload.sessionId, payload.limit, payload.offset)
break

View File

@@ -1453,6 +1453,7 @@ function ChatPage(props: ChatPageProps) {
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [sidebarWidth, setSidebarWidth] = useState(260)
const [isResizing, setIsResizing] = useState(false)
const [isMarkingAllSessionsRead, setIsMarkingAllSessionsRead] = useState(false)
const [showDetailPanel, setShowDetailPanel] = useState(false)
const [showGroupMembersPanel, setShowGroupMembersPanel] = useState(false)
const [sessionDetail, setSessionDetail] = useState<SessionDetail | null>(null)
@@ -3130,6 +3131,35 @@ function ChatPage(props: ChatPageProps) {
}
}
const handleMarkAllSessionsRead = async () => {
if (isMarkingAllSessionsRead || isLoadingSessions || isRefreshingSessions) return
setIsMarkingAllSessionsRead(true)
setConnectionError(null)
try {
const result = await window.electronAPI.chat.markAllSessionsRead()
if (!result.success) {
setConnectionError(result.error || '一键已读失败')
return
}
const latestSessions = useChatStore.getState().sessions || []
const nextSessions = latestSessions.map((session) => (
session.unreadCount > 0 ? { ...session, unreadCount: 0 } : session
))
setSessions(nextSessions)
sessionsRef.current = nextSessions
const scope = await resolveChatCacheScope()
persistSessionListCache(scope, nextSessions)
await loadSessions({ silent: true })
} catch (e) {
console.error('一键已读失败:', e)
setConnectionError(`一键已读失败: ${String(e)}`)
} finally {
setIsMarkingAllSessionsRead(false)
}
}
// 分批异步加载联系人信息(优化:缓存优先 + 可持续队列 + 首屏优先批次)
const enrichSessionsContactInfo = async (sessions: ChatSession[]) => {
if (Array.isArray(sessions) && sessions.length > 0) {
@@ -6775,6 +6805,15 @@ function ChatPage(props: ChatPageProps) {
<button className="icon-btn refresh-btn" onClick={handleRefresh} disabled={isLoadingSessions || isRefreshingSessions}>
<RefreshCw size={16} className={(isLoadingSessions || isRefreshingSessions) ? 'spin' : ''} />
</button>
<button
className="icon-btn refresh-btn mark-read-btn"
onClick={handleMarkAllSessionsRead}
disabled={isMarkingAllSessionsRead || isLoadingSessions || isRefreshingSessions}
title="一键已读"
aria-label="一键已读"
>
{isMarkingAllSessionsRead ? <Loader2 size={16} className="spin" /> : <CheckSquare size={16} />}
</button>
</div>
</div>
{/* 折叠群 header */}

View File

@@ -271,6 +271,7 @@ export interface ElectronAPI {
chat: {
connect: () => Promise<{ success: boolean; error?: string }>
getSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }>
markAllSessionsRead: () => Promise<{ success: boolean; error?: string }>
getAntiRevokeSessions: () => Promise<{ success: boolean; sessions?: ChatSession[]; error?: string }>
getSessionStatuses: (usernames: string[]) => Promise<{
success: boolean