feat: lock or hide the settings the organisation pins or hides

This commit is contained in:
Linus Rath
2026-10-31 15:40:05 +01:00
parent efb92593b2
commit f27e2cebb4
6 changed files with 231 additions and 103 deletions
@@ -4,6 +4,8 @@ import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-s
import Button from '../Button';
import { spacing } from '../../theme/tokens';
import { useLocaleStore } from '../../stores/locale-store';
import { usePolicyStore, useSettingHidden, useSettingLocked } from '../../stores/policy-store';
import { policyAllowsValue } from '../../lib/org-policy';
import { APP_FONTS, FONT_SIZES, useSettingsStore, type AppFont, type FontSize } from '../../stores/settings-store';
import { uiFontsBundled } from '../../theme/reading-fonts';
import { toast } from '../../stores/toast-store';
@@ -81,6 +83,16 @@ export function AccessibilitySettings() {
const animationsEnabled = useSettingsStore((s) => s.animationsEnabled);
const toastDuration = useSettingsStore((s) => s.toastDuration);
const sendConfirmation = useSettingsStore((s) => s.sendConfirmation);
// What the organisation pins or hides (webmail accessibility-settings.tsx):
// a minimum notification duration leaves out the shorter choices.
const policyRules = usePolicyStore((s) => s.rules);
const toastDurationLocked = useSettingLocked('toastDuration');
const sendConfirmationLocked = useSettingLocked('sendConfirmation');
const sendConfirmationHidden = useSettingHidden('sendConfirmation');
const toastChoices = React.useMemo(
() => TOAST_DURATION_CHOICES.filter((value) => value === toastDuration || policyAllowsValue('toastDuration', value, policyRules)),
[toastDuration, policyRules],
);
const confirmDelete = useSettingsStore((s) => s.confirmDelete);
const confirmArchive = useSettingsStore((s) => s.confirmArchive);
@@ -214,23 +226,27 @@ export function AccessibilitySettings() {
<SettingItem
label={t('settings.accessibility.toast_duration.label', 'Notification duration')}
description={t('settings.accessibility.toast_duration.description', 'How long messages such as "Moved to Archive" with their Undo button stay on screen.')}
locked={toastDurationLocked}
>
<Select
value={String(toastDuration)}
onChange={(v) => update('toastDuration', Number(v))}
options={TOAST_DURATION_CHOICES.map((value) => ({ value: String(value), label: durationLabel(value) }))}
/>
</SettingItem>
<SettingItem
label={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
description={t('settings.accessibility.confirm_send.description', 'Asks before a message is sent.')}
>
<ToggleSwitch
checked={sendConfirmation}
onChange={(v) => update('sendConfirmation', v)}
accessibilityLabel={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
options={toastChoices.map((value) => ({ value: String(value), label: durationLabel(value) }))}
/>
</SettingItem>
{!sendConfirmationHidden && (
<SettingItem
label={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
description={t('settings.accessibility.confirm_send.description', 'Asks before a message is sent.')}
locked={sendConfirmationLocked}
>
<ToggleSwitch
checked={sendConfirmation}
onChange={(v) => update('sendConfirmation', v)}
accessibilityLabel={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
/>
</SettingItem>
)}
<SettingItem
label={t('settings.accessibility.confirm_delete.label', 'Confirm before deleting')}
description={t('settings.accessibility.confirm_delete.description', 'Asks before a message is deleted, also for single messages.')}
+23 -16
View File
@@ -7,7 +7,7 @@ import { useSettingsStore, type ThemeMode, type Density } from '../../stores/set
import { useLocaleStore } from '../../stores/locale-store';
import { setPendingSettingsTab } from '../../navigation/pending-settings-tab';
import { densityPreviewConfig } from './density-preview';
import { useSettingLocked } from '../../stores/policy-store';
import { useSettingHidden, useSettingLocked } from '../../stores/policy-store';
type Theme = ThemeMode;
@@ -84,6 +84,8 @@ export function AppearanceSettings() {
const setTheme = useSettingsStore((s) => s.setTheme);
// The organisation's theme policy can pin the colour mode (#1280).
const themeLocked = useSettingLocked('theme');
const densityLocked = useSettingLocked('density');
const densityHidden = useSettingHidden('density');
const density = useSettingsStore((s) => s.density);
const setDensity = useSettingsStore((s) => s.setDensity);
const showToolbarLabels = useSettingsStore((s) => s.showToolbarLabels);
@@ -122,21 +124,25 @@ export function AppearanceSettings() {
link="page"
/>
<View>
<SettingItem label={t('settings.appearance.list_density.label', "Density")} description={t('settings.appearance.list_density.description', "Control spacing and padding across the UI")} noBorder />
<RadioGroup
value={density}
onChange={(v) => setDensity(v as Density)}
options={[
{ value: 'extra-compact', label: t('settings.appearance.list_density.extra_compact', "Extra Compact") },
{ value: 'compact', label: t('settings.appearance.list_density.compact', "Compact") },
{ value: 'regular', label: t('settings.appearance.list_density.regular', "Regular") },
{ value: 'comfortable', label: t('settings.appearance.list_density.comfortable', "Comfortable") },
]}
/>
<DensityPreview density={density} />
<View style={styles.divider} />
</View>
{!densityHidden && (
<View>
<SettingItem label={t('settings.appearance.list_density.label', "Density")} description={t('settings.appearance.list_density.description', "Control spacing and padding across the UI")} noBorder locked={densityLocked} />
<View pointerEvents={densityLocked ? 'none' : 'auto'} style={densityLocked && styles.lockedControl}>
<RadioGroup
value={density}
onChange={(v) => setDensity(v as Density)}
options={[
{ value: 'extra-compact', label: t('settings.appearance.list_density.extra_compact', "Extra Compact") },
{ value: 'compact', label: t('settings.appearance.list_density.compact', "Compact") },
{ value: 'regular', label: t('settings.appearance.list_density.regular', "Regular") },
{ value: 'comfortable', label: t('settings.appearance.list_density.comfortable', "Comfortable") },
]}
/>
</View>
<DensityPreview density={density} />
<View style={styles.divider} />
</View>
)}
<SettingItem label={t('settings.appearance.toolbar_labels.label', "Show Toolbar Labels")} description={t('settings.appearance.toolbar_labels.description_mobile', "Display text labels next to toolbar icons.")}>
<ToggleSwitch checked={showToolbarLabels} onChange={setShowToolbarLabels} />
@@ -194,6 +200,7 @@ function makeStyles(c: ThemePalette) {
fontSize: 11,
color: c.textMuted,
},
lockedControl: { opacity: 0.6 },
divider: {
height: 1,
backgroundColor: c.border,
+12 -1
View File
@@ -7,6 +7,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
import { useColors } from '../../theme/colors';
import { useSettingsStore, type ReplyIdentityMatch, type SignaturePosition } from '../../stores/settings-store';
import { useLocaleStore } from '../../stores/locale-store';
import { useSettingLocked } from '../../stores/policy-store';
import { SUPPORTED_SUB_ADDRESS_DELIMITERS } from '../../lib/sub-addressing';
import { RADIO_GROUP_A11Y, radioOptionA11y } from '../radio-a11y';
@@ -24,6 +25,10 @@ export function ComposingSettings() {
const setAttachmentReminderKeywords = useSettingsStore((s) => s.setAttachmentReminderKeywords);
const sendDelaySeconds = useSettingsStore((s) => s.sendDelaySeconds);
const plainTextMode = useSettingsStore((s) => s.plainTextMode);
// Settings the organisation can pin (webmail lib/admin/restriction-keys.ts).
const autoSelectReplyIdentityLocked = useSettingLocked('autoSelectReplyIdentity');
const replyIdentityMatchLocked = useSettingLocked('replyIdentityMatch');
const plainTextModeLocked = useSettingLocked('plainTextMode');
const signaturePosition = useSettingsStore((s) => s.signaturePosition);
const signatureSeparatorEnabled = useSettingsStore((s) => s.signatureSeparatorEnabled);
const requestReadReceiptDefault = useSettingsStore((s) => s.requestReadReceiptDefault);
@@ -117,6 +122,7 @@ export function ComposingSettings() {
<SettingItem
label={t('settings.email_behavior.auto_select_reply_identity.label', "Reply From Received Address")}
description={t('settings.email_behavior.auto_select_reply_identity.description_mobile', "Replies always come from the identity a message was sent to. With this on, a reply to another address on your domains (a catch-all alias) is sent from that address too.")}
locked={autoSelectReplyIdentityLocked}
>
<ToggleSwitch checked={autoSelectReplyIdentity} onChange={setAutoSelectReplyIdentity} />
</SettingItem>
@@ -127,7 +133,10 @@ export function ComposingSettings() {
<Text style={styles.subDescription}>
{t('settings.email_behavior.reply_identity_match.description', 'Which received addresses count as yours. Exact address only picks one of your configured identities. Same domain also treats any other address on one of your identity domains as a catch-all alias and rewrites the From header to it. Choose exact address if those addresses are distribution lists rather than aliases.')}
</Text>
{segmented(REPLY_IDENTITY_MATCHES, replyIdentityMatch, (v) => updateSetting('replyIdentityMatch', v))}
{replyIdentityMatchLocked && <Text style={styles.subDescription}>{t('settings.org_locked', 'Set by your organisation')}</Text>}
<View pointerEvents={replyIdentityMatchLocked ? 'none' : 'auto'} style={replyIdentityMatchLocked && styles.lockedControl}>
{segmented(REPLY_IDENTITY_MATCHES, replyIdentityMatch, (v) => updateSetting('replyIdentityMatch', v))}
</View>
</View>
)}
@@ -137,6 +146,7 @@ export function ComposingSettings() {
// the webmail's description (sending only) leaves out; same text as
// under Reading.
description={t('settings.email_behavior.plain_text_mode.description_mobile', 'Compose and read in plain text only.')}
locked={plainTextModeLocked}
>
<ToggleSwitch checked={plainTextMode} onChange={(v) => updateSetting('plainTextMode', v)} />
</SettingItem>
@@ -271,6 +281,7 @@ function makeStyles(c: ThemePalette) {
subBlock: { paddingVertical: spacing.md, gap: spacing.sm },
subLabel: { ...typography.bodyMedium, color: c.text },
subDescription: { ...typography.caption, color: c.mutedForeground },
lockedControl: { opacity: 0.6 },
segmentRow: { flexDirection: 'row', gap: 6, marginTop: 4 },
segment: {
flex: 1,
@@ -19,6 +19,7 @@ import { useAuthStore } from '../../stores/auth-store';
import { useEmailStore } from '../../stores/email-store';
import { useLocaleStore } from '../../stores/locale-store';
import { useSettingsStore } from '../../stores/settings-store';
import { useSettingHidden, useSettingLocked } from '../../stores/policy-store';
import {
DEFAULT_RELAY_BASE_URL,
disablePushForAccount,
@@ -99,6 +100,13 @@ export function NotificationSettings() {
// The Inbox-only filter needs the server's emailPush filter too (webmail #983).
const inboxOnly = useSettingsStore((s) => s.pushNotifyInboxOnly);
const calEnabled = useSettingsStore((s) => s.calendarNotificationsEnabled);
// Settings the organisation pins or hides (webmail notification-settings.tsx).
const emailEnabledLocked = useSettingLocked('emailNotificationsEnabled');
const emailEnabledHidden = useSettingHidden('emailNotificationsEnabled');
const inboxOnlyLocked = useSettingLocked('pushNotifyInboxOnly');
const inboxOnlyHidden = useSettingHidden('pushNotifyInboxOnly');
const calEnabledLocked = useSettingLocked('calendarNotificationsEnabled');
const calEnabledHidden = useSettingHidden('calendarNotificationsEnabled');
const invitationParsing = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
const appIconBadge = useSettingsStore((s) => s.appIconUnreadBadge);
@@ -570,16 +578,20 @@ export function NotificationSettings() {
title={t('settings.notifications.email.title', 'Email Notifications')}
description={t('settings.notifications.email.description', 'Configure notifications for incoming emails')}
>
<SettingItem
label={t('settings.notifications.email.enabled', 'Email notifications')}
description={t('settings.notifications.email.enabled_desc', 'Show notifications when new emails arrive')}
>
<ToggleSwitch checked={emailEnabled} onChange={(v) => update('emailNotificationsEnabled', v)} />
</SettingItem>
{inboxOnlyAvailable && (
{!emailEnabledHidden && (
<SettingItem
label={t('settings.notifications.email.enabled', 'Email notifications')}
description={t('settings.notifications.email.enabled_desc', 'Show notifications when new emails arrive')}
locked={emailEnabledLocked}
>
<ToggleSwitch checked={emailEnabled} onChange={(v) => update('emailNotificationsEnabled', v)} />
</SettingItem>
)}
{inboxOnlyAvailable && !inboxOnlyHidden && (
<SettingItem
label={t('settings.notifications.email.inbox_only', 'Inbox only')}
description={t('settings.notifications.email.inbox_only_desc', 'Only notify for mail that lands in the Inbox; skip mail a filter files into another folder')}
locked={inboxOnlyLocked}
>
<ToggleSwitch
checked={inboxOnly}
@@ -629,12 +641,15 @@ export function NotificationSettings() {
title={t('settings.notifications.calendar.title', 'Calendar Notifications')}
description={t('settings.notifications.calendar.description', 'Configure notifications for calendar events')}
>
<SettingItem
label={t('settings.notifications.calendar.enabled', 'Event notifications')}
description={t('settings.notifications.calendar.enabled_desc', 'Show alerts for upcoming calendar events')}
>
<ToggleSwitch checked={calEnabled} onChange={(v) => update('calendarNotificationsEnabled', v)} />
</SettingItem>
{!calEnabledHidden && (
<SettingItem
label={t('settings.notifications.calendar.enabled', 'Event notifications')}
description={t('settings.notifications.calendar.enabled_desc', 'Show alerts for upcoming calendar events')}
locked={calEnabledLocked}
>
<ToggleSwitch checked={calEnabled} onChange={(v) => update('calendarNotificationsEnabled', v)} />
</SettingItem>
)}
<SettingItem
label={t('settings.notifications.calendar.invitation_parsing', 'Parse email invitations')}
description={t(
+93 -62
View File
@@ -18,7 +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';
import { usePolicyFeature, useSettingHidden, useSettingLocked } from '../../stores/policy-store';
export function ReadingSettings() {
const c = useColors();
@@ -51,6 +51,22 @@ export function ReadingSettings() {
const plainTextFont = useSettingsStore((s) => s.plainTextFont);
const messageSpacing = useSettingsStore((s) => s.messageSpacing);
const readReceiptResponse = useSettingsStore((s) => s.readReceiptResponse);
// Settings the organisation pins or hides (webmail reading-settings.tsx,
// message-list-settings.tsx, layout-settings.tsx).
const markAsReadLocked = useSettingLocked('markAsReadDelay');
const markAsReadHidden = useSettingHidden('markAsReadDelay');
const deleteActionLocked = useSettingLocked('deleteAction');
const deleteActionHidden = useSettingHidden('deleteAction');
const showPreviewLocked = useSettingLocked('showPreview');
const showPreviewHidden = useSettingHidden('showPreview');
const includeGroupLocked = useSettingLocked('includeGroupInUnified');
const includeGroupHidden = useSettingHidden('includeGroupInUnified');
const plainTextModeLocked = useSettingLocked('plainTextMode');
const autoSelectReplyIdentityLocked = useSettingLocked('autoSelectReplyIdentity');
const messageSpacingLocked = useSettingLocked('messageSpacing');
const messageSpacingHidden = useSettingHidden('messageSpacing');
const emailsPerPageLocked = useSettingLocked('emailsPerPage');
const emailsPerPageHidden = useSettingHidden('emailsPerPage');
const [newKeyword, setNewKeyword] = useState('');
const [reorganizing, setReorganizing] = useState(false);
@@ -118,38 +134,44 @@ export function ReadingSettings() {
return (
<SettingsSection title={t('settings.email_behavior.title', "Email Behavior")} description={t('settings.email_behavior.description', "Configure how emails are handled")}>
<SettingItem label={t('settings.email_behavior.mark_read.label', "Mark as Read")} description={t('settings.email_behavior.mark_read.description', "When to mark emails as read when opened")}>
<Select
value={String(markAsReadDelay)}
onChange={(v) => update('markAsReadDelay', Number(v))}
options={[
{ value: '0', label: t('settings.email_behavior.mark_read.instant', "Instantly") },
{ value: '3000', label: t('settings.email_behavior.mark_read.delay_3s', "After 3 seconds") },
{ value: '5000', label: t('settings.email_behavior.mark_read.delay_5s', "After 5 seconds") },
{ value: '-1', label: t('settings.email_behavior.mark_read.never', "Never") },
]}
/>
</SettingItem>
{!markAsReadHidden && (
<SettingItem label={t('settings.email_behavior.mark_read.label', "Mark as Read")} description={t('settings.email_behavior.mark_read.description', "When to mark emails as read when opened")} locked={markAsReadLocked}>
<Select
value={String(markAsReadDelay)}
onChange={(v) => update('markAsReadDelay', Number(v))}
options={[
{ value: '0', label: t('settings.email_behavior.mark_read.instant', "Instantly") },
{ value: '3000', label: t('settings.email_behavior.mark_read.delay_3s', "After 3 seconds") },
{ value: '5000', label: t('settings.email_behavior.mark_read.delay_5s', "After 5 seconds") },
{ value: '-1', label: t('settings.email_behavior.mark_read.never', "Never") },
]}
/>
</SettingItem>
)}
<View style={styles.group}>
<SettingItem label={t('settings.email_behavior.delete_action.label', "Delete Action")} description={t('settings.email_behavior.delete_action.description', "What happens when you delete an email")} noBorder />
<Select
value={deleteAction}
onChange={(v) => update('deleteAction', v as DeleteAction)}
options={[
{ value: 'trash', label: t('settings.email_behavior.delete_action.trash', "Move to Trash") },
{ value: 'trash-and-read', label: t('settings.email_behavior.delete_action.trash_and_read', "Move to Trash and mark as read") },
{ value: 'permanent', label: t('settings.email_behavior.delete_action.permanent', "Delete Permanently") },
]}
/>
{deleteAction === 'permanent' && (
<View style={styles.warning}>
<AlertTriangle size={14} color={c.error} />
<Text style={styles.warningText}>{t('settings.email_behavior.delete_action.warning', "Emails will be permanently deleted and cannot be recovered. This action is irreversible.")}</Text>
{!deleteActionHidden && (
<View style={styles.group}>
<SettingItem label={t('settings.email_behavior.delete_action.label', "Delete Action")} description={t('settings.email_behavior.delete_action.description', "What happens when you delete an email")} noBorder locked={deleteActionLocked} />
<View pointerEvents={deleteActionLocked ? 'none' : 'auto'} style={deleteActionLocked && styles.lockedControl}>
<Select
value={deleteAction}
onChange={(v) => update('deleteAction', v as DeleteAction)}
options={[
{ value: 'trash', label: t('settings.email_behavior.delete_action.trash', "Move to Trash") },
{ value: 'trash-and-read', label: t('settings.email_behavior.delete_action.trash_and_read', "Move to Trash and mark as read") },
{ value: 'permanent', label: t('settings.email_behavior.delete_action.permanent', "Delete Permanently") },
]}
/>
</View>
)}
<View style={styles.divider} />
</View>
{deleteAction === 'permanent' && (
<View style={styles.warning}>
<AlertTriangle size={14} color={c.error} />
<Text style={styles.warningText}>{t('settings.email_behavior.delete_action.warning', "Emails will be permanently deleted and cannot be recovered. This action is irreversible.")}</Text>
</View>
)}
<View style={styles.divider} />
</View>
)}
{archiveEnabled && (
<View style={styles.group}>
@@ -196,9 +218,11 @@ export function ReadingSettings() {
<ToggleSwitch checked={clearSearchOnFolderChange} onChange={(v) => update('clearSearchOnFolderChange', v)} />
</SettingItem>
<SettingItem label={t('settings.email_behavior.show_preview.label', "Show Preview Text")} description={t('settings.email_behavior.show_preview.description', "Display email preview in the list")}>
<ToggleSwitch checked={showPreview} onChange={(v) => update('showPreview', v)} />
</SettingItem>
{!showPreviewHidden && (
<SettingItem label={t('settings.email_behavior.show_preview.label', "Show Preview Text")} description={t('settings.email_behavior.show_preview.description', "Display email preview in the list")} locked={showPreviewLocked}>
<ToggleSwitch checked={showPreview} onChange={(v) => update('showPreview', v)} />
</SettingItem>
)}
<SettingItem label={t('settings.email_behavior.show_verification_codes.label', "Show Verification Codes")} description={t('settings.email_behavior.show_verification_codes.description', "Offer the code from sign-in and confirmation emails for one-click copying, in the list for a day and in the message")}>
<ToggleSwitch checked={showVerificationCodes} onChange={(v) => update('showVerificationCodes', v)} />
@@ -208,15 +232,17 @@ export function ReadingSettings() {
<ToggleSwitch checked={disableThreading} onChange={(v) => update('disableThreading', v)} />
</SettingItem>
<SettingItem label={t('settings.appearance.unified_mailbox.include_group.label', "Include group inboxes")} description={t('settings.appearance.unified_mailbox.include_group.description', "Also merge shared/group inboxes into the unified view.")}>
<ToggleSwitch checked={includeGroupInUnified} onChange={(v) => update('includeGroupInUnified', v)} />
</SettingItem>
{!includeGroupHidden && (
<SettingItem label={t('settings.appearance.unified_mailbox.include_group.label', "Include group inboxes")} description={t('settings.appearance.unified_mailbox.include_group.description', "Also merge shared/group inboxes into the unified view.")} locked={includeGroupLocked}>
<ToggleSwitch checked={includeGroupInUnified} onChange={(v) => update('includeGroupInUnified', v)} />
</SettingItem>
)}
<SettingItem label={t('settings.email_behavior.plain_text_mode.label', "Plain Text Only")} description={t('settings.email_behavior.plain_text_mode.description_mobile', "Compose and read in plain text only.")}>
<SettingItem label={t('settings.email_behavior.plain_text_mode.label', "Plain Text Only")} description={t('settings.email_behavior.plain_text_mode.description_mobile', "Compose and read in plain text only.")} locked={plainTextModeLocked}>
<ToggleSwitch checked={plainTextMode} onChange={(v) => update('plainTextMode', v)} />
</SettingItem>
<SettingItem label={t('settings.email_behavior.auto_select_reply_identity.label', "Reply From Received Address")} description={t('settings.email_behavior.auto_select_reply_identity.description_mobile', "When replying, send from the address the message was originally sent to.")}>
<SettingItem label={t('settings.email_behavior.auto_select_reply_identity.label', "Reply From Received Address")} description={t('settings.email_behavior.auto_select_reply_identity.description_mobile', "When replying, send from the address the message was originally sent to.")} locked={autoSelectReplyIdentityLocked}>
<ToggleSwitch checked={autoSelectReplyIdentity} onChange={(v) => update('autoSelectReplyIdentity', v)} />
</SettingItem>
@@ -320,17 +346,19 @@ export function ReadingSettings() {
/>
</SettingItem>
<SettingItem label={t('settings.email_behavior.message_spacing.label', "Message Spacing")} description={t('settings.email_behavior.message_spacing.description', "Padding around the message body in the reader")}>
<Select
value={messageSpacing}
onChange={(v) => update('messageSpacing', v as MessageSpacing)}
options={[
{ value: 'auto', label: t('settings.email_behavior.message_spacing.auto', "Auto (recommended)") },
{ value: 'always', label: t('settings.email_behavior.message_spacing.always', "Always add spacing") },
{ value: 'edge', label: t('settings.email_behavior.message_spacing.edge', "Edge to edge") },
]}
/>
</SettingItem>
{!messageSpacingHidden && (
<SettingItem label={t('settings.email_behavior.message_spacing.label', "Message Spacing")} description={t('settings.email_behavior.message_spacing.description', "Padding around the message body in the reader")} locked={messageSpacingLocked}>
<Select
value={messageSpacing}
onChange={(v) => update('messageSpacing', v as MessageSpacing)}
options={[
{ value: 'auto', label: t('settings.email_behavior.message_spacing.auto', "Auto (recommended)") },
{ value: 'always', label: t('settings.email_behavior.message_spacing.always', "Always add spacing") },
{ value: 'edge', label: t('settings.email_behavior.message_spacing.edge', "Edge to edge") },
]}
/>
</SettingItem>
)}
<SettingItem label={t('settings.email_behavior.read_receipt_response.label', "Respond to read-receipt requests")} description={t('settings.email_behavior.read_receipt_response.description', "What to do when an incoming message asks for a read receipt.")}>
<Select
@@ -344,18 +372,20 @@ export function ReadingSettings() {
/>
</SettingItem>
<SettingItem label={t('settings.email_behavior.emails_per_page.label', "Emails Per Page")} description={t('settings.email_behavior.emails_per_page.description', "Number of emails to load at once")}>
<Select
value={String(emailsPerPage)}
onChange={(v) => update('emailsPerPage', Number(v))}
options={[
{ value: '10', label: '10' },
{ value: '25', label: '25' },
{ value: '50', label: '50' },
{ value: '100', label: '100' },
]}
/>
</SettingItem>
{!emailsPerPageHidden && (
<SettingItem label={t('settings.email_behavior.emails_per_page.label', "Emails Per Page")} description={t('settings.email_behavior.emails_per_page.description', "Number of emails to load at once")} locked={emailsPerPageLocked}>
<Select
value={String(emailsPerPage)}
onChange={(v) => update('emailsPerPage', Number(v))}
options={[
{ value: '10', label: '10' },
{ value: '25', label: '25' },
{ value: '50', label: '50' },
{ value: '100', label: '100' },
]}
/>
</SettingItem>
)}
</SettingsSection>
);
@@ -364,6 +394,7 @@ export function ReadingSettings() {
function makeStyles(c: ThemePalette) {
return StyleSheet.create({
group: {},
lockedControl: { opacity: 0.6 },
warning: {
flexDirection: 'row',
gap: spacing.sm,
@@ -63,4 +63,52 @@ describe('organisation policy on the settings pages', () => {
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'/);
});
// The webmail's lockable settings (lib/admin/restriction-keys.ts, page
// `policy`) the app has a row for, and whether the webmail also hides the
// row when the restriction says so.
const LOCKABLE_ROWS: ReadonlyArray<[page: string, key: string, hides: boolean]> = [
['AppearanceSettings', 'density', true],
['AccessibilitySettings', 'toastDuration', false],
['AccessibilitySettings', 'sendConfirmation', true],
['ReadingSettings', 'markAsReadDelay', true],
['ReadingSettings', 'deleteAction', true],
['ReadingSettings', 'showPreview', true],
['ReadingSettings', 'includeGroupInUnified', true],
['ReadingSettings', 'plainTextMode', false],
['ReadingSettings', 'autoSelectReplyIdentity', false],
['ReadingSettings', 'messageSpacing', true],
['ReadingSettings', 'emailsPerPage', true],
['ComposingSettings', 'autoSelectReplyIdentity', false],
['ComposingSettings', 'replyIdentityMatch', false],
['ComposingSettings', 'plainTextMode', false],
['NotificationSettings', 'emailNotificationsEnabled', true],
['NotificationSettings', 'pushNotifyInboxOnly', true],
['NotificationSettings', 'calendarNotificationsEnabled', true],
];
it.each(LOCKABLE_ROWS)('%s marks %s as set by the organisation when locked', (page, key, hides) => {
const source = settings(page);
const locked = new RegExp(`const (\\w+) = useSettingLocked\\('${key}'\\);`).exec(source);
expect(locked, `useSettingLocked('${key}')`).not.toBeNull();
// On the row, or on the control of a row whose control sits outside it.
expect(source).toMatch(new RegExp(`locked=\\{${locked![1]}\\}|pointerEvents=\\{${locked![1]} \\? 'none' : 'auto'\\}`));
if (hides) {
const hidden = new RegExp(`const (\\w+) = useSettingHidden\\('${key}'\\);`).exec(source);
expect(hidden, `useSettingHidden('${key}')`).not.toBeNull();
expect(source).toMatch(new RegExp(`\\{!${hidden![1]} && \\(|&& !${hidden![1]} && \\(`));
}
});
it('keeps notification durations under the organisation minimum out of the choices', () => {
const page = settings('AccessibilitySettings');
expect(page).toContain("policyAllowsValue('toastDuration', value, policyRules)");
expect(page).toContain('options={toastChoices.map(');
});
it('keeps the controls of locked rows that sit outside their row still', () => {
expect(settings('AppearanceSettings')).toMatch(/<View pointerEvents=\{densityLocked \? 'none' : 'auto'\}[^>]*>\s*<RadioGroup\s+value=\{density\}/);
expect(settings('ReadingSettings')).toMatch(/<View pointerEvents=\{deleteActionLocked \? 'none' : 'auto'\}[^>]*>\s*<Select\s+value=\{deleteAction\}/);
expect(settings('ComposingSettings')).toMatch(/<View pointerEvents=\{replyIdentityMatchLocked \? 'none' : 'auto'\}[^>]*>\s*\{segmented\(REPLY_IDENTITY_MATCHES/);
});
});