Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Baileys engine: the Chats list now shows saved/contact names instead of a raw number or `@lid`.** When
Baileys supplied a chat without a title, the dashboard Chats list fell back to the raw JID user-part (a
bare number, or a privacy-id for `@lid` contacts). The session store now resolves a best-known display
name from the synced contacts — preferring the saved name, then the business `verifiedName`, then the
pushName (`notify`) — and for a `@lid` chat it also looks up the contact behind the resolved phone. The
raw user-part remains the last resort, so a name is shown whenever WhatsApp has delivered one. No API
shape change (`ChatSummary.name` is simply better populated). (#369)

- **Baileys engine: inbound message ids are now engine-neutral (`@c.us`).** The Baileys adapter emitted
its native `<phone>@s.whatsapp.net` / `<lid>@lid` ids in message payloads (`from` / `to` / `chatId` /
`author`, plus revoked and reaction events), while the whatsapp-web.js engine and the rest of the
Expand Down
32 changes: 32 additions & 0 deletions src/engine/adapters/baileys-session-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,38 @@ describe('BaileysSessionStore', () => {
expect(store.resolvePhone('111:7@lid')).toBe('628999');
});

describe('toNeutralChat contact-name resolution (#369)', () => {
it('keeps the chat title when Baileys supplies one (it wins over the contact)', () => {
store.upsertChats([{ id: '628111@s.whatsapp.net', name: 'Chat Title' }]);
store.upsertContacts([{ id: '628111@s.whatsapp.net', name: 'Saved Name' }]);
expect(store.listChats()[0].name).toBe('Chat Title');
});

it('falls back to the saved contact name for a titleless bare-number chat', () => {
store.upsertChats([{ id: '628111@s.whatsapp.net' }]); // no chat title
store.upsertContacts([{ id: '628111@s.whatsapp.net', name: 'Alice' }]);
expect(store.listChats()[0].name).toBe('Alice');
});

it('resolves a saved name for a @lid chat via the lid->pn mapping', () => {
store.upsertChats([{ id: '111@lid' }]); // titleless, lid-keyed
store.addLidMappings([{ lid: '111@lid', pn: '628999@s.whatsapp.net' }]);
store.upsertContacts([{ id: '628999@s.whatsapp.net', name: 'Carol' }]);
expect(store.listChats()[0].name).toBe('Carol');
});

it('uses pushName (notify) when no saved/verified name exists', () => {
store.upsertChats([{ id: '628222@s.whatsapp.net' }]);
store.upsertContacts([{ id: '628222@s.whatsapp.net', notify: 'Dave' }]);
expect(store.listChats()[0].name).toBe('Dave');
});

it('falls back to the raw user-part when nothing is known (last resort)', () => {
store.upsertChats([{ id: '628333@s.whatsapp.net' }]);
expect(store.listChats()[0].name).toBe('628333');
});
});

describe('toNeutralJid', () => {
it('maps @s.whatsapp.net to @c.us and strips the device suffix', () => {
expect(store.toNeutralJid('628111@s.whatsapp.net')).toBe('628111@c.us');
Expand Down
37 changes: 36 additions & 1 deletion src/engine/adapters/baileys-session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,49 @@ export class BaileysSessionStore {
const last = this.lastMessages.get(c.id);
return {
id: c.id,
name: c.name ?? userPart(c.id),
name: c.name ?? this.resolveContactName(c.id),
isGroup: c.id.endsWith('@g.us'),
unreadCount: c.unreadCount ?? 0,
timestamp: last?.timestamp ?? this.toUnixSeconds(c.conversationTimestamp),
lastMessage: last?.text,
};
}

/**
* Best-known display name for a chat id when Baileys gave the chat no title (#369). Prefers the saved
* contact name, then verifiedName, then pushName (`notify`); for a @lid chat it also tries the contact
* behind the resolved phone. Falls back to the raw user-part so a number/lid is never shown as a JID.
*/
private resolveContactName(id: string): string {
const direct = this.contactDisplayName(id);
if (direct) {
return direct;
}
const parsed = parseWaId(id);
if (parsed.kind === 'lid') {
const lidJid = `${parsed.userPart}@lid`;
const pn =
this.lidToPn.get(lidJid) ??
this.lidToPn.get(id) ??
(this.contacts.get(lidJid) ?? this.contacts.get(id))?.phoneNumber;
if (pn) {
const viaPhone =
this.contactDisplayName(pn) ??
this.contactDisplayName(`${userPart(pn)}@s.whatsapp.net`) ??
this.contactDisplayName(`${userPart(pn)}@c.us`);
if (viaPhone) {
return viaPhone;
}
}
}
return userPart(id);
}

private contactDisplayName(id: string): string | undefined {
const c = this.contacts.get(id);
return c ? (c.name ?? c.verifiedName ?? c.notify ?? undefined) : undefined;
}

private toUnixSeconds(ts: number | { toNumber(): number } | null | undefined): number {
if (ts == null) {
return 0;
Expand Down
Loading