diff --git a/stores/__tests__/contact-store.test.ts b/stores/__tests__/contact-store.test.ts index 8569ef391..75fc251da 100644 --- a/stores/__tests__/contact-store.test.ts +++ b/stores/__tests__/contact-store.test.ts @@ -1201,6 +1201,58 @@ describe('contact-store', () => { ); }); + // Card ids repeat across accounts (live on 0.16 and 1.0: the sharee's own + // card and one it creates in the owner's shared book both got "c"). A card + // created in a shared book was kept under its bare id: deleting it right + // after deleted the own card with that id. + it('keeps a card created in a shared book apart from an own card with the same id', async () => { + useContactStore.setState({ + addressBooks: [ + { id: 'ab-1', name: 'Personal', isDefault: true }, + { id: 'acct-a:ab-7', originalId: 'ab-7', name: 'Team', isShared: true, accountId: 'acct-a', accountName: 'Alice' }, + ], + contacts: [makeContact({ id: 'c', addressBookIds: { 'ab-1': true } })], + }); + const client = makeClient(); + client.createContact.mockImplementation(async (c: Partial) => ({ ...makeContact(), ...c, id: 'c' })); + + await useContactStore.getState().createContact( + client as unknown as IJMAPClient, + { addressBookIds: { 'acct-a:ab-7': true } }, + ); + + expect(client.createContact).toHaveBeenCalledWith(expect.objectContaining({ addressBookIds: { 'ab-7': true } }), 'acct-a'); + const created = useContactStore.getState().contacts[1]; + expect(created).toMatchObject({ + id: 'acct-a:c', originalId: 'c', isShared: true, accountId: 'acct-a', accountName: 'Alice', + addressBookIds: { 'acct-a:ab-7': true }, + }); + + await useContactStore.getState().deleteContact(client as unknown as IJMAPClient, 'acct-a:c'); + + expect(client.deleteContact).toHaveBeenCalledWith('c', 'acct-a'); + expect(useContactStore.getState().contacts.map((c) => c.id)).toEqual(['c']); + }); + + it('namespaces a card created through another login like that login\'s listing', async () => { + useContactStore.setState({ + addressBooks: [ + { id: 'me@host::ab-1', originalId: 'ab-1', localAccountId: 'me@host', name: 'Personal', isDefault: true }, + ], + }); + const client = makeClient(); + + await useContactStore.getState().createContact( + client as unknown as IJMAPClient, + { addressBookIds: { 'me@host::ab-1': true } }, + ); + + expect(useContactStore.getState().contacts[0]).toMatchObject({ + id: 'me@host::created-id', originalId: 'created-id', localAccountId: 'me@host', + addressBookIds: { 'me@host::ab-1': true }, + }); + }); + it('moves to a namespaced personal book with the raw id and keeps the display id locally', async () => { const target = { id: 'me@host::ab-2', originalId: 'ab-2', localAccountId: 'me@host', name: 'Work' }; useContactStore.setState({ diff --git a/stores/contact-store.ts b/stores/contact-store.ts index 62b41cf2a..7171f4b7d 100644 --- a/stores/contact-store.ts +++ b/stores/contact-store.ts @@ -710,19 +710,31 @@ export const useContactStore = create()( ? await writeContactWithNewPhoto(owner, accountId, cleanedContact, options.photoKey, (card) => owner.createContact(card, accountId)) : await owner.createContact(cleanedContact, accountId); - // Preserve shared account metadata - if (contact.isShared && contact.accountId) { - created.accountId = contact.accountId; - created.accountName = contact.accountName; - created.isShared = true; - created.id = `${contact.accountId}:${created.id}`; - created.originalId = created.id.includes(':') ? created.id.split(':').slice(1).join(':') : created.id; + // The card under the id the listings give it (getAllContacts, then + // prefixContactsWithLocalAccount): card ids repeat across accounts, + // and one from a shared book kept under its bare id was taken for + // the own card of that id by the next change or delete. + let stored = created; + if (accountId) { + stored = { + ...created, + id: `${accountId}:${created.id}`, + originalId: created.id, + accountId, + accountName: contact.accountName + ?? get().addressBooks.find(b => b.isShared && b.accountId === accountId)?.accountName, + isShared: true, + addressBookIds: Object.fromEntries( + Object.entries(created.addressBookIds ?? {}).map(([bookId, v]) => [`${accountId}:${bookId}`, v]), + ), + }; } + if (localAccountId) stored = prefixContactsWithLocalAccount([stored], localAccountId)[0]; set((state) => ({ - contacts: [...state.contacts, created], + contacts: [...state.contacts, stored], isLoading: false, })); - void contactHooks.onAfterContactCreate.emit(contactToView(created)); + void contactHooks.onAfterContactCreate.emit(contactToView(stored)); return true; } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to create contact';