mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 16:07:12 +00:00
feat: take archive out of the swipe and quick action settings when the organisation turns it off
This commit is contained in:
@@ -7,7 +7,6 @@ import { useColors } from '../../theme/colors';
|
|||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
import {
|
import {
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
normalizeBottomQuickActions,
|
|
||||||
REPLY_QUICK_ACTIONS,
|
REPLY_QUICK_ACTIONS,
|
||||||
ALL_QUICK_ACTIONS,
|
ALL_QUICK_ACTIONS,
|
||||||
type SwipeAction,
|
type SwipeAction,
|
||||||
@@ -16,7 +15,8 @@ import {
|
|||||||
} from '../../stores/settings-store';
|
} from '../../stores/settings-store';
|
||||||
import { useEmailStore } from '../../stores/email-store';
|
import { useEmailStore } from '../../stores/email-store';
|
||||||
import { useKeywordsStore } from '../../stores/keywords-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 {
|
import {
|
||||||
ORDER_PRESETS, detectPreset, presetLevels, sanitizeSortLevels,
|
ORDER_PRESETS, detectPreset, presetLevels, sanitizeSortLevels,
|
||||||
type OrderPreset, type MessageListOrderScope,
|
type OrderPreset, type MessageListOrderScope,
|
||||||
@@ -74,12 +74,16 @@ export function LayoutSettings() {
|
|||||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||||
const t = useLocaleStore((s) => s.t);
|
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 SWIPE_MODE_OPTIONS = React.useMemo(() => swipeModeOptions(t), [t]);
|
||||||
const QUICK_ACTION_LABELS = React.useMemo(() => quickActionLabels(t), [t]);
|
const QUICK_ACTION_LABELS = React.useMemo(() => quickActionLabels(t), [t]);
|
||||||
const QUICK_ACTION_OPTIONS = React.useMemo(
|
const QUICK_ACTION_OPTIONS = React.useMemo(
|
||||||
() => ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })),
|
() => withoutArchive(ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })), archiveEnabled),
|
||||||
[QUICK_ACTION_LABELS],
|
[QUICK_ACTION_LABELS, archiveEnabled],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => { if (!hydrated) void hydrate(); }, [hydrated, hydrate]);
|
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') },
|
{ 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
|
// Pick an action for a bar slot. If the chosen action already occupies
|
||||||
// another slot, swap the two so the three slots stay unique.
|
// another slot, swap the two so the three slots stay unique.
|
||||||
const setQuickActionSlot = (index: number, value: QuickAction) => {
|
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.")}
|
{t('settings.email_behavior.swipe_right_action.description_mobile', "Action when you drag a row from its left edge towards the right.")}
|
||||||
</Text>
|
</Text>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={swipeRightAction}
|
value={swipeActionShown(swipeRightAction, archiveEnabled)}
|
||||||
onChange={(v) => setSwipeRightAction(v as SwipeAction)}
|
onChange={(v) => setSwipeRightAction(v as SwipeAction)}
|
||||||
options={SWIPE_OPTIONS}
|
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.")}
|
{t('settings.email_behavior.swipe_left_action.description_mobile', "Action when you drag a row from its right edge towards the left.")}
|
||||||
</Text>
|
</Text>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={swipeLeftAction}
|
value={swipeActionShown(swipeLeftAction, archiveEnabled)}
|
||||||
onChange={(v) => setSwipeLeftAction(v as SwipeAction)}
|
onChange={(v) => setSwipeLeftAction(v as SwipeAction)}
|
||||||
options={SWIPE_OPTIONS}
|
options={SWIPE_OPTIONS}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useEmailStore } from '../../stores/email-store';
|
|||||||
import { archiveEmails, queryEmails, getEmails } from '../../api/email';
|
import { archiveEmails, queryEmails, getEmails } from '../../api/email';
|
||||||
import { ownMailboxes } from '../../lib/mailbox-tree';
|
import { ownMailboxes } from '../../lib/mailbox-tree';
|
||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
|
import { usePolicyFeature } from '../../stores/policy-store';
|
||||||
|
|
||||||
export function ReadingSettings() {
|
export function ReadingSettings() {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
@@ -28,6 +29,9 @@ export function ReadingSettings() {
|
|||||||
const update = useSettingsStore((s) => s.updateSetting);
|
const update = useSettingsStore((s) => s.updateSetting);
|
||||||
|
|
||||||
const archiveMode = useSettingsStore((s) => s.archiveMode);
|
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 markAsReadDelay = useSettingsStore((s) => s.markAsReadDelay);
|
||||||
const deleteAction = useSettingsStore((s) => s.deleteAction);
|
const deleteAction = useSettingsStore((s) => s.deleteAction);
|
||||||
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
|
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
|
||||||
@@ -147,40 +151,42 @@ export function ReadingSettings() {
|
|||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.group}>
|
{archiveEnabled && (
|
||||||
<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 />
|
<View style={styles.group}>
|
||||||
<Select
|
<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 />
|
||||||
value={archiveMode}
|
<Select
|
||||||
onChange={(v) => update('archiveMode', v as ArchiveMode)}
|
value={archiveMode}
|
||||||
options={[
|
onChange={(v) => update('archiveMode', v as ArchiveMode)}
|
||||||
{ value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") },
|
options={[
|
||||||
{ value: 'year', label: t('settings.email_behavior.archive_mode.year', "A folder per year") },
|
{ value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") },
|
||||||
{ value: 'month', label: t('settings.email_behavior.archive_mode.month', "A folder per month") },
|
{ 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' && (
|
/>
|
||||||
<>
|
{archiveMode !== 'single' && (
|
||||||
<Pressable
|
<>
|
||||||
style={[styles.inlineBtn, reorganizing && styles.inlineBtnDisabled]}
|
<Pressable
|
||||||
onPress={reorganizing ? undefined : handleReorganizeArchive}
|
style={[styles.inlineBtn, reorganizing && styles.inlineBtnDisabled]}
|
||||||
disabled={reorganizing}
|
onPress={reorganizing ? undefined : handleReorganizeArchive}
|
||||||
>
|
disabled={reorganizing}
|
||||||
{reorganizing ? (
|
>
|
||||||
<ActivityIndicator size="small" color={c.text} />
|
{reorganizing ? (
|
||||||
) : (
|
<ActivityIndicator size="small" color={c.text} />
|
||||||
<FolderSync size={14} 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>
|
<View style={styles.divider} />
|
||||||
</Pressable>
|
</View>
|
||||||
{reorganizeResult && (
|
)}
|
||||||
<Text style={styles.reorganizeResult}>{reorganizeResult}</Text>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<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")}>
|
<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)} />
|
<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).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*\)\}/);
|
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'/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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'));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user