diff --git a/components/email/__tests__/thread-list-item.test.tsx b/components/email/__tests__/thread-list-item.test.tsx index 340bd02c9..47c79b1a6 100644 --- a/components/email/__tests__/thread-list-item.test.tsx +++ b/components/email/__tests__/thread-list-item.test.tsx @@ -296,6 +296,44 @@ describe('ThreadListItem opening a thread on the phone', () => { }); }); +// #1231: opening a message threw away the ticked rows. Mountain View on the +// desktop keeps them, as Gmail does, so selecting can carry on after reading. +describe('ThreadListItem keeping the selection when a message opens', () => { + function open(interfaceLayout: 'mountain-view' | 'default', emails: Email[]) { + useSettingsStore.setState({ interfaceLayout, mailLayout: 'split' }); + useUIStore.setState({ isMobile: false, isTablet: false, isDesktop: true }); + useEmailStore.setState({ selectedEmailKeys: new Set(['other']), selectedMailbox: 'inbox' }); + const onEmailSelect = vi.fn(); + const [thread] = groupEmailsByThread(emails); + render( + {}} onEmailSelect={onEmailSelect} />, + ); + fireEvent.click(screen.getByText('Test Subject')); + return onEmailSelect; + } + const conversation = () => [ + makeEmail({ id: 'older', receivedAt: '2024-01-14T10:00:00Z' }), + makeEmail({ id: 'newer', receivedAt: '2024-01-15T10:00:00Z' }), + ]; + + it('keeps the ticked rows when a Mountain View message opens', () => { + const onEmailSelect = open('mountain-view', [makeEmail()]); + expect(onEmailSelect).toHaveBeenCalledTimes(1); + expect([...useEmailStore.getState().selectedEmailKeys]).toEqual(['other']); + }); + + it('keeps them when a Mountain View conversation opens', () => { + const onEmailSelect = open('mountain-view', conversation()); + expect(onEmailSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'newer' })); + expect([...useEmailStore.getState().selectedEmailKeys]).toEqual(['other']); + }); + + it('still clears them in the default layout', () => { + open('default', [makeEmail()]); + expect(useEmailStore.getState().selectedEmailKeys.size).toBe(0); + }); +}); + describe('ThreadListItem Mountain View phone row layout', () => { it('sets the star at the end of the preview line, not under the time', () => { useSettingsStore.setState({ interfaceLayout: 'mountain-view', mailLayout: 'split', showPreview: true, density: 'regular' } as never); diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index 4699cd877..f40a91e07 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -438,7 +438,9 @@ const SingleEmailItem = React.forwardRef( e.preventDefault(); selectRangeEmails(email); } else { - if (selectedEmailKeys.size > 0) clearSelection(); + // Mountain View keeps the ticked rows while a message is read, as + // Gmail does, so selecting can carry on afterwards. (#1231) + if (selectedEmailKeys.size > 0 && !mvRow) clearSelection(); onClick(); } }; @@ -971,7 +973,8 @@ export const ThreadListItem = React.forwardRef 0) clearSelection(); + // Desktop keeps the ticked rows while the conversation is read (#1231). + if (selectedEmailKeys.size > 0 && !mvRow) clearSelection(); onEmailSelect(latestEmail); return; } diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index dcbef0d7f..385bbbdaa 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -804,7 +804,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { }, onArchive: async () => { if (isScheduledView) return; - if (selectedEmailKeys.size > 0 && client) { + if (shortcutsTargetSelection && client) { const count = selectedEmailKeys.size; await runBatchEmailAction(() => batchArchive(client), { success: t('notifications.emails_archived', { count }), @@ -819,7 +819,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { }, onDelete: async () => { if (isScheduledView) return; - if (selectedEmailKeys.size > 0 && client) { + if (shortcutsTargetSelection && client) { const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); const permanent = bulkDeleteIsPermanent(currentMailbox); if (shouldConfirmBulkDelete(currentMailbox)) { @@ -849,7 +849,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { onMarkAsUnread: async () => { if (isScheduledView) return; if (!client) return; - if (selectedEmailKeys.size > 0) { + if (shortcutsTargetSelection) { await batchMarkAsRead(client, false); } else if (selectedEmail) { await markAsRead(client, selectedEmail.id, false); @@ -858,7 +858,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { onMarkAsRead: async () => { if (isScheduledView) return; if (!client) return; - if (selectedEmailKeys.size > 0) { + if (shortcutsTargetSelection) { await batchMarkAsRead(client, true); } else if (selectedEmail) { await markAsRead(client, selectedEmail.id, true); @@ -872,7 +872,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { // is a no-op there too. if (['sent', 'drafts', 'scheduled'].includes(currentMailbox?.role || '')) return; const isInJunk = currentMailbox?.role === 'junk'; - if (selectedEmailKeys.size > 0 && client) { + if (shortcutsTargetSelection && client) { // The spam actions take bare ids of the viewed account: resolve the // selection keys through the loaded rows and thread members. const ids = resolveSelectedEmails(useEmailStore.getState()).map(e => e.id); @@ -981,6 +981,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { }), [activeEmails, selectedEmail, client, selectedMailbox, isMobile, isTablet, selectedEmailKeys, mailboxes, isScheduledView, mvShell, showCrossStarred]); // Initialize keyboard shortcuts + // Mountain View keeps the ticked rows while a message is read (#1231). The + // shortcuts then act on the message on screen, as its toolbar does, and + // reach the selection again back in the list. + const shortcutsTargetSelection = selectedEmailKeys.size > 0 && !(mvShell && !cursorOnly && selectedEmail); useKeyboardShortcuts({ enabled: isAuthenticated && !showComposer, emails: activeEmails,