fix: hide every archive action when the organisation turns archiving off

This commit is contained in:
Linus Rath
2026-10-31 15:52:37 +01:00
parent 6b7e752c4b
commit 13dcbdc086
5 changed files with 75 additions and 13 deletions
@@ -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") },
]}
/>
+7 -3
View File
@@ -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);
+8 -4
View File
@@ -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;
+8 -4
View File
@@ -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' ? <ShieldCheck size={20} color={c.text} /> : <ShieldAlert size={20} color={c.text} />}
</Pressable>
)}
{role !== 'archive' && (
{archiveEnabled && role !== 'archive' && (
<Pressable
onPress={() => { void archive(selectedEmails).then((done) => { if (done) clearSelection(); }); }}
style={styles.headerBtn}
@@ -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+<Pressable\s+onPress=\{\(\) => \{ 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'/);
});
});