diff --git a/src/lib/__tests__/list-view-key.test.ts b/src/lib/__tests__/list-view-key.test.ts new file mode 100644 index 00000000..c2358a36 --- /dev/null +++ b/src/lib/__tests__/list-view-key.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { listViewKey } from '../list-view-key'; + +// The list kept its scroll offset when the folder, tag or search changed, so +// the new list opened somewhere in the middle (webmail #1138). + +describe('list scroll position on a view switch (bulwarkmail/webmail#1138)', () => { + it('changes with the folder, the search and the tag, and only with them', () => { + const base = listViewKey('inbox', '', {}); + expect(listViewKey('inbox', '', {})).toBe(base); + expect(listViewKey('inbox', ' ', {})).toBe(base); + expect(listViewKey('archive', '', {})).not.toBe(base); + expect(listViewKey('inbox', 'invoice', {})).not.toBe(base); + expect(listViewKey('inbox', '', { keyword: '$label:red' })).not.toBe(base); + }); + + it('scrolls the list to the top when it changes', () => { + const list = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailListScreen.tsx'), 'utf8'); + expect(list).toMatch(/ref=\{listRef\}/); + expect(list).toMatch(/listRef\.current\?\.scrollToOffset\(\{ offset: 0, animated: false \}\);\s*\}, \[viewKey\]\);/); + }); +}); diff --git a/src/lib/list-view-key.ts b/src/lib/list-view-key.ts new file mode 100644 index 00000000..94ad631d --- /dev/null +++ b/src/lib/list-view-key.ts @@ -0,0 +1,13 @@ +/** + * What the message list shows, as one string: the folder, the search and the + * filters (a tag view is a keyword filter). The list scrolls back to the top + * when it changes, so another folder, tag or search doesn't open at the + * offset the last one was left at (webmail #1138). + */ +export function listViewKey( + currentMailboxId: string | null, + searchQuery: string, + filters: object, +): string { + return JSON.stringify([currentMailboxId, searchQuery.trim(), filters]); +} diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index b5c66884..ba535963 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -63,6 +63,7 @@ import { import { isPermanentDelete, confirmPermanentDelete } from '../lib/delete-confirm'; import { confirmMailRemoval } from '../lib/mail-removal-confirm'; import { moveSheetCurrentId } from '../lib/move-targets'; +import { listViewKey } from '../lib/list-view-key'; import { draftContextFromEmail, isDraftEmail } from '../lib/draft-context'; import { getFullEmail, emptyMailbox as apiEmptyMailbox } from '../api/email'; import type { RootStackParamList } from '../navigation/types'; @@ -458,6 +459,13 @@ export default function EmailListScreen({ const currentMailboxId = useEmailStore((s) => s.currentMailboxId); const storeSearchQuery = useEmailStore((s) => s.searchQuery); const filters = useEmailStore((s) => s.filters); + // Another folder, tag or search starts at the top of its list, not at the + // offset the last one was scrolled to (webmail #1138). + const listRef = React.useRef>(null); + const viewKey = listViewKey(currentMailboxId, storeSearchQuery, filters); + React.useEffect(() => { + listRef.current?.scrollToOffset({ offset: 0, animated: false }); + }, [viewKey]); const accountErrors = useEmailStore((s) => s.accountErrors); const fetchMailboxes = useEmailStore((s) => s.fetchMailboxes); const ensureMailboxes = useEmailStore((s) => s.ensureMailboxes); @@ -1688,6 +1696,7 @@ export default function EmailListScreen({ ) : (