From 358ee7a105253e70a9eb048a67e23e9469b5b32c Mon Sep 17 00:00:00 2001 From: kei Date: Thu, 11 Jun 2026 09:39:31 +0200 Subject: [PATCH 1/2] test(contacts): cover custom contact field types --- src/app/contacts-app/contact.spec.ts | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/app/contacts-app/contact.spec.ts b/src/app/contacts-app/contact.spec.ts index e7190d6bb..3ff385495 100644 --- a/src/app/contacts-app/contact.spec.ts +++ b/src/app/contacts-app/contact.spec.ts @@ -253,6 +253,41 @@ END:VCARD`); expect(sut.addresses.length).toBe(2); }); + it('can display custom x-prefixed field types from Android vcards', () => { + const sut = Contact.fromVcard(null, `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.2.0//EN +UID:f205d9d6-f5cb-4625-be9f-54557a6c3bf0 +FN:Name Surname +N:Surname;Name;;; +GROUP1.TEL;TYPE=x-starý,PREF:+1234567890 +TEL;TYPE=cell:+0987654321 +GROUP1.X-ABLABEL:Starý +GROUP2.X-ABLABEL:Obsolete +EMAIL;TYPE=PREF:name@domain.com +GROUP2.EMAIL;TYPE=x-obsolete:name@anotherdomain.com +END:VCARD`); + + expect(sut.phones[0].types).toEqual(['starý', 'pref']); + expect(sut.phones[1].types).toEqual(['cell']); + expect(sut.emails[0].types).toEqual(['pref']); + expect(sut.emails[1].types).toEqual(['obsolete']); + }); + + it('can save custom field types with x-prefixes in vcards', () => { + const sut = new Contact({}); + + sut.phones = [ + { types: ['cell'], value: '+0987654321' }, + { types: ['starý'], value: '+1234567890' }, + ]; + + const vcard = sut.vcard(); + + expect(vcard).toContain('TEL;TYPE=cell:+0987654321'); + expect(vcard).toContain('TEL;TYPE=x-starý:+1234567890'); + }); + it('contact with exceptionally empty name shows up as email', () => { /* eslint-disable no-trailing-spaces */ const sut = Contact.fromVcard(null, `BEGIN:VCARD From 863d1afec02cbf1c6e99226aa3dd46c7e3b24954 Mon Sep 17 00:00:00 2001 From: kei Date: Thu, 11 Jun 2026 09:39:31 +0200 Subject: [PATCH 2/2] fix(contacts): normalize custom field type labels --- src/app/contacts-app/contact.ts | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/app/contacts-app/contact.ts b/src/app/contacts-app/contact.ts index 6a730bb2c..16969ed59 100644 --- a/src/app/contacts-app/contact.ts +++ b/src/app/contacts-app/contact.ts @@ -140,6 +140,52 @@ export class Contact { component: ICAL.Component; url: string; + private static readonly standardPropertyTypes = new Set([ + 'acquaintance', + 'agent', + 'bbs', + 'car', + 'cell', + 'child', + 'co-resident', + 'co-worker', + 'colleague', + 'contact', + 'crush', + 'date', + 'dom', + 'emergency', + 'fax', + 'friend', + 'home', + 'internet', + 'intl', + 'isdn', + 'kin', + 'me', + 'met', + 'modem', + 'msg', + 'muse', + 'neighbor', + 'pager', + 'parcel', + 'parent', + 'pcs', + 'personal', + 'postal', + 'pref', + 'sibling', + 'spouse', + 'sweetheart', + 'text', + 'textphone', + 'video', + 'voice', + 'work', + 'x400', + ]); + private static preprocessVcf(vcf: string): string { // since ical.js can't parse these // (not until https://github.com/mozilla-comm/ical.js/pull/442/files is merged anyway) @@ -150,6 +196,18 @@ export class Contact { return vcf.replace(group, ''); } + private static displayPropertyType(type: string): string { + return type.toLowerCase().replace(/^x-/, ''); + } + + private static serializePropertyType(type: string): string { + const lowerType = type.toLowerCase(); + if (lowerType.startsWith('x-') || Contact.standardPropertyTypes.has(lowerType)) { + return lowerType; + } + return 'x-' + lowerType; + } + // may throw ICAL.ParserError if input is not a valid vcf static fromVcf(vcf: string): Contact[] { let cards = ICAL.parse(this.preprocessVcf(vcf)); @@ -233,7 +291,7 @@ export class Contact { for (const e of values) { const prop = this.component.addPropertyWithValue(name, e.value); if (e.types.length > 0) { - prop.setParameter('type', e.types); + prop.setParameter('type', e.types.map(Contact.serializePropertyType)); } } } @@ -245,7 +303,7 @@ export class Contact { } else if (typeof types === 'string') { types = [types]; } - return types.map(t => t.toLowerCase()); + return types.map(Contact.displayPropertyType); } private normalizeStringProperty(p: ICAL.Property): StringValueWithTypes { @@ -433,7 +491,7 @@ export class Contact { for (const e of values) { const prop = this.component.addPropertyWithValue('adr', e.value.values); if (e.types.length > 0) { - prop.setParameter('type', e.types); + prop.setParameter('type', e.types.map(Contact.serializePropertyType)); } } }