Files
native/src/api/contacts.ts
T
Linus RathandClaude Opus 4.7 5e99bb36b2 Sync local work: multi-account, calendar/contacts, compose, perf
Contents accumulated in the working tree since the initial commit:

- Multi-account: account-store, per-accountId credential scoping, account
  switcher in sidebar, add-account flow on LoginScreen
- Calendar: recurring event expansion, EventModal/EventDetailSheet, week/
  agenda views, calendar sidebar, participant input, per-day event index
  for O(1) cell lookup (MonthView/WeekView/AgendaView)
- Contacts: contact detail/form/group screens, vCard import/export,
  avatar utils, contact picker sheet, sidebar drawer
- Email: compose screen, thread screen, sender avatar, EmailBodyView with
  CID image resolution and external-content policy
- Infrastructure: navigation types, mailbox tree helper, settings store

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 01:21:50 +02:00

94 lines
2.8 KiB
TypeScript

import { jmapClient } from './jmap-client';
import { CAPABILITIES } from './types';
import type { ContactCard, AddressBook } from './types';
const USING = [CAPABILITIES.CORE, CAPABILITIES.CONTACTS];
function methodResult<T = any>(res: any, index = 0): T {
const entry = res?.methodResponses?.[index];
if (!entry) throw new Error('JMAP: empty method response');
if (entry[0] === 'error') {
const err = entry[1] || {};
throw new Error(err.description || err.type || 'JMAP method error');
}
return entry[1] as T;
}
export async function getAddressBooks(): Promise<AddressBook[]> {
const accountId = jmapClient.accountId;
const res = await jmapClient.request(
[['AddressBook/get', { accountId }, '0']],
USING,
);
return methodResult<{ list: AddressBook[] }>(res).list ?? [];
}
export async function queryContacts(
filter?: { text?: string; inAddressBook?: string },
limit = 1000,
): Promise<string[]> {
// Match webmail: no `sort` (unsupported by Stalwart for ContactCard/query).
// Client sorts by display name after fetch.
const accountId = jmapClient.accountId;
const args: Record<string, unknown> = { accountId, limit };
if (filter && Object.keys(filter).length > 0) args.filter = filter;
const res = await jmapClient.request(
[['ContactCard/query', args, '0']],
USING,
);
return methodResult<{ ids: string[] }>(res).ids ?? [];
}
export async function getContacts(ids: string[]): Promise<ContactCard[]> {
if (ids.length === 0) return [];
const accountId = jmapClient.accountId;
const batchSize = jmapClient.getMaxObjectsInGet();
const all: ContactCard[] = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const res = await jmapClient.request(
[['ContactCard/get', { accountId, ids: batch }, '0']],
USING,
);
const list = methodResult<{ list: ContactCard[] }>(res).list ?? [];
all.push(...list);
}
return all;
}
export async function createContact(
contact: Partial<ContactCard>,
addressBookId: string,
): Promise<ContactCard> {
const accountId = jmapClient.accountId;
const res = await jmapClient.request(
[['ContactCard/set', {
accountId,
create: {
'new-contact': { ...contact, addressBookIds: { [addressBookId]: true } },
},
}, '0']],
USING,
);
return res.methodResponses[0][1].created['new-contact'];
}
export async function updateContact(
id: string,
changes: Partial<ContactCard>,
): Promise<void> {
const accountId = jmapClient.accountId;
await jmapClient.request(
[['ContactCard/set', { accountId, update: { [id]: changes } }, '0']],
USING,
);
}
export async function deleteContacts(ids: string[]): Promise<void> {
const accountId = jmapClient.accountId;
await jmapClient.request(
[['ContactCard/set', { accountId, destroy: ids }, '0']],
USING,
);
}