TEMPORARY DEMO-ONLY: add a foreground Remind me notification action that opens the exact message and presents only the five fixed reminder presets. This stretch commit is isolated for quick reversion if it interferes with the demo.
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
Ai-assisted: true
### What changed?
After a new mobile invite claim succeeds, Buzz now best-effort ensures
membership in the same public starter channels as desktop:
- `#general`
- `#welcome-everyone`
Missing starters use desktop's deterministic per-relay IDs and exact
public channel configuration, so concurrent mobile and desktop setup
converges safely. Setup failures do not invalidate or retry an already
successful invite claim, and failure for one starter does not block the
other.
The success sheet offers **Continue to #welcome-everyone** when that
channel is available. Mobile does not create the private `Welcome`
channel because it cannot provision the desktop Welcome agents that make
that channel useful.
This PR is stacked on #6145 because it deliberately reuses that PR's
open-channel directory and join behavior. Once #6145 merges, this PR can
be retargeted to `main` without changing its BUZZ-12 diff.
Fixes
[BUZZ-12](https://linear.app/squareup/issue/BUZZ-12/bug-community-appears-empty-after-using-invite-link-on-mobile).
### How is it tested?
- Desktop/mobile deterministic starter-ID parity coverage.
- Existing-channel join and missing-channel creation coverage.
- Duplicate-create convergence and per-channel failure isolation
coverage.
- Invite success remains successful when starter setup fails.
- Widget coverage for continuing directly into `#welcome-everyone`.
- Focused invite/deep-link tests: 23 passed.
- `just mobile-check`: passed.
- `just mobile-test`: 1,483 passed.
- Pre-push repository checks: passed.
---------
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
## 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>
## Summary
- downgrade Mobile Huddle authentication and native media configuration
from protocol v3 to the currently deployed relay's v2 contract
- restore the released one-byte relay peer prefix while retaining later
reconnect, roster, and playout-reset reliability fixes
- update Android, iOS, protocol documentation, and focused tests
together
Protocol v2 does not carry v3's occupancy epoch on audio frames, so it
cannot fence the narrow delayed-packet/peer-index-reuse race. This is an
intentional compatibility tradeoff until the relay v3 rollout is ready.
### Related issue
None found.
### Testing
- `just mobile-check`
- `just mobile-test` — 1,661 tests passed
- Android debug build installed and launched on Pixel 10 as
`xyz.block.buzz.mobile.sprout_mobile_profile_settings`; foreground
process verified
- signed iOS Release build installed and launched on iPhone as
`com.buzz.buzzMobile`; running process verified
A live two-device Huddle audio call remains a manual verification step.
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
- align iOS back controls and affected titles across channel details,
settings, and pairing
- make channel star and mute actions reflect their state immediately
- animate locally sent channel, thread, and DM messages from behind the
composer with a 300ms ease-out
## Testing
- `just mobile-check`
- full mobile test suite (1,573 tests)
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
## Summary
- Hide the thread Latest control once the lazy tail is fully reached.
- Polish the mobile channel header and use native iOS liquid glass for
Back.
- Keep the two-line header aligned and safe at larger text sizes.
## Validation
- `just mobile-check`
- `flutter test` (1,553 tests)
- Signed iPhone build installed and launched
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
## Summary
- align the mobile security-code confirmation screen with the Add
Community visual system while keeping the SAS verification flow
unchanged
- use “Confirm desktop code” and a 15% primary-color outline on each
digit box
- support a gitignored, debug-only Android app name and application ID
override for side-by-side device testing; release and profile identities
remain unchanged
## Validation
- `flutter test test/features/pairing/pairing_page_test.dart` (21 tests)
- full mobile pre-push suite (1,469 tests)
- focused Flutter analysis and formatting
- mobile worktree identity contract checks
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** new-feature
**User Impact:** Mobile users must confirm with Face ID, biometrics, or
their device passcode before sending their Buzz identity to Desktop.
**Problem:** A signed-in phone could send its full identity, including
the `nsec`, to a desktop without fresh local verification.
**Solution:** Require OS device authentication before opening the
identity-recovery scanner, retain that authorization only for the active
pairing session and short pairing window, and require fresh
authentication again if it expires before the identity payload is sent.
Normal app opening, identity import, and community removal remain
unchanged.
## Screencasts
| Enable Face ID | Use Face ID |
| --- | --- |
| 
| 
|
<details>
<summary>File changes</summary>
**Android and iOS integration**
- `mobile/android/app/build.gradle.kts` declares the AppCompat
dependency required by the biometric activity theme.
-
`mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/MainActivity.kt`
uses the activity type required by the system authentication prompt.
- `mobile/android/app/src/main/res/values/styles.xml` and
`mobile/android/app/src/main/res/values-night/styles.xml` use the
compatible launch theme.
- `mobile/ios/Podfile.lock` records the native local-authentication
dependency.
- `mobile/ios/Runner/Info.plist` explains why Buzz requests Face ID
access.
**Identity policy and pairing flow**
- `mobile/lib/shared/security/sensitive_action_authorizer.dart` wraps OS
authentication and maps platform errors to stable app-level outcomes.
- `mobile/lib/shared/community/community.dart` and
`mobile/lib/shared/community/community_storage.dart` persist the
sensitive-action policy.
- `mobile/lib/features/invites/invite_join_provider.dart` assigns the
explicit policy for invite-created communities.
- `mobile/lib/features/pairing/pairing_provider.dart` gates export,
binds grants to the active community/session, reauthenticates expired
grants, and clears grants on every terminal path.
- `mobile/lib/features/pairing/pairing_page.dart` lets users choose
biometric protection while importing an identity.
- `mobile/lib/features/settings/settings_page.dart` wires pairing into
settings.
- `mobile/lib/features/settings/settings_page/connection_section.dart`
authenticates before opening export recovery and bounds the
foreground-resume wait.
- `mobile/pubspec.yaml` and `mobile/pubspec.lock` add and lock
`local_auth`.
**Coverage**
- `mobile/test/shared/security/sensitive_action_authorizer_test.dart`
covers native result mapping, unsupported devices, and single-flight
behavior.
- `mobile/test/shared/community/community_test.dart` and
`mobile/test/shared/community/community_storage_test.dart` cover policy
defaults and persistence.
- `mobile/test/features/invites/invite_join_provider_test.dart` covers
the invite policy.
- `mobile/test/features/pairing/pairing_page_test.dart` covers import
protection controls.
- `mobile/test/features/pairing/pairing_provider_test.dart` covers
export/import authorization, stale/reset/concurrent guards, malformed
payload cleanup, and no-export failure paths.
- `mobile/test/features/settings/connection_section_test.dart` covers
the tap gate, lifecycle resume, and timeout behavior.
</details>
## Reproduction steps
1. Pair an identity into the mobile app.
2. Open Settings and choose “Send identity to desktop.”
3. Verify Face ID, biometrics, or the device passcode is required before
the recovery scanner opens.
4. Cancel device authentication and verify the scanner does not open and
no identity transfer begins.
5. Authenticate, scan a Desktop recovery code, confirm the SAS, and
verify the identity transfer completes.
## Validation
At `be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b`:
- `cd mobile && ../bin/flutter analyze` — no issues
- `cd mobile && ../bin/flutter test` — 1,368 tests passed
- `cd mobile/android && JAVA_HOME=$(/usr/libexec/java_home -v 21)
./gradlew app:assembleDebug` — debug APK assembled successfully
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary
Buzz Mobile now expands decrypted ACP observer batch envelopes into
their inner telemetry frames before sending them through the existing
per-agent dedupe, ordering, cap, and channel-filter pipeline. Singleton
observer events keep their existing behavior.
Malformed batch envelopes remain visible as outer frames, matching the
desktop consumer convention, while invalid inner frames use the existing
observer decrypt error path. This restores batched agent progress, tool
activity, and incremental transcript updates that Mobile previously
ignored.
### Related issue
Related to #4917.
### Testing
Added tests:
-
[`observer_subscription_test.dart`](https://github.com/block/buzz/blob/main/mobile/test/features/channels/agent_activity/observer_subscription_test.dart)
covers valid batches, singleton behavior, malformed envelopes, and
invalid inner frames.
Full mobile analysis, formatting, file-size validation, and Flutter
tests passed. The repository pre-push gate also passed.
---------
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now display recognizable context and navigate reliably in
the mobile app.
**Problem:** Bare Buzz permalinks appeared as raw or ambiguous URLs on
mobile, while channel and message links were not handled consistently
across Markdown forms and startup states.
**Solution:** Normalize eligible bare Buzz URLs without consuming
Markdown syntax, render them as semantic icon-prefixed chips, and route
channel/message targets through the mobile deep-link dispatcher while
preserving authored Markdown labels as ordinary links.
<details>
<summary>File changes</summary>
**mobile/lib/features/channels/deep_link_dispatcher.dart**
Routes parsed channel and message links through the appropriate in-app
navigation callbacks.
**mobile/lib/features/channels/message_content.dart**
Presents all bare Buzz permalinks as semantic icon chips and keeps
authored labels as ordinary links.
**mobile/lib/features/channels/message_content/link_normalizer.dart**
Normalizes bare and autolinked Buzz URLs without consuming Markdown
delimiters, code, or punctuation.
**mobile/lib/shared/deeplink/deep_link.dart**
Adds strict channel and project-entity parsing alongside message deep
links.
**mobile/lib/shared/deeplink/pending_deep_link_provider.dart**
Preserves pending navigation until the mobile routing surface is ready.
**mobile/test/features/channels/channel_detail_page_test.dart**
Updates navigation integration coverage for icon-prefixed channel chips.
**mobile/test/features/channels/deep_link_dispatcher_test.dart**
Covers channel/message dispatch and missing-target behavior.
**mobile/test/features/channels/message_content/link_normalizer_test.dart**
Exercises Markdown-safe normalization across the full Buzz link suite.
**mobile/test/features/channels/message_content_test.dart**
Verifies chip labels, icons, semantics, authored-label opt-out, and
navigation callbacks.
**mobile/test/shared/deeplink/deep_link_test.dart**
Covers strict parsing for channel, message, repository, pull-request,
and issue links.
</details>
## Reproduction steps
1. Run the mobile app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
from inline and autolinked forms.
## Screenshots / demos
**iOS Simulator — channel, message, repository, pull request, and issue
permalink chips**
Real app build (`37b2cb5eb`) running on an iPhone 17 Pro simulator.

---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Slice 6 of #2216. Independent of #3642 — cut from `main`, no shared
files in conflict.
## Why
Five surfaces formatted the same thing five ways, and none of them
matched the writing standard's Today / Yesterday / weekday / date
progression.
| Surface | Before |
|---|---|
| Chat day divider | `Monday, March 31st` — ordinal suffix, which the
standard says to avoid |
| Inbox section header | `Yesterday`, but never `Today`; always printed
the year |
| Inbox list row | A third implementation |
| Inbox thread pane header | `Jul 8, 2026, 2:34 PM` — always absolute,
always with the year, never relative at any distance |
| Channel message header | `9:05 AM` — a bare clock, so a message from
last week has nothing to anchor it once its day divider scrolls away |
There were three separate date implementations doing this, which is the
symptom worth naming: **two different jobs were being solved ad hoc at
each call site.** A header that labels a *group* of items needs a
different label than an individual item's own timestamp.
## What
`shared/lib/datetime.ts` owns both ladders:
```
formatDayGroupLabel formatItemTimestamp
(day divider, section header) (list row, message header)
Today → Today withTime:false withTime:true
Yesterday → Yesterday 2:34 PM 2:34 PM
2–6 days → Monday Yesterday Yesterday at 2:34 PM
this year → June 20 Monday Monday at 2:34 PM
older → June 20, 2025 Jun 20 Jun 20 at 2:34 PM
Jun 20, 2025 Jun 20, 2025 at 2:34 PM
```
## Two deliberate deviations from the standard
Both are documented at the definition, not just here.
**The oldest band keeps the day.** The standard collapses anything over
ten months to month-and-year (`Aug 2022`). A group label has to
*identify* its day — collapsing would give every day in a month the same
divider, so scrolling old history would show a run of identical headers
with no way to tell one day from the next. Only the year is conditional.
There's a test asserting three consecutive 2022 dates produce three
distinct labels.
**Roomy surfaces keep the time of day at every band.** `Yesterday at
9:05 AM`, not `Yesterday`. This is a chat and collaboration workspace
rather than a transactional product — where you read conversation, the
time is content, not chrome. Narrow list rows still drop it (`withTime:
false`) and rely on the existing hover tooltip, which stays the absolute
value. `withTime` is a surface decision, not a preference.
Today needs no date word in either mode: a bare clock already reads as
today, and "Today at 2:34 PM" is longer without saying more.
## Derived rather than captured
`MessageTimestamp` now takes only `createdAt` and derives both of its
labels, instead of receiving a pre-formatted `time` string. A relative
label captured when the message list was formatted would be frozen at
that wording; deriving it means each render recomputes.
This does not make it live — `MessageRow` is memoized, so a row already
on screen when the clock passes midnight keeps saying "Today" until
something re-renders it. The day divider above it has always had the
same property, and both correct themselves on the next message, scroll,
or navigation. Called out in the component doc so the next person
doesn't read "derived" as "reactive".
The memo comparator moved from `message.time` to `message.createdAt`.
Behavior-identical — `time` was a pure function of `createdAt` — but it
now names the prop the row actually reads.
The 36px continuation hover gutter stays clock-only. A relative label
doesn't fit in `w-9`.
## Middot between metadata segments
`managed by you 9:53 AM` ran two unrelated facts together as if they
were one phrase. Now `managed by you · 9:53 AM`.
- `aria-hidden` — punctuation for the eye only. The header already reads
as separate nodes to a screen reader, and `MessageAgentOwner` supplies
its own "Agent managed by" label.
- Grouped with the segment it precedes, so it can't wrap to the start of
a line on its own — as loose siblings in a `flex-wrap` row, an orphaned
divider is exactly what happens.
- No margin; spacing comes from the container gap.
- **No separator after the author name.** "Alice 9:53 AM" already reads
as a name followed by a time. Dividers go between metadata segments
only.
Middot is already the app's separator for this —
`MessageThreadSummaryRow`, the mention list, project rows, 46 files in
total.
Applied to the channel message header, channel system rows, and the
Inbox thread pane. Left-side Inbox activity rows deliberately unchanged.
## Verified
Screenshots taken through `just desktop-screenshot`:
- `#agents` — `nadia 🤖 managed by you · 10:20 AM`, and the `Today`
divider with clock-only rows
- Inbox thread pane — `alice 🤖 owner unavailable · 12:00 PM`
**Gap worth naming:** every mock channel message is same-day, so the
past-day labels (`Yesterday at 9:05 AM`, `Jun 20 at 2:34 PM`) are
covered by unit tests rather than by a rendered screenshot. Happy to add
a spec that seeds an older `created_at` if a reviewer wants to see them.
## Validation
- `pnpm check`, `pnpm typecheck` — clean
- Unit: **3800/3800**, including 17 new tests in
`shared/lib/datetime.test.mjs` and 4 in
`messageTimestampContract.test.mjs`
The datetime tests pin the things that are easy to regress:
Today/Yesterday as *calendar* boundaries rather than 24-hour windows (a
message 15 hours old across midnight is "Yesterday"; one 22 hours old on
the same day is "Today"), the weekday band bounded at both ends so a
future timestamp from clock skew never gets labelled with a past
weekday, no ordinals across all the tricky days
(1/2/3/11/12/13/21/22/23/31), the year omitted within the current year,
and compact labels staying ≤12 chars for a narrow row.
- Smoke E2E: **783 passed, 2 failed, 1 skipped**
Both failures are pre-existing and unrelated, confirmed by re-running
each against a clean tree:
1. `video-attachment.spec.ts:223` — fails deterministically on clean
`main`
2. `community-rail.spec.ts:797` (keyboard drag-and-drop reorder) — flaky
on clean `main`: 2/5 failures there vs 3/5 with this branch, i.e. noise
## Mobile
Mobile had the same divergence, so it moves with desktop rather than
drifting until the next pass.
`mobile/lib/features/channels/date_formatters.dart`:
| Before | After |
|---|---|
| `formatDayHeading` → Today / Yesterday / `Tuesday, March 31, 2026` |
Today / Yesterday / `Tuesday` / `March 31` / `March 31, 2025` |
| `formatThreadSummaryLastReplyTime` → `on May 19th` | `on May 19` |
Same two departures from the standard as desktop, documented at the
definition and cross-referenced to `datetime.ts` so the next person
editing one finds the other. Day comparison also moved to a rounded
start-of-day difference, so a DST transition counts as one calendar day
rather than zero — Dart's `Duration.inDays` truncates.
**Message timestamps stay clock-only on mobile.** Desktop message
headers now read `Yesterday at 9:05 AM`; mobile keeps `9:05 AM` at every
band. That's the compact side of the same surface split the desktop
change makes — a mobile timestamp sits inside a chat bubble on a narrow
screen with the day divider a short scroll away, where a date word costs
width it doesn't earn. Recorded as a decision at `formatMessageTime` so
it doesn't read as an oversight.
Mobile needs no middot work: message headers have no "managed by"
segment, and the mention suggestion list already uses `\u00b7`.
Validation: `dart format` clean, `flutter analyze` no issues, `flutter
test` **911 passed, 1 skipped** — 8 new day-heading tests covering the
weekday band, the year boundary, ordinals across
1/2/3/11/12/13/21/22/23/31, distinct labels for consecutive days in the
oldest band, and calendar-day rather than 24-hour bands.
## Out of scope
- **Search results.** `SearchResultItem.tsx` and `TopbarSearch.tsx`
hand-roll a `5m ago` elapsed format. That's a third *kind* of label —
elapsed rather than relative-calendar — and deciding whether search
should switch is a separate call.
- **`formatThreadSummaryLastReplyTime`** keeps its own "3 hours ago"
elapsed scale on both platforms; only its old-reply fallback lost the
ordinal (`on May 19th` → `on May 19`).
- **Mobile search.** `relativeTime` returns `7/31/2026` past a week,
matching the desktop search format that's also out of scope above. Both
should change together or not at all.
---------
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
- keep transparent channel-list gaps in Flutter's gesture arena
- allow a new drag to interrupt active ballistic scrolling immediately
- add a behavioral fling-and-counter-drag regression test
## Scope audit
- audited mobile list and scroll constructors across `mobile/lib`
- channels is the only app scrollable overriding `hitTestBehavior`
- all other lists retain Flutter's default opaque hit testing and do not
share this defect
## Verification
- regression test fails before the production change: ballistic offset
continues from `271.17` to `345.56`
- focused interruption regression passes with the fix
- profile/community control test passes
- pre-commit: Dart formatting and Flutter analyzer pass
- pre-push: complete mobile suite passes, 1323 tests
- simulator: immediate counter-drag from the transparent gutter
interrupts deceleration
Simulator evidence:
`/Users/wesb/.buzz/.scratch/mobile-scroll-videos/interruption-verified.mp4`
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- restore private-channel invitations for every active member
- keep owner/admin-only enforcement for elevated role grants, active
role changes, and removals
- preserve #4612's unrelated Desktop/mobile failure handling and
hardening
- add relay coverage for the ordinary actor/target role matrix
(`member`, `guest`, `bot`)
## Validation
- pre-push hook passed on `7de700e17642ad7e10155f9537033168d9249268`:
branch skew, Desktop checks/typecheck/tests/Tauri checks, mobile tests,
and Rust tests
- `cargo test -p buzz-test-client --test e2e_relay --no-run`
- `cargo fmt --all -- --check`
- `git diff --check`
- Donut and Mongo independently reviewed the cross-layer authorization
behavior; Donut's role-matrix coverage finding is addressed in this
revision
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Mobile users who jump to Latest now see the newest
message fully above the composer instead of partially hidden behind it.
**Problem:** The channel message list treated the raw viewport bottom as
the latest boundary even though the composer occupies part of that
viewport. Latest jumps and follow-mode corrections could therefore place
the newest message underneath the composer.
**Solution:** Derive the latest alignment from the measured composer
inset and use that same boundary for scrolling, follow detection, and
layout correction.
<img width="498" height="1008" alt="Screen Recording 2026-08-05 at 5 18
19 PM"
src="https://github.com/user-attachments/assets/a7fc1a94-3ffb-4c34-908d-9bf4f3f082b4"
/>
<details>
<summary>File changes</summary>
**mobile/lib/features/channels/channel_detail_page/message_list.dart**
Aligns Latest navigation and follow-mode correction with the visible
bottom edge above the composer, and evaluates boundary state against the
same geometry.
**mobile/test/features/channels/channel_detail_page_test.dart**
Adds a regression assertion that the newest live message clears the
composer and that the Latest control disappears after navigation.
</details>
## Reproduction steps
1. Open a mobile channel with enough messages to scroll away from the
newest message.
2. Tap **Latest**.
3. Confirm the newest message is fully visible immediately above the
composer and the **Latest** control disappears.
4. Resize the composer or keyboard while following latest and confirm
the newest message remains above the composer.
## Tested fix
The newest message remains fully visible above the composer after
jumping to **Latest**.

## Validation
- `flutter analyze` — no issues
- `flutter test` — 1,243 passed
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
## Summary
- replace reaction-only mobile sheets with an anchored long-press
popover
- add haptics, a dimmed frosted spotlight, and a spring-staggered
reaction tray
- preserve the full action sheet whenever other message actions are
available
- keep existing reaction pills outside the spotlight and make long press
reliable across nested content
## Testing
- `flutter analyze`
- `flutter test` (1,248 tests)
- signed iPhone release build installed and launched
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
**Category:** new-feature
**User Impact:** People who lose a desktop identity can securely restore
it from a signed-in Buzz phone without creating a replacement identity.
**Problem:** A fresh or identity-lost desktop could not recover its
existing full Buzz identity from an already-authorized phone.
**Solution:** Add a SAS-confirmed reverse NIP-AB transfer, durable
desktop import, a dedicated mobile recovery entry point, and clearer
desktop recovery dialogs with tested loading, drag-and-drop, and failure
states.
https://github.com/user-attachments/assets/e9215c9c-80d0-462f-9161-0fa184ca2f74
<details>
<summary>File changes</summary>
**crates/buzz-core/src/pairing/session.rs**
Adds the reverse encrypted payload and source-completion state
transitions used for phone-to-desktop recovery.
**desktop/src-tauri/src/commands/identity.rs**
Exposes the existing guarded identity commit path for recovery imports.
**desktop/src-tauri/src/commands/pairing.rs**
Adds recovery-mode pairing, durable nsec import, start serialization,
stale-task protection, and explicit rejection of unsupported recovery
payloads.
**desktop/src-tauri/src/lib.rs**
Registers the recovery pairing command.
**desktop/src/app/App.tsx**
Refreshes the recovered identity before continuing onboarding.
**desktop/src/features/onboarding/machineOnboarding.ts**
Adds recovery transitions to the onboarding state machine.
**desktop/src/features/onboarding/ui/BackupPasswordTimeline.tsx**
Adds the visual backup-to-password-to-unlock progression.
**desktop/src/features/onboarding/ui/IdentityRecoveryPairing.tsx**
Implements QR generation, copy fallback, SAS confirmation, cancellation,
expiry, and completion UI.
**desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx**
Connects private-key, phone, and backup recovery paths to the onboarding
flow.
**desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx**
Polishes recovery dialogs, backup drag-and-drop, loading stability, and
security copy.
**desktop/src/shared/api/tauri.ts**
Keeps the existing pairing API surface focused on standard
desktop-to-mobile pairing.
**desktop/src/shared/api/tauriPairing.ts**
Adds the recovery pairing invoke without growing the ratcheted shared
API file.
**desktop/src/testing/e2eBridge.ts**
Mocks recovery pairing commands and lifecycle events for browser tests.
**desktop/tests/e2e/identity-lost.spec.ts**
Covers lost-identity entry, QR/copy recovery, SAS, cancellation, expiry,
success, errors, backup import, drag-and-drop, and screenshots.
**desktop/tests/e2e/onboarding.spec.ts**
Verifies recovered identities continue through harness setup without
replacement-key side effects.
**mobile/lib/features/pairing/pairing_page.dart**
Adds recovery-only scanning and explicit identity-handoff warnings.
**mobile/lib/features/pairing/pairing_provider.dart**
Recognizes recovery codes, returns the signed-in nsec after mutual SAS
approval, and waits for desktop completion.
**mobile/lib/features/settings/settings_page.dart**
Accepts the recovery route builder at the app composition boundary to
preserve feature isolation.
**mobile/lib/features/settings/settings_page/connection_section.dart**
Adds the signed-in “Send identity to desktop” settings action.
**mobile/test/features/pairing/pairing_page_test.dart**
Covers recovery-only validation and handoff messaging.
**mobile/test/features/pairing/pairing_provider_test.dart**
Covers reverse payload encryption, confirmation ordering, success,
failure, timeout, and cleanup.
</details>
## Reproduction steps
1. Launch Buzz Desktop with identity-lost state and choose **Recover
from your phone**.
2. Confirm the QR and persistent **Copy pairing code** fallback appear
without layout shift.
3. On a signed-in phone, open **Settings → Send identity to desktop**,
scan or paste the recovery code, and compare the six-digit SAS on both
devices.
4. Confirm on both sides and verify Desktop restores the identity and
continues to harness setup.
5. Repeat from identity-lost state with **Recover from a backup file**;
verify picker and drag-and-drop both advance to password entry and
restore the encrypted backup.
6. Exercise cancellation, mismatched/unsupported codes, expired
sessions, and an invalid backup; verify each returns actionable,
non-stuck UI.
## Screenshots
### Desktop phone recovery — complete flow
| Recovery entry | Pairing QR | Code match | Receiving identity |
|---|---|---|---|
| 
| 
| 
| 
|
### iOS Simulator — complete handoff flow
| Settings entry | Recovery scanner | Manual recovery code | Code
confirmation |
|---|---|---|---|
| 
| 
| 
| 
|
### Encrypted backup recovery — adjusted file flow
| File picker | Drag-and-drop target | Password step |
|---|---|---|
| 
| 
| 
|
## Verification
- `cargo test -p buzz-core pairing` — 71 passed
- `just mobile-test` — 1,169 passed
- `pnpm build:e2e && pnpm exec playwright test identity-lost.spec.ts
--project=smoke` — 15 passed
- Full pre-push gates — desktop checks, desktop unit tests, Rust tests,
Tauri checks, and mobile tests passed
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
## Summary
- Keep mobile thread reply badges current by merging relay recounts with
replies observed locally.
- Retain replies in the local channel store while continuing to filter
them from the main timeline.
- Match the thread summary behavior already used on desktop, including
the reply count, latest reply time, and participant avatars.
## Why
On mobile, the "N replies" badge under a channel message can stall at a
stale count or remain missing after a reply arrives. This makes the
badge unreliable and can cause people to miss replies.
The badge has two inputs: best-effort recounts from the relay and
replies the client sees arrive. Mobile previously let any positive relay
recount override the local view, while also discarding replies from its
local message store. A delayed or lost recount, or a reply received
after the recount, could therefore leave the badge behind.
This change combines both inputs by using the higher reply count, the
later last-reply time, and a merged participant list. Relay timestamps
have one-second precision, so equal timestamps do not prove that a
recount included a locally observed reply. Comparing counts preserves
that reply instead of trusting recency alone. Desktop already uses this
merge behavior.
## Validation
At commit `4e3356636f5ad62e8f07910af305c532186c6c08` with a clean
worktree:
- `flutter test` for mobile: 1105 passed, 1 skipped
- `flutter analyze` for mobile: no issues found
- Reverting the merge so a positive relay recount shadows local replies
fails 4 of the new tests, including the same-second and
reply-after-recount cases. Restoring the store-level reply drop fails
both new provider tests.
Added tests:
-
[`timeline_message_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/features/channels/timeline_message_test.dart),
covering relay-only recounts, a reply newer than the recount, a reply in
the same second as the recount, a lost recount, a zero recount, nested
replies at the root and at the reply they answer, a deleted reply, and
participant merging and capping.
-
[`channel_messages_provider_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/features/channels/channel_messages_provider_test.dart),
covering a live reply reaching the store while staying out of the main
timeline, and a reply newer than the relay recount raising the badge.
---------
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: npub12uu53ml9upy7ww9apmtv6vm0u8xlcldx7znsjvwgsr7uvy5g0kssw943ca <573948efe5e049e738bd0ed6cd336fe1cdfc7da6f0a70931c880fdc612887da1@buzz.block.builderlab.xyz>