diff --git a/components/mail/__tests__/mail-app-list-ux.test.tsx b/components/mail/__tests__/mail-app-list-ux.test.tsx index 7f11c1bf9..b209cea91 100644 --- a/components/mail/__tests__/mail-app-list-ux.test.tsx +++ b/components/mail/__tests__/mail-app-list-ux.test.tsx @@ -37,8 +37,8 @@ vi.mock('@/components/ui/welcome-banner', () => ({ useWelcomeBannerDue: () => false, })); vi.mock('@/components/email/email-viewer', () => ({ - EmailViewer: ({ emptyPaneContent, emptyPaneFooter }: { emptyPaneContent?: React.ReactNode; emptyPaneFooter?: React.ReactNode }) => ( -
{emptyPaneContent ??

no conversation selected

}{emptyPaneFooter}
+ EmailViewer: ({ email, emptyPaneContent, emptyPaneFooter }: { email?: { id: string } | null; emptyPaneContent?: React.ReactNode; emptyPaneFooter?: React.ReactNode }) => ( +
{email ?

reading {email.id}

: <>{emptyPaneContent ??

no conversation selected

}{emptyPaneFooter}}
), })); vi.mock('@/components/email/email-list', () => ({ @@ -94,7 +94,7 @@ beforeEach(() => { viewingAccountId: null, accountMailboxes: {}, isUnifiedView: false, unifiedRole: null, isScheduledView: false, isLoadingEmail: false, isLoading: false, searchQuery: '', searchFilters: { ...DEFAULT_SEARCH_FILTERS }, searchMailboxId: '', isAdvancedSearchOpen: false, - selectedEmailKeys: new Set(), unreadSection: null, + selectedEmailKeys: new Set(), unreadSection: null, listRows: null, fetchMailboxes: vi.fn().mockResolvedValue(undefined), fetchEmails: vi.fn().mockResolvedValue(undefined), fetchQuota: vi.fn().mockResolvedValue(undefined), fetchTagCounts: vi.fn().mockResolvedValue(undefined), refreshScheduledMetadata: vi.fn().mockResolvedValue(undefined), @@ -138,6 +138,35 @@ describe('the reading pane for a selection', () => { expect(screen.getByTestId('viewer').contains(listProps.host!)).toBe(true); }); + it('replaces the open message once other rows are ticked too (mail-p2-3)', () => { + useEmailStore.setState({ selectedEmail: message, selectedEmailKeys: new Set(['m1', 'm2']) } as never); + render(); + expect(screen.queryByText('reading m1')).toBeNull(); + expect(listProps.host).toBeInstanceOf(HTMLElement); + expect(screen.getByTestId('viewer').contains(listProps.host!)).toBe(true); + }); + + it('replaces the open message when another single row is ticked', () => { + useEmailStore.setState({ selectedEmail: message, selectedEmailKeys: new Set(['m2']) } as never); + render(); + expect(screen.queryByText('reading m1')).toBeNull(); + }); + + it('keeps the open message when only its own row is ticked', () => { + useEmailStore.setState({ selectedEmail: message, selectedEmailKeys: new Set(['m1']) } as never); + render(); + expect(screen.getByText('reading m1')).toBeTruthy(); + }); + + it('keeps the open message when its conversation row is ticked', () => { + useEmailStore.setState({ + selectedEmail: message, selectedEmailKeys: new Set(['m1', 'm1-reply']), + listRows: [{ key: 'm1', members: ['m1', 'm1-reply'] }, { key: 'm2', members: ['m2'] }], + } as never); + render(); + expect(screen.getByText('reading m1')).toBeTruthy(); + }); + it('says nothing is selected without a selection', () => { render(); expect(screen.getByText('no conversation selected')).toBeTruthy(); diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index bdc888cfc..4853163de 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -4927,8 +4927,23 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { const dockedReply = dockCandidate && dockedSessionRef.current === composerSessionId; const composerHost = floatingComposer ? 'window' : mvInlineReply ? 'inline' : dockedReply ? 'dock' : 'pane'; composerHostRef.current = composerHost; + // Rows ticked beyond the open message take the reading pane (mail-p2-3): + // the selection bar and the keys act on the ticked rows, so a reader + // toolbar acting on the one open message would be a second, different set. + // Unticking brings the message back. Mountain View keeps the message while + // rows are ticked (#1231); the focus layout can't tick rows while reading. + const openRowKeys = (() => { + if (!selectedEmail) return null; + const key = emailKeyFor(selectedEmail); + // A conversation row ticks all its messages: the open one's whole row. + const row = useEmailStore.getState().listRows?.find((r) => r.key === key || r.members.includes(key)); + return new Set(row ? [key, ...row.members] : [key]); + })(); + const selectionTakesReader = !isMobile && !isFocusedMailLayout && !mvShell && !isScheduledView + && selectedEmailKeys.size > 0 + && !(openRowKeys && [...selectedEmailKeys].every((key) => openRowKeys.has(key))); const hasViewerContent = (showComposer && !floatingComposer) || Boolean(conversationThread) - || Boolean(selectedEmail && !(mvShell && cursorOnly)) || deepLinkPane !== null; + || Boolean(selectedEmail && !(mvShell && cursorOnly) && !selectionTakesReader) || deepLinkPane !== null; // A linked message on a phone opens over the list, as one picked there does. const mobileView = deepLinkPane !== null && isMobile ? 'viewer' : activeView; const shouldCollapseListPane = !isMobile && isFocusedMailLayout && hasViewerContent; @@ -6106,9 +6121,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { <>