Repository navigation
Conversation
…for real #131 modelled the 14 missing `GET /api/user` fields; this makes them editable and makes the one consumption point that isn't in an in-flight file actually obey its value. Theme precedence (#46's acceptance criterion) now works: an explicit server `theme` of light/dark wins over the OS, and `system` — or null, or anything else — falls back to the existing HKCU/`UserPreferenceChanged` behaviour, which stays wired. `App` re-resolves on every `CurrentUser` change, so login, session restore and a settings save all take effect without a restart. The fallback is not defensive padding: the server does NOT validate `theme` (it accepted and stored `"zzz_bogus"` in a live probe), so an unrecognized value has to degrade to the OS setting. Fixes a live 500 along the way. `UpdateProfileAsync` built its body from an anonymous object, and `JsonSerializerDefaults.Web` does not drop nulls, so every "Save profile" shipped `"theme": null` — which the server answers with `500 internal_error`, writing nothing. Both writers now build a sparse dictionary and omit untouched fields, which also stops a profile save from stomping preferences set elsewhere. Verified live 2026-09-16 against the shared test account with a throwaway `issue-46-probe` sync-token. All 15 `PATCH /api/user/update` field names accept native JSON numbers/booleans (the OpenAPI *request* schema types them as `"string"` — that's wrong). The server named the `viewingPreference` enum itself in a 400, and enforces maxMessageLength 1–10000, messagesPerPage 10–30, notificationTrayLimit 10–40; those ranges are now validated client-side so a typo is an inline message rather than a raw 400. Every value changed was captured first and restored; a follow-up `GET /api/user` diffed byte-identical. The PATCH 200 does return a `user`, but a narrower one than `GET /api/user` (no accountStatus/cleared/pendingEmail/isAdministrator), so callers re-fetch rather than trust it — house read-after-write rule. 33 assertions covering the theme truth table, the normalizers, the sparse-body serialization and the captured live payload were run in a throwaway net10.0 console project outside the repo; all pass. Debug and Release both build clean. Closes #46 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified your
|
|
Auto-closed by GitHub when its base branch I've switched to merging without |
Stack
This is PR 2 of a 3-PR stack. Do not merge out of order.
issue-127-wire-types— adds the 14 missingCurrentUserfields + the derived helpersissue-127-wire-types— makes them editable, honorsthemeissue-46-honor-preferences— the account-status bannerEvery field this PR edits comes from #131, so this is unreviewable and unbuildable against
mainon its own.What this does
#131 did the modelling. This does the honoring — or as much of it as the file boundaries allow.
SettingsViewfor all ten writable preferences: theme, home-feed filter, max message length, messages per page, alerts tray limit, link previews, default post visibility, advanced post settings, and profile lat/long. Radio groups for the two enum-shaped fields (a small two-wayStringEqualsToBoolConverterkeeps the view model's plain strings, which is what the API wants, without a ComboBox to re-theme), numeric fields for the bounded ones, checkboxes for the flags.themeprecedence, the one acceptance criterion whose consumption point is in a permitted file.Appnow resolvesUserPreferenceOptions.ResolveDark(serverTheme, IsSystemDarkMode()): explicitlight/darkwins outright,systemfalls back to the OS. The HKCU read and theSystemEvents.UserPreferenceChangedlistener both stay — the listener just re-resolves the precedence instead of deciding alone, so flipping Windows' theme no longer moves an app whose account pinned a mode.Appre-resolves on everySessionService.CurrentUserchange, which covers session restore, login, and the settings save's read-after-write. No restart needed.UpdateProfileAsyncbuilt its request from an anonymous object, andJsonSerializerDefaults.Webdoes not drop nulls, so every "Save profile" shipped"theme": null. That is a500 internal_errorserver-side (isolated repro below), which means the existing Save button on this screen was failing against the live API. Both writers now build a sparse dictionary and omit untouched fields — which also stops a profile save from stomping preferences set elsewhere.Verified live
Probed 2026-09-16 against the shared test account with a throwaway
issue-46-probesync-token (revocation note at the bottom).All 15
PATCH /api/user/updatefield names, confirmed accepted and round-tripping:displayName,bio,avatar,isPrivateAccount,githubDefaultRepo,theme,maxMessageLength,messagesPerPage,viewingPreference,showPreviews,notificationTrayLimit,defaultPubliclyVisible,showAdvancedPostSettings,latitude,longitude.Findings worth not re-learning:
"string". Sending real numbers and booleans works and round-trips. (The response schema,UserWire, does use native types.)nullis a 500, not a no-op.{"theme": null}→500 internal_error, and a follow-upGETconfirmed it wrote nothing. This is the root cause of the profile-save bug above, and the reason both writers are sparse.viewingPreferenceis enum-validated and the server names the values itself.{"viewingPreference":"zzz_not_a_real_option"}→400 viewingPreference must be one of: my_messages, all_messages, followers_only, following_only. So the documented "My / All / Followers Only / Following Only" maps to those four snake_case wire values.themeis NOT validated. The server accepted and stored"zzz_bogus". That is whyResolveDarktreats anything unrecognized as "follow the OS" rather than assuming one of three values — it's a real case, not defensive padding.maxMessageLength1–10000,messagesPerPage10–30,notificationTrayLimit10–40.useris narrower thanGET /api/user— noaccountStatus,cleared,pendingEmail,isAdministrator, or the layout/notification blobs. Deserializing it intoCurrentUserwould silently blank the account-status banner Settings: account-status banner and gated-feature messaging #50 adds, so callers re-fetch. House read-after-write rule, now with a concrete reason.GET /api/messageshas no viewing-preference parameter — onlylimit,offset/cursor,onlyMine,tag. There is no/api/feed. See the wiring notes below.showPreviews' data source isGET /api/messages/{id}/metadata("Returns ONLY the message's stored link metadata"). The app never calls it today.Restored on the shared account
Every value I changed was captured before the write and restored afterwards, and the restore was confirmed by a
GET /api/userthat diffed byte-identical to the pre-probe capture:themelightdark, thenzzz_boguslightmaxMessageLength666500666messagesPerPage202520notificationTrayLimit203020showPreviewsfalsetruefalsedefaultPubliclyVisiblefalsetruefalseshowAdvancedPostSettingstruefalsetruelatitude47.606247.6147.6062longitude-122.3321-122.33-122.3321viewingPreferenceall_messagesNo
email,username, or other account-identifying field was touched.Offline proof
33 assertions — the theme truth table, the normalizers, the sparse-body serialization (including a contrast case showing the old anonymous-object shape did emit
"theme":null), theEffective*clamps, and the captured liveGET /api/userpayload — run in a throwawaynet10.0console project outside the repo. All pass. The WPF project can't run on macOS, hence the console harness.dotnet buildclean in both-c Debugand-c Release.Consumption points still needing wiring, and by which issue
FeedViewModel,MainWindow, andViews/FeedView.xamlare off-limits here (in flight as #149 and the feed work). The Settings side — read, edit, validate, persist, refresh — is complete for every field; these consumers still need a line each.EffectiveMessagesPerPageandEffectiveNotificationTrayLimitexist precisely so each is a one-liner.messagesPerPageFeedViewModel.cs:12PageSize = 20_session.CurrentUser?.EffectiveMessagesPerPage ?? 20. One line.notificationTrayLimitNotificationsViewModel.cs:34GetNotificationsAsync(limit: 20)_session.CurrentUser?.EffectiveNotificationTrayLimit ?? 20. One line.maxMessageLengthFeedViewModel+FeedView.xamlCurrentUser.MaxMessageLength(guard> 0).showAdvancedPostSettingsFeedView.xamlVisibilityto the flag.showPreviewsFeedView.xaml/MessageItemViewModelGET /api/messages/{id}/metadata. Needs preview cards built first.falseon the test account, so previews must be off by default there.viewingPreferenceFeedViewModel+InterlinedApiClient.csmy_messagesmaps cleanly to the existingonlyMine=truequery param andall_messagesis the default, butfollowers_onlyandfollowing_onlyhave no server-side equivalent —GET /api/messagesexposes no such filter and there is no/api/feed. Either the web filters through a session-only route, or it's client-side overGET /api/follow/{userId}/following. Worth confirming before building.defaultPubliclyVisibleFeedViewModel.cs:282.themeApp.xaml.cslatitude/longitudeaccountStatus/clearedare surfaced by the stacked #50 PR.Probe token — revoked
The
issue-46-probetoken (sessione3d2728b-4f9e-41de-8691-d16dbea003bd) is revoked:GET /api/user/sessionswent 1278 → 1277 rows with zeroissue-46-*labels remaining. Nothing left dangling in the token pile.Two incidental findings there:
DELETE /api/user/sessions/{own-id}returned204and really did remove the row. It also reportedisCurrent: falsefor the very token making the request, so "current" appears to mean cookie-session, not bearer.GET /api/userwith the revoked token still returned200right after the204, while the session row was already gone — so there's a server-side token cache. Worth knowing before anyone builds a "sign out this device, verify it's dead" flow on top of the Settings session list.Closes #46
🤖 Generated with Claude Code