fix: fall back when only truncation indicator remains in email preview
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate, stripInvisibleLeading } from "@/lib/utils";
|
||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
@@ -51,7 +51,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
|
|||||||
const sender = showRecipient ? (email.to?.[0] ?? email.from?.[0]) : email.from?.[0];
|
const sender = showRecipient ? (email.to?.[0] ?? email.from?.[0]) : email.from?.[0];
|
||||||
const isFocusedMailLayout = mailLayout === 'focus';
|
const isFocusedMailLayout = mailLayout === 'focus';
|
||||||
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
|
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
|
||||||
const trimmedPreview = email.preview?.replace(/^\s+/, '') ?? '';
|
const trimmedPreview = stripInvisibleLeading(email.preview ?? '');
|
||||||
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
||||||
|
|
||||||
// Resolve color tags using keyword definitions from settings; unknown tags fall back to gray
|
// Resolve color tags using keyword definitions from settings; unknown tags fall back to gray
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate, stripInvisibleLeading } from "@/lib/utils";
|
||||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
@@ -71,7 +71,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined;
|
const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined;
|
||||||
const isChecked = selectedEmailIds.has(email.id);
|
const isChecked = selectedEmailIds.has(email.id);
|
||||||
const isFocusedMailLayout = mailLayout === 'focus';
|
const isFocusedMailLayout = mailLayout === 'focus';
|
||||||
const trimmedPreview = email.preview?.replace(/^\s+/, '') ?? '';
|
const trimmedPreview = stripInvisibleLeading(email.preview ?? '');
|
||||||
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
||||||
|
|
||||||
// Resolve color tags using keyword definitions; unknown tags fall back to gray
|
// Resolve color tags using keyword definitions; unknown tags fall back to gray
|
||||||
@@ -367,7 +367,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
|
const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
|
||||||
const isFocusedMailLayout = mailLayout === 'focus';
|
const isFocusedMailLayout = mailLayout === 'focus';
|
||||||
const trimmedPreview = latestEmail.preview?.replace(/^\s+/, '') ?? '';
|
const trimmedPreview = stripInvisibleLeading(latestEmail.preview ?? '');
|
||||||
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : '';
|
||||||
|
|
||||||
const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView } = useEmailStore();
|
const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView } = useEmailStore();
|
||||||
|
|||||||
+11
-6
@@ -78,14 +78,19 @@ export function formatDateTime(
|
|||||||
return d.toLocaleString(undefined, localeOptions);
|
return d.toLocaleString(undefined, localeOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marketing emails often pad the preheader with invisible Unicode (combining
|
// Marketing emails pad the preheader with whitespace, format chars (soft
|
||||||
// grapheme joiners, soft hyphens, zero-width chars) alongside whitespace, to
|
// hyphens, zero-width chars, BOM, directional marks) and combining marks
|
||||||
// push real content out of the preview window. \s catches normal whitespace
|
// (e.g. U+034F) to push real content past the preview window. Strip them all.
|
||||||
// including figure space U+2007; we also strip the common invisible formatters.
|
// \p{Cf} = Format, \p{Mn} = combining marks; \s covers figure space, NBSP, etc.
|
||||||
const LEADING_INVISIBLE_RE = /^[\s\u00AD\u034F\u200B-\u200F\u2060-\u2064\uFEFF]+/;
|
const LEADING_INVISIBLE_RE = /^[\s\p{Cf}\p{Mn}]+/u;
|
||||||
|
// After stripping, a server-side truncation indicator like "..." may be all
|
||||||
|
// that's left. Treat that as no preview so callers can fall back.
|
||||||
|
const ONLY_PUNCTUATION_RE = /^[.\u2026\s]+$/;
|
||||||
|
|
||||||
export function stripInvisibleLeading(text: string): string {
|
export function stripInvisibleLeading(text: string): string {
|
||||||
return text.replace(LEADING_INVISIBLE_RE, '');
|
const stripped = text.replace(LEADING_INVISIBLE_RE, '');
|
||||||
|
if (ONLY_PUNCTUATION_RE.test(stripped)) return '';
|
||||||
|
return stripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truncateText(text: string, maxLength: number): string {
|
export function truncateText(text: string, maxLength: number): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user