mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-08 15:43:22 +08:00
refactor: 重构一下档案搜索功能 支持拼音微信号等
This commit is contained in:
@@ -98,6 +98,7 @@
|
|||||||
"jsonrepair": "^3.15.0",
|
"jsonrepair": "^3.15.0",
|
||||||
"koffi": "^3.1.0",
|
"koffi": "^3.1.0",
|
||||||
"openai": "^6.10.0",
|
"openai": "^6.10.0",
|
||||||
|
"pinyin-pro": "^3.26.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sherpa-onnx-node": "1.13.3",
|
"sherpa-onnx-node": "1.13.3",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
|
|||||||
Generated
+6
@@ -61,6 +61,7 @@ specifiers:
|
|||||||
jsonrepair: ^3.15.0
|
jsonrepair: ^3.15.0
|
||||||
koffi: ^3.1.0
|
koffi: ^3.1.0
|
||||||
openai: ^6.10.0
|
openai: ^6.10.0
|
||||||
|
pinyin-pro: ^3.26.0
|
||||||
postcss: ^8.5.26
|
postcss: ^8.5.26
|
||||||
prettier: ^3.7.4
|
prettier: ^3.7.4
|
||||||
qrcode: ^1.5.4
|
qrcode: ^1.5.4
|
||||||
@@ -108,6 +109,7 @@ dependencies:
|
|||||||
jsonrepair: 3.15.0
|
jsonrepair: 3.15.0
|
||||||
koffi: 3.1.0
|
koffi: 3.1.0
|
||||||
openai: 6.10.0
|
openai: 6.10.0
|
||||||
|
pinyin-pro: 3.29.3
|
||||||
qrcode: 1.5.4
|
qrcode: 1.5.4
|
||||||
sherpa-onnx-node: 1.13.3
|
sherpa-onnx-node: 1.13.3
|
||||||
silk-wasm: 3.7.1
|
silk-wasm: 3.7.1
|
||||||
@@ -6835,6 +6837,10 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pinyin-pro/3.29.3:
|
||||||
|
resolution: {integrity: sha512-+UU9bx6vfDw8amOJGHm0TE0rdQl8VPylsDWviQ5OOQ3e+on1xRP4OqDbiDuMT5OISgvfl/Y6ez1BBRaIP80GLQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pirates/4.0.7:
|
/pirates/4.0.7:
|
||||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service'
|
import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service'
|
||||||
import type { ExportImageQuality } from '../../shared/image-quality'
|
import type { ExportImageQuality } from '../../shared/image-quality'
|
||||||
import { wcdbDebugLog } from '../wcdb-debug'
|
import { wcdbDebugLog } from '../wcdb-debug'
|
||||||
|
import {
|
||||||
|
buildContactSearchIndex,
|
||||||
|
filterContactSearchIndex,
|
||||||
|
type ContactSearchIndex
|
||||||
|
} from '../../shared/contact-search'
|
||||||
|
|
||||||
export function getCurrentKey(): string {
|
export function getCurrentKey(): string {
|
||||||
if (!dbRef) return ''
|
if (!dbRef) return ''
|
||||||
@@ -46,6 +51,8 @@ export interface FormattedContact {
|
|||||||
remark?: string
|
remark?: string
|
||||||
alias?: string
|
alias?: string
|
||||||
wechatId?: string
|
wechatId?: string
|
||||||
|
wxid?: string
|
||||||
|
legacyIdentifier?: string
|
||||||
isFolded?: boolean
|
isFolded?: boolean
|
||||||
isMuted?: boolean
|
isMuted?: boolean
|
||||||
}
|
}
|
||||||
@@ -137,6 +144,7 @@ function normalizeMsgType(value: string | number | undefined): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dbRef: WechatDb | null = null
|
let dbRef: WechatDb | null = null
|
||||||
|
let contactSearchIndexCache: { signature: string; index: ContactSearchIndex } | null = null
|
||||||
let shutdownRequested = false
|
let shutdownRequested = false
|
||||||
|
|
||||||
export interface ImageMessageReference {
|
export interface ImageMessageReference {
|
||||||
@@ -156,6 +164,7 @@ export function setChatDb(db: WechatDb | null): boolean {
|
|||||||
}
|
}
|
||||||
dbRef?.close()
|
dbRef?.close()
|
||||||
dbRef = db
|
dbRef = db
|
||||||
|
contactSearchIndexCache = null
|
||||||
imageMessageReferences.clear()
|
imageMessageReferences.clear()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -188,7 +197,7 @@ export function listContacts(filter?: string): FormattedContact[] {
|
|||||||
|
|
||||||
const contacts: FormattedContact[] = []
|
const contacts: FormattedContact[] = []
|
||||||
const groupContacts = dbRef.getAllGroupContacts()
|
const groupContacts = dbRef.getAllGroupContacts()
|
||||||
const userList = dbRef.getUserList(filter)
|
const userList = dbRef.getUserList()
|
||||||
const existingMd5s = new Set<string>()
|
const existingMd5s = new Set<string>()
|
||||||
|
|
||||||
for (const user of userList) {
|
for (const user of userList) {
|
||||||
@@ -206,6 +215,8 @@ export function listContacts(filter?: string): FormattedContact[] {
|
|||||||
remark: user.remark,
|
remark: user.remark,
|
||||||
alias: user.alias,
|
alias: user.alias,
|
||||||
wechatId: user.wechatId,
|
wechatId: user.wechatId,
|
||||||
|
wxid: user.wxid || user.m_nsUsrName,
|
||||||
|
legacyIdentifier: user.legacyIdentifier,
|
||||||
isFolded: user.isFolded,
|
isFolded: user.isFolded,
|
||||||
isMuted: user.isMuted
|
isMuted: user.isMuted
|
||||||
})
|
})
|
||||||
@@ -236,7 +247,30 @@ export function listContacts(filter?: string): FormattedContact[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return contacts
|
if (!filter) return contacts
|
||||||
|
const signature = contacts
|
||||||
|
.map((contact) =>
|
||||||
|
[
|
||||||
|
contact.md5,
|
||||||
|
contact.m_nsNickName,
|
||||||
|
contact.remark,
|
||||||
|
contact.wechatNickname,
|
||||||
|
contact.alias,
|
||||||
|
contact.wechatId,
|
||||||
|
contact.wxid,
|
||||||
|
contact.legacyIdentifier,
|
||||||
|
contact.avatar,
|
||||||
|
contact.isFolded,
|
||||||
|
contact.isMuted
|
||||||
|
]
|
||||||
|
.map((value) => String(value || ''))
|
||||||
|
.join('\u0001')
|
||||||
|
)
|
||||||
|
.join('\u0002')
|
||||||
|
if (!contactSearchIndexCache || contactSearchIndexCache.signature !== signature) {
|
||||||
|
contactSearchIndexCache = { signature, index: buildContactSearchIndex(contacts) }
|
||||||
|
}
|
||||||
|
return filterContactSearchIndex(contactSearchIndexCache.index, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> {
|
export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> {
|
||||||
@@ -247,6 +281,7 @@ export async function listContactsAsync(filter?: string): Promise<FormattedConta
|
|||||||
hydrateDisplayNames: true,
|
hydrateDisplayNames: true,
|
||||||
hydrateStatuses: true
|
hydrateStatuses: true
|
||||||
})
|
})
|
||||||
|
await dbRef.hydrateContactIdentitiesAsync?.()
|
||||||
return listContacts(filter)
|
return listContacts(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,13 +738,18 @@ export function resolveMd5(query: string): FormattedContact | null {
|
|||||||
(c) =>
|
(c) =>
|
||||||
c.md5 === trimmed ||
|
c.md5 === trimmed ||
|
||||||
c.m_nsUsrName.toLowerCase() === lower ||
|
c.m_nsUsrName.toLowerCase() === lower ||
|
||||||
|
c.wechatId?.toLowerCase() === lower ||
|
||||||
|
c.wxid?.toLowerCase() === lower ||
|
||||||
c.m_nsNickName.toLowerCase() === lower
|
c.m_nsNickName.toLowerCase() === lower
|
||||||
)
|
)
|
||||||
if (exact) return exact
|
if (exact) return exact
|
||||||
|
|
||||||
const partial = contacts.find(
|
const partial = contacts.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.m_nsNickName.toLowerCase().includes(lower) || c.m_nsUsrName.toLowerCase().includes(lower)
|
c.m_nsNickName.toLowerCase().includes(lower) ||
|
||||||
|
c.m_nsUsrName.toLowerCase().includes(lower) ||
|
||||||
|
c.wechatId?.toLowerCase().includes(lower) ||
|
||||||
|
c.wxid?.toLowerCase().includes(lower)
|
||||||
)
|
)
|
||||||
return partial || null
|
return partial || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const aliases = (contact: Contact): Array<{ value: string; primary: boolean }> =
|
|||||||
{ value: contact.m_nsNickName, primary: true },
|
{ value: contact.m_nsNickName, primary: true },
|
||||||
{ value: contact.remark || '', primary: false },
|
{ value: contact.remark || '', primary: false },
|
||||||
{ value: contact.wechatNickname || '', primary: false },
|
{ value: contact.wechatNickname || '', primary: false },
|
||||||
{ value: contact.m_nsUsrName, primary: false },
|
{ value: contact.wechatId || contact.alias || '', primary: false },
|
||||||
|
{ value: contact.wxid || contact.m_nsUsrName, primary: false },
|
||||||
...safeGroupAlias
|
...safeGroupAlias
|
||||||
].filter((item) => Boolean(normalizeContactName(item.value)))
|
].filter((item) => Boolean(normalizeContactName(item.value)))
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ export function resolveContact(
|
|||||||
const contact = matches.get(candidate.conversationId)!.contact
|
const contact = matches.get(candidate.conversationId)!.contact
|
||||||
return {
|
return {
|
||||||
matched: true,
|
matched: true,
|
||||||
personId: contact.m_nsUsrName,
|
personId: contact.wxid || contact.m_nsUsrName,
|
||||||
conversationId: contact.md5,
|
conversationId: contact.md5,
|
||||||
canonicalName: displayName(contact),
|
canonicalName: displayName(contact),
|
||||||
displayName: candidate.displayName,
|
displayName: candidate.displayName,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface Wcdb4Session {
|
|||||||
remark?: string
|
remark?: string
|
||||||
alias?: string
|
alias?: string
|
||||||
wechatId?: string
|
wechatId?: string
|
||||||
|
wxid?: string
|
||||||
|
legacyIdentifier?: string
|
||||||
isFolded?: boolean
|
isFolded?: boolean
|
||||||
isMuted?: boolean
|
isMuted?: boolean
|
||||||
raw: Record<string, unknown>
|
raw: Record<string, unknown>
|
||||||
@@ -112,6 +114,13 @@ type Wcdb4ContactMemberNames = {
|
|||||||
remark: string
|
remark: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Wcdb4ContactIdentity = {
|
||||||
|
username: string
|
||||||
|
wechatId?: string
|
||||||
|
wxid: string
|
||||||
|
legacyIdentifier?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Wcdb4ImageHardlink {
|
export interface Wcdb4ImageHardlink {
|
||||||
file_name?: string
|
file_name?: string
|
||||||
full_path?: string
|
full_path?: string
|
||||||
@@ -385,6 +394,7 @@ export class Wcdb4Client {
|
|||||||
private koffi: KoffiModule | null = null
|
private koffi: KoffiModule | null = null
|
||||||
private handle: number | null = null
|
private handle: number | null = null
|
||||||
private displayNameCache = new Map<string, string>()
|
private displayNameCache = new Map<string, string>()
|
||||||
|
private contactIdentityCache = new Map<string, Wcdb4ContactIdentity>()
|
||||||
private avatarCache = new Map<string, string>()
|
private avatarCache = new Map<string, string>()
|
||||||
private sessionStatusCache = new Map<string, { isFolded: boolean; isMuted: boolean }>()
|
private sessionStatusCache = new Map<string, { isFolded: boolean; isMuted: boolean }>()
|
||||||
private groupNicknameCache = new Map<string, Map<string, string>>()
|
private groupNicknameCache = new Map<string, Map<string, string>>()
|
||||||
@@ -733,6 +743,7 @@ export class Wcdb4Client {
|
|||||||
this.sessionStatusesInFlight = null
|
this.sessionStatusesInFlight = null
|
||||||
this.sessionStatusesUpdatedAt = 0
|
this.sessionStatusesUpdatedAt = 0
|
||||||
this.displayNameCache.clear()
|
this.displayNameCache.clear()
|
||||||
|
this.contactIdentityCache?.clear()
|
||||||
this.avatarCache.clear()
|
this.avatarCache.clear()
|
||||||
this.sessionStatusCache.clear()
|
this.sessionStatusCache.clear()
|
||||||
this.groupNicknameCache.clear()
|
this.groupNicknameCache.clear()
|
||||||
@@ -933,6 +944,32 @@ export class Wcdb4Client {
|
|||||||
return this.cachedSessions
|
return this.cachedSessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContactIdentity(username: string): Wcdb4ContactIdentity | undefined {
|
||||||
|
return this.contactIdentityCache.get(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
async hydrateContactIdentitiesAsync(usernames: string[]): Promise<void> {
|
||||||
|
if (!this.wcdbExecQuery || usernames.length === 0) return
|
||||||
|
const missing = this.uniq(usernames).filter(
|
||||||
|
(username) => !this.contactIdentityCache.has(username)
|
||||||
|
)
|
||||||
|
if (missing.length === 0) return
|
||||||
|
const inList = missing.map((username) => `'${username.replace(/'/g, "''")}'`).join(',')
|
||||||
|
try {
|
||||||
|
const rows = await this.callJsonAsync<Record<string, unknown>[]>(
|
||||||
|
this.wcdbExecQuery as unknown as KoffiAsyncFunction,
|
||||||
|
'contact',
|
||||||
|
'',
|
||||||
|
`SELECT * FROM contact WHERE username IN (${inList})`
|
||||||
|
)
|
||||||
|
for (const identity of this.contactIdentitiesFromRows(rows)) {
|
||||||
|
this.contactIdentityCache.set(identity.username, identity)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Older native builds may not expose the contact table.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSessionsAsync(options: Wcdb4SessionQueryOptions = {}): Promise<Wcdb4Session[]> {
|
async getSessionsAsync(options: Wcdb4SessionQueryOptions = {}): Promise<Wcdb4Session[]> {
|
||||||
const hydrateDisplayNames = options.hydrateDisplayNames !== false
|
const hydrateDisplayNames = options.hydrateDisplayNames !== false
|
||||||
if (this.cachedSessions) {
|
if (this.cachedSessions) {
|
||||||
@@ -1030,6 +1067,7 @@ export class Wcdb4Client {
|
|||||||
this.sessionStatusCache.clear()
|
this.sessionStatusCache.clear()
|
||||||
this.sessionStatusesUpdatedAt = 0
|
this.sessionStatusesUpdatedAt = 0
|
||||||
this.sessionDisplayNamesHydrated = false
|
this.sessionDisplayNamesHydrated = false
|
||||||
|
this.contactIdentityCache?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateAvatarCache(usernames?: string[]): void {
|
invalidateAvatarCache(usernames?: string[]): void {
|
||||||
@@ -2793,6 +2831,7 @@ export class Wcdb4Client {
|
|||||||
'username',
|
'username',
|
||||||
'user_name',
|
'user_name',
|
||||||
'userName',
|
'userName',
|
||||||
|
'm_nsUsrName',
|
||||||
'usrName',
|
'usrName',
|
||||||
'UsrName',
|
'UsrName',
|
||||||
'talker',
|
'talker',
|
||||||
@@ -2807,6 +2846,7 @@ export class Wcdb4Client {
|
|||||||
'displayName',
|
'displayName',
|
||||||
'display_name',
|
'display_name',
|
||||||
'remark',
|
'remark',
|
||||||
|
'm_nsNickName',
|
||||||
'name'
|
'name'
|
||||||
])
|
])
|
||||||
const wechatNickname = this.pickString(row, [
|
const wechatNickname = this.pickString(row, [
|
||||||
@@ -2814,6 +2854,7 @@ export class Wcdb4Client {
|
|||||||
'wechat_nickname',
|
'wechat_nickname',
|
||||||
'nickname',
|
'nickname',
|
||||||
'nickName',
|
'nickName',
|
||||||
|
'm_nsNickName',
|
||||||
'name'
|
'name'
|
||||||
])
|
])
|
||||||
const remark = this.pickString(row, [
|
const remark = this.pickString(row, [
|
||||||
@@ -2827,10 +2868,22 @@ export class Wcdb4Client {
|
|||||||
const wechatId = this.pickString(row, [
|
const wechatId = this.pickString(row, [
|
||||||
'wechatId',
|
'wechatId',
|
||||||
'wechat_id',
|
'wechat_id',
|
||||||
|
'wechatID',
|
||||||
'wxAccount',
|
'wxAccount',
|
||||||
'wx_account',
|
'wx_account',
|
||||||
'account'
|
'account'
|
||||||
])
|
])
|
||||||
|
const wxid = this.pickString(row, ['wxid', 'wx_id', 'internalWxid']) || username
|
||||||
|
const legacyIdentifier = this.pickString(row, [
|
||||||
|
'legacyIdentifier',
|
||||||
|
'legacy_identifier',
|
||||||
|
'originalIdentifier',
|
||||||
|
'original_identifier',
|
||||||
|
'originalUsername',
|
||||||
|
'original_username',
|
||||||
|
'initialIdentifier',
|
||||||
|
'initial_identifier'
|
||||||
|
])
|
||||||
const status = this.sessionStatusCache.get(username)
|
const status = this.sessionStatusCache.get(username)
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
@@ -2839,6 +2892,8 @@ export class Wcdb4Client {
|
|||||||
remark,
|
remark,
|
||||||
alias,
|
alias,
|
||||||
wechatId: wechatId || alias,
|
wechatId: wechatId || alias,
|
||||||
|
wxid,
|
||||||
|
legacyIdentifier: legacyIdentifier || undefined,
|
||||||
isFolded: status?.isFolded,
|
isFolded: status?.isFolded,
|
||||||
isMuted: status?.isMuted,
|
isMuted: status?.isMuted,
|
||||||
raw: row
|
raw: row
|
||||||
@@ -3171,6 +3226,42 @@ export class Wcdb4Client {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private contactIdentitiesFromRows(rows: Record<string, unknown>[]): Wcdb4ContactIdentity[] {
|
||||||
|
if (!Array.isArray(rows)) return []
|
||||||
|
return rows.flatMap((row) => {
|
||||||
|
const username = this.pickString(row, ['username', 'user_name', 'userName', 'm_nsUsrName'])
|
||||||
|
if (!username) return []
|
||||||
|
const alias = this.pickString(row, ['alias', 'aliasName', 'alias_name', 'm_nsAlias'])
|
||||||
|
const wechatId = this.pickString(row, [
|
||||||
|
'wechatId',
|
||||||
|
'wechat_id',
|
||||||
|
'wechatID',
|
||||||
|
'wxAccount',
|
||||||
|
'wx_account',
|
||||||
|
'account'
|
||||||
|
])
|
||||||
|
const wxid = this.pickString(row, ['wxid', 'wx_id', 'internalWxid']) || username
|
||||||
|
const legacyIdentifier = this.pickString(row, [
|
||||||
|
'legacyIdentifier',
|
||||||
|
'legacy_identifier',
|
||||||
|
'originalIdentifier',
|
||||||
|
'original_identifier',
|
||||||
|
'originalUsername',
|
||||||
|
'original_username',
|
||||||
|
'initialIdentifier',
|
||||||
|
'initial_identifier'
|
||||||
|
])
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
wechatId: wechatId || alias || undefined,
|
||||||
|
wxid,
|
||||||
|
legacyIdentifier: legacyIdentifier || undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private readContactAvatarUrls(usernames: string[]): Map<string, string> {
|
private readContactAvatarUrls(usernames: string[]): Map<string, string> {
|
||||||
const result = new Map<string, string>()
|
const result = new Map<string, string>()
|
||||||
if (!this.wcdbExecQuery || usernames.length === 0) return result
|
if (!this.wcdbExecQuery || usernames.length === 0) return result
|
||||||
|
|||||||
+19
-1
@@ -9,6 +9,8 @@ export interface UserContact {
|
|||||||
remark?: string
|
remark?: string
|
||||||
alias?: string
|
alias?: string
|
||||||
wechatId?: string
|
wechatId?: string
|
||||||
|
wxid?: string
|
||||||
|
legacyIdentifier?: string
|
||||||
isFolded?: boolean
|
isFolded?: boolean
|
||||||
isMuted?: boolean
|
isMuted?: boolean
|
||||||
}
|
}
|
||||||
@@ -105,7 +107,15 @@ export class WechatDb {
|
|||||||
wechatNickname: session.wechatNickname,
|
wechatNickname: session.wechatNickname,
|
||||||
remark: session.remark,
|
remark: session.remark,
|
||||||
alias: session.alias,
|
alias: session.alias,
|
||||||
wechatId: session.wechatId,
|
wechatId:
|
||||||
|
session.wechatId || this.wcdb4Client.getContactIdentity(session.username)?.wechatId,
|
||||||
|
wxid:
|
||||||
|
session.wxid ||
|
||||||
|
this.wcdb4Client.getContactIdentity(session.username)?.wxid ||
|
||||||
|
session.username,
|
||||||
|
legacyIdentifier:
|
||||||
|
session.legacyIdentifier ||
|
||||||
|
this.wcdb4Client.getContactIdentity(session.username)?.legacyIdentifier,
|
||||||
isFolded: session.isFolded,
|
isFolded: session.isFolded,
|
||||||
isMuted: session.isMuted
|
isMuted: session.isMuted
|
||||||
}))
|
}))
|
||||||
@@ -117,11 +127,19 @@ export class WechatDb {
|
|||||||
contact.nickname,
|
contact.nickname,
|
||||||
contact.alias,
|
contact.alias,
|
||||||
contact.wechatId,
|
contact.wechatId,
|
||||||
|
contact.wxid,
|
||||||
contact.m_nsUsrName
|
contact.m_nsUsrName
|
||||||
].some((value) => value?.toLocaleLowerCase().includes(keyword))
|
].some((value) => value?.toLocaleLowerCase().includes(keyword))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async hydrateContactIdentitiesAsync(): Promise<void> {
|
||||||
|
const sessions = this.wcdb4Client.getSessions()
|
||||||
|
await this.wcdb4Client.hydrateContactIdentitiesAsync(
|
||||||
|
sessions.map((session) => session.username)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public getAllGroupContacts(): Record<string, string> {
|
public getAllGroupContacts(): Record<string, string> {
|
||||||
const groupContacts: Record<string, string> = {}
|
const groupContacts: Record<string, string> = {}
|
||||||
for (const session of this.wcdb4Client.getSessions()) {
|
for (const session of this.wcdb4Client.getSessions()) {
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ import { AppUpdatePrompt } from './features/app-update/AppUpdatePrompt'
|
|||||||
import { GroupExitMonitorWorkspace } from './features/group-exit-monitor/GroupExitMonitorWorkspace'
|
import { GroupExitMonitorWorkspace } from './features/group-exit-monitor/GroupExitMonitorWorkspace'
|
||||||
import { LogsWorkspace } from './features/logs/LogsWorkspace'
|
import { LogsWorkspace } from './features/logs/LogsWorkspace'
|
||||||
import { selectContactAvatarRefreshUsernames } from './utils/contact-avatar'
|
import { selectContactAvatarRefreshUsernames } from './utils/contact-avatar'
|
||||||
import { filterContacts } from './utils/contact-search'
|
import {
|
||||||
|
buildContactSearchIndex,
|
||||||
|
filterContactSearchIndex,
|
||||||
|
type ContactSearchIndex
|
||||||
|
} from '../../shared/contact-search'
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 260
|
const SIDEBAR_MIN_WIDTH = 260
|
||||||
const SIDEBAR_MAX_WIDTH = 380
|
const SIDEBAR_MAX_WIDTH = 380
|
||||||
@@ -140,6 +144,9 @@ const areMessagesEquivalent = (left: Message[], right: Message[]): boolean => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterContactList = (index: ContactSearchIndex, keyword: string): Contact[] =>
|
||||||
|
filterContactSearchIndex(index, keyword)
|
||||||
|
|
||||||
type GroupSnapshot = {
|
type GroupSnapshot = {
|
||||||
roomId: string
|
roomId: string
|
||||||
memberCount: number
|
memberCount: number
|
||||||
@@ -218,6 +225,7 @@ function App(): React.ReactElement {
|
|||||||
const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false)
|
const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false)
|
||||||
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
|
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
|
||||||
const [contacts, setContacts] = useState<Contact[]>([])
|
const [contacts, setContacts] = useState<Contact[]>([])
|
||||||
|
const contactSearchIndex = React.useMemo(() => buildContactSearchIndex(contacts), [contacts])
|
||||||
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [isMessagesLoading, setIsMessagesLoading] = useState(false)
|
const [isMessagesLoading, setIsMessagesLoading] = useState(false)
|
||||||
@@ -591,7 +599,9 @@ function App(): React.ReactElement {
|
|||||||
options?.onProgress?.('正在加载联系人...', 35)
|
options?.onProgress?.('正在加载联系人...', 35)
|
||||||
const list = await window.api.getContacts()
|
const list = await window.api.getContacts()
|
||||||
setContacts(list)
|
setContacts(list)
|
||||||
setFilteredContacts(filterContacts(list, options?.filterKeyword || ''))
|
setFilteredContacts(
|
||||||
|
filterContactSearchIndex(buildContactSearchIndex(list), options?.filterKeyword || '')
|
||||||
|
)
|
||||||
const runId = ++contactAvatarHydrationRunRef.current
|
const runId = ++contactAvatarHydrationRunRef.current
|
||||||
const hydrate = (): Promise<void> => hydrateContactAvatars(list, runId, options?.onProgress)
|
const hydrate = (): Promise<void> => hydrateContactAvatars(list, runId, options?.onProgress)
|
||||||
if (options?.waitForAvatars) {
|
if (options?.waitForAvatars) {
|
||||||
@@ -1436,7 +1446,7 @@ function App(): React.ReactElement {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const handleSearchContacts = (keyword: string): void => {
|
const handleSearchContacts = (keyword: string): void => {
|
||||||
setFilteredContacts(filterContacts(contacts, keyword))
|
setFilteredContacts(filterContactList(contactSearchIndex, keyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefreshContacts = async (filterKeyword: string): Promise<void> => {
|
const handleRefreshContacts = async (filterKeyword: string): Promise<void> => {
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export function ConversationItem({
|
|||||||
onSelect
|
onSelect
|
||||||
}: ConversationItemProps): React.ReactElement {
|
}: ConversationItemProps): React.ReactElement {
|
||||||
const nickname = contact.m_nsNickName?.trim()
|
const nickname = contact.m_nsNickName?.trim()
|
||||||
const wxid = contact.m_nsUsrName
|
const avatarUsername = contact.m_nsUsrName
|
||||||
const displayName = nickname || wxid || '未命名会话'
|
const internalWxid = contact.wxid || avatarUsername
|
||||||
const initial = (displayName || wxid || '?').charAt(0)
|
const displayName = nickname || internalWxid || '未命名会话'
|
||||||
|
const initial = (displayName || internalWxid || '?').charAt(0)
|
||||||
const [repairedAvatar, setRepairedAvatar] = useState<{
|
const [repairedAvatar, setRepairedAvatar] = useState<{
|
||||||
username: string
|
username: string
|
||||||
source: string
|
source: string
|
||||||
@@ -23,22 +24,22 @@ export function ConversationItem({
|
|||||||
}>()
|
}>()
|
||||||
const [failedAvatar, setFailedAvatar] = useState<{ username: string; source: string }>()
|
const [failedAvatar, setFailedAvatar] = useState<{ username: string; source: string }>()
|
||||||
const repairedSource =
|
const repairedSource =
|
||||||
repairedAvatar?.username === wxid && repairedAvatar.replaces === contact.avatar
|
repairedAvatar?.username === avatarUsername && repairedAvatar.replaces === contact.avatar
|
||||||
? repairedAvatar.source
|
? repairedAvatar.source
|
||||||
: undefined
|
: undefined
|
||||||
const avatar = repairedSource || contact.avatar
|
const avatar = repairedSource || contact.avatar
|
||||||
const avatarFailed = failedAvatar?.username === wxid && failedAvatar.source === avatar
|
const avatarFailed = failedAvatar?.username === avatarUsername && failedAvatar.source === avatar
|
||||||
|
|
||||||
const handleAvatarError = (): void => {
|
const handleAvatarError = (): void => {
|
||||||
if (!avatar || avatarFailed) return
|
if (!avatar || avatarFailed) return
|
||||||
setFailedAvatar({ username: wxid, source: avatar })
|
setFailedAvatar({ username: avatarUsername, source: avatar })
|
||||||
if (avatar.startsWith('data:')) return
|
if (avatar.startsWith('data:')) return
|
||||||
void window.api
|
void window.api
|
||||||
.getContactAvatars([wxid], { refresh: true })
|
.getContactAvatars([avatarUsername], { refresh: true })
|
||||||
.then((avatars) => {
|
.then((avatars) => {
|
||||||
const fallback = avatars[wxid]
|
const fallback = avatars[avatarUsername]
|
||||||
if (!fallback || fallback === avatar) return
|
if (!fallback || fallback === avatar) return
|
||||||
setRepairedAvatar({ username: wxid, source: fallback, replaces: avatar })
|
setRepairedAvatar({ username: avatarUsername, source: fallback, replaces: avatar })
|
||||||
setFailedAvatar(undefined)
|
setFailedAvatar(undefined)
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
@@ -49,7 +50,11 @@ export function ConversationItem({
|
|||||||
type="button"
|
type="button"
|
||||||
className={`conversation-item ${active ? 'active' : ''}`}
|
className={`conversation-item ${active ? 'active' : ''}`}
|
||||||
onClick={() => onSelect(contact)}
|
onClick={() => onSelect(contact)}
|
||||||
title={wxid && wxid !== displayName ? wxid : displayName}
|
title={
|
||||||
|
contact.wechatId ||
|
||||||
|
contact.alias ||
|
||||||
|
(internalWxid !== displayName ? internalWxid : displayName)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="conversation-item-active-mark" aria-hidden />
|
<span className="conversation-item-active-mark" aria-hidden />
|
||||||
<span className="conversation-item-avatar">
|
<span className="conversation-item-avatar">
|
||||||
@@ -68,6 +73,9 @@ export function ConversationItem({
|
|||||||
</span>
|
</span>
|
||||||
<span className="conversation-item-body">
|
<span className="conversation-item-body">
|
||||||
<span className="conversation-item-name">{displayName}</span>
|
<span className="conversation-item-name">{displayName}</span>
|
||||||
|
{contact.wechatId && (
|
||||||
|
<span className="conversation-item-meta">微信号: {contact.wechatId}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ export function ConversationSidebar({
|
|||||||
const handleSearchChange = (term: string): void => {
|
const handleSearchChange = (term: string): void => {
|
||||||
setSearchTerm(term)
|
setSearchTerm(term)
|
||||||
onSearch(term)
|
onSearch(term)
|
||||||
|
if (term.trim()) {
|
||||||
|
setExpandedSections((current) => ({
|
||||||
|
...current,
|
||||||
|
groups: true,
|
||||||
|
folded: true,
|
||||||
|
officialAccounts: true,
|
||||||
|
contacts: true
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = async (): Promise<void> => {
|
const handleRefresh = async (): Promise<void> => {
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ function ExportContactRow({
|
|||||||
{name}
|
{name}
|
||||||
</strong>
|
</strong>
|
||||||
<small className="text-[11px] text-muted-foreground">
|
<small className="text-[11px] text-muted-foreground">
|
||||||
{contact.type === 'group' ? '群聊' : '联系人'}
|
{contact.type === 'group'
|
||||||
|
? '群聊'
|
||||||
|
: contact.wechatId
|
||||||
|
? `联系人 · 微信号: ${contact.wechatId}`
|
||||||
|
: '联系人'}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
{showSelection && (
|
{showSelection && (
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import type {
|
|||||||
import { displayName } from './exportUtils'
|
import { displayName } from './exportUtils'
|
||||||
import type { VoiceModelStatus } from '../../../../shared/voice-recognition'
|
import type { VoiceModelStatus } from '../../../../shared/voice-recognition'
|
||||||
import { resolveMemberName } from '../../../../shared/member-names'
|
import { resolveMemberName } from '../../../../shared/member-names'
|
||||||
|
import {
|
||||||
|
buildContactSearchIndex,
|
||||||
|
filterContactSearchIndex
|
||||||
|
} from '../../../../shared/contact-search'
|
||||||
|
|
||||||
const ALL_CONTACT_TYPES: ExportContactType[] = ['group', 'user']
|
const ALL_CONTACT_TYPES: ExportContactType[] = ['group', 'user']
|
||||||
const contactTypeKey = (types: ExportContactType[] | undefined): string =>
|
const contactTypeKey = (types: ExportContactType[] | undefined): string =>
|
||||||
@@ -57,6 +61,7 @@ export function ExportWorkspace({
|
|||||||
const [selectedContacts, setSelectedContacts] = useState<Contact[]>(() =>
|
const [selectedContacts, setSelectedContacts] = useState<Contact[]>(() =>
|
||||||
initialSelection ? [initialSelection] : []
|
initialSelection ? [initialSelection] : []
|
||||||
)
|
)
|
||||||
|
const contactSearchIndex = useMemo(() => buildContactSearchIndex(contacts), [contacts])
|
||||||
const [activeContactId, setActiveContactId] = useState(initialSelection?.md5 || '')
|
const [activeContactId, setActiveContactId] = useState(initialSelection?.md5 || '')
|
||||||
const [previewByContact, setPreviewByContact] = useState<Record<string, Message[]>>({})
|
const [previewByContact, setPreviewByContact] = useState<Record<string, Message[]>>({})
|
||||||
const [range, setRange] = useState<ExportRange>(() => (runningAllTask ? 'all' : 'today'))
|
const [range, setRange] = useState<ExportRange>(() => (runningAllTask ? 'all' : 'today'))
|
||||||
@@ -109,15 +114,8 @@ export function ExportWorkspace({
|
|||||||
}, [exportAll, loadPreviewMessages, previewByContact, selectedContacts])
|
}, [exportAll, loadPreviewMessages, previewByContact, selectedContacts])
|
||||||
|
|
||||||
const filteredContacts = useMemo(() => {
|
const filteredContacts = useMemo(() => {
|
||||||
const keyword = contactFilter.trim().toLowerCase()
|
return filterContactSearchIndex(contactSearchIndex, contactFilter, contactType)
|
||||||
return contacts.filter((contact) => {
|
}, [contactFilter, contactSearchIndex, contactType])
|
||||||
if (contactType !== 'all' && contact.type !== contactType) return false
|
|
||||||
if (!keyword) return true
|
|
||||||
return [contact.m_nsNickName, contact.m_nsUsrName].some((value) =>
|
|
||||||
value.toLowerCase().includes(keyword)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [contactFilter, contactType, contacts])
|
|
||||||
|
|
||||||
const activeContact =
|
const activeContact =
|
||||||
selectedContacts.find((contact) => contact.md5 === activeContactId) ||
|
selectedContacts.find((contact) => contact.md5 === activeContactId) ||
|
||||||
|
|||||||
@@ -287,6 +287,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-item-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--wxex-text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-sidebar-account {
|
.conversation-sidebar-account {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import type { Contact } from '../../../shared/types'
|
import type { Contact } from '../../../shared/types'
|
||||||
|
import { buildContactSearchIndex, filterContactSearchIndex } from '../../../shared/contact-search'
|
||||||
|
|
||||||
export function filterContacts(list: Contact[], keyword: string): Contact[] {
|
export function filterContacts(list: Contact[], keyword: string): Contact[] {
|
||||||
const query = keyword.trim().toLocaleLowerCase()
|
if (!keyword.trim()) return list
|
||||||
if (!query) return list
|
return filterContactSearchIndex(buildContactSearchIndex(list), keyword)
|
||||||
|
|
||||||
return list.filter((contact) =>
|
|
||||||
[
|
|
||||||
contact.remark,
|
|
||||||
contact.wechatNickname,
|
|
||||||
contact.m_nsNickName,
|
|
||||||
contact.alias,
|
|
||||||
contact.wechatId,
|
|
||||||
contact.m_nsUsrName
|
|
||||||
].some((value) => value?.trim().toLocaleLowerCase().includes(query))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { pinyin } from 'pinyin-pro'
|
||||||
|
import type { Contact } from './types'
|
||||||
|
|
||||||
|
export interface ContactSearchText {
|
||||||
|
raw: string
|
||||||
|
normalized: string
|
||||||
|
pinyin: string
|
||||||
|
initials: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactSearchIndexEntry {
|
||||||
|
contact: Contact
|
||||||
|
fields: ContactSearchText[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ContactSearchIndex = ContactSearchIndexEntry[]
|
||||||
|
|
||||||
|
export function normalizeContactSearchIdentity(value: unknown): string {
|
||||||
|
return String(value ?? '')
|
||||||
|
.normalize('NFKC')
|
||||||
|
.trim()
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPinyin(value: string): { full: string; initials: string } {
|
||||||
|
if (!value) return { full: '', initials: '' }
|
||||||
|
try {
|
||||||
|
const syllables = pinyin(value, { toneType: 'none', type: 'array' })
|
||||||
|
.map((item) => normalizeContactSearchIdentity(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
return {
|
||||||
|
full: syllables.join(''),
|
||||||
|
initials: syllables.map((item) => item.charAt(0)).join('')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { full: '', initials: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchableText(value: unknown, withPinyin: boolean): ContactSearchText | null {
|
||||||
|
const raw = String(value ?? '').trim()
|
||||||
|
const normalized = normalizeContactSearchIdentity(raw)
|
||||||
|
if (!normalized) return null
|
||||||
|
const phonetic = withPinyin ? buildPinyin(raw) : { full: '', initials: '' }
|
||||||
|
return { raw, normalized, pinyin: phonetic.full, initials: phonetic.initials }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildContactSearchIndex(contacts: Contact[]): ContactSearchIndex {
|
||||||
|
return contacts.map((contact) => ({
|
||||||
|
contact,
|
||||||
|
fields: [
|
||||||
|
// The WCDB session shape can expose a contact remark as m_nsNickName
|
||||||
|
// when the dedicated remark column is unavailable. Index it phonetically
|
||||||
|
// so both normalized data shapes support the same search behavior.
|
||||||
|
searchableText(contact.m_nsNickName, true),
|
||||||
|
searchableText(contact.remark, true),
|
||||||
|
searchableText(contact.wechatNickname, true),
|
||||||
|
searchableText(contact.alias, false),
|
||||||
|
searchableText(contact.wechatId, false),
|
||||||
|
searchableText(
|
||||||
|
contact.legacyIdentifier === (contact.wxid || contact.m_nsUsrName)
|
||||||
|
? undefined
|
||||||
|
: contact.wxid || contact.m_nsUsrName,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
].filter((field): field is ContactSearchText => Boolean(field))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesContactSearchText(field: ContactSearchText, query: string): boolean {
|
||||||
|
const normalizedQuery = normalizeContactSearchIdentity(query)
|
||||||
|
if (!normalizedQuery) return true
|
||||||
|
return [field.normalized, field.pinyin, field.initials].some((value) =>
|
||||||
|
value.includes(normalizedQuery)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterContactSearchIndex(
|
||||||
|
index: ContactSearchIndex,
|
||||||
|
query: string,
|
||||||
|
type: Contact['type'] | 'all' = 'all'
|
||||||
|
): Contact[] {
|
||||||
|
return index
|
||||||
|
.filter(({ contact, fields }) => {
|
||||||
|
if (type !== 'all' && contact.type !== type) return false
|
||||||
|
return fields.some((field) => matchesContactSearchText(field, query))
|
||||||
|
})
|
||||||
|
.map(({ contact }) => contact)
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ export interface Contact {
|
|||||||
remark?: string
|
remark?: string
|
||||||
alias?: string
|
alias?: string
|
||||||
wechatId?: string
|
wechatId?: string
|
||||||
|
wxid?: string
|
||||||
|
legacyIdentifier?: string
|
||||||
isFolded?: boolean
|
isFolded?: boolean
|
||||||
isMuted?: boolean
|
isMuted?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,4 +57,35 @@ describe('ConversationSidebar', () => {
|
|||||||
await userEvent.click(screen.getByRole('button', { name: '公众号 (1)' }))
|
await userEvent.click(screen.getByRole('button', { name: '公众号 (1)' }))
|
||||||
expect(screen.getByText('测试公众号')).toBeInTheDocument()
|
expect(screen.getByText('测试公众号')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reveals contact results when searching a collapsed contact section', async () => {
|
||||||
|
render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<ConversationSidebar
|
||||||
|
contacts={[
|
||||||
|
{
|
||||||
|
m_nsUsrName: 'wxid_fixture',
|
||||||
|
m_nsNickName: '微信昵称',
|
||||||
|
md5: 'contact-md5',
|
||||||
|
type: 'user',
|
||||||
|
remark: '小号'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedContact={null}
|
||||||
|
onSelectContact={vi.fn()}
|
||||||
|
onSearch={vi.fn()}
|
||||||
|
onContentFilter={vi.fn()}
|
||||||
|
width={320}
|
||||||
|
selfInfo={null}
|
||||||
|
dbReady
|
||||||
|
onOpenSettings={vi.fn()}
|
||||||
|
onRefresh={vi.fn(async () => undefined)}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('微信昵称')).not.toBeInTheDocument()
|
||||||
|
await userEvent.type(screen.getByRole('searchbox', { name: '搜索会话' }), 'xiaohao')
|
||||||
|
expect(screen.getByText('微信昵称')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const contact: Contact = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('filterContacts', () => {
|
describe('filterContacts', () => {
|
||||||
it.each(['Shinven', '小号', '工作', 'abc123', 'WECHAT_ABC', 'WXID_xxy']) (
|
it.each(['Shinven', '小号', '工作', 'gongzuoshouji', 'gzsj', 'abc123', 'WECHAT_ABC', 'WXID_xxy'])(
|
||||||
'matches all contact identity fields with %s',
|
'matches all contact identity fields with %s',
|
||||||
(query) => {
|
(query) => {
|
||||||
expect(filterContacts([contact], ` ${query} `)).toEqual([contact])
|
expect(filterContacts([contact], ` ${query} `)).toEqual([contact])
|
||||||
@@ -26,4 +26,18 @@ describe('filterContacts', () => {
|
|||||||
expect(filterContacts(list, ' ')).toBe(list)
|
expect(filterContacts(list, ' ')).toBe(list)
|
||||||
expect(filterContacts(list, 'not-found')).toEqual([])
|
expect(filterContacts(list, 'not-found')).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('matches pinyin when the Chinese name is stored in m_nsNickName', () => {
|
||||||
|
const list = [{ ...contact, remark: '', wechatNickname: '' }]
|
||||||
|
|
||||||
|
expect(filterContacts(list, 'xiaohao')).toEqual(list)
|
||||||
|
expect(filterContacts(list, 'xh')).toEqual(list)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches pinyin for a dedicated Chinese remark', () => {
|
||||||
|
const list = [{ ...contact, m_nsNickName: 'Shinven', remark: '小号' }]
|
||||||
|
|
||||||
|
expect(filterContacts(list, 'xiaohao')).toEqual(list)
|
||||||
|
expect(filterContacts(list, 'xh')).toEqual(list)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user