mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
feat: 刷新消息列表时间改为350ms
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
|||||||
mergeMessagePages,
|
mergeMessagePages,
|
||||||
sortMessagesChronologically
|
sortMessagesChronologically
|
||||||
} from './utils/message-pages'
|
} from './utils/message-pages'
|
||||||
|
import { isRelevantMessageMonitorEvent, parseWcdbMonitorEvent } from './utils/message-monitor'
|
||||||
import { enrichQuotedMessages } from './utils/quoted-messages'
|
import { enrichQuotedMessages } from './utils/quoted-messages'
|
||||||
import type { SelectableReportTemplateId } from '../../shared/report-templates'
|
import type { SelectableReportTemplateId } from '../../shared/report-templates'
|
||||||
import { switchGeneratedReportTemplate } from './utils/report-template-switch'
|
import { switchGeneratedReportTemplate } from './utils/report-template-switch'
|
||||||
@@ -77,7 +78,7 @@ interface SelfInfo {
|
|||||||
|
|
||||||
const MAC_KEY_FAQ_URL = 'https://github.com/Wxw-Gu/TraceMemo/blob/main/docs/mac-disable-sip.md'
|
const MAC_KEY_FAQ_URL = 'https://github.com/Wxw-Gu/TraceMemo/blob/main/docs/mac-disable-sip.md'
|
||||||
const FIRST_USE_WELCOME_SEEN_KEY = 'wxe_first_use_welcome_seen'
|
const FIRST_USE_WELCOME_SEEN_KEY = 'wxe_first_use_welcome_seen'
|
||||||
const MESSAGE_MONITOR_DEBOUNCE_MS = 8000
|
const MESSAGE_MONITOR_DEBOUNCE_MS = 350
|
||||||
const INITIAL_MESSAGE_COUNT = 20
|
const INITIAL_MESSAGE_COUNT = 20
|
||||||
const MESSAGE_PAGE_SIZE = 100
|
const MESSAGE_PAGE_SIZE = 100
|
||||||
const MESSAGE_PREFETCH_COUNT = INITIAL_MESSAGE_COUNT + MESSAGE_PAGE_SIZE
|
const MESSAGE_PREFETCH_COUNT = INITIAL_MESSAGE_COUNT + MESSAGE_PAGE_SIZE
|
||||||
@@ -1387,11 +1388,14 @@ function App(): React.ReactElement {
|
|||||||
const latestMessages = await window.api.getMessages(contactMd5, undefined, undefined, {
|
const latestMessages = await window.api.getMessages(contactMd5, undefined, undefined, {
|
||||||
limit: INITIAL_MESSAGE_COUNT
|
limit: INITIAL_MESSAGE_COUNT
|
||||||
})
|
})
|
||||||
|
if (disposed || selectedContactMd5Ref.current !== contactMd5) return
|
||||||
|
const nextHistory = mergeMessagePages(messageHistoryRef.current, latestMessages)
|
||||||
|
messageHistoryRef.current = nextHistory
|
||||||
const nextMessages = applyGroupMemberMeta(
|
const nextMessages = applyGroupMemberMeta(
|
||||||
selectedContact,
|
selectedContact,
|
||||||
mergeSyntheticMessages(selectedContact, latestMessages)
|
mergeSyntheticMessages(selectedContact, nextHistory.slice(-INITIAL_MESSAGE_COUNT))
|
||||||
)
|
)
|
||||||
if (!disposed) {
|
if (!disposed && selectedContactMd5Ref.current === contactMd5) {
|
||||||
setMessages((current) =>
|
setMessages((current) =>
|
||||||
areMessagesEquivalent(current, nextMessages) ? current : nextMessages
|
areMessagesEquivalent(current, nextMessages) ? current : nextMessages
|
||||||
)
|
)
|
||||||
@@ -1411,11 +1415,13 @@ function App(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = window.api.onWcdbChange(({ json }) => {
|
const unsubscribe = window.api.onWcdbChange(({ json }) => {
|
||||||
const eventText = String(json || '').toLowerCase()
|
const payload = String(json || '')
|
||||||
const targetIds = [contactMd5, selectedContact.m_nsUsrName]
|
const targetIds = [contactMd5, selectedContact.m_nsUsrName].filter(Boolean)
|
||||||
.filter(Boolean)
|
if (!isRelevantMessageMonitorEvent(payload, targetIds)) return
|
||||||
.map((value) => value.toLowerCase())
|
const event = parseWcdbMonitorEvent(payload)
|
||||||
if (!targetIds.some((targetId) => eventText.includes(targetId))) return
|
if (event?.table || event?.db) {
|
||||||
|
console.debug(`event queued table=${event.table || 'unknown'} db=${event.db || 'unknown'}`)
|
||||||
|
}
|
||||||
if (refreshTimer) window.clearTimeout(refreshTimer)
|
if (refreshTimer) window.clearTimeout(refreshTimer)
|
||||||
refreshTimer = window.setTimeout(() => {
|
refreshTimer = window.setTimeout(() => {
|
||||||
refreshTimer = null
|
refreshTimer = null
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export interface WcdbMonitorEvent {
|
||||||
|
db?: string
|
||||||
|
table?: string
|
||||||
|
action?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeMonitorValue = (value: unknown): string =>
|
||||||
|
typeof value === 'string' ? value.trim() : ''
|
||||||
|
|
||||||
|
const monitorBasename = (value: string): string => {
|
||||||
|
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '')
|
||||||
|
const basename = normalized.slice(normalized.lastIndexOf('/') + 1)
|
||||||
|
return basename.replace(/-(?:wal|shm)$/i, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseWcdbMonitorEvent = (rawPayload: string): WcdbMonitorEvent | null => {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(rawPayload)
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
||||||
|
const event = parsed as Record<string, unknown>
|
||||||
|
const result: WcdbMonitorEvent = {}
|
||||||
|
const db = normalizeMonitorValue(event.db)
|
||||||
|
const table = normalizeMonitorValue(event.table)
|
||||||
|
const action = normalizeMonitorValue(event.action)
|
||||||
|
if (db) result.db = db
|
||||||
|
if (table) result.table = table
|
||||||
|
if (action) result.action = action
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMessageStoreName = (value: string): boolean => {
|
||||||
|
const basename = monitorBasename(value).toLowerCase()
|
||||||
|
return (
|
||||||
|
basename === 'message' ||
|
||||||
|
basename.startsWith('message_') ||
|
||||||
|
basename === 'msg' ||
|
||||||
|
basename.startsWith('msg_')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isRelevantMessageMonitorEvent = (
|
||||||
|
rawPayload: string,
|
||||||
|
targetIds: string[] = []
|
||||||
|
): boolean => {
|
||||||
|
const event = parseWcdbMonitorEvent(rawPayload)
|
||||||
|
if (!event) return false
|
||||||
|
|
||||||
|
const db = event.db || ''
|
||||||
|
const table = event.table || ''
|
||||||
|
if (isMessageStoreName(table) || isMessageStoreName(db)) return true
|
||||||
|
|
||||||
|
if (!db && !table) return true
|
||||||
|
|
||||||
|
const normalizedTargets = targetIds.map((value) => value.trim().toLowerCase()).filter(Boolean)
|
||||||
|
const eventText = `${db} ${table}`.toLowerCase()
|
||||||
|
return normalizedTargets.some((targetId) => eventText.includes(targetId))
|
||||||
|
}
|
||||||
@@ -229,6 +229,44 @@ test('CHAT-01 archive More menu is keyboard-safe and keeps the page usable', asy
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('CHAT-REALTIME-01 archive refreshes from a native message-shard event', async () => {
|
||||||
|
const fixture = await launchTestApp()
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||||
|
try {
|
||||||
|
await fixture.setWindowContentSize({ width: 820, height: 600 })
|
||||||
|
await fixture.page.getByText('产品测试群', { exact: true }).click()
|
||||||
|
await expect(fixture.page.getByText('这是一条脱敏测试消息', { exact: true })).toBeVisible()
|
||||||
|
|
||||||
|
const result = await fixture.page.evaluate(() =>
|
||||||
|
window.electron.ipcRenderer.invoke('test:messageChange', {
|
||||||
|
md5: 'group-regular-md5',
|
||||||
|
event: { db: 'message_0.db', table: 'message', action: 'update' },
|
||||||
|
message: {
|
||||||
|
id: 'msg-monitor',
|
||||||
|
localId: 999,
|
||||||
|
from: 'user',
|
||||||
|
type: '普通文本',
|
||||||
|
datetime: '2026-08-31 12:00:00',
|
||||||
|
content: '消息',
|
||||||
|
isSender: false,
|
||||||
|
name: '测试成员',
|
||||||
|
senderId: 'wxid_fixture_member',
|
||||||
|
createTime: Math.floor(Date.now() / 1000),
|
||||||
|
contentData: { type: 'text', content: '消息' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(result).toEqual({ success: true })
|
||||||
|
await expect(fixture.page.getByText('消息', { exact: true })).toBeVisible({
|
||||||
|
timeout: 3_000
|
||||||
|
})
|
||||||
|
expect(pageErrors).toEqual([])
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport', async () => {
|
test('CHAT-02 personal WeChat send dialog is keyboard-safe and fits the viewport', async () => {
|
||||||
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
|
test.skip(process.platform !== 'darwin', 'Personal WeChat sending is currently macOS-only')
|
||||||
const fixture = await launchTestApp()
|
const fixture = await launchTestApp()
|
||||||
|
|||||||
@@ -606,6 +606,19 @@ handle('db:getMessages', (md5, startTime, endTime, options) => {
|
|||||||
if (options && options.limit) messages = messages.slice(-options.limit)
|
if (options && options.limit) messages = messages.slice(-options.limit)
|
||||||
return messages
|
return messages
|
||||||
})
|
})
|
||||||
|
handle('test:messageChange', (payload = {}) => {
|
||||||
|
const md5 = String(payload.md5 || '')
|
||||||
|
const message = payload.message
|
||||||
|
if (md5 && message && Array.isArray(fixture.messages[md5])) {
|
||||||
|
fixture.messages[md5].push(message)
|
||||||
|
}
|
||||||
|
const event = payload.event || { db: 'message_0.db', table: 'message', action: 'update' }
|
||||||
|
const json = typeof event === 'string' ? event : JSON.stringify(event)
|
||||||
|
for (const window of BrowserWindow.getAllWindows()) {
|
||||||
|
if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type: 'update', json })
|
||||||
|
}
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
handle('db:getGroupSnapshot', (md5) =>
|
handle('db:getGroupSnapshot', (md5) =>
|
||||||
md5.startsWith('group-')
|
md5.startsWith('group-')
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
isRelevantMessageMonitorEvent,
|
||||||
|
parseWcdbMonitorEvent
|
||||||
|
} from '../../src/renderer/src/utils/message-monitor'
|
||||||
|
|
||||||
|
describe('message monitor events', () => {
|
||||||
|
it('parses structured native events and normalizes empty fields', () => {
|
||||||
|
expect(
|
||||||
|
parseWcdbMonitorEvent('{"db":"message_0.db","table":"message","action":"update"}')
|
||||||
|
).toEqual({
|
||||||
|
db: 'message_0.db',
|
||||||
|
table: 'message',
|
||||||
|
action: 'update'
|
||||||
|
})
|
||||||
|
expect(parseWcdbMonitorEvent('{"table":" ","action":null}')).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refreshes the active archive for message shard events without a session id', () => {
|
||||||
|
expect(
|
||||||
|
isRelevantMessageMonitorEvent('{"db":"message_0.db","table":"message","action":"update"}', [
|
||||||
|
'group-md5',
|
||||||
|
'room@chatroom'
|
||||||
|
])
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
isRelevantMessageMonitorEvent(
|
||||||
|
'{"db":"/tmp/db_storage/message/Msg_1.db-wal","table":"database"}',
|
||||||
|
['group-md5']
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores unrelated session events but keeps legacy action-only compatibility', () => {
|
||||||
|
expect(
|
||||||
|
isRelevantMessageMonitorEvent('{"db":"session.db","table":"Session"}', [
|
||||||
|
'group-md5',
|
||||||
|
'room@chatroom'
|
||||||
|
])
|
||||||
|
).toBe(false)
|
||||||
|
expect(isRelevantMessageMonitorEvent('{"action":"update"}', ['group-md5'])).toBe(true)
|
||||||
|
expect(isRelevantMessageMonitorEvent('not-json', ['group-md5'])).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user