fix(settings): complete View Preferences parity (G35 / #43) - #73
Merged
Merged
Conversation
… 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
This was referenced Sep 13, 2026
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.
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's10...30control 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
messagesPerPage<input type="number" min="10" max="30">notificationTrayLimitmin="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 onUserDTO)viewingPreference<select>offering exactlymy_messages/all_messages/followers_only/following_only{showAdvancedPostSettings}— so the preference is the panel's default open state, and the gear writes it back?scope=following,?viewingPreference=…,?filter=…all return the identical unfiltered page. OnlyonlyMineworks — P1-G is still open.Two corrections to things the repo believed:
NotificationsServicealready reads a tray limit" is false — it only documented one; nothing read it.limit" is wrong:?limit=5→ 5 rows,?limit=40→ 36. Onlyscope=trayignores it.Changes
Kit
UpdateUserRequestgainsnotificationTrayLimit(encodeIfPresent, so an unrelated patch never clobbers it).Notifications.tray(scope:limit:)takes an optional limit.Domain
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.allscope rather than throwing.UserSettingsgainsviewingPreference+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.followersjoins.followingas a backend-less scope;MessagesService.timelineshort-circuits both viahasBackendFeed.UserService.setShowAdvancedPostSettingswrites 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-authoritativeunreadCountuntouched.App
PreferencesView: page size corrected to10...30 step 1; new Viewing picker and "Notifications in tray" stepper.NotificationsListViewModelpasses the tray limit.Decisions worth a reviewer's eye
TimelineScope.followersshort-circuiting like.following. Mapping it to.allinstead would be the "silently-wrong feed" the issue explicitly forbids.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 InterlinedKit→Executed 403 tests, with 0 failures (0 unexpected)swift test InterlinedDomain→Executed 795 tests, with 0 failures (0 unexpected)swift test InterlinedPersistence→Executed 135 tests, with 0 failures (0 unexpected)grep -rE "^\s*import InterlinedKit" App/Features App/Navigation App/MenuCommands→ 0 hits (Decision 0003)ContractTestsran against the live API →Executed 4 tests, with 0 failuresproject.pbxprojuntouchedOne flake to flag honestly. The first App-target run immediately after merging
origin/devreported 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 useTask.sleepor any timing. The tree still contains six pre-existing sleep-based test files (ExportViewModelTestsalone 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=trayreturns 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