feat: implement time format preference across calendar and email components

This commit is contained in:
Linus Rath
2026-03-17 21:39:59 +01:00
parent 828ad4df72
commit 5c933a595f
9 changed files with 71 additions and 40 deletions
@@ -360,6 +360,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
const client = useAuthStore((s) => s.client);
const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email);
const calendarInvitationParsingEnabled = useSettingsStore((s) => s.calendarInvitationParsingEnabled);
const timeFormat = useSettingsStore((s) => s.timeFormat);
const { calendars, supportsCalendar, importEvents, rsvpEvent, updateEvent, events: storeEvents, setSelectedDate } = useCalendarStore();
const [state, setState] = useState<BannerState>('loading');
@@ -679,6 +680,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: timeFormat === '12h',
});
};
+6 -4
View File
@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock } from "lucide-react";
import { cn, formatFileSize } from "@/lib/utils";
import { cn, formatFileSize, formatDateTime } from "@/lib/utils";
import { debug } from "@/lib/debug";
import { toast } from "@/stores/toast-store";
import { sanitizeEmailHtml } from "@/lib/email-sanitization";
@@ -14,6 +14,7 @@ import { useAuthStore } from "@/stores/auth-store";
import { useIdentityStore } from "@/stores/identity-store";
import { useSmimeStore } from "@/stores/smime-store";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { buildMimeMessage, wrapCmsAsSmimeMessage } from "@/lib/smime/mime-builder";
import type { MimeAttachment } from "@/lib/smime/mime-builder";
import { smimeSign } from "@/lib/smime/smime-sign";
@@ -86,6 +87,7 @@ export function EmailComposer({
}: EmailComposerProps) {
const t = useTranslations('email_composer');
const tCommon = useTranslations('common');
const timeFormat = useSettingsStore((state) => state.timeFormat);
// Initialize with reply/forward data if provided
const getInitialTo = () => {
@@ -124,7 +126,7 @@ export function EmailComposer({
const prefix = initialDraftText || "";
if (!replyTo?.body && !replyTo?.htmlBody) return prefix;
const date = replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : "";
const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : "";
const from = replyTo.from?.[0];
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
@@ -669,7 +671,7 @@ export function EmailComposer({
const signatureHtml = currentIdentity?.textSignature
? `<br><br>-- <br>${currentIdentity.textSignature.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')}`
: '';
const date = replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : '';
const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
const fromAddr = replyTo.from?.[0];
const fromStr = fromAddr ? `${fromAddr.name || fromAddr.email}` : tCommon('unknown');
const quoteHeader = mode === 'forward'
@@ -1092,7 +1094,7 @@ export function EmailComposer({
<div className="px-4 py-2 text-xs text-muted-foreground">
{mode === 'forward'
? `---------- ${t('prefix.forward')} ----------`
: `${replyTo.receivedAt ? new Date(replyTo.receivedAt).toLocaleString() : ''}, ${replyTo.from?.[0]?.name || replyTo.from?.[0]?.email || tCommon('unknown')}:`
: `${replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ''}, ${replyTo.from?.[0]?.name || replyTo.from?.[0]?.email || tCommon('unknown')}:`
}
</div>
<div
+6 -28
View File
@@ -7,7 +7,7 @@ import { Email, ContactCard, Mailbox } from "@/lib/jmap/types";
import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/email-sanitization";
import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import { formatFileSize, cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime } from "@/lib/utils";
import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers";
import {
Reply,
@@ -818,6 +818,7 @@ export function EmailViewer({
const toolbarPosition = useSettingsStore((state) => state.toolbarPosition);
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
const timeFormat = useSettingsStore((state) => state.timeFormat);
// Detect if current mailbox is Junk folder
const isInJunkFolder = currentMailboxRole === 'junk';
@@ -2224,7 +2225,7 @@ export function EmailViewer({
const handlePrint = () => {
if (!email) return;
const printSender = email.from?.[0];
const date = email.sentAt ? new Date(email.sentAt).toLocaleString() : '';
const date = email.sentAt ? formatDateTime(email.sentAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : '';
const toList = email.to?.map(r => r.name ? `${r.name} &lt;${r.email}&gt;` : r.email).join(', ') || '';
const ccList = email.cc?.map(r => r.name ? `${r.name} &lt;${r.email}&gt;` : r.email).join(', ') || '';
@@ -2976,14 +2977,7 @@ export function EmailViewer({
<div className="flex items-center gap-2 lg:gap-3 mt-1 lg:mt-1.5 text-xs lg:text-sm text-muted-foreground">
<span className="flex items-center gap-1 lg:gap-1.5 whitespace-nowrap">
<Clock className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
{new Date(email.receivedAt).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
</span>
{isImportant && (
<span className="px-1.5 lg:px-2 py-0.5 bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full text-xs font-medium whitespace-nowrap">
@@ -3458,14 +3452,7 @@ export function EmailViewer({
{/* Date and size on the right */}
<div className="text-right flex-shrink-0">
<div className="text-sm text-muted-foreground whitespace-nowrap">
{new Date(email.receivedAt).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
</div>
{email.size > 0 && (
<div className="text-xs text-muted-foreground/70 mt-0.5">
@@ -3596,16 +3583,7 @@ export function EmailViewer({
<div className="flex items-start gap-2">
<span className="text-muted-foreground font-medium w-12 shrink-0">{t('date')}:</span>
<span className="text-foreground">
{new Date(email.receivedAt).toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
})}
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', second: '2-digit', timeZoneName: 'short' })}
</span>
</div>
{/* Reply-To if different */}