#228 skipped a vCard whose UID the book already held. The reporter asked for the opposite on #174 and he is right: the reason to import a file a second time is usually that the first one was not right, so skipping means a corrected export corrects nothing. A merge, not a replacement. Properties the file carries overwrite what is here; properties it does not mention are left alone, so a phone number added in ihasmail after the first import survives a re-import of the original file. The cost is that a field genuinely deleted at the source stays here, which is the better way to be wrong -- the other way round loses work nobody asked to lose. Worth confirming with him rather than assuming. `addressBookIds` is left off the patch. The card is already in this book, so saying it again says nothing, and saying it on a card that is also in another book would move it. Creates and updates now share one batch budget. Stalwart counts every object in a /set together, so batching the halves separately would send 300 new and 300 changed as two calls of 300 and be refused for a limit of 500 that neither half exceeds. LDIF is untouched and still reports look-alikes without acting on them, since what it should match on is the question still open on #223. Both imports keep one answer shape so a caller need not know which it called; LDIF's `updated` is always 0, which is the honest number rather than a missing field. The message a vCard attached to a message shows changes with it: the newer copy now wins instead of being dropped, so it says the contact was brought up to date rather than that nothing was added. Refs #223.
47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
import { useState } from "react";
|
|
import { UserPlus } from "lucide-react";
|
|
import type { EmailBodyPart, Id } from "@/jmap/types";
|
|
import { useContacts } from "@/store/contacts";
|
|
import { client } from "@/jmap/client";
|
|
import { toast } from "@/ui/toast";
|
|
import { plural, t } from "@/lib/i18n";
|
|
|
|
export function VCardCard({ part, accountId }: { part: EmailBodyPart; accountId: Id }) {
|
|
const contacts = useContacts();
|
|
const [busy, setBusy] = useState(false);
|
|
const [done, setDone] = useState(false);
|
|
if (!contacts.available || !part.blobId) return null;
|
|
const add = async () => {
|
|
setBusy(true);
|
|
try {
|
|
const text = await client.fetchBlobText(accountId, part.blobId!, "text/vcard");
|
|
const book = Object.values(contacts.books).find((b) => b.isDefault) ?? Object.values(contacts.books)[0];
|
|
if (!book) throw new Error("No address book available");
|
|
const { created, updated } = await contacts.importVCard(text, book.id);
|
|
setDone(true);
|
|
/*
|
|
* A card attached to a message is usually one you already have, and now
|
|
* the newer copy wins rather than being dropped -- so the message says it
|
|
* was brought up to date. "Added 0 contacts" would read as a failure when
|
|
* the opposite happened.
|
|
*/
|
|
if (!created && updated) toast.success(plural(updated, { one: "Updated the contact you already had", other: "Updated {n} contacts you already had" }));
|
|
else toast.success(plural(created, { one: "Added {n} contact", other: "Added {n} contacts" }));
|
|
} catch (err) {
|
|
toast.error((err as Error).message);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
return (
|
|
<div className="vcard-card">
|
|
<UserPlus size={20} style={{ color: "var(--accent)" }} />
|
|
<div className="grow">
|
|
<div style={{ fontWeight: 600 }}>{part.name ?? "Contact card"}</div>
|
|
<div className="hint">{t("vCard attachment")}</div>
|
|
</div>
|
|
<button className="btn btn-sm" disabled={busy || done} onClick={() => void add()}>{done ? "Added" : "Add to contacts"}</button>
|
|
</div>
|
|
);
|
|
}
|