The header checkbox selected the loaded page. On a folder of ten thousand that is fifty of them, and the only way to act on the rest was to scroll until they loaded and tick again.
This is the case that makes archive-by-month (#198) actually useful: "archive everything from 2024 by month" is unusable at fifty at a time.
The interaction
Ticking the box still takes the loaded rows. A line then appears offering the rest by name and count — "Select all 9,950 in Inbox" — and taking it is a separate press. A checkbox that silently meant ten thousand when the screen shows fifty would be the worst of both, so each option says what it actually covers. Once taken, the line says so and offers a way back, and the toolbar count tells the truth.
The selection model
It is a query, not a list of ids. What it reaches is resolved from the server when an action runs, walked a page at a time — a folder holds far more than one call returns, and Email/set refuses more ids than maxObjectsInSet in one go (which setEmails already chunks for).
It resolves uncollapsed, unlike the list. "Everything in this folder" means every message rather than one per thread. Expanding threads the way a click does is impossible here anyway: that walks loaded Email objects, and these are precisely the ones that were never loaded.
It only applies to actions aimed at the selection. A right-click or a swipe on one row still means that row, whatever is ticked.
Two bugs this exposed
Undo would have destroyed data.move() builds its undo from emails[id]?.mailboxIds ?? {} — the folders each message was in, taken from what the browser holds. For an unloaded message that is {}, so undoing would have written an emptymailboxIds and left the message in no folder at all, which is worse than the move it was undoing. move() and archiveByDate() both did this. Both now withhold the Undo offer when any target is not loaded, rather than restoring something wrong.
An action did not consume the wider selection. The optimistic paths cleared the selected ids but not the flag, so the next action would have silently reached the whole folder again.
Both are covered by tests.
Testing
12 new store tests against a simulated server: the two-step selection, paging behaviour and the short-page stop, the uncollapsed query (asserted against the list being collapsed), Undo offered when everything is loaded and withheld when it is not, the move still happening either way, and the flag not outliving its action.
Suite green: web 729 across 80 files, server 116, typecheck clean.
Not verified
Not exercised in a browser — automation stopped cooperating for me partway through this session. The store behaviour is well covered; the banner itself and the toolbar count have not been seen rendered.
Worth a look before merge, and worth trying one large-folder action against a real mailbox: the paging is tested against a fake server with a page size of 3, not against Stalwart with a page size of 500 and a folder that actually needs several round trips.
The header checkbox selected the loaded page. On a folder of ten thousand that is fifty of them, and the only way to act on the rest was to scroll until they loaded and tick again.
This is the case that makes archive-by-month (#198) actually useful: "archive everything from 2024 by month" is unusable at fifty at a time.
## The interaction
Ticking the box still takes the loaded rows. A line then appears offering the rest **by name and count** — "Select all 9,950 in Inbox" — and taking it is a separate press. A checkbox that silently meant ten thousand when the screen shows fifty would be the worst of both, so each option says what it actually covers. Once taken, the line says so and offers a way back, and the toolbar count tells the truth.
## The selection model
**It is a query, not a list of ids.** What it reaches is resolved from the server when an action runs, walked a page at a time — a folder holds far more than one call returns, and `Email/set` refuses more ids than `maxObjectsInSet` in one go (which `setEmails` already chunks for).
**It resolves uncollapsed, unlike the list.** "Everything in this folder" means every message rather than one per thread. Expanding threads the way a click does is impossible here anyway: that walks loaded `Email` objects, and these are precisely the ones that were never loaded.
**It only applies to actions aimed at the selection.** A right-click or a swipe on one row still means that row, whatever is ticked.
## Two bugs this exposed
**Undo would have destroyed data.** `move()` builds its undo from `emails[id]?.mailboxIds ?? {}` — the folders each message was in, taken from what the browser holds. For an unloaded message that is `{}`, so undoing would have written an *empty* `mailboxIds` and left the message in no folder at all, which is worse than the move it was undoing. `move()` and `archiveByDate()` both did this. Both now withhold the Undo offer when any target is not loaded, rather than restoring something wrong.
**An action did not consume the wider selection.** The optimistic paths cleared the selected ids but not the flag, so the next action would have silently reached the whole folder again.
Both are covered by tests.
## Testing
12 new store tests against a simulated server: the two-step selection, paging behaviour and the short-page stop, the uncollapsed query (asserted against the list being collapsed), Undo offered when everything is loaded and withheld when it is not, the move still happening either way, and the flag not outliving its action.
Suite green: web 729 across 80 files, server 116, typecheck clean.
## Not verified
Not exercised in a browser — automation stopped cooperating for me partway through this session. The store behaviour is well covered; the banner itself and the toolbar count have not been seen rendered.
Worth a look before merge, and worth trying one large-folder action against a real mailbox: the paging is tested against a fake server with a page size of 3, not against Stalwart with a page size of 500 and a folder that actually needs several round trips.
**Merged** 2026-09-01 as coffey-labs/ihasmail@9cace7c90cb1
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The header checkbox selected the loaded page. On a folder of ten thousand that is fifty of them, and the only way to act on the rest was to scroll until they loaded and tick again.
This is the case that makes archive-by-month (#198) actually useful: "archive everything from 2024 by month" is unusable at fifty at a time.
The interaction
Ticking the box still takes the loaded rows. A line then appears offering the rest by name and count — "Select all 9,950 in Inbox" — and taking it is a separate press. A checkbox that silently meant ten thousand when the screen shows fifty would be the worst of both, so each option says what it actually covers. Once taken, the line says so and offers a way back, and the toolbar count tells the truth.
The selection model
It is a query, not a list of ids. What it reaches is resolved from the server when an action runs, walked a page at a time — a folder holds far more than one call returns, and
Email/setrefuses more ids thanmaxObjectsInSetin one go (whichsetEmailsalready chunks for).It resolves uncollapsed, unlike the list. "Everything in this folder" means every message rather than one per thread. Expanding threads the way a click does is impossible here anyway: that walks loaded
Emailobjects, and these are precisely the ones that were never loaded.It only applies to actions aimed at the selection. A right-click or a swipe on one row still means that row, whatever is ticked.
Two bugs this exposed
Undo would have destroyed data.
move()builds its undo fromemails[id]?.mailboxIds ?? {}— the folders each message was in, taken from what the browser holds. For an unloaded message that is{}, so undoing would have written an emptymailboxIdsand left the message in no folder at all, which is worse than the move it was undoing.move()andarchiveByDate()both did this. Both now withhold the Undo offer when any target is not loaded, rather than restoring something wrong.An action did not consume the wider selection. The optimistic paths cleared the selected ids but not the flag, so the next action would have silently reached the whole folder again.
Both are covered by tests.
Testing
12 new store tests against a simulated server: the two-step selection, paging behaviour and the short-page stop, the uncollapsed query (asserted against the list being collapsed), Undo offered when everything is loaded and withheld when it is not, the move still happening either way, and the flag not outliving its action.
Suite green: web 729 across 80 files, server 116, typecheck clean.
Not verified
Not exercised in a browser — automation stopped cooperating for me partway through this session. The store behaviour is well covered; the banner itself and the toolbar count have not been seen rendered.
Worth a look before merge, and worth trying one large-folder action against a real mailbox: the paging is tested against a fake server with a page size of 3, not against Stalwart with a page size of 500 and a folder that actually needs several round trips.
Merged 2026-09-01 as coffey-labs/ihasmail@9cace7c90c
Rebuilt from: git history, session transcript.