Merge branch 'main' into feature/scheduled-send
This commit is contained in:
@@ -12,6 +12,7 @@ import { useAccountSecurityStore, type AppPasswordInfo, type ApiKeyInfo, type Ap
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { toast } from '@/stores/toast-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sanitizeI18nHtml } from '@/lib/email-sanitization';
|
||||
|
||||
function PasswordChangeSection() {
|
||||
const t = useTranslations('settings.security');
|
||||
@@ -671,7 +672,7 @@ export function AccountSecuritySettings() {
|
||||
if (isStalwart === false) {
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
<div className="text-sm text-muted-foreground py-4" dangerouslySetInnerHTML={{ __html: t('not_available') }} />
|
||||
<div className="text-sm text-muted-foreground py-4" dangerouslySetInnerHTML={{ __html: sanitizeI18nHtml(t('not_available')) }} />
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function AppearanceSettings() {
|
||||
const tAdvanced = useTranslations('settings.advanced');
|
||||
const tTour = useTranslations('tour');
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
const { fontSize, density, animationsEnabled, senderFavicons, showAvatarsInJunk, updateSetting } = useSettingsStore();
|
||||
const { fontSize, density, animationsEnabled, senderFavicons, showAvatarsInJunk, showOnboardingOnNewDevices, updateSetting } = useSettingsStore();
|
||||
const { startTour, resetTourCompletion } = useTour();
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
|
||||
@@ -145,6 +145,13 @@ export function AppearanceSettings() {
|
||||
{tTour('restart_button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={tTour('show_on_new_devices_title')} description={tTour('show_on_new_devices_desc')}>
|
||||
<ToggleSwitch
|
||||
checked={showOnboardingOnNewDevices}
|
||||
onChange={(checked) => updateSetting('showOnboardingOnNewDevices', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useCalendarStore } from '@/stores/calendar-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
@@ -10,7 +10,7 @@ import { SettingsSection } from './settings-section';
|
||||
import { Plus, Pencil, Trash2, Calendar as CalendarIcon, Copy, Link, Upload, Globe, RefreshCw, Eraser, Users } from 'lucide-react';
|
||||
import { ShareCollectionDialog } from './share-collection-dialog';
|
||||
import type { CalendarRights } from '@/lib/jmap/types';
|
||||
import { cn, formatDateTime } from '@/lib/utils';
|
||||
import { cn, formatDateTime, redactUrlCredentials } from '@/lib/utils';
|
||||
import { ICalImportModal } from '@/components/calendar/ical-import-modal';
|
||||
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
@@ -155,7 +155,14 @@ export { CalendarColorPicker, CALENDAR_COLORS };
|
||||
export function CalendarManagementSettings() {
|
||||
const t = useTranslations('calendar.management');
|
||||
const { client, serverUrl, username } = useAuthStore();
|
||||
const { calendars, updateCalendar, shareCalendar, createCalendar, removeCalendar, clearCalendarEvents, fetchCalendars, icalSubscriptions, removeICalSubscription, refreshICalSubscription, isSubscriptionCalendar } = useCalendarStore();
|
||||
const { calendars, updateCalendar, shareCalendar, createCalendar, removeCalendar, clearCalendarEvents, fetchCalendars, icalSubscriptions: allSubs, removeICalSubscription, refreshICalSubscription, isSubscriptionCalendar } = useCalendarStore();
|
||||
// Subscriptions are persisted globally but scoped per JMAP account via
|
||||
// accountId. Legacy entries with no accountId show in the active account.
|
||||
const currentAccountId = client?.getAccountId();
|
||||
const icalSubscriptions = useMemo(
|
||||
() => allSubs.filter(s => !s.accountId || s.accountId === currentAccountId),
|
||||
[allSubs, currentAccountId],
|
||||
);
|
||||
|
||||
const [discoveredCalDavUrls, setDiscoveredCalDavUrls] = useState<Record<string, string | null>>({});
|
||||
const [wellKnownCalDavUrl, setWellKnownCalDavUrl] = useState<string | null>(null);
|
||||
@@ -652,9 +659,14 @@ export function CalendarManagementSettings() {
|
||||
<Globe className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">{sub.name}</span>
|
||||
<span className="text-xs text-muted-foreground truncate block" title={sub.url}>
|
||||
{sub.url}
|
||||
</span>
|
||||
{(() => {
|
||||
const safeUrl = redactUrlCredentials(sub.url);
|
||||
return (
|
||||
<span className="text-xs text-muted-foreground truncate block" title={safeUrl}>
|
||||
{safeUrl}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
{sub.lastRefreshed && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{tSub('last_refreshed', { time: formatDateTime(sub.lastRefreshed, timeFormat, { month: 'short', day: 'numeric', year: 'numeric' }) })}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import type { SendDelaySeconds } from '@/stores/settings-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { Mail, X } from 'lucide-react';
|
||||
import { getPathPrefix } from '@/lib/browser-navigation';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
SUPPORTED_SUB_ADDRESS_DELIMITERS,
|
||||
isSupportedSubAddressDelimiter,
|
||||
@@ -20,8 +18,6 @@ const DEFAULT_CUSTOM_DELIMITER = '~';
|
||||
|
||||
export function ComposingSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const { appName } = useConfig();
|
||||
const [defaultMailStatus, setDefaultMailStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
|
||||
const {
|
||||
@@ -30,22 +26,13 @@ export function ComposingSettings() {
|
||||
attachmentReminderKeywords,
|
||||
sendDelaySeconds,
|
||||
subAddressDelimiter,
|
||||
signaturePosition,
|
||||
signatureSeparatorEnabled,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
const { client } = useAuthStore();
|
||||
const delayedSendSupported = client?.hasDelayedSend() ?? false;
|
||||
|
||||
const handleSetDefaultMailProgram = useCallback(() => {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.registerProtocolHandler) {
|
||||
navigator.registerProtocolHandler('mailto', `${window.location.origin}${getPathPrefix()}/compose?mailto=%s`);
|
||||
setDefaultMailStatus('success');
|
||||
}
|
||||
} catch {
|
||||
setDefaultMailStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
<SettingItem label={t('auto_select_reply_identity.label')} description={t('auto_select_reply_identity.description')}>
|
||||
@@ -73,6 +60,24 @@ export function ComposingSettings() {
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('signature_position.label')} description={t('signature_position.description')}>
|
||||
<Select
|
||||
value={signaturePosition}
|
||||
onChange={(value) => updateSetting('signaturePosition', value as 'above_quote' | 'below_quote')}
|
||||
options={[
|
||||
{ value: 'above_quote', label: t('signature_position.above_quote') },
|
||||
{ value: 'below_quote', label: t('signature_position.below_quote') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('signature_separator.label')} description={t('signature_separator.description')}>
|
||||
<ToggleSwitch
|
||||
checked={signatureSeparatorEnabled}
|
||||
onChange={(checked) => updateSetting('signatureSeparatorEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('sub_address_delimiter.label')}
|
||||
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
||||
@@ -171,24 +176,6 @@ export function ComposingSettings() {
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('default_mail_program.label')} description={t('default_mail_program.description', { appName: appName || 'Bulwark' })}>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
onClick={handleSetDefaultMailProgram}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="text-sm text-foreground">{t('default_mail_program.button')}</span>
|
||||
</button>
|
||||
{defaultMailStatus === 'success' && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">{t('default_mail_program.success')}</p>
|
||||
)}
|
||||
{defaultMailStatus === 'error' && (
|
||||
<p className="text-xs text-destructive">{t('default_mail_program.error')}</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useContactStore } from '@/stores/contact-store';
|
||||
export function ContentSendersSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const [showTrustedModal, setShowTrustedModal] = useState(false);
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||
|
||||
const {
|
||||
externalContentPolicy,
|
||||
@@ -32,7 +32,7 @@ export function ContentSendersSettings() {
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{!isSettingHidden('externalContentPolicy') && (
|
||||
{isFeatureEnabled('externalContentEnabled') && !isSettingHidden('externalContentPolicy') && (
|
||||
<SettingItem label={t('external_content.label')} description={t('external_content.description')} locked={isSettingLocked('externalContentPolicy')}>
|
||||
<Select
|
||||
value={externalContentPolicy}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Inbox, Send, FileText, Trash, ShieldAlert, Archive,
|
||||
Star, Heart, Bookmark, Tag, Flag, Briefcase, Users,
|
||||
Bell, Zap, Globe, Lock, Eye, MessageSquare, Mail,
|
||||
AlertTriangle, NotebookPen, CalendarClock, BellOff,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils';
|
||||
@@ -27,6 +28,11 @@ const ROLE_ICONS: Record<string, LucideIcon> = {
|
||||
trash: Trash,
|
||||
junk: ShieldAlert,
|
||||
archive: Archive,
|
||||
shared: Users,
|
||||
important: AlertTriangle,
|
||||
memos: NotebookPen,
|
||||
scheduled: CalendarClock,
|
||||
snoozed: BellOff,
|
||||
};
|
||||
|
||||
const ICON_CHOICES: { name: string; icon: LucideIcon }[] = [
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
|
||||
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
||||
@@ -13,6 +15,12 @@ const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||
{ sender: 'Billing', subject: 'Invoice 1042', preview: 'Your receipt is attached.', selected: false },
|
||||
];
|
||||
|
||||
const MAIL_LAYOUT_PREVIEW_ROWS_FOCUS = [
|
||||
...MAIL_LAYOUT_PREVIEW_ROWS,
|
||||
{ sender: 'Sam', subject: 'Lunch?', preview: '', selected: false },
|
||||
{ sender: 'Newsletter', subject: 'Weekly digest', preview: '', selected: false },
|
||||
];
|
||||
|
||||
function MailLayoutPreview({
|
||||
value,
|
||||
t,
|
||||
@@ -20,8 +28,6 @@ function MailLayoutPreview({
|
||||
value: MailLayout;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const isSplit = value === 'split';
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-border bg-background p-3">
|
||||
<div>
|
||||
@@ -33,7 +39,7 @@ function MailLayoutPreview({
|
||||
<div className="flex h-28">
|
||||
<div className="w-11 border-r border-border bg-muted/40" />
|
||||
|
||||
{isSplit ? (
|
||||
{value === 'split' && (
|
||||
<>
|
||||
<div className="w-28 border-r border-border bg-background">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
@@ -56,15 +62,36 @@ function MailLayoutPreview({
|
||||
<div className="mt-1.5 h-2 w-2/3 rounded bg-foreground/10" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 bg-background px-2 py-2">
|
||||
<div className="space-y-1.5">
|
||||
)}
|
||||
|
||||
{value === 'focus' && (
|
||||
<div className="flex-1 bg-background">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS_FOCUS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'border-b border-border px-2 py-1 text-[10px] last:border-b-0',
|
||||
row.selected && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className="truncate text-foreground">
|
||||
<span className="font-medium">{row.sender}</span>
|
||||
<span className="mx-1.5 text-muted-foreground">{row.subject}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{value === 'horizontal' && (
|
||||
<div className="flex-1 flex flex-col bg-background">
|
||||
<div className="border-b border-border bg-background">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-[10px]',
|
||||
row.selected ? 'bg-primary/10' : 'bg-muted/20'
|
||||
'border-b border-border px-2 py-1 text-[10px] last:border-b-0',
|
||||
row.selected && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className="truncate text-foreground">
|
||||
@@ -74,6 +101,11 @@ function MailLayoutPreview({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 bg-background px-3 py-2">
|
||||
<div className="h-2 w-20 rounded bg-foreground/10" />
|
||||
<div className="mt-1.5 h-1.5 w-full rounded bg-foreground/10" />
|
||||
<div className="mt-1 h-1.5 w-5/6 rounded bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -85,9 +117,10 @@ function MailLayoutPreview({
|
||||
export function LayoutSettings() {
|
||||
const t = useTranslations('settings.appearance');
|
||||
const tEmail = useTranslations('settings.email_behavior');
|
||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, updateSetting } = useSettingsStore();
|
||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
const accounts = useAccountStore(s => s.accounts);
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
@@ -100,6 +133,7 @@ export function LayoutSettings() {
|
||||
options={[
|
||||
{ value: 'split', label: tEmail('mail_layout.split') },
|
||||
{ value: 'focus', label: tEmail('mail_layout.focus') },
|
||||
{ value: 'horizontal', label: tEmail('mail_layout.horizontal') },
|
||||
]}
|
||||
/>
|
||||
<MailLayoutPreview value={mailLayout} t={tEmail} />
|
||||
@@ -157,6 +191,23 @@ export function LayoutSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
|
||||
<div className="flex items-center gap-3">
|
||||
{proInterface && isDesktop && (
|
||||
<Link
|
||||
href="/pro"
|
||||
className="text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
{t('pro_interface.open_label')}
|
||||
</Link>
|
||||
)}
|
||||
<ToggleSwitch
|
||||
checked={proInterface}
|
||||
onChange={(v) => updateSetting('proInterface', v)}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,11 +52,14 @@ export function NotificationSettings() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!supported) return;
|
||||
if (!client) return;
|
||||
const accountId = client.getAccountId();
|
||||
if (!accountId) return;
|
||||
void (async () => {
|
||||
const enabled = await isWebPushEnabled();
|
||||
if (enabled) setPushStatus({ kind: 'enabled' });
|
||||
const enabled = await isWebPushEnabled(accountId);
|
||||
setPushStatus(enabled ? { kind: 'enabled' } : { kind: 'idle' });
|
||||
})();
|
||||
}, [supported]);
|
||||
}, [supported, client]);
|
||||
|
||||
const trimmedRelay = relayUrl.trim().replace(/\/+$/, '');
|
||||
const isValidRelay = /^https?:\/\/.+/i.test(trimmedRelay);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getPathPrefix } from "@/lib/browser-navigation";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import type { ProtocolOpenMode } from "@/stores/settings-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { SettingsSection, SettingItem, Select } from "./settings-section";
|
||||
|
||||
type Protocol = "mailto" | "webcal";
|
||||
|
||||
function canRegisterProtocolHandler(): boolean {
|
||||
return typeof navigator !== "undefined"
|
||||
&& "registerProtocolHandler" in navigator
|
||||
&& typeof window !== "undefined"
|
||||
&& window.isSecureContext;
|
||||
}
|
||||
|
||||
function getProtocolHandlerUrl(protocol: Protocol) {
|
||||
return `${window.location.origin}${getPathPrefix()}/protocol/${protocol}?url=%s`;
|
||||
}
|
||||
|
||||
function registerProtocolHandler(protocol: Protocol) {
|
||||
navigator.registerProtocolHandler(
|
||||
protocol,
|
||||
getProtocolHandlerUrl(protocol),
|
||||
);
|
||||
}
|
||||
|
||||
interface ProtocolHandlerSettingsProps {
|
||||
supportsCalendar: boolean;
|
||||
}
|
||||
|
||||
export function ProtocolHandlerSettings({ supportsCalendar }: ProtocolHandlerSettingsProps) {
|
||||
const t = useTranslations("protocol_handlers");
|
||||
const protocolOpenMode = useSettingsStore((state) => state.protocolOpenMode);
|
||||
const updateSetting = useSettingsStore((state) => state.updateSetting);
|
||||
const [supported, setSupported] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSupported(canRegisterProtocolHandler());
|
||||
}, []);
|
||||
|
||||
const handleOpenModeChange = async (value: string) => {
|
||||
const openMode = value as ProtocolOpenMode;
|
||||
|
||||
if (openMode === "active-session"
|
||||
&& typeof window !== "undefined"
|
||||
&& "Notification" in window
|
||||
&& Notification.permission === "default") {
|
||||
await Notification.requestPermission();
|
||||
}
|
||||
|
||||
updateSetting("protocolOpenMode", openMode);
|
||||
};
|
||||
|
||||
const handleRegister = (protocol: Protocol) => {
|
||||
try {
|
||||
registerProtocolHandler(protocol);
|
||||
toast.success(protocol === "mailto" ? t("mailto_registered") : t("webcal_registered"));
|
||||
} catch {
|
||||
toast.error(t("registration_failed"));
|
||||
}
|
||||
};
|
||||
|
||||
const renderRegistrationControl = (protocol: Protocol) => {
|
||||
return (
|
||||
<Button size="sm" onClick={() => handleRegister(protocol)} disabled={!supported}>
|
||||
{protocol === "mailto" ? t("register_mailto") : t("register_webcal")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title={t("title")} description={t("description")}>
|
||||
{!supported && (
|
||||
<div className="rounded-md border border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">
|
||||
{t("unsupported")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SettingItem label={t("mailto_label")} description={t("mailto_description")}>
|
||||
{renderRegistrationControl("mailto")}
|
||||
</SettingItem>
|
||||
|
||||
{supportsCalendar && (
|
||||
<SettingItem label={t("webcal_label")} description={t("webcal_description")}>
|
||||
{renderRegistrationControl("webcal")}
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t("protocol_open_mode_label")} description={t("protocol_open_mode_description")}>
|
||||
<Select
|
||||
value={protocolOpenMode}
|
||||
onChange={handleOpenModeChange}
|
||||
options={[
|
||||
{ value: "new-tab", label: t("protocol_open_mode_new_tab") },
|
||||
{ value: "active-session", label: t("protocol_open_mode_active_session") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<p className="text-xs text-muted-foreground">{t("browser_note")}</p>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,6 @@ export function SmimeSettings() {
|
||||
identityKeyBindings,
|
||||
defaultSignIdentity,
|
||||
defaultEncrypt,
|
||||
rememberUnlockedKeys,
|
||||
autoImportSignerCerts,
|
||||
isLoading,
|
||||
error,
|
||||
@@ -44,7 +43,6 @@ export function SmimeSettings() {
|
||||
lockKey,
|
||||
setSignDefault,
|
||||
setEncryptDefault,
|
||||
setRememberUnlockedKeys,
|
||||
setAutoImportSignerCerts,
|
||||
isKeyUnlocked,
|
||||
setError,
|
||||
@@ -465,16 +463,6 @@ export function SmimeSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t("remember_unlocked")}
|
||||
description={t("remember_unlocked_desc")}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={rememberUnlockedKeys}
|
||||
onChange={setRememberUnlockedKeys}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t("auto_import_signer_certs")}
|
||||
description={t("auto_import_signer_certs_desc")}
|
||||
|
||||
Reference in New Issue
Block a user