feat(hutch,ios): emit an explicit read-state so the client stops hard-coding "read"#926
Closed
FagnerMartinsBrack wants to merge 2 commits into
Closed
feat(hutch,ios): emit an explicit read-state so the client stops hard-coding "read"#926FagnerMartinsBrack wants to merge 2 commits into
FagnerMartinsBrack wants to merge 2 commits into
Conversation
…ard-coding it The iOS reader mints a browser session cookie from the bearer token before loading the cookie-authenticated reader page. The client hard-coded that route and method (`POST /auth/session`), so a server-side rename would break every article tap until an App Store release — the exact URL coupling the hypermedia contract exists to remove. - Server: advertise a `create-session` action on the collection (href `/auth/session`, method POST). It carries no fields — the route reads the bearer from the Authorization header — and no title, since it is a machine capability a client invokes bespoke rather than a rendered control (like `save-articles`). - Client: discover the action from the loaded collection and follow its href and method to mint the session; map `create-session` to a non-toolbar presentation so it never renders as a control. A server that hasn't advertised the action yet falls back to the fixed path, so an older shipped build keeps working. The action is additive and non-breaking: a client without a handler skips it.
…-coding "read" The iOS client derived an article's read indicator from the status vocabulary (`status == "read" || readAt != nil`) — a domain literal baked into the client that goes stale if the server's status vocabulary evolves (e.g. an `archived` state, or keeping `readAt` after a mark-unread). - Server: emit an explicit `isRead` boolean property on the article entity (already computed for the toggle label), so the read-state is server-authored. - Client: render read-state from the server's `isRead` when present, falling back to the old status/readAt derivation only for an older server that doesn't emit it (App Store back-compat). The property is additive and non-breaking. The optimistic row-removal prediction on the swipe mark-read (`removesItem`) still reads the update-status field value; that path is the client-side optimism that was deliberately kept, and its literal is out of scope here.
Base automatically changed from
ios/discover-session-action
to
ios/testable-view-seams
July 5, 2026 12:58
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
The iOS client derived an article's read indicator from the status vocabulary —
isRead = status == "read" || readAt != nil— a domain literal baked into the client. It goes stale the moment the server's status vocabulary evolves (anarchivedstate that's also "read", or keepingreadAtafter a mark-unread), leaving the checkmark disagreeing with server truth. Two explicit tests pinned the dual-signal derivation, so this couldn't be fixed client-only without a server signal — which is why it was split out of the earlier client-only PR.What changed & the value
article-siren.ts): emit an explicitisReadboolean property on the article entity (the value it already computes for the toggle label). Read-state is now server-authored, one property.isReadwhen present, falling back to the oldstatus/readAtderivation only for an older server that doesn't emit it (App Store back-compat). Once the server is deployed the fallback never runs.The property is additive: an older client ignores it, a newer client prefers it. No breaking change.
Scope note
The optimistic row-removal prediction on swipe-mark-read (
removesItemFromUnreadList) still reads theupdate-statusfield value — that path is the client-side optimistic synthesis that was deliberately kept (it's not the read-state rendering this PR targets), so its literal is intentionally out of scope here.Testing
article-siren/collection-sirensuites green (incl. newisReadassertions and the updated exact-property-key lists); 508 article/route/view tests pass;pnpm checkpasses.isReadoverrides the status literal in both directions, and the fallback derivation still holds when the server omits it; coverage gate + STAGING smoke pass.