Labels in Mail UI (chips + Actions + sidebar), plain-text Lato, calendar-panel polish
Labels:
- Moved labelling out of the "+label" message slot into Mail: label chips on the
thread subject bar (removable ✕) and before list subjects, and a Labels submenu
in the per-message Actions menu (multi-select checkboxes + "Create new…").
Per-message storage kept; the reading pane writes a thread-union aggregate so the
list (no message ids) can show chips.
- Sidebar LABELS section rewritten: aligned monocolor tag rows, collapsible
(collapsed by default), inline "+" create popover, hover "−" delete with a confirm
popover, and click-to-filter the mail list by label.
- Backend: batch message/thread label reads, thread aggregate, create + delete
(delete unhooks from all mail), and GET /labels/{name}/threads. Core GET /threads
gained an `ids` param to power the label view. Removed the plugin-pages list from
the Mail sidebar.
Plain-text mail now renders in Lato (self-hosted at /fonts, @font-face'd into the
sandboxed message iframe) instead of the monospace <pre> default.
Calendar right-panel (embed mode): restored the "…" view-options menu, pinned the
toolbar + mini-calendar (only the agenda scrolls), plain "All calendars" button
(inline resets), and the event popover now closes on any outside click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { useT } from "../../i18n";
|
||||
import { api } from "../../lib/api";
|
||||
import { formatClock, getDateLocale } from "../../lib/timeFormat";
|
||||
import type { Address, ThreadSummary } from "../../lib/types";
|
||||
import { Icon } from "../Icon";
|
||||
import { LabelChips } from "../mail/LabelChips";
|
||||
|
||||
function timeLabel(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
@@ -86,6 +89,15 @@ export function MessageList({
|
||||
// every row shows a checkbox (selection mode) until all are cleared.
|
||||
const checkMode = checkedIds.size > 0;
|
||||
|
||||
// Aggregated labels per thread (written by the reading pane) — shown before the subject.
|
||||
const threadIds = threads.map((th) => th.id);
|
||||
const labelsQuery = useQuery({
|
||||
queryKey: ["thread-labels", threadIds],
|
||||
queryFn: () => api.threadLabelsBatch(threadIds),
|
||||
enabled: threadIds.length > 0,
|
||||
});
|
||||
const labelMap = labelsQuery.data ?? {};
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: threads.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
@@ -208,11 +220,12 @@ export function MessageList({
|
||||
{/* Subject + snippet (extra left margin -> 3x the gap to the sender). */}
|
||||
<div className="ml-6 flex min-w-0 flex-1 flex-col justify-center">
|
||||
<div
|
||||
className={`truncate text-[14px] ${
|
||||
className={`flex min-w-0 items-center gap-1.5 text-[14px] ${
|
||||
unread ? "font-black text-app-fg" : "text-app-fg"
|
||||
}`}
|
||||
>
|
||||
{thread.subject}
|
||||
<LabelChips labels={labelMap[thread.id] ?? []} />
|
||||
<span className="truncate">{thread.subject}</span>
|
||||
</div>
|
||||
<div className="truncate text-xs text-app-faint">{thread.snippet}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user