Merge dev into main - version 1.4.8
This commit is contained in:
@@ -448,7 +448,8 @@ export function CalendarWeekView({
|
||||
{pendingPreview && !pendingPreview.allDay && isSameDay(pendingPreview.start, day) && (
|
||||
(() => {
|
||||
const startMin = pendingPreview.start.getHours() * 60 + pendingPreview.start.getMinutes();
|
||||
const endMin = pendingPreview.end.getHours() * 60 + pendingPreview.end.getMinutes();
|
||||
let endMin = pendingPreview.end.getHours() * 60 + pendingPreview.end.getMinutes();
|
||||
if (endMin <= startMin) endMin = 1440;
|
||||
const durationMin = Math.max(15, endMin - startMin);
|
||||
const cal = calendars.find(c => c.id === pendingPreview.calendarId);
|
||||
const color = cal?.color || "hsl(var(--primary))";
|
||||
|
||||
@@ -59,10 +59,12 @@ function buildDuration(startDate: Date, endDate: Date): string {
|
||||
const minutes = totalMinutes % 60;
|
||||
let dur = "P";
|
||||
if (days > 0) dur += `${days}D`;
|
||||
dur += "T";
|
||||
if (hours > 0) dur += `${hours}H`;
|
||||
if (minutes > 0) dur += `${minutes}M`;
|
||||
if (dur === "PT") dur = "PT0M";
|
||||
if (hours > 0 || minutes > 0) {
|
||||
dur += "T";
|
||||
if (hours > 0) dur += `${hours}H`;
|
||||
if (minutes > 0) dur += `${minutes}M`;
|
||||
}
|
||||
if (dur === "P") dur = "PT0M";
|
||||
return dur;
|
||||
}
|
||||
|
||||
@@ -83,9 +85,9 @@ function getAlertLabel(event: CalendarEvent, t: ReturnType<typeof useTranslation
|
||||
if (!first || first.trigger["@type"] !== "OffsetTrigger") return null;
|
||||
const offset = first.trigger.offset;
|
||||
if (offset === "PT0S") return t("alerts.at_time");
|
||||
const minMatch = offset.match(/-?PT?(\d+)M$/);
|
||||
const minMatch = offset.match(/-?PT(\d+)M$/);
|
||||
if (minMatch) return t("alerts.minutes_before", { count: parseInt(minMatch[1]) });
|
||||
const hourMatch = offset.match(/-?PT?(\d+)H$/);
|
||||
const hourMatch = offset.match(/-?PT(\d+)H$/);
|
||||
if (hourMatch) return t("alerts.hours_before", { count: parseInt(hourMatch[1]) });
|
||||
const dayMatch = offset.match(/-?P(\d+)D/);
|
||||
if (dayMatch) return t("alerts.days_before", { count: parseInt(dayMatch[1]) });
|
||||
@@ -209,9 +211,9 @@ export function EventModal({
|
||||
if (first.trigger["@type"] === "OffsetTrigger") {
|
||||
const offset = first.trigger.offset;
|
||||
if (offset === "PT0S") return "at_time";
|
||||
const minMatch = offset.match(/-?PT?(\d+)M$/);
|
||||
const minMatch = offset.match(/-?PT(\d+)M$/);
|
||||
if (minMatch) return minMatch[1] as AlertOption;
|
||||
const hourMatch = offset.match(/-?PT?(\d+)H$/);
|
||||
const hourMatch = offset.match(/-?PT(\d+)H$/);
|
||||
if (hourMatch) return String(parseInt(hourMatch[1]) * 60) as AlertOption;
|
||||
const dayMatch = offset.match(/-?P(\d+)D/);
|
||||
if (dayMatch) return String(parseInt(dayMatch[1]) * 1440) as AlertOption;
|
||||
@@ -384,7 +386,11 @@ export function EventModal({
|
||||
if (!event || !onDuplicate) return;
|
||||
const start = parseISO(event.start);
|
||||
const newStart = addDays(start, 1);
|
||||
const newUid = typeof crypto !== 'undefined' && crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
const data: Partial<CalendarEvent> = {
|
||||
uid: newUid,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start: format(newStart, "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
|
||||
@@ -943,11 +943,16 @@ export function EmailComposer({
|
||||
onChange={(e) => setSelectedIdentityId(e.target.value)}
|
||||
className="flex-1 bg-transparent text-sm text-foreground outline-none cursor-pointer hover:text-muted-foreground transition-colors min-w-0 truncate"
|
||||
>
|
||||
{identities.map((identity) => (
|
||||
<option key={identity.id} value={identity.id}>
|
||||
{identity.name ? `${identity.name} <${identity.email}>` : identity.email}
|
||||
</option>
|
||||
))}
|
||||
{identities.map((identity) => {
|
||||
const displayEmail = subAddressTag
|
||||
? generateSubAddress(identity.email, subAddressTag)
|
||||
: identity.email;
|
||||
return (
|
||||
<option key={identity.id} value={identity.id}>
|
||||
{identity.name ? `${identity.name} <${displayEmail}>` : displayEmail}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<span className="text-sm text-foreground flex-1 truncate">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatDate } from "@/lib/utils";
|
||||
import { Email } from "@/lib/jmap/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Paperclip, Star, Circle, CheckSquare, Square, Tag } from "lucide-react";
|
||||
import { Paperclip, Star, Circle, CheckSquare, Square, Tag, Reply, Forward } from "lucide-react";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
@@ -41,6 +41,8 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const isImportant = email.keywords?.["$important"];
|
||||
const isAnswered = email.keywords?.$answered;
|
||||
const isForwarded = email.keywords?.$forwarded;
|
||||
const sender = email.from?.[0];
|
||||
|
||||
// Resolve color tag using keyword definitions from settings
|
||||
@@ -175,6 +177,18 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
|
||||
</span>
|
||||
)}
|
||||
<EmailIdentityBadge email={email} identities={identities} compact={true} />
|
||||
{isAnswered && !isForwarded && (
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{isForwarded && !isAnswered && (
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{isAnswered && isForwarded && (
|
||||
<>
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{email.hasAttachment && (
|
||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { formatDate } from "@/lib/utils";
|
||||
import { Email } from "@/lib/jmap/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
|
||||
import { Paperclip, Star, Circle, CheckSquare, Square, Reply, Forward } from "lucide-react";
|
||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
@@ -29,6 +29,8 @@ export function ThreadEmailItem({
|
||||
}: ThreadEmailItemProps) {
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const isAnswered = email.keywords?.$answered;
|
||||
const isForwarded = email.keywords?.$forwarded;
|
||||
const sender = email.from?.[0];
|
||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
@@ -151,6 +153,18 @@ export function ThreadEmailItem({
|
||||
{isStarred && (
|
||||
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
|
||||
)}
|
||||
{isAnswered && !isForwarded && (
|
||||
<Reply className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
{isForwarded && !isAnswered && (
|
||||
<Forward className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
{isAnswered && isForwarded && (
|
||||
<>
|
||||
<Reply className="w-3 h-3 text-muted-foreground" />
|
||||
<Forward className="w-3 h-3 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{email.hasAttachment && (
|
||||
<Paperclip className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { formatDate } from "@/lib/utils";
|
||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square } from "lucide-react";
|
||||
import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward } from "lucide-react";
|
||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||
import { useUIStore } from "@/stores/ui-store";
|
||||
import { useEmailStore } from "@/stores/email-store";
|
||||
@@ -53,6 +53,8 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
function SingleEmailItem({ email, selected, onClick, onContextMenu, showPreview, colorTag, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetColorTag, onMarkAsSpam }, ref) {
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const isAnswered = email.keywords?.$answered;
|
||||
const isForwarded = email.keywords?.$forwarded;
|
||||
const sender = email.from?.[0];
|
||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
||||
@@ -182,6 +184,18 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
{isStarred && (
|
||||
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
||||
)}
|
||||
{isAnswered && !isForwarded && (
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{isForwarded && !isAnswered && (
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{isAnswered && isForwarded && (
|
||||
<>
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{email.hasAttachment && (
|
||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
@@ -267,7 +281,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, emailCount } = thread;
|
||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
|
||||
|
||||
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore();
|
||||
|
||||
@@ -482,6 +496,18 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
{hasStarred && (
|
||||
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
|
||||
)}
|
||||
{hasAnswered && !hasForwarded && (
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{hasForwarded && !hasAnswered && (
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{hasAnswered && hasForwarded && (
|
||||
<>
|
||||
<Reply className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<Forward className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{hasAttachment && (
|
||||
<Paperclip className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { isEmbedded, listenFromParent } from "@/lib/iframe-bridge";
|
||||
import { getPathPrefix, getLocaleFromPath } from "@/lib/browser-navigation";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
|
||||
@@ -16,9 +17,9 @@ export function EmbeddedBridgeProvider({ children }: { children: React.ReactNode
|
||||
switch (msg.type) {
|
||||
case "sso:trigger-login": {
|
||||
// Navigate to login page to start SSO flow
|
||||
const segments = window.location.pathname.split("/").filter(Boolean);
|
||||
const locale = segments[0] || "en";
|
||||
window.location.href = `/${locale}/login`;
|
||||
const prefix = getPathPrefix();
|
||||
const locale = getLocaleFromPath();
|
||||
window.location.href = `${prefix}/${locale}/login`;
|
||||
break;
|
||||
}
|
||||
case "sso:trigger-logout":
|
||||
|
||||
@@ -3,29 +3,44 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
import { SettingsSection, SettingItem } from './settings-section';
|
||||
import { formatFileSize } from '@/lib/utils';
|
||||
|
||||
export function AccountSettings() {
|
||||
const t = useTranslations('settings.account');
|
||||
const { username, serverUrl, isDemoMode, primaryIdentity } = useAuthStore();
|
||||
const { username, serverUrl, isDemoMode, primaryIdentity, authMode, activeAccountId } = useAuthStore();
|
||||
const { quota } = useEmailStore();
|
||||
const account = useAccountStore((s) => activeAccountId ? s.getAccountById(activeAccountId) : undefined);
|
||||
|
||||
const quotaPercentage = quota ? Math.round((quota.used / quota.total) * 100) : 0;
|
||||
const displayName = primaryIdentity?.name || (isDemoMode ? 'Demo User' : undefined);
|
||||
const displayName = primaryIdentity?.name || account?.displayName || (isDemoMode ? 'Demo User' : undefined);
|
||||
const email = primaryIdentity?.email || account?.email || username;
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Display Name (show in demo mode or when identity has a name) */}
|
||||
{displayName && (
|
||||
<SettingItem label={t('name_label')}>
|
||||
<span className="text-sm text-foreground">{displayName}</span>
|
||||
</SettingItem>
|
||||
)}
|
||||
{/* Display Name */}
|
||||
<SettingItem label={t('name_label')}>
|
||||
<span className="text-sm text-foreground">{displayName || t('../../common.unknown')}</span>
|
||||
</SettingItem>
|
||||
|
||||
{/* Email Address */}
|
||||
<SettingItem label={t('email.label')}>
|
||||
<span className="text-sm text-foreground">{username || t('../../common.unknown')}</span>
|
||||
<span className="text-sm text-foreground">{email || t('../../common.unknown')}</span>
|
||||
</SettingItem>
|
||||
|
||||
{/* Username / Login (show when it differs from email) */}
|
||||
{username && username !== email && (
|
||||
<SettingItem label={t('username_label')}>
|
||||
<span className="text-sm text-foreground">{username}</span>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Authentication Method */}
|
||||
<SettingItem label={t('auth_method_label')}>
|
||||
<span className="text-sm text-foreground">
|
||||
{authMode === 'oauth' ? t('auth_method_oauth') : t('auth_method_basic')}
|
||||
</span>
|
||||
</SettingItem>
|
||||
|
||||
{/* Server */}
|
||||
|
||||
@@ -16,9 +16,6 @@ export function CalendarSettings() {
|
||||
firstDayOfWeek,
|
||||
showTimeInMonthView,
|
||||
showWeekNumbers,
|
||||
calendarNotificationsEnabled,
|
||||
calendarNotificationSound,
|
||||
calendarInvitationParsingEnabled,
|
||||
enableCalendarTasks,
|
||||
showTasksOnCalendar,
|
||||
updateSetting,
|
||||
@@ -103,37 +100,6 @@ export function CalendarSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem
|
||||
label={t('notifications_enabled')}
|
||||
description={t('notifications_enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('calendarNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('notification_sound')}
|
||||
description={t('notification_sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationSound}
|
||||
onChange={(checked) => updateSetting('calendarNotificationSound', checked)}
|
||||
disabled={!calendarNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('invitation_parsing')}
|
||||
description={t('invitation_parsing_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarInvitationParsingEnabled}
|
||||
onChange={(checked) => updateSetting('calendarInvitationParsingEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import type { ArchiveMode, HoverAction } from '@/stores/settings-store';
|
||||
import { ALL_HOVER_ACTIONS } from '@/stores/settings-store';
|
||||
@@ -10,13 +11,26 @@ import { useEmailStore } from '@/stores/email-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
||||
import { ChevronRight, AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
|
||||
import { ChevronRight, AlertTriangle, FolderSync, Loader2, Mail } from 'lucide-react';
|
||||
|
||||
export function EmailSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const { appName } = useConfig();
|
||||
const [showTrustedModal, setShowTrustedModal] = useState(false);
|
||||
const [isReorganizing, setIsReorganizing] = useState(false);
|
||||
const [reorganizeResult, setReorganizeResult] = useState<string | null>(null);
|
||||
const [defaultMailStatus, setDefaultMailStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleSetDefaultMailProgram = useCallback(() => {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.registerProtocolHandler) {
|
||||
navigator.registerProtocolHandler('mailto', `${window.location.origin}/compose?mailto=%s`);
|
||||
setDefaultMailStatus('success');
|
||||
}
|
||||
} catch {
|
||||
setDefaultMailStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const {
|
||||
markAsReadDelay,
|
||||
@@ -280,6 +294,25 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Default Mail Program */}
|
||||
<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>
|
||||
|
||||
{/* Trusted Senders */}
|
||||
<SettingItem label={t('trusted_senders.label')} description={t('trusted_senders.description')}>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch, Select } from './settings-section';
|
||||
import { playNotificationSound, NOTIFICATION_SOUNDS } from '@/lib/notification-sound';
|
||||
import type { NotificationSoundChoice } from '@/lib/notification-sound';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Volume2 } from 'lucide-react';
|
||||
|
||||
export function NotificationSettings() {
|
||||
const t = useTranslations('settings.notifications');
|
||||
const {
|
||||
emailNotificationsEnabled,
|
||||
emailNotificationSound,
|
||||
notificationSoundChoice,
|
||||
calendarNotificationsEnabled,
|
||||
calendarNotificationSound,
|
||||
calendarInvitationParsingEnabled,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
|
||||
const soundOptions = NOTIFICATION_SOUNDS.map((s) => ({
|
||||
value: s.id,
|
||||
label: t(`sounds.${s.id}`),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SettingsSection title={t('sound_selection.title')} description={t('sound_selection.description')}>
|
||||
<SettingItem
|
||||
label={t('sound_selection.choose')}
|
||||
description={t('sound_selection.choose_desc')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => playNotificationSound(notificationSoundChoice)}
|
||||
title={t('test_sound')}
|
||||
>
|
||||
<Volume2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Select
|
||||
value={notificationSoundChoice}
|
||||
onChange={(value) => {
|
||||
const choice = value as NotificationSoundChoice;
|
||||
updateSetting('notificationSoundChoice', choice);
|
||||
playNotificationSound(choice);
|
||||
}}
|
||||
options={soundOptions}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('email.title')} description={t('email.description')}>
|
||||
<SettingItem
|
||||
label={t('email.enabled')}
|
||||
description={t('email.enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={emailNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('emailNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('email.sound')}
|
||||
description={t('email.sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={emailNotificationSound}
|
||||
onChange={(checked) => updateSetting('emailNotificationSound', checked)}
|
||||
disabled={!emailNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('calendar.title')} description={t('calendar.description')}>
|
||||
<SettingItem
|
||||
label={t('calendar.enabled')}
|
||||
description={t('calendar.enabled_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationsEnabled}
|
||||
onChange={(checked) => updateSetting('calendarNotificationsEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('calendar.sound')}
|
||||
description={t('calendar.sound_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarNotificationSound}
|
||||
onChange={(checked) => updateSetting('calendarNotificationSound', checked)}
|
||||
disabled={!calendarNotificationsEnabled}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('calendar.invitation_parsing')}
|
||||
description={t('calendar.invitation_parsing_desc')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={calendarInvitationParsingEnabled}
|
||||
onChange={(checked) => updateSetting('calendarInvitationParsingEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user