mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
f7708d0694dbbd92bb6f4cd2b6872f4bbdc3cebb
370 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f7708d0694 |
Polish mobile onboarding and profile setup (#8438)
Originally stacked on #8375, now merged. Rebased onto main. Polishes the onboarding landing page, community-link entry, and recovery sheets. Profile setup uses a consistent sheet with a larger avatar, inline display name, and draft photo/emoji/animated editing; iOS uses native controls and coordinated avatar/sheet transitions. Validation: mobile analysis, full tests, and normal pre-push checks passed after rebase. Prior iOS build and device walkthrough passed; native validation was not repeated after this rebase. Snapshots omitted as requested. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> |
||
|
|
8260b6ef4c |
feat(mobile): group typing avatars into one animated indicator (#8543)
## Summary Replace stacked mobile typing rows with one animated avatar cluster and one typing bubble. People and agents share diagonal, triangle, and four-corner layouts, with a circular +N badge for overflow and reduced-motion support. Includes an opt-in local demo (`BUZZ_TYPING_PREVIEW=true`) with mixed human/agent avatars and count controls; it never publishes typing events. ### Related issue Follow-up to #8505. No duplicate found in the typing-avatar PR search. ### Testing - 3,736 mobile tests plus four unconfigured-push tests passed; analysis and file-size checks passed. - Installed and iterated on a physical iPhone with the existing profile/community preserved; owner reviewed the motion and badge sizing. Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
e8781caa6f |
fix(mobile): remove Browse channels from channel list (#8539)
## Summary Remove the “Browse channels” action above the mobile channel list. The list now starts with its channel sections or empty state. Remove the unreachable browse sheet and its obsolete tests; keep channel directory providers and the create-channel/direct-message actions. ### Related issue No duplicate removal PR found. Related: #6243 introduced mobile channel browsing. ### Testing - `just mobile-check mobile-test`: formatting and analysis passed; 3,729 tests passed, 16 skipped, plus all four unconfigured-push tests. - Release iOS build succeeded. Installed and launched in place on a physical iPhone Air in Pairing Preview; existing preferences retained except channel read-state updated after launch. - Existing quick-action test asserts the browse button is absent. - `git diff --check` passed. - Pre-push hooks passed on commit `269df8b3e` (full mobile checks and tests plus file-size gate). The first attempt timed out in the unrelated iOS push-gateway validator test; the unchanged-commit retry passed. - Full repository `just ci` remains in progress through native desktop checks; not claimed green. Draft pending owner visual confirmation and review. Screenshots have not been captured; installation/launch is not visual acceptance. Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
1a54eec3db |
fix(mobile): keep token labels visible in authored Markdown links (#7545)
## Summary On iOS, an authored Markdown link renders as blank when its label is recognized as a Buzz channel, mention, or custom emoji. For example: ```markdown [#2959](https://example.com/) ``` The surrounding text remains visible, but the link has no visible label or tap target. Buzz's `_ChannelLinkMd`, `_MentionMd`, and `CustomEmojiMd` components return `WidgetSpan`s. `gpt_markdown` also renders an authored link through a `WidgetSpan` and reparses its label through the registered components. Because the Buzz components were allowed to run inside link labels, the result was a placeholder nested inside another placeholder, which does not paint on iOS. This PR adds the same scope restriction used by `gpt_markdown`'s built-in link component: ```dart @override Set<MarkdownScope> get scopes => MarkdownComponent.allScopesExceptLinkLabel; ``` The override is applied to all three widget-based components. Authored labels such as `[#2959](url)`, `[@Alice](url)`, and `[👋](url)` therefore remain ordinary link text, while the same tokens outside links continue rendering as Buzz pills and custom emoji. The PR also updates the shared test helpers to find `RichText` subclasses. Since the upgrade to `gpt_markdown` 1.2.1, body paragraphs can render through `BidiRichText`; `find.byType(RichText)` does not match that subclass and could allow an absence assertion to pass without inspecting the message text. The handling of unknown bare `#tokens` is deliberately unchanged. Whether those should remain non-interactive pills or become plain text is a separate product decision. ### Related issue Fixes #6124. This supersedes #6135. That PR solves the same issue by maintaining a custom fork of `gpt_markdown`'s link component; this PR instead uses the scoped-component API already available in `gpt_markdown` 1.2.1. ### Testing Focused suites: ```bash flutter test \ test/features/channels/message_content_test.dart \ test/features/channels/message_content_custom_emoji_test.dart ``` Result: 95 tests passed, 0 failed. Additional checks: - `just mobile-check` — passed; analyzer reported no issues. - `just mobile-fmt` — passed; 549 files formatted with 0 changes. - Six new regression tests cover channel, mention, emoji, plain-link control, and outside-link behavior. - Removing the `_ChannelLinkMd` override causes two regression tests to fail; removing `_MentionMd` or `CustomEmojiMd` causes one test to fail in each case. - Three tests in `message_content_test.dart` directly assert that the nested-placeholder structure is absent. The custom-emoji tests separately verify literal text and the absence or presence of `CustomEmojiImage`, depending on whether the shortcode is inside or outside a link. The complete local mobile suite reports 15 failures that reproduce identically on a clean checkout of `main` at `44316ff7`. In [CI for that same commit](https://github.com/block/buzz/actions/runs/34238736818), `Clients / Mobile`, `Mobile`, and `Mobile Swift Domain / Mobile Swift` all passed. No native-device screenshot is included. The nested-placeholder fix was verified through the rendered widget structure, but an iOS simulator build was blocked by the existing `MLImage.framework` arm64-simulator incompatibility. Signed-off-by: Mohammad Asfour <masfour700@gmail.com> Signed-off-by: Samin Yeasar Sohag <sssohag172@gmail.com> Co-authored-by: Samin Yeasar Sohag <sssohag172@gmail.com> |
||
|
|
0768397092 |
feat(mobile): show inline human typing and outgoing avatars (#8505)
## Summary People typing in a mobile channel now appear beside their avatar in an inline three-dot bubble, using the existing agent typing animation instead of the separate composer status strip. Human avatars stay round; agent identities retain their agent shape. Reduced Motion keeps the dots still, and thread-scoped typing remains inside its thread. Restore the viewer’s avatar on the right of their own messages in channel and Me timelines. Grouped messages show the avatar on the final bubble, with reactions and reply footers aligned to the bubble. ### Related issue No duplicate found. Related: #3668 tracks observer-backed agent working status; this PR changes presentation only and does not add observer lifecycle behavior. ### Testing - `just mobile-install`; final pre-push `just mobile-check && just mobile-test` passed (3,728 tests passed, 16 skipped; 4 unconfigured-push tests passed). Repository file-size gate and commit formatting passed. - Regression checks fail against the original channel typing and outgoing-avatar implementation. - Pixel 10 / Honeycomb Studios: the recording build exercised inline human/agent typing, avatar shapes, own-message avatars, and message arrival. That build predates this fresh-main PR; the PR additionally uses the shared agent identity lookup. - Before/after widget-rendered previews use generic sample messages. No recording identity, app-package override, or demo scripts are included. ### Before / after Rendered from the production channel widgets using generic sample messages; Reduce Motion enabled for the stills. | Before | After | | --- | --- | |  |  | ### Manual check Open a channel and have another person type: their round avatar and animated dots should appear at the bottom of the conversation and disappear when the message arrives. Send two consecutive messages: your avatar should appear once beside the final bubble. Repeat with Reduce Motion and larger text. Draft pending human testing of this PR snapshot. Do not merge based on the earlier recording alone. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
93114b4279 |
fix(mobile): lazily lay out new-DM recipients (#8435)
The new-message picker laid out every recipient inside an outer scroll view. Searching or changing selection could therefore mount the whole directory in one frame. Use a bounded sliver viewport so offscreen recipients stay lazy while the search field and selection chips still scroll above the keyboard. ### Validation - Current-head CI passed, including all Flutter test shards, Android, and native/release Swift jobs. Code and security reviews found no issues. After merging main and retaining both test imports, all 81 channel-page tests and the full mobile check/test suite passed. - Added a 200-person widget regression: the original mounts all 200 rows; the fix keeps fewer than 30 mounted through open, search, selection and removal, and can scroll to the final recipient. Covers no-match and closing too. - Strengthened the keyboard/chip test to verify the eight-recipient limit and that removing a chip makes the ninth recipient reachable. - All 79 channel-page tests passed; the strengthened limit test and Flutter analysis passed. - Native iOS simulator build passed. On a separately cloned paired simulator, verified populated directory/search, eight selected recipients, limit message, removal, no-match, scrolling to the final recipient and close without creating a conversation. Debug/AX instrumentation and a newer Flutter baseline make this functional validation, not a controlled release-speed benchmark. - Normal pre-push mobile checks passed, including formatting, analysis and the full Flutter test suite. Required CI exposed existing formatting in `message_carousel_preview_cases.dart`; a separate formatter-only commit corrects it. - The full local repository gate hit an unrelated Rust failure in `buzz-acp::signals_cancel_startup_turn_and_pending_stdin_without_hanging` (exit status `None` rather than `Some(143)`). No Rust code changed. The gate stopped at unit tests, so later build stages did not run. ### Related issue No duplicate recipient-layout fix found in the existing issue/PR search. ### Before and after The synthetic 200-person picker renders identically before and after (with a reserved keyboard inset). One image represents both; mounted-row assertions establish the performance mechanism separately.  --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
bf4a163510 |
fix(mobile): lazily lay out channel member groups (#8437)
Opening a large channel member sheet mounted and laid out every person in a grouped column. A captured Debug simulator entrance spent 242 ms in the UI callback, including 223 ms in layout. Build each person and agent as a lazy sliver row while preserving the grouped cards, names, statuses, management menus, and activity actions. Screen-reader row indices continue across both groups. A separate formatting-only commit fixes an existing carousel-test formatter discrepancy. No duplicate member-sheet performance PR or issue found. ### Validation - Added a 200-member widget regression: the original mounts 100 people initially; the fix mounts fewer than 20 nearby rows and reaches the final person and agent. - Added grouping, compact sizing, empty state, member tap identity, accessible indices, and scrolled member/agent menu checks. Existing channel-details “See all” navigation test passes. - Mobile formatting, static analysis, and the full mobile test suite passed on the committed tree. - Synthetic before/after rendering is pixel-identical at the same viewport. - Populated native iOS 26.5 simulator workflow: open members, scroll through people and agents to the end, then close. Final installed build passed this workflow. Debug, accessibility automation, profiler overhead, different Flutter versions, and shared-host load limit timing conclusions. This removes eager offscreen layout; it does not establish a controlled end-to-end speedup or release-device performance. ### Visual check Synthetic member sheet; before and after are pixel-identical at this viewport.  --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
a93f3282cc |
perf(mobile): retain preference crypto across reconnects (#8442)
Foreground reconnects rebuild preference managers and repeatedly derive the same NIP-44 keys on the UI isolate. Retain the six crypto objects in providers scoped to relay configuration and identity while preserving manager refresh, subscription, and publication behavior. Community changes, key rotation, and sign-out invalidate the retained objects. RelayConfig normalizes the signing key once at its shared accessor, so identity, theme encryption, event signing, and NIP-42 authentication receive the same value, including when an active configuration key has surrounding whitespace. The identity-export authorization snapshot uses the same normalized key for the active-identity check and the outgoing recovery payload. The branch incorporates main's relay-scoped preference managers. ### Validation - Regression exercises six production crypto providers across reconnects, configuration/key changes, and sign-out. Reintroducing a session dependency makes it fail. - Added a loopback WebSocket regression that exercises actual RelaySocket NIP-42 authentication with a padded key and verifies its challenge, relay, pubkey and signature. It fails before shared key normalization. - Added a connected-theme regression for whitespace-padded keys, encryption round-trip, and a valid published event signature. It fails before the normalization fix. - Added padded-key identity export coverage through authorization, SAS confirmation, exactly one normalized payload, and successful completion. Separate relay-change and key-rotation tests verify both fresh and expired grants fail closed. The padded-key regression failed before the fix. - Current head: all 48 focused pairing/crypto/auth tests and mobile analysis passed. All [CI checks](https://github.com/block/buzz/actions/runs/39497098787) passed, including mobile test shards, Android, native Swift tests and release builds. Codex feedback is addressed; Jude approved this exact head after independent regression and mutation checks. Fresh [Codex security review](https://github.com/block/buzz/pull/8442#issuecomment-6300273942) found no concrete issues. - Previous head: 122 config/socket/crypto/theme tests, 44 read-state tests, and all [GitHub CI checks](https://github.com/block/buzz/actions/runs/39491550425) passed, including mobile test shards, Android, native Swift tests and release builds. - Original implementation: native iOS Debug build and full local repository checks passed. A paired simulator background/resume capture had no samples in the targeted crypto constructors or key derivation; an 82 ms UI frame remained. Debug/JIT, profiler overhead and shared-host activity prevent an overall speedup claim. The later normalization change is covered by the production-provider regression above. - Two image decode timing failures occurred in the initial local full suite; direct baseline/patch runs, the subsequent full repository run, and the latest full mobile suite passed. Cold-start credential validation is unchanged: padded persisted keys are rejected by AuthNotifier before RelayConfig is constructed. The normalization regressions cover active configurations and identity export, not broader persisted-key acceptance. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
ca35114209 |
fix(mobile): simplify and refine the quick actions menu (#8491)
Keeps the mobile + menu focused on Create channel and New direct message, with 8-point facing corners, a 6-point gap, and the active Home/Solo tab fill on iOS. Moves Browse channels directly above the channel list, including its empty state, preserving open-channel discovery, retry, and Join. Adds a cold-start journey through the real channel loader and join action with relay boundaries faked. Validation: mobile formatting/analysis and full mobile test suite; mutation check confirms the new journey fails when directory loading is removed. The original menu styling was tried on a physical iPhone; the replacement Browse entry has widget-journey coverage and has not yet been tried on device. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
cdc2b129c0 |
fix(mobile): resolve DM display labels once per sort (#8488)
Home's DM sort resolves participant identity names again for every comparator call. This work appeared in a captured 82.163 ms UI frame after foregrounding the full app (Debug/JIT iOS simulator, shared host). Resolve each lowercase display label once per sort, retaining the same label ordering and channel-ID tie-breaker. Sort keys are invocation-local, so later viewer, profile, owner, participant, and community changes cannot reuse stale labels. No matching open mobile sorting PR or issue found. ### Validation - Added synthetic regression coverage for bounded contextual resolution and ordering parity across identity/name/channel changes, ties, unnamed DMs, and empty/singleton lists. The 80-DM fixture performs 80 contextual resolutions instead of the original comparator’s 982; restoring that comparator makes the regression fail. - 133 focused tests passed, including existing Home scrolling/reconnect widget tests and identity-name tests. - Mobile static analysis passed; 3,697 tests passed with 16 existing skips, plus four no-gateway configuration tests. - No new full-app or hardware capture. The existing trace establishes repeated synchronous work, not its release-build magnitude or the cause of every scrolling stall. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
5fefec604f |
fix(mobile): restore channel icons and align chat colors with desktop (#8478)
## Summary - Restores `#` and lock icons for public and private channels on Home, with matching loading placeholders. - Updates the dark-mode navigation colors to the desktop design-system tokens (read and unread text, section headings). - Replaces the chat color picker with the seven desktop accents (Gray, Chartreuse, Flame, Magenta, Iris, Cobalt, Jade). Saved legacy colors map to the closest one. - Uses white text in your own bubbles, and black where white falls below 4.5:1 (Chartreuse, dark-mode Gray). Links (underlined), quotes, tables and code use the same text color. - Insets code blocks in bubbles evenly, with corners concentric to the bubble and even inner padding. - The swipe-to-reply indicator uses the chosen chat color. ## Testing - `flutter analyze` and `dart format` are clean. The full mobile suite passes (3,705 tests). - Installed and checked on a Pixel 10 and an iPhone Air against a real community. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Queue <874bf2d815c2c4b459b90438c669c57f2702f3a76bb6e16fa97184259aac3e9e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c9e35f8ac7 |
fix(mobile): restore Solo chats from channel labels (#8466)
Restore Solo chats and project placement on mobile using `buzz.solo`
labels and creator metadata. Invalidate stale channel details when
identity changes so opening and sending preserve refreshed Solo
identity; shared chats retain ordinary sending. New mobile Solo chats
publish the discovery label, and the personal send guard enforces
ownership even when route hints or legacy placement suggest Solo.
The current desktop producer lives in `block/buzz-app`. [Contract
documentation](
|
||
|
|
edbfbf08db |
test(mobile): journey-first testing guidance and suite cleanup (#8397)
## Summary This PR does two things. It updates the mobile testing conventions to favor a few journey tests over many small logic tests. It also applies those conventions to the existing mobile suite, one commit per step. ### Testing conventions (`AGENTS.md`, `mobile/README.md`) - Start with one widget journey per user-visible flow, driven through the real widgets, notifiers and coordinators. Assert what the user sees and what is durably saved. - Cover retry, cancel and resume as steps inside the journey, not as separate tests. - Fake only external boundaries (network, relay, OS, browser, storage). - Use focused, table-driven tests only for security and data-loss invariants, protocol or crypto vectors, and pure functions with many input combinations. - When review finds a defect, extend an existing journey or table instead of adding a test per finding. - Delete a lower-level test only once a journey demonstrably catches the same failures. - Every test must fail when the production code it guards is broken. These conventions keep the Review-Proven Rules' safety bar and change only how coverage is organized. ### Suite inventory and plan The suite has 234 Dart test files, about 110k lines and about 2,600 declared tests. Native tests are separate: BuzzPushKit, RunnerTests, NativeControlTests and Android. Line-saving estimates are hypotheses, not a deletion budget. Each deletion needs a replacement that rejects a representative production mutation. 1. **CI and reliability.** Run the native tests CI skipped, make time-dependent tests deterministic, and make tests that could not fail falsifiable. *(done, see below)* 2. **Shared harness.** Add one fake relay (socket plus HTTP boundary, with the real `RelaySessionNotifier`) in `test/helpers/`. Today 38 files each write their own `RelaySessionNotifier` fake. *(done, see below)* 3. **Per-area journeys.** Convert page tests that fake the flow's own notifiers (for example, `channel_detail_page_test` overrides about 60 providers). Fold near-duplicates into tables. Remove constant-pinning, pixel-geometry and call-count tests once a journey covers the outcome. Split `channel_detail_page_test.dart` (17.7k lines). Each area commit reports before/after counts and the mutation it rejects. 4. **CI speed.** Shard by file instead of by test, since every shard currently compiles the whole suite. *(done, see below)* Keep as-is: relay protocol and NIP handling, crypto vectors, auth, push security, persistence and journal invariants, recipient routing, and real-socket transport and cancellation tests. ### Step 1 changes - **Android CI** runs the 23 JVM unit tests, which were never run before. It retries the APK build once, only on SDK or plugin download failures (the cause of 4 recent `main` failures). - **iOS CI** runs all RunnerTests (previously 2 of 99) and the previously uncalled Me-agent accessibility script. The Swift Native lane builds the simulator app once, in the background while the simulator boots and runs the native-control scripts, instead of twice. The lane stays non-required. - **Stale RunnerTests.** Two reaction-details tests still expected the filter list that #8280 replaced; they now cover the toggle list. #8393 moved navigation bar controls onto a shared glass surface and made unknown Tabler symbols assert. The avatar and members-activity tests now find controls on that surface, three tests use real symbol names, and a 240 pt title width pinned to the old control sizes is removed (its gap assertions remain). - **Known bug, not fixed here.** Since #8393 the DM title avatar image is nil, because `makeItem` returns avatar data as a custom-view item. No screen passes a title avatar yet. The test marks it `XCTExpectFailure`. - **Typing retry** and **sections backoff** tests use fake time instead of about 37 s of real sleeps and a `Stopwatch < 100ms` threshold. - **Manage-channel sheet tests** previously asserted inside callbacks that the sheet's `catch (_)` swallows. A wrong saved description made the test loop until killed. The tests now record arguments, assert outside the caught code, and cancel instead of re-presenting endlessly. - **`video_viewer_transport_test` removed.** It exercised only `package:http`. Removing the production `request.sink.close()` already fails `video_viewer_test`. ### Step 2 changes - **`test/helpers/fake_relay.dart`** fakes only the websocket (through the session's socket factory) and the HTTP bridge (through its client). The real `RelaySessionNotifier` still connects from `build()`, handles OK and CLOSED, batches events, backs off and replays subscriptions. - The fake serves the NIP-CW channel window. It can hold OK and live echo independently, reject a chosen publish, and drop the connection. Any request it cannot answer faithfully fails the test at teardown, because the app often catches relay errors and falls back. - **`channel_messages_journey_test`**: history through the channel window, a live message, a send that is pending until OK and stays one row after the echo, a rejected send rolled back, then drop and reconnect with missed and later messages. - **`channel_send_journey_test`**: type and send on `ChannelDetailPage` with no feature-notifier overrides. The row and sending spinner show at once, OK settles them, and a rejected send puts the text back in the composer and shows a snackbar. - Existing per-file relay fakes and `SidebarRelay` are unchanged. Step 3 migrates them area by area. ### Step 4 changes - `scripts/mobile-test-shard.sh` splits the 235 test files across the 3 shards, balanced by line count. `flutter test --total-shards` split tests, so each shard compiled and loaded every file. Locally at 4 workers, one shard took 63 s instead of 73 s and used 40% less CPU. `channel_detail_page_test.dart` (17.7k lines) is now the long pole; splitting it in step 3 shortens the slowest shard. Out of scope here: requiring branches to be up to date before merge (a repository setting), and the onboarding tests in #8375. ## Validation - Mutation checks: - Typing retry bound: test fails. - Sections backoff reset: test fails. - Wrong saved description: fails in about 11 s. The old tests hung instead. - Removed sink close: `video_viewer_test` fails. - Changed Dart files pass locally, and `flutter analyze` is clean. - Android `testDebugUnitTest` passes locally (23 tests). The Me-agent accessibility script passes on a local simulator. - Step 2 mutation checks, each made a journey fail: accepted OK ignored, rejected row not rolled back, optimistic row not added, draft not restored, local message left pending after OK, channel window replaced by the fallback, live subscriptions not replayed after reconnect. - Both journeys run in about 1 s on fake time. - All 99 RunnerTests pass on an iPhone 16 iOS 26.5 simulator, the CI destination, after rebasing on #8398. - The shard script partitions all 235 test files exactly once. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3ec6327618 |
fix(mobile): keep cached state with its community (#8349)
## Summary One identity can be paired with several communities, but mobile keyed several caches by pubkey only. Mobile keeps a single `ProviderScope` and doesn't remount on a community switch, so one community's data showed up in another, and some of it was published to the other community's relay. - **Channel groups**: the second community showed the first community's groups. - **Read marks** (contexts, publishable set, NIP-RS client and slot IDs): the first community's context IDs were merged into, and published to, the second community's read-state slot. - **Inbox done/unread rows and thread follows**: one shared 500-entry cap, so activity in one community evicted another's entries. - **Channel stars and mutes**: union-merged across communities and published to every relay. Every one of these is now keyed by pubkey + normalized community relay, the same way channel sort already is. **Switching communities.** The relay session rebuilds for the new community before the previous community's managers are retired. Each manager (groups, read state, stars, mutes) now checks that the session still serves its own community before it publishes, merges a fetch, or handles a live event. Previously, a pending publish flushed on dispose, or an in-flight fetch, could cross into the next community. **Old pubkey-only entries.** Channel IDs can repeat across communities, so nothing tells us which community an old channel-keyed entry belongs to: - **Groups, stars, mutes and read marks** are dropped on upgrade, and each community restores its own from its relay. Any change that never reached a relay is lost, and a read mark the relay doesn't hold shows as unread once. That's an accepted tradeoff: such changes would be very old by now. - **Inbox, thread follows, and read-state slot and client IDs** seed every community, and the old entry is kept. The inbox and follows entries are event IDs, which only ever match their own community. The slot ID keeps each community publishing to the slot it already has on its relay. This PR also removes the **Pulse** feed (`features/pulse/` and the Nostr filters only it used). Nothing in the app navigates to it anymore, and its providers were among the community-unscoped caches. ### Related issue None found. ### Testing - Each affected store has a regression test through the real provider that switches A → B → A. For groups, read marks, stars and mutes, the test sets a value in A, switches to B while A's change is still pending, and checks two cases: B's data arriving live and B answering A's in-flight fetch. It asserts that nothing sent to B contains A's IDs, B shows only its own value, and A comes back unchanged. Each guard was removed in turn to confirm a test fails without it, except two that the current switch order never reaches: the groups merge guard and the read-state live-event guard. - Upgrade tests cover each kind of old entry: dropped for groups, stars, mutes and read marks, seeded for inbox, follows and slot IDs. - `flutter analyze` is clean, and the full mobile suite passes in the pre-push hook at `89ebfcc12`. - Not yet exercised in the app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dcced2c4f7 |
fix(mobile): preserve saved identity lookup after enumeration fails (#8470)
An account-scoped saved identity could fail to open when secure-storage enumeration succeeded while showing choices, then failed before selection. Retain the discovered storage location and reread/validate that exact record before falling back to inventory lookup. Only public location hints are retained; signing material is read fresh. Follow-up accepted at merge of #8375: https://github.com/block/buzz/pull/8375#pullrequestreview-5577985447. Validation: existing self-hosted widget journey now injects enumeration failure after choices load and verifies the same key is admitted and persisted. Three focused files passed: 37 tests with hosted domains configured; 28 passed and 9 hosted-only skips with default configuration. Changed-file analysis passed. In an isolated clone, removing the location lookup failed that journey; control and restored runs passed. Self-review found no blocking defect. Draft acceptance gate: native/device behavior and human acceptance are not established by widget fixtures. On a disposable saved identity, enter a self-hosted community link, choose the identity, and confirm admission preserves it; cancel/retry as well. Hosted live-backend acceptance remains separate and pending backend availability. No full local suite was manually run; mandatory hooks/CI provide broader coverage. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@feedback.communities.buzz.xyz> |
||
|
|
91b1cc4761 |
perf(mobile): reuse the derived pubkey when signing auth events (#8460)
## Summary Each relay `/query` signs a NIP-98 event, and each relay media image GET signs a Blossom auth event. Both run on the UI isolate. `nostr`'s `Event.from` derives the pubkey from the secret key on every call, which is about a third of the cost of a signature. Both call sites now pass the pubkey that `myPubkeyProvider` already caches for the active key. The provider is derived from the same `relayConfigProvider` state as the key, so the two always match. This is the quick win. Moving signing to native secp256k1 or a worker isolate would remove the rest of the cost, and that would be a separate change. ## Before and after AOT-compiled benchmark on Apple silicon, `nostr` 2.0.0 `Event.from`, three runs of 50 signatures each: | | ms per signature | |---|---| | `main` (pubkey derived) | 8.8 to 9.6 | | this PR (pubkey passed) | 5.9 to 6.4 | A scroll that shows 20 uncached relay-media images now spends about 60 ms less on the UI thread. ## Test There are no new tests. The existing `queryRelay` test runs through the real `relayConfigProvider` and `myPubkeyProvider`, and asserts the signed event's pubkey for one identity. It does not verify the signature or switch identities, and it would still pass with this optimization removed. The media tests build the service directly, so they only cover the fallback where the pubkey is derived. Review traced the pairing instead. Both the key and the pubkey come from `relayConfigProvider`. `_postForEvents` reads both and signs with no await in between, and the media provider watches both. So the key and pubkey change together on community switch and sign-out. ## Validation - `flutter analyze` - Full mobile suite: 3,534 passed. **Human test:** Wes ran #8456, #8460 and #8462 together on the simulator with his real account and confirmed they work. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
52aa37a929 |
perf(mobile): stop an open chat rebuilding on other channels' events (#8456)
## Summary While a chat is open, a message in any other channel rebuilt every built row of that chat. There were two causes: - **The chat watched the whole channel list.** `channelsProvider` republishes the list on every live event in any channel. The chat page, its message list and the thread page watched all of it. They now watch only their own channel's entry. The `#channel` name map comes from a new `channelIdsByNameProvider`, which notifies only when a channel name or id changes. - **The root `App` rebuilt often, and each rebuild rebuilt every pushed route.** It watched the agent observer and the user-status cache only to keep them alive, and it watched the whole Me unread set. It now listens to the first two and selects only whether Me has unread. Rows still remount when a message arrives in the open channel. That comes from `scrollable_positioned_list` keying rows by index, and it gets its own PR. ## Before and after Setup: - iPhone 18 Pro simulator, debug build. - Local relay with about 120 ms of added round-trip time. - Seeded community: 120 channels, about 5.5k messages, a busy chat of about 1.4k messages. - The same scripted run on `main` at `5730c83f7` and on this branch, counted with local probes that are not part of this PR. - Counts are exact. Debug milliseconds are only useful for comparing the two runs. 10 messages arrive in another channel while the busy chat is open: | | `main` | this PR | |---|---|---| | Chat page rebuilds | 42 | 0 | | Message row rebuilds | 1,218 | 0 | | Frames with build time over 16.7 ms | 42 of 129 | 0 of 10 | | Worst frame build time (debug) | 78 ms | 13 ms | 5 messages arrive in the open chat: 1,300 row rebuilds on `main`, 693 on this PR. Most of the rest are the remounts from the index-keyed list. ## Test A widget test publishes a new channel list where only another channel changed, and fails if any `MessageContent` rebuilds. Reverting the production change makes it fail. ## Validation - `just mobile-check` - Full mobile suite: 3,532 passed. - The simulator run above. **Human test:** Wes ran #8456, #8460 and #8462 together on the simulator with his real account and confirmed they work. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
db50afd8a7 |
perf(mobile): load the channel list's membership-scoped reads together (#8462)
## Summary At startup and on every community switch, the channel list waits on a chain of relay reads, one round trip after another. Four of those reads need only the membership ids: channel metadata, hidden DMs, member snapshots and Huddle starts. Yet each one waited for the read before it. They now go out together, right after the membership list. Everything that depends on them is unchanged: DM participant names still follow metadata, and the latest-message batch still follows the filtered list. - `Future.wait(eagerError: true)` handles every request's failure and rethrows the first one unchanged, as soon as it happens. Before this change a failed read also stopped the chain. Hidden DMs and Huddle starts still fall back to empty on failure, as before. - The refresh fence checks the combined result once, before any state is written. That's the same fence check each read had before. This keeps the duplicate member-snapshot read. Replacing it with the membership list's events would need a relay-contract check first. ## Before and after Setup: - Simulator, debug build. - Local relay with about 120 ms of added round-trip time. - Seeded community with 120 channels. - 5 cold launches each, after one warm-up launch, on the same build with and without this change. - Timed with local probes that are not part of this PR. All 10 launches loaded 120 channels. | | `main` | this PR | |---|---|---| | Connected → channel list ready (median) | 1,233 ms | 715 ms | | Range | 1,154–1,297 ms | 689–747 ms | ## Test A test parks the metadata, hidden-DM and member-snapshot reads at the same time, then checks that all four reads were sent. If any read waits on another, it stays unsent and the test fails. It fails on `main`, where only metadata goes out, and on a version that sends metadata first and the rest after. Three switch tests park before discovery now, so their names say "during a directory refresh" instead of "after directory success". ## Validation - `flutter analyze` - Full mobile suite: 3,535 passed. - The simulator runs above. **Human test:** Wes ran #8456, #8460 and #8462 together on the simulator with his real account and confirmed they work. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bc9e7de904 |
fix(mobile): stop Settings rows blinking while Settings opens (#8463)
## Summary On iOS, opening Settings made the Status, Profile and Notifications cards disappear for most of the slide-up. They came back when the transition finished. Closing Settings did the same thing. **Cause:** Flutter's iOS compositor (`flow/view_slicer.cc`) puts Flutter content that's painted after a platform view and overlaps no native view into the background canvas. During the transition, Home's native nav bar and glass tab bar are still on screen under the full-screen Settings page, so the overlay created for Home's tab bar already covers the whole background. Everything painted after the native presence pill, outside a native view's rect, was hidden behind it. "Send identity to desktop" stayed visible only because it overlaps Home's tab bar. **Fix:** `IosPresenceMenu` mounts its native view only after its route settles (animation completed, nothing covering it). During route transitions, and while the native view is being created, it draws a matching Flutter stand-in: same capsule, colors, SF Pro Text at the same size and weight. ### Related issue N/A, none found. Reported by Wes from a screen recording. ### Testing - Reproduced in an uncommitted harness on an iOS 27 simulator: Home with native nav bar and glass tabs, then `BottomUpPageRoute`, then a Settings-shaped page using the real `IosPresenceMenu`. The cards blank out on every open and close. - A/B in the same harness: the blink goes away if any one of the three is removed (native pill, Home nav bar, Home glass tabs). - With this change, the open and close cycles in the harness show no blank frames. At rest the native pill is mounted again. Its pixels are identical to before, and the stand-in matches it within about 1px of text rasterization. - `settings_overlay_test.dart` now checks that the native pill isn't mounted mid-transition, and that exactly one native view is created after settling. The new assertion fails without the fix. - Pre-push mobile checks passed: analyze, format, full mobile test suite. **Human test:** on iOS, open and close Settings from Home a few times. Every card should stay visible throughout, and the "Online" pill should open the native availability menu once Settings has settled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b6aa5539e3 |
feat(mobile): account onboarding, key recovery and hosted reconnect (#8375)
## Behavior Adds optional mobile Builderlab sign-in, identity backup/recovery, community discovery/creation, and account-aware reconnect. **Builderlab features are disabled by default:** when `BUZZ_AUTH_DOMAINS` is unset or empty, account login, creation, backup/recovery, reconnect prompts, and background account restoration are hidden/disabled. QR/code pairing, links, and invites retain Nostr-only entry. Saved account state is retained for later opt-in. `--dart-define=BUZZ_AUTH_DOMAINS=communities.buzz.xyz,communities.public.builderlabstaging.xyz` opts those DNS domains and their subdomains into required account setup. The same policy gates pairing, deep links, durable community admission, and reconnect. A nonempty list enables Builderlab features; `BUZZ_BUILDERLAB_URL` alone does not. Enabled flows still require their backend services. Existing Nostr relay authentication remains mandatory. `BUZZ_HOSTED_RELAY_AUTH=true` enables scoped relay assertions and reconnect prompts only for configured authentication domains. Configure assertion issuance and each community's issuer binding before enabling assertions; enable relay enforcement last. Account credentials stay at Builderlab. Wrong-account recovery preserves keys; failed writes are not replayed; authentication is bounded by request deadlines. Hosted flows preserve keys across cancellation/restart, reconcile interrupted creation/invitation acceptance, and support manual nsec file import/export. Account identities are scoped to backend and authenticated subject; reusing an existing local key for an unbound account requires an explicit selection. Recovery reconciles the advertised and recovered inventories before automatic selection. Successful self-hosted admission retires its pending key, and iOS startup removes interrupted export files. Adds authenticated relay `GET /operator/communities/joined` for the companion Builderlab membership endpoint. ## Validation - Follow-up review repair at `66a12ac92`: self-hosted entry presents readable choices and import despite incomplete enumeration, while generation still requires a complete empty inventory; explicit selection uses exact-key lookup. Consuming a value-equal duplicate now advances dispatch. Reconnect requires an existing account binding or a subject-scoped backup receipt before signing an assertion, and pins the subject inside the serialized account request. An empty different account is refused without sending key proof. Focused checks: configured domains 58 passed; default 49 passed, 9 hosted-only skips. Separate isolated mutations fail the incomplete-inventory sheet, exact-key reuse, duplicate dispatch and reconnect journeys (including an empty-account-only bypass); restored controls pass. Evidence uses synthetic storage/network boundaries, not physical Android or live backend association. All mandatory commit/push hooks passed; fresh GitHub CI is pending. - Bounded review repair at `d3cc0003f`: incomplete secure-storage enumeration now blocks automatic identity generation while explicit known-key reads retain their fallback; declined/failed onboarding links release the queue; HTTP 429/5xx keeps Retry regardless of response encoding. Default app-gate coverage exercises a bind link followed by ordinary community entry. Focused checks: configured domains 47 passed; default 38 passed, 9 hosted-only skips. Removing the three production fixes in an isolated source/Git/build copy fails the identity-resolution, declined/failed queue, and Retry journey assertions; clean and restored controls pass. Storage and HTTP failures are synthetic; physical Android and live outage behavior were not exercised. Published after all mandatory commit/push hooks passed (mobile analysis/tests, branch-skew and file-size gates); fresh GitHub CI remains pending. - Existing whole-PR source review cleared the reconnect defects at `fb15d5b82`. - Domain-policy increment: focused environment, entry, app-gate, pairing and onboarding journeys pass with empty and configured domains. Existing pairing provider/page tests pass. Changed Dart files analyze cleanly. - Isolated source/Git/build mutation: forcing paired credentials through hosted login fails the default-build journey; clean and restored controls pass. CI now runs configured-domain journeys in addition to the default-build suite. - Integrated main through `5b433d929` at `0067c069c`, including its pinned Flutter upgrade. Preserved the PR welcome UI with the updated transition API and added Material surfaces around onboarding selection/consent rows. Focused post-merge runs pass in both configurations; all mandatory local push gates passed (mobile analysis/tests, Rust tests, desktop-native checks, branch-skew and file-size checks). - Merge-review repair at `a315bce11`: focused identity/storage/admission and widget journeys pass, including account switching with an empty or already-bound second account, explicit reuse, and self-hosted removal/rejoin with another key. Default-domain journeys pass too (hosted-only cases intentionally skip there). - Isolated source/Git/build mutation checks: bypassing account scoping, reuse consent, pending-key retirement, or recovery inventory reconciliation fails the relevant journey/invariant assertions; clean and restored controls pass. - Native `RunnerTests/IdentityFilePluginTests`: both tests pass on the iPhone 18 Pro iOS 27 simulator with an isolated app identifier. At `24a0dcaac`, the picker journey seeds residue in Library/Caches, matching path_provider_foundation, and verifies plugin startup removes it while preserving an unrelated file. The earlier tmp-only fixture did not exercise the actual export location. This is synthetic native cleanup/picker evidence, not a real terminated-share or live account round trip. - Follow-up: Caches and tmp cleanup; fallback to individually known identity slots when Android bulk enumeration fails; explicit replacement of a refused self-hosted key preserves its original before swapping the retry slot. Configured-domain journeys/invariants: 34 passed; default: 31 passed, 3 hosted-only skips. Isolated enumeration/retry mutations fail with passing controls. Bulk-read fallback preserves but cannot enumerate arbitrary other-account slots until bulk reads recover. - Configured-domain CI exposed a stale pairing-status assertion after account scoping. The repaired test checks the admission mismatch, preserved account/transfer keys, and no admitted community. The configured-domain CI selection passes locally (56 tests); removing the production mismatch guard fails the updated test, with clean/restored controls passing. This is synthetic coordinator evidence, not physical QR acceptance. - Final review repair (`40e1e6101`): native iOS export creation uses atomic complete file protection; normal and restart cleanup remain. Account/policy/claim HTTP uses one 30-second total deadline with request abort and body cancellation. Removed the unrelated NIP-FA spec edit. - Focused account/admission/export tests: 25 pass; configured-domain account/admission/onboarding journeys: 33 pass. Isolated deadline and native-dispatch mutations fail, with passing clean/restored controls. Native export/picker tests: 2 pass on iOS 27 simulator. The real file-protection attribute assertion is physical-device-only because simulator provides no Data Protection attribute; locked-device protection and interrupted real sharing have not been observed. - Opt-in UI: configured-domain CI selection passes (57 tests); default selection passes (7 tests, 10 hosted-only skips); URL-only plus whitespace/empty domains passes (6 tests, 9 hosted-only skips). Changed Dart files analyze cleanly. Independent mutations that unconditionally initialize the coordinator in the app gate, welcome entry, or link form fail the default journey; clean/restored controls pass. - Native welcome screens exercised on iPhone 18 Pro / iOS 27 simulator in default and configured builds: default exposes community link / QR / pairing code only; configured build additionally exposes Builderlab connection and community creation. No live hosted login was exercised in this check. - Delivery at `e48fe73a9`: all mandatory push hooks passed (full mobile analysis/tests, Rust tests, desktop-native checks, branch and file-size checks). GitHub checks had not yet populated at the immediate post-push snapshot. ## Review disposition | Feedback | Disposition | | --- | --- | | Explicit self-hosted entry under unreadable inventory, duplicate invite dispatch, reconnect cross-account binding ([entry](https://github.com/block/buzz/pull/8375#discussion_r4318421622), [queue](https://github.com/block/buzz/pull/8375#discussion_r4318421640), [reconnect](https://github.com/block/buzz/pull/8375#discussion_r4318455512)) | Repaired at `66a12ac92`; evidence above. Reconnect refuses unproven ownership rather than silently establishing a binding. | | Incomplete Android identity inventory, stranded deep-link queue, transient HTTP retry classification ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5576864143)) | All three repaired in `d3cc0003f`; focused journeys and isolated mutation evidence above. | | Account cross-link, incomplete recovery inventory, committed invite checkpoint, pending local key retirement/replacement | Repaired in the preceding commits; journey and mutation evidence above. | | iOS interrupted key export ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575253252)) | Native cleanup plus complete protection at creation implemented. Physical-device lock and real interrupted sharing acceptance remains pending. | | Trickle responses bypass idle timeout; unrelated NIP-FA edit ([review](https://github.com/block/buzz/pull/8375#pullrequestreview-5575764556)) | Total request deadline/abort implemented; spec edit removed. | | Production domains rejected | Ordinary entry now needs no Builderlab configuration. Opt-in account-required domains must still use supported community mappings. | | Lost Android Keystore recovery | Backup exclusions prevent the reviewed stale-restore scenario; genuine key loss still has no in-app recovery/reset and remains deferred. | | Native secp256k1 dependency question | Obsolete against the current base: upstream C integration landed in #8398; no package diff remains here. | | Operator membership endpoint contract | Documentation now states the operator chooses the pubkey; no end-user session binding is implied. Additional handler-level coverage is optional follow-up. | | Optional Android partial document cleanup/activity retention; local work after backup cancellation | Follow-up hardening, not claimed implemented. Cancellation prevents remote backup upload. | | Semgrep crypto inventory notices | Informational inventory, not demonstrated defects. | ## Remaining draft gates Native QR/browser-return acceptance remains unverified for this revision. Optional Builderlab onboarding still needs backend backup-status and joined-community routes, recovery policy/configuration, and real backup/recovery/reconnect acceptance. The empty-domain build removes these services as prerequisites for ordinary pairing; it does not make the optional hosted flows backend-ready. Human acceptance and current CI are still required before merge. Review dispositions retained: production auth-domain configuration must use supported community mappings; enabling `builderlab.xyz` does not add support for `buzz.block.builderlab.xyz`. Recovery after genuinely losing an Android Keystore key remains a follow-up, not a newly implemented capability. Human acceptance steps: build once without `BUZZ_AUTH_DOMAINS` (including with a previously saved account) and confirm no Builderlab controls/restoration while ordinary link and QR/code entry work. Build with supported domains and confirm connection/creation appear; exercise browser return and live backup/recovery/reconnect. On physical iOS, exercise export and interrupted sharing/locked-device protection. Record results before marking review complete. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@wes.communities.public.builderlabstaging.xyz> |
||
|
|
dc4cdc4990 |
fix(mobile): reset community notification preferences (#8455)
Community notification choices now use `pushNotificationsEnabledV2`, defaulting ON when absent. This gives every installed community a fresh enabled preference, including communities previously disabled. Subsequent explicit OFF choices persist per community through reloads and reconnects. Previous values are deliberately discarded, not migrated or preserved. There is no migration or compatibility read. Enabled communities enter the existing APNs enrollment and active relay lease publication path when connected to a push-capable relay. iOS still controls notification display authorization. Notification eligibility and lease addressing are unchanged. Related work: [#8425](https://github.com/block/buzz/pull/8425). ### Validation - Extended the settings integration test to load a saved community with the previous preference OFF and no new preference and verify native enrollment plus decrypted active lease publication, then OFF across reconnect and ON again. - The focused model test proves both previous values are ignored and new-key OFF wins even when the previous key says ON. A temporary compatibility-fallback mutation failed this test as expected; the implementation was restored unchanged. - Added storage coverage for independent community choices across reloads; updated existing model round-trip coverage for the new key. - Mobile dependency setup, formatting, analysis, and the full mobile suite passed, including gateway-unconfigured coverage. - Agent review found no blockers. Enrollment coverage uses the real Flutter bootstrap, storage, signing, and lease construction with mocked native and relay boundaries; no physical-device or live APNs delivery test was run. The fresh enabled preference is an intentional product exception to the general zero-notification default in VISION.md. Subsequent user choices and existing eligibility filters remain authoritative. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
4f1f1b5300 |
test(mobile): remove flaky channel collection assertion (#8440)
Remove the intermittent `departed channel composer and route are collectible` test introduced in [#8419](https://github.com/block/buzz/pull/8419), as requested after its CI failure could not be reproduced in focused macOS and Linux investigation. Keep the Flutter upgrade containing the upstream leak fix and all other tests. No replacement GC-based test is added. ### Validation All 379 remaining channel tests pass, along with formatting and analysis. The broad local gate was stopped during unrelated Rust compilation; no full-gate pass is claimed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
3debf18f14 |
fix(ios): supply missing push notification sound at handoff (#8443)
Temporarily default missing notification sound until the [gateway fix](https://github.com/block/buzz/pull/8431) is deployed. Preserve any sound already provided by APNs. ### Validation Final build 10151: automation verified the push banner on a real iPhone 12 mini against the unchanged staging gateway, and Tom confirmed hearing the sound. iOS build, two existing handoff tests, and normal pre-push passed. ### Before merge - [x] Verify notification delivery and sound on the final iPhone build. Replaces [#8429](https://github.com/block/buzz/pull/8429) with the identical commit to obtain a fresh owner review. The prior review requested tests; this temporary workaround intentionally adds no tests or helper at the author's request. [Prior review response](https://github.com/block/buzz/pull/8429#issuecomment-6256498498). --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
8f2c7710e9 |
fix(mobile): default community push notifications to on (#8425)
New mobile communities currently require a manual Settings opt-in before push can start. Default that local preference to on for new communities and stored records without a preference. Preserve every saved `true` or `false`, including values written by older clients. Older default-off values are indistinguishable from explicit opt-outs, so this does not migrate them to on. Push still requires a configured gateway, authenticated connectivity, and a valid relay capability. iOS continues to control display authorization through its existing permission prompt. Notification eligibility and subscriptions are unchanged. Settings retains its existing opt-out control for enabled communities even when capability is unavailable. Drafted from the 0.24 baseline; release-branch inclusion is a separate decision. ### Related issue Closest related issue: #3238 (synced defaults). This change is limited to the mobile device-local community preference; no duplicate found. ### Validation - Added or updated coverage for new and missing preferences, saved true/false persistence, opt-out after restart, capability-gated registration, and Settings without a configured gateway. - Full configured mobile suite: 3,499 passed, 4 skipped. Separate no-gateway suite: 4 passed. Flutter analysis passed. - Existing permission-denied and unsupported-relay tests passed as part of the mobile suite. No physical-device or end-to-end APNs validation performed. - Full local repository gate passed (formatting, lint, unit tests, desktop/web builds, Tauri checks/tests, and mobile tests). - Follow-up review edits: restored the original VISION.md wording and shared the default in one private Community constant. All 43 focused community/bootstrap tests passed. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
bbb5183f33 |
feat(mobile): add swipe-to-reply message gesture (#8436)
## Summary Swipe right on channel and thread messages to open the existing reply destination. Adds a centred accent progress ring, Reply label, threshold haptic/pulse, drag resistance, cancellation, and reduced-motion support. Retreating below readiness re-arms feedback. Includes a separate formatting-only fix required by main’s updated Dart formatter. ### Related issue Related message styling: #8281. No matching swipe-to-reply issue found. ### Testing - Full pre-push checks passed, including mobile analysis/tests and file-size checks. - Widget coverage: routing, repeated threshold crossings, cancellation, resistance, alignment, and centred scaling. - Release preview installed on iPhone with the existing profile/community; Kenny tested and confirmed the interaction. Draft: native preview verification predates the rebase onto main’s Flutter upgrade. No native gesture recording attached. --------- Signed-off-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
819dd7924f |
fix(mobile): bump Flutter 3.47, resolving memory leak (#8419)
With accessibility semantics active, opening and leaving channels
retained disposed composer overlay states through Flutter’s semantics
traversal map. Those states kept departed route and message trees alive.
This upgrades the pinned Flutter SDK from 3.41.7 to 3.47.7, which
includes [the upstream
fix](
|
||
|
|
e048f5ac78 |
fix(mobile): bound search result rendering work (#8418)
Global search eagerly mounted every result and parsed entire messages even for two-line previews. Large markdown hits could block the UI while changing categories, updating results, or reopening search. Build result rows lazily and bound long message previews to a literal prefix of at most 512 UTF-16 units without splitting surrogate pairs. Short messages retain rich formatting and mentions. Long snippets show markdown punctuation literally; opening a result still uses the original message. Truncated markdown is never passed to the rich renderer. ### Validation - Added regressions for lazy message/channel/people rows, scrolling to the final result, and a >100 KB markdown hit with a surrogate pair at the preview boundary. All 23 focused search tests pass; full mobile analysis is clean. The original large-hit test consumed a CPU core for over 30 seconds before being terminated. - Native iOS simulator build passed. Diagnostic Debug/JIT captures attributed a 1,033 ms search frame and a separate 12,972 ms result-update callback to widget/markdown work. A verified populated-results run of the final candidate completed category switching, scrolling, and closing, with a longest UI callback of 70 ms. Separate instrumented Debug/JIT runs have uncontrolled host load, warm-up, and live data; they are not release-performance benchmarks. - Full local gate passed through desktop builds/checks, then failed two unchanged native subprocess PID/timeout tests; both passed in isolation. The remaining web build and mobile suite passed. - [Codex review](https://github.com/block/buzz/pull/8418#issuecomment-6243734066) completed on the current head with no findings. [CI](https://github.com/block/buzz/actions/runs/39082758977) passed, including mobile checks, all three test shards, Android, the iOS release build, and native simulator build/tests. ### Preview examples Synthetic widget captures with app fonts, without real community data or native iOS glass controls. | Long message before | Bounded literal preview after | | --- | --- | |  |  | --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
27b9c827fc |
Polish mobile message layout and navigation controls (#8427)
Align outgoing messages and galleries to the right, reclaim the outgoing avatar gutter, and match desktop calendar dividers with balanced spacing. Standardize navigation and sheet icons at 24 points, restore native Edit/Done styling, and align section menus with trailing channel indicators. Validation: clean analysis; 3,513 mobile tests and push-configuration checks passed; iPhone Release build installed and tested by the owner. Includes regression coverage for message placement, date spacing, gallery direction, and section alignment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: fin <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
3a45e87b25 |
fix(mobile): polish Inbox controls and system notices (#8423)
Use Tabler `filter-2` in Inbox and keep the iOS filter button rounded when its menu closes. Remove shaded containers from chat system notices while preserving spacing, profile links, and reactions. Validation: mobile formatting, analysis, full test suite, and iOS Release build; independent review found no blockers. Kenny verified the icon and menu dismissal on his iPhone. --------- Signed-off-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
a96ace4e55 |
fix(mobile perf): avoid eager emoji palette scans for prose (#8416)
Ordinary mobile messages eagerly built a set of every community emoji shortcode and segmented their entire body just to decide whether to use enlarged emoji rendering. Stop at the first non-emoji grapheme and build the shortcode set only when a complete shortcode is encountered. Emoji-only sizing and shortcode matching stay unchanged. ### Validation - Full `just ci` passed. - 125 focused detector and message-rendering tests pass; targeted analysis is clean. The palette-access regression fails on the original implementation (10,320 unnecessary reads) and passes with the fix. It also checks that consecutive shortcodes share one palette scan. - **Full Buzz app, real community data, debug/JIT iOS simulator, not a rendering fixture:** two paired actual-input captures measured 125–127 µs per original detector call versus under 1 µs with the fix. All 5,200 executions per version agreed across 260 observed prose calls and a 2,580-entry palette. Instrumentation repeated each detector call, so these captures are excluded from frame comparisons. - Separate clean-code scrolling runs do **not** establish a reliable smoothness improvement: quiet original UI-callback p95 was 10.1–12.6 ms versus 10.1–12.9 ms fixed. A busy fixed run still reached 90.8 ms. Normal app services stayed enabled; only scrolling was exercised. Live background refresh, debug mode, host scheduling, and profiler overhead limit interpretation. No physical-device/release-AOT or end-to-end interaction-latency claim. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
bf8b356f3b |
Fix iOS channel crash from unbounded scroll depth (#8407)
### Summary Opening a channel on iOS 27.0.1 could terminate Buzz when a reversed, lazily loaded timeline reported an unbounded scroll extent. Flutter forwarded `Infinity` to the native navigation bar, and UIKit threw `CALayerInvalidGeometry` while setting the scroll view offset. Use the finite viewport depth for that unbounded case, preserving the scrolled-under appearance. Reject non-finite offsets at the native platform boundary as a final safety check. ### Validation - Added tests: a Flutter widget regression for an unbounded reversed timeline, and an iOS native regression for a non-finite platform-channel offset. - Reproduced the original crash by tapping `#general` on an iPhone SE running iOS 27.0.1. With this change, the channel rendered and survived repeated back-and-reopen cycles. Native logging confirmed the channel now sends finite offsets where it previously sent `Infinity`. - The native regression passed on iOS 26.5 and 27.0 simulators with Xcode 27.0. Flutter widget tests and `flutter analyze` passed. - A full channel-flow simulator run was not available: the isolated simulator Profile build could not resolve `BuzzPushKit`. The iOS 27.0.1 physical run exercised the full channel flow. - The full Flutter suite had eight notification and push test failures, all reproduced on the unchanged 0.24.0 source. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
2de484baf7 |
docs(ios): document Xcode 27 development setup (#8400)
Document Xcode 27.0 as the recommended iOS development toolchain while retaining Xcode 26 compatibility. Explain per-command selection, the known Flutter native-assets compiler/SDK mismatch on hosts with multiple Xcode versions, and where CI and release toolchains are configured. The compatibility changes are already merged in [#8379](https://github.com/block/buzz/pull/8379) and [#8398](https://github.com/block/buzz/pull/8398). Related open work is [#8330](https://github.com/block/buzz/pull/8330); this PR changes documentation only. The release version bump remains separate in [buzz-releases#110](https://github.com/squareup/buzz-releases/pull/110). ### Validation - Ordinary unsigned Release build passed on [main at |
||
|
|
c8ea4fb4eb |
feat(mobile): polish Home navigation and Solo workspace (#8393)
## Summary Polish mobile Home and Solo: group Search/Inbox beside a separate profile avatar, move Inbox and Settings to bottom-up pages with Close controls, and place tabs opposite the plus/compose action. Morph plus to compose clockwise when entering Solo and reverse counterclockwise when returning Home, with a directional quarter-turn of the icon inside a level button and a subtle scale pulse, respecting reduced motion. Align Search’s Close control with Inbox, simplify its placeholder, and initialize selected filter contrast correctly. Collapse the plus menu when leaving Home. Center Solo’s native-glass New chat empty state and keep it stationary behind the keyboard. Standardize Tabler icons and neutral backgrounds, retain SF Back/Close on iOS, use native Liquid Glass headers, tab container, and actions (solid grey selected tab, slightly darker on iOS) on iOS and black actions on Android, and smooth the expanded menu corners. Repair native header safe-area/return layout and compose movement after cancellation. ### Related issue None found for this UI refinement; follows #8337. ### Testing - Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3 push-configuration tests passed. File-size checks passed. - Base UI built and installed on Pixel and iPhone in place. Pixel navigation/New chat cancellation checked. Subsequent glass and rotation refinements are previewed on iPhone; motion feel awaits human confirmation. - Draft pending human confirmation of the final Settings entrance/return behavior on iPhone. ### Pixel snapshots | Solo | Expanded actions | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png" width="280" alt="Solo empty state with New chat" /> | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png" width="280" alt="Expanded channel actions with smooth corners" /> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
73962435b6 |
Use upstream C secp256k1 for native Nostr signatures (#8398)
Replace the Swift secp256k1 wrapper with a small native adapter over Bitcoin Core libsecp256k1. This is an alternative to the wrapper upgrade in [#8381](https://github.com/block/buzz/pull/8381), not an additional change to merge alongside it. The adapter covers the three Nostr operations Buzz needs: x-only public-key derivation, BIP-340 signing, and signature verification. It compiles pinned, unmodified upstream C source through SwiftPM. There is no build plugin, plugin approval script, or underscored Swift import. The app and notification extension use an explicit C module map and header search path. This trades the third-party Swift wrapper for a Buzz-owned adapter and responsibility for updating the vendored C source. Upstream v0.8.0 source, its license, and update provenance are included. The release Xcode pin remains separate in [buzz-releases#110](https://github.com/squareup/buzz-releases/pull/110). ### Validation - Native package tests passed on Xcode 27.0 and 26.5: 96 XCTest tests and 65 Swift Testing tests per toolchain. - Added tests cover the official BIP-340 vectors for 32-byte Nostr messages, invalid secret keys, and malformed input lengths. - Full unsigned Release app and notification extension build passed on exact Xcode 27.0. - Vendored files were compared byte-for-byte with upstream v0.8.0. - Full local `just ci` passed. - [Hosted full unsigned iOS Release build](https://github.com/block/buzz/actions/runs/38924020583/job/117051237094) passed. [Hosted Native Swift validation](https://github.com/block/buzz/actions/runs/38924020583/job/117051237153) passed, including the complete simulator build and native controls/push navigation tests. - Codex code review and security review completed with no findings. ### Before merge - [x] Tom selected the direct-C approach with vendored upstream source; [the wrapper alternative](https://github.com/block/buzz/pull/8381) is closed. Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
93ffc37cbd |
feat(mobile): find people by name through the relay's name-only profile search (#8333)
🤖 ## Summary In the mobile app, searching for "wes" did not find Wes. People search matched text anywhere in a profile, including the image data of a shared default avatar. Many agent profiles use that avatar, so they filled the results and the real Wes never appeared. Now every people search in the app uses the relay's new name search: the Search page, adding channel members, new messages, and @mention suggestions. It matches only a profile's name and display name and puts exact names first. The desktop app (block/buzz-app#1084), agent tools (block/buzz-app#1124) and the CLI (#8331) make the same change. Compatibility: a relay without the new search answers 404 or 405. That search uses the old search, and so do later searches on the same relay for five minutes; then the app tries the new search again, so a relay that is upgraded while the app runs is used soon after. A query or result count that the new search cannot take also uses the old search. Other errors are shown as before. Order: search results keep the relay's ranking, so "Wes" comes before "Abe Wes". Before, the Search page and the member and new-message pickers sorted results by name. The list with no query is still alphabetical. Each screen still applies its own filters. Not in scope: the community invite screen accepts only an npub and has no name search, so this PR does not change it. ### Related issue Relay endpoint: #8301. This PR does not depend on it; either can merge first. ### Testing - Relay session tests: the name query goes to the new search with a signed request; a 404 falls back and stays on the old search until the retry time; a 405 falls back; other errors do not fall back; an unsupported result count uses the old search. - Updated tests for add members and @mention search, and a test that search results keep the relay's order while the browse list stays alphabetical. - `just mobile-check` and the relay, channel, mention, invite and search tests (1,962) pass. - No screenshots: results look the same; only which people are found changes. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
46159fcf91 |
fix(ios): respect app minimum in generated CocoaPods targets (#8379)
Xcode 27 rejects generated CocoaPods targets with deployment versions
below iOS 15, even though the app already requires iOS 16. Raise lower
explicit pod deployment targets to the existing iOS 16 minimum during
installation, preserving targets that require a newer version.
### Validation
On main at
|
||
|
|
66d5354586 |
fix(mobile): split channel history batches at the relay channel cap (#8342)
## Problem The relay rejects any request that names more than 128 explicit channels in aggregate (`MAX_EXPLICIT_CHANNEL_VALUES`, `crates/buzz-relay/src/handlers/req.rs`). Mobile sent every active channel in one batch: - **Latest-message and unread catch-up batches** were rejected with `400 too many explicit channels` and fell back to websocket requests four at a time. For 144 active channels that took about 5 s each. - **The Huddle backing-channel query** (one filter with every membership) was closed as `restricted: too many explicit channels` and returned no results, so that visibility filter was effectively disabled. On a first pairing with a large community, these serialized fallbacks consumed most of the community switcher's 20 s landing budget. That's one cause of "This community is taking longer to load." ## Change - `_fetchChannelHistoryBatch` splits filters into requests whose combined `#h` values stay within `kMaxExplicitChannelValues`, four at a time. A deadline in any request still makes the whole batch unavailable. Requests that fail for other reasons are collected, and one websocket fallback runs over their filters after every request settles, so a batch never has more than four relay requests in flight. - `_fetchHuddleStarts` splits its parent channel IDs into REQs within the same cap, also four at a time. ## Evidence iPhone 18 Pro simulator (iOS 27, debug build) on a community with 411 memberships and 144 active channels, with temporary stage timing that is not part of this PR. Eight cold launches before and after: | Median since app start | Before | After | | --- | --- | --- | | Channel rows painted | 8.3 s | 4.0 s | | Unread catch-up complete | 14.4 s | 5.1 s | The Huddle query now returns successfully instead of being closed. ## Tests The fake relay session now rejects over-cap requests the way the relay does, and answers Huddle starts only for the requested parent channels. - `channel batches stay within the relay explicit-channel cap` (129 channels): every history batch and Huddle filter is within the cap, and a Huddle backing channel in the over-cap tail stays hidden. Fails when either split is removed. - `channel batches keep four relay requests in flight` (513 channels, query bridge down): peak in-flight requests are 4 for latest-message, unread catch-up and Huddle, and the fallback covers every channel. The previous version of this branch measured 16, 16 and 5. - Full mobile suite via the pre-push hook (`just mobile-check && just mobile-test`): passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b1d60f98e2 |
Bring synced Solo chats and projects to mobile (#8337)
## Summary Bring desktop Solo chats and project organization to mobile, with synced collapsible sections, shared unread highlights and tab badges, agent selection, shared search, native iOS creation sheets, and inline typing bubbles that transition into replies. Resolve Solo placement for every chat entry route, retain encrypted organization snapshots for offline reading, and let users resume or discard unfinished chat and project setup without deleting existing messages. ### Related issue Related: #6565; expandable agent transcripts remain separate work. ### Testing - Fresh Solo read state seeds each active chat from its captured initial last-message timestamp before exposing row/section/tab unread state. Baselines survive intervening channel updates and reset across identity/community boundaries while surviving read-state readiness changes. Regressions cover multiple chats, events before read-state readiness and during/after seeding, and identity changes; removing either the seed or the display gate fails them. - Chat completion reconciles a retained Solo-save journal even when placement is already confirmed. False/throwing cleanup regressions ensure the outer intent remains until reconciliation and only one placement is published. Typing terminal-close loops remain bounded; the retry budget resets after an uninterrupted eight-second health interval or transport reconnect. - Typing retry budgets reset after sustained recovery; invalid Solo updates retain cached rows with a retry warning that survives failed refreshes. Project names use the editors’ 120-grapheme limit, including emoji and combining characters. Regression coverage exercises repeated recovery, invalid live/refresh heads, and character-limit boundaries. - Rebased onto current main, preserving the newer Search controls, Huddle timeline filtering, and message-media layout. - Failed project journal writes reload storage before claiming recovery and reopen the entered name for retry. Typing subscription rejections are caught; terminal closures clear stale indicators and retry with a bounded backoff, canceled on disposal. Regression tests cover both false/throwing storage writes and initial/late subscription rejection. - Explicit ordinary destinations skip optional Solo lookup waits. Generic destinations perform one bounded lookup in the destination (search no longer duplicates it); an unavailable classification falls back to ordinary sending per the product owner's explicit choice. Positively identified Solo destinations use Solo providers. - Terminal Solo subscription closure preserves cached rows and exposes live-update Retry. Page coverage verifies warning visibility, usable rows during recovery, and a subsequent terminal close. Removing the warning or the immediate ordinary-route guard fails its regression. - Generic channel entries share an ordinary default when Solo classification is unavailable, per the requested product behavior. Known Solo placement takes precedence; callers can explicitly require unresolved placement to remain blocked. Bounded initial lookup preserves known Solo deep-link routing. - Dirty newest snapshots and offline-save warnings survive transport reconnects; Retry persists that same event even if the relay returns an older head. Community changes replace the service, and agent-directory reconnect refresh remains intact. - Pending-save discard reloads failed removals. Repeated reconnect testing confirms typing subscriptions are released on provider rebuild and disposal; removing the existing cleanup fails the regression. - Ordinary Home/create routes explicitly preserve private-channel sending during Solo failures; identified Solo sends use the Solo sender and explicit unresolved routes remain fail-closed. Solo unread replies beyond the initial 200-event page are loaded through the oldest-unread boundary. - Chat/project/agent cleanup reloads failed removals, retains same-process recovery, and protects replaced records. False/throw regressions verify retries do not duplicate creation or membership. Route, unread, and cleanup guard-removal mutations each fail their regression. - Ordinary channels, DMs, and forums remain sendable when optional Solo sync fails; explicit unresolved placement stays blocked. Controlled cache races retain the newest verified snapshot, keep usable chats visible, and retry offline persistence without stale writes. Both protection-removal regressions fail as expected. - Confirmed journal cleanup checks false/throwing removal and exact-record identity. Interrupted agent choices are visible immediately, identify the saved agent, and cover Resume/Later/Discard with accessible name/key-fallback controls. - Notification deep links preserve the contiguous history cursor; same-second messages infer the newest agent consistently. Failed offline-cache writes surface Retry, with false/throwing storage failures and offline restart covered. - Composite history cursors cover 350 same-second events plus older history; removing the ID cursor makes the regression fail. Agent changes now journal before granting access, with restart/failure and Resume/Later/Discard widget coverage. Signed malformed profiles remain isolated. - Native iOS avatar tests passed on the simulator: visible-row loading, three-request concurrency, streamed byte limits, response validation, downsampling/pixel limits, cancellation, and visible avatar updates. - Solo and Home share observed-event/read-marker logic. Widget coverage verifies unread rows, collapsed-section indicators, the tab badge before first Solo visit, message/thread reads, forced unread, and filtering archived/nonmember/unplaced chats. - Chat intent is saved before ownership and organization preflight; tests cover interrupted ownership lookup and a disconnect while the native creation form is open. - Mobile analysis, formatting, file-size checks, and full mobile tests passed at `1d4c41354`. - Project creation persists its encrypted name/UUID before connectivity checks or relay reads. Restart tests cover Resume/Later/Discard, scope isolation, stale-intent rejection, and lost acknowledgements. Removing the pre-network save makes all four production-flow regressions fail. - Regression coverage includes notification/Activity routing, inline replies, unknown-placement send protection, restart recovery/discard, and encrypted offline cache isolation/refresh. - Earlier feature build exercised on a physical iPhone with existing account/community data; review follow-ups have not been reinstalled on the device. UIKit agent-row accessibility checks passed on an iOS simulator. - Earlier cross-platform `just ci` stopped on the existing `buzz-acp` test `signals_cancel_startup_turn_and_pending_stdin_without_hanging` (process exit-code assertion); no Rust production changes are included in this PR. ### Remaining review limitation Agent discovery is bounded to 10 pages / 1,000 profiles with automatic retries disabled. Owner-scoped discovery remains open because the current relay ignores `#auth` filters; supporting that query requires a relay change. ### Snapshots Sample data, shared Flutter/Android surface. Native iOS uses platform sheets and controls. | Solo: projects and chats | New chat | Choose an agent | | --- | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/d9b29aa079a3b9a88e8ed89b70df8d6af06fa41b/pr-8337--01-solo.png" width="240" alt="Solo with collapsible projects and chats" /> | <img src="https://raw.githubusercontent.com/block/buzz/d9b29aa079a3b9a88e8ed89b70df8d6af06fa41b/pr-8337--02-new-chat.png" width="240" alt="New chat form" /> | <img src="https://raw.githubusercontent.com/block/buzz/d9b29aa079a3b9a88e8ed89b70df8d6af06fa41b/pr-8337--03-agent-picker.png" width="240" alt="Agent selection list" /> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
fea02d174d |
fix(ios): fetch profiles and avatars in notification extension (#8369)
A notification can arrive from someone whose profile the main app has never loaded. The iOS notification extension now fetches and caches sender profiles, channel details, and avatars itself. The main app supplies account configuration and Keychain credentials only. After resolving the message, the extension allows 1.5 seconds for sender and channel details. It uses available stale data when a refresh is slow or fails, delivers once, and lets late results update its private cache. Profiles, channel details, and avatar refresh checks share a one-hour freshness threshold. A verified avatar removal clears the previous image. Account removal prevents further use of that sign-in's cached data. Profile, channel metadata, and membership use separate bounded downloads, so an oversized roster cannot discard the smaller records. Sender enrichment can finish while channel requests are pending. A successful partial channel refresh atomically removes its old counterpart, preventing a mixed group identity or recipient count. Group-specific notification presentation still requires a verified recipient count. The [specification](https://github.com/block/buzz/blob/native-notification-enrichment/docs/native-notification-enrichment.md) describes the display, caching, and account-removal behavior. General persistent image caching in Flutter remains separate. ### Validation [Hosted CI](https://github.com/block/buzz/actions/runs/38780108487) passed on the final commit, including all Flutter shards, Android, the full iOS release and simulator builds, and native controls/push-navigation tests. The [current-commit security review](https://github.com/block/buzz/pull/8369#issuecomment-6197508385) found no issues. The native package passed 159 tests, including new coverage for cache freshness, avatar replacement and removal, sign-in isolation, concurrent storage, late results, single delivery, bounded downloads, and SVG validation. A regression test exposed identical HTTP authentication event IDs for requests in the same second; each request now includes a unique nonce, matching the Dart and CLI clients. Full local CI passed before that small authentication fix. The complete native suite and signed device build passed again on the latest commit, including response-size limits, overlapping avatar downloads, canonical relay-origin matching, independent profile/channel refreshes, account-key changes before and during requests, and a single initial snapshot read across 64 accounts, signed IPv6 avatar authorities, and partial channel refreshes. Focused Flutter regression tests passed 198 tests with three configuration-specific skips; seven avatar widget tests also passed. Flutter analysis is clean, and the notification extension builds for the simulator. On an iPhone SE, the notification displayed the sender name, avatar, channel, and message before the updated main app was opened. Repeat notifications used a byte-identical private cache. Three notifications delivered together displayed the avatar after the authentication fix. The next notification also displayed correctly after terminating the extension process. The existing generic-push design can select the same newest unread message for several pushes; this change does not alter message selection.  Authenticated HTTP avatar fetching and persistent HTTP cache reuse also passed on the SE. The first notification downloaded a relay thumbnail. After terminating the extension and expiring its separate avatar entry, the next notification displayed the avatar and refreshed that entry without another media request in the relay logs. The main app remained unopened throughout. The original app build and test profile were restored afterward. Automated tests cover slow and failed requests, stale images, removal, and account revocation; those cases have not all been exercised on the phone. ### Before merge - [x] Verify authenticated HTTP avatar fetching and cache reuse on iOS against a working relay media service. <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
d61fe2071d |
Use native UIKit scrolling for iOS photo carousels (#8361)
Builds on the mobile polish merged in #8359; this PR now targets `main`. On iOS, inline photo galleries now use a UIKit collection view for dragging, deceleration, and snapping. Photo taps retain the existing full-screen viewer and reply/more actions; gallery scrolling continues to fade the message avatar. The final photo stops at the full message gutter rather than the smaller gap between photos. Android keeps the current Flutter carousel. Visible cells subscribe to continuing frames from Flutter's existing authenticated image providers, preserving animated GIF/APNG/WebP media. Request IDs fence late frames and cancel subscriptions on reuse/disappearance/disposal. A four-slot bound includes retired unresolved loads until their transport terminates. Shared media fetches now use abortable HTTP requests with a 30-second deadline through response-body completion, so stalled requests release their slots without leaving live transports behind; native eager precaching is disabled so it cannot bypass that bound. Frame conversion coalesces to the newest pending frame. Native cells expose photo labels and positions to VoiceOver, support RTL, and preserve the selected photo during resizing. Validation: - Mobile formatting and analysis passed. - Native simulator regression checks passed for geometry, RTL, snapping, taps, frame replacement, cancellation, and resizing. The native CI lane now runs the carousel executable. - Full mobile suite and pre-push gates passed, including two-frame GIF delivery, delayed-provider bounds/cancellation, real HTTP stalled-header/body recovery with four requests maximum, disposal, and unconfigured-push tests. - Signed iPhone Release build of the frame/subscription implementation passed. Physical iPhone testing covered the earlier carousel/gutter revision; the new animation/cancellation changes have local Flutter and native simulator coverage, not a new physical-device journey. Preview-only signing overrides preserve its bundle/Keychain group; production entitlements are unchanged. Local provisioning excludes production push/App Attest capabilities. - Full `just ci` was started but stopped after unrelated Rust/desktop compilation consumed about 5 GB; broad CI is incomplete. Device build required local Xcode 27 workarounds: iOS 16 deployment target and a generated swift-secp256k1 cache compatibility edit. Neither changes repository source. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
9296660d1e |
Match mobile reply links to desktop connectors (#8362)
## Summary Reply summaries now appear below mobile message bubbles as inset avatar/count links with the same rounded connector used on desktop. Reaction trays keep their overlap with the bubble; connector clearance adjusts when reactions are present. Channel and nested reply summaries share the same 20px avatar stack, capped at three, with a 44px minimum tap target and accessible thread-opening label. ### Related issue Follows merged #8359 and now targets `main`. This diff contains only the reply-link changes. No matching duplicate was found in the open issue/PR search for reply connectors. ### Testing - Full mobile formatting, analysis, and tests passed before stacking: 3,348 tests plus 3 push-configuration tests, 4 skips. - Normal pre-push checks passed on the rebased head against main: formatting, analysis, full mobile tests, and repository file-size checks. - Before stacking, installed and launched the preview on Pixel 10 and iPhone Air with existing app data. Ten profile/community/draft preference values remained unchanged on each. Verified both connector spacing variants and thread opening on Pixel; the owner tried and approved the preview. iPhone screenshot verification was unavailable. - The rebased build has not been reinstalled on devices. Device evidence above is from the approved preview before rebasing. Before: channel replies were filled pills beside reactions; nested replies used a separate larger avatar row. After, as verified in the device preview:  Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> |
||
|
|
42b395cdef |
Polish mobile bubbles, mentions, galleries, and huddle cards (#8359)
Builds on the merged themes work in #8358 and inherits its appearance and chat-color settings. Polishes mobile conversations with uniform rounded bubbles, desktop-style text-only mentions and agent icon, aligned gallery counts, fading gallery avatars, and standard iOS scrolling. Huddles use rounded cards with attached reaction trays, without duplicate start/end notices; other activity rows stay centered with smaller text. Validation: mobile checks, the full mobile test suite, and the gallery regression test passed. Release build installed on iPhone. Full `just ci` could not finish: desktop compilation exhausted local disk space. ### Snapshots Flutter-rendered light and dark previews using seeded sample messages. | Light | Dark | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
a5dfdd6d55 |
Align mobile appearance and channels with desktop (#8358)
Aligns mobile with desktop's neutral palette and six chat colors. Appearance now offers Light, Dark, and System instead of themes, while preserving community preferences and the single Home and Chat preview pair. Chat colors survive local preference reloads without historical-index remapping, and the Chat preview shows the draft outgoing bubble color. Channel and section icons are removed, section chevrons sit beside names, and section dividers use 7% opacity. DM avatars remain. Validation: `just mobile-check`, 35 focused tests, and the full mobile suite passed (3,351 tests; 4 skipped). Updated light/dark previews were rendered and visually checked. The earlier release was verified on iPhone; this follow-up has not been reinstalled. The earlier repository-wide lint passed, but full `just ci` was stopped for low disk space. Hosted CI is running for the new head. **Intentional design parity:** Section labels and chevrons retain desktop’s `--text-unavailable` colors: light `#959595`, dark `#595959`. Verified against `block/buzz-app` main at `0201b54c496120a44e020834663dc534cae9a79f`. These colors do not meet 4.5:1 text contrast on the mobile section surfaces; the product decision for this PR is to preserve desktop parity and leave them unchanged. Kenneth explicitly accepts this known contrast tradeoff for this PR/release and requests that reviewers treat it as a scoped product exception rather than a blocker. This does not claim WCAG compliance or change the repository-wide accessibility target. Any future contrast adjustment should be coordinated across desktop and mobile. ### Snapshots Channel list: physical iPhone. Appearance and chat colors: Flutter-rendered previews. | Channels | Light | Dark | Chat colors | | --- | --- | --- | --- | |  |  |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
b77ba86262 |
perf(mobile): cut test wall time and run mobile CI lanes in parallel (#8347)
## Problem Mobile pushes and PRs are slow. - **Pre-push takes ~133s on a mobile-only change.** About 115s of that is `flutter test`, and the finish time is set by a few files that wait on real clocks. - `push_presentation_export_test` (75s) and `push_presentation_recovery_test` (19s) sleep through the full 7.75s retry backoff. - `channel_messages_provider_test` (70s) sleeps through recount and ownership retry backoff on real time. - **The required Mobile CI job takes ~15–16 min.** It runs tests (390–430s on a 4-vCPU runner, 2 test files at a time) and then the Android debug APK build (~400s, no Gradle cache) back to back. - **Mobile Swift takes 18–21 min.** The unsigned release build (~4.5 min) waits behind the simulator tests. Pre-commit is ~3s and isn't part of the problem. ## Change **Tests** (each one still asserts what it did before) - **Push exports.** Production retries wait through `debugPushPresentationRetryWait`, which defaults to `Future.delayed`. The export and recovery tests skip the real waits and assert the exact 250ms → 4s backoff schedule instead. Export fixtures sign each event once per file. - **`channel_messages_provider_test`.** Every test runs on `fakeAsync`. A file-local `test` wrapper keeps the 90 call sites unchanged. Timers fire in their real order, and a test that waits on work outside fake time fails instead of hanging. Raising the production retry bound (`attempt < 2` → `3`) still fails the 5 exhausted-retry tests. - **`signed qualified caller`.** This test mounts the page 48 times and opens the profile sheet twice per mount. It now disables the sheet animation; it asserts which pubkey and labels show, not motion. **CI** - **Mobile runs as five parallel matrix lanes:** checks (format, analyze, unconfigured push test, recipe test), three `flutter test --total-shards=3` shards, and the Android build. The required `Mobile` gate still reads the combined result. - **Android restores a Gradle cache,** which is saved only on pushes to `main`. - **Mobile Swift builds the unsigned release** in a parallel lane beside the native build, tests and simulator checks. ## Validation Measured locally on an 18-core Mac with warm caches: | | Before | After | |---|---|---| | `flutter test` (full suite) | 115s | 74s | | Summed per-test time | 411s | 307s | | `push_presentation_export_test` | 75s | 38s | | `channel_messages_provider_test` | 70s | 22s | | Pre-push, mobile-only push | 133s | 95s | - The CI lane timings come from this PR's own run. - `actionlint` passes, and so do `scripts/test-ci-required-context-isolation.sh` and `scripts/ci-required-gates.test.mjs`. - The pre-push hook (format, analyze, full `flutter test`) passed on push. ## Not in this PR - `channel_detail_page_test` (~55s) is now the local long pole. Splitting it into shard files would let it run in parallel, but it is one 16k-line library with `part` files, so that is a separate refactor. - Caching Hermit and pub on macOS would cut ~100s of setup from each Mobile Swift lane. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8222e99915 |
fix(mobile): keep thread references consistent across push and Inbox (#8332)
Follow-up to [#8294](https://github.com/block/buzz/pull/8294). Malformed trailing thread tags could make a notification open the right thread while Flutter indexed its reply elsewhere; uppercase references could similarly miss lowercase event IDs. Share one validated thread-reference parser between the mobile timeline, Inbox, and feed helper. It ignores malformed markers, keeps the last valid root/reply, and normalizes lookup IDs to lowercase. Native push parsing now uses the same casing. Existing thread fixtures use valid event IDs so they continue exercising real thread relationships. Add RunnerTests coverage for the production native-to-Flutter argument conversion, including buffered cold-start navigation and omitted optional thread IDs, and run it in the native CI lane. ### Validation - New parser and Inbox regressions cover malformed markers and uppercase references. Widget regressions open and highlight the exact reply for valid, uppercase, and trailing-malformed tags. - After integrating [#8106](https://github.com/block/buzz/pull/8106), Flutter analysis and the complete mobile suite pass: 3,337 tests, four existing skips. Integration preserves per-event read predicates and migrates incoming thread fixtures to valid IDs. An unchanged keyboard-history test also failed with main's original parser; recording reads before the scroll transition fixes its timing assumption while retaining visibility assertions. - Native package tests passed (127 tests), and both Runner bridge tests passed on an iOS simulator with Xcode 26.5. Removing the production `threadRootId` assignment made the bridge regression fail; restoring it passed. Native code is unchanged by the main integration. - [Current-head CI passed](https://github.com/block/buzz/actions/runs/38659307247), including Mobile and Mobile Swift. [Codex review](https://github.com/block/buzz/pull/8332#issuecomment-6181458528) and [security review](https://github.com/block/buzz/pull/8332#issuecomment-6181449235) found no issues on the integrated commit. [Owner approval](https://github.com/block/buzz/pull/8332#pullrequestreview-5526812957) covers the exact integrated commit. - Standards and spec reviews found no blockers, including the merge resolutions and test adjustments. Validation is scoped to mobile/native per VISION_MOBILE; unrelated repository-wide checks were stopped, not counted as passing. The bridge test covers serialization and buffering, not end-to-end APNs delivery or app launch. No new physical-device acceptance is claimed; routine post-merge dogfood remains human-owned. --------- Signed-off-by: Tom Brow <tomb@block.xyz> |
||
|
|
9ade5d1ad1 |
feat(mobile): write catch-up and visible-message read marks like the web app (#8106)
🤖 ## Summary When you read a channel or thread on the phone, the web and desktop app (buzz-app) should show it as read too, and the reverse. buzz-app no longer saves a "read" mark for each ordinary message. It saves one "caught up to here" mark per channel and per thread, and keeps per-message marks only for mentions, DMs, broadcasts and replies. Mobile already reads those marks. This PR makes mobile write them, so reading on the phone clears unread on other devices and saved read state stays small. What changes on the phone: - Reaching the bottom of a channel, and staying there for 300 ms, marks it caught up. Reaching the end of a thread does the same for that thread. - A message you see fully on screen, away from the bottom, is marked read on its own. - Mentions, DMs, broadcasts and thread replies you see keep their own read marks, as in buzz-app. - Automatic reading never marks the whole channel read. Only the explicit **Mark read** action does that. DMs and forums are the exception: opening one marks it all read, as mobile does today. - Reaching the bottom of a channel you marked unread ends that manual unread. Unseen mentions and replies stay unread. - Activity rows clear when you have read every message in the row, not just the newest. ### Related issue None found. buzz-app makes the same DM and manual-unread changes in https://github.com/block/buzz-app/pull/616. ### Testing - Unit tests cover the new write rules, Activity rows and the size limit on saved read state. - Widget tests cover reading on the channel page and the thread page, opening a DM, and reaching the bottom of a channel you marked unread. - No visual changes. ## Details **Read marks.** "Caught up" is saved as `activity:<channel>` for a channel and `thread-activity:<root>` for a thread. A caught-up mark reads only ordinary top-level messages (channel) or that thread's replies (thread). A per-message mark is `msg:<id>`. **Caught-up time.** The mark is set to the time of the bottom row, and never later than the current time. The time of a newer reply is not used. So a top-level message that arrives late, with an earlier timestamp, stays unread. A clock that runs fast, on this phone or the sender's, cannot mark messages read before they arrive. **Nested threads.** A thread opened on a nested reply shows only one branch. Reaching its end writes per-message marks only, so unread messages in other branches stay unread. **Reading delay.** The 300 ms reading delay now restarts only when messages arrive or leave. Before, any page rebuild restarted it, for example a typing indicator. **Activity rows.** A row that groups several events is done only when each event is read. Each event is checked against the channel mark, its own `msg:` mark, and its thread marks if it is a reply. Opening a row goes to the oldest unread event. **Publishing.** Mobile no longer republishes per-message marks it got from other devices. buzz-app removes those once a caught-up mark covers them. Broad marks (channel, thread, caught-up) are still republished, so they stay available after the fetch window. The published blob is capped at buzz-app's 40 KiB limit. Channel marks are kept first, then thread marks, then caught-up marks, then the newest per-message marks. Before, a blob over the encryption limit (about 64 KiB) turned off sync for the rest of the session. **DMs and manual unread.** Opening a DM reads it through the newest loaded message, replies included, as mobile does today. Reaching the bottom of a channel ends a manual unread. Manual unread on mobile is local to this session, so ending it does not change what other devices see. **Compatibility.** Older desktop builds read only channel and per-message marks. They may show extra unread for channels read on the phone. **Follow-ups, not in this PR** - Unread catch-up fetches start after the channel mark and are capped at 1,000 events. In a busy channel, an old unread mention can drop out of the badge. The fetch should also start from the caught-up mark, or load more pages. - Each mark triggers its own save and rebuild. They could be batched. - Broadcast replies are counted slightly differently from buzz-app. - buzz-app can also move its caught-up mark past the current time. --------- Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
a83ea69b72 |
fix(mobile): replay native navigation state when returning Home (#8314)
Restore the iOS navigation bar after any covering route pops, including opaque custom Settings and community routes. Use the shared app RouteObserver and didPopNext, then replay scroll depth and request native layout after Flutter paints the uncovered view. Voice recording retains the same observer through its existing alias. Regression coverage uses the real HomePage with both MaterialPageRoute and opaque PageRouteBuilder, at scrolled and top-of-list positions. The custom-route case fails before this change. Validation: 107 focused tests and analysis passed. Full mobile runs stalled under local disk pressure, so full-suite validation is incomplete. On-device verification of this follow-up remains pending. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
231a469de5 |
Refresh mobile message bubbles, reactions, and activity cards (#8281)
## Summary Refresh mobile conversations with left-aligned grouped bubbles without tails, tighter consecutive-message spacing, distinct sender colors, theme-aware incoming backgrounds, and compact overlapping reaction trays. Attach reply counts beside reactions with up to three participant avatars; keep outgoing mentions and links readable in both themes. Coordinate a 500 ms composer-to-message transition with spring contraction and an anchored avatar. Message ownership follows the signing identity so profile reloads cannot recolor outgoing bubbles. Center channel activities without timestamps or icons, keep names tappable, and show two affected members plus a **See all** sheet. Hide legacy purpose activities, match media corners to bubble groups, and add iOS status-bar scroll-to-top. This is an independent mobile PR; no desktop changes or color picker. ## Validation - Full `just mobile-check` passed; `just mobile-test` passed 3,275 tests with 4 skips, plus 3 unconfigured-gateway checks. - Fixed message-widget lint warnings and a stale notification fixture that blocked analysis. Made the video-controller disposal test wait for lifecycle events instead of fixed delays. Mobile pre-push checks are enabled. - Physical iPhone previews were installed throughout development. The latest bubble, reply-avatar, contrast, and spacing updates were installed and launched on the physical iPhone. Final motion and status-bar behavior still need device visual verification; Android has automated coverage but no physical-device verification. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
7b9551aba8 |
fix(ios): open push replies in their containing thread (#8294)
Tapping a push notification for a thread reply currently opens the channel at its parent. Preserve the verified event's direct-parent thread ID through the iOS notification payload and Flutter bridge so the tap opens the containing thread and highlights that exact reply, even when newer replies have arrived. This matches Inbox's destination selection for nested replies; broadcast replies still open in the channel. Closest related work: https://github.com/block/buzz/pull/6159 (original push-tap routing). ### Validation - Follow-up [ |
||
|
|
b5223d2525 |
Use grouped message action sheets and fix date layering (#8280)
## Summary Replace message long-press popovers with the shared grouped bottom sheet: an emoji row, Reply / Copy link / Remind me tiles, and remaining actions in regular sections. Match emoji backgrounds to the sections and use the theme primary color for the selected reaction. Keep the scrolling date above the navigation blur in channels and threads. Restore composer focus when the sheet is canceled, keep section gaps consistent, and soften shared dividers to 7% opacity. The reaction details sheet shows all reactors by default. Emoji count chips toggle your own reaction, show your selected state, and update live without dismissing the sheet. Unselected reactions have visible outlines; adding a supported emoji plays the chat-style burst and respects reduced motion. ### Related issue No matching issue found. Related sheet styling: #4911. ### Testing - Full mobile checks and tests passed, including 23 reaction tests and composer-focus regression coverage. - Analysis and file-size checks passed; signed iOS build installed and launched on a physical iPhone. - Full `just ci` stopped during Rust compilation due to low disk space. - On-device visual capture unavailable. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |