From ee33f9ef29619c4d9ed7532d623c6388b2b8d8cc Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:41:56 +0100 Subject: [PATCH] fix: put the preview after the subject at the regular density so about ten rows fit The split-layout column test looked for a preview paragraph to tell a stacked row; a regular row now carries its preview on the subject line, so the test accepts either. --- .../__tests__/list-row-preview-lines.test.tsx | 46 ++++++++++++------- .../thread-list-item-columns.test.tsx | 4 +- components/email/email-list.tsx | 10 ++-- components/email/thread-list-item.tsx | 22 +++++++-- hooks/use-list-row-shape.ts | 16 +++++++ 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/components/email/__tests__/list-row-preview-lines.test.tsx b/components/email/__tests__/list-row-preview-lines.test.tsx index a32f33131..366ab7709 100644 --- a/components/email/__tests__/list-row-preview-lines.test.tsx +++ b/components/email/__tests__/list-row-preview-lines.test.tsx @@ -54,24 +54,38 @@ beforeEach(() => { useEmailStore.setState({ selectedEmailKeys: new Set(), selectedMailbox: 'inbox' }); }); -// mail-15: the default density shows one preview line, so more rows fit; -// comfortable keeps two. +// mail-15: the default density puts the preview after the subject, on the +// subject's line, so about ten rows fit at 1366×768 (they were about 96 px, +// seven on screen); comfortable keeps two preview lines, and a phone keeps +// the preview under the subject. describe('list row preview lines', () => { - it.each([ - ['regular', 'line-clamp-1'], - ['comfortable', 'line-clamp-2'], - ] as const)('a %s row clamps its preview with %s', (density, clamp) => { - useSettingsStore.setState({ density }); - renderRow([makeEmail()]); - expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain(clamp); - }); + const onSubjectLine = () => { + const preview = screen.getByText(PREVIEW, { exact: false }); + return preview.closest('[data-row-preview]') !== null; + }; it.each([ - ['regular', 'line-clamp-1'], - ['comfortable', 'line-clamp-2'], - ] as const)('a %s conversation row clamps its preview with %s', (density, clamp) => { - useSettingsStore.setState({ density }); - renderRow([makeEmail({ id: 'a' }), makeEmail({ id: 'b', receivedAt: '2024-01-16T10:00:00Z' })]); - expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain(clamp); + ['message', [makeEmail()]], + ['conversation', [makeEmail({ id: 'a' }), makeEmail({ id: 'b', receivedAt: '2024-01-16T10:00:00Z' })]], + ] as const)('a regular %s row shows its preview on the subject line', (_kind, emails) => { + useSettingsStore.setState({ density: 'regular' }); + renderRow([...emails]); + expect(onSubjectLine()).toBe(true); + expect(document.querySelector('p.line-clamp-1')).toBeNull(); + }); + + it('a comfortable row keeps two preview lines of its own', () => { + useSettingsStore.setState({ density: 'comfortable' }); + renderRow([makeEmail()]); + expect(onSubjectLine()).toBe(false); + expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain('line-clamp-2'); + }); + + it('a regular row on a phone keeps the preview under the subject', () => { + useUIStore.setState({ isMobile: true }); + useSettingsStore.setState({ density: 'regular' }); + renderRow([makeEmail()]); + expect(onSubjectLine()).toBe(false); + expect(screen.getByText(PREVIEW).className.split(/\s+/)).toContain('line-clamp-1'); }); }); diff --git a/components/email/__tests__/thread-list-item-columns.test.tsx b/components/email/__tests__/thread-list-item-columns.test.tsx index 98c275ee0..ed27738ce 100644 --- a/components/email/__tests__/thread-list-item-columns.test.tsx +++ b/components/email/__tests__/thread-list-item-columns.test.tsx @@ -93,7 +93,9 @@ describe('message list rows under the bottom reading pane (#262)', () => { it('keeps stacked rows in the split layout', () => { useSettingsStore.setState({ mailLayout: 'split' }); const { container } = renderEmails([makeEmail()]); - expect(container.querySelector('p')).not.toBeNull(); + // Stacked: a preview line of its own, or the preview after the subject + // on the subject's line at the regular density (mail-15). + expect(container.querySelector('p, [data-row-preview]')).not.toBeNull(); }); describe('as columns', () => { diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index c8d3fa426..307d3159e 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -38,7 +38,7 @@ import { useOpenEmailInWindow } from "@/hooks/use-popouts-available"; import { useIsDesktop } from "@/hooks/use-media-query"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; import { useSettingsStore } from "@/stores/settings-store"; -import { useListRowShape } from "@/hooks/use-list-row-shape"; +import { useListRowShape, showsPreviewOnSubjectLine } from "@/hooks/use-list-row-shape"; import { MailListRowsSkeleton, type MailRowShape } from "@/components/boot-skeleton/mail-skeleton"; import type { Density } from "@/lib/density"; import { useUIStore } from "@/stores/ui-store"; @@ -491,7 +491,11 @@ export function EmailList({ // up after a jump down (scrollbar drag, End). So guess per row. const latest = item?.kind === 'row' ? item.thread.latestEmail : undefined; let size = { 'extra-compact': 32, compact: 60, regular: 84, comfortable: 104 }[density]; - if (showPreview && density !== 'extra-compact') { + if (showsPreviewOnSubjectLine({ showPreview, density, isMobile, mountainView: mountainViewList })) { + // The preview follows the subject on its line, and the subject line + // drops its margin (mail-15). + size -= 4; + } else if (showPreview && density !== 'extra-compact') { // A mail without a preview draws a one-line "No preview available", // a line (23px) shorter than a real one. Only comfortable rows show a // second preview line (mail-15). @@ -502,7 +506,7 @@ export function EmailList({ const hasCode = !!latest && showVerificationCodes && !!listVerificationCode(latest); if (latest && (hasCode || (onOpenAttachment && listRowShowsChips(latest, loadAttachments)))) size += 28; return size; - }, [density, isFocusedMailLayout, showPreview, showVerificationCodes, items, onOpenAttachment, loadAttachments]); + }, [density, isFocusedMailLayout, showPreview, showVerificationCodes, items, onOpenAttachment, loadAttachments, isMobile, mountainViewList]); // Stable per list, so the virtualizer does not rebuild every row's // measurement on each scroll render. diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index a65986286..a54107ee4 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -16,7 +16,7 @@ import { Paperclip, Star, Pin, Circle, ChevronRight, ChevronDown, Loader2, Messa import type { AppIcon } from "@/components/icons"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; import { useShallow } from "zustand/react/shallow"; -import { useListRowShape, LIST_COLUMN_CLASS } from "@/hooks/use-list-row-shape"; +import { useListRowShape, LIST_COLUMN_CLASS, showsPreviewOnSubjectLine } from "@/hooks/use-list-row-shape"; import { SizeCell } from "./list-columns"; import { accountTintKey, generateAvatarColor } from "@/lib/account-utils"; import { useUIStore } from "@/stores/ui-store"; @@ -498,6 +498,7 @@ const SingleEmailItem = React.forwardRef( // together. const mvRow = interfaceLayout === 'mountain-view' && oneLineRow; const mvMobileRow = interfaceLayout === 'mountain-view' && isMobile; + const previewOnSubjectLine = showsPreviewOnSubjectLine({ showPreview, density, isMobile, mountainView: interfaceLayout === 'mountain-view' }); const trimmedPreview = cleanPreview(email.preview); const inlinePreview = showPreview && trimmedPreview ? ` ${trimmedPreview}` : ''; // Search hits carry server snippets with the matched terms marked; they @@ -971,7 +972,7 @@ const SingleEmailItem = React.forwardRef( -
+
{tagPlacement === 'subject' && tagIds.length > 0 && ( {tagIds.map((id) => ( @@ -986,11 +987,16 @@ const SingleEmailItem = React.forwardRef( : "font-normal text-foreground/90" )}> {subjectSnippet ? : (email.subject || tNoSubject)} + {previewOnSubjectLine && ( + + {' – '}{previewSnippet ? : (trimmedPreview || t('no_preview_available'))} + + )}
- {showPreview && density !== 'extra-compact' && density !== 'compact' && ( + {showPreview && !previewOnSubjectLine && density !== 'extra-compact' && density !== 'compact' && (

-
+
{tagPlacement === 'subject' && tagIds.length > 0 && ( {tagIds.map((id) => ( @@ -1774,11 +1781,16 @@ export const ThreadListItem = React.forwardRef {subjectSnippet ? : (latestEmail.subject || tNoSubject)} + {previewOnSubjectLine && ( + + {' – '}{previewSnippet ? : (trimmedPreview || tEmailViewer('no_preview_available'))} + + )}
- {showPreview && density !== 'extra-compact' && density !== 'compact' && ( + {showPreview && !previewOnSubjectLine && density !== 'extra-compact' && density !== 'compact' && (