From fd7450e5b933c1121372f000a663da7e7aac79ca Mon Sep 17 00:00:00 2001 From: Mahmoud Ibrahim Date: Sat, 10 Jan 2026 01:26:21 +0200 Subject: [PATCH 1/5] WhatsApp: include numbers in contact cards --- src/web/inbound.test.ts | 64 +++++++++++++++++++++++++++++++++++++---- src/web/inbound.ts | 43 +++++++++++++++++++-------- src/web/vcard.ts | 12 +++++++- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index 68940cc4e..5850114da 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -60,18 +60,72 @@ describe("web inbound helpers", () => { expect(body).toBe(""); }); + it("normalizes tel: prefixes in WhatsApp vcards", () => { + const body = extractText({ + contactMessage: { + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Ada Lovelace", + "TEL;TYPE=CELL:tel:+15555550123", + "END:VCARD", + ].join("\n"), + }, + } as unknown as import("@whiskeysockets/baileys").proto.IMessage); + expect(body).toBe(""); + }); + it("extracts multiple WhatsApp contact cards", () => { const body = extractText({ contactsArrayMessage: { contacts: [ - { displayName: "Alice" }, - { displayName: "Bob" }, - { displayName: "Charlie" }, - { displayName: "Dana" }, + { + displayName: "Alice", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Alice", + "TEL;TYPE=CELL:+15555550101", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Bob", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Bob", + "TEL;TYPE=CELL:+15555550102", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Charlie", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Charlie", + "TEL;TYPE=CELL:+15555550103", + "TEL;TYPE=HOME:+15555550104", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Dana", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Dana", + "TEL;TYPE=CELL:+15555550105", + "END:VCARD", + ].join("\n"), + }, ], }, } as unknown as import("@whiskeysockets/baileys").proto.IMessage); - expect(body).toBe(""); + expect(body).toBe( + "", + ); }); it("summarizes empty WhatsApp contact cards with a count", () => { diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 9c8a22447..9a1368367 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -762,11 +762,11 @@ function extractContactPlaceholder( if (!message) return undefined; const contact = message.contactMessage ?? undefined; if (contact) { - const { name, phone } = describeContact({ + const { name, phones } = describeContact({ displayName: contact.displayName, vcard: contact.vcard, }); - return formatContactPlaceholder(name, phone); + return formatContactPlaceholder(name, phones); } const contactsArray = message.contactsArrayMessage?.contacts ?? undefined; if (!contactsArray || contactsArray.length === 0) return undefined; @@ -774,7 +774,7 @@ function extractContactPlaceholder( .map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard }), ) - .map((entry) => entry.name ?? entry.phone) + .map((entry) => formatContactLabel(entry.name, entry.phones)) .filter((value): value is string => Boolean(value)); return formatContactsPlaceholder(labels, contactsArray.length); } @@ -782,20 +782,17 @@ function extractContactPlaceholder( function describeContact(input: { displayName?: string | null; vcard?: string | null; -}): { name?: string; phone?: string } { +}): { name?: string; phones: string[] } { const displayName = (input.displayName ?? "").trim(); const parsed = parseVcard(input.vcard ?? undefined); const name = displayName || parsed.name; - const phone = parsed.phones[0]; - return { name, phone }; + return { name, phones: parsed.phones }; } -function formatContactPlaceholder(name?: string, phone?: string): string { - const parts = [name, phone].filter((value): value is string => - Boolean(value), - ); - if (parts.length === 0) return ""; - return ``; +function formatContactPlaceholder(name?: string, phones?: string[]): string { + const label = formatContactLabel(name, phones); + if (!label) return ""; + return ``; } function formatContactsPlaceholder(labels: string[], total: number): string { @@ -810,6 +807,28 @@ function formatContactsPlaceholder(labels: string[], total: number): string { return ``; } +function formatContactLabel( + name?: string, + phones?: string[], +): string | undefined { + const phoneLabel = formatPhoneList(phones); + const parts = [name, phoneLabel].filter((value): value is string => + Boolean(value), + ); + if (parts.length === 0) return undefined; + return parts.join(", "); +} + +function formatPhoneList(phones?: string[]): string | undefined { + const cleaned = + phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; + if (cleaned.length === 0) return undefined; + const [primary, ...rest] = cleaned; + if (!primary) return undefined; + if (rest.length === 0) return primary; + return `${primary} (+${rest.length} more)`; +} + export function extractLocationData( rawMessage: proto.IMessage | undefined, ): NormalizedLocation | null { diff --git a/src/web/vcard.ts b/src/web/vcard.ts index 4d1b48ce2..fd1da00cb 100644 --- a/src/web/vcard.ts +++ b/src/web/vcard.ts @@ -32,7 +32,8 @@ export function parseVcard(vcard?: string): ParsedVcard { continue; } if (baseKey === "TEL") { - phones.push(value); + const phone = normalizeVcardPhone(value); + if (phone) phones.push(phone); } } return { name: nameFromFn ?? nameFromN, phones }; @@ -56,3 +57,12 @@ function cleanVcardValue(value: string): string { function normalizeVcardName(value: string): string { return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); } + +function normalizeVcardPhone(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return ""; + if (trimmed.toLowerCase().startsWith("tel:")) { + return trimmed.slice(4).trim(); + } + return trimmed; +} From 77c7387bbd9ae1bf83cbfead38362c00074bf924 Mon Sep 17 00:00:00 2001 From: Mahmoud Ibrahim Date: Sat, 10 Jan 2026 01:27:06 +0200 Subject: [PATCH 2/5] Changelog: note multi-contact numbers --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d6ee2aa..1b61605e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete +- WhatsApp: include phone numbers when multiple contacts are shared. (#625) — thanks @mahmoudashraf93 - Pairing: cap pending DM pairing requests at 3 per provider and avoid pairing replies for outbound DMs. — thanks @steipete - macOS: replace relay smoke test with version check in packaging script. (#615) — thanks @YuriNachos - macOS: avoid clearing Launch at Login during app initialization. (#607) — thanks @wes-davis From 18338bc60fc7af7665218eb4b3e101aca98d0c6e Mon Sep 17 00:00:00 2001 From: Mahmoud Ibrahim Date: Sat, 10 Jan 2026 01:31:22 +0200 Subject: [PATCH 3/5] Style: format contact label helper --- src/web/inbound.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 9a1368367..8fc094515 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -820,8 +820,7 @@ function formatContactLabel( } function formatPhoneList(phones?: string[]): string | undefined { - const cleaned = - phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; + const cleaned = phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; if (cleaned.length === 0) return undefined; const [primary, ...rest] = cleaned; if (!primary) return undefined; From d1e10af1e1071663e45ce7b6e7480171ac4b2939 Mon Sep 17 00:00:00 2001 From: Mahmoud Ibrahim Date: Sat, 10 Jan 2026 01:45:24 +0200 Subject: [PATCH 4/5] WhatsApp: show all contacts in shares --- src/web/inbound.test.ts | 2 +- src/web/inbound.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index 5850114da..de9fd4a13 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -124,7 +124,7 @@ describe("web inbound helpers", () => { }, } as unknown as import("@whiskeysockets/baileys").proto.IMessage); expect(body).toBe( - "", + "", ); }); diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 8fc094515..38f43b3fe 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -801,10 +801,7 @@ function formatContactsPlaceholder(labels: string[], total: number): string { const suffix = total === 1 ? "contact" : "contacts"; return ``; } - const shown = cleaned.slice(0, 3); - const remaining = Math.max(total - shown.length, 0); - const suffix = remaining > 0 ? ` +${remaining} more` : ""; - return ``; + return ``; } function formatContactLabel( From 103dd3af64badb637547b3c4cfb852dfd7839b5b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 01:02:28 +0100 Subject: [PATCH 5/5] fix: keep contact summary counts (#625) (thanks @mahmoudashraf93) --- src/web/inbound.test.ts | 24 +++++++++++++++++++++++- src/web/inbound.ts | 5 ++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index de9fd4a13..1a31303cd 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -124,10 +124,32 @@ describe("web inbound helpers", () => { }, } as unknown as import("@whiskeysockets/baileys").proto.IMessage); expect(body).toBe( - "", + "", ); }); + it("counts empty WhatsApp contact cards in array summaries", () => { + const body = extractText({ + contactsArrayMessage: { + contacts: [ + { + displayName: "Alice", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Alice", + "TEL;TYPE=CELL:+15555550101", + "END:VCARD", + ].join("\n"), + }, + {}, + {}, + ], + }, + } as unknown as import("@whiskeysockets/baileys").proto.IMessage); + expect(body).toBe(""); + }); + it("summarizes empty WhatsApp contact cards with a count", () => { const body = extractText({ contactsArrayMessage: { diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 38f43b3fe..8fc094515 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -801,7 +801,10 @@ function formatContactsPlaceholder(labels: string[], total: number): string { const suffix = total === 1 ? "contact" : "contacts"; return ``; } - return ``; + const shown = cleaned.slice(0, 3); + const remaining = Math.max(total - shown.length, 0); + const suffix = remaining > 0 ? ` +${remaining} more` : ""; + return ``; } function formatContactLabel(