Repository navigation
Settings: account-status banner and gated-feature messaging (#50) - #170
Merged
Merged
Conversation
#131 modelled `accountStatus` and `cleared`; this surfaces them. Without it a probationary or restricted account collects bare 403s from half the app with nothing explaining why. The banner names the status, what it means, exactly what is locked, and the way out — verify-email for probation, appeal for restricted/ suspended/banned. Amber for probation (a state you can work your way out of), the view's existing #FFE81123 red for the three that need someone else to intervene, and a 4px status edge reusing the post-card vocabulary. Danger is the *default* branch on purpose: a status the server adds after this ships must not render as mild. That unknown branch names the raw status, declines to invent locks, and leaves every capability allowed rather than silently blocking things it doesn't understand. `AccountCapabilities` is the truth table behind it, exposed through `AccountStatusViewModel` as Can*/*BlockedReason pairs so a locked control binds `IsEnabled` and `ToolTip` in two lines and explains itself before the click instead of after. Probation deliberately leaves posting, replying, digging and following *allowed* — the docs rate-limit plain posting rather than blocking it, and the server enforces that. Sign-in: `banned` is the one status the app refuses rather than annotates. A saved token whose account has since closed is discarded instead of opening a shell that 403s on everything, and the login window stays up so the server's own rejection surfaces on retry. `restricted`/`suspended` explicitly still sign in and read — they get the read-only banner, which is the documented behaviour. The shared test account is `accountStatus: "active"`, so the other four statuses are NOT live-verifiable. All five were constructed locally and checked in a throwaway net10.0 console project outside the repo: 59 assertions over severity, headline, copy, the locked-feature list, every capability gate, the unknown-status and no-session fallbacks, and the live `active` payload for contrast. All pass. Debug and Release both build clean. MainWindow is the right host — the web shows this at the top of the home page — but it's in flight as #149, so the banner lands at the top of Settings. Same block, different host; moving it is a re-parent. Closes #50 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 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.
Stack
This is PR 3 of a 3-PR stack. Do not merge out of order.
issue-127-wire-types— addsaccountStatus/cleared+ theIsProbationary/IsReadOnly/IsBanned/NeedsStatusBannerhelpersthemefor real (#46) #167issue-46-honor-preferences— preference editing +themeprecedenceissue-46-honor-preferencesThe diff against
mainwould be unreviewable; review againstissue-46-honor-preferences.What this does
#131 modelled
accountStatus. This surfaces it. Without it a probationary or restricted account collects bare403s from half the app with nothing explaining why — which is what #50 called the single worst first-run experience available.The banner names the status, says what it means, lists exactly what is locked, and offers the way out — verify-email for probation, appeal for the rest.
AmberBrush) for probation: a state the user can work their way out of.#FFE81123red for restricted / suspended / banned: someone else has to intervene.active, so a normal account sees nothing.Danger is the default branch, on purpose. A status the server adds after this ships must not render as a mild, recoverable one. The unknown branch names the raw status verbatim, declines to invent a locked-feature list, and leaves every capability allowed rather than silently blocking things it doesn't understand.
AccountCapabilitiesis the truth table behind it — nine capabilities keyed off status, withBlockedReasonreturning null when allowed and a user-facing sentence when not.AccountStatusViewModelre-exposes it asCan*/*BlockedReasonpairs, so a locked control bindsIsEnabledandToolTipin two lines and explains itself before the click. That's the "disabled up front with the reason" criterion, as far as the file boundary reaches (see below).Probation deliberately leaves posting, replying, digging and following allowed. Per
/help/accountplain posting is rate-limited to a few per hour, not blocked, and the server enforces that — so disabling the composer would be wrong.Sign-in.
bannedis the one status the app refuses rather than annotates:403on every action.LoginSucceededrefuses to open the shell and leaves the login window up, so the server's own rejection surfaces on retry.restrictedandsuspendedexplicitly do not hit that path — the docs say those accounts can still sign in, read and browse. They get the read-only banner.No blocking native dialogs anywhere in this.
Verification
The test account is
accountStatus: "active", so the other four statuses are not live-verifiable — there is no way to observe a real probationary or restricted account without one. All five were constructed locally from the live payload's shape and checked in a throwawaynet10.0console project outside the repo (the WPF project can't run on macOS): 59 assertions, all passing, oversuspendedvsrestrictedproducing genuinely different copy, not one shared string;IsBannedbeing the only statusApprefuses sign-in on, andrestricted/suspendednot tripping it;activepayload producing no banner, for contrast.dotnet buildclean in both-c Debugand-c Release.Nothing was written to the shared test account by this PR — it is read-only work. (The preference writes and their restoration are documented in #167; every value there was restored to a byte-identical
GET /api/user, and that PR's probe token is revoked.)Spec-derived, not live-probed:
POST /api/auth/send-verification-emailiscookieAuth/x-auth-type: session, so a bearer-token client structurally cannot trigger it. The verify-email action is therefore a browser handoff, the same pattern the app already uses for billing and OAuth linking. There is no appeal endpoint at all, so that's a handoff too.What still needs wiring, and by which issue
Banner placement. The web shows this at the top of the home page and
MainWindowis the equivalent host here, butMainWindow.xaml/.xaml.csare off-limits while #149 is in flight. The banner lands at the top ofSettingsViewinstead — a real, visible banner in a permitted place beats a correctly-placed one that doesn't exist. Moving it is a re-parent: drop the sameBorderblock into the shell and bind it toAccountStatusViewModel; nothing in the view model is Settings-specific.Per-action disabling. The
Can*/*BlockedReasonsurface is built and tested, but every action it gates lives in a file this PR may not touch:FeedViewModel,Views/FeedView.xamlIsEnabled→CanPost/CanReply,ToolTip→ matching reasonMessageItemViewModelCanReactProfileViewModel,Views/PeopleView.xamlCanFollowDirectMessagesViewModel,Views/DirectMessagesView.xamlCanDirectMessageViews/FeedView.xamlCanUploadMedia/CanCrossPost/CanSchedulePostsListsViewModel,DocumentsViewModel,OrganizationsViewModelCanCreateContentNothing in Settings maps onto the documented locked set, so there was no reachable action here to gate — I'd rather say that than invent a policy the docs don't support (e.g. blocking profile edits for a restricted account, which
/help/accountdoes not list).Closes #50
🤖 Generated with Claude Code