chore(release): cut edge over to main — Google Play, account deletion, report/block/hide (v0.7.0) #65

Merged
whitlocktech merged 11 commits from edge into main 2026-10-30 17:26:19 -05:00
Member

This is M8's cutover (android/PLAN.md M8, "the order from here" step 5; ACCOUNT_DELETION.md §6 step 7; USER_SAFETY.md §7 step 6). edge → main carries five PRs since v0.6.0. After it merges I tag v0.7.0 on the merge commit. That's a minor bump: new features on a pre-1.0 app, versionCode 700. release.yml then builds the signed APK and bundle and publishes the release.

⚠️ Before this merges (org lead, by hand)

Enroll the existing release keystore as the app-signing key in Play Console → App integrity → "use my own key". PLAY_SERVICE_ACCOUNT_JSON is already set, so the v0.7.0 release run will call Play. The signing choice is permanent.

On the draft app, CI's upload is refused as the plan expects ("Only releases with status draft may be created on draft app"). That failure comes after the Gitea release exists, so the APK channel isn't affected. The first bundle is then uploaded by hand from the release's .aab (M8 step 2).

What ships

  • #60 — M8 step 1: compile/target API 36 (AGP 8.13.2, Gradle 8.14.3). The release also builds an AAB, attaches it to the Gitea release and uploads it to Play's internal track.
  • #61 — demo mode: the demo's 403 DEMO_LOCKED refusals show as one app-wide snackbar in the server's words, and Home gets the demo strip (DM19, DM20).
  • #62 — push: the ntfy connection runs as a specialUse foreground service, not dataSync. Android 15+ caps dataSync at six hours a day, and that cap would have crashed PushService. Push also restarts after a reboot or an update when it was on.
  • #63 — account deletion: Delete my account on My account. The refusals use the website's wording, the trust token is dropped, and the app lands on sign-in with a notice (AD1, AD7, AD12).
  • #64 — report, block and hide: a long-press menu on Rust chat lines (Report, Hide for me, Block player, and Hide for everyone for staff), plus Account → Blocked (S8, S11-S14).

Verified

  • edge at b20abdf (this head) has the same tree as 5386889, #64's head. Its pr-checks run (#97: unit tests, lint, debug build) passed in 24m48s. This PR's own run gates the merge.
  • Each feature was walked on the emulator in its own PR: #60 on API 36.1 and s22_ultra; #61 on s22_ultra against the public demo; #62 and #63 on s22_ultra; #64 on Medium_Phone_API_36.1. The local walk of a demo copy is recorded in docs/website/USER_SAFETY.md §7 step 5.

The server half: Module-Rust#73

The chat menu and deletion's unlink need Module-Rust's rust.chat target and onUserDeleting. Those are on Module-Rust edge, and Module-Rust#73 cuts them over as v0.7.0. The demo installs the newest module release on its next wipe, so the features are there for the Play reviewer. The demo's seed is not touched (org lead, 2026-10-30).

After the merge

  1. Tag v0.7.0 on the merge commit, which triggers release.yml.
  2. Check the release's APK, AAB and SHA256SUMS, and that the Play step failed only with the draft-app refusal.
  3. runicgateway.com: bump platform.json to Android-app v0.7.0 and Module-Rust v0.7.0.
  4. docs: record the cutover in android/PLAN.md M8.
  • AI-assisted: Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

This is M8's cutover (`android/PLAN.md` M8, "the order from here" step 5; `ACCOUNT_DELETION.md` §6 step 7; `USER_SAFETY.md` §7 step 6). `edge` → `main` carries five PRs since **v0.6.0**. After it merges I tag **`v0.7.0`** on the merge commit. That's a minor bump: new features on a pre-1.0 app, versionCode 700. `release.yml` then builds the signed APK and bundle and publishes the release. ## ⚠️ Before this merges (org lead, by hand) **Enroll the existing release keystore as the app-signing key** in Play Console → App integrity → "use my own key". `PLAY_SERVICE_ACCOUNT_JSON` is already set, so the `v0.7.0` release run will call Play. The signing choice is permanent. On the draft app, CI's upload is refused as the plan expects ("Only releases with status draft may be created on draft app"). That failure comes *after* the Gitea release exists, so the APK channel isn't affected. The first bundle is then uploaded by hand from the release's `.aab` (M8 step 2). ## What ships - **#60 — M8 step 1:** compile/target API 36 (AGP 8.13.2, Gradle 8.14.3). The release also builds an AAB, attaches it to the Gitea release and uploads it to Play's internal track. - **#61 — demo mode:** the demo's `403 DEMO_LOCKED` refusals show as one app-wide snackbar in the server's words, and Home gets the demo strip (DM19, DM20). - **#62 — push:** the ntfy connection runs as a `specialUse` foreground service, not `dataSync`. Android 15+ caps `dataSync` at six hours a day, and that cap would have crashed `PushService`. Push also restarts after a reboot or an update when it was on. - **#63 — account deletion:** *Delete my account* on *My account*. The refusals use the website's wording, the trust token is dropped, and the app lands on sign-in with a notice (AD1, AD7, AD12). - **#64 — report, block and hide:** a long-press menu on Rust chat lines (*Report*, *Hide for me*, *Block player*, and *Hide for everyone* for staff), plus *Account → Blocked* (S8, S11-S14). ## Verified - `edge` at `b20abdf` (this head) has the same tree as `5386889`, #64's head. Its `pr-checks` run (#97: unit tests, lint, debug build) passed in 24m48s. This PR's own run gates the merge. - Each feature was walked on the emulator in its own PR: #60 on API 36.1 and `s22_ultra`; #61 on `s22_ultra` against the public demo; #62 and #63 on `s22_ultra`; #64 on `Medium_Phone_API_36.1`. The local walk of a demo copy is recorded in `docs/website/USER_SAFETY.md` §7 step 5. ## The server half: Module-Rust#73 The chat menu and deletion's unlink need Module-Rust's `rust.chat` target and `onUserDeleting`. Those are on Module-Rust `edge`, and **Module-Rust#73** cuts them over as **v0.7.0**. The demo installs the newest module release on its next wipe, so the features are there for the Play reviewer. The demo's seed is not touched (org lead, 2026-10-30). ## After the merge 1. Tag `v0.7.0` on the merge commit, which triggers `release.yml`. 2. Check the release's APK, AAB and `SHA256SUMS`, and that the Play step failed only with the draft-app refusal. 3. runicgateway.com: bump `platform.json` to Android-app v0.7.0 and Module-Rust v0.7.0. 4. docs: record the cutover in `android/PLAN.md` M8. - [x] AI-assisted: Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 10 commits 2026-10-30 14:54:26 -05:00
feat(release): target API 36 and upload each tag's bundle to Google Play
All checks were successful
PR Checks / android-build (pull_request) Successful in 21m22s
7a8231ba9c
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
feat(demo): explain the public demo's refusals and show its strip on Home
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m2s
aca358d72b
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
Reviewed-on: #60
Reviewed-on: #61
fix(push): hold the ntfy connection as specialUse and restart it on boot
All checks were successful
PR Checks / android-build (pull_request) Successful in 24m52s
690441a8c3
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
Reviewed-on: #62
feat(account): delete my account in the app
All checks were successful
PR Checks / android-build (pull_request) Successful in 14m55s
6be83fe70a
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
Reviewed-on: #63
feat(safety): report, block and hide chat lines in the app
All checks were successful
PR Checks / android-build (pull_request) Successful in 24m48s
5386889ef4
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
Merge pull request 'feat(safety): report, block and hide chat lines in the app' (#64) from feat/user-safety into edge
All checks were successful
PR Checks / android-build (pull_request) Successful in 4m41s
b20abdf4ba
Reviewed-on: #64
whitlocktech added 1 commit 2026-10-30 17:23:08 -05:00
Merge branch 'main' into edge
All checks were successful
PR Checks / android-build (pull_request) Successful in 3m3s
6630d05e76
whitlocktech approved these changes 2026-10-30 17:23:25 -05:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-10-30 17:23:33 -05:00
whitlocktech merged commit 02fb7d47d5 into main 2026-10-30 17:26:19 -05:00
Sign in to join this conversation.
No description provided.