Skip to content

fix(settings): complete View Preferences parity (G35 / #43) - #73

Merged
Adron merged 2 commits into
devfrom
fix/view-preferences-parity
Sep 13, 2026
Merged

Adron merged 2 commits into
devfrom
fix/view-preferences-parity

Conversation

@Adron

@Adron Adron commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Settings ▸ Preferences shipped three of the five documented View Preferences, and the one range it did ship (5...100 step 5) could save values the web's 10...30 control cannot represent. This adds the missing Viewing filter and notification tray limit, corrects the page-size range, and makes the already-present "Show advanced post options" toggle actually drive the composer's gear. Closes #43 (tracked as G35).

Every contract detail here was pinned against the live site read-only on 2026-09-09 (GET + OPTIONS only, no writes to the shared test account) rather than guessed.

What the probe settled

Thing Evidence
messagesPerPage web form <input type="number" min="10" max="30">
notificationTrayLimit min="10" max="40" value="20"and the web's own View Preferences card PATCHes it to /api/user/update, settling that it is an accepted write key (it was read-only on UserDTO)
viewingPreference <select> offering exactly my_messages / all_messages / followers_only / following_only
Advanced post settings the web gear PATCHes the single key {showAdvancedPostSettings} — so the preference is the panel's default open state, and the gear writes it back
Follower feeds ?scope=following, ?viewingPreference=…, ?filter=… all return the identical unfiltered page. Only onlyMine works — P1-G is still open.

Two corrections to things the repo believed:

Changes

Kit

  • UpdateUserRequest gains notificationTrayLimit (encodeIfPresent, so an unrelated patch never clobbers it).
  • Notifications.tray(scope:limit:) takes an optional limit.

Domain

  • New ViewingPreference: four typed cases plus an .other(String) escape hatch that round-trips an unrecognised token instead of silently rewriting the account. Unknown tokens resolve to the .all scope rather than throwing.
  • UserSettings gains viewingPreference + notificationTrayLimit, and clamps both integers on read and write — an account still holding a legacy value from the old control is corrected rather than re-sent.
  • TimelineScope.followers joins .following as a backend-less scope; MessagesService.timeline short-circuits both via hasBackendFeed.
  • UserService.setShowAdvancedPostSettings writes that one field, so the composer gear persists without carrying a stale snapshot.
  • NotificationsService.tray(limit:) sends the limit and caps rendered rows client-side, leaving the server-authoritative unreadCount untouched.

App

  • PreferencesView: page size corrected to 10...30 step 1; new Viewing picker and "Notifications in tray" stepper.
  • Timeline opens on the stored preference and gains a Followers segment with the existing coming-soon state.
  • Composer's advanced sections now sit behind a gear seeded from the preference; toggling persists optimistically and rolls back on failure.
  • NotificationsListViewModel passes the tray limit.

Decisions worth a reviewer's eye

  1. Followers Only needed a home. Added TimelineScope.followers short-circuiting like .following. Mapping it to .all instead would be the "silently-wrong feed" the issue explicitly forbids.
  2. The timeline picker is a session override, not a write-back. Multiple timeline windows can be open on macOS; one window's transient filter must not mutate the account under the others. Documented in the view model.
  3. All four viewing values are offered, not just the two that work. Hiding the unserved two would let the next unrelated save quietly overwrite a preference set on the web; instead the pane says the feed isn't available yet.

Verification

E2E gate on the post-merge tree (CODE_SIGNING_ALLOWED=NO -derivedDataPath DerivedData):

  • xcodebuild build** BUILD SUCCEEDED **
  • xcodebuild test (App) → Executed 768 tests, with 0 failures (0 unexpected)** TEST SUCCEEDED **
  • swift test InterlinedKitExecuted 403 tests, with 0 failures (0 unexpected)
  • swift test InterlinedDomainExecuted 795 tests, with 0 failures (0 unexpected)
  • swift test InterlinedPersistenceExecuted 135 tests, with 0 failures (0 unexpected)
  • grep -rE "^\s*import InterlinedKit" App/Features App/Navigation App/MenuCommands0 hits (Decision 0003)
  • env-gated ContractTests ran against the live API → Executed 4 tests, with 0 failures
  • project.pbxproj untouched

One flake to flag honestly. The first App-target run immediately after merging origin/dev reported 1 failure. I did not capture the test name on that run, and it has not reproduced in 14 consecutive full-gate runs since. My new suite is 6/6 green in isolation, and none of the files this PR touches use Task.sleep or any timing. The tree still contains six pre-existing sleep-based test files (ExportViewModelTests alone has 13 × 100ms sleeps), which is the likely source. Reporting it rather than calling the gate unconditionally clean.

Follow-up (not in this PR)

GET /api/notifications?scope=tray returns unread-only, while the web bell shows the last N regardless of read state. Separate tray-semantics parity bug, outside #43's scope — worth its own issue.

🤖 Generated with Claude Code

Adron and others added 2 commits September 9, 2026 09:55
… filter, tray limit, composer gear

Settings ▸ Preferences shipped three of the five documented View
Preferences, and the one range it did ship could save values the web
cannot represent. Closes the gap tracked as G35 / issue #43.

Everything here was pinned against the live site read-only on
2026-09-09 (GET + OPTIONS only) rather than guessed:

  • the web Settings form's own controls give the ranges —
    `messagesPerPage` `min=10 max=30`, `notificationTrayLimit`
    `min=10 max=40` default 20;
  • its `<select id="viewingPreference">` gives the wire vocabulary —
    my_messages / all_messages / followers_only / following_only;
  • its View Preferences card PATCHes all four keys to
    `/api/user/update`, which settles that `notificationTrayLimit` is
    an accepted write key (it was read-only on `UserDTO` before);
  • its composer gear PATCHes the single key
    `{ showAdvancedPostSettings }`, so the preference is the panel's
    default open state and the gear writes it back;
  • `GET /api/messages` still honours only `onlyMine` — `?scope=following`,
    `?viewingPreference=…` and `?filter=…` all return the identical
    unfiltered page, so neither follower feed exists (P1-G);
  • `GET /api/notifications` honours `limit` unscoped but ignores it
    under `scope=tray`, correcting a stale comment that claimed the
    server ignores it outright.

Kit
  • `UpdateUserRequest` gains `notificationTrayLimit` (encodeIfPresent,
    so an unrelated patch never clobbers it).
  • `Notifications.tray(scope:limit:)` takes an optional limit.

Domain
  • New `ViewingPreference` — four typed cases plus an `.other(String)`
    escape hatch that round-trips a token this build doesn't recognise
    instead of silently rewriting the account. Unknown tokens resolve
    to the `.all` scope rather than throwing.
  • `UserSettings` gains `viewingPreference` + `notificationTrayLimit`
    and clamps both integers on read *and* write, so an account still
    holding a legacy value (the old `5...100 step 5` control) is
    corrected rather than re-sent.
  • `TimelineScope.followers` joins `.following` as a backend-less
    scope; `MessagesService.timeline` short-circuits both via
    `hasBackendFeed`.
  • `UserService.setShowAdvancedPostSettings` writes that one field, so
    the composer gear can persist without carrying a stale snapshot.
  • `NotificationsService.tray(limit:)` sends the limit and caps the
    rendered rows client-side, leaving the server-authoritative
    `unreadCount` untouched.

App
  • `PreferencesView`: posts-per-page corrected to 10...30 step 1; new
    Viewing picker and "Notifications in tray" stepper. All four
    viewing values are offered — the two with no backend say so
    honestly instead of being hidden, which would let the next save
    quietly overwrite a preference set on the web.
  • The timeline opens on the stored preference and gains a Followers
    segment with the existing coming-soon state. The toolbar picker
    stays a session override and does not write the preference back —
    multiple timeline windows can be open, and one window's transient
    filter must not mutate the account under the others.
  • The composer's advanced sections now sit behind a gear seeded from
    the preference; toggling it persists optimistically and rolls back
    on failure. Before this the toggle was read by the Preferences pane
    and by nothing else.
  • `NotificationsListViewModel` passes the tray limit — the setting
    had no reader at all.

Gate (CODE_SIGNING_ALLOWED=NO, -derivedDataPath DerivedData): build
succeeded; App 760 tests / 0 failures; Kit 403 / 0; Domain 795 / 0;
Persistence 135 / 0; zero `import InterlinedKit` in App/Features,
App/Navigation, App/MenuCommands; live ContractTests 4/4 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtmXBbJVxqGvGnsJzbBywr
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