🤖
## Summary
This PR finishes the change on the remaining mobile screens. After it,
every place that names a person or agent uses a contextual name:
- The channel list, DM labels, and the new-DM picker
- The channel header and details, and the members and add-members sheets
- Huddle avatars, spotlight, and overlay
- The Activity inbox
- Search results for people and messages
- Pulse notes, agent cards, and reply previews
Screens that belong to a channel compare names against its members.
Search, Pulse, and the new-DM picker have no channel, so they compare
the identities shown together. In the example below, Search showed four
results all named "murderbot". Now the human keeps the plain name, and
baxen's three agents are labeled by owner and key suffix.
Part 3 of 3: #7894 (resolver) → #7895 (conversations) → #7896 (lists,
Search, Pulse).
### Related issue
Related to #2910. The desktop search change in #6483 addresses the same
problem in a different app.
### Testing
- Widget tests cover the Activity inbox, Search, and Pulse notes with
colliding names.
- `flutter analyze` is clean, and the full `flutter test` suite passes.
- Checked on an Android emulator against the live relay by searching for
"murderbot". The after image also shows "classy-murderbot", a new
account that was created between the two captures.
| Before | After |
| --- | --- |
| <img width="300" alt="Before: Search shows four people all named
murderbot"
src="https://github.com/user-attachments/assets/21bbcc52-fea8-4b31-ab8c-ff3ec9b32a39"
/> | <img width="300" alt="After: three results show as baxen's
murderbot with a key suffix, and the human shows as murderbot"
src="https://github.com/user-attachments/assets/d59584d9-4a35-46da-b39a-83addfad3cd2"
/> |
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
🤖
## 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>
## 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>
## Summary
Hiding a DM row and then receiving a new message from that person never
brought the row back — the reader was left with only an Inbox
notification. This restores the row from live activity, client-side,
with no relay or database changes.
- Subscribe to hidden-DM activity **channel-scoped** over the hidden-DM
id set (`#h`), re-registering whenever the set changes. The relay's
`fan_out_scoped` only routes channel-scoped events (every kind:9 DM
carries a `channel_id`) to channel-scoped subscriptions, so a
community-global `#p` filter receives nothing. Hiding a DM never drops
membership, so `#h` authorization holds. Because every delivered event
is already for a hidden DM the reader belongs to, eligibility no longer
needs a self `p` tag — untagged CLI/agent DMs resurface too — and no
per-event visibility fetch is required.
- On qualifying activity, invoke the existing open-DM action (`41010`)
to reopen the hidden one-to-one or group DM, validating that the relay
reopened the same channel.
- Preserve the existing relay-synced hide snapshot (`41012` → `30622`);
hide state still syncs across devices.
- Let Inbox reopen a hidden DM before navigating to its message, with a
busy state on the row and context-menu "Open in channel" action while a
reopen is pending.
- Coalesce resurface attempts per channel: a concurrent message that
arrives while a reopen is in flight triggers a retry on failure instead
of being marked handled and dropped.
- Write exact dm-visibility query keys on open/hide mutations.
This is the client-only replacement agreed in the Buzz design thread and
supersedes #6713. The accepted limitation is that resurface only fires
while a client is running — a message that arrives while all clients are
closed does not restore the row on next launch (the `30622` snapshot has
no watermark); Inbox click-through is the safety net for that case.
Addresses #6316.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
## 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>
## Summary
Communities joined via an invite link never connect: the app dials
`ws://` on port 80 instead of `wss://` on 443 and sits on
"Reconnecting…" indefinitely.
`RelayConfig.baseUrl` is documented as an HTTP origin, but the two
onboarding flows disagree on what they persist:
- **Device pairing** validates and stores `https://` —
`pairing_provider.dart:657` throws on anything else.
- **Invite join** stores the relay URL straight off the invite link, and
`deep_link.dart:165` always emits `ws://` or `wss://`.
`wsUrl` only special-cased `https://`, so a `wss://` base fell through
to the plaintext branch:
```dart
final scheme = uri.scheme == 'https' ? 'wss' : 'ws'; // 'wss' is not 'https'
```
The claim request itself succeeds, because `_claimUrlFromRelay`
(`invite_join_provider.dart:242`) maps `wss → https` explicitly. Only
the socket path is missing that conversion — which is why the community
appears, correctly named, and then never loads.
The same `baseUrl` also feeds `/query` (`relay_session.dart:136`), media
upload (`media_upload.dart:765`), Blossom auth (`media_auth.dart:128`)
and `relayClientProvider` (`relay_provider.dart:113`), so those requests
were malformed too. Where port 80 *does* answer, it is additionally a
silent TLS downgrade after `validateInviteRelayUri` insisted on
`wss://`.
This folds the websocket schemes back to their HTTP equivalents in
`baseUrl` itself, so every consumer is correct by construction rather
than needing a second getter remembered at each call site, and
communities **already persisted** with `wss://` are repaired on read
without a migration. `community_icon_provider.dart:46` already performs
this same conversion locally.
One subtlety worth flagging for review: the normalization is derived in
the getter rather than applied in the constructor, so the constructor
stays `const`. The compile-time fallback at `relay_provider.dart:77`
relies on const canonicalization for a stable identity across rebuilds,
and Riverpod's `defaultUpdateShouldNotify` is `previous != next`
(`element.dart:361`), which falls back to identity for this class. A
`factory` constructor here yields a fresh instance per rebuild, which
tears down and resubscribes every listener —
`channels_provider_test.dart` catches it as an unexpected unsubscribe
during reconnect.
### Related issue
Fixes#2662.
### Testing
`flutter test` — **705 passed, 1 skipped, 0 failed**
`flutter analyze` — No issues found
`dart format --set-exit-if-changed .` — 249 files, 0 changed
Run against the Hermit-pinned SDK (Flutter 3.41.7 / Dart 3.11.5),
matching CI.
10 new unit tests in `mobile/test/shared/relay/relay_config_test.dart`
covering both onboarding schemes, `http`/`https` passthrough,
non-default ports, and agreement between the invite and pairing paths
for the same relay.
Verified end-to-end against a self-hosted relay behind `tailscale
serve`, which terminates TLS on 443 and leaves port 80 closed. Relay
logs show the invite claim succeeding over HTTPS at the moment of
joining, while no WebSocket connection ever arrives — no `WebSocket
connection established`, no NIP-42 auth, no `kind:0` profile, no push
registration — across the relay's entire history, even though the member
row is present and correct. Port-80 refusals are not logged by
`tailscaled`'s netstack, which is why the retries leave no trace
server-side. Reproduced on both iOS and Android.
---------
Signed-off-by: Krishna C <github@kumb.uk>
## Summary
- Add a shared footer fade behind the floating tabs on Home, Activity,
and Search.
- Use a shared anchored popover for Activity filters and section
actions, with working section move controls.
- Polish message grouping/press states and remove the initial Search
back button.
<img width="630" height="1368" alt="Screenshot 2026-07-29 at 08 49 37"
src="https://github.com/user-attachments/assets/9e787adf-0bb3-49c6-8224-5819e8cfb1ad"
/>
### Testing
- `flutter analyze`
- `flutter test`
- Release build installed and checked on a connected iPhone
### Screenshots
A real-device Activity baseline showing the original solid footer is
attached in a PR comment. The updated review build was checked on the
connected iPhone.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## The bug
When you tap an item in the Activity inbox that refers to a message
inside a thread (for example, someone replied to you or mentioned you in
a thread reply), the app opened the channel at its top level. It did not
open the thread, and it did not show you the message the notification
was about. You had to hunt for the reply manually.
## The fix
Activity items now keep track of two things: the root message of the
thread and the specific message that triggered the notification. Tapping
the item now:
1. Opens the thread detail view for that thread (instead of the
channel's top level).
2. Scrolls to the specific message that triggered the notification.
3. Briefly highlights that message so it is easy to spot.
This works for both direct replies and replies nested deeper in a
thread, and it fetches the thread from the relay if it is not already
loaded (for example, right after app launch).
## Testing
- Flutter analyzer
- 64 focused mobile tests covering direct and nested thread markers plus
Activity navigation
- Full pre-push suite (mobile, desktop, Rust, and Tauri tests)
## Manual verification
Open Activity, tap a mention for a reply inside a thread, and confirm
Buzz opens that thread, scrolls to the reply, and highlights it.
---------
Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Mobile users can scan Activity as a focused
conversation inbox and open the exact unread message or thread
represented by each item.
## Context
Mobile's Activity tab had not kept pace with Desktop: it presented
isolated event headlines, advertised categories that were often empty,
and opened a channel without clearly landing on the selected item.
This PR brings the Mobile surface toward the conversation-oriented
direction explored in Clay Delk's Desktop [Inbox refactor PR
#2045](https://github.com/block/buzz/pull/2045), while adapting it to
Mobile rather than copying the Desktop split-pane implementation. The
related product/UX discussion is captured in the originating [Buzz
thread](buzz://message?channel=a9bbc0e5-d25d-4740-849c-93c34bb578a4&id=a7d9a4d33dcd8c6bf0dc67d81c328892b9e38dedaa8548920224ef388301b6ab).
## UX decisions in this PR
- **Conversation-oriented, not event-oriented:** related updates
collapse into one row per thread/DM conversation, represented by the
latest update and ordered by latest activity. Separate top-level
conversations in the same channel remain separate rows.
- **Resume at the oldest unread:** tapping a grouped row opens the
represented canonical message/thread/DM at its oldest unread item,
rather than merely opening the channel at an arbitrary position.
- **Desktop-aligned row hierarchy:** rows lead with a full avatar and
sender, followed by contextual location/type metadata, unread dot +
time, and a two-line preview. A **New** boundary separates unread and
read content.
- **Mobile-native navigation:** Mobile keeps a single-column `Activity →
canonical conversation → Back` flow. It does not introduce Desktop's
persistent detail pane.
- **Compact filtering:** the old horizontal chip rail becomes a compact
filter menu so the source set fits a phone viewport without horizontal
scanning. Filters are All, Mentions, Threads, Needs Action, Activity,
Agents, Reminders, and Drafts.
- **Focused source semantics:** All covers personally relevant work—DMs,
mentions, thread replies, needs-action events, owned-agent activity, due
reminders, and active drafts—rather than becoming a generic stream of
every channel message. Mobile's standalone Activity source is currently
limited to DM traffic because it does not have Desktop's aggregated
channel-activity feed.
- **Shared read behavior:** rows project canonical
channel/thread/message markers, support unread-only and mark-all-read,
and use local overrides only where canonical markers cannot represent an
item.
- **Reminders and drafts are real data:** reminders use the same
encrypted NIP-ER events as Desktop. Drafts persist device-local composer
state, restore on return, survive failed sends, and clear after
successful sends.
- **Explain navigation failures:** an unavailable destination produces
an explanatory message rather than silently doing nothing or falling
back to an unrelated channel position.
## Implementation summary
- Adds a Mobile inbox model for conversation grouping, category
priority, contextual labels, sorting, filtering, and oldest-unread
targets.
- Expands relay-backed sources for mentions, approvals, owned-agent
lifecycle events, and DM traffic.
- Adds fail-closed NIP-ER reminder decryption and device-local
compose-draft persistence.
- Redesigns Activity rows, boundaries, filters, unread controls, and
empty/loading states.
- Routes rows through Mobile's existing canonical channel/thread screens
with precise target IDs.
- Adds model, provider, widget, reminder, read-state, draft-lifecycle,
and deep-link coverage.
## Reproduction steps
1. Run Mobile and open **Activity**.
2. Confirm full avatars, sender-first rows, context labels, unread
indicators, timestamps, two-line previews, and the compact filter
control.
3. Open the filter menu and verify All, Mentions, Threads, Needs Action,
Activity, Agents, Reminders, and Drafts.
4. Tap a grouped thread row and confirm the canonical conversation opens
at its oldest unread message.
5. Mark rows read/unread, enable unread-only mode, and use
mark-all-read; confirm state agrees with the channel/thread destination.
6. Type without sending in a channel or thread, leave, and confirm the
draft appears in Activity and restores in the composer.
## Screenshots
| Before — merge-base `dd222a509` | After — PR head `52ad40aee` |
|---|---|
| <img width="1206" height="2622" alt="image"
src="https://github.com/user-attachments/assets/ae961b08-bf8a-4bd5-b487-f6321ae8d85b"
/> | <img width="1206" height="2622" alt="image"
src="https://github.com/user-attachments/assets/bcbe7ab0-552a-417c-9e85-7a85eb4592ee"
/> |
Recaptured on the same authenticated iPhone 17 simulator, account,
theme, and Activity view, at this PR's current merge-base (`dd222a509`)
and head (`52ad40aee`). Both frames were taken within a few minutes on
the same live feed, so the visible conversation set overlaps closely
(the recent Ned/Bart/Tommy items appear in both). The compared change is
the row *structure*: Before leads with an `@ Mention` headline over a
small inline avatar and a horizontal chip rail; After leads with a full
avatar, a compact `labelMedium` sender label, contextual "Mentioned in"
metadata, and a filter menu. The sender username now renders at the same
compact scale the old `@ Mention` label used.
## Verification
- Current rebased head: `5bd87f4f4` on `origin/main` at `dd222a509`;
GitHub reports the PR mergeable.
- `flutter analyze` — clean at `5bd87f4f4`.
- Full Mobile suite — 698 passed, 1 skipped, 4 failed; all four failures
reproduce identically on clean `origin/main` (`channels_page_test`
create-channel sheet and three `compose_bar_test` agent-mention cases).
- The prior PR-specific `home_page_test` failures were fixed by
providing the Activity local-state dependency in that harness.
- Independent code and simulator UI review — approved.
- Post-rebase GitHub checks are running.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>