diff --git a/components/contacts/contacts-app.tsx b/components/contacts/contacts-app.tsx index 5e4a23a15..1926ab5a7 100644 --- a/components/contacts/contacts-app.tsx +++ b/components/contacts/contacts-app.tsx @@ -392,6 +392,13 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) { const handleCreateNew = () => { setSelectedContact(null); + // When a specific address book is being viewed, create the new contact in + // that book instead of falling back to the account's default book. + if (typeof activeCategory === "object" && "addressBookId" in activeCategory) { + setDefaultBookIdForCreate(activeCategory.addressBookId); + } else { + setDefaultBookIdForCreate(undefined); + } setView("create"); }; @@ -962,7 +969,8 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) { } : undefined} onCreateContactInBook={(book) => { setDefaultBookIdForCreate(book.id); - handleCreateNew(); + setSelectedContact(null); + setView("create"); }} onDeleteAddressBook={client ? async (book) => { const ok = await confirmDialog({ diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index a3c12c4d9..37eae2669 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -156,6 +156,11 @@ export function EmailList({ // open it shrinks the list and would shove every row downwards. Feed each // height change back into scrollTop so the rows stay put on screen (and // slide back when the toolbar collapses again). + // + // Except at the very top: there are no rows above to hold steady, so the + // compensation just scrolls the first message underneath the toolbar and + // reads as the toolbar covering the message you selected. Let the list + // move down there instead. useEffect(() => { const toolbar = batchToolbarRef.current; if (!toolbar || typeof ResizeObserver === 'undefined') return; @@ -166,7 +171,8 @@ export function EmailList({ lastHeight = height; const list = parentRef.current; if (!list || delta === 0) return; - list.scrollTop += delta; + if (delta > 0 && list.scrollTop <= 0) return; + list.scrollTop = Math.max(0, list.scrollTop + delta); }); observer.observe(toolbar); return () => observer.disconnect(); diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index 2f77adda3..923d2fbd3 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -2364,6 +2364,20 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { // Pro sidebar: user clicked a folder under a specific account group. // accountId === null means the active account; non-null means a viewing // override that fetches via that account's JMAP client. + // When "Clear search when switching folders" is on, a folder click drops + // any active text query + advanced filters and browses the folder instead + // of re-running the search there (#553 is the default: search follows you + // across folders). Returns true when it cleared, so the caller browses + // rather than re-searching. Reads the current query/filters from the + // closure to decide whether there was anything to clear. + const clearSearchIfFolderChangeResets = (): boolean => { + if (!useSettingsStore.getState().clearSearchOnFolderChange) return false; + if (isFilterEmpty(searchFilters) && !searchQuery) return false; + setSearchQuery(""); + clearSearchFilters(); + return true; + }; + const handleAccountMailboxSelect = async (accountId: string | null, mailboxId: string) => { const viewingClient = accountId ? useAuthStore.getState().getClientForAccount(accountId) ?? client @@ -2378,11 +2392,13 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { setTabletListVisible(true); } if (viewingClient) { - // Keep an active search applied when switching folders (#553); the - // store actions resolve the viewing account's client internally. - if (!isFilterEmpty(searchFilters)) { + // Keep an active search applied when switching folders (#553), unless + // the user opted to reset search on folder change. The store actions + // resolve the viewing account's client internally. + const searchCleared = clearSearchIfFolderChangeResets(); + if (!searchCleared && !isFilterEmpty(searchFilters)) { await advancedSearch(viewingClient); - } else if (searchQuery) { + } else if (!searchCleared && searchQuery) { await searchEmails(viewingClient, searchQuery); } else { await fetchEmails(viewingClient, mailboxId); @@ -2432,9 +2448,10 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { } const populated = await buildPopulatedUnifiedAccounts(); + const searchCleared = clearSearchIfFolderChangeResets(); // Keep an active search across the switch and re-run it in this view // (mirrors normal mailboxes), preserving advanced filters; otherwise browse. - if (client && (!isFilterEmpty(searchFilters) || searchQuery)) { + if (client && !searchCleared && (!isFilterEmpty(searchFilters) || searchQuery)) { useEmailStore.setState({ isUnifiedView: true, unifiedRole: role, crossView: null }); if (!isFilterEmpty(searchFilters)) { await advancedSearch(client); @@ -2465,9 +2482,10 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { } const populated = await buildPopulatedUnifiedAccounts(); + const searchCleared = clearSearchIfFolderChangeResets(); // Keep an active search across the switch and re-run it in this view // (mirrors normal mailboxes), preserving advanced filters; otherwise browse. - if (client && (!isFilterEmpty(searchFilters) || searchQuery)) { + if (client && !searchCleared && (!isFilterEmpty(searchFilters) || searchQuery)) { useEmailStore.setState({ isUnifiedView: true, crossView: view, unifiedRole: null }); if (!isFilterEmpty(searchFilters)) { await advancedSearch(client); @@ -2501,13 +2519,15 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { } if (client) { - // If there's an active search, re-run it in the new mailbox. Advanced - // filters must go through advancedSearch (which also includes the text - // query) — falling back to fetchEmails would silently drop them while - // the UI still shows them as active (#553). - if (!isFilterEmpty(searchFilters)) { + // If there's an active search, re-run it in the new mailbox unless the + // user opted to reset search on folder change. Advanced filters must go + // through advancedSearch (which also includes the text query) — falling + // back to fetchEmails would silently drop them while the UI still shows + // them as active (#553). + const searchCleared = clearSearchIfFolderChangeResets(); + if (!searchCleared && !isFilterEmpty(searchFilters)) { await advancedSearch(client); - } else if (searchQuery) { + } else if (!searchCleared && searchQuery) { await searchEmails(client, searchQuery); } else { await fetchEmails(client, mailboxId); diff --git a/components/settings/reading-settings.tsx b/components/settings/reading-settings.tsx index 98057559d..4178185e9 100644 --- a/components/settings/reading-settings.tsx +++ b/components/settings/reading-settings.tsx @@ -25,6 +25,7 @@ export function ReadingSettings() { returnToListAfterAction, swipeRightAction, swipeLeftAction, + clearSearchOnFolderChange, showPreview, mailLayout, disableThreading, @@ -245,6 +246,13 @@ export function ReadingSettings() { /> + + updateSetting('clearSearchOnFolderChange', checked)} + /> + + {!isSettingHidden('showPreview') && ( ()( markAsReadDelay: state.markAsReadDelay, deleteAction: state.deleteAction, returnToListAfterAction: state.returnToListAfterAction, + clearSearchOnFolderChange: state.clearSearchOnFolderChange, showPreview: state.showPreview, mailLayout: state.mailLayout, emailsPerPage: state.emailsPerPage,