fix: drop the folder sidebar from fullscreen email

This commit is contained in:
Linus Rath
2026-08-20 15:11:25 +02:00
parent 3e5ed4f3c4
commit d83c83b0a9
3 changed files with 10 additions and 135 deletions
-21
View File
@@ -106,7 +106,6 @@ import {
type MailDeepLink,
} from "@/lib/deep-links";
import { consumePendingDeepLink } from "@/lib/deep-link-handoff";
import { onProMailNavigation } from "@/lib/pro-mail-navigation";
import { useProInterfaceActive } from "@/components/pro/pro-interface-redirect";
import type { QuoteHeader } from "@/lib/plugin-types";
@@ -2417,26 +2416,6 @@ export function MailApp({ linkSegments }: MailAppProps = {}) {
}
};
// Pro email tabs render their own sidebar; clicking a folder/tag there
// publishes a navigation request that this (always-mounted) embedded
// MailApp serves with its own selection handlers. Refs, not deps: the
// handlers close over client/mailboxes and are rebuilt every render.
const proNavHandlersRef = useRef({ handleMailboxSelect, handleAccountMailboxSelect, handleTagSelect });
proNavHandlersRef.current = { handleMailboxSelect, handleAccountMailboxSelect, handleTagSelect };
useEffect(() => {
if (!isEmbedded) return;
return onProMailNavigation((request) => {
const handlers = proNavHandlersRef.current;
if (request.kind === 'tag') {
void handlers.handleTagSelect(request.keywordId);
} else if (request.accountId) {
void handlers.handleAccountMailboxSelect(request.accountId, request.mailboxId);
} else {
void handlers.handleMailboxSelect(request.mailboxId);
}
});
}, [isEmbedded]);
const handleUnreadFilterClick = async (mailboxId: string) => {
const isTogglingOff = selectedMailbox === mailboxId && searchFilters.isUnread === true;
+10 -74
View File
@@ -3,20 +3,15 @@
import { useCallback, useEffect, useState, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
import { EmailViewer } from "@/components/email/email-viewer";
import { Sidebar } from "@/components/layout/sidebar";
import { ErrorBoundary, EmailViewerErrorFallback, SidebarErrorFallback } from "@/components/error";
import { ErrorBoundary, EmailViewerErrorFallback } from "@/components/error";
import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useIdentityStore } from "@/stores/identity-store";
import { useUIStore } from "@/stores/ui-store";
import { useProMultiAccountIdentities } from "@/hooks/use-pro-multi-account-identities";
import { useDeviceDetection } from "@/hooks/use-media-query";
import { findDraftIdentityId } from "@/lib/reply-identity";
import { useSettingsStore } from "@/stores/settings-store";
import { toast } from "@/stores/toast-store";
import { useProTabStore, type ProEmailTabData, type ProReplyContext } from "@/stores/pro-tab-store";
import { requestProMailNavigation } from "@/lib/pro-mail-navigation";
import { DragDropProvider } from "@/contexts/drag-drop-context";
import type { Email } from "@/lib/jmap/types";
import type { IJMAPClient } from "@/lib/jmap/client-interface";
import { buildReplySubject, buildForwardSubject } from "@/lib/subject-prefix";
@@ -396,28 +391,15 @@ export function ProEmailView({ emailId, client: clientOverride, accountId, onLoa
}
/**
* Renders a single email in its own Pro tab, Roundcube-style: the folder
* sidebar on the left (wide panes only) with the message filling the rest -
* no list pane. Clicking a folder or tag in that sidebar steers the shared
* Mail tab (via the pro-mail-navigation bus) and focuses it.
* Renders a single email in its own Pro tab: a fullscreen mail view - just
* the message, no folder sidebar and no list pane.
*/
export function ProEmailTabBody({ tabId, data }: ProEmailTabBodyProps) {
const closeTab = useProTabStore((s) => s.closeTab);
const openTab = useProTabStore((s) => s.openTab);
const updateTabTitle = useProTabStore((s) => s.updateTabTitle);
const mailboxes = useEmailStore((s) => s.mailboxes);
const accountMailboxes = useEmailStore((s) => s.accountMailboxes);
const sidebarWidth = useUIStore((s) => s.sidebarWidth);
const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed);
// Pane-scoped: a narrow split pane drops the sidebar and keeps the mail.
const { isMobile, isTablet } = useDeviceDetection();
const showSidebar = !isMobile && !isTablet;
const [loadedEmail, setLoadedEmail] = useState<Email | null>(null);
const handleLoaded = useCallback((email: Email) => {
setLoadedEmail(email);
if (email.subject) updateTabTitle(tabId, email.subject);
}, [tabId, updateTabTitle]);
@@ -432,59 +414,13 @@ export function ProEmailTabBody({ tabId, data }: ProEmailTabBodyProps) {
return mb?.isShared ? mb.accountId : undefined;
}, [mailboxes, data.mailboxId]);
// Highlight the folder the message lives in; fall back to the mailbox the
// tab was opened from (may be null for drag/drop-opened tabs).
const highlightedMailbox = useMemo(() => {
if (loadedEmail?.mailboxIds) {
const inList = Object.keys(loadedEmail.mailboxIds).find((id) => mailboxes.some((m) => m.id === id));
if (inList) return inList;
}
return data.mailboxId ?? '';
}, [loadedEmail, mailboxes, data.mailboxId]);
const navigateToFolder = useCallback((accountId: string | null, mailboxId: string) => {
openTab('mail');
requestProMailNavigation({ kind: 'mailbox', accountId, mailboxId });
}, [openTab]);
const navigateToTag = useCallback((keywordId: string | null) => {
openTab('mail');
requestProMailNavigation({ kind: 'tag', keywordId });
}, [openTab]);
return (
<div className="flex h-full w-full bg-background">
{showSidebar && (
<div
className="h-full flex-shrink-0 overflow-hidden"
style={{ width: sidebarCollapsed ? 48 : sidebarWidth }}
>
<ErrorBoundary fallback={SidebarErrorFallback}>
{/* The sidebar's folder rows are email-drop targets and read the
drag context unconditionally - give them their own provider
(MailApp's lives in a different tab body). */}
<DragDropProvider>
<Sidebar
mailboxes={mailboxes}
selectedMailbox={highlightedMailbox}
onMailboxSelect={(mailboxId) => navigateToFolder(null, mailboxId)}
onTagSelect={navigateToTag}
multiAccountMode
accountMailboxes={accountMailboxes}
viewingAccountId={null}
onAccountMailboxSelect={navigateToFolder}
/>
</DragDropProvider>
</ErrorBoundary>
</div>
)}
<ProEmailView
emailId={data.emailId}
accountId={ownerAccountId}
onLoaded={handleLoaded}
onClose={handleClose}
className="min-w-0 flex-1"
/>
</div>
<ProEmailView
emailId={data.emailId}
accountId={ownerAccountId}
onLoaded={handleLoaded}
onClose={handleClose}
className="w-full"
/>
);
}
-40
View File
@@ -1,40 +0,0 @@
/**
* Navigation requests into the Pro shell's Mail tab from surfaces outside it.
*
* A Pro email tab renders its own folder sidebar (Roundcube-style mail view);
* clicking a folder there must steer the *Mail tab's* list. The Mail tab's
* `MailApp` owns all the folder-selection logic (unified views, scheduled,
* cross-account, keeping searches applied, ...), so instead of duplicating
* it, the email tab focuses the Mail tab and publishes a request here that
* the embedded MailApp handles with its own selection handlers.
*
* The deep-link handoff (lib/deep-link-handoff.ts) can't serve this: it is
* consumed once on mount, and in the Pro shell the Mail tab's MailApp stays
* mounted for the whole session.
*/
export type ProMailNavigationRequest =
| {
kind: 'mailbox';
/** Connected-account id owning the folder; null = the active account. */
accountId: string | null;
mailboxId: string;
}
| { kind: 'tag'; keywordId: string | null };
type Listener = (request: ProMailNavigationRequest) => void;
const listeners = new Set<Listener>();
export function onProMailNavigation(listener: Listener): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
/** Publish a request. Returns false when no Mail tab is listening. */
export function requestProMailNavigation(request: ProMailNavigationRequest): boolean {
if (listeners.size === 0) return false;
for (const listener of [...listeners]) listener(request);
return true;
}