M6 hosted mailboxes (off by default); dark message mode; fixes
Hosted mailboxes (SPEC App. D), behind HOSTED_MAILBOXES (off: unchanged):
- Stalwart mail server, provisioned by `python -m app.hosted.provision`;
directory read from a Postgres view, app passwords only for mail apps
- native account per user, the worker signs in as master for the user
- raw messages of hosted mailboxes only cached, fetched back on demand
- migrations 0025 (accounts.kind, directory view), 0026 (IMAP ids bigint)
Mail:
- dark message mode switch in the reading pane (Thunderbird's algorithm)
- moves and folder creation quote the folder name ("Deleted Items"):
swipe-to-delete / Trash no longer bounce back
- thread unread counters: lock before recount, sorted lock order,
UIDVALIDITY re-import recounts, nightly repair_threads
- sidebar accounts sorted alphabetically, more space above headings
Settings: Security headings renamed; antispam accounts are explicit
(none ticked = none filtered), input widths, Activity hidden.
Plugin bundles served with Cache-Control: no-cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,11 @@ import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useT } from "../../i18n";
|
||||
import { personHue } from "../../lib/avatarColor";
|
||||
import { setDarkMessages, useDarkMessages } from "../../lib/darkMessage";
|
||||
import { api } from "../../lib/api";
|
||||
import { printMessages } from "../../lib/print";
|
||||
import { replyTargets } from "../../lib/replyTargets";
|
||||
import { useTheme } from "../../lib/theme";
|
||||
import { useIsMobile } from "../../lib/useIsMobile";
|
||||
import { getDateLocale, getHour12 } from "../../lib/timeFormat";
|
||||
import type { AccountOut, MailboxOut, MessageEnvelope } from "../../lib/types";
|
||||
@@ -726,6 +728,8 @@ export function ThreadView({
|
||||
const composerRef = useRef<HTMLDivElement | null>(null);
|
||||
// On a phone, reply/reply-all/forward open full screen rather than inline.
|
||||
const mobile = useIsMobile();
|
||||
const resolvedTheme = useTheme().resolved;
|
||||
const darkMessages = useDarkMessages();
|
||||
// Bring the inline composer into view when it opens (reply/reply-all/forward).
|
||||
useEffect(() => {
|
||||
if (composing && !mobile) {
|
||||
@@ -754,8 +758,35 @@ export function ThreadView({
|
||||
<LabelChips labels={threadLabels} onRemove={(l) => removeThreadLabel.mutate(l)} />
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{/* Dark message mode (lib/darkMessage.ts) — only the dark theme has it. */}
|
||||
{resolvedTheme === "dark" && (
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={darkMessages}
|
||||
className="flex items-center gap-1.5 rounded px-1.5 py-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
title={darkMessages ? t("mail.darkMessagesOn") : t("mail.darkMessagesOff")}
|
||||
aria-label={t("mail.darkMessages")}
|
||||
onClick={() => setDarkMessages(!darkMessages)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
<span
|
||||
aria-hidden
|
||||
className={`relative inline-block h-4 w-7 rounded-full transition-colors ${
|
||||
darkMessages ? "bg-app-accent" : "bg-app-border"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-3 w-3 rounded-full bg-white shadow transition-[left] ${
|
||||
darkMessages ? "left-[14px]" : "left-0.5"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
className="flex items-center justify-center rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
title={t("mail.print")}
|
||||
aria-label={t("mail.print")}
|
||||
onClick={() => void printMessages(subject, visual)}
|
||||
@@ -763,7 +794,7 @@ export function ThreadView({
|
||||
<Icon name="print" size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
className="flex items-center justify-center rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
title={allExpanded ? t("mail.collapseAll") : t("mail.expandAll")}
|
||||
aria-label={allExpanded ? t("mail.collapseAll") : t("mail.expandAll")}
|
||||
onClick={toggleAll}
|
||||
|
||||
Reference in New Issue
Block a user