import { useRef, useState, type MouseEvent } from "react"; import { Avatar, Button, Icon, icons, Key, Popover, Segment, type SegmentOption, } from "../ui"; import { keyLabel } from "../keys/bindings"; import { useAccounts } from "../store/useAccounts"; import { useMail } from "../store/useMail"; import { useOverlays } from "../store/useOverlays"; import { useSettings } from "../store/useSettings"; import { busy, trouble, useSync } from "../store/useSync"; import { runCommand } from "../keys/commands"; import { accountHue, displayName } from "./format"; import { SearchBar } from "./SearchBar"; import type { Place } from "../ipc"; import "./header.css"; /** The three boxes, and the only navigation that is on screen without being asked for. */ const BOXES: SegmentOption[] = [ { id: "inbox", label: "Inbox", keycap: "1" }, { id: "feed", label: "Feed", keycap: "2" }, { id: "paper-trail", label: "Paper Trail", keycap: "3" }, ]; /** * On macOS Tauri decides the zoom on release rather than on the second press, so it leaves that * press alone and WebKit selects the word under it. Only a press that landed on the drag region * itself is quietened: a double-click in the search box still has a word to pick. */ export function quietDoubleClick(e: MouseEvent) { if ( e.detail >= 2 && e.target instanceof HTMLElement && e.target.hasAttribute("data-tauri-drag-region") ) { e.preventDefault(); } } export function Header() { const place = useMail((s) => s.place); const accountId = useMail((s) => s.accountId); const setAccount = useMail((s) => s.setAccount); const goTo = useMail((s) => s.goTo); const accounts = useAccounts((s) => s.accounts); const statuses = useSync((s) => s.statuses); const syncing = useSync((s) => s.phase); const show = useOverlays((s) => s.show); const showSettings = useSettings((s) => s.show); const settingsOpen = useSettings((s) => s.open); const closeSettings = useSettings((s) => s.close); const [switcher, setSwitcher] = useState(false); const chip = useRef(null); const current = accounts.find((a) => a.id === accountId) ?? null; const note = trouble(statuses, accountId); // The engine's own sentence when it has one. A pass somebody asked for by hand mostly has none: // it lists, finds nothing new and stops, and for those seconds the only sign it is running at all // was the network light. So the ask itself gets a line, in the same slot, until it comes back. const working = busy(statuses, accountId) ?? (syncing === "syncing" ? "Checking for mail" : null); return ( // The traffic lights float over the left end of this row, so the row is what drags the window // and zooms it on a double-click, and none of the controls in it do: a button that also drags // swallows its own click. The lanes and the status line carry the attribute as well, because // that is where the empty space actually is, and a bare region only answers to a press that // lands on it directly.
{/* One line, never two, in the lane the account chip is already in: the boxes are centred in their own grid column and the buttons are right-aligned in theirs, so this can grow and shrink all day without moving anything a hand is aiming at. What is wrong wins over what is happening, and neither is ever a spinner, a bar or something to click. */} {note ? ( {note} ) : working ? ( {working} ) : null}
{/* Going to a box leaves Settings, because Settings is a place and you cannot be in two. The keyboard already knew that: the place commands in App.tsx close it. This did not, so clicking Inbox from Settings changed the place underneath and left Settings on top of it, with no way out but the keyboard. While Settings is up nothing here is selected, because claiming Inbox is selected under a screen that is not the Inbox is the same lie. */} { closeSettings(); goTo(id as Place); }} />
setSwitcher(false)} label="Accounts" width={280} >
    {accounts.map((account, index) => (
  • ))}
{/* docs/settings.md names four ways in and this is one of them. Not an account option: it goes somewhere rather than switching what the list is showing. */}
); } export default Header;