From 3a2f60189fd739aaa214cc9c32d4482b814d0493 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 27 Aug 2026 13:01:01 -0700 Subject: [PATCH] Load the contacts the recipient picker is meant to show The picker opened on "No contacts in this address book" -- about an address book with contacts in it. Nothing was wrong with the button, and that is why it read as one: it opened, correctly, onto nothing. Contacts are fetched on demand. `loadAll` runs when the Contacts view mounts, and `suggest` kicks it off itself, which is why autocomplete has always worked from anywhere. The picker did neither, so opening a composer without having visited Contacts first -- which is most of the time, and every time in a fresh tab -- showed an empty list over a full account. Anyone who had been to Contacts that session saw it work, which is the sort of difference that reads as browser-specific when it is not. It asks for them now, and says it is loading rather than that there are none. While here: the picker decided which shared books to offer on `isSubscribed` alone. Stalwart refuses that flag on a book shared read-only, so those are recorded in settings instead -- for an address book it is the *only* record -- and filtering on the server's flag left every shared book out of the picker while the sidebar showed it. Both now ask the same question. Verified against the mock from a genuinely cold store -- cards emptied, `loaded` false, opening the picker as the first thing that wants them: eight rows, from the reader's own book and a shared one, where before there were none. --- web/src/views/compose/RecipientPicker.tsx | 29 ++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/web/src/views/compose/RecipientPicker.tsx b/web/src/views/compose/RecipientPicker.tsx index f0ad31f..2792b62 100644 --- a/web/src/views/compose/RecipientPicker.tsx +++ b/web/src/views/compose/RecipientPicker.tsx @@ -1,7 +1,9 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Book, BookOpen, Search, Users, X } from "lucide-react"; +import { Spinner } from "@/ui/misc"; import { Dialog } from "@/ui/dialog"; import { useContacts } from "@/store/contacts"; +import { useSettings } from "@/store/settings"; import { contactDisplayName, contactEmails } from "@/lib/contacts"; import type { ContactCard, EmailAddress } from "@/jmap/types"; @@ -37,7 +39,26 @@ export function RecipientPicker({ onPick, onClose }: { onPick: (field: Field, ad const [bookKey, setBookKey] = useState("all"); const [picked, setPicked] = useState>({}); - const subscribed = contacts.sharedBooks.filter((b) => b.book.isSubscribed); + /* + * Contacts are fetched on demand, and nothing had demanded them. + * + * `loadAll` runs when the Contacts view mounts, and `suggest` kicks it off + * itself so autocomplete works from anywhere. This did neither, so opening a + * composer without having visited Contacts first showed an empty picker over + * a full address book -- "no contacts in this address book", about a book + * with contacts in it. + */ + useEffect(() => { + if (contacts.available && !contacts.loaded && !contacts.loading) void contacts.loadAll(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contacts.available, contacts.loaded]); + + /* Added counts whether the server remembered it or the settings did -- + Stalwart refuses the flag on a book shared read-only, so for those the + settings are the only record and filtering on `isSubscribed` alone would + leave every shared book out of the picker. */ + const addedShares = new Set(useSettings((s) => s.settings).addedShares); + const subscribed = contacts.sharedBooks.filter((b) => b.book.isSubscribed || addedShares.has(`${b.accountId}:${b.book.id}`)); const ownBooks = Object.values(contacts.books).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)); const rows = useMemo(() => { @@ -136,7 +157,9 @@ export function RecipientPicker({ onPick, onClose }: { onPick: (field: Field, ad )}
- {!rows.length ? ( + {contacts.loading && !rows.length ? ( + + ) : !rows.length ? (

{q ? "Nobody matches that." : "No contacts in this address book."}

) : ( rows.map((r) => (