Files
buzz/mobile
Wes 01091c15a1 fix(mobile): recover stale and shuffled messages (#6691)
## Summary

- refetch mounted mobile thread replies after relay reconnect,
preserving the previous reply list during recovery
- auto-dispose route-scoped relay reply caches so reopening a thread
queries current relay state
- invalidate live replies through both the channel-window and legacy
websocket-history paths
- preserve optimistic-reply confirmation when the route closes before
its deferred cleanup
- stabilize rapid same-second messages using desktop's existing split
contract: channel timelines render `(created_at ASC, id DESC)` while
threads render `(created_at ASC, id ASC)`
- retain late live rows after a channel window is exhausted instead of
dropping same-second tail messages

Closes #4404.
Closes #4830.
Closes #6204.

## Context

The broad all-channel/all-DM stale-session defect reported in #4402 is
already addressed on current `main` by #4372 and #3053. Two distinct
mobile gaps remained:

1. `threadRepliesProvider` was a process-lifetime one-shot query, so
replies missed while the socket was stale remained absent after
reconnect or after closing and reopening the thread.
2. Mobile had inconsistent timestamp-only and event-id ordering across
channel producers. Rapid messages routinely share Nostr's one-second
timestamp, so later hydration/live reconciliation could reshuffle them.
Desktop deliberately has two render contracts: channel windows reverse
the relay's composite order to `(created_at ASC, id DESC)`, while thread
replies use `(created_at ASC, id ASC)`.

This consolidates the current-main portions of #4831 and #3243 rather
than reviving stale overlapping branches.

## Validation

Exact pushed head: `be92d9542c6cd1342733bdc5e8359664b511ce02`

- focused channel-provider/window/thread suites: 48/48 passed
- incident regression: a mounted thread misses a reply while
disconnected, reconnects, and renders the recovered reply
- route regression: closing and reopening a thread performs a fresh
authoritative query
- websocket fallback regression: live reply invalidates the mounted
thread even without the channel-window path
- disposal regression: optimistic confirmation survives provider
disposal between rebuild and deferred cleanup
- ordering regressions: channel window/live, websocket fallback,
optimistic sends, deep links, both pagination paths, and thread merges
preserve their desktop-compatible same-second order
- boundary regression: exhausted windows admit late same-second live
rows without weakening open-page cursor boundaries
- independent adversarial review: no production blocker; source contract
verified across all producers and relay cursor semantics unchanged
- pre-push Mobile lane passed at exact head, including analysis,
file-size/branch checks, and full Flutter suite: 1,675/1,675 passed
- `git diff --check`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-24 10:59:58 -06:00
..
2026-08-22 05:28:16 -07:00

Buzz Mobile

Flutter mobile client for Buzz.

Setup

Use the Flutter SDK pinned by the repository. Activate Hermit from the repo root before resolving packages or running any Flutter command:

cd /path/to/buzz
. ./bin/activate-hermit
./bin/just mobile-install

mobile-build-android intentionally builds with --no-pub. If an IDE or an external Flutter SDK has touched mobile/.dart_tool, rerun mobile-install with the pinned SDK before building so flutter_test, sky_engine, and the engine all come from the same Flutter version.

Run

# From repo root (applies a worktree-isolated debug identity and starts/reuses Simulator):
just mobile-dev

# Direct (uses the app's configured community; apply worktree overrides first):
cd mobile && flutter run

Worktree-aware debug identity

Debug builds produced from a git worktree get a unique app identifier keyed to the worktree directory name (com.buzz.buzzMobile.<slug> on iOS, xyz.block.buzz.mobile.<slug> on Android) plus a display-only branch label in the app name (Buzz (my-branch), or a short SHA when the worktree is detached). Because the identifier follows the directory rather than the branch, one worktree keeps exactly one installed app — and its login state — across branch switches, and builds from multiple worktrees install side by side, mirroring the desktop dev experience. Release and profile builds always keep the production identity and name.

just mobile-dev and just mobile-build-android apply this automatically by running scripts/mobile-worktree-overrides.sh, which writes two gitignored files:

  • mobile/ios/Flutter/WorktreeOverrides.xcconfig (included by Debug builds only; a developer's AppOverrides.xcconfig is included after it, so app-specific overrides like a personal BUNDLE_IDENTIFIER for device signing always win)
  • mobile/android/worktree.properties (read by the debug build type only)

Android developers can keep a stable local test identity that takes precedence over the generated worktree values by creating the gitignored mobile/android/AppOverrides.properties:

appName=Buzz Pairing
applicationIdSuffix=.device_pairing_e2e1

These values are consumed by the debug build type only. The standard just mobile-build-android command can still be used; regenerating worktree.properties does not overwrite AppOverrides.properties. Release and profile builds keep the production Buzz name and application ID.

For direct Xcode / Android Studio / flutter run development, run ./scripts/mobile-worktree-overrides.sh from the repo root once per branch switch to refresh the display label (the install identity never changes); the persisted files are then picked up by any subsequent build. In the main checkout the script is a no-op that removes stale override files, restoring the plain Buzz identity.

For an Android debug build that must remain installed alongside other Buzz worktree builds, set an explicit launcher name and package suffix when invoking the generator or a recipe that invokes it:

BUZZ_ANDROID_DEBUG_APP_NAME="Buzz Huddles" \
BUZZ_ANDROID_DEBUG_ID_SUFFIX=".huddles_829c" \
./bin/just mobile-build-android

This example produces the debug-only package xyz.block.buzz.mobile.huddles_829c with the launcher label Buzz Huddles. The suffix must start with a dot followed by a lowercase letter and may contain only lowercase letters, digits, and underscores. Release and profile builds ignore these overrides and retain the production package and name.

To remove leftover worktree-suffixed installs from booted iOS simulators and connected Android emulators, run just mobile-clean (add --dry-run via ./scripts/mobile-worktree-clean.sh --dry-run to preview). Production installs are never touched.

Checks

dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test

Or from the repo root: just mobile-check and just mobile-test.

Android release signing

Android release builds fail unless all upload-key inputs are supplied through the environment:

  • BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH: path to a CI-vended keystore file
  • BUZZ_ANDROID_UPLOAD_KEYSTORE_PASSWORD
  • BUZZ_ANDROID_UPLOAD_KEY_ALIAS
  • BUZZ_ANDROID_UPLOAD_KEY_PASSWORD

The keystore path must be absolute, and the keystore must remain outside the repository. Development and debug builds do not require these variables.

Release pipelines that sign through the central APK Signer service instead of a local upload keystore must set BUZZ_ANDROID_RELEASE_SIGNING=external. That mode produces an unsigned release bundle and refuses to run if any BUZZ_ANDROID_UPLOAD_* value is also set.

Architecture

lib/
├── main.dart              # Entry point, Riverpod bootstrap
├── app.dart               # MaterialApp with theme
├── shared/
│   └── theme/             # Catppuccin light/dark, spacing tokens, extensions
└── features/
    └── home/              # Placeholder home surface
  • State management: Riverpod + Hooks (HookConsumerWidget)
  • Theme: Catppuccin Latte (light) / Macchiato (dark) — matches desktop
  • Spacing: Grid tokens for consistent spacing
  • Linting: flutter_lints + riverpod_lint via custom_lint
  • Feature isolation: No cross-feature imports except shared/