Skip to content

feat(hutch,ios): emit an explicit read-state so the client stops hard-coding "read"#926

Closed
FagnerMartinsBrack wants to merge 2 commits into
ios/testable-view-seamsfrom
ios/server-read-state
Closed

feat(hutch,ios): emit an explicit read-state so the client stops hard-coding "read"#926
FagnerMartinsBrack wants to merge 2 commits into
ios/testable-view-seamsfrom
ios/server-read-state

Conversation

@FagnerMartinsBrack

Copy link
Copy Markdown
Member

Stacked on #925 (base = ios/discover-session-action). Review/merge after it. Server + client, additive/non-breaking — deploy either order.

Purpose

The iOS client derived an article's read indicator from the status vocabularyisRead = status == "read" || readAt != nil — a domain literal baked into the client. It goes stale the moment the server's status vocabulary evolves (an archived state that's also "read", or keeping readAt after 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

  • Server (article-siren.ts): emit an explicit isRead boolean property on the article entity (the value it already computes for the toggle label). Read-state is now server-authored, one property.
  • 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). 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 the update-status field 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

  • Server: article-siren / collection-siren suites green (incl. new isRead assertions and the updated exact-property-key lists); 508 article/route/view tests pass; pnpm check passes.
  • Client: full iOS suite green (253 tests) — the server isRead overrides the status literal in both directions, and the fallback derivation still holds when the server omits it; coverage gate + STAGING smoke pass.

…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
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

Superseded by #927, which consolidates all 9 stacked PRs into one PR targeting main (so CI runs and it's independently deployable). See #927 for the full description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant