feat: include group inboxes in unified mailbox view #328
This commit is contained in:
@@ -15,7 +15,7 @@ import { ThreadGroup, Email, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID } from "@/li
|
|||||||
import { useAccountStore } from "@/stores/account-store";
|
import { useAccountStore } from "@/stores/account-store";
|
||||||
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
||||||
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore, buildUnifiedAccountClients } from "@/stores/email-store";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
@@ -294,47 +294,22 @@ export default function Home() {
|
|||||||
useProMultiAccountMailboxes();
|
useProMultiAccountMailboxes();
|
||||||
|
|
||||||
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
||||||
|
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
|
||||||
const accounts = useAccountStore((s) => s.accounts);
|
const accounts = useAccountStore((s) => s.accounts);
|
||||||
const connectedAccountsSignature = useMemo(
|
const connectedAccountsSignature = useMemo(
|
||||||
() => accounts.filter((a) => a.isConnected).map((a) => a.id).sort().join(","),
|
() => accounts.filter((a) => a.isConnected).map((a) => a.id).sort().join(","),
|
||||||
[accounts],
|
[accounts],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildUnifiedAccounts = useCallback((): UnifiedAccountClient[] => {
|
// Builds the populated UnifiedAccountClient[] used by the unified-view
|
||||||
const connected = useAccountStore.getState().accounts.filter((a) => a.isConnected);
|
// effects and one-shot actions in this page. Reads the includeGroup
|
||||||
const clients = useAuthStore.getState().getAllConnectedClients();
|
// setting at call time so the latest toggle value is always honored.
|
||||||
const result: UnifiedAccountClient[] = [];
|
const buildPopulatedUnifiedAccounts = useCallback(async (): Promise<UnifiedAccountClient[]> => {
|
||||||
for (const account of connected) {
|
return buildUnifiedAccountClients({
|
||||||
const accountClient = clients.get(account.id);
|
includeGroup: useSettingsStore.getState().includeGroupInUnified,
|
||||||
if (!accountClient) continue;
|
});
|
||||||
result.push({
|
|
||||||
accountId: account.id,
|
|
||||||
accountLabel: account.label || account.email,
|
|
||||||
client: accountClient,
|
|
||||||
mailboxes: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const populateUnifiedAccountMailboxes = useCallback(
|
|
||||||
async (list: UnifiedAccountClient[]): Promise<UnifiedAccountClient[]> => {
|
|
||||||
const populated = await Promise.all(
|
|
||||||
list.map(async (entry) => {
|
|
||||||
try {
|
|
||||||
const mailboxes = await entry.client.getMailboxes();
|
|
||||||
return { ...entry, mailboxes };
|
|
||||||
} catch (err) {
|
|
||||||
debug.error('Failed to load mailboxes for unified account', entry.accountId, err);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return populated;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMailtoProtocolAccounts = useCallback(() => {
|
const getMailtoProtocolAccounts = useCallback(() => {
|
||||||
const connectedClients = useAuthStore.getState().getAllConnectedClients();
|
const connectedClients = useAuthStore.getState().getAllConnectedClients();
|
||||||
return useAccountStore.getState().accounts.filter((account) =>
|
return useAccountStore.getState().accounts.filter((account) =>
|
||||||
@@ -890,12 +865,12 @@ export default function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableUnifiedMailbox && !isEmbedded) return;
|
if (!enableUnifiedMailbox && !isEmbedded) return;
|
||||||
if (!isAuthenticated || !client) return;
|
if (!isAuthenticated || !client) return;
|
||||||
const built = buildUnifiedAccounts();
|
buildPopulatedUnifiedAccounts().then((built) => {
|
||||||
if (built.length < 2) return;
|
const hasGroupEntry = built.some((b) => b.isShared);
|
||||||
populateUnifiedAccountMailboxes(built).then((populated) => {
|
if (built.length < 2 && !hasGroupEntry && !isEmbedded) return;
|
||||||
refreshUnifiedCounts(populated);
|
refreshUnifiedCounts(built);
|
||||||
});
|
});
|
||||||
}, [enableUnifiedMailbox, isEmbedded, isAuthenticated, client, mailboxes, connectedAccountsSignature, buildUnifiedAccounts, populateUnifiedAccountMailboxes, refreshUnifiedCounts]);
|
}, [enableUnifiedMailbox, includeGroupInUnified, isEmbedded, isAuthenticated, client, mailboxes, connectedAccountsSignature, buildPopulatedUnifiedAccounts, refreshUnifiedCounts]);
|
||||||
|
|
||||||
// System-notification click handler. The push SW navigates the user back
|
// System-notification click handler. The push SW navigates the user back
|
||||||
// here with `?email=<id>` (specific email it built the toast from) or
|
// here with `?email=<id>` (specific email it built the toast from) or
|
||||||
@@ -1475,8 +1450,7 @@ export default function Home() {
|
|||||||
setTabletListVisible(true);
|
setTabletListVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const built = buildUnifiedAccounts();
|
const populated = await buildPopulatedUnifiedAccounts();
|
||||||
const populated = await populateUnifiedAccountMailboxes(built);
|
|
||||||
await fetchUnifiedEmailsAction(populated, role);
|
await fetchUnifiedEmailsAction(populated, role);
|
||||||
refreshUnifiedCounts(populated);
|
refreshUnifiedCounts(populated);
|
||||||
return;
|
return;
|
||||||
@@ -1821,8 +1795,7 @@ export default function Home() {
|
|||||||
if (isUnifiedView) {
|
if (isUnifiedView) {
|
||||||
const role = useEmailStore.getState().unifiedRole;
|
const role = useEmailStore.getState().unifiedRole;
|
||||||
if (role) {
|
if (role) {
|
||||||
const built = buildUnifiedAccounts();
|
const populated = await buildPopulatedUnifiedAccounts();
|
||||||
const populated = await populateUnifiedAccountMailboxes(built);
|
|
||||||
await fetchUnifiedEmailsAction(populated, role);
|
await fetchUnifiedEmailsAction(populated, role);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, ReactNode } from "react";
|
import { useState, useEffect, useMemo, ReactNode } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "@/i18n/navigation";
|
import { useRouter } from "@/i18n/navigation";
|
||||||
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
||||||
@@ -732,15 +732,20 @@ export function Sidebar({
|
|||||||
// pane.
|
// pane.
|
||||||
const hideAccountSwitcher = useSettingsStore(s => s.hideAccountSwitcher) || isEmbedded;
|
const hideAccountSwitcher = useSettingsStore(s => s.hideAccountSwitcher) || isEmbedded;
|
||||||
const enableUnifiedMailbox = useSettingsStore(s => s.enableUnifiedMailbox);
|
const enableUnifiedMailbox = useSettingsStore(s => s.enableUnifiedMailbox);
|
||||||
|
const includeGroupInUnified = useSettingsStore(s => s.includeGroupInUnified);
|
||||||
const colorfulSidebarIcons = useSettingsStore(s => s.colorfulSidebarIcons);
|
const colorfulSidebarIcons = useSettingsStore(s => s.colorfulSidebarIcons);
|
||||||
const tagCounts = useEmailStore(s => s.tagCounts);
|
const tagCounts = useEmailStore(s => s.tagCounts);
|
||||||
const accounts = useAccountStore(s => s.accounts);
|
const accounts = useAccountStore(s => s.accounts);
|
||||||
const connectedAccounts = accounts.filter(a => a.isConnected);
|
const connectedAccounts = accounts.filter(a => a.isConnected);
|
||||||
|
const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]);
|
||||||
// Pro shell treats the unified mailbox as a core part of the multi-account
|
// Pro shell treats the unified mailbox as a core part of the multi-account
|
||||||
// UI, so it ignores the user-facing `enableUnifiedMailbox` toggle. The
|
// UI, so it ignores the user-facing `enableUnifiedMailbox` toggle. With a
|
||||||
// 2+ account requirement still applies - with a single account the
|
// single account we still surface unified when the user has opted into
|
||||||
// unified counts would just duplicate that account's inbox.
|
// merging group/shared inboxes — otherwise the counts would just duplicate
|
||||||
const showUnified = (multiAccountMode || enableUnifiedMailbox) && connectedAccounts.length > 1;
|
// the one inbox.
|
||||||
|
const showUnified =
|
||||||
|
(multiAccountMode || enableUnifiedMailbox) &&
|
||||||
|
(connectedAccounts.length > 1 || (includeGroupInUnified && hasGroupInboxes));
|
||||||
const { unifiedCounts } = useEmailStore();
|
const { unifiedCounts } = useEmailStore();
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
||||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { usePolicyStore } from '@/stores/policy-store';
|
import { usePolicyStore } from '@/stores/policy-store';
|
||||||
import { useAccountStore } from '@/stores/account-store';
|
import { useAccountStore } from '@/stores/account-store';
|
||||||
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
|
|
||||||
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||||
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
||||||
@@ -115,9 +117,11 @@ function MailLayoutPreview({
|
|||||||
export function LayoutSettings() {
|
export function LayoutSettings() {
|
||||||
const t = useTranslations('settings.appearance');
|
const t = useTranslations('settings.appearance');
|
||||||
const tEmail = useTranslations('settings.email_behavior');
|
const tEmail = useTranslations('settings.email_behavior');
|
||||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
||||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||||
const accounts = useAccountStore(s => s.accounts);
|
const accounts = useAccountStore(s => s.accounts);
|
||||||
|
const mailboxes = useEmailStore(s => s.mailboxes);
|
||||||
|
const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<SettingsSection title={t('title')} description={t('description')}>
|
||||||
@@ -177,10 +181,11 @@ export function LayoutSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
{accounts.length > 1 && (
|
{(accounts.length > 1 || hasGroupInboxes) && !isSettingHidden('enableUnifiedMailbox') && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t('unified_mailbox.label')}
|
label={t('unified_mailbox.label')}
|
||||||
description={t('unified_mailbox.description')}
|
description={t('unified_mailbox.description')}
|
||||||
|
locked={isSettingLocked('enableUnifiedMailbox')}
|
||||||
>
|
>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={enableUnifiedMailbox}
|
checked={enableUnifiedMailbox}
|
||||||
@@ -189,6 +194,21 @@ export function LayoutSettings() {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{enableUnifiedMailbox && hasGroupInboxes && !isSettingHidden('includeGroupInUnified') && (
|
||||||
|
<div className="ml-4 border-l-2 border-border pl-4 -mt-2">
|
||||||
|
<SettingItem
|
||||||
|
label={t('unified_mailbox.include_group.label')}
|
||||||
|
description={t('unified_mailbox.include_group.description')}
|
||||||
|
locked={isSettingLocked('includeGroupInUnified')}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={includeGroupInUnified}
|
||||||
|
onChange={(v) => updateSetting('includeGroupInUnified', v)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
|
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={proInterface}
|
checked={proInterface}
|
||||||
|
|||||||
+32
-4
@@ -6,6 +6,11 @@ export interface UnifiedAccountClient {
|
|||||||
accountLabel: string;
|
accountLabel: string;
|
||||||
client: IJMAPClient;
|
client: IJMAPClient;
|
||||||
mailboxes: Mailbox[];
|
mailboxes: Mailbox[];
|
||||||
|
// When true, this entry represents a group/shared account owned by
|
||||||
|
// `accountId` but accessed through someone else's `client`. JMAP requests
|
||||||
|
// must use the mailbox's `originalId` and explicitly target this accountId
|
||||||
|
// so the server routes to the owner's data.
|
||||||
|
isShared?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnifiedFetchResult {
|
export interface UnifiedFetchResult {
|
||||||
@@ -60,10 +65,11 @@ export async function fetchUnifiedEmails(
|
|||||||
const mailbox = findMailboxByRole(account.mailboxes, role);
|
const mailbox = findMailboxByRole(account.mailboxes, role);
|
||||||
if (!mailbox) return null;
|
if (!mailbox) return null;
|
||||||
|
|
||||||
|
const { jmapMailboxId, jmapAccountId } = resolveJmapTarget(account, mailbox);
|
||||||
try {
|
try {
|
||||||
const result = await account.client.getEmails(
|
const result = await account.client.getEmails(
|
||||||
mailbox.id,
|
jmapMailboxId,
|
||||||
undefined,
|
jmapAccountId,
|
||||||
limit,
|
limit,
|
||||||
position,
|
position,
|
||||||
);
|
);
|
||||||
@@ -131,7 +137,8 @@ export async function searchUnifiedEmails(
|
|||||||
position: number,
|
position: number,
|
||||||
): Promise<UnifiedFetchResult> {
|
): Promise<UnifiedFetchResult> {
|
||||||
return fanOutUnifiedQuery(accounts, role, async (account, mailbox) => {
|
return fanOutUnifiedQuery(accounts, role, async (account, mailbox) => {
|
||||||
return account.client.searchEmails(query, mailbox.id, undefined, limit, position);
|
const { jmapMailboxId, jmapAccountId } = resolveJmapTarget(account, mailbox);
|
||||||
|
return account.client.searchEmails(query, jmapMailboxId, jmapAccountId, limit, position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +156,31 @@ export async function advancedSearchUnifiedEmails(
|
|||||||
position: number,
|
position: number,
|
||||||
): Promise<UnifiedFetchResult> {
|
): Promise<UnifiedFetchResult> {
|
||||||
return fanOutUnifiedQuery(accounts, role, async (account, mailbox) => {
|
return fanOutUnifiedQuery(accounts, role, async (account, mailbox) => {
|
||||||
return account.client.advancedSearchEmails(filterFor(mailbox.id), undefined, limit, position);
|
const { jmapMailboxId, jmapAccountId } = resolveJmapTarget(account, mailbox);
|
||||||
|
return account.client.advancedSearchEmails(filterFor(jmapMailboxId), jmapAccountId, limit, position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the JMAP-side mailbox id and accountId for a mailbox living inside
|
||||||
|
* a UnifiedAccountClient. For personal-account entries we use the JMAP id as
|
||||||
|
* returned by the primary client; for shared-owner entries the mailbox id is
|
||||||
|
* namespaced (`${ownerId}:${origId}`) so we must use `originalId` and pass the
|
||||||
|
* owner's accountId through the request.
|
||||||
|
*/
|
||||||
|
function resolveJmapTarget(
|
||||||
|
account: UnifiedAccountClient,
|
||||||
|
mailbox: Mailbox,
|
||||||
|
): { jmapMailboxId: string; jmapAccountId: string | undefined } {
|
||||||
|
if (account.isShared) {
|
||||||
|
return {
|
||||||
|
jmapMailboxId: mailbox.originalId ?? mailbox.id,
|
||||||
|
jmapAccountId: account.accountId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { jmapMailboxId: mailbox.id, jmapAccountId: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
async function fanOutUnifiedQuery(
|
async function fanOutUnifiedQuery(
|
||||||
accounts: UnifiedAccountClient[],
|
accounts: UnifiedAccountClient[],
|
||||||
role: UnifiedMailboxRole,
|
role: UnifiedMailboxRole,
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Sjednocená schránka",
|
"label": "Sjednocená schránka",
|
||||||
"description": "Zobrazovat sloučené složky (Doručené, Odeslané atd.) ze všech připojených účtů"
|
"description": "Zobrazovat sloučené složky (Doručené, Odeslané atd.) ze všech připojených účtů",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Zahrnout skupinové schránky",
|
||||||
|
"description": "Zahrnout do sjednoceného zobrazení také sdílené/skupinové schránky."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Barevné ikony postranního panelu",
|
"label": "Barevné ikony postranního panelu",
|
||||||
|
|||||||
@@ -847,7 +847,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Samlet postkasse",
|
"label": "Samlet postkasse",
|
||||||
"description": "Vis samlede mapper (Indbakke, Sendt osv.) på tværs af alle tilknyttede konti"
|
"description": "Vis samlede mapper (Indbakke, Sendt osv.) på tværs af alle tilknyttede konti",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Inkluder gruppepostkasser",
|
||||||
|
"description": "Inkluder også delte/gruppepostkasser i den samlede visning."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Farverige sidepane-ikoner",
|
"label": "Farverige sidepane-ikoner",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Gemeinsames Postfach",
|
"label": "Gemeinsames Postfach",
|
||||||
"description": "Kombinierte Ordner (Posteingang, Gesendet usw.) für alle verbundenen Konten anzeigen"
|
"description": "Kombinierte Ordner (Posteingang, Gesendet usw.) für alle verbundenen Konten anzeigen",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Gruppenpostfächer einbeziehen",
|
||||||
|
"description": "Gemeinsam genutzte/Gruppenpostfächer ebenfalls in die vereinheitlichte Ansicht aufnehmen."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Farbige Seitenleistensymbole",
|
"label": "Farbige Seitenleistensymbole",
|
||||||
|
|||||||
@@ -847,7 +847,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Unified Mailbox",
|
"label": "Unified Mailbox",
|
||||||
"description": "Show combined folders (Inbox, Sent, etc.) across all connected accounts"
|
"description": "Show combined folders (Inbox, Sent, etc.) across all connected accounts",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Include group inboxes",
|
||||||
|
"description": "Also merge shared/group inboxes into the unified view."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Colorful Sidebar Icons",
|
"label": "Colorful Sidebar Icons",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Buzón unificado",
|
"label": "Buzón unificado",
|
||||||
"description": "Mostrar carpetas combinadas (Entrada, Enviados, etc.) de todas las cuentas conectadas"
|
"description": "Mostrar carpetas combinadas (Entrada, Enviados, etc.) de todas las cuentas conectadas",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Incluir buzones de grupo",
|
||||||
|
"description": "Incluir también los buzones compartidos o de grupo en la vista unificada."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Iconos de barra lateral a color",
|
"label": "Iconos de barra lateral a color",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Boîte aux lettres unifiée",
|
"label": "Boîte aux lettres unifiée",
|
||||||
"description": "Afficher les dossiers combinés (Réception, Envoyés, etc.) de tous les comptes connectés"
|
"description": "Afficher les dossiers combinés (Réception, Envoyés, etc.) de tous les comptes connectés",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Inclure les boîtes de groupe",
|
||||||
|
"description": "Inclure également les boîtes partagées ou de groupe dans la vue unifiée."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Icônes colorées dans la barre latérale",
|
"label": "Icônes colorées dans la barre latérale",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Casella di posta unificata",
|
"label": "Casella di posta unificata",
|
||||||
"description": "Mostra le cartelle combinate (Posta in arrivo, Inviati, ecc.) di tutti gli account collegati"
|
"description": "Mostra le cartelle combinate (Posta in arrivo, Inviati, ecc.) di tutti gli account collegati",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Includi le caselle di gruppo",
|
||||||
|
"description": "Includi anche le caselle condivise o di gruppo nella vista unificata."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Icone colorate nella barra laterale",
|
"label": "Icone colorate nella barra laterale",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "統合メールボックス",
|
"label": "統合メールボックス",
|
||||||
"description": "接続されたすべてのアカウントの統合フォルダ(受信トレイ、送信済みなど)を表示"
|
"description": "接続されたすべてのアカウントの統合フォルダ(受信トレイ、送信済みなど)を表示",
|
||||||
|
"include_group": {
|
||||||
|
"label": "グループ受信トレイを含める",
|
||||||
|
"description": "共有/グループ受信トレイも統合ビューに含めます。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "カラフルなサイドバーアイコン",
|
"label": "カラフルなサイドバーアイコン",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "통합 메일함",
|
"label": "통합 메일함",
|
||||||
"description": "연결된 모든 계정의 통합 폴더(받은편지함, 보낸편지함 등)를 표시합니다"
|
"description": "연결된 모든 계정의 통합 폴더(받은편지함, 보낸편지함 등)를 표시합니다",
|
||||||
|
"include_group": {
|
||||||
|
"label": "그룹 받은편지함 포함",
|
||||||
|
"description": "공유/그룹 받은편지함도 통합 보기에 포함합니다."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "컬러풀한 사이드바 아이콘",
|
"label": "컬러풀한 사이드바 아이콘",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Apvienotā pastkaste",
|
"label": "Apvienotā pastkaste",
|
||||||
"description": "Rādīt apvienotās mapes (Iesūtne, Nosūtītie u.c.) no visiem pievienotajiem kontiem"
|
"description": "Rādīt apvienotās mapes (Iesūtne, Nosūtītie u.c.) no visiem pievienotajiem kontiem",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Iekļaut grupas pastkastes",
|
||||||
|
"description": "Iekļaut apvienotajā skatā arī koplietotās/grupas pastkastes."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Krāsainas sānjoslas ikonas",
|
"label": "Krāsainas sānjoslas ikonas",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Gecombineerd postvak",
|
"label": "Gecombineerd postvak",
|
||||||
"description": "Gecombineerde mappen (Postvak IN, Verzonden, enz.) van alle verbonden accounts weergeven"
|
"description": "Gecombineerde mappen (Postvak IN, Verzonden, enz.) van alle verbonden accounts weergeven",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Groepspostvakken meenemen",
|
||||||
|
"description": "Gedeelde/groepspostvakken ook in de gecombineerde weergave opnemen."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Gekleurde zijbalkpictogrammen",
|
"label": "Gekleurde zijbalkpictogrammen",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Wspólna skrzynka",
|
"label": "Wspólna skrzynka",
|
||||||
"description": "Wyświetlaj połączone foldery (Odebrane, Wysłane itp.) ze wszystkich połączonych kont"
|
"description": "Wyświetlaj połączone foldery (Odebrane, Wysłane itp.) ze wszystkich połączonych kont",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Uwzględnij skrzynki grupowe",
|
||||||
|
"description": "Dodaj również udostępnione/grupowe skrzynki do widoku wspólnego."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Kolorowe ikony paska bocznego",
|
"label": "Kolorowe ikony paska bocznego",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Caixa de correio unificada",
|
"label": "Caixa de correio unificada",
|
||||||
"description": "Mostrar pastas combinadas (Entrada, Enviados, etc.) de todas as contas conectadas"
|
"description": "Mostrar pastas combinadas (Entrada, Enviados, etc.) de todas as contas conectadas",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Incluir caixas de grupo",
|
||||||
|
"description": "Incluir também as caixas partilhadas ou de grupo na vista unificada."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Ícones coloridos na barra lateral",
|
"label": "Ícones coloridos na barra lateral",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Общий почтовый ящик",
|
"label": "Общий почтовый ящик",
|
||||||
"description": "Показывать объединённые папки (Входящие, Отправленные и др.) для всех подключённых аккаунтов"
|
"description": "Показывать объединённые папки (Входящие, Отправленные и др.) для всех подключённых аккаунтов",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Включать групповые ящики",
|
||||||
|
"description": "Также объединять общие/групповые ящики в едином представлении."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Цветные значки боковой панели",
|
"label": "Цветные значки боковой панели",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Birleşik Posta Kutusu",
|
"label": "Birleşik Posta Kutusu",
|
||||||
"description": "Bağlı tüm hesaplardaki birleşik klasörleri (Gelen Kutusu, Gönderilenler vb.) göster"
|
"description": "Bağlı tüm hesaplardaki birleşik klasörleri (Gelen Kutusu, Gönderilenler vb.) göster",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Grup gelen kutularını dahil et",
|
||||||
|
"description": "Paylaşılan/grup gelen kutularını da birleşik görünüme dahil et."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Renkli Kenar Çubuğu Simgeleri",
|
"label": "Renkli Kenar Çubuğu Simgeleri",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Спільна поштова скринька",
|
"label": "Спільна поштова скринька",
|
||||||
"description": "Показувати об'єднані папки (Вхідні, Надіслані тощо) для всіх підключених облікових записів"
|
"description": "Показувати об'єднані папки (Вхідні, Надіслані тощо) для всіх підключених облікових записів",
|
||||||
|
"include_group": {
|
||||||
|
"label": "Включати групові скриньки",
|
||||||
|
"description": "Також об'єднувати спільні/групові скриньки у спільному перегляді."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "Кольорові значки бічної панелі",
|
"label": "Кольорові значки бічної панелі",
|
||||||
|
|||||||
@@ -844,7 +844,11 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "统一邮箱",
|
"label": "统一邮箱",
|
||||||
"description": "显示所有已连接账户的合并文件夹(收件箱、已发送等)"
|
"description": "显示所有已连接账户的合并文件夹(收件箱、已发送等)",
|
||||||
|
"include_group": {
|
||||||
|
"label": "包含群组收件箱",
|
||||||
|
"description": "在统一视图中也合并共享/群组收件箱。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
"label": "彩色侧边栏图标",
|
"label": "彩色侧边栏图标",
|
||||||
|
|||||||
+40
-6
@@ -251,8 +251,16 @@ function resolveActionMailboxes(): Mailbox[] {
|
|||||||
* fresh mailbox list so the helpers can resolve the role mailbox per account.
|
* fresh mailbox list so the helpers can resolve the role mailbox per account.
|
||||||
* Accounts whose mailbox fetch fails are skipped - the unified result will
|
* Accounts whose mailbox fetch fails are skipped - the unified result will
|
||||||
* surface that in its per-account error map.
|
* surface that in its per-account error map.
|
||||||
|
*
|
||||||
|
* When `includeGroup` is true, also emits one synthetic entry per shared
|
||||||
|
* owner account reachable through each logged-in client. The shared entries
|
||||||
|
* are flagged with `isShared: true` so `lib/unified-mailbox.ts` routes JMAP
|
||||||
|
* requests via `originalId` + owner accountId.
|
||||||
*/
|
*/
|
||||||
async function buildUnifiedAccountClients(): Promise<UnifiedAccountClient[]> {
|
export async function buildUnifiedAccountClients(
|
||||||
|
opts: { includeGroup?: boolean } = {},
|
||||||
|
): Promise<UnifiedAccountClient[]> {
|
||||||
|
const { includeGroup = false } = opts;
|
||||||
const authAccounts = useAccountStore.getState().accounts.filter((a) => a.isConnected);
|
const authAccounts = useAccountStore.getState().accounts.filter((a) => a.isConnected);
|
||||||
const allClients = useAuthStore.getState().getAllConnectedClients();
|
const allClients = useAuthStore.getState().getAllConnectedClients();
|
||||||
const built: UnifiedAccountClient[] = [];
|
const built: UnifiedAccountClient[] = [];
|
||||||
@@ -260,8 +268,31 @@ async function buildUnifiedAccountClients(): Promise<UnifiedAccountClient[]> {
|
|||||||
const c = allClients.get(a.id);
|
const c = allClients.get(a.id);
|
||||||
if (!c) continue;
|
if (!c) continue;
|
||||||
try {
|
try {
|
||||||
const mailboxes = await c.getMailboxes();
|
const mailboxes = includeGroup ? await c.getAllMailboxes() : await c.getMailboxes();
|
||||||
built.push({ accountId: a.id, accountLabel: a.label || a.email, client: c, mailboxes });
|
const ownMailboxes = includeGroup
|
||||||
|
? mailboxes.filter((m) => !m.isShared)
|
||||||
|
: mailboxes;
|
||||||
|
built.push({ accountId: a.id, accountLabel: a.label || a.email, client: c, mailboxes: ownMailboxes, isShared: false });
|
||||||
|
|
||||||
|
if (includeGroup) {
|
||||||
|
const sharedByOwner = new Map<string, Mailbox[]>();
|
||||||
|
for (const m of mailboxes) {
|
||||||
|
if (!m.isShared || !m.accountId || m.accountId === a.id) continue;
|
||||||
|
const list = sharedByOwner.get(m.accountId) ?? [];
|
||||||
|
list.push(m);
|
||||||
|
sharedByOwner.set(m.accountId, list);
|
||||||
|
}
|
||||||
|
for (const [ownerId, ownerMailboxes] of sharedByOwner) {
|
||||||
|
const label = ownerMailboxes.find((m) => m.accountName)?.accountName || ownerId;
|
||||||
|
built.push({
|
||||||
|
accountId: ownerId,
|
||||||
|
accountLabel: label,
|
||||||
|
client: c,
|
||||||
|
mailboxes: ownerMailboxes,
|
||||||
|
isShared: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* skip account on mailbox fetch failure */
|
/* skip account on mailbox fetch failure */
|
||||||
}
|
}
|
||||||
@@ -583,8 +614,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
set({ isLoadingMore: true, error: null });
|
set({ isLoadingMore: true, error: null });
|
||||||
try {
|
try {
|
||||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||||
|
const includeGroup = useSettingsStore.getState().includeGroupInUnified;
|
||||||
const position = emails.length;
|
const position = emails.length;
|
||||||
const built = await buildUnifiedAccountClients();
|
const built = await buildUnifiedAccountClients({ includeGroup });
|
||||||
const { searchFilters } = get();
|
const { searchFilters } = get();
|
||||||
const hasFilters = !isFilterEmpty(searchFilters);
|
const hasFilters = !isFilterEmpty(searchFilters);
|
||||||
const result = hasFilters
|
const result = hasFilters
|
||||||
@@ -1261,7 +1293,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||||
|
|
||||||
if (isUnifiedView && unifiedRole) {
|
if (isUnifiedView && unifiedRole) {
|
||||||
const built = await buildUnifiedAccountClients();
|
const includeGroup = useSettingsStore.getState().includeGroupInUnified;
|
||||||
|
const built = await buildUnifiedAccountClients({ includeGroup });
|
||||||
const result = await searchUnifiedEmails(built, unifiedRole, query, emailsPerPage, 0);
|
const result = await searchUnifiedEmails(built, unifiedRole, query, emailsPerPage, 0);
|
||||||
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
|
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
|
||||||
set({
|
set({
|
||||||
@@ -1327,7 +1360,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||||
|
|
||||||
if (isUnifiedView && unifiedRole) {
|
if (isUnifiedView && unifiedRole) {
|
||||||
const built = await buildUnifiedAccountClients();
|
const includeGroup = useSettingsStore.getState().includeGroupInUnified;
|
||||||
|
const built = await buildUnifiedAccountClients({ includeGroup });
|
||||||
const result = await advancedSearchUnifiedEmails(
|
const result = await advancedSearchUnifiedEmails(
|
||||||
built,
|
built,
|
||||||
unifiedRole,
|
unifiedRole,
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ interface SettingsState {
|
|||||||
|
|
||||||
// Unified Mailbox
|
// Unified Mailbox
|
||||||
enableUnifiedMailbox: boolean;
|
enableUnifiedMailbox: boolean;
|
||||||
|
includeGroupInUnified: boolean;
|
||||||
|
|
||||||
// Email Display
|
// Email Display
|
||||||
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
|
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
|
||||||
@@ -375,6 +376,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
|
|
||||||
// Unified Mailbox
|
// Unified Mailbox
|
||||||
enableUnifiedMailbox: false,
|
enableUnifiedMailbox: false,
|
||||||
|
includeGroupInUnified: false,
|
||||||
|
|
||||||
// Email Display
|
// Email Display
|
||||||
disableThreading: false,
|
disableThreading: false,
|
||||||
@@ -543,6 +545,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
// proInterface is intentionally omitted - it's a per-device choice
|
// proInterface is intentionally omitted - it's a per-device choice
|
||||||
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
|
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
|
||||||
enableUnifiedMailbox: state.enableUnifiedMailbox,
|
enableUnifiedMailbox: state.enableUnifiedMailbox,
|
||||||
|
includeGroupInUnified: state.includeGroupInUnified,
|
||||||
senderFavicons: state.senderFavicons,
|
senderFavicons: state.senderFavicons,
|
||||||
showAvatarsInJunk: state.showAvatarsInJunk,
|
showAvatarsInJunk: state.showAvatarsInJunk,
|
||||||
colorfulSidebarIcons: state.colorfulSidebarIcons,
|
colorfulSidebarIcons: state.colorfulSidebarIcons,
|
||||||
|
|||||||
Reference in New Issue
Block a user