From 13dcbdc0869a5bd30d855d2c4da7e4c30f05edc8 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 31 Oct 2026 15:52:37 +0100
Subject: [PATCH] fix: hide every archive action when the organisation turns
archiving off
---
src/components/settings/DownloadsSettings.tsx | 9 +++-
src/screens/EmailListScreen.tsx | 10 +++--
src/screens/EmailThreadScreen.tsx | 12 +++--
src/screens/UnifiedInboxScreen.tsx | 12 +++--
.../__tests__/archive-gate-wiring.test.ts | 45 +++++++++++++++++++
5 files changed, 75 insertions(+), 13 deletions(-)
create mode 100644 src/screens/__tests__/archive-gate-wiring.test.ts
diff --git a/src/components/settings/DownloadsSettings.tsx b/src/components/settings/DownloadsSettings.tsx
index a6aaad5f..c8fd1d97 100644
--- a/src/components/settings/DownloadsSettings.tsx
+++ b/src/components/settings/DownloadsSettings.tsx
@@ -17,6 +17,7 @@ import {
type EmailFilenameOptions,
} from '../../lib/download-filename';
import { useLocaleStore } from '../../stores/locale-store';
+import { usePolicyFeature } from '../../stores/policy-store';
const SAMPLE = buildSampleEmail();
const SAMPLE_ATTACHMENT = { name: 'Rechnung 2026.pdf', type: 'application/pdf' };
@@ -34,7 +35,11 @@ export function DownloadsSettings() {
const spaceReplacement = useSettingsStore((s) => s.exportSpaceReplacement);
const lowercase = useSettingsStore((s) => s.exportLowercase);
const stripDiacritics = useSettingsStore((s) => s.exportStripDiacritics);
- const postExportAction = useSettingsStore((s) => s.postExportAction);
+ // Without the organisation's Archive action there is no "move to archive"
+ // after an export (webmail downloads-settings.tsx).
+ const archiveEnabled = usePolicyFeature('archiveActionEnabled');
+ const storedPostExportAction = useSettingsStore((s) => s.postExportAction);
+ const postExportAction = archiveEnabled || storedPostExportAction !== 'archive' ? storedPostExportAction : 'keep';
useEffect(() => { if (!hydrated) void hydrate(); }, [hydrated, hydrate]);
@@ -136,7 +141,7 @@ export function DownloadsSettings() {
onChange={(v) => update('postExportAction', v as PostExportAction)}
options={[
{ value: 'keep', label: t('settings.downloads.after_export.keep', "Keep in mailbox") },
- { value: 'archive', label: t('settings.downloads.after_export.archive', "Move to archive") },
+ ...(archiveEnabled ? [{ value: 'archive', label: t('settings.downloads.after_export.archive', "Move to archive") }] : []),
{ value: 'trash', label: t('settings.downloads.after_export.trash', "Move to trash") },
]}
/>
diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx
index ae61ec09..a72324f7 100644
--- a/src/screens/EmailListScreen.tsx
+++ b/src/screens/EmailListScreen.tsx
@@ -41,6 +41,8 @@ import {
type EmailFilters,
} from '../stores/email-store';
import { useSettingsStore, type SwipeAction, type SwipeMode } from '../stores/settings-store';
+import { usePolicyFeature } from '../stores/policy-store';
+import { swipeActionShown } from '../lib/swipe-actions';
import { useKeywordsStore, currentTagsLogin, type KeywordDef } from '../stores/keywords-store';
import { mergeSharedTags } from '../lib/shared-tags';
import { useBackWhileFocused } from '../lib/use-back-while-focused';
@@ -538,8 +540,10 @@ export default function EmailListScreen({
const retryFailedOps = useOutboxStore((s) => s.retryFailed);
const discardFailedOps = useOutboxStore((s) => s.discardFailed);
- const swipeLeftAction = useSettingsStore((s) => s.swipeLeftAction);
- const swipeRightAction = useSettingsStore((s) => s.swipeRightAction);
+ // The organisation's Archive gate: a stored Archive swipe does nothing while it is off.
+ const archiveEnabled = usePolicyFeature('archiveActionEnabled');
+ const swipeLeftAction = swipeActionShown(useSettingsStore((s) => s.swipeLeftAction), archiveEnabled);
+ const swipeRightAction = swipeActionShown(useSettingsStore((s) => s.swipeRightAction), archiveEnabled);
const swipeMode = useSettingsStore((s) => s.swipeMode);
const showPreview = useSettingsStore((s) => s.showPreview);
const disableThreading = useSettingsStore((s) => s.disableThreading);
@@ -1041,7 +1045,7 @@ export default function EmailListScreen({
};
const canArchiveSelection =
- archiveMailboxId != null && currentMailboxId !== archiveMailboxId;
+ archiveEnabled && archiveMailboxId != null && currentMailboxId !== archiveMailboxId;
const canSpamSelection =
currentRole !== 'sent' && currentRole !== 'drafts' && (inJunk || junkMailboxId != null);
diff --git a/src/screens/EmailThreadScreen.tsx b/src/screens/EmailThreadScreen.tsx
index 04707a78..3aa9d2b9 100644
--- a/src/screens/EmailThreadScreen.tsx
+++ b/src/screens/EmailThreadScreen.tsx
@@ -33,10 +33,11 @@ import { useEmailStore, listRowsOfAccount } from '../stores/email-store';
import { toast } from '../stores/toast-store';
import {
useSettingsStore,
- normalizeBottomQuickActions,
REPLY_QUICK_ACTIONS,
type QuickAction,
} from '../stores/settings-store';
+import { usePolicyFeature } from '../stores/policy-store';
+import { bottomQuickActionsFor } from '../lib/swipe-actions';
import { patchKeywordsForEmails } from '../api/email';
import {
loadDetail, loadDetails, loadThread, patchDetail, peekDetail, peekRow, peekThread, subscribeEmailCache,
@@ -356,9 +357,12 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr
const markAsReadDelay = useSettingsStore((s) => s.markAsReadDelay);
const bottomQuickActionsRaw = useSettingsStore((s) => s.bottomQuickActions);
+ // The organisation's Archive gate (webmail lib/archive-gate.ts): off, a
+ // stored Archive quick action gives way and no Archive button shows.
+ const archiveEnabled = usePolicyFeature('archiveActionEnabled');
const bottomActions = React.useMemo(
- () => normalizeBottomQuickActions(bottomQuickActionsRaw),
- [bottomQuickActionsRaw],
+ () => bottomQuickActionsFor(bottomQuickActionsRaw, archiveEnabled),
+ [bottomQuickActionsRaw, archiveEnabled],
);
// Any reply-family action the user pulled out of the bottom bar is surfaced
// in the top toolbar so it stays reachable.
@@ -488,7 +492,7 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr
[scopedMailboxes, email, currentMailboxId],
);
const isInTrash = !!(trashMailbox && sourceMailbox?.id === trashMailbox.id);
- const canArchive = !!archiveMailbox && sourceMailbox?.id !== archiveMailbox.id;
+ const canArchive = archiveEnabled && !!archiveMailbox && sourceMailbox?.id !== archiveMailbox.id;
// Not spam files back into the Inbox, so it needs one.
const canToggleSpam = isInJunk ? !!inboxMailbox : !!junkMailbox;
diff --git a/src/screens/UnifiedInboxScreen.tsx b/src/screens/UnifiedInboxScreen.tsx
index 274bcfbf..86ac0825 100644
--- a/src/screens/UnifiedInboxScreen.tsx
+++ b/src/screens/UnifiedInboxScreen.tsx
@@ -22,6 +22,8 @@ import { SearchSnippetText } from '../components/email/SearchSnippetText';
import { useAccountStore } from '../stores/account-store';
import { useAuthStore } from '../stores/auth-store';
import { useSettingsStore, type SwipeAction } from '../stores/settings-store';
+import { usePolicyFeature } from '../stores/policy-store';
+import { swipeActionShown } from '../lib/swipe-actions';
import { useLocaleStore } from '../stores/locale-store';
import { formatListDate } from '../lib/date-format';
import { singleLine } from '../lib/single-line';
@@ -79,8 +81,10 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
const crossAccount = useSettingsStore((s) => s.unifiedCrossAccount);
const deleteAction = useSettingsStore((s) => s.deleteAction);
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
- const swipeLeftAction = useSettingsStore((s) => s.swipeLeftAction);
- const swipeRightAction = useSettingsStore((s) => s.swipeRightAction);
+ // The organisation's Archive gate: a stored Archive swipe does nothing while it is off.
+ const archiveEnabled = usePolicyFeature('archiveActionEnabled');
+ const swipeLeftAction = swipeActionShown(useSettingsStore((s) => s.swipeLeftAction), archiveEnabled);
+ const swipeRightAction = swipeActionShown(useSettingsStore((s) => s.swipeRightAction), archiveEnabled);
const swipeMode = useSettingsStore((s) => s.swipeMode);
const showAvatarsInJunk = useSettingsStore((s) => s.showAvatarsInJunk);
const locale = useLocaleStore((s) => s.locale);
@@ -303,7 +307,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
const handleSwipe = (email: UnifiedEmail, action: SwipeAction) => {
switch (action) {
- case 'archive': if (role !== 'archive') void archive([email]); break;
+ case 'archive': if (archiveEnabled && role !== 'archive') void archive([email]); break;
case 'delete': void remove([email]); break;
case 'spam': if (role !== 'sent' && role !== 'drafts') spam([email]); break;
case 'read': setRead([email], !email.keywords?.$seen); break;
@@ -482,7 +486,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
{role === 'junk' ? : }
)}
- {role !== 'archive' && (
+ {archiveEnabled && role !== 'archive' && (
{ void archive(selectedEmails).then((done) => { if (done) clearSelection(); }); }}
style={styles.headerBtn}
diff --git a/src/screens/__tests__/archive-gate-wiring.test.ts b/src/screens/__tests__/archive-gate-wiring.test.ts
new file mode 100644
index 00000000..fbaa3b50
--- /dev/null
+++ b/src/screens/__tests__/archive-gate-wiring.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+
+// The organisation's archiveActionEnabled gate (webmail lib/archive-gate.ts,
+// email-list.tsx, email-viewer.tsx, downloads-settings.tsx): off, no swipe,
+// selection bar, reader toolbar, quick action, more-menu or after-export
+// Archive. No React Native renderer in the test env, so this checks the
+// wiring; the helpers have unit tests in lib/__tests__/swipe-actions.test.ts.
+
+const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
+
+describe('archive gate wiring', () => {
+ it('drops a stored Archive swipe and the selection Archive in the folder list', () => {
+ const src = read('screens/EmailListScreen.tsx');
+ expect(src).toMatch(/const archiveEnabled = usePolicyFeature\('archiveActionEnabled'\);/);
+ expect(src).toMatch(/const swipeLeftAction = swipeActionShown\(useSettingsStore\(\(s\) => s\.swipeLeftAction\), archiveEnabled\);/);
+ expect(src).toMatch(/const swipeRightAction = swipeActionShown\(useSettingsStore\(\(s\) => s\.swipeRightAction\), archiveEnabled\);/);
+ expect(src).toMatch(/const canArchiveSelection =\s+archiveEnabled && /);
+ });
+
+ it('drops them in the unified inbox', () => {
+ const src = read('screens/UnifiedInboxScreen.tsx');
+ expect(src).toMatch(/const archiveEnabled = usePolicyFeature\('archiveActionEnabled'\);/);
+ expect(src).toMatch(/const swipeLeftAction = swipeActionShown\(useSettingsStore\(\(s\) => s\.swipeLeftAction\), archiveEnabled\);/);
+ expect(src).toMatch(/const swipeRightAction = swipeActionShown\(useSettingsStore\(\(s\) => s\.swipeRightAction\), archiveEnabled\);/);
+ expect(src).toMatch(/\{archiveEnabled && role !== 'archive' && \(\s+ \{ void archive\(selectedEmails\)/);
+ expect(src).toMatch(/case 'archive': if \(archiveEnabled && role !== 'archive'\)/);
+ });
+
+ it('drops the reader Archive button, quick action and menu entry', () => {
+ const src = read('screens/EmailThreadScreen.tsx');
+ expect(src).toMatch(/bottomQuickActionsFor\(bottomQuickActionsRaw, archiveEnabled\)/);
+ expect(src).not.toMatch(/normalizeBottomQuickActions\(bottomQuickActionsRaw\)/);
+ // canArchive feeds the toolbar button, the quick action, the more menu
+ // and the after-export move.
+ expect(src).toMatch(/const canArchive = archiveEnabled && !!archiveMailbox/);
+ });
+
+ it('offers no move to archive after an export', () => {
+ const src = read('components/settings/DownloadsSettings.tsx');
+ expect(src).toMatch(/archiveEnabled \|\| storedPostExportAction !== 'archive' \? storedPostExportAction : 'keep'/);
+ expect(src).toMatch(/\.\.\.\(archiveEnabled \? \[\{ value: 'archive'/);
+ });
+});