mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
f59b3265682f82d4eafb7d5c5ef484acedf9b098
334 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f59b326568 |
Fix mobile onboarding policy loading, browser cancellation and pairing feedback (#8874)
Mobile onboarding could reject valid large legal documents, stay busy after closing Android browser sign-in, and hide pairing-import errors behind the community-link form. This change bounds join-policy responses at 4 MiB while retaining 64 KiB for other responses, handles Android browser cancellation through the native lifecycle/channel, keeps Cancel sign-in usable, and shows friendly pairing errors on the landing screen with a live-region announcement. Identity mismatch checks remain intact. The browser-cancellation implementation incorporates the candidate from #8847 (`cb241b3f1`); coordinate landing to avoid duplicate work. This PR consolidates the three fixes exercised together during Android onboarding acceptance. ## Validation - Pixel 9 / Android 17, production hosted services, isolated debug installs. Human acceptance plus native UI, saved public-identity and restart checks as noted below. No iOS acceptance in this session. - Latest build and invite-first acceptance: base `a12ad1cdd29da8b2923eca5408e963a2659777ab` plus these patches. - Focused tests on that base plus patches: **88 passed** across hosted_journeys_test, hosted_admission_test, browser_auth_test, pairing_hosted_handoff_test and pairing_provider_test, with `BUZZ_AUTH_DOMAINS=communities.buzz.xyz`. - Earlier baseline: 81 focused cases passed plus one staging-configuration case passed separately (82 distinct cases across seven onboarding/recovery files); these are historical, not current-head certification. - Earlier policy change: 41 focused tests, changed-file analysis and isolated mutation check passed on the earlier base. Earlier browser candidate native instrumentation/mutation evidence was not rerun on this combined revision. - Android debug APK built and installed successfully. Mandatory commit/push hooks passed at `6442e5ff9`: analysis clean; mobile tests 4,047 passed / 34 skipped; additional package runs 4 and 27 passed; file-size checks passed. These broad tests ran through the required push hook, not a manually invoked suite. CI remains pending. ## Live testing report — October 31 | Journey | Result and evidence boundary | |---|---| | Fresh email signup / identity / hosted invite | Reproduced policy response exceeding 64 KiB; after fix, admission and restart passed. Profile/message visible locally; cross-account message receipt remains unproven. Tested at `cfcaef4e9f` plus policy patch. | | Browser close → retry → login → existing identity recovery → restart | Passed at `d14f33a170` plus patches; recovered the expected public identity and community. | | Pairing a different identity into an existing install | Rejected without replacing the original key. Initially hidden error became visible with friendly copy; Back and restart preserved the community at `b80217de3` plus patches. | | Fresh pairing → wait for desktop approval → sign-in → community → restart | Passed at `20742a236` plus patches; human confirmed approval gate, saved public identity matched. | | First-time desktop cancellation → welcome → fresh QR retry → community → restart | Passed at `20742a236` plus patches. Cancellation error remained raw `user_denied`. | | Android system Back while awaiting pairing approval | Human-confirmed in already-onboarded Add Community; first-time phone-side Back variation remains untested. | | Wrong pairing code → rejection → correction → completion | Human-confirmed at `a2540fbe3` plus patches. Exact error and same-session versus fresh-QR retry were not independently captured. | | Interrupt account onboarding at community picker → process restart → recovery → admission | Passed at `a2540fbe3` plus patches. Restart opened Add a community; Find my communities restored choices without another login. Human completed admission. No destination had yet been selected. | | Invite-first into fresh signed-out app → intended community → restart | Passed at `a12ad1cdd` plus patches; saved identity/destination matched. Existing member and existing browser session. Initial Android VIEW intent explicitly targeted the test package; ordinary HTTPS default app-link resolution and new-member admission remain untested. | | Malformed community input → error → Back → restart | Passed for `not a community`; original community preserved. `not-a-community` is a valid single-label hostname and was not counted as malformed-input evidence. | ## Remaining gaps and findings Not release certification. Pairing landing-error regression/mutation evidence is now recorded below. Current combined-revision native instrumentation has not been rerun; its limited scope is described below. Deferred behavior, not implemented here: - Wrong browser account after fresh pairing is correctly rejected, but Retry reuses that app account session; in-app account switching must preserve the transferred identity and destination. The live happy path used a manual browser-account switch and fresh install. - Pairing wait has no visible Cancel; tested system Back works. - Desktop cancellation exposes raw error wording. - Interrupted community selection requires an extra Find my communities step. Master checklist remains partial: seven categories exercised this session, signup/community creation has historical coverage, seven categories unexercised. New-member link-first admission, deeper interruption/server-commit recovery, expiry/revocation/network failures, key import, organization/self-hosted paths, phone-to-desktop transfer, eligibility gates, accessibility/app-link variants and iOS require separate coverage. Channel-discovery work is separate (#8869). ## Review checkpoint [Carl’s review](https://github.com/block/buzz/pull/8874#pullrequestreview-5724095754) found no code blockers at `6442e5ff9`. Carl also reviewed test-only follow-up `d43958653` and found no issues, accepting both validation gaps as closed. A subsequent review identified stale callback → resume cancelling a newer Android login. A narrow native fix and regression validation are in progress; prior readiness attestation is withdrawn pending this behavior change. - **Android stack check passed (Pixel 9, Android 17):** after fresh account sign-in, the human stopped at the community picker without closing/restarting. `dumpsys activity activities` showed exactly one MainActivity in Invite First task 567, with no CustomTab in that task or the full component list. ChromeTabbedActivity remained in a separate pre-existing browser task. This rules out a lingering duplicate activity in this run; it does not independently trace `onNewIntent` or establish all Android/browser variants. Binary production sources match `6442e5ff9`. - **Pairing regression/mutation passed:** extended the existing QR journey using the real encrypted NIP-AB import and hosted coordinator. Asserts visible friendly mismatch feedback, live-region wrapper, unchanged stored community/account identity, and Back to home. Reused the existing socket fake. Hosted journey + pairing provider files: 70 passed with configured auth domain; opt-out hosted journey file: 3 passed / 21 skipped; changed-file analysis clean. Isolated clone: control passed, hiding the landing error failed the visible text assertion, restoration passed. No production source change in this follow-up. - **Duplicate landing:** #8847 is superseded by this combined PR; its Android slice is included here. No merge performed. BrowserAuthPluginTest manually drives plugin hooks using an unmanaged Activity and fake browser launcher. It proves the plugin state machine only, not MainActivity wiring, real lifecycle order, process death, or duplicate app_links callback delivery. Current CI does not run Android instrumentation. Android ignores the debug ephemeral-login flag and shares browser cookies. Readiness checklist: agent review completed on production changes and test follow-up; agent exercised the changed behavior; human completed the documented Android journeys and final sign-in check. ## Stale callback follow-up (`b7aaaa788`) Addresses [the later Android lifecycle review](https://github.com/block/buzz/pull/8874#pullrequestreview-5724197312): a recognized callback now clears browser-dismissal tracking before rejecting a mismatched state. A stale callback cannot complete or cancel the newer login. The existing native journey now includes the intervening resume, successful current callback, and genuine browser dismissal after a stale callback. Only two native files changed; policy and pairing fixes are unchanged. Validation: 88 focused Dart tests passed. Android app and test APK build passed. On Pixel 9, the native plugin journey passed; in an isolated source/Git/build clone, removing the reset failed at the newer attempt's expected-zero-replies assertion; exact restoration passed. This proves the plugin state machine on Android, not a real late browser callback through OS lifecycle delivery. Carl reviewed `b7aaaa788` and found the fix correct and minimal with no blockers. Human browser-close/retry check remains pending; PR stays draft until that passes. Current-head CI is also required for merge. Accepted limitation: if a recognized stale callback arrives after the current browser tab has already gone, automatic dismissal is suppressed and the user may need the visible Cancel button. The callback cannot complete sign-in without matching state; Dart validation remains authoritative. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
a12ad1cdd2 |
feat(mobile): refine agent profiles and model selection (#8773)
## Summary - Simplify agent profiles with Message/Bestie actions, relay-authored published instructions (unavailable without a config channel), management details, public-key copying, and a separate final Delete section. - Sort the Agents directory by name and mark the selected Bestie with a muted filled star. - Place Model and Thinking together below Name. Use native iOS Liquid Glass menus and preserve selection callbacks as controls become enabled. Related: #8685. ## Validation Full mobile checks passed. Regression coverage includes relay-author filtering, unavailable instructions, metadata retry, resizing native pickers, accurate deletion state, selection after loading, and Bestie switching. Real iOS simulator taps verified both pickers; the user confirmed the earlier iPhone build works. Review fixes were checked with widget journeys and the native UIKit resize/selection probe. <details> <summary>Previews</summary> Synthetic fixture renders: | Profile | Create agent | Bestie card | | --- | --- | --- | |  |  |  | Native iOS simulator interaction with a synthetic catalog:  </details> --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
a2540fbe3d |
Align Android profile setup with iOS steps (#8871)
Android profile setup now matches iOS: choose a display name, then an optional avatar using the existing dotted-circle placeholder and Image / Emoji / Animated editor. Back preserves drafts; Skip saves the name without an avatar, and failed saves retain retry/exit options. Validated on Pixel 10 and approved by Kenny; analysis clean, 4,064 mobile tests passed (8 skipped). Device preview uses no-write callbacks; preview-only files are excluded. | Display name | Avatar | | --- | --- | |  |  | buzz-review-completed Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
20742a236d |
fix(mobile): only render known channels as #channel chips (#8862)
Follows [#8860](https://github.com/block/buzz/pull/8860) (merged). On mobile, any `#token` rendered as a channel chip, so GitHub-style references like `#8700` turned into inert chips for channels that don't exist. Desktop already avoids this by only linking names in the known channel list. This makes mobile's message body and one-line preview do the same. With no known names (channels still loading, or none visible), nothing becomes a chip. An open channel the reader hasn't joined stays plain text until the directory has loaded; before this change it was an inert, untappable chip. messages rebuild with chips once channel names arrive. Unlike desktop, there's no "any `#token` while empty" fallback, because an empty map can't distinguish loading from an account with no visible channels. The known names come from the existing `channelIdsByNameProvider` (the reader's channels, plus open directory channels once Browse channels or invite recovery has loaded the directory), so there is no new fetch, subscription or sync change. When the list is non-empty, the regex has one alternative fewer than before. ### Validation Added tests: - Preview: `#8700`, `#8774's`, a known-name prefix (`#gen` vs `general`) and `#eng-ios` with only `eng` known stay text, while `#general` and a longer known `#eng-ios` are chips. With no channel names, nothing chips. - Message body: `Took over #8700 from #general` renders one chip (`general`) and keeps `#8700` as text. - Message body: with no channel names, `#unknown` stays text. The new assertions fail with the previous always-generic pattern. The full mobile test suite passed in pre-push. Simulator render on an iPhone 18 Pro (iOS 27): real message-body and preview widgets with channels `general` and `eng` loaded, rendered with both stacked PRs applied. | Before (`main`) | After ([#8860](https://github.com/block/buzz/pull/8860) + [#8862](https://github.com/block/buzz/pull/8862)) | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--01-before-main.png" width="300"> | <img src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--02-after-both-prs.png" width="300"> | Human app test: not applicable under the mobile-client testing exception; agent review and simulator validation above. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> |
||
|
|
e3b1ef0d84 |
docs(agents): mark desktop/ as legacy, not a reference for mobile (#8868)
Agents working on the mobile app have been looking at `desktop/` for implementation and design cues. `desktop/` will soon be superseded by [block/buzz-app](https://github.com/block/buzz-app), while mobile is current, so it should never serve as that reference. - Root `AGENTS.md`: labels `desktop/` as legacy in the repo layout, adds a callout at the top of the Desktop App section, adds the same guidance to the Mobile App section, and drops "matches desktop" from the mobile theme note. Mobile kind constants now sync with `crates/buzz-core/src/kind.rs` (also in the `nostr_models.dart` comment) instead of desktop. - `VISION_MOBILE.md`: replaces the line that allowed the desktop implementation to inform shared mobile rules, so the vision and agent guidance agree. Written intent still takes priority. ### Validation Documentation-only. Reviewed the diff and links. Codex review's one finding (the vision conflict) is addressed. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b80217de32 |
fix(mobile): end #channel tokens at apostrophes, quotes and dashes (#8860)
Mobile renders `#channel` tokens as chips, but a token followed by an apostrophe, quote, ellipsis or dash stayed plain text: `#general's`, `"#general"`, `#general…` and `#general—` all lost their chip because the token only ended at whitespace or `, ; . ! ? : ) ] }`. This ends channel tokens at those characters too (`' " ‘ ’ “ ” » … – —`). The message body and the one-line preview each had their own copy of the channel regex; they now share one `channelTokenPattern`, so they can't drift apart again. `@mention` boundaries are unchanged on purpose: on mobile and desktop the same boundary also decides which mentions are bound and tagged when sending, so changing it is a separate, cross-surface decision. ### Validation Added tests: - Preview table covering each new trailing character (and matching opening quotes). - Message body: `#general's` keeps the chip and renders the `'s` suffix. Both fail with the previous boundary. The full mobile test suite passed in pre-push. Simulator render on an iPhone 18 Pro (iOS 27): real message-body and preview widgets with channels `general` and `eng` loaded, rendered with both stacked PRs applied. | Before (`main`) | After ([#8860](https://github.com/block/buzz/pull/8860) + [#8862](https://github.com/block/buzz/pull/8862)) | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--01-before-main.png" width="300"> | <img src="https://raw.githubusercontent.com/block/buzz/5afead6a162d5c8554ad17175fc2dccff93bf2af/pr-8862--02-after-both-prs.png" width="300"> | Human app test: not applicable under the mobile-client testing exception; agent review and simulator validation above. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> |
||
|
|
d14f33a170 |
fix(mobile): keep toasts above native navigation and sheets (#8866)
## Summary Keep mobile toasts above navigation and modal sheets with an app-level overlay and a tiny transparent iOS composition boundary. Preserve Retry, automatic dismissal, and taps on underlying controls. ### Related issue No duplicate found in the toast issue/PR search. ### Testing Full mobile tests and analysis pass; iOS simulator and device builds succeed. Verified layering, Retry, dismissal, and underlying taps in a native simulator preview; human confirmed on iPhone with their existing profile/community. Broader repository checks are in progress. Before / after (isolated native simulator preview using production widgets): | Before | After | | --- | --- | |  |  | <!-- buzz-review-completed --> Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
120cbae8e5 |
fix(mobile): restore inline typing groups and keep working thread links (#8865)
Restores mobile’s shared inline typing bubble and animated avatar cluster after #8765 replaced them with composer activity. Humans and agents share the group, including the existing overflow badge and reduced-motion behavior; fresh replies regain the bubble-to-message handoff. Keeps the working-agent reply link from #8754, including its current DM support: a root message can show working agents and open its thread before the first reply. Thread timelines now use the same inline typing group after their last message, so reverting the redesign does not bring back the old full-width typing bar. Archived and non-member conversations retain their existing no-typing-indicator behavior. This intentionally follows the requested mobile presentation rather than buzz-app’s composer activity layout. VISION_MOBILE.md allows platform-specific presentation. The composer’s “Working now” list is removed with the redesign; the root-message thread links remain. Validation: - At `5df48ad4b`: `just mobile-install`, `just mobile-check` and `just mobile-test` passed (4,036 Flutter tests passed, 30 skipped; unconfigured-push checks and 27 crypto tests also passed). Repository file-size checks passed. - Required pre-push hooks passed, including a second full mobile check/test run and the repository file-size gate. Hosted CI is pending. - Extended the existing thread journey through opening an empty working thread, mixed human/agent typing after a reply, clearing typing, and Back. Existing coverage exercises avatar grouping, overflow, reduced motion, reply handoff, and followed/detached thread scrolling. - Agent self-review completed; no additional blockers found after correcting inventory-only agent avatar shapes and waiting for timeline layout before following new thread typing. - Simulator component preview uses synthetic identities in a local, uncommitted entrypoint. It is not evidence of a live relay exchange. Human verification pending (draft): 1. In a channel or DM, have an agent work on a threaded reply. Confirm the root’s working-agent reply link opens that thread before a reply arrives. 2. Have multiple people/agents type. Confirm one inline avatar group and dot bubble, including inside the opened thread, with no wide composer bar. 3. Receive a fresh reply in the main conversation/Solo and check the typing-to-message handoff. In a long thread, check typing while following the latest message and while scrolled up, then navigate Back. Component preview on iPhone 18 Pro (iOS 27.0 simulator). These are production widgets in an uncommitted preview shell with synthetic identities; surrounding screen chrome is not a full-app screenshot.   --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
0a46f329c0 |
Fade welcome bees with the QR scanner transition (#8859)
Fixes the welcome bees remaining visible when the QR scanner opens after #8853. Bees now share the foreground’s fade and scale progress, pause while hidden, and return on dismissal. Reduce Motion skips scaling. Validated scanner open/close in the iPhone simulator, normal/reduced-motion widget journeys, analysis, and the full mobile suite. Simulator camera hardware is unavailable; QR decoding was not exercised. Independent review found no blockers. https://github.com/user-attachments/assets/1ce046b2-63e3-4441-90e9-9d9ec048bdfa Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
2ca77747d1 |
Fix mobile Add Community loading and membership restoration (#8796)
## Summary Open Add Community immediately from the post-login picker and return directly on Back, without membership restoration or repeated discovery during navigation. Completing Add restores the saved selection while keeping the newly added community active; failures retain progress and offer Retry. Follow-up to #8729. Mobile app update only; no relay deployment required. ## Testing `just mobile-install mobile-check mobile-test` passed (4,025 standard tests), plus 4,049 Builderlab-enabled tests on the merged revision. Production-path journeys cover hosted and self-hosted completion, partial restoration and Retry, persisted memberships, and the active destination. Removing the completion hook makes the new journeys fail. Independent agent review found no blockers. Native walkthrough and human verification remain pending. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
cfcaef4e9f |
Update mobile toast animations (#8855)
Updates mobile toast animations and styling. | Sign in | Retry | Copied | | --- | --- | --- | |  |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
5f13984a19 |
Bring buzz.xyz branding and animated bees to mobile welcome (#8853)
Replaces the mobile welcome gradient with buzz.xyz’s green and 12 animated bees. The swarm keeps flying through rotation, respects Reduce Motion, and leaves controls tappable; the primary button label is now white. The ghost button has an opaque chartreuse fill so bees cannot show through its text. Validated in the iPhone simulator, with animation/rotation journeys, analysis, and the full mobile suite passing. Agent review found no blockers. https://github.com/user-attachments/assets/9aa1472c-c8e5-4e8b-a016-a8cba36805e1 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
4de1b6aeac |
Simplify mobile Solo and DM empty states (#8851)
Replace the embedded Solo onboarding block with “No chats yet.” Give Solo and DM empty states matching left alignment and spacing below their headings, with softer text at 65% opacity. Validated with mobile formatting, analysis, the full mobile test suite, and a Flutter widget preview measuring equal 16-point gaps below both headings. ### Preview Actual Flutter widgets with sample data; not a device capture.  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
9c3ba83adb |
Simplify signed-in mobile community onboarding (#8852)
Replace the signed-in mobile welcome screen with **Add a community**: QR scan first, then grey secondary actions for community links, finding saved communities, and creation. Hide **Find my communities** when none are known; refresh on entry/resume and every 30 seconds while active, retaining results on transient failure. Community switcher → Add keeps its two-action flow and uses the same grey secondary link button. Validation: Flutter analysis, full mobile suite (4,014 tests), Builderlab-enabled full suite (4,036 tests), widget interactions, and independent agent review. Draft pending native sign-in and human testing. Snapshots render the production Flutter widgets; native navigation/status bars are omitted. | Communities available | No communities | Switcher → Add | |---|---|---| |  |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
547de61aa5 |
fix(mobile): show channel posts before a burst of join notices (#8798)
## Summary When many people join a channel at once, the mobile app could open that channel and show only a "… joined" line, with none of the channel's posts. You had to scroll up before any messages appeared. With this change, the posts always show first, and the join lines appear among them in the right places. The cause: the app loads the newest 50 timeline rows when a channel opens, and join notices counted toward those 50. A burst of 50 or more joins filled the whole first page. The app combines a run of joins into one line only when it draws the screen, so the user saw a single "Guest 10 joined … and 48 others" line. The app loads older rows only after the user scrolls. This is the mobile port of the desktop app's fix, https://github.com/block/buzz-app/pull/1983, which has already merged. It needs no relay change. - History pages no longer include join notices, so the first page is all posts. - The app reads join notices with a separate read, newest first, 500 per read. It reads down to the oldest post loaded so far, and continues after each older page loads. Like desktop, it stops after 2,000 notices. - Each notice read also reads the reactions on those notices, and any removals of those reactions. A page brings these back for its own rows, but notices are no longer on the pages, so they need their own read. These reads name the channel in `#h`, so a system channel, or an unlisted channel opened by a non-member, still returns them (NIP-CL). Deletions are also read without `#h`, because a deletion can be stored with no channel. Each read names at most 500 target ids, so it stays under the relay's 1 MiB request limit. A notice's reactions show even when they sort below the oldest loaded post. - Posts show as soon as their page loads. The notices follow and are merged into the timeline, so they do not replace rows that arrived live in the meantime. - A notice that is older than the oldest loaded post waits until its page loads. This prevents a join from showing at the top of the timeline with a gap of missing posts below it. - If a notice read fails, the posts still show. The next page load tries the notices again; if a page loaded while the read was running, the read runs once more right away. - The same applies to every system notice (joins, topic changes, channel created or archived), as they share one event kind. ### Related issue AIDA-2328. Desktop fix (merged): https://github.com/block/buzz-app/pull/1983. ### Testing Before (left, older build) and after (right, this branch) on the Android emulator, against a local relay. #general and #4things each have 4 posts followed by 60 joins: https://github.com/user-attachments/assets/53b80942-d8c3-4604-8b95-cc5ca7ab6946  - New `channel_messages_provider_test.dart` cases run against the real channel provider, with only relay responses faked: - A burst of 500+ joins does not hold posts back. If the first notice read fails, the posts still show. A later page read retries the notices and continues the descent. - Notices in the same second as the oldest loaded post follow the post's position within that second. - The descent stops at 2,000 notices. - A notice keeps its reactions, and a removed reaction stays removed. Reactions are read past the relay's 1,000-event page. The fake relay applies the NIP-CL rule, so a read without `#h` finds no channel reactions. - A page that loads while a notice read fails still gets its notices. - `just mobile-check` and the full `just mobile-test` pass at `7a38763af`. Not yet tested on a physical device, or on iOS. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa5f3e7bde |
feat(mobile): show conversation activity by the composer, as buzz-app does (#8765)
## Summary Stacked on #8754. This brings the rest of buzz-app's conversation activity to mobile. buzz-app shows who is active in a conversation in one place, just above the composer (`src/features/messages/TypingIndicator.tsx`). Mobile had its own design: a typing bubble at the end of the timeline that turned into the agent's reply (the "handoff"). This change replaces that design with buzz-app's indicator in channels, DMs, Me and threads. What the indicator shows, as in buzz-app: - **On the right: your agents working here.** Up to four agent avatars, then "+N". "Your agents" are your agent inventory (kind 30177), as in #8754. Tapping the avatars opens the "Working now" list. - **On the left: everyone else typing here.** Typing dots and buzz-app's text: "Alice is typing", "Alice, Bob are typing", "Alice, Bob and 1 others are typing". - **"Working now" list.** Each row shows the agent, "Working" and, in a channel view, where it works: "Channel conversation", or "Thread · <first 60 characters of the root>", with "+N more" after two places. - If the agent works in exactly one thread, the row opens that thread. A thread whose root is not loaded opens by its ID, as buzz-app's link does. A button at the end of the row opens the agent's activity. - Otherwise (in a thread view, in Me, in the channel only, or in several threads), the row opens the agent's activity. As in buzz-app, work in the channel does not count as a thread: an agent in the channel and one thread opens that thread. - "Activity" is mobile's existing `AgentActivitySheet`. - The list closes when the last agent stops. - **Scope** (`current-activity.ts`: "Channel views include threads; thread views do not"): | View | Your agents | Everyone else | |---|---|---| | Channel or DM | Here and in any thread | Here only | | Thread | This thread only | This thread only | | Me | Anywhere | Anywhere | - **Me** works like buzz-app's Solo chat: with one agent working, tapping the avatars opens its activity directly. - **Spoken status:** buzz-app's live status, for example "Jerry, Morty, Summer are working. Alice is typing". The button says "Activity: Morty working" or "Activity: 3 agents working". Changes you will notice compared with #8754 and main: - The typing bubble at the end of the timeline and the bubble-to-reply handoff are removed. #8754's channel typing row goes with them. The root message's reply row from #8754 stays. - In DMs, other people's agents typing in a thread no longer show at the DM level. Only your agents do, as in buzz-app. This reverses part of #8731. - With no composer, there is no indicator. This applies when you are not a member or the channel is archived, as in buzz-app (`ChannelsPage`, and `ThreadPanel`'s `composerRoot`). Before, mobile showed typing in those threads. Less than buzz-app, not in this PR: - Status is always "Working". buzz-app also uses lifecycle evidence (local agent state, kind 24200) and tool labels. Mobile uses typing only, as in #8754. - No hover preview and no keyboard path (Down arrow) into the list. - The indicator sits in the composer dock, so it moves the timeline up a little. In buzz-app it floats over the history. Code: - `conversation_activity_state.dart` holds the scope rules, read from typing and the inventory. - `conversation_activity.dart` is the indicator. `working_now_sheet.dart` is the list. - `channel_detail_page/composer_activity.dart` puts it in the channel composer dock and opens threads. The thread page puts it above its composer. - Removed: `inline_agent_typing.dart`, `agent_reply_handoff.dart`, `agent_typing_bubble.dart`, `typing_avatar_cluster.dart`, `typing_motion_preview.dart`, `channel_typing_indicator.dart`, and their tests. ### Related issue N/A. Reported in Buzz #buzz-bugs (on iOS you cannot tell whether an agent is down or typing). Searched issues and PRs for duplicates: none found. ### Testing Android emulator, local relay, debug build of `8362d46af`. **Channel and thread.** Demo User mentions Morty. Morty, Summer and Jerry (in Demo User's agent inventory) type in the mention's thread. Rick (not in the inventory) types in the thread and in the channel itself. In the video: 1. The channel shows three agent avatars on the right and "Rick is typing" on the left. Rick's thread typing does not show. 2. Tapping the avatars opens "Working now". Each row reads "Working" and "Thread · …". Tapping Morty's row opens the thread. 3. In the thread, the same three agents show, with "Rick is typing" (his thread typing). Rows there open activity: tapping Morty's opens his activity sheet. 4. Back in the channel, Summer, Jerry and Rick stop. Only Morty's avatar is left. Then Morty replies and the indicator goes away. No typing bubble shows at any point. https://github.com/user-attachments/assets/b300e16e-8bec-4489-9ee4-a68c5c2dc30d **DM.** A DM with Morty (in the inventory) and Rick (not). Demo User posts. Both type in its thread, and only Morty's avatar shows. Then Rick also types in the DM itself, and "Rick is typing" shows. Morty replies, and his avatar goes away. https://github.com/user-attachments/assets/3614be5f-3a69-4a1a-9290-d1fad16a5445 The UI tree confirmed each state, for example "Summer, Jerry, Morty are working. Rick is typing", "Activity: 3 agents working", "Morty is working" (agents only), and "Morty is working. Rick is typing" (DM). - Widget tests in `channel_detail_page_test.dart` (Typing indicator group): - Scope table: Me, channel and DM, for a person and for your agent. While the inventory loads, an agent never shows as a person. - The journey from #8754, extended: the composer status and button, the list's places, opening the thread, thread-view rows opening activity, and the list closing when work stops. - Six agents in several places: four avatars and "+2", "+1 more", scrolling to the last row, opening a thread that is not loaded, and opening activity. - Me opens a lone agent's activity directly. People typing: 1, 2 and 3. - Each new check was made to fail on purpose by removing the code it guards. - `just mobile-check` and the full `just mobile-test` passed on `3480ad238` (3983 passed, 28 skipped). - Typing names use the full foreground, so they stay readable in dark themes (review feedback). A test checks the color. - Auto and Cerveau reviewed it adversarially against buzz-app origin/main. Their findings are fixed: the list did not scroll, unloaded threads did not open, the Me behaviour, the non-member indicator, and a double-tap pop. Their last pass on `2c45f3ec0` found nothing. The device run then showed that, with only agents working, the spoken status had no size and Android dropped it. `8362d46af` fixes this, and their review of that fix found nothing. - Not yet tested on iOS or by a human. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
851250524e |
feat(mobile): show your agents working in a thread on its root message (#8754)
## Summary When you mention one of your agents in an ordinary channel on mobile, nothing in the channel showed that the agent was working until its reply landed. The agent answers in the mention's thread, so its typing (kind 20002) is thread-scoped, and the channel timeline hides thread typing. The header working dot that used to cover this was removed in #8052, #8075 and #8082. This change brings buzz-app's behaviour to mobile. In buzz-app, the thread summary under a message shows the viewer's own agents while they type in that thread (`MessageRow.tsx` `useThreadAgents`, described in `docs/agents.md`). Mobile now does the same. The root message's reply row shows the working agents' avatars and the typing dots, and the row appears even under a mention that has no replies yet. It then reads "View thread". Tapping it opens the thread. When the reply arrives and the typing ends, the dots go away and the row reads "1 reply". The details follow buzz-app: - Only your agents count. buzz-app's "your agents" (`agentChoices.identities`) are your agent inventory, which is the kind 30177 records you author with archived agents included, plus your local agents. It does not read the NIP-OA owner tag. Mobile uses the same inventory. An agent that only names you as owner in its profile does not count. People and other agents typing in a thread stay inside the thread view. - Working agents leave the participant stack for their own group: up to two agent-shaped avatars side by side, then "+N", then the dots. - The curved reply branch shows only when the message has replies. - buzz-app's channel indicator by the composer also includes your agents working in any thread ("Channel views include threads" in `current-activity.ts`). Mobile's channel-level surface is the typing row at the end of the timeline. In ordinary channels, that row now also shows your agents that are typing in a thread, with the agent avatar shape. It goes away when the agent's reply arrives. - The spoken labels use buzz-app's strings: "View thread: Morty is working", "View thread: 3 agents are working" and "View thread: 1 reply. Morty working". - Me and DMs already show thread typing inline, so they are unchanged. Code changes: - `TypingDots` is extracted from `AgentTypingBubble`, so the reply row and the bubble share one animation. It respects reduce motion. - `ReplySummaryLink` accepts a nullable summary and a list of working agents with their names. `SmallAvatar` gains an `agent` flag for the working group, and `InlineAgentTyping` a `knownAgents` set so the channel typing row gives inventory agents the agent shape. - `meAgentInventoryProvider` holds the inventory keys. It is read only while a thread in the channel has typing. Each read, failed ones included, is reused for 30 seconds, then read again if still watched, or dropped. So the inventory refreshes every 30 seconds while it is watched, and a short gap between an agent's messages does not query again. - The page selects a "pubkey root" key from channel typing, so typing expiry refreshes do not rebuild it. Less than buzz-app, not in this PR: - buzz-app prefers lifecycle evidence (local agent state and kind 24200 observer turns) and falls back to typing. Mobile uses typing only. So when an agent stops without replying, mobile keeps its dots until the typing expires (8 seconds). A second buzz-app device without that evidence behaves the same way. I found no 24200 publisher in buzz-app itself. buzz-app publishes typing (kind 20002) for the agents it runs. - The channel typing row reads "Morty is typing" where buzz-app's indicator says "working". It has no popover that names the thread and no tap action. It sits after the newest message, so it scrolls away with the timeline, while buzz-app's indicator stays by the composer. ### Related issue N/A. Reported in Buzz #buzz-bugs. Searched issues and PRs for duplicates: none found. ### Testing Android emulator, local relay, build of `3caa17a20`. Demo User mentions Morty. Morty, Summer, Jerry and Rick send thread typing: - Morty is a channel bot in Demo User's agent inventory. - Summer and Jerry are in the inventory but are ordinary members, so only the inventory gives them the agent shape. - Rick names Demo User as owner in his profile but is not in the inventory. In the video: 1. The root message shows Morty and Summer, "+1", the dots and "View thread", with no reply branch. The channel typing row reads "Morty and 2 others" with three agent avatars. Rick does not show. 2. Tapping "View thread" opens the thread before the first reply. Back to the channel. 3. Summer and Jerry stop. Their typing expires, and only Morty is left on both rows. 4. Morty replies. The typing row goes away, the reply branch shows, and the root row reads "1 reply". The UI tree confirmed "View thread: 3 agents are working", "Morty and 2 others are typing", "View thread: Morty is working", "Morty is typing" and "View thread: 1 reply". https://github.com/user-attachments/assets/2237e565-aa48-405a-84ca-39ab9a06b7a6 - The widget test "an agent working in a thread shows on its root message" (`channel_detail_page_test.dart`, Typing indicator group) covers these cases: - A person, and an agent outside the inventory that names you as owner, show no row and no channel typing row. - Inventory agents also show in the channel typing row ("Morty and 2 others are typing", checked in the semantics tree), with the agent shape. That row goes away when the reply arrives. - Three inventory agents show two avatars with the agent shape, "+1", "View thread", no branch, and "View thread: 3 agents are working". - The row opens the thread before the first reply. - After the reply, Morty shows once, the branch shows, and the label is "View thread: 1 reply. Morty working". - With Summer still typing, Morty removed from the inventory stops counting after the 30-second refresh. - Each check was made to fail on purpose by removing the code it guards. - `just mobile-check` and the full `just mobile-test` passed on `3caa17a20` (3996 passed, 28 skipped). - Auto and Cerveau reviewed it adversarially against buzz-app origin/main. Their findings are fixed, and their last pass found nothing. - Not yet tested on iOS or by a human. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f3a7bab098 |
Clarify mobile community onboarding and saved identity selection (#8729)
## Summary Make community onboarding easier to scan: shorten hosted hostname fallbacks, clarify Add Community instructions, and replace raw saved-key buttons with identity cards. Use white pages, grey cards, ghost secondary actions, standard page transitions, and a “Getting ready” screen during joining. Native profile setup keeps a horizontal modal transition. Identity cards load signed name/avatar metadata from saved communities without switching accounts; unavailable profiles use a neutral fallback. Cards show only the avatar, a 16px medium-weight name, and muted npub; community lists are omitted. Retiring profile setup closes its avatar editor too. ### Related issue Follow-up to #8687 and #8599; no duplicate PR found. ### Testing - Formatting and analysis passed. Full suite with hosted-auth defines: 4,019 passed, 5 skipped; includes profile-fetch and cancellation regressions. - Prior iOS device preview and native profile tests passed; hosted CI native Swift checks passed on the preceding head. Latest Dart fixes are not yet reinstalled on the phone. - Simulator walkthrough and owner-tested iPhone preview use sample identities and simulated admission. Sample-data previews; the identity snapshot shows the unavailable-profile fallback rendered by Flutter. Media is attached here only. | Community picker | Add Community | Saved identity | | --- | --- | --- | |  |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
c0c606e2e3 |
Polish mobile navigation and restore community switch animation (#8732)
Removes aggregate unread dots from mobile filters, Inbox, and Solo
section headings. The aggregate-dot removal is an explicitly requested
product change. Solo and Team names match the current `block/buzz-app`
sidebar’s light/dark colours and 400/600 read/unread weights (not this
repository’s older `desktop/` implementation); Solo rows show
owned-agent working dots with reduced-motion support.
Restores the community-switch loading and avatar flight with a leftward
arc to the community menu in its current top-right position.
Mobile uses one fixed light/dark application palette; legacy catalog
theme names do not select navigation palettes. The 600 unread weight
follows [the current desktop sidebar
override](
|
||
|
|
75854111b8 |
fix(mobile): retain invite keys and reject incomplete onboarding (#8753)
Self-hosted invite claims retain the generated identity in secure storage before the HTTP request and reuse it after a lost response or restart. Saving or reopening an existing destination retires the matching journal. Sign-out/removal deletes all matching pending key copies before removing the saved community, preserving the community for retry if cleanup fails. Pairing verifies that relay authentication completed before saving/activating a destination or reporting success. On iOS 16, hosted entry permits recovery and reuse of backed-up identities, including binding a recovered identity with a confirmed backup receipt. Paths requiring a new secure backup explain the iOS 17 requirement before generating or binding a new key. Self-hosted entry remains available. ### Validation - Initial focused entry/recovery files: 83 passed, 1 skipped. Review-fix focused storage, community, invite and hosted files: 82 passed, 1 skipped; extended invite journey: 17 passed, 1 skipped. - Final hosted journey extension: 20 passed, including both bound and unbound recovery with native sealing unavailable. - Isolated mutations removing invite retention, pairing validation, journal removal/re-entry cleanup or restoring the blanket platform gate fail their intended regressions. Restored controls pass. - iOS simulator build passed on the original integration base; final native/device acceptance remains outstanding. - Carl and three specialist reviewers found no source-review blockers at `323cec6ea2`. `a55a246857` adds analyzer fixes, copy cleanup and the recovered-unbound journey coverage. Reviewers did not run tests or the app. - Fixed a push-hook timeout in `4564b3562a`: idle serial queues now release completed futures, avoiding scheduler retention across widget tests while preserving in-flight ordering and failure isolation. The existing dispatcher journey fails promptly before the fix and passes afterward; Carl reviewed this delta with no blockers. Focused dispatcher/invite/storage checks passed, and all 20 hosted journeys passed with hosted auth enabled. - Mandatory pre-push checks passed at `4564b3562a` (scope, branch skew, file size, mobile analysis/tests and crypto package checks); GitHub head verified at that commit. ### Remaining acceptance Draft pending agent/device and human acceptance. Exercise interrupted one-use invite claim and retry across restart, sign-out then reopen without restoring the removed identity, rejected self-hosted pairing, iOS 16 backed-up identity recovery (including binding), and iOS 16 new setup blocked before key creation. Synthetic network/storage boundaries do not prove native durability or live server behavior. Hosted CI results are not claimed here. Optional follow-ups remain outside this patch: pruning provably unadmitted journals, malformed-record recovery, avoiding an iOS 16 dead-end device-key selection, and pre-existing removal/push-revocation ordering. This PR contains three existing-path fixes. The two organization-login fixes depend on #8700 and remain separate: its stale parent conflicts with main in unrelated relay code, and the required branch-skew hook prevents publishing that stack unchanged. This PR does not refresh that parent or change deployment policy. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
aae0081a90 |
fix(mobile): show agent DM typing and balance reply bubble padding (#8731)
Messages with a reply footer now keep even vertical text padding unless they have actual overlapping reactions. Agent typing indicators are now visible in both one-to-one and group DMs when the response is thread-scoped, regardless of Bestie selection. The typing fix originated in #8665 and now applies to every agent DM using the existing agent classification. It retains main's `visibleTypingProvider` filtering. Ordinary channels and human thread-typing signals retain their existing scope; voice recording is not included. Validation: required pre-push hooks passed, including mobile formatting, analysis, tests, native crypto package tests, and file-size checks. Flutter analysis and the full mobile suite pass (3,995 passed, 28 skipped). Widget coverage compares bubble heights with and without a reply footer and checks typing across Solo, ordinary channels, agent DMs, human DMs, and group DMs with agents. Marked ready for review at the owner’s request. Device verification remains pending. To try: open a DM with any agent, including one not selected as Bestie, while it handles a thread-scoped response and check for its avatar and three dots; repeat in a group DM with an agent; compare equal text in messages with and without a reply footer, with no reactions, and confirm equal bubble heights. The broad `just ci` check was stopped during the unrelated desktop Rust dependency build after the mobile pre-push checks passed; no full-repository CI pass is claimed. ### Before / after Flutter widget-harness snapshots with identical synthetic data; not native device captures. Before: the reply-footer bubble has excess bottom padding and the thread-scoped Bestie typing signal is hidden. After: both text bubbles have equal height, and Bestie’s avatar and three dots are visible. | Before | After | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
64312fa5be |
fix(mobile): address #8650 second-pass review (#8691)
Follow-up to #8650. This addresses the items from the [second-pass review](https://github.com/block/buzz/pull/8650#pullrequestreview-5700660448), which arrived after #8650 merged. - **Setting menu semantics.** Options in the shared `BuzzSettingMenu` (model, reasoning effort, and channel TTL) expose their checked state again. The trigger is announced as a button with enabled and expanded state, so a disabled "Reasoning effort" no longer reads as plain text. The `PopupMenuButton` this menu replaced also provided a named route and a "Dismiss menu" action. `MenuAnchor` does not provide them, so this PR does not restore them. - **Remote listing failures.** "Could not load agents" for a failed Builderlab listing now appears only in communities that support remote agents. For example, it no longer appears in a non-hosted community while signed in. - **Add agent during a community reload.** No change needed here. #8693 already disables Add while the page has no community. ### Validation - Added tests: setting-menu checked, button, enabled and expanded semantics; a failed remote listing stays hidden where remote agents are off. Both fail without these changes. - Mobile analysis is clean, and the Agents and shared widget tests pass locally after rebasing onto #8693. - Not covered: VoiceOver on a device. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
a94ebece0e |
Revert "fix(mobile): push every message in your own Solo chats (#8615)" (#8690)
Reverts [#8615](https://github.com/block/buzz/pull/8615).
Mobile push for Solo chats returns to mention-only, matching buzz-app,
which alerts on every message only in DMs. This also removes the
lease-size guards #8615 added, since they existed to fit Solo chats into
the lease.
### Validation
- Clean revert of
|
||
|
|
4b21a10c4b |
fix: don't notify workflow owners through their attribution tag (#8694)
A workflow message names its owner in a marked p tag, ["p", owner, "", "owner"], for attribution. #p live, history and push matching, operator listeners, desktop, mobile and the iOS NSE treat that tag as attribution rather than a mention. A workflow that mentions its owner adds a plain p, so the owner is notified only when actually mentioned. NOSTR.md records the #p exception. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@squareup.com> |
||
|
|
050d545264 |
perf(mobile): skip unchanged message footer layout (#8433)
Rebuilding an unchanged message footer currently schedules another layout even when its overlap value is identical. Only invalidate footer layout when that value changes, preserving layout updates for actual geometry changes. ### Stable release recommendation Exclude from `mobile-release/conference-2026-10`; defer to the following release. The regression test demonstrates eliminated redundant layout, but the effect on navigation latency has not been quantified. ### Validation The added widget regression checks that unchanged footer rebuilds do not repeat layout and changed overlap still updates geometry. The independent footer build passes the full mobile suite, analysis, and native iOS Simulator build. Channel scrolling, opening an existing reaction list, and thread navigation were exercised on iOS 26.5 Simulator. The full local repository gate previously encountered an unrelated, unchanged desktop subprocess timing test failure; that test passed in isolation. All PR CI checks pass, including the native iOS suite. <!-- buzz-review-completed --> Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
be1ac392e8 |
perf(mobile): render inbox and search previews as plain text (#8675)
### Summary Inbox rows and search results show each message as a 2-line preview. That preview was a capped `MessageContent`, so gpt_markdown parsed and laid out the whole source (syntax-highlighted code blocks and measured tables included), even though only two lines are painted. A native trace on an iPhone 12 mini attributed a 102 ms inbox-open frame mostly to this work. Previews now skip Markdown entirely. `messagePreviewSource` takes only the leading lines a preview can show, and at most the first 4,000 characters of a single long line, so link normalization, mention binding and emoji lookup never scan the rest of a long message. `messagePreviewPieces` is then a linear pass over those lines, similar to desktop's `getMarkdownPreviewText`. It drops block syntax (headings, quotes, list markers, rules, table dividers, code fences) and unwraps inline emphasis and `<u>`, honoring backslash escapes and keeping inline (including multi-backtick) and fenced code as literal text. It also replaces links with their label. The new `MessagePreview` widget renders the result as a single `Text.rich` with `maxLines` and an ellipsis. Tokens keep the look they have in the thread view: - A bound mention is the same `MessageMentionPill` chip, with the bot icon for agents. An ambiguous mention stays as text. - `#channel` tokens and `buzz://channel/...` links are the same channel token chip. - A `buzz://message`, repo, PR or issue link that shows its own URL previews with the thread view's short label, such as `general · abcdef01`. Both use the shared `buzzLinkLabel` helper. - Custom emoji render as inline images. Only shortcodes the message names are looked up in the palette. - Media links show as "Image", "Video" or "Audio". Chips in previews are not tappable, and tapping the row opens the message. Bold, italic, inline code and link styling are no longer shown in previews. The timeline and thread view are unchanged, and so is the profile status line, which still uses `MessageContent`. This replaces this PR's earlier approach of cutting the Markdown source before parsing. That approach needed delimiter-balance heuristics, and a security review found a real case (`**` inside a link label) where the heuristic misfired. ### Validation - `preview_text_test.dart` is a table test of the parser: block syntax, emphasis that must and must not unwrap (`snake_case`, a lone `*`), inline and fenced code, unpaired backticks, links, tables, the line limit and the long-line cap. It also covers mentions (bound, ambiguous, inside emphasis, link labels and code), custom emoji (including adjacent `🅰️b:`), channel tokens and links, message and repo link labels, and media. The long-line row also asserts that `messagePreviewSource` returns a bounded prefix. - `message_preview_test.dart` checks the widget: no `GptMarkdown`, one capped and ellipsized paragraph, mention chip, custom emoji image, channel chip, media label and empty content. - Existing inbox and search tests updated to find `MessagePreview`, including the agent-chip test in search. - `flutter analyze` and the full mobile test suite pass at |
||
|
|
762adad54f |
fix(mobile): require lowercase hex in native verification (#8702)
Follows [#8657](https://github.com/block/buzz/pull/8657) with the three optional notes from Carl's [review](https://github.com/block/buzz/pull/8657#pullrequestreview-5703482477). - **Lowercase hex only.** `verifySchnorrHex` now rejects uppercase pubkey, message and sig hex, and `verifiedNostrEventFromJson` rejects an uppercase sig, as the pure-Dart `bip340` decoder does and NIP-01 requires. (The `nostr` package already lowercases an event's pubkey when it parses one.) Before this, a key holder could publish a second, uppercase encoding of the same signature and it would verify. `_decodeSecretKey` still accepts either case. - **Bestie record.** `decodeDefaultAgent` now verifies with `verifiedNostrEventFromJson`, like the other Solo `me_*` records. - **Rejection tests.** The "other pubkey" row now recomputes the event id, so it reaches the native Schnorr check instead of failing on the id. Every row now asserts its `EventValidationReason`, a new row covers an uppercase sig, and `verifySchnorrHex` is checked with uppercase pubkey, message and sig. ## Testing - Making either lowercase pattern accept uppercase again fails the tests. - `just mobile-check mobile-test` passed on this change before it moved onto main. On main, the crypto and Solo/me tests pass. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fffd8ca957 |
fix(mobile): recover Solo sync silently instead of showing a retry banner (#8701)
## Problem The mobile Solo section could display "Live updates stopped. Your saved chats are still available." with a **Retry live updates** button. Any failed Solo head refresh raised this banner. That includes the refresh that runs on app resume before the relay has reconnected, and an 8s history timeout. In those cases the live subscription itself was still healthy. A later successful refresh did not clear the banner, so it stayed up until someone tapped Retry. ## Change - Remove the banner along with `MeSync.syncClosed` and `meSyncWarningProvider`. Saved chats stay visible the whole time. - `meGroupsProvider`: on app resume, an interrupted reader (terminal CLOSED or a failed read) resubscribes on its own while the relay is connected. Before the relay reconnects, the existing rebuild-on-reconnect still owns recovery. A healthy reader keeps doing a plain head refresh. - Home's pull-to-refresh (`ChannelsPage` with the Solo sliver) now also restarts the Solo reader. Before this change, that path refreshed channels only, so in the shipped layout the banner was the only manual recovery. - The no-snapshot `_MeRetry` state and the offline-save warning are unchanged. ## Validation - `flutter analyze` is clean. - The pre-push `mobile-checks` passed. - The journey step in `offline_save_page_test.dart` closes the subscription, cycles background and foreground, asserts a new subscription, and checks that a newer event delivered on it is saved. Removing the `subscriptionClosed` recovery branch fails it. - The new `channels_page_test.dart` case drags to refresh with a Solo sliver and asserts the Solo reader restarts. Removing the invalidation fails it. - Not yet exercised in the running app. That is checklist steps 2 and 3. Reviewed by the Royal Court agents (provider lifecycle, recovery/UX, independent challenge); the one blocker (Home pull-to-refresh) is fixed here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
ccfc1490e4 |
perf(mobile): verify signatures with native libsecp256k1 (#8657)
The app already verifies signatures in a few places: NIP-OA agent
profiles and owner attestations, Solo `me_*` records (agent inventory,
archived agents, assistants, sync), and pairing events. Each used
`nostr.Event.fromJson`'s pure-Dart BIP-340 verify, about 11.2 ms per
event on an iPhone SE (2nd gen, Profile). This routes them through
`verifiedNostrEventFromJson` and `verifySchnorrHex`, which keep the same
checks (timestamp range, canonical id, Schnorr signature) and the same
`nostr` exception types, with the signature check in libsecp256k1 (40
µs). NIP-OA owner authorizations are now signed with `signSchnorrHex`,
which removes the last pure-Dart Schnorr call in `lib`.
Incoming relay events are still not verified; that stays a separate
decision.
A native trace showed pure-Dart Schnorr verify in `me_sync` and `nip_oa`
costs 100-125 ms per foreground. This PR moves exactly those sites.
### Validation
- Added tests: the native verifier accepts a valid event and rejects
tampered content, a wrong signature, a different pubkey, malformed and
short signatures, and a zero timestamp, each also rejected by the
pure-Dart verifier. Hex Schnorr helpers are checked in both directions
against `nostr.Schnorr`.
- Focused existing validation: crypto, NIP-OA, Solo/me, pairing and
agent tests pass. `just mobile-install mobile-check mobile-test` passes
at
|
||
|
|
ff284d00d3 |
mobile: stop typing prune ticks from rebuilding channel pages (#8695)
### Summary Typing in the mobile composer felt slower than a native iOS text field. A device trace on an iPhone 12 mini showed the cause: while anyone in the channel is typing, the whole channel page rebuilds once per second, including the visible message list. - `ChannelTypingNotifier._prune()` runs on a 1 s timer and assigned a new list to `state` on every tick, even when nothing had changed. - `_ResolvedChannelDetailPage` watched the whole list and only filtered out the current user afterwards, so every tick rebuilt the page and its `PositionedList` (12 to 35 ms per frame, 8 to 19 ms for the message list alone). - Your own typing indicator echoes back from the relay, so the timer kept running the whole time you typed. In a 30-key trace, 33 of the 35 frames over 12 ms were these rebuilds, 1.00 s apart. ### Fix - `_prune()` publishes only when the visible entries change. `TypingEntry` now compares by value. - The channel and thread pages watch `visibleTypingProvider`, a `VisibleTyping` value that already excludes the current user and applies the page's scope. Your own echo refreshes its expiry every 3 s, which used to rebuild the page. Now it doesn't. Other `channelTypingProvider` consumers are unchanged. ### Related issue None found. ### Testing - Provider tests: a typing entry held across five prune ticks notifies listeners once, and the pages' shared `visibleTypingProvider` is not notified by the user's own refreshed echo but is by another user. Breaking either guard fails its test. - `just mobile-check` and `just mobile-test` pass at |
||
|
|
0cde1fe2f4 |
Make mobile Agents a full-page owned-agent directory (#8693)
Replace the mobile Agents sheet with a full-page directory of verified owned agents. Add dedicated creation and avatar pages with catalog-backed model selection, reuse the profile emoji/photo/camera controls, and expose Builderbot model settings while labeling desktop agents “Manage on desktop.” Share one account-scoped model catalog across editable cards and retries. Choose Bestie from the agent profile using the shared encrypted main-agent record (text-only action, filled star once selected), and move confirmed deletion to a top-left trash control with native iOS glass. Include uploaded avatars in owner registrations so desktop can display them. Validation: mobile analysis and all 3,958 hosted-enabled app tests passed; default-build tests run by the push hook. Earlier iPhone build installed and iterated with the user; the current iOS build succeeds, but final changes still need a device pass. Independent review found no new blockers. Actual camera capture and live Bestie sync remain unconfirmed; the existing shared `main-agent` wire format and Bestie terminology are retained. ### Snapshots Widget-harness captures with test data (not native iOS chrome). | Agents directory | Image avatar editor | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
3dbc95c3f6 |
perf(mobile): compute NIP-44 ECDH with native libsecp256k1 (#8656)
`ecdhSharedSecret`, the input to every NIP-44 conversation key
(preferences, read state, reminders, themes, mutes, stars, sections,
sort, pairing, agent observer), used pointycastle's BigInt point
multiplication. That costs about 5.9 ms per call on an iPhone SE (2nd
gen, Profile) and is variable time in the secret scalar. It now calls
libsecp256k1's constant-time `secp256k1_ecdh` through `ecdhX` from #8654
(39.8 µs on the same device), returning the same raw x-coordinate NIP-44
requires. The decoded secret key bytes are zeroed after the call.
Invalid keys now throw `ArgumentError` instead of pointycastle's errors;
callers already treat any throw as a failed derivation.
### Validation
- Added tests: `ecdh_test.dart` compares the native result with the
previous pointycastle algorithm for 32 random key pairs, checks the
NIP-44 v2 `get_conversation_key` vector (secret 1, peer pubkey of secret
2), and checks rejection of a zero secret, an off-curve public key and
wrong lengths.
- Full mobile analyze and test suite pass after rebasing onto main at
|
||
|
|
e922dc08ff |
Unify mobile onboarding and community entry screens (#8687)
## Summary Use consistent fullscreen screens for identity setup, community selection, joining, and Add Community. Restore all memberships and open the selected community; share the community grid with the switcher. Add QR-first choices, animated link entry with 12px keyboard clearance, a two-finger double-tap pairing toggle, and the centered reversing bee loader. Preserve cancellation, Back, and reduced-motion behavior. Add intentionally omits community creation and Builderlab sign-in: this route follows sign-in and the existing-community picker. ### Related issue Related: #8628 (native profile setup). No duplicate fullscreen onboarding PR found. ### Testing - Full standard and hosted-authentication mobile suites, static analysis, and required push checks passed. - iPhone preview iterated with Kenny; final simulator walkthrough below uses synthetic data and does not authenticate or save account changes. - Independent review completed; regressions cover cancellation during loading, Add recovery after failed/conflicting memberships, retrying skipped communities, and explicit picker stages. Walkthrough recorded before the copy and recovery followups: https://github.com/user-attachments/assets/bfea3ad3-1dde-40c4-8748-44c84226c292 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
8abcf2ea2a |
fix(mobile): keep read state across reconnects and resume (#8672)
### Context Every app resume and relay reconnect tore down `ReadStateManager` and built a new one. `ReadStateNotifier.build()` watched the whole `relaySessionProvider` state, so each status change (disconnected, connecting, connected) rebuilt the notifier. On an iPhone 12 mini each resume re-ran the full read-state fetch and decrypt (about 19 to 24 decrypts, 60 to 95 ms). The rebuild also cleared `_forcedUnreadContexts`, so a message or channel the user marked unread lost that flag on any connection change. The watch dates from #422, when the manager was built with `remoteEnabled` tied to the connection status. #802 made `remoteEnabled` constant and added the `ref.listen` that calls `reinitializeRemote()` on connect, but left the watch behind. ### Change - Watch `relaySessionProvider.notifier` instead of its state. Config, nsec, community, pubkey, crypto and a replaced session still rebuild. Status changes don't. - Reconnects re-sync through the existing listener (`reinitializeRemote()`: re-fetch, then re-subscribe, preserving the history and live overlap). - Re-syncs are serialized. One runs at a time after any pending `initialize()`, and reconnects that arrive while one waits share it. Overlapping passes could otherwise each install a live subscription and leak one. - A connect that lands while the first `initialize()` is running (the cold-start connect) skips the re-sync when that initialize fetched and subscribed successfully. Without this, cold start did the read-state work twice. ### Validation - Provider journey: disconnected, connecting and reconnecting cause no refetch, no re-subscribe and no readiness drop, and the forced-unread flag and read mark survive. Connected runs exactly one refetch and one live-subscription swap. A second reconnect behaves the same. Account and community switches still rebuild and clear forced flags. - Manager tests: a connect during a successful `initialize()` does not sync again; one during a failed `initialize()` does, and leaves one live subscription; three reconnects during a running re-sync produce one more shared pass and one live subscription. - Falsified: restoring the old watch fails the reconnect journey (`isReady` false, flag lost). The previous version without serialized re-syncs fails the overlap test (5 passes instead of 3). - `just mobile-check` and `just mobile-test` pass. iPhone 12 mini (Profile builds of base and this branch, real community over the production relay, instrumented manager lifecycle and live-subscription counts): | | base | this branch | | --- | --- | --- | | Resume after 3 s, 10 s (x3), 60 s background | 3 new managers and 3 initializes per resume | no new manager, 1 re-sync | | Live read-state subscriptions after resume | 1 | 1 | | Channel marked unread, then 10 s / 60 s background | flag lost after the first resume | still unread after both | | Read-state work per resume (median) | ~24 decrypts, ~176 ms main isolate | ~18 decrypts, ~125 ms | | Unread badges | same bold set | same bold set | The first device run also found the doubled cold-start sync, fixed above. A re-check on this head: | Cold launch | base | first version | this head | | --- | --- | --- | --- | | Managers built | 3 | 1 | 1 | | Read-state syncs | 2 initializes | 1 initialize + 1 re-sync | 1 initialize | | Decrypts | 19 | 30 | 15 | | Main-isolate ms in read-state code | 124 | 185 to 197 | 94 to 98 | Four resumes on this head: no new manager, 1 re-sync and 1 live subscription each, 17 to 20 decrypts. A foreground socket drop was not exercised on the device; every resume there was a full disconnect and reconnect. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8161a7cad0 |
perf(mobile): derive public keys with native libsecp256k1 (#8655)
Follows #8654. Deriving an x-only public key from a secret key with `nostr.Keys(secret).public` costs about 5.6 ms in pure Dart on an iPhone SE (2nd gen, Profile). The app does it in 10 places, including relay config, the signed-event relay's `pubkey` getter, push lease checks, status updates, and the settings identity row's `build` method. This routes all of them through `derivePublicKeyHex` from #8654, which takes 16.5 µs on the same device. Behavior is unchanged for valid keys: output is the same lowercase hex. For a malformed or out-of-range key the thrown type changes from nostr's `InvalidKeyException` to `ArgumentError`; every affected site either catches all errors or rethrows its own `FormatException`/`StateError`, and nothing in `lib` or `test` matches on the old type. Sites that need a full `nostr.Keys` object (key generation, `AccountIdentityStore`) are unchanged. ### Validation - `derivePublicKeyHex` is checked against `nostr.Keys` for 64 random keys in #8654's `event_signing_test.dart`. - Full mobile analyze and test suite pass on this branch after rebasing onto main, including the package's own tests. - iPhone 12 mini, Profile build of this stack (with #8656 and #8657 on top): paired to the internal Buzz community through Desktop; relay auth, identity row, avatar, and channel/DM/inbox loading all worked under the derived key. buzz-review-completed (agent review, Codex and security review clean; device validation above; human app test not applicable under the mobile testing exception) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
770ab7ff82 |
perf(mobile): decode relay query responses off the UI isolate (#8669)
### Context Profiling on an iPhone 12 mini (Profile build, real community) showed `RelaySessionNotifier._postForEvents` producing contiguous main-isolate busy runs with p90 110 ms and max 124 ms (27 runs of 50 ms or longer). Stacks were dominated by `jsonDecode` of `response.body`, with `NostrEvent.fromJson` smaller. The whole HTTP response decode ran on the UI isolate: gzip inflate (done by `dart:io`), the UTF-8 decode behind `response.body`, JSON parsing, and event parsing. ### What changes - Successful `/query` and `/profiles/search` responses (the only two callers of `_postForEvents`) are now decoded in a single `Isolate.run` from `response.bodyBytes`: inflate, UTF-8, JSON, validation, `NostrEvent.fromJson`. The parsed events come back by ownership transfer (`Isolate.exit`), not a copy. - Gzip: the query transport now sends `Accept-Encoding: gzip` and sets `autoUncompress = false`, so compressed bodies reach the isolate still compressed. Whether bytes are gzip is decided by the gzip magic bytes, so a transport that already inflated (or an injected test client) still works. Error bodies are inflated inline (small). - Unchanged: `FormatException` messages, the malformed-event wrapping, `RelayException.body`, and the rate-limit gate reading the error body. - Fencing (Review-Proven Rule 2): the `_disposed` / `_ageRestricted` / `_contextGeneration` check still runs before the decode and now runs again after it. If the session retires during the decode, the query reports `StateError` even when the decode itself failed, matching the old ordering where a retired session never decoded. ### Design notes - `Isolate.run` per response instead of a long-lived worker: no lifecycle or disposal to manage, no idle isolate heap on a small phone, easy to fake in tests, and the same shape as `parseProfileEventBatch`. The main-isolate side of a spawn measured at microseconds, so there is no size threshold; small responses take the same path. - Event id and signature verification does not happen on these events on the main isolate today (nothing in the mobile app verifies queried events; NIP-OA verification in profile parsing is already in an isolate), so nothing was moved. - Other HTTP decode sites found and left alone: invite create/join, hosted join policy, BuilderLab account, media upload response, community icon lookup, dev push lease NIP-11, archived-agents. These decode small, rare responses (one small JSON object each), where an isolate adds complexity without a measurable gain. Media and avatar byte downloads do no JSON decoding. The mobile app has no `/count` caller. ### Validation Added tests (`relay_session_test.dart`, through the production `queryRelay` / `searchProfiles` path with the real isolate decoder): parsing with non-ASCII content; profile search; gzip success body and `Accept-Encoding`; gzip error body feeding the rate-limit gate and `RelayException.body`; malformed JSON; non-list body; non-object element; malformed event; invalid UTF-8; error status skips decoding; session disposed, age-restricted, or retired-before-decode while a decode is outstanding. The fencing tests are falsifiable: removing the post-decode check fails the disposed and age-restricted tests; removing the retired-wins-over-decode-error branch fails its test; removing the pre-decode check fails the never-decodes test. `FakeRelay` (test helper) now injects an inline decoder, because `fakeAsync` cannot deliver isolate results; it calls the same parse function. The default transport still honors `http.runWithClient`, which the hosted journey tests use to fake the network (`relay_http_query_client_test.dart`; fails without the fix). `just mobile-check` and `just mobile-test` pass. Host benchmark (AOT executable on an Apple silicon Mac, median of 25, copy of the parse logic, main-isolate stall measured with a 0.5 ms heartbeat timer): | Response | Main-isolate blocked before | after | | --- | --- | --- | | 500 events, ~630 KB | 2.4 ms | 0 ms (max gap 0.05 ms) | | 500 events, ~630 KB gzip | 3.2 ms | 0 ms (max gap 0.19 ms) | | 500 events with 50 20 KB avatars, ~1.6 MB | 5.1 ms | 0 ms (max gap 0.28 ms) | | 500 events, ~4.5 MB | 14.0 ms | 0 ms (max gap 0.27 ms) | | 500 events, ~4.5 MB (827 KB gzip) | 17.4 ms | 0 ms (max gap 0.35 ms) | Total latency is unchanged within noise (for example 14.5 ms to 14.9 ms). The host is much faster than the 12 mini, so absolute device savings should be several times larger. On the iPhone 12 mini (Profile builds of base and this branch, real community over the production relay, two runs per scenario; main-isolate stalls measured with a 4 ms heartbeat timer, since the wireless device tunnel has no VM service): | Scenario | Main-isolate `_postForEvents` time, base | this branch | Max stall, base | this branch | | --- | --- | --- | --- | --- | | Cold start + 60 s (~16k events, 7.8 MB gzip) | 282 to 295 ms (max 116 ms) | 0.01 ms | 147 to 150 ms | 100 to 102 ms | | Inbox + search (~7k events) | 122 to 123 ms (max 111 ms) | 0.01 ms | 138 to 142 ms | 97 to 99 ms | | Open 6 channels (558 events) | 5 to 6 ms | 0.01 ms | 88 ms | 86 to 87 ms | | Background 10 s + resume (~7k events) | 118 to 124 ms | 0.01 ms | n/a (suspension) | n/a | - The production relay serves gzip (about 4.4:1; the largest catch-up response is 3.15 MB on the wire, 13.9 MB inflated, 7k events). Inside the isolate that response takes 30 to 47 ms to inflate, 39 to 55 ms for UTF-8, 65 to 105 ms for `jsonDecode` and 7 to 32 ms for `NostrEvent.fromJson`. - Many 16 to 90 ms stalls remain on both builds; they come from other work (mostly markdown build and layout), not response decoding. - Home, channels, inbox, search, profile and a thread render the same on both builds. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
a7e65c91da |
feat(mobile): choose remote agent models and gate agents on Builderlab sign-in (#8650)
Follow-up to [#8626](https://github.com/block/buzz/pull/8626). Mobile remote-agent creation now matches desktop. - **Model and reasoning effort:** Create agent has Model and Reasoning effort menus. Their options come from the remote service's catalog, and nothing is preselected (same as desktop). The server checks the selection against the catalog again before registering, and saves it on the agent. - **Builderlab gate:** creating a remote agent requires a hosted community and Builderlab sign-in. The Agents directory, community bots and the Bestie chooser stay available without it. Signing out closes an open Create agent sheet; the Agents sheet stays open. - **Enrollment:** `attest-agent` enrolls the agent in the requested community, as on desktop, so the separate `add-buzz-subscription` call is gone. - **Quieter progress:** spinners replace the progress bars and the "Working…" label. Status and success text ("is ready", "Remote agent deleted.", setup steps) is gone, and only failures show text. Refreshing an already-loaded agent list no longer blanks it. - **Shared setting menu:** `BuzzSettingMenu` is a bordered setting row that opens a Material 3 menu spanning the row's width. Opening it dismisses the keyboard. The channel-TTL setting in Create channel uses it too. Agent avatars and QR/link-based creation follow in stacked PRs. Editing existing agents comes later. | Agents | Create | Model menu | | --- | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--1-agents.png" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--2-create.png" width="240"> | <img src="https://raw.githubusercontent.com/block/buzz/d0bee87c45d30b9bdd2163a75966ccd5d64f322a/pr-8650--3-model-menu.png" width="240"> | ### Validation - Checked on the staging iOS simulator with a Builderlab account: the Agents list, the Create sheet, and the model and effort menus (including with the keyboard open). - Added tests: catalog-gated model selection (including switching models clears an unsupported effort), the Builderlab sign-in gate, controller creation and resume with a model, a failed listing reported instead of an empty registry, and create sheet busy and failure states (including the catalog reload after a rejected create). - The hosted-only agent tests are skipped without Builderlab defines, which `mobile-test` doesn't pass. They pass locally with the staging defines (20/20). - Not covered: Android. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
39b8b2b6a6 |
feat(mobile): unify Messages navigation with Solo and Team filters (#8649)
## Summary Unify mobile conversations in Messages with All / Solo / Team filters, a collapsing header, and separate Search, New, and default-agent shortcuts. Move workspace actions into the community menu and keep collapsed sections intact across filters. Align Inbox and list typography, spacing, and muted controls. Preserve native glass layering through navigation and give every filtered list enough clearance above the footer. The default-agent shortcut reads the shared encrypted preference and opens an existing chat immediately. When unset, it opens Agents; a star action selects the default through a production signed, self-encrypted preference writer. Save failures remain visible and retryable. Android now uses rounded navigation controls and filters, optically balanced search icons, and expanding New/community menus with leading Tabler icons matching iOS. Preset avatars retain transparent WebM animation through an Android WebView renderer, with lifecycle pausing and poster fallback. ### Related issue None found in searches for mobile navigation issues and open PRs. ### Testing - Android follow-up: ARM64 debug build, Dart analysis, and emulator checks passed for menu placement, selection, icon alignment, search, and avatar playback/pause/resume. The avatar implementation passed 3,840 Flutter tests and native Android tests before subsequent styling iterations. Automated tests for the later menu/icon iterations were skipped at the author’s request; those earlier results do not cover the final styling changes. - Mobile formatting/analysis and full test suite (3,838 passed, 23 skipped, plus 4 push-configuration tests); regression journey covers filtering, collapse retention, and scrolling the last Solo row above the footer. - iPhone 18 Pro simulator (Honeycomb): filters, menus, Inbox, Invite/Back, Switch Community, and default-agent chat/Back exercised. iOS release build and Pairing Preview install/launch passed during iteration, preserving the saved profile. - P1 follow-up: production writer-to-reader widget journey covers selection, publish failure/retry, and the unset footer opening Agents. Simulator Agents open/close passes; its saved community has no selectable agents, so live selection was not verified there. - Independent agent review completed; its Solo footer-clearance finding is fixed. The final clearance adjustment is covered by widget tests; physical-device acceptance of that adjustment and hosted CI remain pending. Final Messages layout:  Switcher layering during iteration (before → after): | Before: filter edge showing through | After: Home hidden behind switcher | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
7c165c3718 |
Smooth profile avatar editor transitions (#8640)
Smooths profile avatar editing with concurrent 200 ms entry/return transitions, fixed-size circle movement between tabs, and a crossfade to the dotted placeholder on cancel. New empty avatars open on Emoji, ready to save. Review fixes: keep the suggested emoji separate from the Image fallback, and preserve native editor transparency across appearance changes. Regression journeys cover empty Image previews, retained Emoji selection, and light/dark updates. Validation: focused Flutter journeys pass; restoring the old baseline fails both fresh-editor regressions. At `bffb83f5e`: full Flutter suite and push checks passed; all 107 native tests passed on the iOS simulator. Prior iPhone visual approval covers the transition work; these review fixes still need a device check. Follow-up to #8628. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
974495fd4a |
fix(mobile): resume reconnect replay from the last live point and spread reconnects (#8659)
## Summary A relay rolling deploy closes every socket with 1012. On reconnect, mobile replayed each live subscription's **original filter** (startup `since`, full `limit`) unless that subscription had received an event, so quiet subscriptions re-read history back to app start every time. Reconnects used a fixed 1s delay with no jitter. The delay also reset to 1s on every successful connect, so a client bounced from pod to pod during a deploy kept coming back at the same moment as every other client. **This PR makes two changes:** 1. **Resume cursor.** If a subscription reached EOSE on a connection, it records `liveUntil`: the time of the last frame received from the relay on that connection, capped at the clock at close. The replay then uses `since = max(original since, lastSeen − 5s, liveUntil − 960s)`, and never goes past the filter's `until`. - **Why 960s:** the relay accepts `created_at` up to ±900s from its clock (`MAX_TIMESTAMP_DRIFT_SECS` in `crates/buzz-relay/src/handlers/ingest.rs`). An event stored after the close can therefore be backdated by that much, and the extra 60s covers client clock skew. - **Only relay frames count as liveness.** Close and error paths never extend `liveUntil`, so a close noticed after the device sleeps doesn't overstate how long the subscription was live. - **Ambiguous EOSE is ignored.** If a subscription has a second REQ on the same socket, that subscription doesn't advance `liveUntil` for the rest of the socket. A CLOSED ends liveness for the rest of that socket, because the retry is a second REQ. Subscription IDs are never reused, so an old EOSE can't mark a re-subscribe as live. - **No cursor:** a subscription that never reached EOSE keeps its previous cursor, or replays as before. 2. **Reconnect spreading.** - After a 1012 or 1001 close, the first attempt waits a uniform random delay in [0, 10s]. A client already backing off waits the longer of that delay and its backoff. - Ordinary backoff gets symmetric ±25% jitter, capped at 30s. - The backoff resets only after a connection stays up for 60s, not on connect. This matches desktop. - Foreground resume and a manual `reconnect()` still reset to the base delay, as before. **Simpler?** The pure reconnect math (cursor, jitter, spread, stability) lives in a new `relay_reconnect_policy.dart` and is table-tested on its own. `relay_session.dart` was near its 1200-line cap, so its five small private bookkeeping classes moved verbatim to `relay_session_subscriptions.dart`. Net concept count is unchanged. **Identified debt (not changed here):** - Dart's WebSocket ping/pong isn't visible to the session, so a long-idle but healthy connection that drops replays back to its last application frame − 960s. This is still bounded, and much cheaper than replaying from app start. - The unread catch-up in `channels_provider.dart` runs one REQ per active channel (`since = readAt + 1`, `limit: 1000`) on reconnect and every 60s. - A retryable CLOSED is retried with a 30s cap, no jitter and no attempt limit. - `lastSeenCreatedAt` advances on stored events received before EOSE. If a relay returns stored events newest-first and the socket dies before EOSE, the next replay can skip older stored events. - The `lastSeen − 5s` term keeps its existing gap for events backdated after a close. ### Related issue N/A. This is part of a set of reconnect-load reductions. Companion PRs: relay read-pool warm (#8651), desktop (#8652), agent harness (#8653). ### Testing - Mobile gate on a Linux workstation, at the head SHA: - `dart format --set-exit-if-changed`: pass. - `flutter analyze`: no issues. - Gateway recipe tests: pass. - `flutter test`: 3881 pass, 1 fail. `push_gateway_origin_validator_test` fails the same way on `main` on that host, due to a host git-config error. - New `relay_reconnect_resume_test.dart`: - pure table tests for the cursor and the delay - a real-socket test that reads the 1012 close code - journeys through `RelaySessionNotifier`. They check that a close or error after an hour asleep doesn't extend liveness, that a second REQ or a delayed EOSE for an unsubscribed ID can't advance the cursor, and that a backdated event inside the 960s window is still replayed. - 22 production mutations, each failing the new tests. They cover every cursor term, the 960s window, the `until` clamp, the liveness cap, the ambiguous-EOSE guard, CLOSED handling, jitter, the 1012 spread and floor, the backoff reset, and close-code capture. - `fake_relay.dart` now draws a fixed jitter midpoint, so existing journey tests stay deterministic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
bad9d26142 |
fix(mobile): push every message in your own Solo chats (#8615)
## Summary Mobile push only covered Solo chats through explicit mentions. Solo chats are private streams rather than DMs, and the push lease subscribed channel-wide only to DMs. Agent replies in your own Solo chats therefore never woke the phone. The lease now subscribes channel-wide to the viewer's own joined, unarchived Solo chats, using the same predicate as the Solo tab, including older unlabelled Solo chats listed in the encrypted Solo record. Self-authored messages stay excluded through the existing lease ignores. Muted Solo chats stay out of the lease, like muted channels, so they push only on mention. The lease waits while the Solo record loads so an unresolved record never drops legacy Solo chats. If that record fails with no cached copy, DM and mention updates proceed without the legacy chats, which the Solo tab cannot list either, and they return when it recovers. Solo chats created by someone else still push only on mention, matching how they appear in the channel list. A lease is bounded by its channel count (750), by the relay's advertised plaintext and content sizes, and by NIP-44's 65535-byte plaintext ceiling. DMs always stay, as before. Solo chats fill the remaining room most recent first, sized with the publisher's own lease encoding, the exact NIP-44 payload length and the largest endpoint the relay accepts. Sizes are computed rather than built, so a relay's advertised limits never drive an allocation. Solo chats beyond the room push only on mention instead of failing the lease update or publication, and Notifications settings says how many. A relay too small even for DMs and mentions fails push setup visibly instead of saving a lease it would reject. Publication accepts only endpoint grants in the gateway's JSON-safe alphabet, so the sizing assumption holds. Thread replies outside DMs still do not push unless they mention you. That is a separate gap. ### Related issue None found. ### Validation - Added test: the desired lease includes joined DMs and the viewer's own Solo chats, and includes legacy Solo chats from the Solo record, and excludes archived and muted Solo chats, other people's Solo chats, and unlabelled private streams. It fails without the fix. - Added test: the sync provider holds the desired lease while the Solo record loads, proceeds without legacy chats if the record fails, and adds them once it arrives. It fails if loading is treated as empty or if a failure blocks updates. - Added tests: over the channel count, and over each size limit (relay plaintext, relay content, NIP-44 plaintext), the lease keeps every DM and exactly the newest Solo chats, reports the rest as omitted, and is accepted by `publishBuzzDevPushLease` with a grant at the relay's endpoint limit, while a max-length grant that JSON would escape is rejected as not a gateway grant. A relay advertising a 2^50-byte endpoint limit is sized without allocating. Each row fails if its guard is removed. - Added test: a relay whose content or plaintext limit cannot fit even the lease without Solo chats rejects that lease at publication, and the provider fails setup instead of saving it. It fails if the guard is removed. - Added test: the NIP-44 payload length matches real encryption at every padding boundary up to 65535 bytes. - Added test: Notifications settings shows "Older Solo chats notify only on mention" with the omitted count. It fails without the row. - Mobile lint and the full mobile test suite pass. - Not exercised end to end on a device. A signed dev build was installed on a test iPhone against a personal dev relay, but the phone's dev identity owns no assistant there, so the device could not create a Solo chat without exporting its key. This will be validated in dogfood. The relay matcher already handles channel-wide `#h` leases for DMs, and Solo messages use the same kind and scoping. 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> |
||
|
|
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> |
||
|
|
d6196a16c1 | fix(relay): revoke invitations and restrict claims (#8237) | ||
|
|
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> |
||
|
|
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 (
|
||
|
|
5c2ea2e507 |
feat(mobile): add full-screen profile setup with inline avatar editing (#8628)
Replace the iOS profile setup sheet with a full-screen name → avatar flow. Require a display name, enable avatar Continue after Done, and let Skip save the name without an avatar. After a failed save, Not now continues without saving the draft. Embed photo, emoji, and animated avatar editing with native Back/Done, matching 220-point previews, a rounded-notch pencil edit badge, an entry haptic, equal emoji-preview spacing, and 200 ms transitions that move the avatar into its editor position without resizing. Apply appearance changes live across setup and editing while preserving the draft. Keep the camera exit rail stationary, use a solid shutter, and present photo positioning full-screen with Back/Done. Preserve the main Flutter view during presentation and hide underlying controls from VoiceOver while editing. Validation: Flutter analysis, 3,796 Flutter tests, and 106 native tests passed. Built, installed, and launched the isolated iPhone preview; inspected the native emoji/badge rendering. The local preview entry point is excluded. Draft pending real login/join → profile-save verification and owner verification of the updated device flow. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
5a8102ee5b |
fix(mobile): preserve invite onboarding and offer identity setup (#8617)
## Summary Cold-start iOS invite links could open the welcome screen without continuing sign-in, and accounts without a stored backup had no explicit identity setup choice. Forward scene launch URLs to `app_links`, offer creation for an unbound account with no backups, and offer matching-key import for an existing binding without a backup. Existing key storage, binding, backup, and admission checks remain in use. Discover saved hosted destinations using `known-communities/list` and remember successfully admitted destinations with `known-communities/add`. Saved destinations are not membership proof; normal admission and the existing failed-claim membership reconciliation remain separate. A failed save preserves the intent and local community for idempotent retry. ### Related issue Related to #8599 (QR-first mobile onboarding) and #8601 (invite policy consent; overlapping setup UI). No exact duplicate found in the open onboarding PR / identity-backup issue search. ### Validation before the latest main integration - 48 tests passed across five focused files: `hosted_journeys_test`, `hosted_communities_test`, `hosted_communities_flow_test`, `hosted_environment_flow_test`, and `hosted_multi_backup_recovery_test`. Test defines enable hosted domains and the push gateway. - Four additional pairing handoff tests passed with hosted domains enabled after updating their shared HTTP fixture for community saving. - Targeted Dart analysis: no issues. Required pre-commit and pre-push hooks passed, including the broader mobile analysis/test lane and file-size checks. - Native iOS build passed during iteration. On an isolated iOS simulator, a terminated app opened from an invite automatically entered browser login; human recording confirmed login continued into identity setup and new identity creation. - Human staging testing on October 27 passed invite entry with an existing email/password and new identity, community entry, messaging, cold restart, and recovery on two separate clean iOS simulators. These results apply to build `ff2138fc325790e0af4d9a64b2ed033fae3a8295`; the backend backup-upload blocker described previously is no longer blocking that tested flow. - Fresh-account signup through an invite is deferred. Physical-camera QR scanning, native regression/mutation coverage, independent review, and native retesting after the latest main integration remain outstanding. - Known limitation: failed-claim membership reconciliation still calls `communities/joined`; its endpoint/response contract needs alignment with the supported backend before that fallback is considered ready. The successful primary recovery path does not prove this fallback. - No account reset tooling, environment-specific credentials, generated dependency files, or recording artifacts are included. ### Current review fixes and validation Head `9902a98d42904a4a7d44a79541d81ceb94d8eac3` addresses the two review blockers and both small import races: - Hosted imports explain a linked-account key mismatch; the shared self-hosted/occupied-device message remains unchanged. The linked-account journey enters the wrong key through the actual sheet, checks the visible error and empty storage, then imports the correct key. - Closing the import sheet while its operation is pending now waits for that operation and resumes successful setup. The new-account journey covers creation and import, including dismissal while awaiting the status response, binding, backup receipt, and retry. - An unbound account with newly appeared backups refuses direct import, preserving recovery's receipt-before-adoption rule. The existing multiple-backup invariant covers matching and different imported keys. - The configured route journey uses host labels and its existing fake account now supports destination saving; the real-account journeys separately exercise persistence. 53 tests passed across six focused files with CI's `BUZZ_AUTH_DOMAINS=buzz.xyz,builderlab.xyz,builderlabstaging.xyz` and push-gateway define. In a separate clone with independent Git metadata/build output, restoring the old production import paths failed the three intended regressions; restoring the fixes passed all 43 tests in that control. Required pre-commit and pre-push hooks passed, including mobile analysis/tests and file-size checks. Hosted CI and native retesting of this head are still pending. The earlier independent review was at `10815e5d9`; these fixes still need reviewer confirmation. Optional accessibility and import-navigation refinements are deferred. Fresh signup, authenticated production add/list and the membership fallback remain open as described above. ### Staging testing and production configuration Staging selection is build configuration, not a changed production default. `HostedEnvironment` is unchanged by this PR: `BUZZ_BUILDERLAB_URL` defaults to `https://app.builderlab.xyz`; `BUZZ_AUTH_DOMAINS` is empty by default (hosted account features disabled). Builds that enable production hosted domains receive these onboarding behavior fixes after merge. This PR does not deploy anything or redirect production builds to staging. Check out this PR (`gh pr checkout 8617` in a dedicated `block/buzz` checkout/worktree), install dependencies per `mobile/README.md`, and boot a clean iOS simulator. With the repository-pinned Flutter toolchain: ```sh . ./bin/activate-hermit ./scripts/mobile-worktree-overrides.sh cd mobile flutter run -d <clean-simulator-udid> \ --dart-define=BUZZ_BUILDERLAB_URL=https://app.builderlabstaging.xyz \ --dart-define=BUZZ_AUTH_DOMAINS=communities.public.builderlabstaging.xyz \ --dart-define=BUZZ_DEBUG_PRIVATE_LOGIN=true ``` Private login requests an ephemeral browser session only in debug builds. Hosted relay assertions remain off by default; this test does not validate assertion-enabled admission. Generated local Xcode settings, dependency metadata, credentials, and simulator state are not included in the PR. - Invite test: open a valid public-staging invite in the simulator browser, choose **Accept invite in Buzz**, sign in or sign up, complete identity setup, enter the community, and send a message. - Recovery test: preserve that simulator, start another clean simulator with the same build, choose **or sign in with email**, and sign into the same account. Verify the recovered public key matches, the saved community and earlier message return, and a new message sends. - Fresh signup: repeat invite entry using a new account. This still needs human verification. Keep this PR draft while the listed validation gaps remain. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> |
||
|
|
d4a07063c8 |
feat(mobile): manage Builderlab remote agents (#8626)
Adds Builderlab remote agents to mobile: a member signed in to Builderlab can list, create and delete their own hosted agents from the channels header, and active remote agents show as Online. Everything is gated by `HostedEnvironment.enabled` (the build-time `BUZZ_AUTH_DOMAINS` define). App Store and internal release builds do not set it, so they render and fetch nothing new. ### Behavior - **Agents** button in the channels header (native iOS bar and Material header) for hosted communities only. It opens a sheet listing the community's agents; tapping one pushes its profile inside the same sheet. - **Create**: for the member's own agents (the NIP-OA owner is the member, not the community). Registers the agent with Beekeeper, attests it to the community, saves optional private instructions, subscribes it to the relay, then publishes the kind 30177 registration with a NIP-OA owner proof. A failed step resumes with the same agent identity on retry; instructions stay in secure storage, never in preferences. - **Delete**: removes the relay registration, confirms it is gone, then deletes the Beekeeper agent. A partial failure keeps a retry row in the list. - **Presence**: an ACTIVE agent with a live kind 30177 registration on the active relay shows Online in profiles, channel rows, DMs and mentions, since remote agents wake on messages. Local presence data is unchanged. - Sign-in reuses the shared hosted flow. The account's saved Buzz key must match the server binding; nothing is adopted or rewritten. - `BuilderlabAccountClient` gains an allowlist for the six Beekeeper routes desktop already uses. Not included from the prototype: the separate entrypoint and second sign-in, test-community creation, staging-only hosts, QR-code agent links, and the USB proxy. ### Validation - Added tests: build-time gate, account/key binding before listing, setup resume with private instructions, deletion contract and retry, owner-proof verification, status parsing, presented presence requiring a live registration in the active community and following registration publish and removal (mutation-checked), account-change clearing, load retry, recovery of a pending setup whose instructions were lost, a lost registration response that can only resume with the same idempotency key (from any community), an HTTP-rejected registration that can be discarded, deletion of other agents while a setup is unfinished, rejection of owner proofs whose signed conditions or profile signature fail, registration publish and removal over a loopback WebSocket relay for an `http://`-saved community, and widget tests for in-sheet detail navigation, creation overlay with failure then resume, and delete confirmation. - The new tests also run in the CI lane with `BUZZ_AUTH_DOMAINS` configured, so the enabled path is covered. - Full mobile format, analyze and test suite pass locally. - Independent agent review: no gate leak or key-handling issue found; its robustness findings (discarding a setup that never got a server identity, clearing registry state on account change, retrying a skipped load, guarded notifications) are fixed; discard and account change have added tests. - Simulator: a `BUZZ_AUTH_DOMAINS` debug build launches to Builderlab sign-in. The signed-in flow was not re-run on this branch. - Codex Security Review: an earlier head received a published review, and its two findings were fixed. On |
||
|
|
f167c4c3e1 |
fix(push): accept canonical channel UUIDs across versions (#8609)
Starter channels use UUIDv5 IDs, but mobile push accepted only UUIDv4 channel IDs, so building a subscription that included a starter channel threw locally. After #8608, no non-v4 ID reaches mobile push today: positive `#h` filters cover joined DMs only, and mutes are limited to subscribed channels. This PR is therefore a safety net for future starter-channel subscriptions, and it prepares mobile for a relay that accepts them. Mobile now accepts canonical lowercase, hyphenated UUIDs of any version in subscriptions. It recognizes both the current `uuid-v4-lowercase` relay descriptor and a future `uuid-lowercase` descriptor. Against a `uuid-v4-lowercase` relay, mobile checks positive and mute channel filters before enrollment or publication, keeps the desired filters, and rechecks relay capabilities every five minutes instead of sending a lease the relay would reject. The relay is unchanged. It keeps validating and advertising `uuid-v4-lowercase`. Installed mobile builds reject any other `h_grammar`, and they parse the descriptor before renewal, opt-out and revocation, so changing the advertisement now would leave their leases active until expiry. The relay validation and advertisement change will follow in a separate PR once those builds have drained. `docs/buzz-push.md` records this sequencing. ### Validation - Regression tests reproduced the UUIDv5 rejection in Dart before the fix. Added coverage checks positive and mute filters, persisted subscription round trips, encrypted lease content, malformed IDs, retained bounds, parsing of both descriptor grammars, and pre-publication rejection of non-v4 IDs against a `uuid-v4-lowercase` relay. - Mobile push tests on the current head: 104 passed, 4 skipped. `flutter analyze` on the push code and tests is clean. - Before the split, exercised actual mobile lease construction, NIP44 encryption and NIP42 publication against an isolated local relay. The relay half of that exercise is no longer part of this PR. - Relay source is identical to `main`, so the relay serves the same descriptor that installed builds parse today. The standing mobile test exception applies. Agent review and changed-behavior exercise are complete. buzz-review-completed --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co> |
||
|
|
b77ad2ed10 |
fix(mobile): align Solo agent policy and labels with the owner's inventory (#8616)
## Summary
`main` now discovers Solo agents from the owner's kind 30177
managed-agent inventory. This PR aligns two remaining parts of that path
with desktop's agent library.
- **Unsupported policies fail closed.** Desktop parses each agent's
latest 30177 record as `ManagedAgentEventContent` and drops records it
can't parse. Mobile only checked `name`. It now also requires an integer
`parallelism` in `u32` range, a `respond_to` of `owner-only`,
`allowlist` or `anyone`, an allowlist that is absent or a list of
strings, and string-or-null optional text fields. A newer unsupported
record hides the agent instead of falling back to an older one. Records
written by the apps always carry these fields.
- **The owner's name labels the agent in Solo's selection, header and
creation surfaces.** The directory already used the 30177 `name`.
Message authors and the typing indicator still use the agent's profile
name. The existing-chat header, pending-change label, fallback avatar
initials (header, native header action, new-chat row) and the resumed
native creation's saved agent preferred the agent's self-published
kind:0 name. They now use the owner's name first and fall back to the
profile. Profile artwork is unchanged.
### Validation
- Added tests: a table of 11 unsupported policy shapes that must hide
the agent, accepted policy shapes, and journeys where the owner's name
("Owner Name", "Alpha") differs from the cached profile name
("Confusable …"). The journeys cover the new-chat row's title and
initial and the existing-chat header tooltip, initial and pending label.
They also assert the native new-chat payload and picker row, which
already used the inventory name.
- Mutation checks: removing the policy check fails 10 tests; preferring
the profile for the header name, header initial or new-chat initial
fails the matching journey. The saved-agent name on resumed native
creation has no dedicated test.
- The full mobile suite passes (3,806 tests), and `flutter analyze` is
clean.
- The human-testing requirement does not apply under the mobile client
testing exception.
🤖 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>
|