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,