Commit Graph
758 Commits
Author SHA1 Message Date
jcoffey-dev 95f7f8008e Run CI on the self-hosted GitLab
Ports ci.yml and publish.yml after the GitHub account was suspended. The
workflow here is identical to the one upstream in ihasmail, so this is the
same pipeline: tests as the image's unprivileged node user, git installed
for the version check, and a tag-driven multi-arch publish under QEMU.

The job environment differences are explained inline -- they are all cases
where a container is not a workstation, not changes to what is tested. No
test was modified.

The Actions workflows stay in the tree as the reference.
2026-09-20 20:15:24 -07:00
jcoffey 097d08900c Start the weekly release (#3)
The schedule has been off since the fork was cut, with the check job
gated to workflow_dispatch so a release could only be cut by hand. The
reason was that a scheduled release publishes an image and mails
everyone watching the repository, and there was nothing yet to tell
them about.

INBUXA is releasing now, so the gate comes off: Mondays at 09:17 UTC,
and nothing on a quiet week, which is what the workflow already decides
for itself. Cutting one by hand still works and is unchanged.

This restores the file to public ihasmail's, byte for byte -- the gate
and the note explaining it were the only difference.

Two things worth knowing before the first one runs. GHCR creates a
package private the first time, even in a public repository, and no
workflow can change that: the first release will publish an image that
an anonymous `docker pull` refuses until the visibility is set by hand
under the package's settings. And GitHub disables scheduled workflows
in a repository with no activity for 60 days, so a silent stop later is
worth checking for before assuming the file is broken.
2026-09-20 16:07:56 -07:00
jcoffey de120ba7ca The source offer is a link to this fork, not a tarball in the image (#2)
INBUXA's webmail built its own source into every image: the whole tree,
web and server, packed as dist/source.tar.gz with an identity string
beside the link naming the exact tree it came from. The sign-in page and
Settings > About offered that download.

It answered the AGPL precisely -- the source of *this* build, uncommitted
work and all -- but it paid for that precision by carrying 2.5 MB of
source into production on every deploy, to a repository that is public
and already has it. The fork is at github.com/inbuxa/ihasmail-inbuxa;
the version shown directly above the link already names the commit the
build came from, so the link and the version together say the same
thing the archive said.

Both links now go there, through the mechanism upstream ihasmail already
has and this fork had replaced: the server's SOURCE_URL, read from
/api/config on the sign-in page and from the session in About, with
web/src/lib/source.ts as the fallback before either answers. That
mechanism is better than a hardcoded URL for the deployer who patches
this tree -- they set SOURCE_URL and both links follow -- which is the
case the AGPL is actually about. The defaults in config.ts, the compose
file and .env.example move from the upstream repo to this one, since a
build from this tree is a modified ihasmail and its offer is ours.

Removed with it: scripts/source-archive.mjs and its type stub, the Vite
plugin that ran it, __SOURCE_ID__, and SOURCE_ARCHIVE/SOURCE_ID. The
build no longer shells out to git or tar, and nothing is written next
to the app.

Links to Coffey-Labs/ihasmail that are credit rather than a source
offer -- the README's "built on", the translation issue link -- are
left alone.

No new strings: "AGPL-3.0 source" is unchanged, and the About line keeps
its existing {source} placeholder, now filled with the host and path
instead of a file name.
2026-09-20 16:07:30 -07:00
jcoffey-dev 25763832f3 Publish to INBUXA's own image, and don't release on a schedule yet
Two things inherited from public ihasmail that became live the moment this
repository went public.

publish.yml pushed ghcr.io/coffey-labs/ihasmail -- the image every public
ihasmail install pulls. A release here would have published INBUXA's webmail
over it, which is the exact confusion SPEC 5 exists to prevent. It now
publishes ghcr.io/inbuxa/ihasmail-inbuxa.

release.yml cuts a release every Monday at 09:17 UTC, which public ihasmail
wants because it has users expecting one. This fork has none yet, and the
release triggers the publish, so on Monday it would have shipped an image
and mailed everyone watching about a product that has not shipped. Scheduled
runs now stop at the first job; a release can still be cut by hand, and the
weekly one comes back by removing one line.
2026-09-20 00:01:02 -07:00
jcoffey a2ba7f5acd Remove development status from README
Removed status update indicating the project is in development.
2026-09-19 23:46:58 -07:00
jcoffey-dev fdcf27f3ea Merge public ihasmail: the app's name comes from APP_NAME everywhere
Upstream's {app} placeholder (#406) replaces most of the fork's own
renamed strings: the user menu, the About heading and its version line
now say INBUXA because APP_NAME does, not because the fork wrote it in.

Kept from the fork: inbuxa.org rather than ihasmail.org, no Documentation
entry until INBUXA has its own, the INBUXA mark and wordmark, the "Built
on ihasmail" credit, and the About note that says nothing about the
server software. DEFAULT_APP_NAME stays INBUXA.

The credit's placeholder is {project} now, so the name of the project is
not spelled inside a key that upstream's new test reads as a hard-coded
app name.
2026-09-19 14:29:43 -07:00
jcoffey 9e3844e94a Call the app by its name in every sentence that names it (#406)
APP_NAME renames an instance, but only the sign-in page, the title bar and
a few headings used it. Two dozen sentences wrote "ihasmail" into
themselves, so a renamed instance still told people to keep an ihasmail
tab open and offered to open mail links "in ihasmail".

Those sentences now take the name as {app}, which also lets a translator
put it where their language wants it. brand.ts grew useAppName() for
components and currentAppName() for the few places that build strings
outside React.

Left as they are: the Files folder "ihasmail", the Sieve script
"ihasmail" and ihasmail.org. Those name things a person can go and look
at, and renaming them would rename real data.

All nine catalogues keep their translations: the name inside each one
became the placeholder. Three of the strings had no translation before
and still fall back to English.

A test walks the sources and the catalogues so a new sentence can't
hard-code the name again.
2026-09-19 14:27:34 -07:00
jcoffey-dev f8119fafbf Merge public ihasmail: folder reordering, full-screen composer, Dutch update
- Reorder folders by dragging, with special folders first (#402, #405).
- Open the composer full screen, as a setting (#401, #404).
- Dutch translation update (#403).

FEATURES.md stays deleted here, as in bb25355.
2026-09-19 14:11:50 -07:00
jcoffeyandClaude Opus 5 8a7ff5d42f Reorder folders by dragging, with special folders first (#402) (#405)
The folder tree ignored sortOrder: Inbox came first, then everything
A–Z, so Sent ended up among ordinary folders. The tree now lists Inbox,
then any order the user has chosen, then the other special folders
(Drafts, Sent, Archive, Junk, Trash), then the rest A–Z. Stalwart gives
every folder sortOrder 0 until someone orders it, so an existing
sidebar changes once, to that default.

Dropping a folder on the top or bottom quarter of a row puts it above or
below that row, with a line to show where it will land. Dropping on the
middle still nests it. Special folders can now be dragged, to be
reordered but never nested; on those, the whole row reorders by the
nearer half. The folder menu gains Move up and Move down, for the
keyboard and touch. Inbox stays first.

A reorder numbers the level 10 apart and writes only the folders whose
number changes, in one Mailbox/set. The order is saved on the server,
so it follows the account to every device and to other JMAP clients.

No new strings: Move up and Move down were already translated.

Fixes #402

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 14:06:21 -07:00
jcoffeyandClaude Opus 5 c201d34377 Open the composer full screen, as a setting (#401) (#404)
Settings > General > Composing has a new switch, "Open the composer full
screen". With it on, every new composer, whether a new message, reply,
forward or reopened draft, starts maximized. Restore still shrinks it to
a window. A draft put back after an undone or failed send keeps the size
it had. It's off by default, and on a phone, where the composer already
fills the screen, it changes nothing.

One new string, translated in all nine catalogs. The count falling back
to English stays at 16 in every language.

Fixes #401

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 14:06:18 -07:00
jcoffeyandmbjboon-netizen 05d1645ab7 Update nl.ts (#403)
Signed-off-by: mbjboon-netizen <[email protected]>
Co-authored-by: mbjboon-netizen <[email protected]>
2026-09-19 13:42:51 -07:00
jcoffey-dev cfa661de20 INBUXA's name and site where the webmail still said ihasmail
- The sign-in footer names the build "INBUXA webmail" and links to
  inbuxa.org, next to its AGPL source link.
- About gives the INBUXA webmail version, and credits ihasmail on a line
  of its own.
- The user menu's "About ihasmail" is "About INBUXA", to inbuxa.org. The
  Documentation entry pointed at ihasmail's docs; it's gone until INBUXA
  has documentation of its own.
- The startup log says "mail server:" rather than naming the server
  software.

"About INBUXA" and "Built on {ihasmail}" are translated in all nine
catalogues.
2026-09-19 10:09:28 -07:00
jcoffey-dev cee4f74257 Source link opens in a new tab 2026-09-19 00:49:24 -07:00
jcoffey-dev bb25355c23 AGPL source offer and name cleanup
Every build writes the exact source it was built from, uncommitted work and
new files included, as source.tar.gz next to the app, named after that tree.
Docker builds, which have no git, pack the build context and name it by a
hash of its files. The sign-in page and Settings > About link to it instead of
a repository that can drift.

What users, operators and packagers see no longer names the upstream server:
- interface text, in all nine catalogues, with a token-session line for
  Security;
- server messages;
- the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and
  MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json;
- the Tenants notice, which is gone;
- the README, CONTRIBUTING and SECURITY.

ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail,
and INBUXA.md is folded into the README.
2026-09-19 00:13:12 -07:00
jcoffey-dev 9d2de725c9 Merge public ihasmail: drag calendar events to another day in the week grid (#400) 2026-09-18 21:30:38 -07:00
jcoffey 091782ae3a Drag calendar events to another day in the week grid (#400)
A timed event in the week view now moves sideways across the columns
as well as up and down, landing on the new day at the hour it was
dragged to. All-day chips above the grid drag between days the way
month chips do. Both drags count from the day the event was picked up
on, so a multi-day event grabbed on its last day moves by the distance
dragged, not by its length.
2026-09-18 21:19:58 -07:00
jcoffey-dev 2e668edb51 Brand the webmail INBUXA: name, mark and wordmark
INBUXA is a product suite and ihasmail an independent product, so INBUXA's
webmail says INBUXA: the sign-in page, header, page title, installed-app name
and About page. The wordmark is drawn in the current text color. ihasmail's
version and AGPL source line stay as its credit. Two new strings, in all nine
catalogues; three Stalwart-only ones are no longer used.
2026-09-18 15:58:56 -07:00
jcoffey-dev cdd8fabff9 No address step before signing in when there's only one mail server
The server's own page asks for the username, so with a single server the
sign-in page keeps only the own-device choice. With several servers the
address still comes first, since its domain picks the server. One new string,
in all nine catalogues.
2026-09-18 15:51:54 -07:00
jcoffey-dev 8857bdac30 Sign in on the mail server's own page (OAuth with PKCE), sessions hold tokens; tenants on every edition
Contract C-8 and C-10: with OAUTH_CLIENT_SECRET set, sign-in goes through the
server's page and the session keeps sealed tokens, renewed before they expire,
instead of a password. Push keeps a credential that renews itself. A password
change signs the session out, since the server revokes its tokens. The mock
answers OAuth for tests and development. Eleven new strings, in all nine
catalogues.
2026-09-18 15:30:37 -07:00
jcoffeyandJoe Esteves c118184975 Match Shift+letter shortcuts (Shift+I, Shift+U) (#399)
comboOf() let a shifted letter encode Shift in its case, so Shift+I
produced "I" and never matched the "shift+i" / "shift+u" bindings for
mark as read / unread. Shifted letters now yield "shift+<letter>";
symbols such as "#" and "!" still carry Shift in the character.

Fixes #398

Co-authored-by: Joe Esteves <[email protected]>
v2026.9.18-gc118184
2026-09-18 08:22:13 -07:00
jcoffey 2740129c6a Keep only the app page as the app page (#396)
The service worker answers app routes from its kept page (#395), and it
kept whatever the mount's root returned at install and whatever HTML a
navigation returned. Where the root is not the app -- demo.ihasmail.com
puts its landing page there -- a returning visitor got the landing page on
every route.

The kept page is now only ever the app page, recognised by the asset list
the build writes into it: install fetches /mail instead of /, a
navigation's page is kept only if it is the app's, and a foreign page left
by the earlier worker is dropped when this one activates. Only the app's
own routes are answered from it; the root and any page in front of the app
go to the network. The reload for a new build primes the kept page from
/mail for the same reason.
v2026.9.16-g2740129
2026-09-16 15:07:45 -07:00
jcoffey 82dc877fe1 Start at once on a device marked as your own (#395)
* Start at once on a device marked as your own

On a distant link, opening the app waited on four round trips before the
inbox showed: the app page, the session, the folder list, then the folder.

A trusted device now starts from what it kept:

- the service worker answers an app route from its kept page and fetches a
  fresh one behind it; the app checks the server's version at start, and a
  reload for a new build puts the new page in place first, so it is not
  answered with the old one. Assets of the page just replaced are kept one
  build longer for a tab still running it.
- the session's public details, so requests for mail go out before the
  server has confirmed the session; the answer replaces it, and a session
  that has ended lands on the sign-in form as before.
- the folder list and the first page of up to four recently read folders,
  list properties only, so the folders and the inbox paint before any reply
  and the folder query does not wait on the folder list. The "folder no
  longer exists" check still waits for the server's list.

All of it goes through the storage gate: nothing is written or read on a
device not marked as the reader's own, and signing out clears it.

* Show nothing kept before the session is confirmed

Starting from a kept session put the kept inbox on screen before the
server had said the session was still good; a session that had ended
showed mail and then the sign-in form. The spinner stays until the
server answers, as before.

The kept session is gone -- it existed only to start early. The kept
folder list and rows are still applied, from setAccount, which runs once
the session is confirmed: the inbox paints the moment that answer
arrives, and the folder query goes out then without waiting on the
folder list. An unreachable server lands on the sign-in form as before.
2026-09-16 13:47:59 -07:00
jcoffey 4c67460450 Fetch the rest of a new build in the background (#394)
The app page names only what it loads at start. The composer, settings,
viewers and the rest were fetched when first used, and after every deploy
that first use waited on the server -- and the worker's tidy-up dropped
them again at the next deploy anyway.

The build now writes the list of all its files into the page as an inert
JSON block. The service worker keeps everything listed and, once a page
names files it does not hold, fetches them three at a time; a load cut
short is resumed at the next navigation. Language catalogs are listed
apart and left to be cached when used, and nothing is fetched ahead when
the browser is set to save data.
2026-09-16 13:29:57 -07:00
jcoffey e158ebac5a Fold a push's follow-up requests together (#393)
A pushed mail change took three round trips: Email/changes beside a
Mailbox/get, then Email/get for what changed, then the list, the open
thread and a second Mailbox/get. Each page of changes now carries its own
Email/get calls by back-reference, and the one Mailbox/get goes out with
it, so a push settles in two. New mail fetched this way is not asked for
again by the notice.

A reply's sessionState that differs from the session is announced once
rather than on every reply, and session refreshes in flight are shared.
The mock's session state now matches the sessionState on its replies, as
Stalwart's does; tying it to the data counter made every reply trigger a
session refresh in development.
2026-09-16 13:22:26 -07:00
jcoffey 786976312f Open conversations in one request, and start them early (#392)
On a 250 ms link, opening a conversation took two round trips: Thread/get,
then the bodies. It now takes one. A known thread sends Thread/get and the
missing bodies in the same tick; an unknown one chains Email/get off
Thread/get with a back-reference, and falls back to fetching in parts when
the thread is longer than one Email/get may carry.

Conversations also start loading before the click: when the pointer rests
on a row, as soon as a press begins, and for the row below the open one.
The open waits for that load and does not repeat it.

Going back to one of the last twelve folders shows its previous list at
once, less messages that have left it, while the query runs.
2026-09-16 13:15:21 -07:00
jcoffey 5fe89d6e15 Merge pull request #391 from Coffey-Labs/feat/share-confirm
Ask before opening a shared item in a message
2026-09-16 12:27:33 -07:00
jcoffey-dev f79915aa89 Drop a wrong issue reference from a comment 2026-09-16 12:23:47 -07:00
jcoffey-dev 191c4e7e68 Ask before opening a shared item in a message
The share address takes a plain form POST, which any website can make,
and the app opened whatever arrived straight into a composer. It now
shows what was shared -- the title, the start of the text and link, and
the file names -- and opens a message only when the reader chooses to.
Discarding drops it.

Confirm dialogs now put a message that is not plain text in a div, since
the summary has blocks of its own.

Three new strings, translated in all nine catalogs.
2026-09-16 12:23:27 -07:00
jcoffey 4c1ceca8e9 Merge pull request #390 from Coffey-Labs/fix/accept-ranges
Advertise byte ranges on downloads, and record the live checks
2026-09-16 12:13:20 -07:00
jcoffey-dev 8a08c3d6db Advertise byte ranges on downloads, and record the live checks
Stalwart honors a single byte range on its download endpoint but sends
no Accept-Ranges, and Chrome's PDF viewer only reads a file in pieces
when the first response says it can. The proxy now says so itself.

Checked live on 0.16.22: ContactCard/changes reports creates, updates
and destroys exactly, which the contacts store's sync relies on, and a
range the server cannot serve gets the whole file with 200, never 416.
The mock now answers ranges the same way and sends no Accept-Ranges.
2026-09-16 12:10:20 -07:00
jcoffey ebf678be73 Merge pull request #389 from Coffey-Labs/fix/push-subscriptions
Stop duplicate push notifications and piling up subscriptions
v2026.9.16-pr389
2026-09-16 11:41:37 -07:00
jcoffey-dev 37eb145652 Merge main into fix/push-subscriptions
# Conflicts:
#	KNOWN-ISSUES.md
2026-09-16 11:39:35 -07:00
jcoffey 3d7602ce74 Merge pull request #388 from Coffey-Labs/fix/contact-photos
Save contact photos inline, and load cards so avatars show
2026-09-16 11:38:22 -07:00
jcoffey-dev 4054f82c37 Stop duplicate push notifications and piling up subscriptions
Browsers subscribed to Email changes, so every read or move on any
client arrived as a push the worker could only show as "New mail". They
now subscribe to EmailDelivery, which changes only on delivery; Stalwart
sends a delivery to a subscription with an emailPush filter as an
EmailPush alone. The payload now names id and threadId, which Stalwart
sends only when asked, so notifications carry their actions and open the
message. The worker stays quiet while a focused window is open, and the
page leaves notifications to the worker where push is on.

Every renewal registered a new subscription, on the belief that a
repeated deviceClientId replaces the old one. Stalwart keeps both and
allows fifteen per account, which filled up. A browser now extends its
subscription, clears its own duplicates, replaces them only when its
endpoint changed, and on overQuota makes room among other browsers'
subscriptions. The server names its subscriptions by installation and
removes what its previous process registered, and extends rather than
re-creates.

Checked live on 0.16.22; the mock now keeps duplicates, enforces the
limit and accepts an expiry update.

Fixes #375.
2026-09-16 11:36:07 -07:00
jcoffey-dev d38dee7eb9 Save contact photos inline, and load cards so avatars show
Stalwart refuses a blobId in a card's media ("blobIds in media is not
supported"), so adding or changing a photo always failed. The editor now
saves the photo as a data: URI, which Stalwart accepts and returns
unchanged, and leaves the card's other media as it was. Checked live on
0.16.22; the mock now refuses a blobId the same way.

Avatars in the mail list come from the address book's cards, and nothing
loaded those at sign-in, so a photo showed only after Contacts had been
opened. The cards now load in the background at start, the avatar uses
whatever cards are held, and a shared card's photo is fetched from the
account it belongs to.

Fixes #376.
2026-09-16 11:27:47 -07:00
jcoffey aa9bf1b9b2 Merge pull request #387 from Coffey-Labs/perf/client-memory
Let go of old message bodies and of exported files
2026-09-16 11:02:42 -07:00
jcoffey c63fd0dfe0 Merge pull request #386 from Coffey-Labs/perf/server-static-and-caches
Precompress the bundle, validate the shell, and pass byte ranges on
2026-09-16 11:02:37 -07:00
jcoffey-dev f123467897 Let go of old message bodies and of exported files
Every message opened kept its full copy for as long as the tab was open.
The store now holds bodies for the 40 messages most recently wanted; older
ones go back to the list properties and are fetched in full again if
opened. The open conversation is never released.

Contact, settings and calendar exports go through downloadFile, which
releases the object URL once the download has started; three of them
never released it.
2026-09-16 10:59:57 -07:00
jcoffey-dev 71d211a13f Precompress the bundle, validate the shell, and pass byte ranges on
The web build now writes a Brotli and a gzip copy of each compressible
file, and the static handler serves the best one the browser accepts.
The bundle was gzipped again for every request and Brotli was never
offered; the main chunk is 122 KB with Brotli against 144 KB gzipped.

index.html and every static file carry an ETag, and a matching
If-None-Match gets a 304. The shell and the worker are revalidated on
every load and were downloaded whole each time.

Attachment downloads pass a plain byte Range to Stalwart and relay a 206,
so a PDF viewer or a video element can read in pieces where the server
allows it. On the reader's own device a blob is cached as immutable,
since its id names its content.

The upstream session and account-info caches drop entries past their age
on a timer; they lost an entry only on sign-out or refusal, not when a
session expired. The mock answers byte ranges.
2026-09-16 10:54:14 -07:00
jcoffey 56bd48e891 Merge pull request #385 from mbjboon-netizen/mbjboon-netizen-patch-1
Mbjboon netizen patch 1
2026-09-16 10:50:14 -07:00
jcoffey da87925b9c Merge pull request #384 from Coffey-Labs/perf/contacts-calendar-changes
Sync contacts by what changed, and hold fewer calendar windows
2026-09-16 10:45:12 -07:00
jcoffey-dev 360420402d Sync contacts by what changed, and hold fewer calendar windows
A pushed contact change, and every edit or import made here, reloaded the
whole address book. The store now keeps the state its cards were read at
and asks ContactCard/changes what changed since, fetching only those cards,
split to maxObjectsInGet. A server that cannot say falls back to the full
load.

The calendar held every week or month the reader had visited, queried each
of them again on any event change, and walked them all on every render. It
now holds the four most recently shown; a change reloads those in place,
without emptying the view first, and a window dropped is loaded again when
it is next shown. Shared calendars' events are fetched from every account
at once, and instancesIn builds the added-shares set once.

The mock keeps a ContactCard change log, answers ContactCard/changes, and
announces a ContactCard/set, as Stalwart does.
2026-09-16 10:41:21 -07:00
mbjboon-netizen 8bd7904a21 Update nl.ts
Signed-off-by: mbjboon-netizen <[email protected]>
2026-09-16 19:07:54 +02:00
jcoffey 6090442058 Merge pull request #383 from Coffey-Labs/perf/lazy-store-init
Ask shared accounts together, and about files only when Files opens
2026-09-16 10:06:53 -07:00
jcoffey-dev 4c7b2ec370 Ask shared accounts together, and about files only when Files opens
At sign-in the files, contacts and calendar stores each asked every shared
account a question, one account after another: a request apiece before the
reader had opened any of those views.

Files now only works out at sign-in whether it is available. Which shared
accounts hold files is asked when the Files view or the file picker opens,
which the Files view already did on every visit. Shared address books and
calendars are asked for in one request, and the calendar store loads its
calendars, identities and shared calendars side by side.
2026-09-16 10:04:24 -07:00
jcoffey 9560ad06f4 Merge pull request #382 from Coffey-Labs/perf/lazy-chunks
Load the composer, previews, dialogs and other sidebars on demand
2026-09-16 09:50:14 -07:00
jcoffey-dev 6139031689 Load the composer, previews, dialogs and other sidebars on demand
The main chunk carried everything the mail view might open: the file
preview and its Markdown renderer, the composer and its editor, the contact
editor, the filter and share dialogs, and the calendar, contacts and files
sidebars. Each is now loaded when first shown. The composer is also
fetched when the browser is idle after startup, so the first Compose does
not wait on the network.

Import the notification helpers statically where they already were: the
dynamic imports beside those static ones split nothing.
2026-09-16 09:48:04 -07:00
jcoffey e3cd56314b Merge pull request #381 from Coffey-Labs/perf/message-list-rows
Render only the message rows that changed
2026-09-16 09:22:08 -07:00
jcoffey-dev c6dbcaef63 Render only the message rows that changed
The rows were memoized, but nothing they were given kept its identity: the
list built each row's thread messages afresh, passed inline handlers and the
whole selection, and the click and context-menu handlers changed with the
selection and the menu. Every visible row rendered on every store write.

Rows now select their own message and conversation from the store, take a
plain selected flag, and get handlers whose identity never changes. The
conversation summary is memoized.

Refreshes also keep the object for a message whose fetched properties did
not change, so a refresh that changed one message renders one row.
2026-09-16 09:10:41 -07:00
jcoffey 460760ba12 Merge pull request #380 from Coffey-Labs/fix/sw-cache
Keep the service worker's cache to the current build
2026-09-16 09:01:20 -07:00