Choose recipients from the address books
Addressing a message worked only if you already knew the name you were half-way through typing. Autocomplete answers "finish this for me"; there was no answer to "who is there?", which is the question someone has when they open a compose window and want the person from the team list whose surname they cannot summon. The To row now opens the address books -- from a button beside Cc and Bcc, where someone thinking about recipients is already looking, and from the To label itself for anyone who tries that first. Search across every book or narrow to one, tick as many people as the message needs, and send them to To, Cc or Bcc. Picking for a field that is hidden opens it, since a Bcc dropped somewhere invisible is worse than no Bcc. Every address is its own row rather than every person. Somebody with a work address and a personal one is a choice the writer has to make, and a picker that listed the card and quietly took the first address would be making it for them. Shared books are in it on the same footing as the reader's own -- that being the point of having added them -- with the account named on each row, so it is never a mystery whose list a name came from. Books that have not been added contribute nothing, the same rule the To field already follows. Verified against the mock: the picker lists the reader's contacts and the shared book's, each row naming its source; ticking one of each and choosing Cc opens the Cc row with both in it.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AlertTriangle, ChevronDown, FileText, FolderOpen, Maximize2, Minimize2, Minus, MoreVertical, Paperclip, Send, Trash2, X, Type, Clock, CheckCheck, ChevronsDown } from "lucide-react";
|
||||
import { AlertTriangle, BookUser, ChevronDown, FileText, FolderOpen, Maximize2, Minimize2, Minus, MoreVertical, Paperclip, Send, Trash2, X, Type, Clock, CheckCheck, ChevronsDown } from "lucide-react";
|
||||
import { useCompose, type Draft } from "@/store/compose";
|
||||
import { useMail } from "@/store/mail";
|
||||
import { useSettings } from "@/store/settings";
|
||||
@@ -13,6 +13,7 @@ import { htmlToText, textToHtml } from "@/lib/text";
|
||||
import { isValidEmail } from "@/lib/address";
|
||||
import { attachmentIcon } from "../mail/MessageView";
|
||||
import { FilePicker } from "./FilePicker";
|
||||
import { RecipientPicker, type Field } from "./RecipientPicker";
|
||||
import { useFiles } from "@/store/files";
|
||||
import { keyboard } from "@/lib/keyboard";
|
||||
import { useIsMobile } from "@/ui/misc";
|
||||
@@ -30,6 +31,7 @@ export function Composer({ draft }: { draft: Draft }) {
|
||||
const addFromFiles = useCompose((s) => s.addFromFiles);
|
||||
const filesAvailable = useFiles((s) => s.available);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [addressBookOpen, setAddressBookOpen] = useState(false);
|
||||
const removeAttachment = useCompose((s) => s.removeAttachment);
|
||||
const setIdentity = useCompose((s) => s.setIdentity);
|
||||
const insertTemplate = useCompose((s) => s.insertTemplate);
|
||||
@@ -174,9 +176,17 @@ export function Composer({ draft }: { draft: Draft }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="composer-field">
|
||||
<label htmlFor={`${key}-to`}>To</label>
|
||||
<label htmlFor={`${key}-to`}>
|
||||
{/* Opens the address books. Autocomplete only helps someone who
|
||||
already knows the name they are half-way through typing. */}
|
||||
<button type="button" className="link-btn" onClick={() => setAddressBookOpen(true)} title="Choose from address books">To</button>
|
||||
</label>
|
||||
<RecipientInput id={`${key}-to`} value={d.to} onChange={(to) => patch({ to })} placeholder="Recipients" autoFocus={initialFocus === "to"} />
|
||||
<span className="field-extra">
|
||||
{/* Beside Cc and Bcc, because that is where someone looks when
|
||||
they are thinking about who the message goes to. The label
|
||||
opens it too, for anyone who tries that first. */}
|
||||
<button type="button" onClick={() => setAddressBookOpen(true)} title="Choose from address books" aria-label="Choose from address books"><BookUser size={15} /></button>
|
||||
{!d.showCc && <button type="button" onClick={() => patch({ showCc: true })}>Cc</button>}
|
||||
{!d.showBcc && <button type="button" onClick={() => patch({ showBcc: true })}>Bcc</button>}
|
||||
{!d.showReplyTo && <button type="button" onClick={() => patch({ showReplyTo: true })} title="Set a Reply-To address">Reply-To</button>}
|
||||
@@ -244,6 +254,20 @@ export function Composer({ draft }: { draft: Draft }) {
|
||||
<MenuItem icon={<Clock size={16} />} label={`Undo window: ${settings.undoSendSeconds}s`} onClick={() => updateSettings({ undoSendSeconds: settings.undoSendSeconds >= 30 ? 0 : settings.undoSendSeconds + 5 })} />
|
||||
{canSchedule && <ScheduleMenuItems maxMs={scheduleMax} onPick={scheduleFor} onCustom={() => { sendMenu.close(); setScheduleOpen(true); }} />}
|
||||
</Popover>
|
||||
{addressBookOpen && (
|
||||
<RecipientPicker
|
||||
onPick={(field: Field, addresses) => {
|
||||
// Added to whatever is already there, and the field is opened if
|
||||
// it was hidden -- picking a Bcc should not put one somewhere
|
||||
// the writer cannot see it.
|
||||
const existing = field === "to" ? d.to : field === "cc" ? d.cc : d.bcc;
|
||||
const merged = [...existing];
|
||||
for (const a of addresses) if (!merged.some((x) => x.email.toLowerCase() === a.email.toLowerCase())) merged.push(a);
|
||||
patch({ [field]: merged, ...(field === "cc" ? { showCc: true } : field === "bcc" ? { showBcc: true } : {}) });
|
||||
}}
|
||||
onClose={() => setAddressBookOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{pickerOpen && <FilePicker onPick={(picked) => void addFromFiles(key, picked)} onClose={() => setPickerOpen(false)} />}
|
||||
{canSchedule && scheduleOpen && (
|
||||
<ScheduleDialog open maxMs={scheduleMax} initial={d.sendAt} onClose={() => setScheduleOpen(false)} onPick={scheduleFor} />
|
||||
|
||||
Reference in New Issue
Block a user