Commit Graph

51 Commits

Author SHA1 Message Date
Tom Brow
a1efbf63bd fix(mobile): delete composer chips as one unit (#9133)
In the mobile composer, Buzz links (channel, message, repo, PR, issue)
and selected agent `@mentions` render as chips, with the source text
kept as invisible characters behind the chip. Backspace right after a
chip removed one of those hidden characters, so the chip turned into a
broken raw URL or partial name with no visible change for most of the
presses.

This PR gives chips one consistent editing model:

- **A link is a chip only once something follows it.** Its pattern runs
to whitespace or the end of the text, so in practice that is any time
the caret isn't at its end. Pasting exactly one link adds a space after
it (unless whitespace already follows), so it becomes a chip straight
away.
- **A link shows its raw URL while the caret is at its end.**
Backspacing the space after a chip, or moving the caret back past it,
reveals the URL for editing, one character at a time. Moving the caret
away turns it back into a chip. A caret at the link's start leaves it a
chip.
- **Agent `@mention` chips stay atomic.** Backspace right after one
removes the whole mention.
- **Chips are atomic for selection and forward delete.** A caret placed
inside a chip moves to its nearer edge, arrow keys step over it, and a
selection that covers part of a chip grows to cover all of it. Forward
delete just before a chip removes the whole chip. Typing, pasting,
deleting or cutting over a selection replaces exactly the (snapped)
selection.

The controller records each chip's source range while it builds the
spans, so only what is actually drawn as a chip behaves as a unit. Text
in code spans and IME compositions are never rewritten. Undo restores
the previous text: edits at the end of a revealed link are plain text
edits, so undo no longer looks like a backspace into a chip.

Human `@mentions` aren't chips today and are unchanged.

After this change, in the simulator: a link becomes a chip once a space
follows it, backspace reveals the raw URL, the next backspace removes
one character, and the arrow keys reveal the link and turn it back into
a chip.


https://github.com/user-attachments/assets/1d051dd7-92fa-47c9-b569-a7ef5e00ca6f

<img
src="https://github.com/user-attachments/assets/9980aa01-bcf2-4a1b-9c25-4349ce999e26"
width="300" alt="Composer with a channel link chip after a space"> <img
src="https://github.com/user-attachments/assets/5dfbaa44-a0f4-4917-b7ca-eeeb63e47dae"
width="300" alt="After one backspace the raw URL shows"> <img
src="https://github.com/user-attachments/assets/1a675d56-7310-4b13-9dd9-caa26ff99283"
width="300" alt="The next backspace removes one character">

### Validation

- Added tests: the composer permalink-chip test now covers the boundary
space and reveal (soft and hardware backspace, arrow keys), caret and
selection snapping, forward delete, paste adding a space, IME
composition, and undo after typing at the end of a link or after
completing a link. The agent-chip test covers whole-mention backspace,
selection snapping over a mention and a mention in a code span. Breaking
each rule (reveal, snapping, paste space, whole-chip delete, widening
only inside one chip) fails a test.
- Focused existing validation: the full mobile suite (`just mobile-check
mobile-test`) passes.
- Automated app run: iPhone 17 Pro simulator, iOS 27, debug build of
this branch, in a private test channel. Typed a Buzz channel link and a
space (chip), backspaced twice (raw URL, then one character removed),
retyped the character and space (chip again), then moved the caret left
past the space (raw URL) and right again (chip). Nothing was sent. The
run covered the composer only; the change touches nothing outside it.

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: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:29:17 -08:00
Tom Brow
ec21753e1b Make mobile New DM and Add members pick recipients like mention autocomplete (#9065)
The mobile New DM picker listed the first 50 relay profiles
alphabetically, mostly identities the viewer doesn't know. It ran its
own debounced directory search with no cache, and its rows disappeared
on every keystroke until the search returned. Mention autocomplete in
the composer already solves the same problem well, so New DM and Add
members now use its implementation. Add members was reusing the same New
DM directory search.

### What changes

- **Same candidates and order as mention autocomplete.** The chooser's
candidate building, ranking, row stability, 200 ms debounced search and
search cache now back both surfaces. New DM is treated like a stream
with no members yet: the viewer's own agents and agents shared with them
appear up front, people are found by name, and agents that couldn't
answer the viewer are left out. Typing a name shows the same people in
the same order as `@name` in a stream.
- **Results stay on screen while a search runs.** A query searched
before shows its people immediately, and a longer query starts from the
shorter query's results. A failed search keeps its rows and adds a retry
row below them.
- **Presentation matches the mention rows.** Rows show the name, and
agents show "agent · managed by …". Npub lines are gone.
- **Sheet chrome.** The title is "New DM", like the menu item that opens
it. Both pickers keep their grouped cards but drop the "People and
agents" and "Selected" section titles and the empty header bands above
them (selected recipients stay in their own card at the top with
checkmarks on iOS, as chips on Android). The native iOS picker now
focuses search once it appears, which iOS used to ignore. In New DM a
pick clears the query, as an inserted mention ends its query.
- **Add members keeps what is specific to it.** Existing members and
members just added are never offered, names are still compared with the
channel roster so a same-name candidate is told apart, the query stays
after a pick so several matches can be added in a row, and agents that
don't answer the viewer can still be added, since a channel can hold
them.

While the WebSocket reconnects, the relay agent directory and its owners
keep their last read in the community instead of going empty, so shared
agents already found stay eligible in both New DM and the composer
chooser.

The debug-only offline preview directory and the old directory listing
and search providers are removed, since no picker reads them any more.

### Validation

Added tests: the New DM widget journey now drives the real mention
search through a fake relay session. It covers the empty-query state,
the debounce, rows kept while a longer query searches, cached results
shown again at once, selection, chips and submit. The layout journey
covers the 50-row cap, and the native-bridge journey covers title, rows
without key detail lines, the selection limit and failure handling. The
full `mobile/test/features/channels` suite passes.

Automated app run (iPhone 17 Pro simulator, iOS 27, debug build, Remote
Agents Pilot community): opened New DM from the menu, confirmed the
keyboard comes up with only the viewer's own agents listed, searched for
people and an agent, selected both and confirmed the query cleared after
each pick. Also exercised the composer mention chooser (same results and
order for the same query) and the shared Add members sheet (plain style,
autofocus, search, query kept after a pick). Captures show real
community members, so they're shared internally rather than here. The DM
was not actually created, to avoid a stray conversation; submit is
covered by the widget tests.

🤖 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-11-04 13:55:26 -08:00
Tom Brow
f09956179d fix(mobile): keep the composer focused through iOS inactive spells (#9055)
On iPhone, opening Control Center or the app switcher while the composer
is focused can leave the keyboard up over a collapsed composer. The
orphaned keyboard then stays put while you navigate, for example back
from a thread to its channel, and it clips the composer pill.

### Cause

iOS reports a zero keyboard inset while the app is inactive and restores
the keyboard natively when the app returns. The composer treated any
inset drop as a user dismissal: it collapsed, unfocused and removed its
text field. When the keyboard came back, it was attached to no Flutter
field, so nothing could dismiss it. Flutter's own report of the native
side is
[flutter/flutter#189051](https://github.com/flutter/flutter/issues/189051).

### Change

The composer's keyboard observer ignores metrics while the app is not
resumed. iOS reports `resumed` with the inset still at zero and brings
the keyboard back a few hundred milliseconds later, so when a keyboard
that was up hasn't returned at resume, the composer waits a one-second
grace before treating it as dismissed. Any nonzero inset in that window
cancels the wait. A restored keyboard leaves the composer expanded and
focused; a keyboard that really stayed down still collapses the composer
once the grace passes.

### Validation

- Extended the existing "returns to the compact capsule when the
keyboard drops" widget journey with both inactive cases in the order iOS
reports them: resumed at zero with the keyboard returning about 400 ms
later (composer stays focused), and resumed with the keyboard staying
down (composer collapses after the grace). It fails without the fix and
against the first revision of this PR.
- Full mobile check and test suite pass.
- iOS 27 simulator (iPhone 17 Pro, debug build of the first revision):
focused the composer, typed, switched to Settings and back, opened and
closed Control Center, sent a message, and left the channel. The
composer stayed focused through each interruption, sending kept focus,
and leaving dismissed the keyboard. Limitation: the simulator keeps the
keyboard inset through these interruptions, so it reproduces neither the
original bug nor the resume grace; the on-device inset drop is from the
reporter's screen recording and the Flutter issue.

Human app testing is waived under the mobile testing exception; the
simulator pass above is the automated full-app run.

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-11-04 13:54:12 -08:00
klopez4212
0bdad8ea96 mobile: simplify verified profiles and prioritize full names (#9051)
Simplifies mobile identity presentation: removes verification checkmarks
from messages, threads, mentions and profiles, plus
issuer/time/disclaimer copy. Profiles show the community-verified full
name as the title and the display name below, with fallback when no full
name exists.

Validation: mobile formatting/analysis, full Flutter suite (4,401
passed, 45 skipped), all 4 unconfigured-push checks, and pre-push hooks
passed. Independent review found no blockers. Device and human visual
checks pending.

<details>
<summary>Before / after snapshots</summary>

Real Flutter profile widget with synthetic data, light/dark themes;
widget previews, not device screenshots. Images are PR attachments only.

| Theme | Before | After |
| --- | --- | --- |
| Light | ![Before,
light](https://github.com/user-attachments/assets/d615c010-7f04-4480-ae5e-e11539abc860)
| ![After,
light](https://github.com/user-attachments/assets/5726e504-349c-4c74-afc5-5557b03061bb)
|
| Dark | ![Before,
dark](https://github.com/user-attachments/assets/29204e7c-840b-41b4-b916-784976e6cbc9)
| ![After,
dark](https://github.com/user-attachments/assets/6c399553-4d95-47e8-9a5d-a2d351598ab3)
|

</details>


<details>
<summary>Inline messages — before / after</summary>

Actual Flutter channel and thread widgets with synthetic verified-user
data; local widget previews, not device screenshots.

| Screen | Before | After |
| --- | --- | --- |
| Channel · light | ![Before: channel,
light](https://github.com/user-attachments/assets/e482b562-5b44-4e48-8a24-ea52e78a8936)
| ![After: channel,
light](https://github.com/user-attachments/assets/2dd3689c-3206-4bc0-84be-0828e0fcca84)
|
| Channel · dark | ![Before: channel,
dark](https://github.com/user-attachments/assets/3c848519-3b96-453f-8751-78c573211f13)
| ![After: channel,
dark](https://github.com/user-attachments/assets/cc781417-e7ef-4988-98b4-b562c075fff6)
|
| Thread · light | ![Before: thread,
light](https://github.com/user-attachments/assets/4117bd8b-de7e-45c7-8e5a-3b25ae600405)
| ![After: thread,
light](https://github.com/user-attachments/assets/b2f8bddb-3023-448b-88fa-a9bc1d3b2ec2)
|
| Thread · dark | ![Before: thread,
dark](https://github.com/user-attachments/assets/4b9fd1fc-0a53-41b0-804c-3f06cd7a2e03)
| ![After: thread,
dark](https://github.com/user-attachments/assets/b407c3c6-c67a-445f-8867-feff60ca5973)
|

</details>

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-04 16:00:52 +00:00
Tom Brow
cad7f1d53d perf(mobile): build composer channel names once per channel list (#8943)
## Summary

`_useComposerChannelNames` ran on every `ComposeBar` build. Each time it
built a lowercase-name to id map over every channel and joined every
entry into a string key, only so a `useEffect` could skip calling
`setChannelNames`. The composer rebuilds on every keystroke and keyboard
animation frame.

This memoizes the map and key on the channel list instance. The effect
is still gated on the content key, so an equal new list still doesn't
call `setChannelNames` again.

## Measurements

iPhone 12 mini, Profile build, Builderlab, raising the composer keyboard
in #4things: `_useComposerChannelNames` was 26 of 31 `ComposeBar.build`
CPU samples (about 26 ms of UI work across the 600 ms raise).

Limitation: I haven't re-measured on the device after the change. The
removal is shown by the test below.

## Testing

- New test: rebuilding the composer 5 times (keyboard insets and typing)
with the same channel list doesn't iterate the list again. It fails with
the change reverted (8 iterations instead of 3).
- New test: changing the channel list still updates the composer's
channel chips.
- `just mobile-check mobile-test` passes (pre-push hook).

🤖 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@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:00:19 -08:00
Tom Brow
985de41f14 feat(mobile): show NIP-FI verified names (#8858)
Stacked on #8700. Review only the last commit until #8700 merges.

NIP-FI relays can publish a relay-signed verified name for each member
who signs in with an organization assertion (kind 30382, specified in
#8745 and implemented in the relay in #8774). Desktop shows these names
in block/buzz-app#2005. Mobile showed only self-declared kind 0 names,
so a verified Block name looked no different from one anyone could set.

This adds verified names to mobile, following the spec and the desktop
client's validation rules.

**What people see**

- A verified member's name is the issuer-verified display name wherever
mobile resolves names through identity names (message headers, threads,
DM titles, member lists).
- Message headers and the profile sheet show a verification mark. The
profile sheet also shows the full name when present, "Verified by
{issuer}", "Last verified {time ago}", and marks the record as cached
when it hasn't been reconciled with the relay on this connection.
Verification is described as historical, never as current access.
- Nothing changes on relays that don't advertise
`federated_identity.public_profile`, and marks never come from kind 0 or
NIP-05.

**How it works**

- NIP-11 discovery decides whether marks may show: a successful response
without `public_profile` hides them (cached records are kept), and a
failed or malformed response keeps the last state. Discovery refreshes
on connect and every 5 minutes while names are tracked.
- Subjects are tracked as the profile cache resolves them, and fetched
in batches of up to 128 with `{kinds:[30382], authors:[self],
#d:[...]}`. Live updates and retractions apply on the same subscription.
A full batch that omits subjects is not treated as proof they have no
record.
- Each event's id and signature are verified (in an isolate) before use,
and it must be authored by the community's NIP-11 `self` key with
exactly the specified tags (plus the optional `full_name` the relay
emits). Names follow the spec's NFC and character rules; `unorm_dart`
(MIT, pure Dart) is added for the NFC check because Dart has no built-in
normalizer.
- The cache is per community and is cleared when the relay's `self` key
changes.

**Not yet covered**

- Some surfaces still read the kind 0 name directly: the members sheet
initial, and search and inbox rows. The mark isn't shown in search rows,
inbox rows, or member tiles. (The mention picker now matches, ranks and
inserts by the verified name and shows the mark, and it also offers
cached verified people outside the channel whose kind 0 name differs.)
- NIP-FI says clients MUST show `issuer_label` beside every verified
name. Here it appears only in the mark's tooltip and accessibility label
and in the profile sheet, and verified names reaching DM titles and
member lists show no mark or issuer. Follow-up: show the issuer inline
wherever a verified name is shown.
- Live updates are coalesced to the newest per subject before signature
proof, ordered by unverified `created_at`/`id`. Only a misbehaving relay
could exploit this (the subscription is `authors:[self]`), and it could
already withhold events. Follow-up: prove first, then coalesce among
valid events.
- The verified `username` isn't shown beside the name in message headers
(the NIP-05 handle still is).
- Collision qualification relies on the existing name-disambiguation
policy rather than the spec's exact self-declared-name rule.

### Validation

Added tests:

- Validation: discovery enabled, disabled and malformed; wrong author;
mismatched or duplicate `d`/`p`; extra or duplicate tags; invalid
usernames; non-NFC and whitespace-padded names; `verified_at` after
`created_at`; bad signatures; retractions.
- Directory: discovery gating, batching, truncation, live retraction,
reconnect reconciliation, and disable hiding cached records.
- Directory failures: a pre-EOSE flood, failed proof retried on refresh,
oversized events (by total size, id or signature) dropped before proof,
live events coalesced, a retry before or after the first EOSE never
proving absence, a retry during proof not promoting records to checked,
and a newer signed malformed projection clearing the older one.
- Mention picker: a verified name finds, ranks and inserts the verified
person; the self-declared name still finds them; a verified person
outside the channel is found by verified name even when an impostor
copies that name into kind 0.
- Identity names: a verified name outranks the kind 0 name.
- UI: the mark in message headers and the profile sheet details, no mark
for a cached record, and a single accessible label.

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

**Live relay (checked 2026-11-01):** not yet run end to end.

- Staging now advertises the feature: the bb-block staging relay's
NIP-11 (`buzz.block.builderlabstaging.xyz`) has
`federated_identity.public_profile` with `kind: 30382` and
`multiple_issuers: false`, bound to `self` `f7f4df94…dcf1`. So #8774 is
deployed and enabled there. The other staging relays
(`communities.public.builderlabstaging.xyz` and its `team.` host)
advertise no `federated_identity`, and the client shows nothing on them.
- I couldn't confirm that real 30382 events are served or render them.
The relay requires NIP-42 auth plus a client-attached NIP-FI assertion,
which needs an interactive Builderlab/Okta sign-in on a physical device.
SSO doesn't work in the iOS simulator. The remaining step is a device
run signed in to the staging Block community, with screenshots of a
verified name, its mark and its details.

**Shipping behind discovery gating:** this is safe. The client reads and
shows verified names only when a successful NIP-11 discovery advertises
`public_profile` bound to `self`. It accepts only kind 30382 events
signed by that key that pass id and signature verification and strict
shape checks. On every relay without the feature, which is every relay
but bb-block staging today, it shows nothing new and changes no names. A
failed or malformed discovery never turns the feature on, and it marks
cached records as unchecked, which hides their mark. The subscription
and reconciliation paths are covered by the tests above, and Carl's
review reran retry probes through the real `RelaySessionNotifier`.

🤖 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 Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 15:51:25 -08:00
Tom Brow
f6f6d9a673 chore(mobile): delete dead code with no remaining UI path (#8880)
## Summary

Deletes mobile code that no longer has a user-reachable path and has no
counterpart in buzz-app, so it is unlikely to come back for parity. Net:
104 files, about 5,500 lines removed. No intended behavior change.

- **iOS native message menu and action surface:**
`NativeMessageMenu.swift` and `NativeMessageActionSurface.swift`, the
`message`/`supportsMessage` coordinator branches, and the Dart selection
hook. Dart stopped creating this view in
[#8280](https://github.com/block/buzz/pull/8280). The native reactions
sheet in the same coordinator is kept.
- **Old home quick-actions launcher and `ComposeMorphIcon`:** only
instantiated in tests since the nav-bar creation actions replaced them.
- **Retired local theme stack:** `ThemeNotifier`, `AccentNotifier`,
`SchemeNotifier`, `adaptive_theme.dart`, `AccentPickerPage`, the
appearance-mode sheet and their helpers. These predate per-community
theme sync. The legacy prefs migration in
`community_theme_preference.dart` is kept.
- **Unused widgets:** `ProfileAvatar` and `MaskedAvatarBadge`.
- **Push permission-status plumbing:** the iOS
`notificationAuthorizationStatus`/`openNotificationSettings` handlers,
plus the uncalled strict age-gate snapshot registration and its Swift
`syncAgeGatePushSnapshot` path.
- **Orphaned files** with no importer: `profile_text_edit_sheet`,
`editor_option_button`, `retained_page_transition`,
`tappable_flapping_bee`, `agent_model_info`, `community_bot_profiles`,
`sheet_divider`, `success_haptic`.
- **Uncalled helpers:** the `pickAndUpload*`/camera helpers in
`media_upload.dart`, unused `NostrFilters` builders,
`mediaGetHeadersFor*`, and about 20 other uncalled functions, providers
and styles.
- **Removed-UI leftovers:** the never-wired invite directory providers,
`AuthNotifier.signOut` and `removeActiveCommunityForSignOut` (the Sign
out row is gone), `renameCommunity`,
`HomePage.profilePageBuilder`/`SettingsPage.embedded`, and the
create-forum branches of the create-channel sheet. Forum viewing is
unchanged.

Tests that only covered deleted code are removed. Tests of surviving
code that used deleted helpers as drivers were repointed to the
surviving API.

An audit of `mobile/android` found no dead Kotlin, resources, manifest
entries or Gradle dependencies, so Android code is unchanged.

## Validation

- `flutter analyze`: no issues.
- After rebasing onto main
([93b8b8f](https://github.com/block/buzz/commit/93b8b8febc)), `just
mobile-check` is clean (format and analyze), and `just mobile-test`
passes (4,070 passed, 35 skipped, 0 failed, plus the push-unconfigured
and secp256k1 suites).
- `dart format`: clean.
- Android: `just mobile-build-android` builds the debug APK, and
`./gradlew :app:testDebugUnitTest` passes.
- iOS: `build-for-testing` succeeds and BuzzPushKit `swift test` passes
(65 tests). In RunnerTests, 97 of 98 pass.
`testNativeNavigationTitleExpandsAgainAtTop` fails, but it fails the
same way on unmodified `main` (acf56fff6f) on the same machine (Xcode
27, iPhone 17 Pro simulator), so it is not caused by this change.
- Agent review of the diff ran. Its one blocker (stale `project.pbxproj`
entries for the deleted Swift files) is fixed.
- In-app smoke run at
[66141c1](66141c10b9):
a debug build on a fresh iPhone 17 Pro simulator (iOS 27), signed in to
the internal community. The following flows all worked with no errors:
- Home: the New menu (channel, DM, workspace, chat) and the
create-channel sheet, including its channel-type options.
- A chat, with the message action sheet (reactions row, Reply, Copy
link, Remind me, Mark unread, Copy text, Edit and Delete).
  - Appearance: the previews, and cycling the System and Light modes.
- Settings: the profile header, the display name editor, and Edit photo
(Image and Emoji tabs).
  - The Agents directory.
- Search, including the Channels filter, plus opening a team channel
from the results.
- Tapping a reaction summary, which opens the native iOS Reactions
sheet.
- The recording isn't attached here because it shows internal channels
and messages.

🤖 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@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:09:50 -08:00
Tom Brow
53ca047cd3 fix(mobile): keep mention rows still and cache directory searches (#8875)
Typing `@s` in a stream showed the matching channel members, then about
half a second later added people from outside the channel. The chooser
sits on the composer and grows upward, so each late row pushed every
shown row up and a tap could land on the wrong person. It happened every
time, even for a query typed moments earlier, because each new `@`
started its directory search from nothing.

### Changes

- **Search cache.** Finished directory searches are kept per community
and signer, in a small LRU keyed by query (`MentionSearchCache`). A
query searched before shows its people at once while a fresh search runs
in the background. A query not searched yet starts from the longest
searched query it extends (`@si` starts from `@s`), since a name-prefix
search for a longer query finds no one the shorter one missed (up to the
search's result limit). This replaces the per-opening "last search" that
reset on every new `@`.
- **Rows hold still.** While a query's directory search can still add
people or a retry row (`mentionSearchPendingProvider`), the chooser
reserves its full height with rows at the top, so late rows fill empty
space instead of moving shown rows. This includes the background refresh
of a cached query. The reservation lasts until the query or the opening
changes, so the panel never shrinks under the finger either.
- **Three rows, with progress.** The chooser has a fixed three-row
height (fewer rows at larger text sizes) before it scrolls, and never
taller than the space above the composer (so a short view, such as
landscape with the keyboard up, keeps its rows on screen). While the
search runs, a thin bar overlays its top edge without changing its
height.

The trade-off: while a query's search runs, the panel is full height
even when it ends with one or two rows.

### Demo

iPhone 18 Pro simulator, local relay. Sam and Sasha are channel members;
Saba, Sierra and Skyler come from the directory search.


https://github.com/user-attachments/assets/efae33ce-ee65-447b-9402-b0741c8dc2da

### Before merge

- [x] Kenny Lopez reviews the visible changes ([approved in Buzz
#buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)).

### Validation

- New tests: a cached query reopens with its people at once, stays
pending while it searches again, and clears when that search finishes; a
longer query starts from its prefix's people; an uncached query is
pending until its search finishes; and a compose bar journey step checks
that a late directory person leaves the shown row's position and the
panel height unchanged, both on a new query and on a cached query's
refresh. Other new tests cover the three-row cap, the progress bar, and
an 844×390 landscape view with the keyboard up, where the reserved
chooser previously pushed the only row above the screen. Disabling the
cache lookup fails the two cache tests, and disabling the reservation
fails the layout tests (the cached-refresh case moved a row 59pt).
- `just mobile-check` and `just mobile-test` pass.
- Exercised on the simulator against a local relay as shown above.

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

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-authored-by: mbp <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:55:46 -08:00
klopez4212
634f48a8bd Mobile: open Solo chats with Bestie and workspace controls (#8884)
Mobile New chat now opens directly with the synced Bestie selected. The
first send creates and names the chat; leaving an empty draft creates
nothing. New and existing Solo chats can select or clear a workspace
from the header popover, with a checkmark for the active choice and a No
Workspace option. Workspace and agent controls have separate iOS glass
surfaces, Bestie stays at the far right, and Back uses a chevron. The
agent sheet fits its list, with scrolling for long lists. Workspace
creation stays in the Home New menu.

The New menu groups Solo above Team. Android expands across the full
control row; iOS keeps its native menu while Search and Bestie merge
into New through Liquid Glass, with synchronized icon motion and the
residual glass fading away. Empty workspaces use a compact, muted
message.

Validation: 4,055 mobile tests passed (34 skipped), static analysis
passed, and native menu checks passed. Earlier physical-device previews
and the iPhone New-menu animation were visually approved before this
consolidation.

This PR consolidates the controls previously developed in #8891 into the
existing draft/first-send implementation. The New menu animations,
attachment handling, and durable retries remain in this PR. The combined
controls have widget interaction coverage; the combined build has not
yet had a new physical-device walkthrough.

Current combined controls below are Android Flutter widget renders with
fixture data, not device screenshots. Images are GitHub attachments
only.

| New conversation | Workspace popover | Compact agent sheet |
| --- | --- | --- |
| ![New chat
composer](https://github.com/user-attachments/assets/f9ea2a16-342e-4cea-a436-0777ff5e37aa)
| ![Checked workspace
popover](https://github.com/user-attachments/assets/80eb7a71-e424-4188-86d7-eb6292b148bf)
| ![Content-sized agent
sheet](https://github.com/user-attachments/assets/be608383-0188-4339-b56b-924085137452)
|

Empty workspace widget preview (unchanged):

![Compact empty workspace
message](https://github.com/user-attachments/assets/387c0729-dea3-4250-bb54-afc4e61b54a7)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-01 21:40:30 +00:00
Tom Brow
543ecf2b1f fix(mobile): list the same owned agents as desktop (#8857)
## Summary

Mobile's Agents modal listed agents that desktop does not show. Most
were agents created in the earlier Buzz app (this repository's
`desktop/`), which the owner had not imported into Buzz 1.0
([block/buzz-app](https://github.com/block/buzz-app)). @ completion
offered them as well.

Desktop lists an owned agent only when the owner's newest kind:30177
record for that agent has a `pluginId/typeId` `runtime` tag and a name.
Records written by the earlier app have no `runtime` tag. Importing an
agent on desktop republishes its record with one
([`listingsOf`](662e91800a/src/features/agents/service.ts (L1413-L1460))).
Mobile now applies the same rule:

- **Agents modal:** shows listed owned agents, plus the account's remote
agents that have a verified owner profile here (so one stays listed
while a deletion that already removed its registration is retried).
Owned kind:10100 directory entries no longer add agents on their own.
- **@ completion:** channel members are always offered. Listed owned
agents are offered in streams and forums, where they can be added, as on
desktop. An owned agent that isn't listed is no longer offered from the
directory or from search, because mobile can't import it. The check that
runs before sending a restored draft follows the same rule.

The owned-agent directory moved to `lib/shared/agents` so the channels
feature can use it.

Shared agents owned by other people still come from the kind:10100
directory, which desktop doesn't use for @. That difference is unchanged
here.

### Related issue

None found.

### Validation

iPhone 17 simulator, debug build, against a local relay seeded with one
owner and two owned agents. Both agents have a NIP-OA profile. "Current
Agent" has a desktop runtime tag. "Old App Agent" has a registration
written the way the earlier app writes it, with no runtime tag. The
before captures are `origin/main` with the same data.

| | Before | After |
| --- | --- | --- |
| Agents modal | ![Agents modal before: Current Agent and Old App
Agent](https://github.com/user-attachments/assets/9089a820-9b30-4372-a133-7a0aa00fb6d8)
| ![Agents modal after: only Current
Agent](https://github.com/user-attachments/assets/f0585e0c-1bbf-4605-910f-90f6f9dc033a)
|
| `@` in a stream | ![Mention list before: only the
member](https://github.com/user-attachments/assets/aa88a5d6-5dec-41e9-aebd-07a62d06e48e)
| ![Mention list after: the member and Current
Agent](https://github.com/user-attachments/assets/d4de914d-23a4-4437-83b7-4b23dd641aac)
|
| `@Old` in a stream | ![Search before: Old App Agent
offered](https://github.com/user-attachments/assets/e038eb64-1fed-40f8-960b-18d6fad0a86f)
| ![Search after: nothing
offered](https://github.com/user-attachments/assets/47246f69-e939-402d-acfc-d0c764df967c)
|

Added tests:
- Directory loading: remote, desktop, earlier-app, later-imported, and
reverted agents, plus agents owned by someone else; only the newest
record counts. A table test covers the runtime and name rule.
- Agents modal widget test: an earlier-app agent is not shown.
- Mention candidates: only listed owned agents are offered outside the
channel, including from search.
- Compose bar: a restored draft that mentions an unlisted owned agent
outside the channel is rejected before sending.

I disabled the runtime check and the unlisted-agent filter, and the four
new guarding tests failed. The full mobile suite passes.

Human test: not applicable for this mobile change under the author's
standing exception.

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-11-01 07:36:37 -08: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
Tom Brow
4ff02bf03b perf(mobile): sign events with native libsecp256k1 on iOS and Android (#8654)
Mobile signs every Nostr event (sends, relay AUTH, media auth headers,
preference writes) in pure Dart on the UI isolate. On an iPhone SE (2nd
gen, Profile build) one BIP-340 signature costs about 10.7 ms and
deriving the public key another 5.6 ms, so a burst of signed writes
drops frames.

This adds `mobile/packages/buzz_secp256k1`, a small FFI package that
builds vendored bitcoin-core/libsecp256k1 v0.8.0 (ecdh, extrakeys,
schnorrsig modules) through a Dart build hook, so the same C and the
same Dart API ship on iOS and Android with no platform bridge code. All
13 `nostr.Event.from` signing sites now go through `signNostrEvent`,
which keeps the `nostr` package's event-id serialization and moves only
key derivation and signing to native code. Calls stay synchronous.

The package also exposes `schnorrVerify` and `ecdhX`; follow-up PRs move
verification and the NIP-44 conversation key over. Incoming relay events
are still not verified, unchanged by this PR.

Safety: the signing context is randomized once per isolate, secret keys
and keypairs are wiped from the native scratch buffer after each call,
aux randomness comes from `Random.secure()`, and the static verify
context runs libsecp256k1's self test before first use.

### Validation

- Added tests: BIP-340 official vectors and native-vs-pure-Dart
differential tests in the package; `event_signing_test.dart` signs with
64 random keys and checks pubkey, event id and signature against the
existing `nostr` builder and verifier, plus malformed-key rejection.
- Full mobile analyze and test suite pass locally.
- Builds: iOS release (framework ~1.3 MB) and Android debug APK with the
library for arm64-v8a, armeabi-v7a and x86_64 (~1.2–1.3 MB each).
- On-device (iPhone SE 2nd gen, Profile, median per op): sign 10,736 µs
→ 41 µs, derive pubkey 5,580 µs → 16.5 µs, full signed event 10,744 µs →
65 µs. Event-id JSON and SHA-256 are 23 µs, so they stay in Dart.
- Device, iOS (iPhone SE 2nd gen, Profile): `buzz_secp256k1.framework`
embedded; the signed-in app authenticated to a real community relay,
loaded channels and history, and paged older messages. No native-asset
or secp256k1 errors in the console.
- Device, Android (emulator, debug): APK contains `libbuzz_secp256k1.so`
and loads it. Against a local mock relay that verifies with the
pure-Dart `nostr` package, the app's NIP-42 AUTH, NIP-98 `POST /query`
headers, and kind 20001/30078 events all verified.
- Blossom GET auth, iOS: the dev relay returns 401 for unauthenticated
`/media/` GETs. From inside the running app, the production
`MediaGetAuthService.headersFor()` header for a nonexistent hash got 404
(accepted, file missing), compared with 401 without it. The community
has no relay-hosted image, so none was rendered.


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-28 16:28:26 -07:00
Tom Brow
b310fea05b mobile: find DMs opened ahead of the list everywhere (#8646)
## Summary

This follows up [#8638](https://github.com/block/buzz/pull/8638) and
covers the optional review notes from that PR.

- **Shared lookup.** A new `channelByIdProvider` returns the channel
from the list, or the copy remembered from a fast DM open when the list
doesn't have it yet. The mention picker, mention search failure state,
compose bar DM check, thread page, and message unread action all use it.
Before this change, those surfaces only searched the list, so a DM
opened before the list loaded was treated as an unknown channel. For
example, the mention picker skipped directory search and never labeled
anyone "not in DM".
- **Eviction and scope.** Opened-ahead DMs live in a notifier scoped to
one relay and identity, and `channelByIdProvider` watches it. Once the
list includes a remembered DM, its opened copy is dropped. Remembering
also skips a DM the list already has, since the open's background
refresh can list it first. A DM hidden or left later, or one from
another community or identity, is no longer found through a stale copy.
- **Call-site tests.** Search, the profile Message button, and the New
message sheet now assert that they open the DM with `waitForChannelList:
false`.

## Validation

- `just mobile-check` and `just mobile-test` pass.
- The journey test "a DM opened before the channel list loads sends
mentions" now also checks the outside-person picker in a DM that isn't
in the list yet: a failed directory search shows Retry, then the result
appears labeled "not in DM". Mutation checks: reverting the lookup in
`mentionCandidatesProvider`, `mentionSearchFailedProvider`, or the
compose bar fails the test.
- New tests "a DM opened ahead of the list is not found after it is
left" (including the refresh-before-remember order) and "a DM opened
ahead of the list stays in its community" (a mounted lookup across
community and identity switches). Mutation checks: removing the
eviction, the listed check in `rememberOpened`, the relay or identity
watch, or the lookup's reactive watch each fails a test with `Expected:
null, Actual: <Instance of 'Channel'>`.
- Call-site assertions. Mutation check: flipping any of the three call
sites to `waitForChannelList: true` fails its test with `Expected:
[false], Actual: [true]`.
- Simulator (iPhone 17 Pro, local relay): I started a fresh DM with Sim
Tester from the New message sheet. It opened at once, and typing `@Pat`
showed Pat Peer from the directory, labeled "not in DM".

<img
src="https://github.com/user-attachments/assets/0fe68178-00b5-4533-99f8-fe5d03e8ed88"
width="300" alt="Fresh DM with Sim Tester: typing @Pat shows Pat Peer
labeled not in DM">

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>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:00:58 -07:00
Tom Brow
9e18d17a36 fix(mobile): open new DMs without waiting for a full channel reload (#8638)
## Summary

Starting a DM from the mobile **New message** sheet could look broken:
after picking a person and pressing Done, the sheet kept spinning, so it
seemed like the picker could only add people. The DM was being created,
but `openDm` reloaded the whole channel list (memberships, metadata,
hidden DMs, huddles, names, latest messages) before returning. On a
large account or slow connection that can take many seconds.

Interactive opens (New message sheet, Search, profile sheet) now open
the DM from its own metadata and roster, the same targeted read Solo
chat creation uses, and refresh the channel list in the background. The
sidebar still picks up the DM once that refresh lands. When the open
reopens a hidden DM, the fast path also reads the caller's visibility
snapshot in the same round trip and fails visibly if the relay accepted
the open but didn't unhide it, so Done can retry instead of opening a DM
the sidebar keeps hidden. Search and the profile sheet now push the DM
as an ordinary route, as New message already did, so it doesn't sit on a
spinner while Solo groups load. Hidden-DM resurfacing from Activity
keeps the old behavior (wait for the full list), because that reload is
what clears its hidden state and its retry coalescing depends on it.

The fast open also records the DM it read on the channel list notifier,
which falls back to it until the list catches up. Both composers
(channel and thread) look the DM up through it, so a selected mention in
a just-opened DM doesn't wait on, or fail against, the list, and thread
replies there keep their DM recipients.

`findJoinedChannel` now also carries the DM's participant pubkeys so the
opened DM gets its proper title ("Pat Peer", not "DM") before the list
catches up. It also queries metadata and roster separately instead of
under one shared limit, so a lingering pre-backfill metadata revision on
an older DM can't crowd out the roster.

### Related issue

N/A (reported in Buzz #buzz-mobile).

### Validation

- Added tests: `dm_open_latency_test.dart`, a regression test with the
full channel-list load stalled. The interactive open finishes within 2s
with the right channel id, DM flag, membership, and participants. It
publishes exactly one kind 41010, then the background list refresh runs
once the stall clears. The fake serves events like a relay (newest
first, one limit across kinds) and keeps a stale metadata revision
around; the shared-limit read fails it. The test also covers: a DM the
caller can't read fails visibly, a reopen whose unhide snapshot failed
fails visibly, the default (resurfacing) path still waits for the list,
and a community switch mid-read cancels the open.
- Added test (compose bar journey): with the channel list still loading,
a fast-opened DM sends a selected participant mention as a recipient
instead of failing "could not check who is in this channel". It fails if
the composer ignores the opened DM.
- Added test (thread): a thread reply in a DM opened ahead of the list
sends with the DM channel, so it keeps DM recipient semantics. It fails
if the thread page ignores the opened DM. The latency test also checks
the open records the DM before the list has it.
- Mutation checks: the test fails if the background refresh is removed,
if the default path stops waiting, if the interactive path waits, if
participant pubkeys are dropped, or if the still-hidden check is
removed.
- Added tests (Search and profile journeys): with Solo groups still
loading, tapping a person result or the profile's Message button opens
the DM with its composer, not a placement spinner. Each fails if its
call site drops the ordinary placement.
- Full mobile suite: `just mobile-check` clean, 3847 tests passed (23
skipped), plus the unconfigured-push suite.
- iOS simulator (iPhone 17 Pro) against a local relay: New message, pick
Pat Peer, Done. The sheet closed and the DM opened titled "Pat Peer" in
about 1s. Reopening the existing DM took about 0.8s. The DM appeared in
the sidebar after the background refresh. With 400 ms of added one-way
latency (800 ms RTT) through the local proxy, the same flow opened the
DM in 2.5s.
- Re-ran on the simulator at the final head (84444d2): a new DM with a
second user opened in 0.9s, reopening the existing DM opened in 0.8s,
and both appeared in the sidebar. The hidden-DM reopen case couldn't be
run in the simulator because mobile has no hide action, so it is covered
by the regression test only.
- Simulator at ea4fade: reopened the Pat Peer DM, selected a mention of
Pat Peer, and sent it; the message posted with the mention.
- Simulator at
[d7329ae](d7329aecd2):
reopened the Pat Peer DM from New message (about 1s), opened a thread,
selected a mention of Pat Peer, and sent the reply. It posted, and the
relay stored it with Pat as the `p` recipient.
- Simulator at
[1fe2e35](1fe2e35fc1):
searched for Sim Tester and tapped the person result. The DM opened with
its composer in about 1s.
- Limitation: I couldn't reproduce the reporter's account size, so there
is no before/after timing on a real slow account. The stalled-list test
is the before/after evidence for the latency.
- Mobile human-test step: N/A under the mobile testing exception.

| Person picked | Done: DM opens | DM in sidebar |
| --- | --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/021ab77313319e51606f75cc4c8070fc82a2bcbc/pr-8638--1-picker-selected.png"
width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/021ab77313319e51606f75cc4c8070fc82a2bcbc/pr-8638--2-dm-opened.png"
width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/021ab77313319e51606f75cc4c8070fc82a2bcbc/pr-8638--3-sidebar.png"
width="240"> |

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-28 10:03:41 -07:00
Tom Brow
819dd7924f fix(mobile): bump Flutter 3.47, resolving memory leak (#8419)
With accessibility semantics active, opening and leaving channels
retained disposed composer overlay states through Flutter’s semantics
traversal map. Those states kept departed route and message trees alive.
This upgrades the pinned Flutter SDK from 3.41.7 to 3.47.7, which
includes [the upstream
fix](3c96406cc4).

Includes SDK compatibility and formatter changes, preserves CocoaPods
integration, and gives create-channel radio lists a Material ink surface
required by the newer framework. Widget regressions verify both the
framework portal and the production channel/composer lifecycle: departed
routes, composer elements, and portal states are collectible while their
application’s semantics owner remains alive.

Related issue: no matching open issue or PR found.

### Validation

- [Current-head CI
passed](https://github.com/block/buzz/actions/runs/39085320351): all
mobile test shards, checks, Android APK, iOS release build, and native
simulator tests.

- Framework regression fails on Flutter 3.41.7 with three retained
states and passes on 3.47.7.
- Signed native Debug iOS Simulator build and paired-community
navigation replay: after 18 channel visits and forced GC, retained
disposed portal keys fell from 18 to 0 and route instances from 19 to 1.
- Review follow-up: all 365 focused channel/overlay tests, analyzer, and
complete mobile pre-push checks passed.
- Full local CI passed before the test-only review follow-up, including
Rust and desktop validation, frontend builds, analyzer checks, 3,499
mobile tests (four skipped), and the separate unconfigured-push test.

The replay used iOS 26.5 Simulator with accessibility semantics enabled,
CPU profiling disabled, and Debug/JIT builds. The before app was a
preinstalled emoji candidate; the after app uses this branch. Object
retention and the focused regression establish the fix; absolute heap
differences are not a controlled release-memory measurement. Android
native execution was not tested.

### Create-channel compatibility

The radio groups retain their layout after switching their container to
Material.

| Before (3.41.7) | After (3.47.7) |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/42525e1bd9d9f264d0142c34ec61dbb6937ef758/pr-8419--01-before.png"
width="280" alt="Original create-channel radio groups"> | <img
src="https://raw.githubusercontent.com/block/buzz/42525e1bd9d9f264d0142c34ec61dbb6937ef758/pr-8419--02-after.png"
width="280" alt="Updated create-channel radio groups"> |

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-18 12:44:43 -07:00
klopez4212
27b9c827fc Polish mobile message layout and navigation controls (#8427)
Align outgoing messages and galleries to the right, reclaim the outgoing
avatar gutter, and match desktop calendar dividers with balanced
spacing. Standardize navigation and sheet icons at 24 points, restore
native Edit/Done styling, and align section menus with trailing channel
indicators.

Validation: clean analysis; 3,513 mobile tests and push-configuration
checks passed; iPhone Release build installed and tested by the owner.
Includes regression coverage for message placement, date spacing,
gallery direction, and section alignment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: fin <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-18 15:17:25 -04:00
klopez4212
c8ea4fb4eb feat(mobile): polish Home navigation and Solo workspace (#8393)
## Summary
Polish mobile Home and Solo: group Search/Inbox beside a separate
profile avatar, move Inbox and Settings to bottom-up pages with Close
controls, and place tabs opposite the plus/compose action. Morph plus to
compose clockwise when entering Solo and reverse counterclockwise when
returning Home, with a directional quarter-turn of the icon inside a
level button and a subtle scale pulse, respecting reduced motion. Align
Search’s Close control with Inbox, simplify its placeholder, and
initialize selected filter contrast correctly. Collapse the plus menu
when leaving Home. Center Solo’s native-glass New chat empty state and
keep it stationary behind the keyboard.

Standardize Tabler icons and neutral backgrounds, retain SF Back/Close
on iOS, use native Liquid Glass headers, tab container, and actions
(solid grey selected tab, slightly darker on iOS) on iOS and black
actions on Android, and smooth the expanded menu corners. Repair native
header safe-area/return layout and compose movement after cancellation.

### Related issue
None found for this UI refinement; follows #8337.

### Testing
- Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3
push-configuration tests passed. File-size checks passed.
- Base UI built and installed on Pixel and iPhone in place. Pixel
navigation/New chat cancellation checked. Subsequent glass and rotation
refinements are previewed on iPhone; motion feel awaits human
confirmation.
- Draft pending human confirmation of the final Settings entrance/return
behavior on iPhone.

### Pixel snapshots
| Solo | Expanded actions |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png"
width="280" alt="Solo empty state with New chat" /> | <img
src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png"
width="280" alt="Expanded channel actions with smooth corners" /> |

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-16 12:28:02 -04:00
klopez4212
231a469de5 Refresh mobile message bubbles, reactions, and activity cards (#8281)
## Summary

Refresh mobile conversations with left-aligned grouped bubbles without
tails, tighter consecutive-message spacing, distinct sender colors,
theme-aware incoming backgrounds, and compact overlapping reaction
trays. Attach reply counts beside reactions with up to three participant
avatars; keep outgoing mentions and links readable in both themes.
Coordinate a 500 ms composer-to-message transition with spring
contraction and an anchored avatar. Message ownership follows the
signing identity so profile reloads cannot recolor outgoing bubbles.

Center channel activities without timestamps or icons, keep names
tappable, and show two affected members plus a **See all** sheet. Hide
legacy purpose activities, match media corners to bubble groups, and add
iOS status-bar scroll-to-top. This is an independent mobile PR; no
desktop changes or color picker.

## Validation

- Full `just mobile-check` passed; `just mobile-test` passed 3,275 tests
with 4 skips, plus 3 unconfigured-gateway checks.
- Fixed message-widget lint warnings and a stale notification fixture
that blocked analysis. Made the video-controller disposal test wait for
lifecycle events instead of fixed delays. Mobile pre-push checks are
enabled.
- Physical iPhone previews were installed throughout development. The
latest bubble, reply-avatar, contrast, and spacing updates were
installed and launched on the physical iPhone. Final motion and
status-bar behavior still need device visual verification; Android has
automated coverage but no physical-device verification.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-14 16:35:26 -04:00
klopez4212
bbcbd7ce13 Preview Tabler icons in the mobile app (#8109)
## Summary

Preview buzz-app's Tabler icon style in the mobile app, replacing
app-drawn Lucide glyphs across channels, messages, activity, search, and
settings. A shared `BuzzIcons` mapping uses the bundled Tabler 3.46.0
outline font, matching buzz-app's version, with its MIT license and
source documented.

Native UIKit controls retain SF Symbols. The former 300/500 navigation
glyphs use Tabler's standard outline; existing selection colors and
labels remain.

### Related issue

No matching Tabler mobile migration issue or PR found. Related native
iOS controls: #8055.

### Testing

- Pre-push mobile analysis, formatting, file-size checks, and the full
mobile test suite passed.
- 194 focused widget tests passed, including channels, home, search,
settings, and message content.
- Light and dark community-settings renders inspected.
- Signed iPhone Release build succeeded; installed and launched as Buzz
Tabler on a physical iPhone.
- Full `just ci` attempted; stopped during Rust clippy compilation
because the Mac ran low on disk space. Full repository validation is not
claimed.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-08 10:40:12 -04:00
Logan Johnson
9322289364 feat(mobile): follow the portable mention rules, including DMs (#8102)
🤖
## Summary
The mobile app's @mention chooser now follows the same rules as the
desktop app (buzz-app). The same typed text finds the same people in the
same order on both apps, and sending works the same way.

- **Same matching and order.** Mobile uses the shared mention rules and
runs their shared test cases. A multi-word search such as `@Mary J`
still searches the community directory, so it can find "Mary Jane" even
when she is not in the channel.
- **DMs can mention people outside the DM.** The chooser marks them "not
in DM". Nobody can be added to a DM, so the message sends without a
prompt. Outside people go out as references, so they are not notified.
This matches buzz-app #510.
- **Channels still ask.** In a stream or forum, mentioning someone
outside the channel asks "Invite" or "Do nothing" before the message
sends. A channel with no type tag counts as a stream.
- **Sessions do not search the directory.** Only session members appear.
- **A failed directory search stays visible.** The chooser shows the
error and a Retry button. Before, it closed.
- **Search results stay in their community.** If you switch communities
while a search runs, its people and its error do not show in the new
community. The search starts again there.
- **A failed send check keeps the draft.** Before a message with a
mention sends, the app checks who is in the channel. If that check
fails, the message stays in the compose bar and a notice offers Retry.
Before, nothing was sent and nothing was shown.
- **Invite stays in its community.** If you switch communities while the
"Invite" question is open, Invite adds nobody.

### Screenshots
The real compose bar widget, rendered by Flutter's test renderer with
the app's fonts and theme. The data is sample data. These are not device
captures.

| DM: outside person is "not in DM" | Stream: outside person is "not in
channel" |
|---|---|
| <img width="320" alt="DM chooser"
src="https://github.com/user-attachments/assets/0e884518-2386-4cd1-9f19-16661aaa194e"
/> | <img width="320" alt="Stream chooser"
src="https://github.com/user-attachments/assets/5395e4e1-032e-459b-98c6-0126c060c4d9"
/> |

| Stream: send asks first | Session: members only | Search failed: Retry
|
|---|---|---|
| <img width="260" alt="Stream ask on send"
src="https://github.com/user-attachments/assets/2b988257-3c6a-445d-acfa-fca47b9752fe"
/> | <img width="260" alt="Session chooser"
src="https://github.com/user-attachments/assets/e193551f-f5bf-41d9-aa29-bc8665c643f9"
/> | <img width="260" alt="Search failed with Retry"
src="https://github.com/user-attachments/assets/e8c4b9bb-067e-4fe9-9fdd-4e5691bc66be"
/> |

### Details
- `mobile/lib/shared/mentions/mention_rules.dart` holds the rules: query
syntax, match tiers, sort order, Space selection of an exact unique
name, stable rows while results arrive, and `p` and `mention` tags. A
`p` tag notifies the person. A `mention` tag is a reference and does not
notify.
- `mobile/test/shared/mentions/mention-rules.fixtures.json` is an exact
copy of the buzz-app fixture file. `mention_rules_test.dart` runs every
case.
- Known difference: desktop gives the directory to a channel with an
unknown explicit type (not stream, forum, DM or session). Mobile does
not. No such type exists today.

### Related issue
None found. Desktop counterpart: block/buzz-app#510. #7513 also edits
`send_message_provider.dart` and may conflict.

### Testing
`just mobile-install mobile-check mobile-test` passes: analyze clean,
2998 tests pass on `49c44653` (before rebase). The pre-push hook ran
`just mobile-check && just mobile-test` again on the rebased head
`de1ca080` and passed.

Each review fix was mutation-checked: removing the fix makes its new
test fail.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-05 18:54:49 -04:00
Logan Johnson
2782b9cd5e fix(mobile): preserve exact mention recipients in saved drafts (#7387)
<!-- Draft PR body for #7387 · fix(mobile): preserve exact mention
recipients in saved drafts · https://github.com/block/buzz/pull/7387 -->

🤖

## Summary

Saved drafts remembered their text but not exactly who each mention
referred to: reopening a draft (or restarting the app) could re-bind a
mention to the wrong same-name teammate, and a draft restored in one
thread could capture text arriving for another. This PR saves the exact
selected identities alongside the draft text and restores them together,
so a reopened draft names the same recipients — with their agent
markings — that you picked, including after a failed send.

- Persists selected keys and agent presentation in the app's existing
per-account/channel/thread draft storage (device-local; no cross-device
portability claim).
- Restores recipient bindings before text listeners run, including
recovery from a rejected send; malformed legacy entries are ignored
rather than guessed.
- Retires the previous draft's listeners before restoring another, so
switching threads can no longer write incoming text into your outgoing
draft.

### Related issue

- Fixes: N/A. No separate issue; the related work is the stack below.
- Depends on #7385 (declared base) — same-name selections and qualified
labels come from that PR.
- Landing note: branches in this series overlap in the composer — when
rebasing, keep exact/durable mention bindings, the explicit
invite/reference-only choice, the account/visit/revision fences, and
authorization before membership preparation and publication; don't
resolve conflicts by taking either side wholesale.
- Draft — not requesting merge yet; the security advisory doesn't run on
this stacked base (it reviews main-based ranges).

### Testing

- Regressions cover two namesakes, restart, rename, rejected send, a
second restart, and changing draft scope.
- At `e544b247c32bdccf1b316d3329be247faf412f5f`: `just mobile-check` and
full `just mobile-test` pass (2,085 tests). The earlier full `just ci`
receipt linked below is reused only for unchanged non-mobile
code/tooling, not claimed as a rerun at this head.
- Previous-head evidence: `just mobile-check`, the full mobile test
suite, and full local `just ci` all pass — receipts in the [exact-head
evidence
comment](https://github.com/block/buzz/pull/7387#issuecomment-5574635970).
- Verification is widget-test level; no native device or simulator run
is claimed.

To see it: mention an agent or a namesake, leave the channel, restart
the app, and return — the draft still names the exact recipients you
picked.

### Screenshots

Flutter production-widget test renders — not native-device screenshots
or acceptance captures.

| Scenario | Before | After |
|---|---|---|
| Draft reopened after leaving the channel and restarting the app |
![Before: the restored draft shows plain @Helper Bot text — the exact
selected recipient is
lost](https://github.com/user-attachments/assets/166b5c64-f7e8-46e5-b6b4-3b9e108217cb)
| ![After: the restored draft shows the highlighted robot mention chip
for the exact selected
recipient](https://github.com/user-attachments/assets/c3583a88-18bc-4513-9a87-7a4f7db113ac)
|

<details>
<summary>Capture provenance</summary>

Rendered by the Flutter widget engine in a `flutter test` run
(production widgets, production theme; no device or simulator). Before:
this PR's declared base `39afd73b0adfde14164f4b10dbd089cb498312b6`.
After: its head `8a781108507dc4501192b0101185decc1ed6f118`.

</details>

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-10-01 21:16:13 -04:00
Logan Johnson
4ef23609b7 feat(mobile): show contextual names in channel conversations (#7895)
🤖
## Summary

In a channel conversation, two identities with the same display name
looked identical. In the example below, the channel shows "Bad Janet"
twice, for two different identities. A reader cannot tell which one was
added and then removed.

This PR uses the contextual names from the previous PR everywhere a
conversation names someone:

- Message authors, threads, forum posts and replies
- Reactions, the typing indicator, and system rows (joined, added,
removed)
- Inline mentions and the agent activity sheet
- The profile sheet title, which now matches the row that opened it

Names are compared against the channel's members, so a label only gets
longer when a real collision exists in that channel.

The mention picker labels each choice against every candidate, so two
"Honey" choices look different. When you pick one, it still inserts the
identity's own name ("@Honey"), so the message text does not change.

Part 2 of 3: #7894 (resolver) → #7895 (conversations) → #7896 (lists,
Search, Pulse).

### Related issue

Related to #2910.

### Testing

- A widget test checks that the profile sheet names an author the same
way the channel does.
- A unit test checks that two same-name mention choices get different
labels but insert the same wire name.
- `flutter analyze` is clean, and the full `flutter test` suite passes.
- Checked on an Android emulator against the live relay, in an archived
channel with two identities named "Bad Janet".

| Before | After |
| --- | --- |
| <img width="300" alt="Before: the channel shows two different
identities both as Bad Janet"
src="https://github.com/user-attachments/assets/c7f6c345-12d8-4e11-9434-bbd2c63722db"
/> | <img width="300" alt="After: the second identity shows as Bad Janet
· sqxl"
src="https://github.com/user-attachments/assets/51c6b9fc-8900-4200-8ce0-ad1887f41dba"
/> |

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-29 15:47:43 -04:00
Logan Johnson
d074576871 fix(mobile): bind same-name mentions to exact selected identities (#7385)
<!-- Draft PR body for #7385 · fix(mobile): bind same-name mentions to
exact selected identities · https://github.com/block/buzz/pull/7385 -->

🤖

## Summary

If two people in a channel share a display name, mobile couldn't tell
them apart in mentions: picking the second could overwrite the first's
selection, a rendered mention linked whichever same-name person matched
first, and a later rename or a shorter name could re-bind the text to
the wrong recipient. This PR binds every mention to the exact selected
identity:

- Each same-name selection keeps its own recipient instead of
overwriting by name; conflicting picks get a qualified label like `Name
(key…)`.
- The longest matching label wins, so a shorter or interior name can
never claim part of a longer one and steal its identity.
- Rendering resolves recipients by the signed identity key from the
event's tags — never from message text alone — so qualified labels stay
correct regardless of tag order and survive later renames; an untagged
ambiguous label blocks shorter mentions instead of silently re-binding.

Ports the landed Desktop exact-recipient behavior (see
`docs/mention-editor.md`).

### Related issue

- Fixes: N/A. No mobile issue; Desktop's landed exact-recipient fixes
are the reference this ports.
- Independent base (`main`). #7387 (child) persists these exact
selections in saved drafts.
- Landing note: branches in this series overlap in the composer — when
rebasing, keep exact/durable mention bindings, the explicit
invite/reference-only choice, the account/visit/revision fences, and
authorization before membership preparation and publication; don't
resolve conflicts by taking either side wholesale.
- Draft — not requesting merge yet; the security advisory run for this
range timed out without results (no verdict).

### Testing

- Regressions cover same-name collisions, prefix/overlap, removal, tag
order, and renames.
- At `acd841354a692243f1cb4c04059baad42f1d2abb`: `just mobile-check` and
full `just mobile-test` pass (2,082 tests). The earlier full `just ci`
receipt linked below is reused only for unchanged non-mobile
code/tooling, not claimed as a rerun at this head.
- Previous-head evidence: `just mobile-check`, the full mobile test
suite, and full local `just ci` all pass — receipts in the [exact-head
evidence
comment](https://github.com/block/buzz/pull/7385#issuecomment-5574635836).
- Verification is widget-test level; no native device or simulator run
is claimed.

To see it: mention two teammates with the same display name — both stay
distinct, the second shows a qualified label, and the rendered message
keeps both correct even after either renames.

### Screenshots

Flutter production-widget test renders — not native-device screenshots
or acceptance captures.

| Scenario | Before | After |
|---|---|---|
| Qualified mention chip for same-name recipients, at 200% text scale
(deliberate stress fixture) | ![Before: the short chip is followed by
the distinguishing key spilling out as raw
text](https://github.com/user-attachments/assets/f1e40acc-24f1-47f7-b2f7-e3ba2348c169)
| ![After: the full qualified label wraps inside the bounded chip,
keeping the @ and robot glyph
visible](https://github.com/user-attachments/assets/b949e44f-e3f6-4c72-98ff-68139b2e1ab3)
|

<details>
<summary>Capture provenance</summary>

Rendered by the Flutter widget engine in a `flutter test` run
(production widgets, production theme; no device or simulator). Before:
this PR's declared base `3c7f288c60d67df78577b237e27c3dfc8831aaa1`.
After: its head `39afd73b0adfde14164f4b10dbd089cb498312b6`.

</details>

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-11 11:19:17 -04:00
Logan Johnson
82656ffea0 fix(mobile): standardize public-key identity display on npub (#7493)
🤖

## Summary

In the mobile app, anyone who hasn't set a display name shows up as a
raw 64-character hex key (e.g. `3a5d4f9c…`) — unreadable, and
unrecognizable as the same identity across screens. Profile and Settings
also let you copy that raw hex. Nostr public keys have a standard
readable form — `npub1…`, the same encoding other Nostr apps and our
desktop app already display. This PR makes every mobile identity surface
render npub instead:

- **Unnamed people everywhere** — message and thread authors, reactions,
typing indicators, member lists, channel details, DM headers and tiles,
inbox, search, forum cards, Pulse notes and reply context, mention
suggestions, and invite rows — now show a compact npub label: first 8 +
last 4 characters of the full npub joined by an ellipsis
(`npub1abcd…wxyz`), the same truncation desktop uses. Previously these
showed truncated raw hex.
- **DM fallback avatars and blank names** — 1:1 DM tiles and headers key
their fallback avatar to the same non-self counterpart the label names,
including self-first participant order; a self-DM keeps its
hex-key-derived initial. Blank or whitespace-only display names fall
back to the compact npub instead of rendering empty, while nonblank
authored names render verbatim (padding included).
- **Profile sheet → "Copy public key"** now copies the full canonical
npub — never raw hex. When the identity string isn't a valid public key,
the copy tile is disabled, so a malformed key never reaches the
clipboard.
- **Settings → Identity (pubkey)** displays and copies the full npub; an
invalid identity reads "Identity unavailable" with copy disabled.
- **Invalid identities never leak truncated raw hex** into the UI
anywhere — they render a neutral "Unknown identity" label.
- **Unchanged on purpose:** display names and verified handles (NIP-05 —
the `name@domain` badge) still render as before. Unnamed avatars keep
distinct per-key initials, derived from the underlying hex key rather
than the npub — otherwise every unnamed key would render the same "N"
initial. Event IDs are not public keys, so they keep their hex
truncation (in Pulse's "Replying to", the parent author shows npub while
an event-id fallback still shows hex). The nevent share link, private
keys, and internal hex storage are untouched. Inputs that accept a key
(invite/member entry) accept both hex and npub and keep working in hex
internally.

### Related issue

N/A. Searched open issues/PRs for npub identity display on mobile —
closest related: none found. Desktop's parallel npub standardization
lives in the stacked desktop PRs (#7488 foundation, #7489 controls,
#7495 display surfaces); this is the independent mobile slice (based
directly on `main`, not on those branches).

### Testing

At exact head `5a620e420a1fd57d9d8011ac26434eed32fcf765` (base: `main`
`44316ff72`; 40 files, +1,345/−154):

- Full mobile suite: 2,098 tests passing (`cd mobile && flutter test`);
`flutter analyze` clean; `dart format --set-exit-if-changed .` clean —
the same checks CI runs.
- Widget/unit coverage at production seams: compact labels and hex-keyed
avatar initials for DM headers/tiles, member rows, mention suggestions,
and Pulse reply context; DM fallback avatars keyed to the labeled
counterpart (self-first order and self-DMs); blank/whitespace
display-name npub fallback with nonblank authored labels verbatim,
including the Activity inbox sender and profile-sheet heading (each with
its own empty/whitespace production-seam regression); full-npub copy and
disabled-copy semantics in profile and settings; invalid-key
suppression; and hex↔npub input round-trips.

Verified via unit and widget tests — no device/simulator validation is
claimed.

### Task provenance

Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`

Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-09 14:08:24 -04:00
klopez4212
6f5ec4a731 Add mobile voice notes (#7121)
## Summary
- add iOS and Android voice-note recording and preview directly in the
mobile composer
- add waveform playback, scrubbing, speed controls, haptics, and
one-shot playback in chat
- package recordings in a canonical H.264/AAC MP4 envelope on both
platforms so existing relays accept them
- preserve the shared composer interaction and attachment-card treatment
across mobile platforms

Mobile counterpart to #6978.

## Testing
- `just ci`
- `just mobile-check`
- `just mobile-test` (2,026 tests)
- Android debug build compiled, installed, launched, and Voice note
verified in the attachment menu on Pixel 10
- signed iOS device build installed on iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-09-03 08:07:04 +00:00
Arjun Mahanti
bc006f6708 feat: render agent avatars as squircles (#7106)
## Summary

- render every agent/AI identity as a 30% squircle across desktop and
mobile while keeping human avatars circular
- propagate agent identity through message, thread, profile, reaction,
member, DM, search, workflow, project, huddle, forum, pulse, and
agent-management surfaces
- preserve squircle geometry for fallbacks, focus/status treatments,
add-agent controls, and overlapping avatar outlines (`calc(30% + 2px)`
for the outer background)

### Related issue

None found. This change was requested and visually reviewed in the
originating Buzz thread.

### Testing

- `just desktop-test` — 5,799 passed
- `just mobile-test` — 2,008 passed
- pre-push gates passed at `0d59d77b120dcb90aac2f918e422c11c9fa5353b`:
desktop check, TypeScript typecheck, desktop full test suite, mobile
format/analyze and full test suite, Rust tests, Tauri checks, and
differential file-size gate
- deterministic desktop visual sweep covered channel messages/thread
summaries; thread, subthread, and sub-subthread depths; reactions and
reactor popovers; hover/full profiles; added-to-channel activity;
channel members/settings; agent library/team overlaps; agent creation;
mention autocomplete; and DM header/sidebar/settings

### UI evidence

The complete labeled visual matrix is available in the originating Buzz
review thread. GitHub-hosted copies will be added in a follow-up PR
comment using the repository screenshot script.

---------

Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
2026-08-31 13:31:33 -04:00
klopez4212
aeb741fd31 Polish mobile channel navigation and message sends (#6488)
## Summary
- align iOS back controls and affected titles across channel details,
settings, and pairing
- make channel star and mute actions reflect their state immediately
- animate locally sent channel, thread, and DM messages from behind the
composer with a 300ms ease-out

## Testing
- `just mobile-check`
- full mobile test suite (1,573 tests)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-21 19:13:16 +01:00
klopez4212
ffb12d3b05 Fix mobile thread tail and iOS channel header (#6399)
## Summary

- Hide the thread Latest control once the lazy tail is fully reached.
- Polish the mobile channel header and use native iOS liquid glass for
Back.
- Keep the two-line header aligned and safe at larger text sizes.

## Validation

- `just mobile-check`
- `flutter test` (1,553 tests)
- Signed iPhone build installed and launched

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-21 15:27:30 +01:00
klopez4212
78267b0c3a Polish mobile message actions (#5873)
## Summary

- Add a lifted-message long-press popover with the reaction tray and
grouped actions.
- Use a native action surface
- Preserve taps and scrolling, exclude attached reactions from the
lifted preview, and align the composition to the bottom safe area.
<img width="1080" height="2424" alt="Screenshot_20260814-160732"
src="https://github.com/user-attachments/assets/a9d5fc91-4ee3-427c-8199-1c3be2b4e98a"
/>

## Testing

- `bin/just mobile-check`
- `flutter test test/features/channels/message_actions_test.dart`
- `flutter test` (1,364 tests)
- Manual interaction review on iPhone and Pixel 10

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-18 15:00:45 -07:00
klopez4212
69107dc3bf Polish mobile message threads and composer (#5645)
## Summary

- refine mobile message metadata, search spacing, and Activity filter
semantics
- add channel-parity Latest navigation and stable tail following to
threads
- synchronize Android composer/keyboard geometry and keep Latest spacing
stable across IME transitions

## Validation

- `bin/just mobile-check`
- `bin/just mobile-test` (1,276 tests)
- Pixel 10 install/launch and channel/thread keyboard, Latest, tail, and
back-navigation review
- signed iPhone install/launch workflow

## Snapshots

See the review snapshots below.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-15 09:14:14 +01:00
Taylor Ho
5ddf23d700 feat(mobile-messages): render compact Buzz permalink chips (#5639)
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now display recognizable context and navigate reliably in
the mobile app.
**Problem:** Bare Buzz permalinks appeared as raw or ambiguous URLs on
mobile, while channel and message links were not handled consistently
across Markdown forms and startup states.
**Solution:** Normalize eligible bare Buzz URLs without consuming
Markdown syntax, render them as semantic icon-prefixed chips, and route
channel/message targets through the mobile deep-link dispatcher while
preserving authored Markdown labels as ordinary links.

<details>
<summary>File changes</summary>

**mobile/lib/features/channels/deep_link_dispatcher.dart**
Routes parsed channel and message links through the appropriate in-app
navigation callbacks.

**mobile/lib/features/channels/message_content.dart**
Presents all bare Buzz permalinks as semantic icon chips and keeps
authored labels as ordinary links.

**mobile/lib/features/channels/message_content/link_normalizer.dart**
Normalizes bare and autolinked Buzz URLs without consuming Markdown
delimiters, code, or punctuation.

**mobile/lib/shared/deeplink/deep_link.dart**
Adds strict channel and project-entity parsing alongside message deep
links.

**mobile/lib/shared/deeplink/pending_deep_link_provider.dart**
Preserves pending navigation until the mobile routing surface is ready.

**mobile/test/features/channels/channel_detail_page_test.dart**
Updates navigation integration coverage for icon-prefixed channel chips.

**mobile/test/features/channels/deep_link_dispatcher_test.dart**
Covers channel/message dispatch and missing-target behavior.


**mobile/test/features/channels/message_content/link_normalizer_test.dart**
Exercises Markdown-safe normalization across the full Buzz link suite.

**mobile/test/features/channels/message_content_test.dart**
Verifies chip labels, icons, semantics, authored-label opt-out, and
navigation callbacks.

**mobile/test/shared/deeplink/deep_link_test.dart**
Covers strict parsing for channel, message, repository, pull-request,
and issue links.

</details>

## Reproduction steps
1. Run the mobile app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
from inline and autolinked forms.

## Screenshots / demos
**iOS Simulator — channel, message, repository, pull request, and issue
permalink chips**

Real app build (`37b2cb5eb`) running on an iPhone 17 Pro simulator.

![Mobile permalink chips on iOS
Simulator](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5639/mobile-permalink-chips-simulator.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 10:44:58 -07:00
klopez4212
98d3d77b42 Fix mobile composer input regressions (#5594)
## Summary

- reuse the channel member snapshot so first-use `@` suggestions appear
immediately
- reduce selection-only composer rebuilds so iOS selection handles stay
responsive
- make Return insert a newline, send only from the composer button, and
animate multiline growth with reduced-motion support

## Validation

- `just mobile-check`
- `just mobile-test` — 1,271 tests passed
- signed iPhone Release and Pixel 10 debug builds installed and launched
- `just ci` passed mobile, Rust, desktop, and web checks until the
unrelated `buzz-terminal` lifecycle test timed out waiting for `$0`;
reproduced unchanged in isolation

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
2026-08-13 17:58:29 +01:00
Wes
2777189d96 fix(channels): restore member invitations to private channels (#5493)
## Summary

- restore private-channel invitations for every active member
- keep owner/admin-only enforcement for elevated role grants, active
role changes, and removals
- preserve #4612's unrelated Desktop/mobile failure handling and
hardening
- add relay coverage for the ordinary actor/target role matrix
(`member`, `guest`, `bot`)

## Validation

- pre-push hook passed on `7de700e17642ad7e10155f9537033168d9249268`:
branch skew, Desktop checks/typecheck/tests/Tauri checks, mobile tests,
and Rust tests
- `cargo test -p buzz-test-client --test e2e_relay --no-run`
- `cargo fmt --all -- --check`
- `git diff --check`
- Donut and Mongo independently reviewed the cross-layer authorization
behavior; Donut's role-matrix coverage finding is addressed in this
revision

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-10 10:59:22 -07:00
klopez4212
27b51144f3 Polish mobile bottom sheets and profile cards (#4911)
## Summary

- standardize mobile sheets with shared spacing, close controls, action
tiles, haptics, and motion
- add uniform native concentric corners on iOS 26+ while preserving the
Android sheet shape
- refresh profile actions/status and normalize membership and huddle
timeline spacing

## Validation

- `just mobile-check`
- `just mobile-test` (1,165 tests)
- signed iPhone Release build and device install
- Android debug build and Pixel 10 install

## Snapshots

### Channel actions

![Channel action sheet on
Pixel](https://raw.githubusercontent.com/block/buzz/3babe5d8e339a1e7ad69de3b07e17eab17fe3f9d/pr-4911--pixel-channel-actions.png)

### Profile card

![Profile card sheet on
Pixel](https://raw.githubusercontent.com/block/buzz/3babe5d8e339a1e7ad69de3b07e17eab17fe3f9d/pr-4911--pixel-profile-card.png)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-05 20:22:37 +01:00
Jordan Mecom
efe1893dd3 fix(channels): restrict private-channel invitations (#4612)
This change requires an active owner or administrator for third-party
additions to private channels. The relay validator and transactional
database authority enforce the same rule, including removed-member
reactivation and role-change paths.

Idempotent self-target behavior remains available, while ordinary
members can no longer extend private-channel access to another identity.

## Testing

- `git diff --check
origin/main...codex/security-private-channel-invite-authority`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
2026-08-05 10:47:00 -07:00
klopez4212
feccf4eabc Polish mobile inbox and media flows (#4512)
## Summary

- make mobile unread state visible with bold channel names, an animated
Inbox badge, and swipe-to-toggle Inbox rows
- add directional transitions for top-level mobile navigation
- let mobile send while media uploads, with cancellable progress UI
- normalize iOS and Android video uploads, attach poster frames, and
improve native video playback

## Validation

- `just mobile-check`
- `just mobile-test`
- `cargo test -p buzz-media`
- Pixel smoke test
- iPhone smoke test

Desktop background uploads moved to #4522 so the two platforms can be
reviewed independently.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
2026-08-04 00:05:13 -07:00
klopez4212
857e63c4dd Polish mobile composer and messaging UI (#3918)
## Summary

- Refine the mobile composer with compact and expanded states, shared
footer fades, haptics, reliable keyboard dismissal, and full-width
camera and photo surfaces.
- Standardize popovers, filters, and section menus with consistent type,
strokes, radii, spacing, icons, and destructive styling.
- Align message presentation with desktop through consistent system
rows, typing and loading feedback, emoji placement, and predictable
photo viewing.

## Validation

- `just mobile-check`
- `just mobile-test` — 1,037 passed, 1 skipped
- Tested on Pixel 10 and a connected iPhone

## Snapshots

<table>
  <tr>
    <td align="center">Compact composer</td>
    <td align="center">Attachment menu</td>
    <td align="center">Recent photos</td>
  </tr>
  <tr>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--01-compact-composer.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--02-attachment-menu.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--03-photo-surface.png"
width="260" /></td>
  </tr>
</table>

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-03 07:30:23 -07:00
klopez4212
06582ee6f0 Render mobile agent mention chips (#3702)
## Summary

- Render selected agent mentions as visible bot chips in the mobile
composer.
- Recognize agent profiles consistently when rendering message-body
mentions.
<img width="630" height="1368" alt="Screenshot 2026-07-30 at 07 54 16"
src="https://github.com/user-attachments/assets/035b46bf-ee78-4ee5-82fc-84591415ed7c"
/>

## Validation

- `flutter test test/features/channels/compose_bar_test.dart
test/features/channels/message_content_test.dart`
- `flutter analyze`

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 10:02:48 -07:00
klopez4212
85edc0572a feat(mobile): desktop-parity emoji and thread experience (#3485)
Brings the Flutter app's emoji and thread surfaces up to desktop parity.

## Emoji

- **Full emoji-mart dataset** generated from the same `@emoji-mart/data`
set desktop uses (1,870 emoji, 8 categories), committed as an asset — so
shortcodes, names, and keywords are identical across clients. `just
mobile-emoji-data` regenerates it.
- **Rebuilt the tray**: search (a Dart port of desktop's tiered
`emojiSearch` ranking, extended to names and keywords), a
frequently-used section, and one continuous scroll with pinned section
headers. The category rail is a shortcut into that list, not a page
switcher, and spans the full width the search field uses. Custom emoji
share the native glyph size and cell.
- **Reaction pills** match desktop's geometry, and the count shows at 1.
- **Emoji-only messages** render at 36px with 1.45em inline custom
emoji, matching desktop's `emojiOnly` treatment.
- **Positive-emoji burst** ported from desktop's `EmojiBurstProvider`,
suppressed under reduced motion.

## Threads

- **Top-down layout** — head first under the app bar, replies flowing
down, like desktop's thread panel. The old reversed list bottom-anchored
the content and jammed the head against the composer.
- **Tap a channel message to open its thread**; long-press still opens
the action sheet.
- **Live reactions.** The thread's relay query is one-shot and its
`kinds` filter carries only content rows, so a reaction event could not
reach an open thread at all, and `allMessages` was a snapshot frozen
when the route was pushed — a new pill only appeared after leaving and
re-entering, which refetched. The live channel events are now unioned
into the thread's list. The burst is also route-guarded, since the
channel timeline stays mounted underneath and was claiming it first.
- The `+` affordance follows the channel: replies stay bare until they
carry a reaction, and the head keeps a standing `+`.

## Keyboard

A deliberate downward drag past ~48px dismisses the keyboard; short
scrolls leave it alone. Applies to the channel list, the thread list,
and the compose bar (via a raw `Listener`, so it can't steal the field's
tap or selection drags).

True finger-tracking dismissal is out of scope — Flutter only offers
`manual`/`onDrag`, and 1:1 tracking needs a native `UIScrollView` proxy
plus Android's `WindowInsetsAnimationController`.

## Testing

`just mobile-check` and `just mobile-test` pass (965 tests). New
coverage for emoji search ranking, dataset parsing, the emoji-only
predicate, tray scroll/rail behavior, reaction pills and the burst, and
both thread fixes above. `just ci` green.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: npub12zsjdqx8dud99s9h47xmk9lq93vryf7zjrae8wdrmma52cg5yglseyulst <50a12680c76f1a52c0b7af8dbb17e02c583227c290fb93b9a3defb456114223f@buzz.block.builderlab.xyz>
Signed-off-by: klopez4212 <klopez4212@gmail.com>
Co-authored-by: npub12zsjdqx8dud99s9h47xmk9lq93vryf7zjrae8wdrmma52cg5yglseyulst <50a12680c76f1a52c0b7af8dbb17e02c583227c290fb93b9a3defb456114223f@buzz.block.builderlab.xyz>
2026-07-30 07:29:40 -07:00
klopez4212
485d03a358 Fix mobile attachment and gallery polish (#3370)
## Summary

- align mobile message metadata and enlarge attachment-menu content
- smooth keyboard-to-camera/photo transitions and initialize the iOS
photo grid at the intended scale
- fix horizontal gallery loading, edge overflow, and end spacing

## Why

The attachment surfaces were reacting to keyboard and compact-menu
geometry during presentation, while gallery clipping and image lifecycle
behavior caused misalignment and occasional blank previews.

## Testing

- `just mobile-check`
- `flutter test` (881 passed, 1 skipped)
- native `RunnerTests` (17 passed)
- verified standalone Release build on a physical iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-29 07:09:08 +01:00
klopez4212
a77212875a Unify mobile loading spinners (#3314)
## What
- add the shared desktop-style arc spinner for mobile
- replace app loading indicators with the shared component
- preserve a static pose when reduced motion is enabled

## Stack
- follows #3313

## Validation
- `just mobile-check`
- focused spinner and pairing widget tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-28 19:12:54 +01:00
klopez4212
a3b097745a Refine mobile attachment picking (#3313)
## What
- morph the composer plus button into the attachment menu, camera, and
photo surfaces
- add ordered multi-select with inline recent photos and system picker
fallback
- add native iOS attachment/photo popovers and align the Android camera
treatment

## Stack
- follows #3312

## Validation
- `just mobile-check`
- `flutter test test/features/channels/compose_bar_test.dart`
- full mobile pre-push suite

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-28 18:05:43 +01:00
klopez4212
8398468ec9 Refine the mobile message composer (#2730)
## Summary
- replace the mobile composer sheet with a compact expanding capsule
- add vertically stacked attachment actions and an inline camera preview
- align suggestion, formatting, and send treatments with desktop

## Validation
- `just mobile-check`
- `just mobile-test` (541 passed, 1 skipped)

---------

Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
Co-authored-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
2026-07-24 16:32:22 -07:00
Tyler
591bbbfa7e fix(mobile): stop media fetch stampede — stable cache keys, bounded decode, 429 cooldown (#2219)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-21 07:28:46 -07:00
Tyler
a1e977cd1b fix(ui): relabel agent owner attribution from "owned by" to "managed by" (#2133)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-19 14:39:59 -04:00
Tyler
f308762852 feat(media): require auth for relay media reads (#1926)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-15 21:06:04 -04:00
Tom Brow
54f4165942 fix(mobile): surface non-member people and owned agents in @mention autocomplete (#1877)
Signed-off-by: npub1qq582hwclq7jnux44a2xul8nhe2ue2zk9z49ehzngznnmmk5ka5sprvchv <0028755dd8f83d29f0d5af546e7cf3be55cca85628aa5cdc5340a73deed4b769@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qq582hwclq7jnux44a2xul8nhe2ue2zk9z49ehzngznnmmk5ka5sprvchv <0028755dd8f83d29f0d5af546e7cf3be55cca85628aa5cdc5340a73deed4b769@sprout-oss.stage.blox.sqprod.co>
2026-07-14 20:14:35 -04:00
Tom Brow
7b6e12657d fix(mobile): fix agent avatars on mobile (#1837)
Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co>
2026-07-14 11:25:00 -07:00
Tom Brow
3112e59fd8 fix(mobile): add mentioned agents to channels (#1696)
Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
2026-07-13 16:17:01 -07:00
Wes
e9318f66d8 mobile: thread scroll-to-bottom and desktop-parity mention autocomplete (#1499)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-07-03 08:55:27 -07:00