feat: take archive out of the swipe and quick action settings when the organisation turns it off

This commit is contained in:
Linus Rath
2026-10-31 15:34:06 +01:00
parent df1e211780
commit efb92593b2
5 changed files with 111 additions and 41 deletions
+12 -8
View File
@@ -7,7 +7,6 @@ import { useColors } from '../../theme/colors';
import { useLocaleStore } from '../../stores/locale-store';
import {
useSettingsStore,
normalizeBottomQuickActions,
REPLY_QUICK_ACTIONS,
ALL_QUICK_ACTIONS,
type SwipeAction,
@@ -16,7 +15,8 @@ import {
} from '../../stores/settings-store';
import { useEmailStore } from '../../stores/email-store';
import { useKeywordsStore } from '../../stores/keywords-store';
import { useSettingLocked } from '../../stores/policy-store';
import { usePolicyFeature, useSettingLocked } from '../../stores/policy-store';
import { bottomQuickActionsFor, swipeActionShown, withoutArchive } from '../../lib/swipe-actions';
import {
ORDER_PRESETS, detectPreset, presetLevels, sanitizeSortLevels,
type OrderPreset, type MessageListOrderScope,
@@ -74,12 +74,16 @@ export function LayoutSettings() {
const hydrated = useSettingsStore((s) => s.hydrated);
const hydrate = useSettingsStore((s) => s.hydrate);
const t = useLocaleStore((s) => s.t);
const SWIPE_OPTIONS = React.useMemo(() => swipeOptions(t), [t]);
// With the organisation's Archive action off, Archive is no swipe or quick
// action, and a stored one shows as what it does (webmail
// message-list-settings.tsx).
const archiveEnabled = usePolicyFeature('archiveActionEnabled');
const SWIPE_OPTIONS = React.useMemo(() => withoutArchive(swipeOptions(t), archiveEnabled), [t, archiveEnabled]);
const SWIPE_MODE_OPTIONS = React.useMemo(() => swipeModeOptions(t), [t]);
const QUICK_ACTION_LABELS = React.useMemo(() => quickActionLabels(t), [t]);
const QUICK_ACTION_OPTIONS = React.useMemo(
() => ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })),
[QUICK_ACTION_LABELS],
() => withoutArchive(ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })), archiveEnabled),
[QUICK_ACTION_LABELS, archiveEnabled],
);
useEffect(() => { if (!hydrated) void hydrate(); }, [hydrated, hydrate]);
@@ -125,7 +129,7 @@ export function LayoutSettings() {
{ value: 'all', label: t('settings.message_list_order.scope_all', 'All folders') },
];
const bottomActions = normalizeBottomQuickActions(bottomQuickActionsRaw);
const bottomActions = bottomQuickActionsFor(bottomQuickActionsRaw, archiveEnabled);
// Pick an action for a bar slot. If the chosen action already occupies
// another slot, swap the two so the three slots stay unique.
const setQuickActionSlot = (index: number, value: QuickAction) => {
@@ -242,7 +246,7 @@ export function LayoutSettings() {
{t('settings.email_behavior.swipe_right_action.description_mobile', "Action when you drag a row from its left edge towards the right.")}
</Text>
<RadioGroup
value={swipeRightAction}
value={swipeActionShown(swipeRightAction, archiveEnabled)}
onChange={(v) => setSwipeRightAction(v as SwipeAction)}
options={SWIPE_OPTIONS}
/>
@@ -257,7 +261,7 @@ export function LayoutSettings() {
{t('settings.email_behavior.swipe_left_action.description_mobile', "Action when you drag a row from its right edge towards the left.")}
</Text>
<RadioGroup
value={swipeLeftAction}
value={swipeActionShown(swipeLeftAction, archiveEnabled)}
onChange={(v) => setSwipeLeftAction(v as SwipeAction)}
options={SWIPE_OPTIONS}
/>
+39 -33
View File
@@ -18,6 +18,7 @@ import { useEmailStore } from '../../stores/email-store';
import { archiveEmails, queryEmails, getEmails } from '../../api/email';
import { ownMailboxes } from '../../lib/mailbox-tree';
import { useLocaleStore } from '../../stores/locale-store';
import { usePolicyFeature } from '../../stores/policy-store';
export function ReadingSettings() {
const c = useColors();
@@ -28,6 +29,9 @@ export function ReadingSettings() {
const update = useSettingsStore((s) => s.updateSetting);
const archiveMode = useSettingsStore((s) => s.archiveMode);
// Where archived mail goes means nothing with the organisation's Archive
// action off (webmail message-list-settings.tsx).
const archiveEnabled = usePolicyFeature('archiveActionEnabled');
const markAsReadDelay = useSettingsStore((s) => s.markAsReadDelay);
const deleteAction = useSettingsStore((s) => s.deleteAction);
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
@@ -147,40 +151,42 @@ export function ReadingSettings() {
<View style={styles.divider} />
</View>
<View style={styles.group}>
<SettingItem label={t('settings.email_behavior.archive_mode.label', "Archive in")} description={t('settings.email_behavior.archive_mode.description', "How to organize emails when archiving")} noBorder />
<Select
value={archiveMode}
onChange={(v) => update('archiveMode', v as ArchiveMode)}
options={[
{ value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") },
{ value: 'year', label: t('settings.email_behavior.archive_mode.year', "A folder per year") },
{ value: 'month', label: t('settings.email_behavior.archive_mode.month', "A folder per month") },
]}
/>
{archiveMode !== 'single' && (
<>
<Pressable
style={[styles.inlineBtn, reorganizing && styles.inlineBtnDisabled]}
onPress={reorganizing ? undefined : handleReorganizeArchive}
disabled={reorganizing}
>
{reorganizing ? (
<ActivityIndicator size="small" color={c.text} />
) : (
<FolderSync size={14} color={c.text} />
{archiveEnabled && (
<View style={styles.group}>
<SettingItem label={t('settings.email_behavior.archive_mode.label', "Archive in")} description={t('settings.email_behavior.archive_mode.description', "How to organize emails when archiving")} noBorder />
<Select
value={archiveMode}
onChange={(v) => update('archiveMode', v as ArchiveMode)}
options={[
{ value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") },
{ value: 'year', label: t('settings.email_behavior.archive_mode.year', "A folder per year") },
{ value: 'month', label: t('settings.email_behavior.archive_mode.month', "A folder per month") },
]}
/>
{archiveMode !== 'single' && (
<>
<Pressable
style={[styles.inlineBtn, reorganizing && styles.inlineBtnDisabled]}
onPress={reorganizing ? undefined : handleReorganizeArchive}
disabled={reorganizing}
>
{reorganizing ? (
<ActivityIndicator size="small" color={c.text} />
) : (
<FolderSync size={14} color={c.text} />
)}
<Text style={styles.inlineBtnText}>
{reorganizing ? t('settings.email_behavior.archive_mode.reorganizing', "Reorganizing…") : t('settings.email_behavior.archive_mode.reorganize', "Reorganize existing archive")}
</Text>
</Pressable>
{reorganizeResult && (
<Text style={styles.reorganizeResult}>{reorganizeResult}</Text>
)}
<Text style={styles.inlineBtnText}>
{reorganizing ? t('settings.email_behavior.archive_mode.reorganizing', "Reorganizing…") : t('settings.email_behavior.archive_mode.reorganize', "Reorganize existing archive")}
</Text>
</Pressable>
{reorganizeResult && (
<Text style={styles.reorganizeResult}>{reorganizeResult}</Text>
)}
</>
)}
<View style={styles.divider} />
</View>
</>
)}
<View style={styles.divider} />
</View>
)}
<SettingItem label={t('settings.email_behavior.permanently_delete_junk.label', "Permanently Delete Junk")} description={t('settings.email_behavior.permanently_delete_junk.description', "Permanently delete emails from the Junk/Spam folder instead of moving them to Trash")}>
<ToggleSwitch checked={permanentlyDeleteJunk} onChange={(v) => update('permanentlyDeleteJunk', v)} />
@@ -49,4 +49,18 @@ describe('organisation policy on the settings pages', () => {
expect(page).toContain("const exportAllowed = usePolicyFeature('settingsExportEnabled');");
expect(page).toMatch(/\{exportAllowed && \(\s*<SettingItem\s+label=\{t\('settings\.backup\.export_import'[\s\S]*?handleExport[\s\S]*?handleImport[\s\S]*?<\/SettingItem>\s*\)\}/);
});
it('takes Archive out of the swipe and quick actions when the organisation turns it off', () => {
const layout = settings('LayoutSettings');
expect(layout).toContain("const archiveEnabled = usePolicyFeature('archiveActionEnabled');");
expect(layout).toContain('withoutArchive(swipeOptions(t), archiveEnabled)');
expect(layout).toContain('value={swipeActionShown(swipeRightAction, archiveEnabled)}');
expect(layout).toContain('value={swipeActionShown(swipeLeftAction, archiveEnabled)}');
expect(layout).toMatch(/withoutArchive\(ALL_QUICK_ACTIONS\.map\([\s\S]*?\), archiveEnabled\)/);
expect(layout).toContain('const bottomActions = bottomQuickActionsFor(bottomQuickActionsRaw, archiveEnabled);');
const reading = settings('ReadingSettings');
expect(reading).toContain("const archiveEnabled = usePolicyFeature('archiveActionEnabled');");
expect(reading).toMatch(/\{archiveEnabled && \(\s*<View style=\{styles\.group\}>\s*<SettingItem label=\{t\('settings\.email_behavior\.archive_mode\.label'/);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { bottomQuickActionsFor, swipeActionShown, withoutArchive } from '../swipe-actions';
describe('archive action gate', () => {
it('shows a stored Archive swipe as none while archiving is off', () => {
expect(swipeActionShown('archive', false)).toBe('none');
expect(swipeActionShown('archive', true)).toBe('archive');
expect(swipeActionShown('delete', false)).toBe('delete');
});
it('drops Archive from the pickers while archiving is off', () => {
const options = [{ value: 'none' }, { value: 'archive' }, { value: 'delete' }];
expect(withoutArchive(options, false).map((o) => o.value)).toEqual(['none', 'delete']);
expect(withoutArchive(options, true).map((o) => o.value)).toEqual(['none', 'archive', 'delete']);
});
it('fills a stored Archive quick action with a reply action, keeping three', () => {
expect(bottomQuickActionsFor(['archive', 'delete', 'reply'], false)).toEqual(['delete', 'reply', 'replyAll']);
expect(bottomQuickActionsFor(['reply', 'archive', 'replyAll'], false)).toEqual(['reply', 'replyAll', 'forward']);
expect(bottomQuickActionsFor(['archive', 'delete', 'reply'], true)).toEqual(['archive', 'delete', 'reply']);
});
});
+24
View File
@@ -0,0 +1,24 @@
import { normalizeBottomQuickActions, type QuickAction, type SwipeAction } from '../stores/settings-store';
// The organisation's archiveActionEnabled gate (webmail
// message-list-settings.tsx): with it off, Archive is no swipe action, no
// row action and no quick action, and a stored one does nothing.
/** What a swipe does: a stored Archive swipe does nothing while archiving is off. */
export function swipeActionShown(action: SwipeAction, archiveEnabled: boolean): SwipeAction {
return !archiveEnabled && action === 'archive' ? 'none' : action;
}
/** The actions a picker offers: without Archive while archiving is off. */
export function withoutArchive<T extends { value: string }>(options: readonly T[], archiveEnabled: boolean): T[] {
return archiveEnabled ? [...options] : options.filter((option) => option.value !== 'archive');
}
/**
* The reader's three quick actions: a stored Archive slot gives way to a
* reply action while archiving is off, so the bar keeps three buttons.
*/
export function bottomQuickActionsFor(value: unknown, archiveEnabled: boolean): QuickAction[] {
const actions = normalizeBottomQuickActions(value);
return archiveEnabled ? actions : normalizeBottomQuickActions(actions.filter((action) => action !== 'archive'));
}