+
+ {((expanded && !isCollapsed) || isCollapsed) && (
+ <>
+ {accounts.map((account) => {
+ const accountExpanded = expandedSharedAccounts.has(account.id);
+ const menuAccountId = sharedAccountMenuId(account.accountId);
+ const compact = compactSharedMailboxes ? compactSharedMailbox(account) : null;
+ if (compact) {
+ // A row without folders below it has no chevron, so what
+ // else hangs below it (Scheduled, tags) stays shown.
+ const compactExpanded = !isCollapsed
+ && (expandedFolders.has(compact.id) || compact.children.length === 0);
+ const scheduledCount = active && menuAccountId ? (scheduledTotalByAccount?.[menuAccountId] ?? 0) : 0;
+ const scheduledRow = () => scheduledCount > 0
+ && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId, depth: 1 });
+ // Scheduled goes after Drafts, as under a header row; with
+ // Drafts left out (empty), after the other folders.
+ const hasDrafts = compact.children.some((child) => child.role === 'drafts');
+ // "Mark subfolders read" covers the inbox's own subfolders
+ // only, not the account's folders listed below the row.
+ const inboxHasSubfolders = (account.children.find((child) => child.id === compact.id)?.children.length ?? 0) > 0;
+ return (
+
+ handleCompactSharedContextMenu(e, node, compact.id, menuAccountId, inboxHasSubfolders) : undefined}
+ pickOnly={!active}
+ renderAfterChild={(child) => child.role === 'drafts' && scheduledRow()}
+ />
+ {compactExpanded && !hasDrafts && scheduledRow()}
+ {active && compactExpanded && menuAccountId && activeAccountId && (
+
+ )}
+
+ );
+ }
+ return (
+
+ toggleSharedAccount(account.id)}
+ isCollapsed={isCollapsed}
+ sub
+ icon={}
+ testId="section-shared-account"
+ onContextMenu={active && menuAccountId
+ ? (e) => handleSharedAccountContextMenu(e, menuAccountId)
+ : undefined}
+ />
+ {accountExpanded && !isCollapsed && (
+ <>
+ {account.children.map((child) => (
+
+
+ {active && child.role === 'drafts' && menuAccountId
+ && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId })}
+
+ ))}
+ {active && menuAccountId && !account.children.some((child) => child.role === 'drafts')
+ && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId })}
+ {active && menuAccountId && activeAccountId && (
+
+ )}
+ >
+ )}
+
+ );
+ })}
+ >
+ )}
+
+ );
+ };
+
// The Tags section. It follows the shared mailboxes unless the admin policy
// lists it above them, so a long list of shared mailboxes does not push the
// tags out of sight.
@@ -1871,7 +2036,7 @@ function SidebarImpl({
)}
{useMultiAccount ? (
- accountGroups.map(({ account, isActive, tree }) => {
+ accountGroups.map(({ account, isActive, tree, shared }) => {
const expanded = !collapsedAccountGroups.has(account.id);
const isViewing = isActive ? viewingAccountId === null : viewingAccountId === account.id;
return (
@@ -1921,6 +2086,17 @@ function SidebarImpl({
active: isActive,
dragAccountId: isActive ? null : account.id,
})}
+ {renderSharedSection(shared, {
+ sub: true,
+ expanded: !collapsedCustomFolders.has(`${SHARED_KEY_PREFIX}${account.id}`),
+ onToggle: () => toggleCustomFolders(`${SHARED_KEY_PREFIX}${account.id}`),
+ // Another login's shared mailbox is opened by switching to that login.
+ selected: selectedKeyword || !isViewing || !isActive ? "" : selectedMailbox,
+ onSelect: isActive
+ ? (mailboxId) => onAccountMailboxSelect?.(null, mailboxId)
+ : (mailboxId) => onLoginSharedMailboxSelect?.(account.id, mailboxId),
+ active: isActive,
+ })}
>
)}
>
@@ -2053,120 +2229,12 @@ function SidebarImpl({
{tagsAboveShared && renderTags()}
- {!useMultiAccount && showSecondary && sharedAccounts.length > 0 && (
-
-
- {((sharedExpanded && !isCollapsed) || isCollapsed) && (
- <>
- {sharedAccounts.map((account) => {
- const accountExpanded = expandedSharedAccounts.has(account.id);
- const menuAccountId = sharedAccountMenuId(account.accountId);
- const compact = compactSharedMailboxes ? compactSharedMailbox(account) : null;
- if (compact) {
- // A row without folders below it has no chevron, so what
- // else hangs below it (Scheduled, tags) stays shown.
- const compactExpanded = !isCollapsed
- && (expandedFolders.has(compact.id) || compact.children.length === 0);
- const scheduledCount = menuAccountId ? (scheduledTotalByAccount?.[menuAccountId] ?? 0) : 0;
- const scheduledRow = () => scheduledCount > 0
- && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId, depth: 1 });
- // Scheduled goes after Drafts, as under a header row; with
- // Drafts left out (empty), after the other folders.
- const hasDrafts = compact.children.some((child) => child.role === 'drafts');
- // "Mark subfolders read" covers the inbox's own subfolders
- // only, not the account's folders listed below the row.
- const inboxHasSubfolders = (account.children.find((child) => child.id === compact.id)?.children.length ?? 0) > 0;
- return (
-
- handleCompactSharedContextMenu(e, node, compact.id, menuAccountId, inboxHasSubfolders)}
- renderAfterChild={(child) => child.role === 'drafts' && scheduledRow()}
- />
- {compactExpanded && !hasDrafts && scheduledRow()}
- {compactExpanded && menuAccountId && activeAccountId && (
-
- )}
-
- );
- }
- return (
-
- toggleSharedAccount(account.id)}
- isCollapsed={isCollapsed}
- sub
- icon={}
- testId="section-shared-account"
- onContextMenu={menuAccountId
- ? (e) => handleSharedAccountContextMenu(e, menuAccountId)
- : undefined}
- />
- {accountExpanded && !isCollapsed && (
- <>
- {account.children.map((child) => (
-
-
- {child.role === 'drafts' && menuAccountId
- && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId })}
-
- ))}
- {menuAccountId && !account.children.some((child) => child.role === 'drafts')
- && renderScheduledRow(`${account.id}-scheduled`, { accountId: menuAccountId })}
- {menuAccountId && activeAccountId && (
-
- )}
- >
- )}
-
- );
- })}
- >
- )}
-
- )}
+ {!useMultiAccount && showSecondary && renderSharedSection(sharedAccounts, {
+ expanded: sharedExpanded,
+ onToggle: toggleShared,
+ selected: selectedKeyword ? "" : selectedMailbox,
+ onSelect: onMailboxSelect,
+ })}
{!tagsAboveShared && renderTags()}
diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx
index 9b37c8029..3718674e6 100644
--- a/components/mail/mail-app.tsx
+++ b/components/mail/mail-app.tsx
@@ -96,6 +96,7 @@ import { FilterPanelHost } from "@/components/search/filter-panel-host";
import { SearchFilterPanel, SELECT_CLASS } from "@/components/search/search-filter-panel";
import type { ContactSuggestion } from "@/lib/search-suggestions";
import type { Attachment, SendEmailResult } from "@/lib/jmap/types";
+import type { IJMAPClient } from "@/lib/jmap/client-interface";
import { peekListAttachments, requestListAttachments, type ListAttachmentSource, type LoadListAttachments } from "@/lib/list-attachments";
import { useSearchHistoryStore } from "@/stores/search-history-store";
import { WelcomeBanner } from "@/components/ui/welcome-banner";
@@ -503,6 +504,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
batchMarkAsSpam,
batchUndoSpam,
accountMailboxes,
+ accountSharedMailboxes,
viewingAccountId,
selectAccountMailbox,
setViewingAccount,
@@ -591,6 +593,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
batchMarkAsSpam: state.batchMarkAsSpam,
batchUndoSpam: state.batchUndoSpam,
accountMailboxes: state.accountMailboxes,
+ accountSharedMailboxes: state.accountSharedMailboxes,
viewingAccountId: state.viewingAccountId,
selectAccountMailbox: state.selectAccountMailbox,
setViewingAccount: state.setViewingAccount,
@@ -3309,10 +3312,12 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
return true;
};
- const handleAccountMailboxSelect = async (accountId: string | null, mailboxId: string) => {
+ // `activeClient` stands in for `client` when the caller has just switched
+ // logins and this render's `client` is still the previous one.
+ const handleAccountMailboxSelect = async (accountId: string | null, mailboxId: string, activeClient?: IJMAPClient) => {
const viewingClient = accountId
? useAuthStore.getState().getClientForAccount(accountId) ?? client
- : client;
+ : activeClient ?? client;
selectAccountMailbox(accountId, mailboxId);
selectEmail(null);
if (isMobile) {
@@ -3337,6 +3342,23 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
}
};
+ // Pro sidebar: a shared mailbox listed under another login (#1486). Shared
+ // mail is read through the active login - its folder list, actions and
+ // tags all assume it - so switch to that login first, wait for its folders
+ // and open the mailbox there, as a folder link to another login does.
+ const handleLoginSharedMailboxSelect = async (loginId: string, mailboxId: string) => {
+ if (useAuthStore.getState().activeAccountId !== loginId) {
+ const before = useEmailStore.getState().mailboxes;
+ await switchAccount(loginId);
+ // Overtaken or failed: the folders on screen are not that login's.
+ if (useAuthStore.getState().activeAccountId !== loginId) return;
+ const fresh = await freshMailboxes(before);
+ if (useAuthStore.getState().activeAccountId !== loginId) return;
+ if (!fresh.some((mb) => mb.id === mailboxId)) return;
+ }
+ await handleAccountMailboxSelect(null, mailboxId, useAuthStore.getState().client ?? undefined);
+ };
+
const handleMailboxSelect = async (mailboxId: string) => {
// A shared account's Scheduled row arrives as "__scheduled__: