mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
5bbdd5c63b025ef3d7b23646332709becc18e2bc
3159 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5bbdd5c63b |
fix(desktop): bound and cancel invite-handoff join policy discovery
A receipt-less buzz://join claim first discovers the relay's join policy through the webview fetch path, which had no deadline, no body cap, and no way to cancel. A relay whose /api/join-policy blackholed, trickled, or streamed an oversized body left onboarding on "Accepting your invite…" forever, and cancelling did not stop the download. The webview branch of getJoinPolicy now matches the native fetch_join_policy bounds: one 15 s deadline covers the headers and the whole body, and the body is capped at 4 MiB while it streams (a larger declared Content-Length fails up front). A timeout surfaces as "The community's join policy took too long to load.", which the claiming stage shows with Retry. getJoinPolicy takes an optional AbortSignal, and useClaimInvite aborts the pending lookup when its transaction is cancelled, replaced, or unmounted. The existing transaction fence still drops any late settlement. The useClaimInvite jsdom suite gains a journey with a suspended /api/join-policy: no error at 14.999 s, a retryable timeout at 15 s with zero claims, then a retry that claims once against a policy-free relay. The single cancel test becomes a cancel/reopen table. Each row asserts the lookup's signal is aborted and that a late policy response neither claims nor moves the cleared or replacement transaction. A getJoinPolicy test rejects both a streamed and a declared body over 4 MiB. Removing the abort, the fence, the timer, or the cap each fails its test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
33a779f9bf |
fix(mobile): allow relay-sized join policy responses
Mobile read GET /api/join-policy through the generic 64 KiB hosted response cap, but the relay allows each policy markdown document up to 256 KiB and Desktop accepts a 4 MiB policy response. A self-hosted relay with a valid policy over 64 KiB was joinable on Desktop but failed on Mobile with an invalid-response error before consent could render, and every retry hit the same cap. boundedHttpResponse and readJoinResponse now take a maxBytes that defaults to the existing 64 KiB hostedResponseMaxBytes. Only loadJoinPolicy opts into the 4 MiB cap that matches Desktop, so the accept-policy, claim, and other hosted replies keep their limit. The reader accumulates into a BytesBuilder so a multi-MiB body is not held as a list of ints. The invite consent journey now serves a policy over 64 KiB and asserts that Mobile loads its full terms and claims only after native acceptance. The hosted-admission "oversized policy" row used bare whitespace, which failed as invalid JSON whatever the cap was. It now sends valid JSON padded past 4 MiB. A focused test serves a relay-sized policy alongside an accept-policy reply over 64 KiB: the policy reaches consent, the reply is rejected, and no claim is sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
099a0bfa7e |
fix(mobile): show invite sheet while join policy loads
The deep-link dispatcher awaits InviteJoinNotifier.prepare before it presents the invite sheet, and prepare awaited GET /api/join-policy. On a slow or blackholed relay, opening an invite did nothing visible until discovery finished or hit the 30 s request deadline. prepare now returns once the pending invite and host are established and loads the policy in the background. A new isLoadingJoinPolicy state renders a "Loading community policy…" row (the spinner owns the announcement) and keeps Join disabled until the policy is shown or confirmed absent. A per-load token fences the result: a re-prepare, reset, or claim supersedes the load, so a stale success or failure cannot overwrite a claim that confirmJoin started, e.g. from the iOS native sheet. confirmJoin still re-reads the policy before it claims. The dispatcher gains a table with a suspended /api/join-policy response. Both rows assert that the host, an enabled Cancel, the loading row, and a disabled Join are visible before the GET resolves, with no claim. The policy row then claims once with the app-minted receipt after age confirmation and acceptance. The absent row claims once without a receipt after Join. The provider claim journey now claims while discovery is still suspended, then fails that discovery and asserts the join stays successful. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
93140fa3a3 |
fix(desktop): ignore deep-link policy receipts for native invite consent
Anyone holding an invite code can mint a join-policy receipt from the public accept-policy endpoint, so a receipt carried by a buzz://join link does not prove this app showed consent. Desktop persisted that receipt into the onboarding transaction (and kept a retained same-relay receipt on resume), which made useClaimInvite skip policy discovery and claim without the native consent stage. startCommunityOnboarding now drops receipts for deep-link-join transactions, both when creating one and when resuming a same-relay transaction, so the invite-consent stage is the only source of the receipt a link join claims with. The deep-link drain no longer forwards the link's receipt. The useClaimInvite jsdom table gains rows for a link-supplied receipt and a retained same-relay receipt. Each row asserts the flow reaches invite-consent with zero claims, then claims once with the app-minted receipt after native acceptance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
7a35f29d49 |
fix(invites): collect native consent for app invite handoff
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
f6f6d9a673 |
chore(mobile): delete dead code with no remaining UI path (#8880)
## Summary Deletes mobile code that no longer has a user-reachable path and has no counterpart in buzz-app, so it is unlikely to come back for parity. Net: 104 files, about 5,500 lines removed. No intended behavior change. - **iOS native message menu and action surface:** `NativeMessageMenu.swift` and `NativeMessageActionSurface.swift`, the `message`/`supportsMessage` coordinator branches, and the Dart selection hook. Dart stopped creating this view in [#8280](https://github.com/block/buzz/pull/8280). The native reactions sheet in the same coordinator is kept. - **Old home quick-actions launcher and `ComposeMorphIcon`:** only instantiated in tests since the nav-bar creation actions replaced them. - **Retired local theme stack:** `ThemeNotifier`, `AccentNotifier`, `SchemeNotifier`, `adaptive_theme.dart`, `AccentPickerPage`, the appearance-mode sheet and their helpers. These predate per-community theme sync. The legacy prefs migration in `community_theme_preference.dart` is kept. - **Unused widgets:** `ProfileAvatar` and `MaskedAvatarBadge`. - **Push permission-status plumbing:** the iOS `notificationAuthorizationStatus`/`openNotificationSettings` handlers, plus the uncalled strict age-gate snapshot registration and its Swift `syncAgeGatePushSnapshot` path. - **Orphaned files** with no importer: `profile_text_edit_sheet`, `editor_option_button`, `retained_page_transition`, `tappable_flapping_bee`, `agent_model_info`, `community_bot_profiles`, `sheet_divider`, `success_haptic`. - **Uncalled helpers:** the `pickAndUpload*`/camera helpers in `media_upload.dart`, unused `NostrFilters` builders, `mediaGetHeadersFor*`, and about 20 other uncalled functions, providers and styles. - **Removed-UI leftovers:** the never-wired invite directory providers, `AuthNotifier.signOut` and `removeActiveCommunityForSignOut` (the Sign out row is gone), `renameCommunity`, `HomePage.profilePageBuilder`/`SettingsPage.embedded`, and the create-forum branches of the create-channel sheet. Forum viewing is unchanged. Tests that only covered deleted code are removed. Tests of surviving code that used deleted helpers as drivers were repointed to the surviving API. An audit of `mobile/android` found no dead Kotlin, resources, manifest entries or Gradle dependencies, so Android code is unchanged. ## Validation - `flutter analyze`: no issues. - After rebasing onto main ([ |
||
|
|
2e0abbcc0c |
fix(mobile): let slow image downloads finish (#8881)
On a slow link, a large image could never appear. Every image fetch was aborted 30s after it started, even while bytes were still arriving, and a failed fetch then sat on a 30s cooldown before it could be retried. At 240 kbps, anything over about 900 KB could not finish. On a shaped iPhone SE, a 1.45 MB avatar was fetched, aborted at 30s and never shown. - **Abort stalls, not slow downloads.** A fetch is now aborted when no headers or body bytes arrive for 20s, so a dead connection still frees up quickly. A download that keeps making progress can run for up to 5 minutes, so a server that trickles bytes cannot hold a connection forever. The 20 MiB response cap still bounds its size. - **Cancel previews scrolled past in the iOS carousel.** The carousel loads at most four previews at a time, and a preview scrolled past kept its slot until its download ended. With the longer limit, four slow previews could block the visible one for minutes. Now the carousel drops a cancelled preview's pending image-cache entry, and with no one left listening, the image loader aborts its HTTP request. Scrolling back starts a fresh download with no failure cooldown. A download that another consumer, such as the full-screen viewer or a gallery prefetch, is still listening to keeps going, and its finished image is still cached. Closing the carousel aborts its pending previews the same way. - **Cancel gallery prefetches that leave the window.** The message gallery prefetches a few pages around the current one with two workers. Each prefetch used to hold its download until it ended, which also kept the carousel's cancel from aborting it. Now a prefetch that leaves the window, or whose gallery goes away, lets go and aborts the same way. ### Validation Added tests (each fails without its fix): - A body that trickles in for longer than the stall timeout still loads, and one that never ends is aborted at the overall cap. - In the iOS carousel, previews scrolled past abort their stalled downloads (before headers and mid-body), the next preview loads, and scrolling back starts a new download. The test fails if the carousel doesn't evict, if the loader doesn't abort, or if the abort leaves a cooldown. - Closing the carousel aborts its stalled previews. - In a message gallery, jumping to the end and back aborts both in-flight prefetches and moves the workers to the new window. - Abandoning a load another consumer still waits for keeps the finished image cached with no second fetch; abandoning one nobody else waits for aborts it. - Rapid scrolling releases each cancelled carousel load at once, and cancelled loads never reach UIKit. The full mobile suite passes. 🤖 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> |
||
|
|
76fd0c6151 |
fix(mobile): show Bestie as a continuous conversation (#8890)
Opening Bestie from the mobile shortcut currently presents the agent DM as a threaded channel. Present that same DM as a continuous conversation, matching the top-level interaction in desktop Bestie. Read existing roots and replies in chronological order using the flat history provider, show typing inline, and keep new sends at the DM top level. Disable thread-opening taps, swipe replies, and the Reply menu entry in this view. Preserve the DM identity, history, membership, and recipient rules; Solo ownership routing remains separate. Regression coverage exercises the Bestie launcher, existing threaded answers, reply-target navigation, taps/swipes, and top-level DM sends. Validation: `just mobile-check` passed; `just mobile-test` passed (4,051 Flutter tests, 34 skipped; 4 unconfigured-push tests; 27 crypto-package tests). The mobile file-size check and `git diff --check` also passed. Manual app check before readiness: 1. Open the bottom-corner Bestie shortcut for an agent whose DM already contains threaded replies. 2. Confirm those answers appear inline with the original messages, without a separate thread screen or reply-count links. 3. Tap and swipe a message; confirm neither opens a thread. Long-press should retain reactions and other actions without Reply. 4. Send another message, then close and reopen Bestie. Confirm it remains in the same DM and conversation history. 5. Open an ordinary channel and confirm its existing thread behavior still works. Native app walkthrough and human confirmation are pending, so this PR remains draft under AGENTS.md's readiness requirements. Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
53ca047cd3 |
fix(mobile): keep mention rows still and cache directory searches (#8875)
Typing `@s` in a stream showed the matching channel members, then about half a second later added people from outside the channel. The chooser sits on the composer and grows upward, so each late row pushed every shown row up and a tap could land on the wrong person. It happened every time, even for a query typed moments earlier, because each new `@` started its directory search from nothing. ### Changes - **Search cache.** Finished directory searches are kept per community and signer, in a small LRU keyed by query (`MentionSearchCache`). A query searched before shows its people at once while a fresh search runs in the background. A query not searched yet starts from the longest searched query it extends (`@si` starts from `@s`), since a name-prefix search for a longer query finds no one the shorter one missed (up to the search's result limit). This replaces the per-opening "last search" that reset on every new `@`. - **Rows hold still.** While a query's directory search can still add people or a retry row (`mentionSearchPendingProvider`), the chooser reserves its full height with rows at the top, so late rows fill empty space instead of moving shown rows. This includes the background refresh of a cached query. The reservation lasts until the query or the opening changes, so the panel never shrinks under the finger either. - **Three rows, with progress.** The chooser has a fixed three-row height (fewer rows at larger text sizes) before it scrolls, and never taller than the space above the composer (so a short view, such as landscape with the keyboard up, keeps its rows on screen). While the search runs, a thin bar overlays its top edge without changing its height. The trade-off: while a query's search runs, the panel is full height even when it ends with one or two rows. ### Demo iPhone 18 Pro simulator, local relay. Sam and Sasha are channel members; Saba, Sierra and Skyler come from the directory search. https://github.com/user-attachments/assets/efae33ce-ee65-447b-9402-b0741c8dc2da ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation - New tests: a cached query reopens with its people at once, stays pending while it searches again, and clears when that search finishes; a longer query starts from its prefix's people; an uncached query is pending until its search finishes; and a compose bar journey step checks that a late directory person leaves the shown row's position and the panel height unchanged, both on a new query and on a cached query's refresh. Other new tests cover the three-row cap, the progress bar, and an 844×390 landscape view with the keyboard up, where the reserved chooser previously pushed the only row above the screen. Disabling the cache lookup fails the two cache tests, and disabling the reservation fails the layout tests (the cached-refresh case moved a row 59pt). - `just mobile-check` and `just mobile-test` pass. - Exercised on the simulator against a local relay as shown above. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: mbp <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
93b8b8febc |
Standardize mobile navigation edges (#8895)
Standardizes mobile navigation headers: Android uses Home’s shared Flutter blur; iOS uses native soft edges, with graduated material retained during large-title collapse and the iOS 26 fallback preserved. Preserves Home’s upward title motion with a soft boundary and keeps the avatar crop title visible. Validation: 4,112 Flutter tests and 111 native tests passed after merging main. Rendered partial-scroll regression verified on the iOS 27 simulator; removing either review fix fails its regression assertion. The earlier preview was installed on a physical iPhone with the existing profile. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
634f48a8bd |
Mobile: open Solo chats with Bestie and workspace controls (#8884)
Mobile New chat now opens directly with the synced Bestie selected. The first send creates and names the chat; leaving an empty draft creates nothing. New and existing Solo chats can select or clear a workspace from the header popover, with a checkmark for the active choice and a No Workspace option. Workspace and agent controls have separate iOS glass surfaces, Bestie stays at the far right, and Back uses a chevron. The agent sheet fits its list, with scrolling for long lists. Workspace creation stays in the Home New menu. The New menu groups Solo above Team. Android expands across the full control row; iOS keeps its native menu while Search and Bestie merge into New through Liquid Glass, with synchronized icon motion and the residual glass fading away. Empty workspaces use a compact, muted message. Validation: 4,055 mobile tests passed (34 skipped), static analysis passed, and native menu checks passed. Earlier physical-device previews and the iPhone New-menu animation were visually approved before this consolidation. This PR consolidates the controls previously developed in #8891 into the existing draft/first-send implementation. The New menu animations, attachment handling, and durable retries remain in this PR. The combined controls have widget interaction coverage; the combined build has not yet had a new physical-device walkthrough. Current combined controls below are Android Flutter widget renders with fixture data, not device screenshots. Images are GitHub attachments only. | New conversation | Workspace popover | Compact agent sheet | | --- | --- | --- | |  |  |  | Empty workspace widget preview (unchanged):  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
cde6c1bf5b |
feat(relay): auto-join new community members to default channels (#8594)
## Summary Adds **default channels**: relay owners and admins can flag channels that every new community member is automatically joined into. **Setting (contract)** - New `channels.auto_join` column (migration `0080`, constant default `false`, so no table rewrite). - kind:9002 accepts `["auto_join", "true"|"false"]`. Authorization comes from the actor's **relay** roster role (owner/admin), not a channel role, so the actor doesn't need to be a channel member. API tokens need `AdminChannels`. - Only open, unarchived `stream` channels qualify. This is checked under the channel row lock, so one event can open and flag a channel in one step, and a concurrent archive can't slip past the check. - kind:39000 carries a bare `auto_join` tag only while the setting is effective (on, open and unarchived). Every change posts one `auto_join_changed` kind:40099 notice. - SDK `build_update_channel(.., auto_join)`, `buzz channels update --auto-join <true|false>`, a NOSTR.md "Default channels" section, and a NIP-CL tag-table row. **Behaviour** - kind:9021's body is pulled out into `join_channel_as_self`, so auto-join uses the same path as a self-join: the channel's `join_role` applies, and it emits `member_joined` 40099, 39000/39002 and 44100. - `auto_join_default_channels` runs inline right after a member is added at three sites: the v2 invite claim, the v1 invite claim, and kind:9030 add. It does not run on login or reconnect, so a later kind:9022 leave stands. - Best effort: each failure is logged and counted (`buzz_auto_join_failures_total`) but never retried, because the roster membership has already committed. This deliberately departs from Review-Proven Rule 1 and is documented in NOSTR.md. `buzz-admin add-member` writes the roster row only. **Communities without a membership roster** (`BUZZ_REQUIRE_RELAY_MEMBERSHIP=false`) - These have no invite claim or kind:9030 to hook, so the three sites above join nobody there. Instead, after the AUTH `OK`, the relay joins a person to every effective default channel they have **never had a membership row in** (`auto_join_never_joined_channels`, spawned so admission never waits on it). - The channel's own membership history is the once-only record, so this adds no new table, column or migration. A kind:9022 leave or a kind:9001 removal keeps the row with `removed_at`, so the person stays out. A new device or another relay pod finds the row and does nothing. `put_first_member` checks the row again under the `put_member` locks, so two admissions racing each other join once. - A channel flagged later is joined on each person's next admission. A failed membership write leaves no row, so the next admission retries it. - Each channel's join holds the session's NIP-FI effect permit, so once the socket closes or its admission expires, no further channels are joined. - Agents are skipped: any socket with an admitted owner (a NIP-OA tag, a NIP-FI agent token, or a stored `agent_owner_pubkey`). - The kind:9021 side effects moved into `announce_self_join`, so both join paths emit the same `member_joined`, 39000/39002 and 44100. **Review follow-ups** - `get_accessible_channels` now selects `auto_join`. Without it, every call failed with `ColumnNotFound`. - The final authorization branch of the 9002 validator now refuses by default. `list_auto_join_channels` filters to `stream` channels itself. ## Testing - Postgres-lane journeys: - `edit_metadata_postgres_tests`: who may set `auto_join`, the 40099 notice, the effective-state tag, and the refusals. - `claim_auto_joins_default_channels_once`: v2 and v1 claims, and leave then re-claim. - `adding_a_member_auto_joins_default_channels`: kind:9030 add. - Removing the auto-join call at any one of the three sites fails the matching test (checked by mutation). - `root_auth_joins_people_to_default_channels_without_a_roster`: through `handle_auth` on a roster-less community. A person is joined, a NIP-OA agent is not, and a leave holds when the person is admitted again. Mutations checked: removing the spawn, and dropping the agent check. Each check waits for that admission's own auto-join outcome (a skip reports through the same test hook as a finished task) and fails if none arrives. A third mutation drops the agent check and delays the task 3s; it still fails. - `admission_auto_join_stops_when_the_session_ends`: closing the socket before the first or second channel stops the remaining joins; a later admission joins what was skipped. Mutation checked: dropping the effect permit. - `first_member_put_joins_once_and_never_undoes_a_leave` (buzz-db): the person is joined once with the channel's `join_role` (`guest` on an announce channel), a second put does nothing, after a leave the channel is neither listed nor joined, and two racing puts write exactly once. Mutations checked: removing the row check, removing the channel lock, and removing the `NOT EXISTS`. - e2e `test_invite_claim_auto_joins_default_channel` against a local relay. - Unit coverage in the SDK and CLI builders and `group_metadata_tags`. ## What got simpler The roster-less path adds no schema and no new concept: it reuses the channel's membership history as its record. It also shares the kind:9021 join code. It replaces the per-device client workarounds, [buzz-app#2029](https://github.com/block/buzz-app/pull/2029) and [buzz#8795](https://github.com/block/buzz/pull/8795), which are now closed. ## Related - Client half (buzz-app): https://github.com/block/buzz-app/pull/1624 ## Demo Recorded against a local relay built from this branch with throwaway identities (Alice = owner, Bob = new member); no real accounts or hosted relays involved. Relay at 141aa01d4, client at buzz-app bb0d97d86 (browser dev build). **Owner turns on "Starter channel for new members" on two channels.** Alice creates `#general` and `#welcome`, then flips the switch on each (~0:25 and ~0:41). The switch greys out while the kind 9002 is in flight and only turns on once the republished 39000 carries the `auto_join` tag. https://github.com/user-attachments/assets/e590d343-11d3-4385-af43-2cfaf19f8468 **A brand-new member joins and lands in a starter channel.** Bob claims Alice's invite with empty storage; from ~0:18 he is reading `#welcome` with both `#general` and `#welcome` in the sidebar, and at 0:27 opens `#general` as a member with no Join prompt. The relay logged one `auto-joined default channel` line per flagged channel. https://github.com/user-attachments/assets/037a0aaa-b25d-474d-a316-8d25e5e54f89 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Signed-off-by: Luke Tornquist <tornquist@squareup.com> Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>mobile-v0.27.0-rc.13 |
||
|
|
21a9839826 | Fix mobile unread state retaining deleted messages (#8883) | ||
|
|
e18d422d72 |
fix(mobile): fill uploaded avatars on the Agents page (#8882)
Uploaded images on the mobile Agents page now crop to fill the avatar squircle, matching desktop. Animated avatars and bundled character posters retain their existing sizing. Validation: `just mobile-check mobile-test` passed (4,050 Flutter tests; 34 skipped), plus before/after rendering of the production card. Regression coverage checks uploaded, preset and animated images. Snapshots use a sample rectangular PNG in a local Flutter widget preview, not an installed build. | Before | After | | --- | --- | |  |  | Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
90772a43e9 |
fix(mobile): keep avatars and images loading on slow links (#8879)
On a slow link, avatars in a channel could fail to appear entirely. Two causes: - **Duplicate downloads.** Each decode size is its own image-cache entry, so an avatar shown at two sizes (a message row and its reply summary) started two downloads of the same bytes, which then competed for bandwidth. The in-flight download is now shared per URL and auth service. - **Full-size avatars.** Avatars a few dozen points wide downloaded the original upload, which can be megabytes. The relay generates a JPEG thumbnail of at most 320px for every uploaded image, and desktop already loads it for small avatars. Mobile now does the same for any image format when an avatar is at most 240 physical pixels on each side. About 88% of relay-hosted avatars on the internal relay are PNG, with a median size of 337 KB. As on desktop, the thumbnail is a still JPEG: transparent areas render black and animation is lost. Rendered side by side, mobile and desktop look the same. If the relay returns 404 for a thumbnail (older or externally populated media), the avatar loads the original instead, as the notification extension's avatar loader already does. ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation On an iPhone SE (iOS 27.0.1) running a profile build, relay traffic went through a Mac-side proxy shaped to 200ms one-way latency, 240 kbps down and 200 kbps up. I opened a busy channel and logged each relay image fetch. Every fetch is aborted 30s after it starts, which [#8881](https://github.com/block/buzz/pull/8881) changes separately; with thumbnails the avatars below no longer come close to that limit. | Avatar | Before | After | | --- | --- | --- | | A | 1.45 MB, fetched twice; both aborted at 30s, never shown | 14.5 KB thumbnail, 3.6s | | B | aborted at 30s, never shown | 14.5 KB thumbnail, 4.1s | | C | 125 KB, fetched twice, 26s each | 21.5 KB thumbnail, 4.3s | | D | 46 KB, 11.1s | 24.7 KB thumbnail, 4.6s | Added tests (each fails without its fix): - One URL resolved at two decode sizes downloads once. - Small relay avatars request the thumbnail and large ones the original. - A thumbnail 404 falls back to the original, and other thumbnail failures show the avatar fallback. - The thumbnail URL is built only for relay-hosted blobs (`/media/<sha256>` with an optional extension), the same pattern desktop's `mediaUrl` uses. The full mobile suite passes. 🤖 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> |
||
|
|
543ecf2b1f |
fix(mobile): list the same owned agents as desktop (#8857)
## Summary
Mobile's Agents modal listed agents that desktop does not show. Most
were agents created in the earlier Buzz app (this repository's
`desktop/`), which the owner had not imported into Buzz 1.0
([block/buzz-app](https://github.com/block/buzz-app)). @ completion
offered them as well.
Desktop lists an owned agent only when the owner's newest kind:30177
record for that agent has a `pluginId/typeId` `runtime` tag and a name.
Records written by the earlier app have no `runtime` tag. Importing an
agent on desktop republishes its record with one
([`listingsOf`](
|
||
|
|
b41caeb510 |
fix(mobile): time out relay connects that never finish the handshake (#8878)
When a phone's network silently stops working (crowded event Wi-Fi, a weak cell signal), Buzz Mobile can stay offline for minutes. In a device test, the app took 30s to notice the dead connection, then hid the channel's messages behind a loading skeleton. It tried to reconnect once, and that attempt hung for 5 minutes (303s). It never retried, because it only retries after an attempt fails. This PR gives every reconnect attempt a 10s limit. When an attempt runs out of time, the app closes it and tries again with its normal backoff. ### What was wrong Opening the relay connection has two code paths. The one used with hosted relay auth already had a 10s limit. The other path had none, so a stuck attempt waited for the operating system to give up, which takes minutes. Both paths now use the same limit. Each attempt also gets its own HTTP client, so a timed-out attempt is actually closed rather than left running in the background. ### Validation I tested on an iPhone SE (iOS 27.0.1) running a profile build, with its relay traffic routed through a Mac proxy that could silently drop everything. - **Before:** during a 5.6 minute outage, the app made one reconnect attempt, which hung for 303s. - **After:** during a 2.5 minute outage, each attempt gave up after 10s and the app kept retrying. It reconnected 36s after the network came back, a delay mostly set by the 30s cap on time between retries. Added tests: `relay_socket_liveness_test` now covers an attempt that never completes, with and without hosted relay auth headers. It checks that the attempt fails and that the abandoned connection is closed. The test fails without the fix. Two existing tests were opening real relay connections by accident or design: - `deep_link_dispatcher_test` now uses a fake channel list, like its neighboring tests. - `remote_agents_test` talks to a real local relay on purpose, so it now uses a real HTTP client instead of the test framework's mock. The full mobile test suite passes. 🤖 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> |
||
|
|
5f98a1fa04 |
mobile: quiet agent messages and caught-up channel highlights (#8876)
Mobile bolded a channel whenever any reply in one of your threads, or any mention, was unread. Catching up on the channel itself never cleared those, so a channel could stay bold with nothing new in its timeline. Agent progress messages (sent with the `agents` audience) also counted, so busy agent threads kept channels lit. This brings the mobile channel list in line with the desktop sidebar rules in buzz-app's `docs/unread.md`: - **Quiet agent messages** (`["audience","agents"]`) never count as unread or earn attention, even when they mention you. They're dropped where mobile records observed unread events, so the channel list, Solo chats and app badges all agree, and Activity drops them too (paging past a mention window they filled). If the bounded catch-up window comes back full of quiet messages, its oldest one still counts as plain backlog, so an older unread message hidden behind them keeps the channel unread. - **Bold means ordinary backlog.** A channel's label bolds only for unread top-level messages (what channel catch-up reads) or a manual unread mark. A manual mark doesn't add a dot, but it keeps the dot of any real attention. - **A dot means attention.** Unread replies in your threads, mentions and broadcasts show a small accent dot (the Activity row's unread dot) without bolding. Opening the thread clears it, as does long-press → Mark read, which the menu now offers for dot-only channels. - Unread DMs bold and show the dot, as on desktop. Solo chats keep their current rule. Not included: desktop's popover listing the unread threads behind the dot. ### Before merge - [x] Kenny Lopez reviews the visible changes ([reviewed in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d); his overlap question about #8883 is answered: the two are complementary). ### Validation - Added tests: - A channel-list unit test covering backlog, reply, mention, DM, manual unread (with and without attention) and Solo. - Widget cases: a reply-only channel shows a dot, not bold, and clears on the thread mark or on long-press → Mark Read. A mention after catch-up is a dot. Backlog plus a mention gives bold plus a dot. - Provider tests: quiet agent messages, both from catch-up and live, are never recorded (fails without the fix). A full catch-up window of quiet messages still counts (fails without the fix). Quiet mentions never become Activity or Inbox items, even when 50 of them fill the mention window in front of a real one (fails without the fix). - Full mobile suite (`just mobile-test`) and `flutter analyze` pass locally. - Simulator recording against a local relay (iPhone 18 Pro, iOS 27). Sam replies in the sim user's thread, which shows a dot without bold. Sam then posts top-level, which bolds the label. Opening the channel reads the backlog, so it returns to regular with the dot. Long-press then offers Mark Read, which cleared the dot. That last step finished just after the clip ends. - Not exercised live: the agents-audience exemption, because the CLI can't set that tag. The provider test covers it. https://github.com/user-attachments/assets/55f74377-d7f3-4b31-a0c8-b456f4e05770 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
83ec9c0e2b |
fix(mobile): show hosted community names consistently (#8872)
When a hosted relay has no metadata name, the hosted add paths save its host (`remote-agent-pilot.communities.buzz.xyz`) as the community name. Onboarding shortens that fallback to `remote-agent-pilot`. After joining, though, the community switcher, the remove dialog, the theme preview and the identity export showed the full host, so the same community appeared under two names. This change shortens the name in the model instead of at each call site. `Community` and `AddCommunityDeepLink` keep the saved value private, and their public `name` getter returns the display form, using the existing `HostedEnvironment.communityDisplayName` rule. Existing and future readers get the short name without opting in. Serialization still writes the saved value, so stored data is unchanged and no migration is needed. Custom names and non-hosted hosts pass through unchanged, as before. Community identity is unaffected because it uses `id` and `relayUrl`, never the name. Beyond the captures below, the same getter shortens the name in the iOS push notification subtitle, onboarding's "Adding …" progress text, the "You joined …" invite sheet, and the VoiceOver labels for removing and loading a community. ### Before and after | | Before (`main`) | After | |---|---|---| | Community switcher |  |  | | Remove dialog |  |  | | Theme preview |  |  | | Identity export |  |  | ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation - Captures come from iPhone 17 simulators (iOS 27) running `main` and this branch. Both builds used the same scratch-only seed with two communities whose saved names are their hosted hosts. The seed is not part of this PR. The relays were unreachable from the simulator, so the screens show loading placeholders. - Changed tests: the community switcher journey now uses a hosted host-name fallback for one community. It asserts the grid label, the remove tooltip and the dialog text show the short name, and it fails when the getter returns the saved value. The onboarding label table now asserts the model's `name` itself is shortened. The hosted-flow fixture records relay hosts instead of names, and the auth-domain tests look up communities by relay URL. - The full mobile test suite and `flutter analyze` pass. The CI step that runs with configured authentication domains also passes locally (113 tests). - Not captured: the push notification subtitle, onboarding's "Adding …" text and the invite sheet. Reaching them needs hosted sign-in or push, which the simulator doesn't support. 🤖 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> |
||
|
|
e71fbe23df |
fix(mobile): keep loaded timeline visible while reconnecting (#8864)
Returning to an already-open channel after a few minutes in the background could replace its messages with the loading skeleton. The relay session drops its socket five seconds into the background, so nearly every return to the app reconnects. When that reconnect took more than two seconds, the channel page covered the loaded timeline with the connection skeleton until the relay came back. The messages provider already keeps the cached timeline through a reconnect, so the overlay was the only thing hiding it. Now a loaded timeline stays visible while the relay reconnects. The skeleton still shows when nothing has loaded yet, and forums keep their existing behavior (skeleton only before any posts load). To keep a reconnect indicator, the iOS title capsule now shows a thin bar in the chat color (the Appearance accent) along its bottom edge while a page with loaded content is not current. For a channel timeline that covers both the reconnect and the catch-up after it (resubscribing and fetching the newest window). The wait has no known length, so the bar sweeps in, pulses until the channel is current, then fades. Reduce Motion shows a static bar. Forums show it only while reconnecting, because their posts provider doesn't refetch after a reconnect. The native navigation view owns the bar, so it survives title rebuilds. Android has no title capsule and is unchanged. ### Demo iPhone 18 Pro simulator on a local relay: background the app for more than 5s with the relay down, return, then let the relay come back. About 8s of relay startup is cut from the middle. https://github.com/user-attachments/assets/e88d0191-766b-4054-bc00-99ed306cd0cc ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation - Updated tests: the reconnect cases in `channel_detail_page_test.dart` and `loading_review_tests.dart` now assert that loaded messages and cached media stay visible and in place during a reconnect. - Added test: the iOS navigation payload sets `titleRefreshing` while a loaded timeline reconnects and clears it on connect. - Added test: `isCatchingUp` holds from reconnect until the newest window merges, and is already false when that state publishes. - Added test: a failed catch-up after reconnect still notifies listeners once, with `isCatchingUp` false, so the bar clears. - `flutter analyze` clean, iOS simulator build succeeds, and the full mobile suite passes on the final head. - Checked visually on an iOS 27 simulator (demo above): loaded messages stay visible on resume, and the bar pulses from foreground until the reconnect's catch-up finishes, then fades. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2e4aacc9e0 |
fix(mobile): find open channels in search and stop the stuck spinner (#8764)
## Summary Mobile search could not find an open channel you had not joined. In the Channels tab it could also show an empty list with a spinner that never stopped. This change fixes both and makes mobile rank channel results the way desktop (buzz-app) does. **Open channels you have not joined.** Mobile ranked channels from the loaded channel list, which holds open channels only after Browse channels has loaded the directory. Desktop search also looks up public channels (`usePublicChannelSearch`). Mobile now loads the open-channel directory when a search starts (`ChannelsNotifier.ensureDirectoryLoaded(maxAge:)`). It reads the directory again when it is older than 30 s, as buzz-app's `PUBLIC_CHANNEL_PAGE_TTL` does. Results are ranked again whenever the channel list or the directory status changes. Unjoined channels already render with the "Open" badge. **Directory failure.** If the directory load fails, search shows "Open channel search couldn't finish." with a Retry channels button, as buzz-app does, instead of "No results". Screen readers announce the notice (live region). The next query also retries. **Stuck spinner.** The message, people and channel lookups shared one `isLoading` flag. When no messages and no channels matched and the people lookup finished last, it set `isLoading = messages.isEmpty && channels.isEmpty`. That turned the spinner back on, and nothing turned it off again. Each relay lookup now has its own pending flag. The spinner shows while any lookup is still running, or while a directory load is running: one this search started, until its future completes, or one another caller started in this community. The shared directory status alone is not enough, because a real refresh marks it loading only after memberships arrive, and loaded before the new list is installed. Lookups are fenced by a query generation, so a late reply to an earlier query, even one with the same text, cannot settle the current search. **Displaced directory loads.** A membership refresh that starts while the directory loads retires that load. It used to mark the directory as failed, so search showed "Open channel search couldn't finish." when nothing had failed. It now marks the load idle. A refresh that starts earlier, during the membership round trips, retires the load before it is marked loading and leaves no status at all. In both cases search loads the directory again once. If that load is retired too, search offers Retry instead of looping. A real relay error still shows Retry at once and is not retried automatically. When a directory refresh starts, it clears an earlier failure for the same community to idle, so a failure left from before (for example before a reconnect) cannot be read as the new attempt's result and skip its retry. The notice follows the live directory status, so it clears when another caller loads the directory. **Directory races.** A community switch drops the cached directory, but the directory status still said the earlier community was loaded, so switching away and back within 30 s skipped the reload. `ensureDirectoryLoaded` now also requires the cached directory to come from the current community. A search during the first channel load used to start a directory refresh that retired the pending load; if that load's reply came last, its stale failure replaced the list. `retryDirectory` now waits for a pending first load, as `fetchFresh` does. Both came from the Codex security review of this branch. **Only the current community's list.** While a community switch reloads the channel list, Riverpod keeps the previous community's list as the loading value. Search now ranks only installed data (`asData`), so the old community's channels cannot match. (The signing identity derives from the community config, so the existing config watch already resets search on an identity change.) The idle marking and the `asData` read come from Wes's https://github.com/block/buzz/pull/8869, which fixed the same bug in parallel. **Same ranking as desktop.** `name_match.dart` ports buzz-app's `matchName` (`src/features/search/match.ts`). Its ranks, best first: - exact - prefix - word start - substring - word runs (`tlp` finds `team-launch-planning`; `buzz community` finds `buzz-community`) - letters in order `SearchNotifier` orders results like buzz-app's `SearchResults.tsx`: - Joined and open channels share one ranking, and joined channels win ties. Open channels with the same rank are ordered by name, then id, like buzz-app's `searchPublic`. - An archived channel ranks half a step behind a live one and stays findable only by its members. - A bare `#` lists joined channels only. Like desktop, it does not load the open-channel directory, so it shows no directory spinner and no failure notice. - The list shows the top 8. A filter tab with none of its own results now shows "No results" instead of a blank list. Plain substring matching on descriptions is removed; buzz-app does not match descriptions. **Differences from desktop:** - Desktop also orders by the viewer's past search choices. Mobile has no such history yet. - Desktop shows Solo chats and DMs in their own groups. Mobile keeps Solo channels in its Channels list. - `from:`, `in:`, `since:` and `until:` operators and desktop's people ranking are not ported. Mobile still sends the raw query to the relay. These can be a separate PR. - Lowercasing differs for a few letters: JS lowercases `İ` to `i` plus U+0307 and Dart does not, so `İstanbul` / `istanbul` ranks exact on mobile and word runs on desktop. - Desktop ranks channels at each keystroke. Mobile still waits for the 300 ms debounce, so channel rows clear while typing. - Open channels that tie in rank are ordered with Dart's case-sensitive `compareTo`; buzz-app uses `localeCompare`. Only names with capital letters can order differently. - A directory refresh runs the full channel-list reload (`retryDirectory`), not only the directory read. With the 30 s TTL this happens at most once per 30 s of searching. ### Related issue Reported in Buzz #buzz-bugs: a search for `4things` on mobile found nothing. No GitHub issue. No duplicate open PRs found. ### Testing - `search_channels_journey_test.dart` drives the real `SearchNotifier` through `SearchPage` with held relay lookups. It covers: - an unjoined open channel that appears when the directory arrives, with the "Open" badge, ranked above a joined channel with a weaker match - the spinner stopping when the people lookup finishes last - a letters-in-order query - a query with no matches that shows "No results" - a clear and retype of the same text, where late replies do not stop the spinner - the Channels tab showing "No results" when only People matched - a bare `#` whose directory finishes last, then failed directory loads that show the notice instead of "No results", then Retry channels finding the open channel - a community switch during a directory load, where the retired load does not settle the new search - A third widget test drives the real `ensureDirectoryLoaded` → `retryDirectory` path, with the membership query held at the relay boundary. The spinner stays on while that query is held. A failure shows the notice, and another caller's directory load also keeps the spinner on. - The journey fails on main's `search_provider.dart`. It also fails in each of these mutations: - the old loading rule restored - the generation fence replaced by the old query-text check - membership sorted before match rank - `name_match_test.dart` checks 20 name/query pairs. Their expected ranks come from running buzz-app's `matchRank` on the same inputs. - `search_channel_rank_test.dart` is a table test of `rankSearchChannels`: joined-wins ties, open ties by name, archived ordering and membership, private and DM filtering, `#` handling, and the 8-row cutoff. - `channels_provider_test.dart` checks that `ensureDirectoryLoaded` reads again only once the directory is older than `maxAge`. - 17 mutations of the review fixes each fail a test: the archived filter, the archived penalty, the 8-row limit, joined ties, open ties, `#` handling (×2), the failure flag, the failure flag while loading, search-owned pending, the load fence, the rebuild reset, the scoped status spinner, the search TTL, the TTL check, filter-aware "No results", and Retry. - Displaced loads (`efc17a5d4`, `22e52d687`): the journey retires a load before it is marked loading, then displaces the retry after (one retry, then Retry channels with no "No results"). It also checks that the notice clears when another caller loads the directory, and that it is not shown during the first debounce. A displaced Retry recovers. A community switch whose held list reload must match neither the old community's `general` channel nor its name on a message hit. The production-path test holds the real refresh's membership query, runs `refresh()`, and asserts the one retry. `channels_provider_test.dart` expects a displaced load to end idle. Each of 8 mutations fails a test: no retry, retry only on idle, retry on a relay error, notice only on error, no attempted flag, `.value` in ranking, `.value` in message names, and `markError` on displacement. - Review fixes (`29c74e8c5`, `fe4276538`): both displaced-load journeys now start from an earlier same-scope failure (asserted) instead of an artificial idle reset. The notice's live region is asserted. A bare `#` after a failure, and while another caller loads the directory, makes no directory request, shows no spinner and no notice. Each of 5 mutations fails a test: no error reset in `beginRefresh`, no live region, a directory load for `#`, the notice for `#`, and the spinner for `#`. - Directory races (`c73b028ac`): `channels_provider_test.dart` switches A→B→A and expects a new directory read, and starts a directory load while the first load's member metadata is held, releases it last, and expects the list with the open channel and no error. Both tests fail on `fe4276538`, and each fails again when its fix is removed. - Review hardening (`85042b1a4`): `retryDirectory` clears a same-scope error before its first await. A test reads `idle` synchronously after the call; it failed before the fix. - `just mobile-check`, `just mobile-test` and `just file-size-check` pass at `85042b1a4` on `origin/main` `b80217de3` (4052 tests, 32 skipped). - Carl's re-review of `c73b028ac` (via Wes's account): no blockers. Its optional hardening note is done in `85042b1a4`. The 8-row cap is still Wes's product call. - Wes's review (Carl) of `22e52d687`: the blocker (a stale same-scope error skipped the retry) is fixed in `29c74e8c5`. The optional `#` and live-region items are done. The 8-row cap is left for the PR author's product call. The in-app run below is from `3b5521c51`. - Agent review of `efc17a5d4`: Cerveau found no must-fix or should-fix. Auto found one should-fix (a load retired before `markLoading` got no retry and no notice), fixed in `22e52d687` with its optional notice-liveness and A→B→A items. Cerveau's optional coverage items are also done there. - Not covered by a test yet (follow-up): the empty state on the Messages and People tabs when only channels matched. - `just mobile-check` and `just mobile-test` pass at `3b5521c51` on `origin/main` `0c82435ae` (4008 tests, 28 skipped). - Agent review: Auto and Cerveau reviewed `b8a268e2c` independently. Every must-fix and should-fix is fixed in `28a0990e5`. Cerveau confirmed both of its must-fix items at that head. It then found one regression in my fix, a spinner gap during the real refresh, and that is fixed in `3b5521c51`. Cerveau re-reviewed `28a0990e5..3b5521c51`, found nothing new, and confirmed that all three of its blockers are closed. Its own full mobile suite run at `3b5521c51`: 4008 passed, 28 skipped. Auto re-checked `b8a268e2c..3b5521c51` and confirmed that all four of its should-fix items are closed and nothing new is must-fix. Auto's optional findings on `İ`, per-keystroke ranking and reload cost are listed under differences above. #### In the app (Android emulator, debug build of `3b5521c51`) No Xcode was available, so this ran on the Android emulator against a local relay. The viewer has joined `general` and `team-4things`. Another user owns the open channels `4things` and `retro2108`. https://github.com/user-attachments/assets/f54700c6-c34c-4ded-b7c7-c7e6e07aa43b 1. Channels tab, `4things`: the spinner shows while the open-channel list loads, then `4things` appears with the Open badge, above the joined `team-4things`, and the spinner stops. 2. `#`: the joined channels are listed, and the spinner stops. 3. Channels tab, `morty` (a person, no channel name): "No results for 'morty'". The All tab shows Morty under People. 4. The phone is cut off from the relay, and `retro2108` is created meanwhile. Searching for `retro2108` more than 30 s after the last load shows "Open channel search couldn't finish." with Retry channels. After the relay is reachable again, Retry channels finds `retro2108` with the Open badge. The local `ws://` relay needed two demo-only edits to the add-community checks, which are not part of this PR. 🤖 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> Co-authored-by: Wes <wesbillman@users.noreply.github.com> |
||
|
|
db4478e8d2 |
feat(mobile): organization sign-in for NIP-FI relays (#8700)
Block is turning on NIP-FI enforcement on the Block relay. Desktop gets organization sign-in in block/buzz-app#1424. Mobile can already attach NIP-FI assertions, but only for Builderlab-hosted communities, through the build's single Builderlab account. The dogfood build needs that account for public hosted communities, so it can't simply be pointed at the Block tenant. This adds a separate **organization realm**, configured at build time: - `BUZZ_ORGANIZATION_URL`: the tenant's HTTPS origin (for example `https://block.builderlab.xyz`). - `BUZZ_ORGANIZATION_RELAYS`: the exact relay hosts that trust that tenant. The two must be set together. The app refuses to start if they're malformed, overlap a hosted community, or reuse the hosted origin. Without them, nothing changes. For a configured relay: - The app keeps its own organization session and signs in through the browser at the tenant's existing login (`/api/goose/v1/auth/login`). Block SSO happens there. - It requests assertions from the same `/v1/buzz/identity/assertions` route that hosted communities use, and attaches them to the relay socket, HTTP, media, huddle and remote-agent requests. - Unlike the hosted path, it doesn't require an identity backup before the first binding. As on desktop, the tenant binds the member's existing key on first issuance. A key already bound to a different one is refused before any request is sent. - When signed out, it connects without an assertion and lets the relay decide. A bottom banner offers **Sign in**. After sign-in, the relay reconnects with a fresh assertion. Hosted communities keep their existing banner and flow. The tenant accepts only the registered `xyz.block.buzz.mobile:/auth/callback` return URL, so both realms share it. Each `BrowserAuth` owner leaves callbacks whose `state` belongs to another owner's attempt to that owner. ### Before merge - [ ] Kenny Lopez reviews the visible changes. ### Validation Added tests (`organization_relay_auth_test.dart`): - Configuration parsing and exact host matching. - An assertion is issued for the exact relay and key, then cached. - A different bound key is refused. - Signed out connects without an assertion. - Other relays never receive the assertion. - The shared callback is routed between the hosted and organization owners. Full `mobile-check` and `mobile-test` pass. RunnerTests pass locally except `testNativeNavigationTitleExpandsAgainAtTop`, which this PR doesn't touch. Added tests (`organization_sign_in_test.dart`, `relay_auth_isolation_test.dart`, `builderlab_account_test.dart`), each failing without its fix: - Concurrent account restores at cold start share one adopted account. - An exchange from a cancelled attempt never changes a newer sign-in. - Storage failures while clearing the intent or cancelling leave the busy state. - An unreadable saved organization session is replaced by signing in again. - A stalled organization issuer never delays a hosted relay's assertion. This test needs `BUZZ_AUTH_DOMAINS` and skips without it. CI's configured-auth-domains step doesn't list it yet, because that workflow change needs a token with `workflow` scope. Added tests also cover a restore after an offline start (the banner clears once a relay retry restores the session) and the iOS bridge allowlist (`/auth/native` and tenant login on Builderlab subdomains; other hosts, paths, ports, userinfo and fragments refused). Simulator (iPhone 17, iOS 27) with the dogfood defines against the production Block relay `buzz.block.builderlab.xyz`, on a WARP Mac: - The app starts with the organization configuration and connects to the relay while signed out, showing the organization **Sign in** banner. - **Sign in** opens the system sign-in session for `block.builderlab.xyz`, and the banner shows **Finishing organization sign-in…**. Cancelling returns to **Sign in** with no error. Before the bridge fix, no browser opened and the banner reported that sign-in did not finish. - The bottom banners (organization sign-in, hosted reconnect, and the sign-in recovery row) now sit above Home's floating Search/New bar, which used to cover **Sign in**. A journey test checks the banner clears the bar, and fails without the fix.  - The simulator run stopped at the system consent prompt (SSO does not work in the simulator). The app calls were driven through the VM service (the same `signIn()`/`cancel()` the banner buttons call) because the simulator could not be tapped. Device (iPhone 12 mini) against bb-block staging in enforce mode, where every probe without an assertion gets 401: - Signed out, the organization banner shows and the app stays responsive. Cancel at the consent prompt returns to **Sign in**, and retry works. - Block SSO through Okta completes, and the relay admits the reconnect with the assertion. - After relaunch the app connects, loads channel history, and shows no banner for over 60 seconds. - Before the restore fix, 2 of 4 cold starts showed "cannot use this community with this key" although the session was valid. With it, 8 of 8 cold starts connected. Known limitation: adding an enforcing organization relay by community link is tracked in #8791. The dogfood build enables this in squareup/buzz-releases (`tomb/dogfood-organization-sign-in`). App Store builds stay unchanged. 🤖 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> |
||
|
|
acf56fff6f |
fix(mobile): use the desktop agent squircle for every agent avatar (#8873)
On mobile, the Create agent form previewed emoji and image avatars as circles. Once the agent was created, its avatar showed as a rounded square, and other surfaces used yet other radii. None of them matched the agent squircle desktop uses. This PR makes `lib/shared/widgets/avatar_shape.dart` the single source of avatar silhouettes. Agents get desktop's squircle (traced from buzz-app's `avatar-squircle.svg`), and people keep circles. `avatarShape(isAgent:)`, `avatarPath()` and `AvatarClip` are the only ways to get either shape, and the squircle border class is private. Every agent avatar surface now goes through that bottleneck: - Create agent form and avatar picker previews (previously circles for emoji and photo avatars, and unclipped for catalog characters; `PresetAgentAvatar` now owns the clip) - The avatar picker's camera cutout (`ImageAvatarCapture` with `isAgent: true`); profile editing keeps the circle - Agents grid card portrait (previously a 22pt rounded rect) - `AvatarImage` (message authors, DM rows, member lists), the profile sheet, `SmallAvatar`, and the native avatar PNG used by iOS Add members (previously 30% rounded rects) - Huddle speaking halo, preparing placeholder, and spotlight frame - The iOS Add members fallback tile (Swift), shown while an avatar loads, now draws the same squircle instead of a 30% rounded rect To catch future drift, `avatar_shape_test.dart` adds a source guard. It fails when code under `lib/` picks an avatar shape inline from agent identity (for example `isAgent ? BoxShape.rectangle` or `if (!isAgent) return ClipOval`), hand-rolls the old 30%/60% agent radius, or uses a person shape (`ClipOval`, `CircleAvatar`, `BoxShape.circle`) under `lib/features/agents/`. Against main it flags 8 of the 10 old sites. It is best effort: the native PNG's inline ternary and the card's RRect didn't match those patterns and are fixed directly here. The Create agent form also gets 8pt above **Choose avatar**, so the button and everything below it move down from the preview, as Kenny requested. ### Before / after Captured on the iPhone SE (iOS 27) with a release build against a hosted Builderlab community, using a throwaway agent with an emoji avatar. In each pair, before (main) is on the left and after is on the right. **Create agent form** (also shows the 8pt spacing)  **Avatar picker**  **Agents card**  **Agent profile sheet**  **Message author avatar** (`AvatarImage`)  **DM list row** (`AvatarImage`)  **Add members** (native iOS list, `nativeAvatarImage`)  Not captured on device: `SmallAvatar` (thread reply and search avatars) and the Huddle halo, placeholder, and spotlight, which need a live agent thread or huddle. Widget tests cover these. ### Validation - Device: the before/after captures above. They predate the catalog preview clip ([ |
||
|
|
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>mobile-v0.27.0-rc.12 |
||
|
|
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>mobile-v0.27.0-rc.11 |
||
|
|
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> |
||
|
|
7d00c70805 |
feat(relay): publish NIP-FI verified profiles (BUZZ-433) (#8774)
## Summary Publish historical verified names from valid human NIP-FI assertions as relay-signed kind 30382 events. Keep profile publication optional and separate from access decisions. Disconnects and access changes initiate asynchronous cleanup without retracting profiles verified after the original trigger time. Publish `display_name` as the chosen short display handle and `username` as the account handle. Carry optional `full_name` in the signed assertion into a matching kind 30382 tag. Adding, changing, or removing the full name updates the profile immediately; retractions remove every name field. Older profiles without `full_name` remain valid. Adds opt-in issuer labels, discovery, and schema migration 0079. Deploy and drain all access writers before enabling publication; use a stable relay key and a shared publication-policy revision. Update clients before enabling assertions with `full_name`. ### Related issue Implements the Relay portion of [the verified-profile specification](https://github.com/block/buzz/pull/8745) at `500829a9` (BUZZ-433). This is the closest existing PR found. Companion client: https://github.com/block/buzz-app/pull/2005. Companion issuer: [KGoose/bb-identity #144773](https://github.com/squareup/cash-server/pull/144773), which includes `nostr_public_profile` in valid human assertions. ### Testing Passed the lifecycle test against a disposable local PostgreSQL database, including full-name addition, change, and removal: `cargo test -p buzz-db store::verified_profiles::postgres_tests::profile_lifecycle_retains_assertion_ordering --lib -- --ignored --exact` with `BUZZ_TEST_DATABASE_URL` set to that database. Independent agent review found no remaining blockers. No live Relay test or human verification yet. This remains a draft and does not attest the repository's human-testing checklist. For end-to-end testing, enable publication for a test issuer, admit a member with a signed profile claim, and query kind 30382. Verify short display handles, optional full names, changed names, missing-claim retraction, disconnect cleanup, and a later admission surviving delayed cleanup. Check the companion app against the same relay. Generated with Codex --------- Signed-off-by: Franco Sola <fsola@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> Co-authored-by: Bradley Axen <baxen@squareup.com> Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
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>mobile-v0.27.0-rc.10 |
||
|
|
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> |
||
|
|
eafd77e9f6 |
ci: bound Hermit fetches and let relay builds run without sccache (#8739)
## Summary
Two CI-workflow fixes from the 23–30 Oct flake audit. This is one of six
PRs split from the reviewed `flakey-tests` branch (siblings below). Each
PR applies on its own to `main`.
### Hermit tool downloads (5 runs, 1 on main)
- **Runs:** 40635610550 and 40941660945, in Detect Changed Paths
(`node`). The selection step printed nothing for 9–10 min, the job hit
its cap, and every gate failed as cancelled. Three more, 40373404267 on
main (PostgreSQL Tests and Desktop E2E Integration 1/3, `pgschema`),
40373003333 (Mobile Android, `cmake`) and 40373467575 (Desktop Build
macOS, `just`), got 504s during GitHub's release-asset outage on 10-26,
~17:54–18:00 UTC.
- **Cause:** jobs start with an empty Hermit cache, so a tool's first
call downloads it, with no deadline. Hermit v0.54.1 tries three times, a
second apart, then gives up.
- **Change:** a new `scripts/ci-hermit-retry.sh` runs a no-op tool
command with a 90 s deadline per attempt, three attempts and 15 s × n
backoff. The deadline is plain bash: job control gives each attempt its
own process group, which gets TERM and then KILL. macOS runners have no
`timeout(1)` and may run bash 3.2. A "Fetch Hermit tools" step calls it
for:
- `node` and `just` in Detect Changed Paths, where one stall cancels
every gate. One `sh -c` invocation covers both, so the worst case is 345
s, inside the 10-minute cap. Two invocations would take 690 s.
- `pgschema` in PostgreSQL Tests, Desktop E2E Integration and Backend
Integration (via `--help`, since it has no `--version`).
- `just` and `cmake` in the Android lane.
- `just` in Desktop Build (macOS).
The test, `scripts/ci-hermit-retry.test.mjs`, joins Detect Changed
Paths' `node --test` list, and the script joins the rust and mobile path
filters. One stalled attempt costs ~105 s, which fits every job's cap.
- A killed attempt doesn't break the retry. Hermit downloads to a temp
file and unpacks to a temp dir, and renames each into place only once
it's complete. Its lock is a kernel flock.
### sccache storage failures (2 runs)
- **Runs:** 40355289475 (Desktop E2E Relay postgres-tests, 10-26,
"Egress is over the account limit") and 40714034945 (relay-binaries,
10-28, "send http request").
- **Cause:** the sccache server checks its GHA cache storage at startup
and won't start if that fails. Cargo's first `sccache rustc -vV` spawns
it, so the build fails. `SCCACHE_IGNORE_SERVER_IO_ERROR` doesn't help
here: in v0.16.0 it is read only when a running server's compile
response fails.
- **Change:** a "Start sccache" step runs `sccache --start-server`. It
outputs `wrapper=sccache`, or an empty wrapper and a warning if the
server won't start. The three build steps take `RUSTC_WRAPPER` from it,
and cargo treats an empty `RUSTC_WRAPPER` as no wrapper.
- The fallback doesn't fail the job later, in sccache-action's post
step. At the pinned fc920bf, that step runs `sccache --show-stats` and
the JSON form, and fails the job if either exits non-zero. With no
server running, v0.16.0's `--show-stats` doesn't start one. It returns
zero stats from the client side and exits 0. So the step keeps its stats
annotation, and `disable_annotations` stays unset.
### Related issue
None found. **Overlaps #8213** ("ci: fix empty root-workspace Rust
caches and merge test-unit nextest runs"). #8213 already conflicts with
`main`, and this PR adds a conflict in `.github/workflows/ci.yml` (path
filters).
### Testing
Verified locally:
- With a fresh Hermit state dir, `node` was SIGKILLed mid-download, then
mid-unpack while holding the lock, then stopped with TERM by the
script's own 1 s deadline. Each retry produced a working v24.15.0, at
clean-unpack speed. The `sh -c` form fetched both tools, and a 1 s
deadline on `sh -c 'sleep 37 && ...'` stopped `sh` and its child,
leaving no orphan. Tested under macOS bash 3.2 and Debian bash 5.2.
- sccache v0.16.0 (aarch64-apple-darwin) ran against a GHA cache whose
`ACTIONS_RESULTS_URL` refused connections, in READ_ONLY and READ_WRITE
modes. `--start-server` exited 2 ("Server startup failed: cache storage
failed to read"). Both `--show-stats` forms then exited 0, with zero
stats and valid JSON. `--stop-server` exited 2, which shows no server
had started.
Only hosted CI can show:
- the script under the runners' real shells, and that healthy runs only
pay a few seconds;
- whether ~3 min of spaced attempts rides out the next outage. The 10-26
outage lasted ~6 min, longer than this covers;
- that the explicit `--start-server` behaves like cargo's implicit start
against the real GHA cache. The fallback branch needs a storage outage
to run in CI.
Not covered here: first fetches of other tools (rustup, uv, pnpm) still
have no deadline. A per-job Hermit package cache is the follow-up.
Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.
### Readiness
An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.
### Siblings
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8741 test(mobile): RunnerTests title fade and alert wait
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
fc0bb778b0 |
test(desktop): settle five E2E flake classes and clear a draft by Backspace (#8744)
## Summary Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two commits. The specs now settle before they measure or act, and remote-owned-mentions clears its draft reliably. Everything is test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### Unit 6: measuring before an animation finishes Each spec now calls `waitForAnimations(page)` before measuring. - `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring check at :1545 receives 5.13): `showPage("owned")` slides content in from `translateX(48px)` over 650 ms. The spec reads its three boxes in separate `boundingBox` round trips, so they can come from different points of the slide. - `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and #8666; the spacing check at :2485 receives 4.255): the avatar dialog has a 200 ms zoom-in and a 250 ms height transition. - `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998, 40759209995 and 40969909906, plus four runs with fractional menu-edge values): the context rail narrows from 380 to 280 px over 200 ms. ### Unit 7: `fill()` racing a caret move the app makes a frame later - `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and 40969858676): edit mode focuses the end of the composer in a `requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is appended instead of replacing the text. **Change:** use #8682's `replaceComposerText` at both call sites. - `workflow-local-controls.spec.ts:151` (main 40926393897 and 40957616146, and #8239): `selectVariable` restores the caret in a `requestAnimationFrame` after the spec's poll has passed. **Change:** wait two animation frames before filling. ### Unit 8: clicks that hit the wrong element - `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success toasts intercept the pointer. **Change:** a local `pressActionRow` helper presses the five action rows with `toBeEnabled`, focus and Enter. - `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235): `.last()` resolved while avatar fallbacks were still filling in on 200 ms timers, so it opened nadia's panel instead of Charlie's. **Change:** pick Charlie's row by its text, and check the panel's name before opening Runtime. ### Unit 9: the `createWorkflow` copies (preventive) - `navigation.spec.ts:153` (7 failures on main, none since #8681; 14 first-attempt failures in Smoke shard 7): an unscoped `getByLabel("Message text")` can fill the trigger's filter while `AnimatePresence` keeps it mounted. **Change:** wait for the step inspector, then fill inside it, as `workflows.spec` already does. The wait now lives in one `addSendMessageStep` helper in `tests/helpers/workflows.ts`, which `navigation.spec.ts` and `workflows.spec.ts` (`createWorkflow`, :716, :820) all call. - `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction Added trigger has no control labelled "Message text". ### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566` - **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed on retry and failed 1 of the 6 attempts it got in the window. `sourceRecord()` at :673 returned thread A's draft instead of null. - **Cause:** on ProseMirror, `fill("")` selects through the DOM, then presses Delete. If ProseMirror hasn't seen the selection yet, it blocks the Delete. Navigating to B then saves A's live text as its draft. This is not a product bug: had the editor really been empty, the cleanup would have cleared A's draft. - **Change:** delete the text with one Backspace per character from the end, then wait for the composer to read empty. The guard assertions are unchanged. ### Related issue None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate hunk. The two merge cleanly in either order. No open PR touches the tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and #3342 edit other tests in the same files. ### Testing Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass, and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over the touched specs and the helper found the same pre-existing errors as on their `main` copies. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
6e6f4e8a0d |
test(mobile): check the title fade's animation, wait 10 s for the alert (#8741)
## Summary Two RunnerTests fixes from the 23–30 Oct flake audit, both in `mobile/ios/RunnerTests/RunnerTests.swift`. Both are test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### `testLargeTitleTextFadesWithScrollAndCompactTitleCanReverse` (3 runs) - **Runs:** 40748163526 (job 123028155856), 40826294064 (#8679, job 123290415194, passed on attempt 2) and 40921441156 (#8694, job 123617149320), all in Mobile (iOS), attempt 1. None of the branches touches the navigation bar. Signature: `XCTAssertLessThan(visibleOpacity(of: centeredTitle), 0.99)` read 0.9999499, 1.0 and 1.0. - **Cause:** collapsing adds a 0.18 s `CABasicAnimation` on the compact title mask's opacity (`IosNavigationBar.swift` `applyScroll`). The test slept 60 ms and expected the presentation opacity to be mid-fade. The presentation layer follows wall-clock media time. When the Flutter app in the test host keeps the main thread busy, `Task.sleep` resumes after the fade has finished. - **Change:** drop the 60 ms sleep and the mid-fade samples. Right after `scroll(to: boundary)`, which runs the handler synchronously, check that the mask's model opacity is 1. Also check that it carries a `"titleFade"` opacity `CABasicAnimation` from 0 to 1, lasting 0.1–0.5 s. Nothing turns the run loop before the check, so the animation is still attached however long the main thread stalls. After the scroll back, a new `XCTAssertNil` checks that reversing removes the fade. The later 240 ms waits and their end-state checks are unchanged. - **Mutations that should fail it:** - Delete `compactTitleMask.add(fade, forKey: "titleFade")`: the `XCTUnwrap` throws. - Delete `removeAllAnimations()`: the new `XCTAssertNil` fails. The old test missed this. - Set `fade.duration = 0.001`: the band's lower bound fails. ### `testNativeRemovalConfirmationUsesDestructiveAlertAndCancelsOnce` (1 run) - **Run:** 40950548609 (#8433, Dart only, job 123717768224), attempt 1. Signature: `fulfillment(of: [completed], timeout: 3)` expired. - **Cause:** the busy test-host main thread that #8678 handled for other classes. The cancel reply comes from UIKit's dismissal completion, after the dismiss animation runs on that thread. - **Change:** raise the wait to 10 s, matching #8678's `callbackTimeout`. The assertion is unchanged. Dropping `resolve` from the dismissal completion still fails it, and so does resolving before `alert.dismiss`. ### Related issue None found. **Conflicts with #8568** ("Adapt iPhone Duo layouts, rail navigation, and native sheets"). #8568 rewrites the fade test into a loop but keeps the 60 ms sleep and the mid-fade samples. It already conflicts with `main`, and this PR adds `RunnerTests.swift` to its conflicts. #8665 only adds a voice-note test. ### Testing Not compiled against the app or run locally, because Xcode and RunnerTests bog this machine down. `swiftc -parse` accepts the file. The new assertions, copied into a stub `XCTestCase`, typecheck with `swiftc -typecheck` against the iOS simulator SDK and XCTest (Swift 5 mode). The full file needs Flutter and the Buzz module, so it wasn't typechecked as a whole. **The Mobile (iOS) lane is the check.** Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus - #8744 test(desktop): five settle classes and remote-owned-mentions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b7b545f090 |
test(db): drop scratch databases and roles even when a test fails (#8747)
## Summary `buzz-db` tests that create their own databases or roles dropped them as the last line of the test body. A failed assertion or a timeout skipped that line and left the database or role on the shared Postgres server. Seven files carried their own copy of `create_scratch_db_through` and its drop helper. - Adds `ScratchDb` and `ScratchRole` guards to `crates/buzz-db/src/test_support.rs`. Their `Drop` runs `DROP DATABASE IF EXISTS ... WITH (FORCE)` or `DROP ROLE IF EXISTS` on a fresh admin connection, on a separate thread with its own runtime. `ScratchSchema` already works this way, so the guards work under both runtime flavours and while a panicking test unwinds. `WITH (FORCE)` ends any sessions still open on the database, so pools need not be closed first. - A failed drop prints a warning instead of panicking, because a panic during unwinding would abort the test binary. - The seven per-file helpers and every end-of-test `DROP DATABASE` in `buzz-db` now use the guard. The replica-fence masked-activity test's least-privilege role uses `ScratchRole`. Admin pools and `close()` calls that only served cleanup are removed. Test behaviour is otherwise unchanged. - `crates/buzz-db/TESTING.md` now points new tests at the guards. - The second commit fixes a comment in `armed_pool_rejects_old_channel_inserts_through_public_api` that still described the old end-of-test drop. **Existing leftovers need dropping by hand.** This change stops new leftovers but does not remove the ones earlier failed runs already left in dev `buzz-postgres` containers. There were 24 on the author's machine, named `stall_r_*`, `buzz_admin_*`, `floor_probe_*`, `fence_*` and `cxl_*`. List them, check the list, then drop each one: ```sql SELECT datname FROM pg_database WHERE datname ~ '^(stall_r|buzz_admin|floor_probe|fence|cxl)_'; DROP DATABASE "<name>" WITH (FORCE); ``` The guards are private to `buzz-db`. `buzz-push-gateway`, `buzz-admin` and `buzz-relay` tests still create and drop scratch databases by hand, which could get a shared copy of the guards in a follow-up. ### Related issue None found. This continues the test-cleanup work in #8727. ### Testing - Ran the 70 affected `buzz-db` tests against the local `buzz-postgres` container: 68 passed. Neither failure comes from this change. `cluster_global_probe_commits_tokens_and_sessions_prove_coverage` hit the known `MaskedActivity { masked: 1 }` flake under load and passed when run alone. Because it panicked while holding a `ScratchDb`, it also showed a panicking test leaves nothing behind. `serving_lease_statement_timeout_cancels_slow_statement` failed with `VersionMismatch(56)`, because the shared dev database already has another branch's migration 56 applied. - After the run, the server's database and role lists matched the snapshot taken before it. - A temporary test panicked while holding an open transaction, an exclusive table lock and a live session as the scratch role. It left no database or role behind under either runtime flavour and printed no drop warnings. That test is not part of this PR. - `cargo fmt --check` and `cargo clippy -p buzz-db --all-targets -- -D warnings` pass. The tests ran before the commits were cherry-picked onto current main. Only clippy was re-run afterwards. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d14e95f90e |
test: stop desktop, buzz-agent and mobile tests leaking temp files (#8748)
## Summary Six test files wrote into the shared temp dir on every normal run and never removed what they wrote. **Desktop and buzz-agent (first commit)** - `desktop/src/protectedFeatures/tauriCommand.test.mjs` wrote its fake Tauri CLI and three result files straight into `/tmp` under pid-suffixed names. They now live under one `mkdtempSync` root that an `after()` hook removes. - `crates/buzz-agent/tests/permission_boundary.rs` wrote a `buzz_perm_calllog_*` file for every test whose tool call reached `fake_mcp`, five per run. `call_log_path` now returns a `TempDir` guard with the log path inside it, so the log is deleted when the test ends, including when an assertion panics. - `cancel_kills_inflight_tool_via_mcp_notification` in `crates/buzz-agent/tests/regressions.rs` spawns `buzz-dev-mcp`, which the agent stops with SIGKILL, so the server's `buzz-dev-mcp-*` and `buzz-dev-mcp-session-*` TempDirs were never deleted. The test now passes the server a `TMPDIR` it owns and removes. **Mobile (second commit)** The three mobile test files left 12 `buzz-video-*` files and 4 `transcode_*` dirs per run. - `video_viewer_test.dart` and `video_controls_test.dart` pointed their fake `path_provider` at `Directory.systemTemp`. The viewer downloads each video there as `buzz-video-*`, and its async delete on close never completes under FakeAsync. Each test now gets its own temp dir for the fake, and an `addTearDown` removes it with `deleteSync`. In `video_controls_test.dart` that teardown is registered first, so it runs after the viewer is disposed. - Four `media_upload_test.dart` tests faked `transcodeVideoToMp4` by writing `out.mp4` into a new `systemTemp` `transcode_*` dir. The upload service deletes `out.mp4` but not its dir. The fakes now create that dir under the test's own temp root, which the test already deletes in `finally`. Production code is unchanged. **`buzz-dev-mcp`'s SIGKILL shutdown is left for a follow-up:** the agent still stops the server with SIGKILL, so outside tests the server's temp dirs are still left behind. Sending SIGTERM first is the real fix. ### Related issue None found. This continues the test-cleanup work in #8727. ### Testing Each run below was bracketed by listings of `/tmp` and the per-user temp dir (`/var/folders/.../T`), so new leftovers would show up. - `tauriCommand.test.mjs`: 3/3 pass, with no new `buzz-tauri-*` or `buzz-fake-tauri-*` entries. - `cargo test -p buzz-agent --test permission_boundary`: 9/9 pass, with no new `buzz_perm_calllog_*` files. The tests still check that the call log is written, so the log really is going into the guarded dir. - `cancel_kills_inflight_tool_via_mcp_notification` passes with `buzz-dev-mcp` built, so it ran rather than skipped, and left no new `buzz-dev-mcp-*` dirs. - Mobile: `video_viewer_test` (7 tests), `video_controls_test` (20) and `media_upload_test` (38) pass, each run on its own. With `TMPDIR` pointed at a scratch dir, the old versions left 12 `buzz-video-*` files and 4 `transcode_*` dirs, and the new versions left it empty. - After cherry-picking onto current main, these pass: - `cargo clippy -p buzz-agent --all-targets -- -D warnings` - biome and `node --check` on `tauriCommand.test.mjs` - `dart format` and `flutter analyze` on the three mobile files 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
704f2b8c27 |
test(desktop): keep unit tests away from the real ~/.buzz nest (#8749)
## Summary
`nest_dir()` fell back to `~/.buzz` whenever `init_nest_dir` had not
run. That is always true in unit tests, so a test that reached nest code
read the real nest and could rewrite `~/.buzz/AGENTS.md`.
- Under `cfg(test)` that fallback now panics. The message says how to
pin the nest: `pin_temp_nest_dir_for_thread()` for work on the test
thread, or `pin_nest_dir_for_test` in a child-process test for work on
other threads. The new thread pin returns a guard that owns a `TempDir`.
Dropping it unpins the thread and deletes the dir. A process-wide pin
does not fit in-process tests, because `cargo test` runs them all in one
process and the `OnceLock` can be set only once.
- Seven tests reached the fallback, all through
`default_agent_workdir()`:
`auth_command_uses_augmented_path_for_node_adapter`,
`test_prepared_install_command_uses_default_workdir` and five
`provider_deploy_*` tests. They now pin a temp nest. Under `cfg!(test)`,
`default_agent_workdir()` resolves on every call instead of caching for
the process, because a cached per-test nest would be deleted before the
next test used it.
- `nest_dir_is_under_home` and `init_nest_dir_prod_sets_buzz` could only
pass by reaching the fallback. A `should_panic` test of the guard
replaces them, plus a test that the thread pin and its dir go away on
drop.
- `init_nest_dir()` still resolved the real `~/.buzz{,-dev}` under
`cfg(test)`. An in-process test that reached `run_boot_migrations` would
have set the process-wide nest to the real one for every later test.
Now, under `cfg(test)`, it panics unless the nest is already pinned with
`pin_nest_dir_for_test`, and is a no-op when it is, as a second call is
in production. A thread pin does not count, because `init_nest_dir` sets
the nest for the whole process. No test reaches this today.
- Both panic messages point at `commands/agents_tests.rs`, which calls
`pin_nest_dir_for_test`, as the example to copy.
Production behaviour is unchanged.
### Related issue
None found. This continues the test-cleanup work in #8727.
### Testing
- `cargo test --workspace` in `desktop/src-tauri` passes: 3430 lib
tests, including both child-process pin tests.
- The new `init_nest_dir()` test runs in a child process, so a missing
guard cannot leak the real path to other tests. With the guard disabled,
it fails at "unpinned init_nest_dir() must panic".
- The `managed_agents::nest` tests pass with the final panic message.
- `just desktop-tauri-clippy` passes on the default and mesh-llm graphs,
re-run after cherry-picking onto current main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
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>mobile-v0.27.0-rc.9 |
||
|
|
0c82435ae0 |
Add Buzz background and flying bees to invitations (#8763)
Adds Buzz’s chartreuse background and flying bee swarm to community invitations, with cursor repulsion and reduced-motion support. Positions the desktop Buzz logo 40px from the top and left; hides the standalone logo on mobile. Follows #8728. Validation: web checks, production build, and all 10 browser tests passed on current main plus this change. Repository-wide local `just ci` did not complete: its process ended during Rust checks without a final result, so full-repository validation remains unverified. Screenshots use preview community metadata with reduced motion enabled. | Desktop | Mobile | | --- | --- | |  |  | --------- 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](
|