mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
feat: lock or hide the settings the organisation pins or hides
This commit is contained in:
@@ -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.')}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user