USER_SAFETY.md step 4 (§4.3): the app half of report, block and hide.
- Long-press a Rust chat line for its menu: Report (the six reasons and an
optional detail), Hide for me, Block player where the line says canBlock,
and for admins and moderators Hide for everyone, or Show to everyone again
on a hidden line. Signed out, it says Sign in to report or block and opens
the login screen (S6).
- The feed is re-read after a hide, a block or a staff hide, never edited in
the app: the server filters it per reader (S11). A report changes nothing
and does not re-read.
- Staff see a line hidden for everyone dimmed and marked Hidden by staff (S10).
- Account -> Blocked lists account and Steam-ID blocks with Unblock, says when
a block no longer applies because its target is now staff (S8), and offers
Show them again for hidden items.
- Core's routes on a new SafetyApi; the module's two staff routes on RustApi.
RustEventDto gains canBlock and hidden.
Two fixes from the emulator walk: Back on the report form or the block
confirmation now steps back to the menu instead of closing the sheet (Back
to put the keyboard away was throwing a half-written report away), and the
sheet opens fully so Send report is on screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
ACCOUNT_DELETION.md §4.3 (step 4). My account gains a Delete my account
row, last and set apart, which opens its own screen: what goes, what stays,
the staff note, then the typed username, the password and, when two-factor is
on, an authenticator or recovery code. Delete stays off until the name matches.
MeApi.deleteAccount sends DELETE /auth/me/account and reads the raw response,
so each refusal's code becomes its own sentence (the website's words). A
wrong password is a 400 (AD12), so the authenticator never replays it.
On success AuthRepository.forgetDeletedAccount does what a sign-out does
without its server calls: the inbox snapshot, the trust token (which a
sign-out keeps, but the account is gone) and the session, whose teardown
also drops the push registration. It is not cancellable. The route holds the
view model, so it tells a deletion from any other sign-out, and opens sign-in
with "Your account has been deleted." on the shell's snackbar.
Tests: the refusal mapping per code, 429, unknown codes, no answer, the
request body; the confirm rule, the staff rule, each refusal's sentence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PushService was a dataSync foreground service. Android 15 stops dataSync
after six hours a day and expects the service to stop itself on
onTimeout; it never did, so with target 36 the app would crash. A held-open
push connection is not a transfer either, which is what Play reserves
dataSync for.
- The service is now specialUse, with the PROPERTY_SPECIAL_USE_FGS_SUBTYPE
reason Play reviews (ntfy does the same on Play). No daily limit.
- PushBootReceiver restarts it after BOOT_COMPLETED and MY_PACKAGE_REPLACED
when push was on and a registration is saved (a sign-out clears it). It
starts the service from the saved topic instead of re-registering, since
the network is often not up at boot.
PLAN.md M8, decided by the org lead 2026-10-29.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Step 4 of docs website/DEMO_MODE.md §10 (website#222).
- DemoLockInterceptor on the main client peeks every 403 body. On
code DEMO_LOCKED it reports the server's sentence to DemoNotices, and the
shell shows it as one snackbar, like the web's toast (DM19). A newer refusal
replaces the one showing. Screens added later get it without doing anything.
- ApiResult.HttpError carries the body's code (read only on a 403, peeked so
the body is not consumed); isDemoLocked(). ErrorKind.DEMO_LOCKED is mapped
ahead of the status in toUiState and toShardUiState, and is not retryable.
- Screens whose 403 copy blames the role now say the demo sentence instead:
the four admin view models, Characters' account creation, and the contact
form's line (the contact form is locked on the demo).
- Home draws the strip from GET /public/settings' demo block: the wipe time in
UTC, the published usernames, and the site's sign-in page for the passwords.
The app does not list the passwords (DM20). A real shard sends no block, so
it draws nothing.
Checked on an emulator against website main (69998ec) with DEMO_MODE=true and
seed:demo: the strip on Home; demo-player's username change refused with the
snackbar under the screen's own line; demo-admin's site-mode switch refused
with the demo sentence in place of "You don't have permission".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Step 1 of M8 (docs/android/PLAN.md item 9).
- compileSdk/targetSdk 36 (Play's floor since 2026-08-31). AGP 8.6.1
cannot build against 36, so AGP goes to 8.13.2 (the last 8.x) with the
Gradle 8.14.3 wrapper. Kotlin, KSP and Hilt are unchanged. All three
workflows install platforms;android-36; AGP 8.13 still defaults to
build-tools 35.0.0.
- release.yml also runs bundleRelease, signed with the same keystore,
and attaches the .aab to the Gitea release beside the APK (the first
bundle has to be uploaded by hand, and this is where it comes from).
- A last step uploads the bundle to Play's internal track through the
Play Developer API v3 edit flow. The OAuth token comes from a
service-account JWT signed with openssl; the key is the new
PLAY_SERVICE_ACCOUNT_JSON secret. While that secret is unset, the
step warns and skips. The step runs after the Gitea release, so a
Play failure never holds back the APK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY