Skip to content

feat(profile): Settings surface + view preferences and link previews (#33) - #93

Merged
Adron merged 1 commit into
parity/queuefrom
issue/33-settings-view-preferences
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/33-settings-view-preferences

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #33. Part of epic #31. Scaffold that #32, #34, #35, #36 and #37 build on.

What changed

A Settings screen at route settings, reached from a new "Settings" row on the Account hub,
following the hub's existing *Route + stateless *Screen drill-down pattern. It renders titled
groups; only the "View preferences" group exists — no empty stubs for the sibling issues.
SettingsGroup, SettingsRadioRow and SettingsSwitchRow are public, so #32/#34/#35/#36/#37 only
need to add a group.

A SettingsRepository over GET /api/user + PATCH /api/user/update, deliberately separate
from ProfileRepository (different consumers: live preferences vs. the profile cache). It exposes
observeSettings(): Flow<UserSettings?>, refresh() and update(UserSettingsUpdate), is
@Singleton with a process-scoped in-memory cache matching the module's "settings are always
fresh, nothing cached" precedent, and republishes on every successful read/write.

All fifteen PATCH fields are modelled now (nullable, nulls omitted via the module's
explicitNulls = false), so the sibling issues only have to add UI. updateProfile(displayName, bio) is untouched and still sends exactly two keys — now covered by a regression test.

UI for this issue only: the viewingPreference radio group and the showPreviews switch, each
saved as its own partial PATCH, applied optimistically and rolled back on failure.

The enum is live-verified, not guessed

First pass guessed all/mine/following/followers. I checked against the live API and that
would have been rejected on every write. The server's own 400 gives the allow-list:

{"error":"viewingPreference must be one of: my_messages, all_messages, followers_only, following_only",
 "code":"bad_request"}

ViewingPreference now carries exactly those four, and the tests pin the literals and assert
every constant round-trips through its own wire, so the two can't drift apart again. fromWire
stays tolerant (case-insensitive, non-letters stripped) so an unexpected read degrades gracefully.

Verification

./gradlew :app:assembleDebug testDebugUnitTest :feature:profile:compileDebugAndroidTestKotlin →
BUILD SUCCESSFUL, 711 unit tests, 0 failures. 8 repository tests (round-trips, body.keys
assertions proving a PATCH carries only the touched field, thin-body re-fetch, failure leaves the
cache intact, defaults when fields are absent), 7 mapper/enum, 9 ViewModel
(load/save/error/rollback/dismiss/no-op reselect), 1 updateProfile regression. 6 Compose tests
compile but were not executed (no emulator).

Reviewer notes

  1. Feed: view preferences (All / My / Following / Followers) and onlyMine #19 (feed consumption) needs a module-dependency decision. No feature module currently
    depends on another; :feature:messages would need implementation(project(":feature:profile"))
    to read SettingsRepository, or the repository gets promoted to a shared module. Left alone
    deliberately — this issue only exposes and persists the value; nothing consumes it yet, and the
    "previews suppressed when off" behaviour is therefore not implemented here.
  2. Request-side field types. The generated spec types maxMessageLength, showPreviews,
    latitude etc. as string on the PATCH body while the live response returns int/bool/float.
    I send the natural JSON types, matching the response. Worth a live PATCH check when Settings: Message settings group (default visibility, max length, messages per page, advanced post settings) #32/Settings: notification tray limit (notificationTrayLimit) #35/Settings: profile location (latitude/longitude) with runtime permission #37
    land.
  3. The error envelope's code is not read anywhere. ErrorDto in :core:network models only
    { "error": ... }, and safeApiCall maps HTTP status → AppError. Wiring code through means
    widening AppError and touching every construction site repo-wide — out of scope here, and
    filed separately.

…eviews

Adds the Settings surface the web app has and Android lacked, reached from
the Account hub at the `settings` route, structured as titled groups so the
remaining preference groups only have to add UI.

- `SettingsRepository` over `GET /api/user` + `PATCH /api/user/update`, with a
  process-scoped in-memory cache published as a flow, so the Settings screen
  and (later) the messages feed read the same values.
- `UpdateProfileRequest` now models all fifteen fields the PATCH endpoint
  accepts, all optional: untouched fields are omitted from the body, so a
  partial update can never clobber another preference. Edit-profile keeps
  sending only `displayName` + `bio`. Values are sent with the natural JSON
  types the live `GET /api/user` payload returns, not the `string` the
  auto-generated spec claims.
- `ViewingPreference` centralises the feed-filter wire values, taken from the
  live API: `GET /api/user` returns `all_messages`, and an invalid PATCH
  answers with the allow-list `my_messages, all_messages, followers_only,
  following_only`. Parsing stays tolerant of casing and shorthands so an
  unexpected spelling cannot silently reset a user's feed.
- UI for this issue's two preferences only: the feed filter and the
  `showPreviews` link-preview toggle, each saved as its own partial PATCH with
  optimistic update and rollback on failure.

Tests: repository round-trips and partial-PATCH body assertions, the four real
wire values pinned in both directions, ViewModel load/save/error states, and a
Compose test for the screen.

Closes #33
@Adron
Adron merged commit 22a2828 into parity/queue Sep 16, 2026
1 check passed
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