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.
This commit is contained in:
@@ -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 { Book, BookOpen, Search, Users, X } from "lucide-react";
|
||||||
|
import { Spinner } from "@/ui/misc";
|
||||||
import { Dialog } from "@/ui/dialog";
|
import { Dialog } from "@/ui/dialog";
|
||||||
import { useContacts } from "@/store/contacts";
|
import { useContacts } from "@/store/contacts";
|
||||||
|
import { useSettings } from "@/store/settings";
|
||||||
import { contactDisplayName, contactEmails } from "@/lib/contacts";
|
import { contactDisplayName, contactEmails } from "@/lib/contacts";
|
||||||
import type { ContactCard, EmailAddress } from "@/jmap/types";
|
import type { ContactCard, EmailAddress } from "@/jmap/types";
|
||||||
|
|
||||||
@@ -37,7 +39,26 @@ export function RecipientPicker({ onPick, onClose }: { onPick: (field: Field, ad
|
|||||||
const [bookKey, setBookKey] = useState<string>("all");
|
const [bookKey, setBookKey] = useState<string>("all");
|
||||||
const [picked, setPicked] = useState<Record<string, Row>>({});
|
const [picked, setPicked] = useState<Record<string, Row>>({});
|
||||||
|
|
||||||
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 ownBooks = Object.values(contacts.books).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
@@ -136,7 +157,9 @@ export function RecipientPicker({ onPick, onClose }: { onPick: (field: Field, ad
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ maxHeight: "48vh", overflowY: "auto" }}>
|
<div style={{ maxHeight: "48vh", overflowY: "auto" }}>
|
||||||
{!rows.length ? (
|
{contacts.loading && !rows.length ? (
|
||||||
|
<Spinner label="Loading contacts…" />
|
||||||
|
) : !rows.length ? (
|
||||||
<p className="hint">{q ? "Nobody matches that." : "No contacts in this address book."}</p>
|
<p className="hint">{q ? "Nobody matches that." : "No contacts in this address book."}</p>
|
||||||
) : (
|
) : (
|
||||||
rows.map((r) => (
|
rows.map((r) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user