Commit Graph

441 Commits

Author SHA1 Message Date
Tom Brow
355ebca27e fix(mobile): clip preset agent avatar previews to the squircle
Catalog avatars in the picker and Create agent previews rendered unclipped,
so the default create preview did not match the created agent. Let
PresetAgentAvatar own the agent clip and assert the catalog state in the
picker journey.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:55:41 -07:00
Tom Brow
00b1ec6cc1 fix(mobile): tighten avatar shape guard and iOS agent fallback
The iOS Add members fallback tile drew agents as a 30% rounded rect while their loaded avatar was a squircle. It now draws the same squircle. The geometry test now rejects the old rounded rect, and the source guard also flags the old 30%/60% agent radius idiom.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:42:51 -07:00
Tom Brow
f5188ee3b9 Merge remote-tracking branch 'origin/main' into fix-agent-avatar-shape
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:32:41 -07:00
klopez4212
a12ad1cdd2 feat(mobile): refine agent profiles and model selection (#8773)
## Summary
- Simplify agent profiles with Message/Bestie actions, relay-authored
published instructions (unavailable without a config channel),
management details, public-key copying, and a separate final Delete
section.
- Sort the Agents directory by name and mark the selected Bestie with a
muted filled star.
- Place Model and Thinking together below Name. Use native iOS Liquid
Glass menus and preserve selection callbacks as controls become enabled.

Related: #8685.

## Validation
Full mobile checks passed. Regression coverage includes relay-author
filtering, unavailable instructions, metadata retry, resizing native
pickers, accurate deletion state, selection after loading, and Bestie
switching. Real iOS simulator taps verified both pickers; the user
confirmed the earlier iPhone build works. Review fixes were checked with
widget journeys and the native UIKit resize/selection probe.

<details>
<summary>Previews</summary>

Synthetic fixture renders:

| Profile | Create agent | Bestie card |
| --- | --- | --- |
| ![Profile with separate Delete
section](https://github.com/user-attachments/assets/4b6e7cf5-b152-43a3-99d9-7c1ee65d0c70)
| ![Model and Thinking
settings](https://github.com/user-attachments/assets/c81dc138-d5d3-43bb-873b-a64171155a7d)
| ![Muted Bestie
star](https://github.com/user-attachments/assets/5ce95e3b-2403-41c5-b6d6-52b8b60cc7d6)
|

Native iOS simulator interaction with a synthetic catalog:

![Both picker values updated after native menu
selections](https://github.com/user-attachments/assets/a3ee6d4d-91e0-4df6-b4cf-b66d5c1a2e87)

</details>

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 11:28:14 -06:00
Tom Brow
51559c75c7 refactor(mobile): route avatar shapes through one bottleneck
avatar_shape.dart now owns the agent squircle and the person circle. avatarShape(isAgent:), avatarPath() and AvatarClip are the only way to get either, and the squircle border is private. A source guard test fails when code keys an avatar shape on agent identity inline, or uses a person shape on an agent-only surface.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:25:54 -07:00
Tom Brow
6703762481 fix(mobile): clip agent card portraits to the agent squircle
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:18:39 -07:00
Tom Brow
c22daaabc9 fix(mobile): use the desktop agent squircle for agent avatars
The Create agent form previewed emoji and image avatars as circles, while
the created agent showed a 30% rounded rectangle. Neither matched desktop's
agent squircle.

Add AgentAvatarBorder, which traces desktop's avatar-squircle.svg mask, and
use it for every agent avatar surface: the create and picker previews,
AvatarImage, the native bar PNG, the profile sheet, SmallAvatar and the
Huddle halos and placeholders. Also add 8pt above Choose avatar so it no
longer crowds the preview.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-31 10:12:02 -07:00
klopez4212
a2540fbe3d Align Android profile setup with iOS steps (#8871)
Android profile setup now matches iOS: choose a display name, then an
optional avatar using the existing dotted-circle placeholder and Image /
Emoji / Animated editor. Back preserves drafts; Skip saves the name
without an avatar, and failed saves retain retry/exit options.

Validated on Pixel 10 and approved by Kenny; analysis clean, 4,064
mobile tests passed (8 skipped). Device preview uses no-write callbacks;
preview-only files are excluded.

| Display name | Avatar |
| --- | --- |
| ![Display name
step](https://github.com/user-attachments/assets/516361ea-2c02-4ed5-8847-f716ed5c0272)
| ![Avatar
step](https://github.com/user-attachments/assets/2a1607e0-59dd-4ff6-9ffa-a1dd13faad3e)
|

buzz-review-completed

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 17:08:39 +00:00
Tom Brow
20742a236d fix(mobile): only render known channels as #channel chips (#8862)
Follows [#8860](https://github.com/block/buzz/pull/8860) (merged).

On mobile, any `#token` rendered as a channel chip, so GitHub-style
references like `#8700` turned into inert chips for channels that don't
exist. Desktop already avoids this by only linking names in the known
channel list.

This makes mobile's message body and one-line preview do the same. With
no known names (channels still loading, or none visible), nothing
becomes a chip. An open channel the reader hasn't joined stays plain
text until the directory has loaded; before this change it was an inert,
untappable chip. messages rebuild with chips once channel names arrive.
Unlike desktop, there's no "any `#token` while empty" fallback, because
an empty map can't distinguish loading from an account with no visible
channels. The known names come from the existing
`channelIdsByNameProvider` (the reader's channels, plus open directory
channels once Browse channels or invite recovery has loaded the
directory), so there is no new fetch, subscription or sync change. When
the list is non-empty, the regex has one alternative fewer than before.

### Validation

Added tests:
- Preview: `#8700`, `#8774's`, a known-name prefix (`#gen` vs `general`)
and `#eng-ios` with only `eng` known stay text, while `#general` and a
longer known `#eng-ios` are chips. With no channel names, nothing chips.
- Message body: `Took over #8700 from #general` renders one chip
(`general`) and keeps `#8700` as text.

- Message body: with no channel names, `#unknown` stays text.

The new assertions fail with the previous always-generic pattern. The
full mobile test suite passed in pre-push.

Simulator render on an iPhone 18 Pro (iOS 27): real message-body and
preview widgets with channels `general` and `eng` loaded, rendered with
both stacked PRs applied.

| Before (`main`) | After
([#8860](https://github.com/block/buzz/pull/8860) +
[#8862](https://github.com/block/buzz/pull/8862)) |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--01-before-main.png"
width="300"> | <img
src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--02-after-both-prs.png"
width="300"> |

Human app test: not applicable under the mobile-client testing
exception; agent review and simulator validation above.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
2026-10-31 09:47:19 -07:00
Tom Brow
e3b1ef0d84 docs(agents): mark desktop/ as legacy, not a reference for mobile (#8868)
Agents working on the mobile app have been looking at `desktop/` for
implementation and design cues. `desktop/` will soon be superseded by
[block/buzz-app](https://github.com/block/buzz-app), while mobile is
current, so it should never serve as that reference.

- Root `AGENTS.md`: labels `desktop/` as legacy in the repo layout, adds
a callout at the top of the Desktop App section, adds the same guidance
to the Mobile App section, and drops "matches desktop" from the mobile
theme note. Mobile kind constants now sync with
`crates/buzz-core/src/kind.rs` (also in the `nostr_models.dart` comment)
instead of desktop.
- `VISION_MOBILE.md`: replaces the line that allowed the desktop
implementation to inform shared mobile rules, so the vision and agent
guidance agree. Written intent still takes priority.

### Validation

Documentation-only. Reviewed the diff and links. Codex review's one
finding (the vision conflict) is addressed.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:46:42 -07:00
Tom Brow
b80217de32 fix(mobile): end #channel tokens at apostrophes, quotes and dashes (#8860)
Mobile renders `#channel` tokens as chips, but a token followed by an
apostrophe, quote, ellipsis or dash stayed plain text: `#general's`,
`"#general"`, `#general…` and `#general—` all lost their chip because
the token only ended at whitespace or `, ; . ! ? : ) ] }`.

This ends channel tokens at those characters too (`' " ‘ ’ “ ” » … –
—`). The message body and the one-line preview each had their own copy
of the channel regex; they now share one `channelTokenPattern`, so they
can't drift apart again.

`@mention` boundaries are unchanged on purpose: on mobile and desktop
the same boundary also decides which mentions are bound and tagged when
sending, so changing it is a separate, cross-surface decision.

### Validation

Added tests:
- Preview table covering each new trailing character (and matching
opening quotes).
- Message body: `#general's` keeps the chip and renders the `'s` suffix.

Both fail with the previous boundary. The full mobile test suite passed
in pre-push.

Simulator render on an iPhone 18 Pro (iOS 27): real message-body and
preview widgets with channels `general` and `eng` loaded, rendered with
both stacked PRs applied.

| Before (`main`) | After
([#8860](https://github.com/block/buzz/pull/8860) +
[#8862](https://github.com/block/buzz/pull/8862)) |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--01-before-main.png"
width="300"> | <img
src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--02-after-both-prs.png"
width="300"> |

Human app test: not applicable under the mobile-client testing
exception; agent review and simulator validation above.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
2026-10-31 09:30:23 -07:00
klopez4212
d14f33a170 fix(mobile): keep toasts above native navigation and sheets (#8866)
## Summary
Keep mobile toasts above navigation and modal sheets with an app-level
overlay and a tiny transparent iOS composition boundary. Preserve Retry,
automatic dismissal, and taps on underlying controls.

### Related issue
No duplicate found in the toast issue/PR search.

### Testing
Full mobile tests and analysis pass; iOS simulator and device builds
succeed. Verified layering, Retry, dismissal, and underlying taps in a
native simulator preview; human confirmed on iPhone with their existing
profile/community. Broader repository checks are in progress.

Before / after (isolated native simulator preview using production
widgets):

| Before | After |
| --- | --- |
| ![Toast hidden by native
navigation](https://github.com/user-attachments/assets/981bf68f-dcea-4584-a071-94ecdad8f1a7)
| ![Toast above native
navigation](https://github.com/user-attachments/assets/d75a28c6-cfce-4eed-87a3-548d03c72559)
|

<!-- buzz-review-completed -->

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 16:17:47 +00:00
klopez4212
120cbae8e5 fix(mobile): restore inline typing groups and keep working thread links (#8865)
Restores mobile’s shared inline typing bubble and animated avatar
cluster after #8765 replaced them with composer activity. Humans and
agents share the group, including the existing overflow badge and
reduced-motion behavior; fresh replies regain the bubble-to-message
handoff.

Keeps the working-agent reply link from #8754, including its current DM
support: a root message can show working agents and open its thread
before the first reply. Thread timelines now use the same inline typing
group after their last message, so reverting the redesign does not bring
back the old full-width typing bar. Archived and non-member
conversations retain their existing no-typing-indicator behavior.

This intentionally follows the requested mobile presentation rather than
buzz-app’s composer activity layout. VISION_MOBILE.md allows
platform-specific presentation. The composer’s “Working now” list is
removed with the redesign; the root-message thread links remain.

Validation:
- At `5df48ad4b`: `just mobile-install`, `just mobile-check` and `just
mobile-test` passed (4,036 Flutter tests passed, 30 skipped;
unconfigured-push checks and 27 crypto tests also passed). Repository
file-size checks passed.
- Required pre-push hooks passed, including a second full mobile
check/test run and the repository file-size gate. Hosted CI is pending.
- Extended the existing thread journey through opening an empty working
thread, mixed human/agent typing after a reply, clearing typing, and
Back. Existing coverage exercises avatar grouping, overflow, reduced
motion, reply handoff, and followed/detached thread scrolling.
- Agent self-review completed; no additional blockers found after
correcting inventory-only agent avatar shapes and waiting for timeline
layout before following new thread typing.
- Simulator component preview uses synthetic identities in a local,
uncommitted entrypoint. It is not evidence of a live relay exchange.

Human verification pending (draft):
1. In a channel or DM, have an agent work on a threaded reply. Confirm
the root’s working-agent reply link opens that thread before a reply
arrives.
2. Have multiple people/agents type. Confirm one inline avatar group and
dot bubble, including inside the opened thread, with no wide composer
bar.
3. Receive a fresh reply in the main conversation/Solo and check the
typing-to-message handoff. In a long thread, check typing while
following the latest message and while scrolled up, then navigate Back.

Component preview on iPhone 18 Pro (iOS 27.0 simulator). These are
production widgets in an uncommitted preview shell with synthetic
identities; surrounding screen chrome is not a full-app screenshot.

![Inline group together with the retained working thread
link](https://github.com/user-attachments/assets/28f57a27-9319-4d27-a965-24d0dca3b620)

![Five typers use the circular overflow
badge](https://github.com/user-attachments/assets/5912862e-ac5a-48f7-a79c-39c2e2231d24)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 09:09:30 -07:00
klopez4212
0a46f329c0 Fade welcome bees with the QR scanner transition (#8859)
Fixes the welcome bees remaining visible when the QR scanner opens after
#8853. Bees now share the foreground’s fade and scale progress, pause
while hidden, and return on dismissal. Reduce Motion skips scaling.

Validated scanner open/close in the iPhone simulator,
normal/reduced-motion widget journeys, analysis, and the full mobile
suite. Simulator camera hardware is unavailable; QR decoding was not
exercised. Independent review found no blockers.


https://github.com/user-attachments/assets/1ce046b2-63e3-4441-90e9-9d9ec048bdfa

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 16:09:27 +00:00
klopez4212
2ca77747d1 Fix mobile Add Community loading and membership restoration (#8796)
## Summary
Open Add Community immediately from the post-login picker and return
directly on Back, without membership restoration or repeated discovery
during navigation. Completing Add restores the saved selection while
keeping the newly added community active; failures retain progress and
offer Retry.

Follow-up to #8729. Mobile app update only; no relay deployment
required.

## Testing
`just mobile-install mobile-check mobile-test` passed (4,025 standard
tests), plus 4,049 Builderlab-enabled tests on the merged revision.
Production-path journeys cover hosted and self-hosted completion,
partial restoration and Retry, persisted memberships, and the active
destination. Removing the completion hook makes the new journeys fail.
Independent agent review found no blockers. Native walkthrough and human
verification remain pending.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 16:01:15 +00:00
klopez4212
cfcaef4e9f Update mobile toast animations (#8855)
Updates mobile toast animations and styling.



| Sign in | Retry | Copied |
| --- | --- | --- |
| ![Reconnect to
Builderlab](https://github.com/user-attachments/assets/5298b507-df5b-49da-a021-03f337e997fa)
| ![Save error with
Retry](https://github.com/user-attachments/assets/492c8929-4f91-4342-943b-a72b96547992)
| ![Copied
confirmation](https://github.com/user-attachments/assets/864fd629-7414-415e-81ef-5f7d3d5333a4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:50:00 -07:00
klopez4212
5f13984a19 Bring buzz.xyz branding and animated bees to mobile welcome (#8853)
Replaces the mobile welcome gradient with buzz.xyz’s green and 12
animated bees. The swarm keeps flying through rotation, respects Reduce
Motion, and leaves controls tappable; the primary button label is now
white. The ghost button has an opaque chartreuse fill so bees cannot
show through its text.

Validated in the iPhone simulator, with animation/rotation journeys,
analysis, and the full mobile suite passing. Agent review found no
blockers.


https://github.com/user-attachments/assets/9aa1472c-c8e5-4e8b-a016-a8cba36805e1

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:47:25 -07:00
klopez4212
4de1b6aeac Simplify mobile Solo and DM empty states (#8851)
Replace the embedded Solo onboarding block with “No chats yet.” Give
Solo and DM empty states matching left alignment and spacing below their
headings, with softer text at 65% opacity.

Validated with mobile formatting, analysis, the full mobile test suite,
and a Flutter widget preview measuring equal 16-point gaps below both
headings.

### Preview

Actual Flutter widgets with sample data; not a device capture.

![Solo and DM empty states with matching
spacing](https://github.com/user-attachments/assets/f044b7e5-b1ed-4d83-912d-a0fa119055d4)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:46:57 -07:00
klopez4212
9c3ba83adb Simplify signed-in mobile community onboarding (#8852)
Replace the signed-in mobile welcome screen with **Add a community**: QR
scan first, then grey secondary actions for community links, finding
saved communities, and creation. Hide **Find my communities** when none
are known; refresh on entry/resume and every 30 seconds while active,
retaining results on transient failure.

Community switcher → Add keeps its two-action flow and uses the same
grey secondary link button.

Validation: Flutter analysis, full mobile suite (4,014 tests),
Builderlab-enabled full suite (4,036 tests), widget interactions, and
independent agent review. Draft pending native sign-in and human
testing. Snapshots render the production Flutter widgets; native
navigation/status bars are omitted.

| Communities available | No communities | Switcher → Add |
|---|---|---|
| ![Onboarding with
communities](https://github.com/user-attachments/assets/444b05c5-711f-41df-9c50-293ad9e13767)
| ![Onboarding without
communities](https://github.com/user-attachments/assets/db78a490-bd8e-4d78-97b4-a391b52290d8)
| ![Switcher Add with grey secondary
button](https://github.com/user-attachments/assets/de9e42d2-dd53-462b-98c7-da87223e5dd4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:46:44 -07:00
Logan Johnson
547de61aa5 fix(mobile): show channel posts before a burst of join notices (#8798)
## Summary

When many people join a channel at once, the mobile app could open that
channel and show only a "… joined" line, with none of the channel's
posts. You had to scroll up before any messages appeared. With this
change, the posts always show first, and the join lines appear among
them in the right places.

The cause: the app loads the newest 50 timeline rows when a channel
opens, and join notices counted toward those 50. A burst of 50 or more
joins filled the whole first page. The app combines a run of joins into
one line only when it draws the screen, so the user saw a single "Guest
10 joined … and 48 others" line. The app loads older rows only after the
user scrolls.

This is the mobile port of the desktop app's fix,
https://github.com/block/buzz-app/pull/1983, which has already merged.
It needs no relay change.

- History pages no longer include join notices, so the first page is all
posts.
- The app reads join notices with a separate read, newest first, 500 per
read. It reads down to the oldest post loaded so far, and continues
after each older page loads. Like desktop, it stops after 2,000 notices.
- Each notice read also reads the reactions on those notices, and any
removals of those reactions. A page brings these back for its own rows,
but notices are no longer on the pages, so they need their own read.
These reads name the channel in `#h`, so a system channel, or an
unlisted channel opened by a non-member, still returns them (NIP-CL).
Deletions are also read without `#h`, because a deletion can be stored
with no channel. Each read names at most 500 target ids, so it stays
under the relay's 1 MiB request limit. A notice's reactions show even
when they sort below the oldest loaded post.
- Posts show as soon as their page loads. The notices follow and are
merged into the timeline, so they do not replace rows that arrived live
in the meantime.
- A notice that is older than the oldest loaded post waits until its
page loads. This prevents a join from showing at the top of the timeline
with a gap of missing posts below it.
- If a notice read fails, the posts still show. The next page load tries
the notices again; if a page loaded while the read was running, the read
runs once more right away.
- The same applies to every system notice (joins, topic changes, channel
created or archived), as they share one event kind.

### Related issue

AIDA-2328. Desktop fix (merged):
https://github.com/block/buzz-app/pull/1983.

### Testing

Before (left, older build) and after (right, this branch) on the Android
emulator, against a local relay. #general and #4things each have 4 posts
followed by 60 joins:


https://github.com/user-attachments/assets/53b80942-d8c3-4604-8b95-cc5ca7ab6946

![Before and after: the old build shows only a joins line; the fix shows
the 4 posts above the joins
line](https://github.com/user-attachments/assets/58e121b9-855c-4844-a73a-c71b54252db2)

- New `channel_messages_provider_test.dart` cases run against the real
channel provider, with only relay responses faked:
- A burst of 500+ joins does not hold posts back. If the first notice
read fails, the posts still show. A later page read retries the notices
and continues the descent.
- Notices in the same second as the oldest loaded post follow the post's
position within that second.
  - The descent stops at 2,000 notices.
- A notice keeps its reactions, and a removed reaction stays removed.
Reactions are read past the relay's 1,000-event page. The fake relay
applies the NIP-CL rule, so a read without `#h` finds no channel
reactions.
  - A page that loads while a notice read fails still gets its notices.
- `just mobile-check` and the full `just mobile-test` pass at
`7a38763af`.

Not yet tested on a physical device, or on iOS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:34:41 -04:00
Matt Toohey
6e6f4e8a0d test(mobile): check the title fade's animation, wait 10 s for the alert (#8741)
## Summary

Two RunnerTests fixes from the 23–30 Oct flake audit, both in
`mobile/ios/RunnerTests/RunnerTests.swift`. Both are test-only. This is
one of six PRs split from the reviewed `flakey-tests` branch (siblings
below).

### `testLargeTitleTextFadesWithScrollAndCompactTitleCanReverse` (3
runs)

- **Runs:** 40748163526 (job 123028155856), 40826294064 (#8679, job
123290415194, passed on attempt 2) and 40921441156 (#8694, job
123617149320), all in Mobile (iOS), attempt 1. None of the branches
touches the navigation bar. Signature:
`XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99)` read
0.9999499, 1.0 and 1.0.
- **Cause:** collapsing adds a 0.18 s `CABasicAnimation` on the compact
title mask's opacity (`IosNavigationBar.swift` `applyScroll`). The test
slept 60 ms and expected the presentation opacity to be mid-fade. The
presentation layer follows wall-clock media time. When the Flutter app
in the test host keeps the main thread busy, `Task.sleep` resumes after
the fade has finished.
- **Change:** drop the 60 ms sleep and the mid-fade samples. Right after
`scroll(to: boundary)`, which runs the handler synchronously, check that
the mask's model opacity is 1. Also check that it carries a
`"titleFade"` opacity `CABasicAnimation` from 0 to 1, lasting 0.1–0.5 s.
Nothing turns the run loop before the check, so the animation is still
attached however long the main thread stalls. After the scroll back, a
new `XCTAssertNil` checks that reversing removes the fade. The later 240
ms waits and their end-state checks are unchanged.
- **Mutations that should fail it:**
- Delete `compactTitleMask.add(fade, forKey: "titleFade")`: the
`XCTUnwrap` throws.
- Delete `removeAllAnimations()`: the new `XCTAssertNil` fails. The old
test missed this.
  - Set `fade.duration = 0.001`: the band's lower bound fails.

### `testNativeRemovalConfirmationUsesDestructiveAlertAndCancelsOnce` (1
run)

- **Run:** 40950548609 (#8433, Dart only, job 123717768224), attempt 1.
Signature: `fulfillment(of: [completed], timeout: 3)` expired.
- **Cause:** the busy test-host main thread that #8678 handled for other
classes. The cancel reply comes from UIKit's dismissal completion, after
the dismiss animation runs on that thread.
- **Change:** raise the wait to 10 s, matching #8678's
`callbackTimeout`. The assertion is unchanged. Dropping `resolve` from
the dismissal completion still fails it, and so does resolving before
`alert.dismiss`.

### Related issue

None found. **Conflicts with #8568** ("Adapt iPhone Duo layouts, rail
navigation, and native sheets"). #8568 rewrites the fade test into a
loop but keeps the 60 ms sleep and the mid-fade samples. It already
conflicts with `main`, and this PR adds `RunnerTests.swift` to its
conflicts. #8665 only adds a voice-note test.

### Testing

Not compiled against the app or run locally, because Xcode and
RunnerTests bog this machine down. `swiftc -parse` accepts the file. The
new assertions, copied into a stub `XCTestCase`, typecheck with `swiftc
-typecheck` against the iOS simulator SDK and XCTest (Swift 5 mode). The
full file needs Flutter and the Buzz module, so it wasn't typechecked as
a whole. **The Mobile (iOS) lane is the check.**

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:37:08 +11:00
Matt Toohey
d14e95f90e test: stop desktop, buzz-agent and mobile tests leaking temp files (#8748)
## Summary

Six test files wrote into the shared temp dir on every normal run and
never removed what they wrote.

**Desktop and buzz-agent (first commit)**

- `desktop/src/protectedFeatures/tauriCommand.test.mjs` wrote its fake
Tauri CLI and three result files straight into `/tmp` under pid-suffixed
names. They now live under one `mkdtempSync` root that an `after()` hook
removes.
- `crates/buzz-agent/tests/permission_boundary.rs` wrote a
`buzz_perm_calllog_*` file for every test whose tool call reached
`fake_mcp`, five per run. `call_log_path` now returns a `TempDir` guard
with the log path inside it, so the log is deleted when the test ends,
including when an assertion panics.
- `cancel_kills_inflight_tool_via_mcp_notification` in
`crates/buzz-agent/tests/regressions.rs` spawns `buzz-dev-mcp`, which
the agent stops with SIGKILL, so the server's `buzz-dev-mcp-*` and
`buzz-dev-mcp-session-*` TempDirs were never deleted. The test now
passes the server a `TMPDIR` it owns and removes.

**Mobile (second commit)**

The three mobile test files left 12 `buzz-video-*` files and 4
`transcode_*` dirs per run.

- `video_viewer_test.dart` and `video_controls_test.dart` pointed their
fake `path_provider` at `Directory.systemTemp`. The viewer downloads
each video there as `buzz-video-*`, and its async delete on close never
completes under FakeAsync. Each test now gets its own temp dir for the
fake, and an `addTearDown` removes it with `deleteSync`. In
`video_controls_test.dart` that teardown is registered first, so it runs
after the viewer is disposed.
- Four `media_upload_test.dart` tests faked `transcodeVideoToMp4` by
writing `out.mp4` into a new `systemTemp` `transcode_*` dir. The upload
service deletes `out.mp4` but not its dir. The fakes now create that dir
under the test's own temp root, which the test already deletes in
`finally`.

Production code is unchanged. **`buzz-dev-mcp`'s SIGKILL shutdown is
left for a follow-up:** the agent still stops the server with SIGKILL,
so outside tests the server's temp dirs are still left behind. Sending
SIGTERM first is the real fix.

### Related issue

None found. This continues the test-cleanup work in #8727.

### Testing

Each run below was bracketed by listings of `/tmp` and the per-user temp
dir (`/var/folders/.../T`), so new leftovers would show up.

- `tauriCommand.test.mjs`: 3/3 pass, with no new `buzz-tauri-*` or
`buzz-fake-tauri-*` entries.
- `cargo test -p buzz-agent --test permission_boundary`: 9/9 pass, with
no new `buzz_perm_calllog_*` files. The tests still check that the call
log is written, so the log really is going into the guarded dir.
- `cancel_kills_inflight_tool_via_mcp_notification` passes with
`buzz-dev-mcp` built, so it ran rather than skipped, and left no new
`buzz-dev-mcp-*` dirs.
- Mobile: `video_viewer_test` (7 tests), `video_controls_test` (20) and
`media_upload_test` (38) pass, each run on its own. With `TMPDIR`
pointed at a scratch dir, the old versions left 12 `buzz-video-*` files
and 4 `transcode_*` dirs, and the new versions left it empty.
- After cherry-picking onto current main, these pass:
  - `cargo clippy -p buzz-agent --all-targets -- -D warnings`
  - biome and `node --check` on `tauriCommand.test.mjs`
  - `dart format` and `flutter analyze` on the three mobile files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:35:26 +11:00
Logan Johnson
aa5f3e7bde feat(mobile): show conversation activity by the composer, as buzz-app does (#8765)
## Summary

Stacked on #8754. This brings the rest of buzz-app's conversation
activity to mobile.

buzz-app shows who is active in a conversation in one place, just above
the composer (`src/features/messages/TypingIndicator.tsx`). Mobile had
its own design: a typing bubble at the end of the timeline that turned
into the agent's reply (the "handoff"). This change replaces that design
with buzz-app's indicator in channels, DMs, Me and threads.

What the indicator shows, as in buzz-app:

- **On the right: your agents working here.** Up to four agent avatars,
then "+N". "Your agents" are your agent inventory (kind 30177), as in
#8754. Tapping the avatars opens the "Working now" list.
- **On the left: everyone else typing here.** Typing dots and buzz-app's
text: "Alice is typing", "Alice, Bob are typing", "Alice, Bob and 1
others are typing".
- **"Working now" list.** Each row shows the agent, "Working" and, in a
channel view, where it works: "Channel conversation", or "Thread ·
<first 60 characters of the root>", with "+N more" after two places.
- If the agent works in exactly one thread, the row opens that thread. A
thread whose root is not loaded opens by its ID, as buzz-app's link
does. A button at the end of the row opens the agent's activity.
- Otherwise (in a thread view, in Me, in the channel only, or in several
threads), the row opens the agent's activity. As in buzz-app, work in
the channel does not count as a thread: an agent in the channel and one
thread opens that thread.
  - "Activity" is mobile's existing `AgentActivitySheet`.
  - The list closes when the last agent stops.
- **Scope** (`current-activity.ts`: "Channel views include threads;
thread views do not"):

  | View | Your agents | Everyone else |
  |---|---|---|
  | Channel or DM | Here and in any thread | Here only |
  | Thread | This thread only | This thread only |
  | Me | Anywhere | Anywhere |

- **Me** works like buzz-app's Solo chat: with one agent working,
tapping the avatars opens its activity directly.
- **Spoken status:** buzz-app's live status, for example "Jerry, Morty,
Summer are working. Alice is typing". The button says "Activity: Morty
working" or "Activity: 3 agents working".

Changes you will notice compared with #8754 and main:

- The typing bubble at the end of the timeline and the bubble-to-reply
handoff are removed. #8754's channel typing row goes with them. The root
message's reply row from #8754 stays.
- In DMs, other people's agents typing in a thread no longer show at the
DM level. Only your agents do, as in buzz-app. This reverses part of
#8731.
- With no composer, there is no indicator. This applies when you are not
a member or the channel is archived, as in buzz-app (`ChannelsPage`, and
`ThreadPanel`'s `composerRoot`). Before, mobile showed typing in those
threads.

Less than buzz-app, not in this PR:

- Status is always "Working". buzz-app also uses lifecycle evidence
(local agent state, kind 24200) and tool labels. Mobile uses typing
only, as in #8754.
- No hover preview and no keyboard path (Down arrow) into the list.
- The indicator sits in the composer dock, so it moves the timeline up a
little. In buzz-app it floats over the history.

Code:

- `conversation_activity_state.dart` holds the scope rules, read from
typing and the inventory.
- `conversation_activity.dart` is the indicator.
`working_now_sheet.dart` is the list.
- `channel_detail_page/composer_activity.dart` puts it in the channel
composer dock and opens threads. The thread page puts it above its
composer.
- Removed: `inline_agent_typing.dart`, `agent_reply_handoff.dart`,
`agent_typing_bubble.dart`, `typing_avatar_cluster.dart`,
`typing_motion_preview.dart`, `channel_typing_indicator.dart`, and their
tests.

### Related issue

N/A. Reported in Buzz #buzz-bugs (on iOS you cannot tell whether an
agent is down or typing). Searched issues and PRs for duplicates: none
found.

### Testing

Android emulator, local relay, debug build of `8362d46af`.

**Channel and thread.** Demo User mentions Morty. Morty, Summer and
Jerry (in Demo User's agent inventory) type in the mention's thread.
Rick (not in the inventory) types in the thread and in the channel
itself. In the video:

1. The channel shows three agent avatars on the right and "Rick is
typing" on the left. Rick's thread typing does not show.
2. Tapping the avatars opens "Working now". Each row reads "Working" and
"Thread · …". Tapping Morty's row opens the thread.
3. In the thread, the same three agents show, with "Rick is typing" (his
thread typing). Rows there open activity: tapping Morty's opens his
activity sheet.
4. Back in the channel, Summer, Jerry and Rick stop. Only Morty's avatar
is left. Then Morty replies and the indicator goes away. No typing
bubble shows at any point.


https://github.com/user-attachments/assets/b300e16e-8bec-4489-9ee4-a68c5c2dc30d

**DM.** A DM with Morty (in the inventory) and Rick (not). Demo User
posts. Both type in its thread, and only Morty's avatar shows. Then Rick
also types in the DM itself, and "Rick is typing" shows. Morty replies,
and his avatar goes away.


https://github.com/user-attachments/assets/3614be5f-3a69-4a1a-9290-d1fad16a5445

The UI tree confirmed each state, for example "Summer, Jerry, Morty are
working. Rick is typing", "Activity: 3 agents working", "Morty is
working" (agents only), and "Morty is working. Rick is typing" (DM).

- Widget tests in `channel_detail_page_test.dart` (Typing indicator
group):
- Scope table: Me, channel and DM, for a person and for your agent.
While the inventory loads, an agent never shows as a person.
- The journey from #8754, extended: the composer status and button, the
list's places, opening the thread, thread-view rows opening activity,
and the list closing when work stops.
- Six agents in several places: four avatars and "+2", "+1 more",
scrolling to the last row, opening a thread that is not loaded, and
opening activity.
- Me opens a lone agent's activity directly. People typing: 1, 2 and 3.
- Each new check was made to fail on purpose by removing the code it
guards.
- `just mobile-check` and the full `just mobile-test` passed on
`3480ad238` (3983 passed, 28 skipped).
- Typing names use the full foreground, so they stay readable in dark
themes (review feedback). A test checks the color.
- Auto and Cerveau reviewed it adversarially against buzz-app
origin/main. Their findings are fixed: the list did not scroll, unloaded
threads did not open, the Me behaviour, the non-member indicator, and a
double-tap pop. Their last pass on `2c45f3ec0` found nothing. The device
run then showed that, with only agents working, the spoken status had no
size and Android dropped it. `8362d46af` fixes this, and their review of
that fix found nothing.
- Not yet tested on iOS or by a human.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:17:34 -04:00
Logan Johnson
851250524e feat(mobile): show your agents working in a thread on its root message (#8754)
## Summary

When you mention one of your agents in an ordinary channel on mobile,
nothing in the channel showed that the agent was working until its reply
landed. The agent answers in the mention's thread, so its typing (kind
20002) is thread-scoped, and the channel timeline hides thread typing.
The header working dot that used to cover this was removed in #8052,
#8075 and #8082.

This change brings buzz-app's behaviour to mobile. In buzz-app, the
thread summary under a message shows the viewer's own agents while they
type in that thread (`MessageRow.tsx` `useThreadAgents`, described in
`docs/agents.md`). Mobile now does the same. The root message's reply
row shows the working agents' avatars and the typing dots, and the row
appears even under a mention that has no replies yet. It then reads
"View thread". Tapping it opens the thread. When the reply arrives and
the typing ends, the dots go away and the row reads "1 reply".

The details follow buzz-app:

- Only your agents count. buzz-app's "your agents"
(`agentChoices.identities`) are your agent inventory, which is the kind
30177 records you author with archived agents included, plus your local
agents. It does not read the NIP-OA owner tag. Mobile uses the same
inventory. An agent that only names you as owner in its profile does not
count. People and other agents typing in a thread stay inside the thread
view.
- Working agents leave the participant stack for their own group: up to
two agent-shaped avatars side by side, then "+N", then the dots.
- The curved reply branch shows only when the message has replies.
- buzz-app's channel indicator by the composer also includes your agents
working in any thread ("Channel views include threads" in
`current-activity.ts`). Mobile's channel-level surface is the typing row
at the end of the timeline. In ordinary channels, that row now also
shows your agents that are typing in a thread, with the agent avatar
shape. It goes away when the agent's reply arrives.
- The spoken labels use buzz-app's strings: "View thread: Morty is
working", "View thread: 3 agents are working" and "View thread: 1 reply.
Morty working".
- Me and DMs already show thread typing inline, so they are unchanged.

Code changes:

- `TypingDots` is extracted from `AgentTypingBubble`, so the reply row
and the bubble share one animation. It respects reduce motion.
- `ReplySummaryLink` accepts a nullable summary and a list of working
agents with their names. `SmallAvatar` gains an `agent` flag for the
working group, and `InlineAgentTyping` a `knownAgents` set so the
channel typing row gives inventory agents the agent shape.
- `meAgentInventoryProvider` holds the inventory keys. It is read only
while a thread in the channel has typing. Each read, failed ones
included, is reused for 30 seconds, then read again if still watched, or
dropped. So the inventory refreshes every 30 seconds while it is
watched, and a short gap between an agent's messages does not query
again.
- The page selects a "pubkey root" key from channel typing, so typing
expiry refreshes do not rebuild it.

Less than buzz-app, not in this PR:
- buzz-app prefers lifecycle evidence (local agent state and kind 24200
observer turns) and falls back to typing. Mobile uses typing only. So
when an agent stops without replying, mobile keeps its dots until the
typing expires (8 seconds). A second buzz-app device without that
evidence behaves the same way. I found no 24200 publisher in buzz-app
itself. buzz-app publishes typing (kind 20002) for the agents it runs.
- The channel typing row reads "Morty is typing" where buzz-app's
indicator says "working". It has no popover that names the thread and no
tap action. It sits after the newest message, so it scrolls away with
the timeline, while buzz-app's indicator stays by the composer.

### Related issue

N/A. Reported in Buzz #buzz-bugs. Searched issues and PRs for
duplicates: none found.

### Testing

Android emulator, local relay, build of `3caa17a20`. Demo User mentions
Morty. Morty, Summer, Jerry and Rick send thread typing:
- Morty is a channel bot in Demo User's agent inventory.
- Summer and Jerry are in the inventory but are ordinary members, so
only the inventory gives them the agent shape.
- Rick names Demo User as owner in his profile but is not in the
inventory.

In the video:
1. The root message shows Morty and Summer, "+1", the dots and "View
thread", with no reply branch. The channel typing row reads "Morty and 2
others" with three agent avatars. Rick does not show.
2. Tapping "View thread" opens the thread before the first reply. Back
to the channel.
3. Summer and Jerry stop. Their typing expires, and only Morty is left
on both rows.
4. Morty replies. The typing row goes away, the reply branch shows, and
the root row reads "1 reply".

The UI tree confirmed "View thread: 3 agents are working", "Morty and 2
others are typing", "View thread: Morty is working", "Morty is typing"
and "View thread: 1 reply".


https://github.com/user-attachments/assets/2237e565-aa48-405a-84ca-39ab9a06b7a6

- The widget test "an agent working in a thread shows on its root
message" (`channel_detail_page_test.dart`, Typing indicator group)
covers these cases:
- A person, and an agent outside the inventory that names you as owner,
show no row and no channel typing row.
- Inventory agents also show in the channel typing row ("Morty and 2
others are typing", checked in the semantics tree), with the agent
shape. That row goes away when the reply arrives.
- Three inventory agents show two avatars with the agent shape, "+1",
"View thread", no branch, and "View thread: 3 agents are working".
  - The row opens the thread before the first reply.
- After the reply, Morty shows once, the branch shows, and the label is
"View thread: 1 reply. Morty working".
- With Summer still typing, Morty removed from the inventory stops
counting after the 30-second refresh.
- Each check was made to fail on purpose by removing the code it guards.
- `just mobile-check` and the full `just mobile-test` passed on
`3caa17a20` (3996 passed, 28 skipped).
- Auto and Cerveau reviewed it adversarially against buzz-app
origin/main. Their findings are fixed, and their last pass found
nothing.
- Not yet tested on iOS or by a human.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:17:33 -04:00
klopez4212
f3a7bab098 Clarify mobile community onboarding and saved identity selection (#8729)
## Summary

Make community onboarding easier to scan: shorten hosted hostname
fallbacks, clarify Add Community instructions, and replace raw saved-key
buttons with identity cards. Use white pages, grey cards, ghost
secondary actions, standard page transitions, and a “Getting ready”
screen during joining. Native profile setup keeps a horizontal modal
transition. Identity cards load signed name/avatar metadata from saved
communities without switching accounts; unavailable profiles use a
neutral fallback. Cards show only the avatar, a 16px medium-weight name,
and muted npub; community lists are omitted. Retiring profile setup
closes its avatar editor too.

### Related issue

Follow-up to #8687 and #8599; no duplicate PR found.

### Testing

- Formatting and analysis passed. Full suite with hosted-auth defines:
4,019 passed, 5 skipped; includes profile-fetch and cancellation
regressions.
- Prior iOS device preview and native profile tests passed; hosted CI
native Swift checks passed on the preceding head. Latest Dart fixes are
not yet reinstalled on the phone.
- Simulator walkthrough and owner-tested iPhone preview use sample
identities and simulated admission.

Sample-data previews; the identity snapshot shows the
unavailable-profile fallback rendered by Flutter. Media is attached here
only.

| Community picker | Add Community | Saved identity |
| --- | --- | --- |
| ![Shortened community
name](https://github.com/user-attachments/assets/cf0af6aa-3a3d-4ffe-b469-e26f9bb92c30)
| ![White Add Community
page](https://github.com/user-attachments/assets/b2f71c77-1aa0-465a-b302-7eb5f1e92819)
| ![Saved identity card and ghost import
action](https://github.com/user-attachments/assets/47f20f34-f047-4ce5-befa-f56834096e95)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-30 19:00:42 +00:00
klopez4212
c0c606e2e3 Polish mobile navigation and restore community switch animation (#8732)
Removes aggregate unread dots from mobile filters, Inbox, and Solo
section headings. The aggregate-dot removal is an explicitly requested
product change. Solo and Team names match the current `block/buzz-app`
sidebar’s light/dark colours and 400/600 read/unread weights (not this
repository’s older `desktop/` implementation); Solo rows show
owned-agent working dots with reduced-motion support.

Restores the community-switch loading and avatar flight with a leftward
arc to the community menu in its current top-right position.

Mobile uses one fixed light/dark application palette; legacy catalog
theme names do not select navigation palettes. The 600 unread weight
follows [the current desktop sidebar
override](36a283b238/src/bundled/channels/Channels.module.css (L47)).

Working status uses live agent typing; desktop’s local waiting state is
not available on mobile.

Validation: clean analysis; 3,997 mobile tests passed, 28 skipped; four
push-configuration checks passed. Navigation styling visually approved
on iPhone; community-switch regression covers portrait/landscape and
normal/reduced motion; authenticated-shell recovery is covered. Hosted
CI passed at `bfeb0fecd`. Updated Pairing Preview installed on iPhone
for transition testing.

### Snapshots

Actual Flutter widgets with sample conversations; static working dots
for reduced motion.

| Light | Dark |
|---|---|
| ![Light theme: unread chat above read
chat](https://github.com/user-attachments/assets/d83b35d5-ab9b-44e0-b7d6-6527ef10a776)
| ![Dark theme: unread chat above read
chat](https://github.com/user-attachments/assets/45f64291-5d39-4e17-8f72-1aa1271c10b7)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-30 18:03:25 +00:00
Wes
75854111b8 fix(mobile): retain invite keys and reject incomplete onboarding (#8753)
Self-hosted invite claims retain the generated identity in secure
storage before the HTTP request and reuse it after a lost response or
restart. Saving or reopening an existing destination retires the
matching journal. Sign-out/removal deletes all matching pending key
copies before removing the saved community, preserving the community for
retry if cleanup fails.

Pairing verifies that relay authentication completed before
saving/activating a destination or reporting success. On iOS 16, hosted
entry permits recovery and reuse of backed-up identities, including
binding a recovered identity with a confirmed backup receipt. Paths
requiring a new secure backup explain the iOS 17 requirement before
generating or binding a new key. Self-hosted entry remains available.

### Validation

- Initial focused entry/recovery files: 83 passed, 1 skipped. Review-fix
focused storage, community, invite and hosted files: 82 passed, 1
skipped; extended invite journey: 17 passed, 1 skipped.
- Final hosted journey extension: 20 passed, including both bound and
unbound recovery with native sealing unavailable.
- Isolated mutations removing invite retention, pairing validation,
journal removal/re-entry cleanup or restoring the blanket platform gate
fail their intended regressions. Restored controls pass.
- iOS simulator build passed on the original integration base; final
native/device acceptance remains outstanding.
- Carl and three specialist reviewers found no source-review blockers at
`323cec6ea2`. `a55a246857` adds analyzer fixes, copy cleanup and the
recovered-unbound journey coverage. Reviewers did not run tests or the
app.

- Fixed a push-hook timeout in `4564b3562a`: idle serial queues now
release completed futures, avoiding scheduler retention across widget
tests while preserving in-flight ordering and failure isolation. The
existing dispatcher journey fails promptly before the fix and passes
afterward; Carl reviewed this delta with no blockers. Focused
dispatcher/invite/storage checks passed, and all 20 hosted journeys
passed with hosted auth enabled.

- Mandatory pre-push checks passed at `4564b3562a` (scope, branch skew,
file size, mobile analysis/tests and crypto package checks); GitHub head
verified at that commit.

### Remaining acceptance

Draft pending agent/device and human acceptance. Exercise interrupted
one-use invite claim and retry across restart, sign-out then reopen
without restoring the removed identity, rejected self-hosted pairing,
iOS 16 backed-up identity recovery (including binding), and iOS 16 new
setup blocked before key creation. Synthetic network/storage boundaries
do not prove native durability or live server behavior. Hosted CI
results are not claimed here.

Optional follow-ups remain outside this patch: pruning provably
unadmitted journals, malformed-record recovery, avoiding an iOS 16
dead-end device-key selection, and pre-existing removal/push-revocation
ordering.

This PR contains three existing-path fixes. The two organization-login
fixes depend on #8700 and remain separate: its stale parent conflicts
with main in unrelated relay code, and the required branch-skew hook
prevents publishing that stack unchanged. This PR does not refresh that
parent or change deployment policy.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-10-30 10:38:24 -06:00
klopez4212
aae0081a90 fix(mobile): show agent DM typing and balance reply bubble padding (#8731)
Messages with a reply footer now keep even vertical text padding unless
they have actual overlapping reactions. Agent typing indicators are now
visible in both one-to-one and group DMs when the response is
thread-scoped, regardless of Bestie selection.

The typing fix originated in #8665 and now applies to every agent DM
using the existing agent classification. It retains main's
`visibleTypingProvider` filtering. Ordinary channels and human
thread-typing signals retain their existing scope; voice recording is
not included.

Validation: required pre-push hooks passed, including mobile formatting,
analysis, tests, native crypto package tests, and file-size checks.
Flutter analysis and the full mobile suite pass (3,995 passed, 28
skipped). Widget coverage compares bubble heights with and without a
reply footer and checks typing across Solo, ordinary channels, agent
DMs, human DMs, and group DMs with agents.

Marked ready for review at the owner’s request. Device verification
remains pending. To try: open a DM with any agent, including one not
selected as Bestie, while it handles a thread-scoped response and check
for its avatar and three dots; repeat in a group DM with an agent;
compare equal text in messages with and without a reply footer, with no
reactions, and confirm equal bubble heights.

The broad `just ci` check was stopped during the unrelated desktop Rust
dependency build after the mobile pre-push checks passed; no
full-repository CI pass is claimed.



### Before / after

Flutter widget-harness snapshots with identical synthetic data; not
native device captures. Before: the reply-footer bubble has excess
bottom padding and the thread-scoped Bestie typing signal is hidden.
After: both text bubbles have equal height, and Bestie’s avatar and
three dots are visible.

| Before | After |
| --- | --- |
| ![Before: uneven bubble padding and hidden Bestie
typing](https://github.com/user-attachments/assets/b7b9279d-4b6d-4263-8b4c-91451ee57fd0)
| ![After: balanced bubbles and visible Bestie
typing](https://github.com/user-attachments/assets/c97be4fd-5fc0-47e5-8b78-5b0b9c60254a)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-30 15:09:07 +00:00
Matt Toohey
fae3f0346c test(mobile): keep late callbacks and fixed sleeps from failing tests (#8678)
## Summary

Fixes three mobile flakes from the 21–28 Oct 2026 CI runs, in test code
only:

- In two iOS `RunnerTests` classes, late bridge callbacks no longer fail
whichever test is running when they fire.
- The waits on a busy test-host main thread now cap at 10 s.
- Two Flutter avatar tests poll for the ready frame instead of sleeping
a fixed 50 ms.

This is one of seven PRs split out of the `flakey-tests` branch
(siblings below).

### Related issue

None found.

### Flaky tests fixed

| Test | Failing runs | Cause | Change |
|---|---|---|---|
| `BuzzPushSnapshotEnrichmentTests` (RunnerTests) | 40362671989 (job
121712747802, main), 39681273328#1 (job 119504560518), 40658588384 (job
122715950541, PR #8640) | The 1 s waits ran out while the test host's
Flutter app held the main thread, and the test returned and deleted its
temp directory. The bridge callback's assertions then ran later and were
blamed on whichever test was running (`ProfileSetupRecoveryTests`,
`IdentityFilePluginTests`). | Callbacks only record the value and
fulfill. Every assertion, including the `isRestricted` checks, runs
after the wait, so a late callback writes into a dead local. The seven
waits cap at 10 s (`callbackTimeout`). |
| `ProfileSetupRecoveryTests`
`testStartedEditorWithoutReadyRestoresProfileAndAllowsRetry` |
40585805357 (job 122477274956, PR #8568) | Engine release and editor
dismissal must both happen within 2 s, on the main thread the host app
keeps busy. | Both predicate waits cap at 10 s. |
| Flutter `channel_detail_page_test.dart` "profile sheet shows the
poster before autoplay and restores it on tap" and
`settings_profile_header_test.dart` "shows the poster until the animated
avatar is ready" | 40368315739 (job 121732711851, main) | After the
image response is released, both tests slept a fixed 50 ms of real time
before `pumpAndSettle`. A slower real decode left no frame scheduled, so
the ready key was missing. | A new `WidgetHelpers.pumpUntilFound` pumps
in 10 ms real-time steps until the ready key appears, capped at about 5
s. |

Local evidence is from the original `flakey-tests` commit (53aaec97c),
Xcode 27.1 on the iOS 26.2 simulator:

- Enrichment, with `Self.complete` delayed 1.5 s and 4 s: 6 failures
before, 107/107 after. With a 12 s delay, every failure lands in its
owning test. A mutated error code is still caught.
- Profile setup, with the engine release held 4 s: 2 failures before,
4/4 after. A leaked engine still fails.
- Avatar tests: under load, 94/96 before and 96/96 after. With a 200 ms
image delay, 0/5 before and 5/5 after. A widget that never signals ready
still fails.

Not included:

- The planned `AppDelegate` / `Runner.xcscheme` change that keeps
Flutter out of the RunnerTests host. It did not work locally: restored
scene sessions and the `UIMainStoryboardFile` fallback, and a normal
launch came up black afterwards.
- `IdentityFilePluginTests`
`testSyntheticDocumentsReadInPlaceAndRejectInvalidOrOversize`
(40441255307#1). It is left unchanged and stays open.

### Verification

This branch was pushed without local re-verification, by the user's
choice: local test runs were overloading the machine and failing for
that reason. **Hosted CI is the check.** It was pushed with `git push
--no-verify`, because the pre-push hook runs the Flutter tests.

- Every file is byte-identical to its final version on `flakey-tests`
(67185df6a).
- Checked for this PR: `git diff --check`, and a conflict-free merge
with all six siblings onto `main`.
- #8613's flake-check lane will repeat the two changed Dart test files.

### Readiness

Test-only change. AGENTS.md's readiness steps (agent review, then human
confirmation) are still outstanding, so `buzz-review-completed` is not
set.

### Merge order

Independent of the other six. It merges cleanly onto `main`, and with
every sibling, in any order.

### Sibling PRs

Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease +
lane), #8677 (relay E2E helper + Windows shell), #8679 (Tauri apt cache
+ Windows Rust cap), #8680 (desktop product races), #8681 (desktop E2E
state gates), #8682 (desktop E2E composer + row actions).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
2026-10-30 11:58:40 +11:00
Tom Brow
d263e2c7f7 fix(mobile): resolve iOS pushes on a fresh connection and retry a stale one (#8722)
## Summary

iOS users were seeing a run of push notifications that just said
"Reconnect to your relay now". That is the push gateway's fixed APNs
body. It shows when the Notification Service Extension (NSE) can't fetch
the message from the relay.

On-device logs from one affected iPhone over a full day showed 12 of 385
NSE runs fail to resolve. Each failed relay `/query`: 10 hit the 8
second timeout and 2 got `NSURLErrorNetworkConnectionLost` (-1005). All
12 ran on a reused pooled connection from `URLSession.shared` in a
long-lived NSE process. That process lived for hours while the network
path changed between VPN and cellular. Of the fresh connections, 0 of
473 failed.

This change:

- Gives each notification its own ephemeral session, so it never
inherits a pooled connection. The session is cancelled when the
extension finishes or expires.
- Retries `/query` once if it times out or loses its connection
mid-request. The retry uses a new session and a freshly signed NIP-98
proof, because the relay rejects a replayed proof. Other failures (HTTP
errors, offline, cancellation) are not retried. The retry is bounded to
one attempt, so the worst case is about 16 seconds, inside the NSE's
roughly 30 second budget.
- Replaces the gateway body with "You have a new message." when the NSE
can't resolve the push. The restricted fallback ("Open Buzz to view this
message.") is unchanged, and so is the gateway payload.

### Related issue

N/A. Reported in Buzz #buzz-mobile.

### Validation

- Added tests in `BuzzPushNotificationResolverTests`:
- A timeout or -1005 is retried once, on a new session and with a
different `Authorization` proof, and resolves.
  - Repeated timeouts stop after 2 attempts.
  - `notConnectedToInternet` is not retried.
- The retry is skipped if the account key changed during the first
attempt.
- `cancel()` (called when the NSE finishes or expires) stops a pending
retry.
- Each new test fails when the behavior it covers is removed: the retry,
the new session, the account recheck, or the cancel guard.
- The full BuzzPushKit `swift test` suite passes.
- A debug simulator build of Runner and the NotificationService
extension succeeded.
- An agent review of the diff found no blockers. Its suggestions to
create the session per `didReceive` and to cancel it on expiry are
included.
- Limitations:
- The NSE target has no unit tests, so the fallback text and the session
lifecycle are covered only by build and review.
- I couldn't send a real push through the NSE. Test phones run a
personal-team bundle that isn't the gateway's APNs topic. In the
simulator, `simctl push` needs notification permission, which the app
only requests after joining a community.
- Under the mobile testing exception, the human-test step does not
apply.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:34:12 +00:00
Tom Brow
64312fa5be fix(mobile): address #8650 second-pass review (#8691)
Follow-up to #8650. This addresses the items from the [second-pass
review](https://github.com/block/buzz/pull/8650#pullrequestreview-5700660448),
which arrived after #8650 merged.

- **Setting menu semantics.** Options in the shared `BuzzSettingMenu`
(model, reasoning effort, and channel TTL) expose their checked state
again. The trigger is announced as a button with enabled and expanded
state, so a disabled "Reasoning effort" no longer reads as plain text.
The `PopupMenuButton` this menu replaced also provided a named route and
a "Dismiss menu" action. `MenuAnchor` does not provide them, so this PR
does not restore them.
- **Remote listing failures.** "Could not load agents" for a failed
Builderlab listing now appears only in communities that support remote
agents. For example, it no longer appears in a non-hosted community
while signed in.
- **Add agent during a community reload.** No change needed here. #8693
already disables Add while the page has no community.

### Validation

- Added tests: setting-menu checked, button, enabled and expanded
semantics; a failed remote listing stays hidden where remote agents are
off. Both fail without these changes.
- Mobile analysis is clean, and the Agents and shared widget tests pass
locally after rebasing onto #8693.
- Not covered: VoiceOver on a device.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@sprout-oss.stage.blox.sqprod.co>
2026-10-29 15:36:21 -07:00
Tom Brow
a94ebece0e Revert "fix(mobile): push every message in your own Solo chats (#8615)" (#8690)
Reverts [#8615](https://github.com/block/buzz/pull/8615).

Mobile push for Solo chats returns to mention-only, matching buzz-app,
which alerts on every message only in DMs. This also removes the
lease-size guards #8615 added, since they existed to fit Solo chats into
the lease.

### Validation

- Clean revert of bad9d26142 with no conflicts.
- Mobile lint and the full mobile test suite pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Tom Brow <tomb@squareup.com>
2026-10-29 15:36:16 -07:00
Tom Brow
4b21a10c4b fix: don't notify workflow owners through their attribution tag (#8694)
A workflow message names its owner in a marked p tag, ["p", owner, "", "owner"], for attribution. #p live, history and push matching, operator listeners, desktop, mobile and the iOS NSE treat that tag as attribution rather than a mention. A workflow that mentions its owner adds a plain p, so the owner is notified only when actually mentioned. NOSTR.md records the #p exception.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@squareup.com>
2026-10-29 15:21:56 -07:00
Tom Brow
050d545264 perf(mobile): skip unchanged message footer layout (#8433)
Rebuilding an unchanged message footer currently schedules another
layout even when its overlap value is identical. Only invalidate footer
layout when that value changes, preserving layout updates for actual
geometry changes.

### Stable release recommendation

Exclude from `mobile-release/conference-2026-10`; defer to the following
release. The regression test demonstrates eliminated redundant layout,
but the effect on navigation latency has not been quantified.

### Validation

The added widget regression checks that unchanged footer rebuilds do not
repeat layout and changed overlap still updates geometry. The
independent footer build passes the full mobile suite, analysis, and
native iOS Simulator build. Channel scrolling, opening an existing
reaction list, and thread navigation were exercised on iOS 26.5
Simulator.

The full local repository gate previously encountered an unrelated,
unchanged desktop subprocess timing test failure; that test passed in
isolation. All PR CI checks pass, including the native iOS suite.

<!-- buzz-review-completed -->

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-29 14:41:53 -07:00
Tom Brow
be1ac392e8 perf(mobile): render inbox and search previews as plain text (#8675)
### Summary

Inbox rows and search results show each message as a 2-line preview.
That preview was a capped `MessageContent`, so gpt_markdown parsed and
laid out the whole source (syntax-highlighted code blocks and measured
tables included), even though only two lines are painted. A native trace
on an iPhone 12 mini attributed a 102 ms inbox-open frame mostly to this
work.

Previews now skip Markdown entirely. `messagePreviewSource` takes only
the leading lines a preview can show, and at most the first 4,000
characters of a single long line, so link normalization, mention binding
and emoji lookup never scan the rest of a long message.
`messagePreviewPieces` is then a linear pass over those lines, similar
to desktop's `getMarkdownPreviewText`. It drops block syntax (headings,
quotes, list markers, rules, table dividers, code fences) and unwraps
inline emphasis and `<u>`, honoring backslash escapes and keeping inline
(including multi-backtick) and fenced code as literal text. It also
replaces links with their label. The new `MessagePreview` widget renders
the result as a single `Text.rich` with `maxLines` and an ellipsis.

Tokens keep the look they have in the thread view:
- A bound mention is the same `MessageMentionPill` chip, with the bot
icon for agents. An ambiguous mention stays as text.
- `#channel` tokens and `buzz://channel/...` links are the same channel
token chip.
- A `buzz://message`, repo, PR or issue link that shows its own URL
previews with the thread view's short label, such as `general ·
abcdef01`. Both use the shared `buzzLinkLabel` helper.
- Custom emoji render as inline images. Only shortcodes the message
names are looked up in the palette.
- Media links show as "Image", "Video" or "Audio".

Chips in previews are not tappable, and tapping the row opens the
message. Bold, italic, inline code and link styling are no longer shown
in previews. The timeline and thread view are unchanged, and so is the
profile status line, which still uses `MessageContent`.

This replaces this PR's earlier approach of cutting the Markdown source
before parsing. That approach needed delimiter-balance heuristics, and a
security review found a real case (`**` inside a link label) where the
heuristic misfired.

### Validation

- `preview_text_test.dart` is a table test of the parser: block syntax,
emphasis that must and must not unwrap (`snake_case`, a lone `*`),
inline and fenced code, unpaired backticks, links, tables, the line
limit and the long-line cap. It also covers mentions (bound, ambiguous,
inside emphasis, link labels and code), custom emoji (including adjacent
`🅰️b:`), channel tokens and links, message and repo link labels, and
media. The long-line row also asserts that `messagePreviewSource`
returns a bounded prefix.
- `message_preview_test.dart` checks the widget: no `GptMarkdown`, one
capped and ellipsized paragraph, mention chip, custom emoji image,
channel chip, media label and empty content.
- Existing inbox and search tests updated to find `MessagePreview`,
including the agent-chip test in search.
- `flutter analyze` and the full mobile test suite pass at 8c721ba3a
(with `--dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example`, as
`just mobile-test` runs it).
- Device A/B on an iPhone 12 mini, using Profile builds against the real
inbox on the internal relay. Each run was a cold launch followed by 6
Inbox opens, in the order main, fix, main, fix:

  | | main eda26b942 | this PR 6661d594a |
  |---|---|---|
| First open, worst frame (run 1 / run 2) | 40.4 / 34.9 ms | 22.4 / 23.2
ms |
| First open, build total (run 1 / run 2) | 77.8 / 91.1 ms | 38.5 / 52.3
ms |
  | All opens, median worst frame | 27.2 ms | 23.3 ms |
  | All opens, median build total | 43.4 ms | 26.6 ms |

First open drops about 40% in build time and about 14 ms in worst frame.
On warm opens, build time also drops about 40%, but the worst frame
barely moves, so what remains in those frames is not the preview parse.

## Codex review status

Codex reviewed this PR over four rounds. Every finding about cost or
correctness is fixed, and those threads are resolved. Later rounds
turned up narrower and narrower preview edge cases, so the author
decided to stop iterating on Codex findings. Any Codex thread still
open, or any finding Codex posts after f538853ed, is a deliberately
accepted preview limitation and doesn't block merging. Example: a [code
span that continues across a line
break](https://github.com/block/buzz/pull/8675#discussion_r4415196032)
can preview a mention as a chip. The full message view is unaffected.
Only the one- or two-line inbox and search previews can differ from it.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:54:32 -07:00
Tom Brow
762adad54f fix(mobile): require lowercase hex in native verification (#8702)
Follows [#8657](https://github.com/block/buzz/pull/8657) with the three
optional notes from Carl's
[review](https://github.com/block/buzz/pull/8657#pullrequestreview-5703482477).

- **Lowercase hex only.** `verifySchnorrHex` now rejects uppercase
pubkey, message and sig hex, and `verifiedNostrEventFromJson` rejects an
uppercase sig, as the pure-Dart `bip340` decoder does and NIP-01
requires. (The `nostr` package already lowercases an event's pubkey when
it parses one.) Before this, a key holder could publish a second,
uppercase encoding of the same signature and it would verify.
`_decodeSecretKey` still accepts either case.
- **Bestie record.** `decodeDefaultAgent` now verifies with
`verifiedNostrEventFromJson`, like the other Solo `me_*` records.
- **Rejection tests.** The "other pubkey" row now recomputes the event
id, so it reaches the native Schnorr check instead of failing on the id.
Every row now asserts its `EventValidationReason`, a new row covers an
uppercase sig, and `verifySchnorrHex` is checked with uppercase pubkey,
message and sig.

## Testing

- Making either lowercase pattern accept uppercase again fails the
tests.
- `just mobile-check mobile-test` passed on this change before it moved
onto main. On main, the crypto and Solo/me tests pass.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:53:24 -07:00
Wes
fffd8ca957 fix(mobile): recover Solo sync silently instead of showing a retry banner (#8701)
## Problem

The mobile Solo section could display "Live updates stopped. Your saved
chats are still available." with a **Retry live updates** button. Any
failed Solo head refresh raised this banner. That includes the refresh
that runs on app resume before the relay has reconnected, and an 8s
history timeout. In those cases the live subscription itself was still
healthy. A later successful refresh did not clear the banner, so it
stayed up until someone tapped Retry.

## Change

- Remove the banner along with `MeSync.syncClosed` and
`meSyncWarningProvider`. Saved chats stay visible the whole time.
- `meGroupsProvider`: on app resume, an interrupted reader (terminal
CLOSED or a failed read) resubscribes on its own while the relay is
connected. Before the relay reconnects, the existing
rebuild-on-reconnect still owns recovery. A healthy reader keeps doing a
plain head refresh.
- Home's pull-to-refresh (`ChannelsPage` with the Solo sliver) now also
restarts the Solo reader. Before this change, that path refreshed
channels only, so in the shipped layout the banner was the only manual
recovery.
- The no-snapshot `_MeRetry` state and the offline-save warning are
unchanged.

## Validation

- `flutter analyze` is clean.
- The pre-push `mobile-checks` passed.
- The journey step in `offline_save_page_test.dart` closes the
subscription, cycles background and foreground, asserts a new
subscription, and checks that a newer event delivered on it is saved.
Removing the `subscriptionClosed` recovery branch fails it.
- The new `channels_page_test.dart` case drags to refresh with a Solo
sliver and asserts the Solo reader restarts. Removing the invalidation
fails it.
- Not yet exercised in the running app. That is checklist steps 2 and 3.

Reviewed by the Royal Court agents (provider lifecycle, recovery/UX,
independent challenge); the one blocker (Home pull-to-refresh) is fixed
here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-10-29 14:33:35 -06:00
Tom Brow
ccfc1490e4 perf(mobile): verify signatures with native libsecp256k1 (#8657)
The app already verifies signatures in a few places: NIP-OA agent
profiles and owner attestations, Solo `me_*` records (agent inventory,
archived agents, assistants, sync), and pairing events. Each used
`nostr.Event.fromJson`'s pure-Dart BIP-340 verify, about 11.2 ms per
event on an iPhone SE (2nd gen, Profile). This routes them through
`verifiedNostrEventFromJson` and `verifySchnorrHex`, which keep the same
checks (timestamp range, canonical id, Schnorr signature) and the same
`nostr` exception types, with the signature check in libsecp256k1 (40
µs). NIP-OA owner authorizations are now signed with `signSchnorrHex`,
which removes the last pure-Dart Schnorr call in `lib`.

Incoming relay events are still not verified; that stays a separate
decision.

A native trace showed pure-Dart Schnorr verify in `me_sync` and `nip_oa`
costs 100-125 ms per foreground. This PR moves exactly those sites.

### Validation

- Added tests: the native verifier accepts a valid event and rejects
tampered content, a wrong signature, a different pubkey, malformed and
short signatures, and a zero timestamp, each also rejected by the
pure-Dart verifier. Hex Schnorr helpers are checked in both directions
against `nostr.Schnorr`.
- Focused existing validation: crypto, NIP-OA, Solo/me, pairing and
agent tests pass. `just mobile-install mobile-check mobile-test` passes
at f90823cd1.
- Device: a Profile build of the full native stack ran on an iPhone 12
mini against the internal relay.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
2026-10-29 12:50:19 -07:00
Tom Brow
ff284d00d3 mobile: stop typing prune ticks from rebuilding channel pages (#8695)
### Summary

Typing in the mobile composer felt slower than a native iOS text field.
A device trace on an iPhone 12 mini showed the cause: while anyone in
the channel is typing, the whole channel page rebuilds once per second,
including the visible message list.

- `ChannelTypingNotifier._prune()` runs on a 1 s timer and assigned a
new list to `state` on every tick, even when nothing had changed.
- `_ResolvedChannelDetailPage` watched the whole list and only filtered
out the current user afterwards, so every tick rebuilt the page and its
`PositionedList` (12 to 35 ms per frame, 8 to 19 ms for the message list
alone).
- Your own typing indicator echoes back from the relay, so the timer
kept running the whole time you typed.

In a 30-key trace, 33 of the 35 frames over 12 ms were these rebuilds,
1.00 s apart.

### Fix

- `_prune()` publishes only when the visible entries change.
`TypingEntry` now compares by value.
- The channel and thread pages watch `visibleTypingProvider`, a
`VisibleTyping` value that already excludes the current user and applies
the page's scope. Your own echo refreshes its expiry every 3 s, which
used to rebuild the page. Now it doesn't.

Other `channelTypingProvider` consumers are unchanged.

### Related issue

None found.

### Testing

- Provider tests: a typing entry held across five prune ticks notifies
listeners once, and the pages' shared `visibleTypingProvider` is not
notified by the user's own refreshed echo but is by another user.
Breaking either guard fails its test.
- `just mobile-check` and `just mobile-test` pass at 8b8a16845.

- Device trace on an iPhone 12 mini: 30 keys typed into a channel
composer, Profile builds with build tracing on both sides. Control is
6661d594a, which has the same page code as main.

  | | control | this PR (5ff754131) |
  |---|---|---|
| Channel page rebuilds while typing | about 1 per second (44 in 40 s) |
0 |
| Largest UI build per key, median / p90 | 4.9 / 29.3 ms | 1.0 / 1.4 ms
|
  | Keys with a build over 16.7 ms | 9/30 | 0/30 |
| Frames with UI work over 12 ms | 35 (33 page rebuilds) | 1 (a
semantics update from the test driver) |
  | ComposeBar frame, median | 10.2 ms | 3.0 ms |

The test driver turns on accessibility, so semantics runs on every frame
in both traces. About 1 key in 4 still has a slow frame with about 1 ms
of build and 2 ms of raster. A Time Profiler capture put that time in
the driver's accessibility queries on the main thread (52% of
main-thread samples while typing, none before the first automated tap),
so it is a test-harness artifact rather than app work.

- Dictation on an iPhone SE (2nd gen): a 15 s paragraph dictated into a
channel composer, Profile builds with build tracing.

  | | main | this PR (5ff754131) |
  |---|---|---|
| UI frame, median / p99 / max | 0.9 / 10.4 / 26.1 ms | 0.9 / 5.3 / 10.8
ms |
  | Frames over 16.7 ms | 10 | 0 |
| Channel page rebuilds | 26 (once a second) | 6 (composer growth, none
over budget) |

Each dictation update costs about 1 ms. The caret stayed at the end and
nothing flickered on either build.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:14:55 -07:00
klopez4212
0cde1fe2f4 Make mobile Agents a full-page owned-agent directory (#8693)
Replace the mobile Agents sheet with a full-page directory of verified
owned agents. Add dedicated creation and avatar pages with
catalog-backed model selection, reuse the profile emoji/photo/camera
controls, and expose Builderbot model settings while labeling desktop
agents “Manage on desktop.” Share one account-scoped model catalog
across editable cards and retries. Choose Bestie from the agent profile
using the shared encrypted main-agent record (text-only action, filled
star once selected), and move confirmed deletion to a top-left trash
control with native iOS glass. Include uploaded avatars in owner
registrations so desktop can display them.

Validation: mobile analysis and all 3,958 hosted-enabled app tests
passed; default-build tests run by the push hook. Earlier iPhone build
installed and iterated with the user; the current iOS build succeeds,
but final changes still need a device pass. Independent review found no
new blockers. Actual camera capture and live Bestie sync remain
unconfirmed; the existing shared `main-agent` wire format and Bestie
terminology are retained.

### Snapshots

Widget-harness captures with test data (not native iOS chrome).

| Agents directory | Image avatar editor |
| --- | --- |
| ![Agents
directory](https://github.com/user-attachments/assets/c68a0505-90a5-41db-980f-e3197cac63ac)
| ![Image avatar
editor](https://github.com/user-attachments/assets/9b126186-1e7c-4169-9a74-8f7f252cfa93)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 18:30:17 +00:00
Tom Brow
3dbc95c3f6 perf(mobile): compute NIP-44 ECDH with native libsecp256k1 (#8656)
`ecdhSharedSecret`, the input to every NIP-44 conversation key
(preferences, read state, reminders, themes, mutes, stars, sections,
sort, pairing, agent observer), used pointycastle's BigInt point
multiplication. That costs about 5.9 ms per call on an iPhone SE (2nd
gen, Profile) and is variable time in the secret scalar. It now calls
libsecp256k1's constant-time `secp256k1_ecdh` through `ecdhX` from #8654
(39.8 µs on the same device), returning the same raw x-coordinate NIP-44
requires. The decoded secret key bytes are zeroed after the call.

Invalid keys now throw `ArgumentError` instead of pointycastle's errors;
callers already treat any throw as a failed derivation.

### Validation

- Added tests: `ecdh_test.dart` compares the native result with the
previous pointycastle algorithm for 32 random key pairs, checks the
NIP-44 v2 `get_conversation_key` vector (secret 1, peer pubkey of secret
2), and checks rejection of a zero secret, an off-curve public key and
wrong lengths.
- Full mobile analyze and test suite pass after rebasing onto main at
da1190e2c, including the desktop nostr-rs NIP-44 interop vector.
- On a device: an iPhone 12 mini Profile build of the full native stack,
connected to the internal relay.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
2026-10-29 10:58:25 -07:00
klopez4212
e922dc08ff Unify mobile onboarding and community entry screens (#8687)
## Summary

Use consistent fullscreen screens for identity setup, community
selection, joining, and Add Community. Restore all memberships and open
the selected community; share the community grid with the switcher.

Add QR-first choices, animated link entry with 12px keyboard clearance,
a two-finger double-tap pairing toggle, and the centered reversing bee
loader. Preserve cancellation, Back, and reduced-motion behavior.

Add intentionally omits community creation and Builderlab sign-in: this
route follows sign-in and the existing-community picker.

### Related issue

Related: #8628 (native profile setup). No duplicate fullscreen
onboarding PR found.

### Testing

- Full standard and hosted-authentication mobile suites, static
analysis, and required push checks passed.
- iPhone preview iterated with Kenny; final simulator walkthrough below
uses synthetic data and does not authenticate or save account changes.
- Independent review completed; regressions cover cancellation during
loading, Add recovery after failed/conflicting memberships, retrying
skipped communities, and explicit picker stages.

Walkthrough recorded before the copy and recovery followups:


https://github.com/user-attachments/assets/bfea3ad3-1dde-40c4-8748-44c84226c292

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 16:12:31 +00:00
Tom Brow
8abcf2ea2a fix(mobile): keep read state across reconnects and resume (#8672)
### Context
Every app resume and relay reconnect tore down `ReadStateManager` and
built a new one. `ReadStateNotifier.build()` watched the whole
`relaySessionProvider` state, so each status change (disconnected,
connecting, connected) rebuilt the notifier. On an iPhone 12 mini each
resume re-ran the full read-state fetch and decrypt (about 19 to 24
decrypts, 60 to 95 ms).

The rebuild also cleared `_forcedUnreadContexts`, so a message or
channel the user marked unread lost that flag on any connection change.

The watch dates from #422, when the manager was built with
`remoteEnabled` tied to the connection status. #802 made `remoteEnabled`
constant and added the `ref.listen` that calls `reinitializeRemote()` on
connect, but left the watch behind.

### Change
- Watch `relaySessionProvider.notifier` instead of its state. Config,
nsec, community, pubkey, crypto and a replaced session still rebuild.
Status changes don't.
- Reconnects re-sync through the existing listener
(`reinitializeRemote()`: re-fetch, then re-subscribe, preserving the
history and live overlap).
- Re-syncs are serialized. One runs at a time after any pending
`initialize()`, and reconnects that arrive while one waits share it.
Overlapping passes could otherwise each install a live subscription and
leak one.
- A connect that lands while the first `initialize()` is running (the
cold-start connect) skips the re-sync when that initialize fetched and
subscribed successfully. Without this, cold start did the read-state
work twice.

### Validation
- Provider journey: disconnected, connecting and reconnecting cause no
refetch, no re-subscribe and no readiness drop, and the forced-unread
flag and read mark survive. Connected runs exactly one refetch and one
live-subscription swap. A second reconnect behaves the same. Account and
community switches still rebuild and clear forced flags.
- Manager tests: a connect during a successful `initialize()` does not
sync again; one during a failed `initialize()` does, and leaves one live
subscription; three reconnects during a running re-sync produce one more
shared pass and one live subscription.
- Falsified: restoring the old watch fails the reconnect journey
(`isReady` false, flag lost). The previous version without serialized
re-syncs fails the overlap test (5 passes instead of 3).
- `just mobile-check` and `just mobile-test` pass.

iPhone 12 mini (Profile builds of base and this branch, real community
over the production relay, instrumented manager lifecycle and
live-subscription counts):

| | base | this branch |
| --- | --- | --- |
| Resume after 3 s, 10 s (x3), 60 s background | 3 new managers and 3
initializes per resume | no new manager, 1 re-sync |
| Live read-state subscriptions after resume | 1 | 1 |
| Channel marked unread, then 10 s / 60 s background | flag lost after
the first resume | still unread after both |
| Read-state work per resume (median) | ~24 decrypts, ~176 ms main
isolate | ~18 decrypts, ~125 ms |
| Unread badges | same bold set | same bold set |

The first device run also found the doubled cold-start sync, fixed
above. A re-check on this head:

| Cold launch | base | first version | this head |
| --- | --- | --- | --- |
| Managers built | 3 | 1 | 1 |
| Read-state syncs | 2 initializes | 1 initialize + 1 re-sync | 1
initialize |
| Decrypts | 19 | 30 | 15 |
| Main-isolate ms in read-state code | 124 | 185 to 197 | 94 to 98 |

Four resumes on this head: no new manager, 1 re-sync and 1 live
subscription each, 17 to 20 decrypts. A foreground socket drop was not
exercised on the device; every resume there was a full disconnect and
reconnect.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:12:17 -07:00
Wes
0d19ca69d3 fix(mobile): wait for app activation before browser auth (#8647)
## Summary

Opening an invite from Safari can deliver the login request while iOS
still considers Buzz inactive. `ASWebAuthenticationSession.start()` then
returns false, showing a connection error until the user retries. Wait
for app activation before starting the retained session, with a
ten-second timeout and cancellation cleanup.

Extract the existing browser-auth bridge from the size-limited
AppDelegate and add one native journey test through its Flutter method
channel. URL validation, callback fencing, and normal active-app login
retain their existing behavior. Temporary diagnostic logging and
generated dependency changes are excluded.

### Related issue

No duplicate found in the open handoff/authentication search. Related
invite-consent work: #8601; this fixes the subsequent iOS authentication
presentation race.

### Testing

- Focused `RunnerTests/BrowserAuthBridgeTests` passed on an iPhone 18
Pro / iOS 27 simulator. Covers inactive handoff, cancellation before
activation, retry, duplicate activation notifications, and immediate
active-app start.
- A local build reproduced the original inactive-app `start=false`
failure; the fixed build opened authentication on the first
background-to-app attempt. The same fixed build subsequently completed
an existing-account staging flow.
- Mutation check passed in isolated source, Git metadata, and build
outputs: clean control passed; bypassing the production activation wait
failed the inactive-handoff assertion; restored source passed again,
with its SHA-256 verified against the committed file.
- Self-review found no blocking issue. CI and final review remain
pending. Mandatory pre-commit and pre-push hooks passed, including
mobile analysis and the hook-run Flutter suite. No full local suite was
manually invoked.
- Human acceptance: from Safari, tap **Accept invite in Buzz** and
confirm sign-in opens on the first attempt without **Try again**. Final
acceptance of this PR remains pending.

The separately reported callback failure may have involved production
rather than staging. That cause is unconfirmed and is outside this
change.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-10-29 09:54:21 -06:00
Tom Brow
8161a7cad0 perf(mobile): derive public keys with native libsecp256k1 (#8655)
Follows #8654.

Deriving an x-only public key from a secret key with
`nostr.Keys(secret).public` costs about 5.6 ms in pure Dart on an iPhone
SE (2nd gen, Profile). The app does it in 10 places, including relay
config, the signed-event relay's `pubkey` getter, push lease checks,
status updates, and the settings identity row's `build` method. This
routes all of them through `derivePublicKeyHex` from #8654, which takes
16.5 µs on the same device.

Behavior is unchanged for valid keys: output is the same lowercase hex.
For a malformed or out-of-range key the thrown type changes from nostr's
`InvalidKeyException` to `ArgumentError`; every affected site either
catches all errors or rethrows its own `FormatException`/`StateError`,
and nothing in `lib` or `test` matches on the old type.

Sites that need a full `nostr.Keys` object (key generation,
`AccountIdentityStore`) are unchanged.

### Validation

- `derivePublicKeyHex` is checked against `nostr.Keys` for 64 random
keys in #8654's `event_signing_test.dart`.
- Full mobile analyze and test suite pass on this branch after rebasing
onto main, including the package's own tests.
- iPhone 12 mini, Profile build of this stack (with #8656 and #8657 on
top): paired to the internal Buzz community through Desktop; relay auth,
identity row, avatar, and channel/DM/inbox loading all worked under the
derived key.

buzz-review-completed (agent review, Codex and security review clean;
device validation above; human app test not applicable under the mobile
testing exception)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:53:58 -07:00
Tom Brow
770ab7ff82 perf(mobile): decode relay query responses off the UI isolate (#8669)
### Context

Profiling on an iPhone 12 mini (Profile build, real community) showed
`RelaySessionNotifier._postForEvents` producing contiguous main-isolate
busy runs with p90 110 ms and max 124 ms (27 runs of 50 ms or longer).
Stacks were dominated by `jsonDecode` of `response.body`, with
`NostrEvent.fromJson` smaller. The whole HTTP response decode ran on the
UI isolate: gzip inflate (done by `dart:io`), the UTF-8 decode behind
`response.body`, JSON parsing, and event parsing.

### What changes

- Successful `/query` and `/profiles/search` responses (the only two
callers of `_postForEvents`) are now decoded in a single `Isolate.run`
from `response.bodyBytes`: inflate, UTF-8, JSON, validation,
`NostrEvent.fromJson`. The parsed events come back by ownership transfer
(`Isolate.exit`), not a copy.
- Gzip: the query transport now sends `Accept-Encoding: gzip` and sets
`autoUncompress = false`, so compressed bodies reach the isolate still
compressed. Whether bytes are gzip is decided by the gzip magic bytes,
so a transport that already inflated (or an injected test client) still
works. Error bodies are inflated inline (small).
- Unchanged: `FormatException` messages, the malformed-event wrapping,
`RelayException.body`, and the rate-limit gate reading the error body.
- Fencing (Review-Proven Rule 2): the `_disposed` / `_ageRestricted` /
`_contextGeneration` check still runs before the decode and now runs
again after it. If the session retires during the decode, the query
reports `StateError` even when the decode itself failed, matching the
old ordering where a retired session never decoded.

### Design notes

- `Isolate.run` per response instead of a long-lived worker: no
lifecycle or disposal to manage, no idle isolate heap on a small phone,
easy to fake in tests, and the same shape as `parseProfileEventBatch`.
The main-isolate side of a spawn measured at microseconds, so there is
no size threshold; small responses take the same path.
- Event id and signature verification does not happen on these events on
the main isolate today (nothing in the mobile app verifies queried
events; NIP-OA verification in profile parsing is already in an
isolate), so nothing was moved.
- Other HTTP decode sites found and left alone: invite create/join,
hosted join policy, BuilderLab account, media upload response, community
icon lookup, dev push lease NIP-11, archived-agents. These decode small,
rare responses (one small JSON object each), where an isolate adds
complexity without a measurable gain. Media and avatar byte downloads do
no JSON decoding. The mobile app has no `/count` caller.

### Validation

Added tests (`relay_session_test.dart`, through the production
`queryRelay` / `searchProfiles` path with the real isolate decoder):
parsing with non-ASCII content; profile search; gzip success body and
`Accept-Encoding`; gzip error body feeding the rate-limit gate and
`RelayException.body`; malformed JSON; non-list body; non-object
element; malformed event; invalid UTF-8; error status skips decoding;
session disposed, age-restricted, or retired-before-decode while a
decode is outstanding.

The fencing tests are falsifiable: removing the post-decode check fails
the disposed and age-restricted tests; removing the
retired-wins-over-decode-error branch fails its test; removing the
pre-decode check fails the never-decodes test.

`FakeRelay` (test helper) now injects an inline decoder, because
`fakeAsync` cannot deliver isolate results; it calls the same parse
function.

The default transport still honors `http.runWithClient`, which the
hosted journey tests use to fake the network
(`relay_http_query_client_test.dart`; fails without the fix).

`just mobile-check` and `just mobile-test` pass.

Host benchmark (AOT executable on an Apple silicon Mac, median of 25,
copy of the parse logic, main-isolate stall measured with a 0.5 ms
heartbeat timer):

| Response | Main-isolate blocked before | after |
| --- | --- | --- |
| 500 events, ~630 KB | 2.4 ms | 0 ms (max gap 0.05 ms) |
| 500 events, ~630 KB gzip | 3.2 ms | 0 ms (max gap 0.19 ms) |
| 500 events with 50 20 KB avatars, ~1.6 MB | 5.1 ms | 0 ms (max gap
0.28 ms) |
| 500 events, ~4.5 MB | 14.0 ms | 0 ms (max gap 0.27 ms) |
| 500 events, ~4.5 MB (827 KB gzip) | 17.4 ms | 0 ms (max gap 0.35 ms) |

Total latency is unchanged within noise (for example 14.5 ms to 14.9
ms). The host is much faster than the 12 mini, so absolute device
savings should be several times larger.

On the iPhone 12 mini (Profile builds of base and this branch, real
community over the production relay, two runs per scenario; main-isolate
stalls measured with a 4 ms heartbeat timer, since the wireless device
tunnel has no VM service):

| Scenario | Main-isolate `_postForEvents` time, base | this branch |
Max stall, base | this branch |
| --- | --- | --- | --- | --- |
| Cold start + 60 s (~16k events, 7.8 MB gzip) | 282 to 295 ms (max 116
ms) | 0.01 ms | 147 to 150 ms | 100 to 102 ms |
| Inbox + search (~7k events) | 122 to 123 ms (max 111 ms) | 0.01 ms |
138 to 142 ms | 97 to 99 ms |
| Open 6 channels (558 events) | 5 to 6 ms | 0.01 ms | 88 ms | 86 to 87
ms |
| Background 10 s + resume (~7k events) | 118 to 124 ms | 0.01 ms | n/a
(suspension) | n/a |

- The production relay serves gzip (about 4.4:1; the largest catch-up
response is 3.15 MB on the wire, 13.9 MB inflated, 7k events). Inside
the isolate that response takes 30 to 47 ms to inflate, 39 to 55 ms for
UTF-8, 65 to 105 ms for `jsonDecode` and 7 to 32 ms for
`NostrEvent.fromJson`.
- Many 16 to 90 ms stalls remain on both builds; they come from other
work (mostly markdown build and layout), not response decoding.
- Home, channels, inbox, search, profile and a thread render the same on
both builds.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 08:44:52 -07:00
Tom Brow
a7e65c91da feat(mobile): choose remote agent models and gate agents on Builderlab sign-in (#8650)
Follow-up to [#8626](https://github.com/block/buzz/pull/8626). Mobile
remote-agent creation now matches desktop.

- **Model and reasoning effort:** Create agent has Model and Reasoning
effort menus. Their options come from the remote service's catalog, and
nothing is preselected (same as desktop). The server checks the
selection against the catalog again before registering, and saves it on
the agent.
- **Builderlab gate:** creating a remote agent requires a hosted
community and Builderlab sign-in. The Agents directory, community bots
and the Bestie chooser stay available without it. Signing out closes an
open Create agent sheet; the Agents sheet stays open.
- **Enrollment:** `attest-agent` enrolls the agent in the requested
community, as on desktop, so the separate `add-buzz-subscription` call
is gone.
- **Quieter progress:** spinners replace the progress bars and the
"Working…" label. Status and success text ("is ready", "Remote agent
deleted.", setup steps) is gone, and only failures show text. Refreshing
an already-loaded agent list no longer blanks it.
- **Shared setting menu:** `BuzzSettingMenu` is a bordered setting row
that opens a Material 3 menu spanning the row's width. Opening it
dismisses the keyboard. The channel-TTL setting in Create channel uses
it too.

Agent avatars and QR/link-based creation follow in stacked PRs. Editing
existing agents comes later.

| Agents | Create | Model menu |
| --- | --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--1-agents.png"
width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--2-create.png"
width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--3-model-menu.png"
width="240"> |

### Validation

- Checked on the staging iOS simulator with a Builderlab account: the
Agents list, the Create sheet, and the model and effort menus (including
with the keyboard open).
- Added tests: catalog-gated model selection (including switching models
clears an unsupported effort), the Builderlab sign-in gate, controller
creation and resume with a model, a failed listing reported instead of
an empty registry, and create sheet busy and failure states (including
the catalog reload after a rejected create).
- The hosted-only agent tests are skipped without Builderlab defines,
which `mobile-test` doesn't pass. They pass locally with the staging
defines (20/20).
- Not covered: Android.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
2026-10-29 08:43:45 -07:00
klopez4212
39b8b2b6a6 feat(mobile): unify Messages navigation with Solo and Team filters (#8649)
## Summary

Unify mobile conversations in Messages with All / Solo / Team filters, a
collapsing header, and separate Search, New, and default-agent
shortcuts. Move workspace actions into the community menu and keep
collapsed sections intact across filters.

Align Inbox and list typography, spacing, and muted controls. Preserve
native glass layering through navigation and give every filtered list
enough clearance above the footer. The default-agent shortcut reads the
shared encrypted preference and opens an existing chat immediately. When
unset, it opens Agents; a star action selects the default through a
production signed, self-encrypted preference writer. Save failures
remain visible and retryable.

Android now uses rounded navigation controls and filters, optically
balanced search icons, and expanding New/community menus with leading
Tabler icons matching iOS. Preset avatars retain transparent WebM
animation through an Android WebView renderer, with lifecycle pausing
and poster fallback.

### Related issue

None found in searches for mobile navigation issues and open PRs.

### Testing

- Android follow-up: ARM64 debug build, Dart analysis, and emulator
checks passed for menu placement, selection, icon alignment, search, and
avatar playback/pause/resume. The avatar implementation passed 3,840
Flutter tests and native Android tests before subsequent styling
iterations. Automated tests for the later menu/icon iterations were
skipped at the author’s request; those earlier results do not cover the
final styling changes.

- Mobile formatting/analysis and full test suite (3,838 passed, 23
skipped, plus 4 push-configuration tests); regression journey covers
filtering, collapse retention, and scrolling the last Solo row above the
footer.
- iPhone 18 Pro simulator (Honeycomb): filters, menus, Inbox,
Invite/Back, Switch Community, and default-agent chat/Back exercised.
iOS release build and Pairing Preview install/launch passed during
iteration, preserving the saved profile.
- P1 follow-up: production writer-to-reader widget journey covers
selection, publish failure/retry, and the unset footer opening Agents.
Simulator Agents open/close passes; its saved community has no
selectable agents, so live selection was not verified there.
- Independent agent review completed; its Solo footer-clearance finding
is fixed. The final clearance adjustment is covered by widget tests;
physical-device acceptance of that adjustment and hosted CI remain
pending.

Final Messages layout:

![Messages with All, Solo and Team filters and separate bottom
shortcuts](https://github.com/user-attachments/assets/562ddc3e-945b-4945-bd58-217a2c62b570)

Switcher layering during iteration (before → after):

| Before: filter edge showing through | After: Home hidden behind
switcher |
| --- | --- |
| ![Filter edge visible beside Switch
Community](https://github.com/user-attachments/assets/b32a0157-8b17-4f01-9a23-29b12ce3b1af)
| ![Clean Switch Community
screen](https://github.com/user-attachments/assets/222956e4-9989-4c8f-a162-65de45029845)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 14:27:22 +00:00
klopez4212
7c165c3718 Smooth profile avatar editor transitions (#8640)
Smooths profile avatar editing with concurrent 200 ms entry/return
transitions, fixed-size circle movement between tabs, and a crossfade to
the dotted placeholder on cancel. New empty avatars open on Emoji, ready
to save.

Review fixes: keep the suggested emoji separate from the Image fallback,
and preserve native editor transparency across appearance changes.
Regression journeys cover empty Image previews, retained Emoji
selection, and light/dark updates.

Validation: focused Flutter journeys pass; restoring the old baseline
fails both fresh-editor regressions. At `bffb83f5e`: full Flutter suite
and push checks passed; all 107 native tests passed on the iOS
simulator. Prior iPhone visual approval covers the transition work;
these review fixes still need a device check.

Follow-up to #8628.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 08:16:17 +00:00
Tom Brow
82b2781376 fix(mobile): rebuild buzz_secp256k1 when vendored headers change (#8664)
Follow-up to #8654, from [Kalvin's review
note](https://github.com/block/buzz/pull/8654#discussion_r4407146414).

`native_toolchain_c`'s `CBuilder` registers only the listed C sources
(and files under `includes`) as build-hook dependencies. The
`buzz_secp256k1` hook lists four sources and no headers, so after an
initial build a header-only vendor update (for example
`src/modules/schnorrsig/main_impl.h`) could reuse the old library in
incremental app and test builds. The hook now also registers every `.h`
file under the vendored tree. It does not add include paths, so
compilation is unchanged.

### Validation

- With the change, editing the content of
`third_party/secp256k1/src/modules/schnorrsig/main_impl.h` makes the
next `dart test` rebuild `libbuzz_secp256k1.dylib`. Without it, the
library is reused unchanged.
- The hook output now lists all 62 vendored headers as dependencies.
- Package tests pass (27).

buzz-review-completed (Codex and security review clean; agent
verification above; build-only change, no app behavior to test)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:56:35 -07:00