Commit Graph

236 Commits

Author SHA1 Message Date
Tom Brow
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>
2026-10-19 10:09:25 -07:00
Tom Brow
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>
2026-10-19 07:12:44 -07:00
Tom Brow
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>
2026-10-18 16:10:01 -07:00
klopez4212
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>
2026-10-18 18:19:21 -04:00
Tom Brow
819dd7924f fix(mobile): bump Flutter 3.47, resolving memory leak (#8419)
With accessibility semantics active, opening and leaving channels
retained disposed composer overlay states through Flutter’s semantics
traversal map. Those states kept departed route and message trees alive.
This upgrades the pinned Flutter SDK from 3.41.7 to 3.47.7, which
includes [the upstream
fix](3c96406cc4).

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

Related issue: no matching open issue or PR found.

### Validation

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

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

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

### Create-channel compatibility

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

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

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-18 12:44:43 -07:00
Tom Brow
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 |
| --- | --- |
|
![Before](https://raw.githubusercontent.com/block/buzz/5840caf6cc3e35ce0ecdba68cfbcb10506f3eb23/pr-8418--search-long-before.png)
|
![After](https://raw.githubusercontent.com/block/buzz/5840caf6cc3e35ce0ecdba68cfbcb10506f3eb23/pr-8418--search-long-preview.png)
|

---------

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

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

---------

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

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

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

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

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

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-16 12:28:02 -04:00
Logan Johnson
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>
2026-10-15 17:33:15 -07:00
Wes
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>
2026-10-15 18:18:56 -06:00
klopez4212
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>
2026-10-15 17:07:23 -04:00
Tom Brow
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.

![Notification before first app launch after
upgrade](https://raw.githubusercontent.com/block/buzz/37d450855e026178836e2a9e05b0081ebbc3b479/pr-8369--before-first-app-launch.png)

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>
2026-10-15 11:16:04 -07:00
klopez4212
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>
2026-10-15 13:22:12 -04:00
klopez4212
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:

![Pixel preview showing reply connectors with and without
reactions](https://buzz.block.builderlab.xyz/media/119f0fbd358895ef3fe5e6c1bc4f2c399c660b1debded5efff08864ff796792c.png)

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-15 13:08:41 -04:00
klopez4212
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 |
| --- | --- |
|
![light](https://raw.githubusercontent.com/block/buzz/62346699199dd707579f46d21608c2a49a8095ef/pr-8359--light.png)
|
![dark](https://raw.githubusercontent.com/block/buzz/62346699199dd707579f46d21608c2a49a8095ef/pr-8359--dark.png)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-15 12:39:21 -04:00
klopez4212
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 |
| --- | --- | --- | --- |
|
![channel-list-iphone](https://raw.githubusercontent.com/block/buzz/1effb78b36cbcd77bd6339f11742133ec9e8214a/pr-8358--channel-list-iphone.png)
|
![appearance-light](https://raw.githubusercontent.com/block/buzz/1effb78b36cbcd77bd6339f11742133ec9e8214a/pr-8358--appearance-light.png)
|
![appearance-dark](https://raw.githubusercontent.com/block/buzz/1effb78b36cbcd77bd6339f11742133ec9e8214a/pr-8358--appearance-dark.png)
|
![chat-colors](https://raw.githubusercontent.com/block/buzz/1effb78b36cbcd77bd6339f11742133ec9e8214a/pr-8358--chat-colors.png)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-15 08:45:16 -04:00
Wes
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>
2026-10-14 20:57:47 -06:00
Tom Brow
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>
2026-10-14 18:42:39 -07:00
Logan Johnson
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>
2026-10-14 17:04:57 -07:00
klopez4212
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>
2026-10-14 16:37:59 -04:00
klopez4212
231a469de5 Refresh mobile message bubbles, reactions, and activity cards (#8281)
## Summary

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

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

## Validation

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

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-14 16:35:26 -04:00
Tom Brow
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
[5b70e1a](5b70e1a5cb)
selects the last valid 64-character ASCII-hex reply marker, matching
relay ingest. Regression tests failed before the fix; all 80 XCTest and
47 Swift Testing tests passed afterward with Xcode 26.5. [Current-head
CI](https://github.com/block/buzz/actions/runs/38607128406) passed,
including Flutter tests, Android build, native package tests, complete
iOS simulator and unsigned release builds, and native accessibility
tests. The [current-head security
review](https://github.com/block/buzz/pull/8294#issuecomment-6174205374)
reports no findings. Physical evidence below predates this
validation-only follow-up.

- [Baseline CI on
f465f91](https://github.com/block/buzz/actions/runs/38436936153) passed,
including Flutter and native package tests, Android build, complete iOS
simulator and unsigned release builds, and native accessibility tests.
- Full local validation passed on
[f465f91](f465f9196f),
including 3,215 Flutter tests (four skipped) and three unconfigured-push
tests. The 49 focused navigation tests also passed after the baseline
merge.
- Added coverage for cold/warm bridge delivery, exact reply highlighting
with newer arrivals, direct-parent and broadcast destinations, and
malformed optional thread IDs. The Codex finding for an empty reply
marker is fixed and resolved.
- Physical acceptance passed on the managed iPhone 12 mini using signed
Buzz Dev 10140, built from f465f91 with SDK 26.5. Real push taps and
Inbox opened the same containing thread and highlighted the exact reply.
An older alert retained its target after a newer reply arrived; a
retained alert also worked after terminating the app. Top-level push
navigation reached the channel.
- Installed in place with the existing identity, app data and pairing
retained. No key reset or rotation was performed. Notifications were
restored to off and device control released. Xcode 27 hits an unchanged
dependency compiler error; the matching Xcode 26.5 toolchain built and
signed this tested candidate successfully.

### Physical acceptance

| Warm push: older reply A | Inbox: reply B | Cold push: reply B |
| --- | --- | --- |
| ![Older reply A highlighted with newer B
visible](https://raw.githubusercontent.com/block/buzz/87eb00d209cf0b0dd3f4146e3219b15884133b9d/pr-8294--warm-older-reply.png)
| ![Inbox highlights reply
B](https://raw.githubusercontent.com/block/buzz/87eb00d209cf0b0dd3f4146e3219b15884133b9d/pr-8294--inbox-exact-reply.png)
| ![Cold push highlights reply
B](https://raw.githubusercontent.com/block/buzz/87eb00d209cf0b0dd3f4146e3219b15884133b9d/pr-8294--cold-push-exact-reply.png)
|

[Physical test
details](https://github.com/block/buzz/pull/8294#issuecomment-6151991840).

### Before and after

Synthetic Flutter widget captures with a newer reply already present.

| Before: channel at outer parent | After: exact reply in containing
thread |
| --- | --- |
|
![Before](https://raw.githubusercontent.com/block/buzz/d9a0ff6c7a5ea481b04cb9c61d301553b4c23c10/pr-8294--before.png)
|
![After](https://raw.githubusercontent.com/block/buzz/d9a0ff6c7a5ea481b04cb9c61d301553b4c23c10/pr-8294--after.png)
|

After merge, repeat warm/cold push versus Inbox in the integrated mobile
candidate, including older alerts, top-level and broadcast
notifications.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-14 12:35:58 -07:00
klopez4212
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>
2026-10-13 12:55:07 -04:00
Wes
03ce924e32 fix(ci): restore green main after merge skew and new advisory (#8277)
Main CI has been red since c09730e87 (#8223). The Mobile and Security
checks fail on main, and Docker image fails because it requires a green
CI run for the same commit. Locally, the pre-push Rust lane also failed
on load-sensitive `buzz-acp` timing tests. This PR fixes all three so
main passes locally and on GitHub.

- **Mobile:** #8223 was tested against a base from before #8206, which
removed `buzzDevPushAppProfile` and app-profile selection. Its new
`notification_toggle_test.dart` still put `appProfile` in the native
grant map, so the merged tree doesn't compile. This removes the stale
field.
- **Security:** cargo-deny now reports
[RUSTSEC-2024-0449](https://rustsec.org/advisories/RUSTSEC-2024-0449)
(`unsafe-libyaml` is unmaintained), which comes in through `serde_yaml`.
It's an unmaintained notice, not a vulnerability. The `serde_yaml_ng`
fork depends on the same crate, so removing it means migrating YAML
parsing in five crates. Like the existing unmaintained entries, it goes
in `deny.toml` with a reason.
- **buzz-acp test flakes:** the `spawn_script` test helper returned
before bash was running, and the timing tests started their idle clock
right away. Under full-suite load, process startup took longer than the
idle windows. `keepalive_resets_idle_past_deadline`,
`idle_resets_on_stdout_activity` and
`acp_steer_request_omits_expected_run_id_and_carries_session_and_prompt`
failed this way across three pre-push runs. The helper now has the
script print a ready line and consumes it before returning. The
keepalive loops also use brace expansion instead of `$(seq)`, which
avoids an extra process launch before the first line. The keepalive test
now uses a 250ms idle window with a 750ms floor. Both tests still fail
if keepalives don't reset the idle clock.

### Validation
- `cargo-deny check advisories` passes with this `deny.toml` and fails
on main's.
- The `notification_toggle_test.dart` tests and `flutter analyze` pass.
- `cargo test -p buzz-acp --lib` passes all 1055 tests.
- The `acp::tests` suite passed 20 of 20 runs with 4 concurrent copies
over 5 rounds. Under the same load, the version without the
brace-expansion change failed every run.
- All pre-push hooks pass on 5dbaa4d32: rust-tests, mobile-checks,
desktop-tauri-checks, file-size-check, branch-skew.

🤖 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>
2026-10-13 16:46:31 +00:00
Tom Brow
c09730e874 fix(mobile): keep notification toggle usable and match row height (#8223)
Turning Notifications off invalidated the relay capability provider
because it watched the entire saved community. If the redundant NIP-11
request stalled or failed, the switch became disabled and reopening
Settings did not recover it. Discovery now follows community identity
and connection status, so preference and lease bookkeeping retain the
validated capability while a real reconnect still refreshes it. Each
opt-in also starts a fresh active lease immediately instead of waiting
for the old renewal timer. Failed attempts retain their gate until the
retry delay expires, preventing rebuilds from bypassing backoff.

Notifications now has the same single-line height as Set status, with
the full switch hit area and one combined accessibility label. This
follows [#8201](https://github.com/block/buzz/pull/8201); no duplicate
open PR was found.

### Validation

Added iOS widget regressions mount the production settings page and push
bootstrap with production storage logic, signing, and lease publication.
They verify a higher-generation active replacement after off/on with
accepted, failed, and pending cleanup, plus closing/reopening Settings,
switch and row taps, 2× text, accessibility, and rediscovery on
reconnect. A gate test verifies that rebuilds cannot bypass failure
backoff. Both UI failures and the missing active replacement were
reproduced before their fixes.

Mobile dependency installation, formatting/static checks, 3,234
configured tests, and three unconfigured-build tests passed. Captures
use the real Flutter settings widget at 375pt width, with synthetic data
and mocked native/network boundaries. Native navigation is outside the
capture. No device installation or deployment was performed.

After merge, validate the candidate on iOS: compare Notifications with
Set status, toggle off then immediately on, close/reopen Settings while
off and turn on, and repeat after reconnect. Confirm delivery resumes
after opt-in.

### Layout

Flutter widget captures with synthetic data; native navigation is
excluded.

| Before | After |
| --- | --- |
| ![Notifications row
before](https://raw.githubusercontent.com/block/buzz/b2cf4b8b1f0abaa95893af0e5a4e194c6af41333/pr-8223--01-before.png)
| ![Notifications row
after](https://raw.githubusercontent.com/block/buzz/b2cf4b8b1f0abaa95893af0e5a4e194c6af41333/pr-8223--02-after.png)
|

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-12 09:39:53 -07:00
Tom Brow
868e2f8f32 refactor(push): one app profile per gateway (#8206)
Buzz push currently selects an application profile, such as `dogfood`,
during client registration. We want each gateway to serve one Apple app
configured by the operator instead. This removes profile selection from
the client, relay, and gateway, while allowing internal and public
gateways to use their own Apple identities.

This PR implements that model in the Buzz source and upstream deployment
chart. Discovery, enrollment, grants, and leases no longer carry an
application-profile selector. Each gateway has one configured Apple
application and APNs transport.

The protocol changes are incompatible with the old registration format,
so relays advertise `buzz-push-v1` instead of `nip-pl`. Old clients do
not discover the new push capability. Updated clients use separate
Keychain records and register again. The [push
notes](https://github.com/block/buzz/blob/push-capability-v1/docs/buzz-push.md)
describe the experimental contract.

Landing this PR is the first step. Publish the matching gateway image
and upstream chart, then land the [shared chart
update](https://github.com/squareup/cash-helm-charts/pull/5740) and
[gateway deployment
update](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/1085).
Before cutover, stop old gateway instances and check legacy registration
state. Migrations refuse to proceed if old gateway installations or
active relay leases remain; they do not delete that state.
Physical-device verification remains necessary before production
activation.

### Validation
Gateway and focused Flutter push tests pass, as do the Swift package,
simulator build, and native notification/enrichment tests. A local relay
advertises `buzz-push-v1` without `nip-pl` or `app_profiles`. The
PostgreSQL suite passed 868 tests; its two migration-test failures were
corrected and pass a focused rerun together with the gateway cutover
guard. Chart rendering and release-contract checks pass.

Desktop JavaScript tests, desktop and web builds, native desktop
compilation, mobile analysis/tests, and desktop native tests pass. The
full Rust unit lane and prescribed `just test` command pass, including
workspace integration tests against task-isolated local services. The
production overlay now requires an explicitly supplied APNs topic, with
omission and rendered-value regression coverage. The corrected Swift
checksum assertion passes the complete package suite: 78 XCTest tests
and 42 Swift Testing tests. No physical-device enrollment or production
cutover is claimed.

Local merge validation: the embedded migration test and formatting pass
after reserving profile migration 0060 behind rollback migration 0059.
The local pre-push Rust lane failed ACP timing/steering tests
(`idle_resets_on_stdout_activity`,
`keepalive_resets_idle_past_deadline`, and the failed/missing-outcome
steering tests). ACP and its dependency manifests are unchanged by this
branch; all four pass in an isolated single-thread rerun. The documented
local-check exception skips only the Rust pre-push lane for publication;
required CI remains mandatory. Mobile and desktop pre-push lanes passed.

Combined migration validation: after rollback 0059 landed, the main
merge preserves profile migration 0060 and both embedded migration
assertions. The focused migration test and formatting pass. The next
local push again failed only the unchanged ACP idle/keepalive timing
tests (1053 passed, 2 failed, 3 ignored); other lanes passed.
Publication uses the same documented local Rust-lane exception; required
CI remains mandatory.

Follow-up review fix: removed the legacy profile column from the
copied-lease fixture. All four event-follow-up contract tests pass on
isolated PostgreSQL databases, covering desired and migrated schemas in
dual and app-only modes. Formatting passes. Publication retains the
prior passing local-lane evidence and uses the authorized pre-push
exception for the known unrelated ACP timing failures; required CI
remains mandatory.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-09 15:09:53 -07:00
Tom Brow
394d4a434b fix(mobile): normalize relay URL before disabling push (#8201)
Communities joined with a `ws://` or `wss://` relay URL cannot disable
push: the opt-out path passes that URL directly to the HTTP-only NIP-11
fetcher, fails before publishing the inactive lease, and leaves the
durable retry pending. This was reproduced in USB logs from TestFlight
0.20.1 (49).

Normalize the URL with the existing push relay HTTP helper before
fetching metadata, matching the revocation outbox's handling. No
settings UI changes. Related: #8199 removes the status text but does not
fix this synchronization failure; no duplicate fix found.

### Validation

Four regression cases exercise the real community deactivator through
its HTTP boundary. The WebSocket cases fail with the device's exact
exception before the fix; all four pass afterward. All 37 focused
community/revocation tests and targeted Flutter analysis pass. An
initial full mobile run had three failures (recount timing and native
export timeout/order); all three passed in isolated reruns. The full
mobile checks and test suite passed on retry. The local repository-wide
gate stopped during Rust compilation without a final result; a full
local pass is not claimed. [GitHub
CI](https://github.com/block/buzz/actions/runs/37865048873) passed,
including mobile tests, Android build, Swift tests, iOS simulator build,
and unsigned iOS release build. [Codex
review](https://github.com/block/buzz/pull/8201#issuecomment-6071795750)
reported no major issues.

The installed TestFlight build is unchanged. After this fix lands,
validate that upgrading the same installation clears its existing
pending opt-out and leaves the relay lease inactive, without toggling
notifications back on.

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-09 10:44:01 -07:00
Tom Brow
b6d97a69f0 fix(mobile): keep thread content visible while positioning replies (#8134)
Opening a mobile thread could blank the original message while the app
positioned replies. This affected empty and populated threads, including
links to a specific reply.

Keep one original-message widget mounted and usable throughout
positioning. It follows a measured placeholder in the reply list, which
becomes visible only at its final position. This preserves media and
reaction ownership, supports scrolling through tall originals, and
excludes the head from input and accessibility when it leaves the
viewport.

### Validation

- Added production-path frame regressions for empty, short, long, and
deep-linked threads; two video lifecycle cases verify exactly one
initialization and disposal, including completion after navigation away.
- Added coverage for dragging, mouse-wheel and accessibility scrolling,
removal of offscreen and app-bar-covered head semantics, and scrolling
to the interactive bottom of a 70-line original message. Removing the
accessibility exclusion or restoring the raw viewport-top boundary fails
both long-thread regressions; restoring the duplicated head fails the
lifecycle test.
- Mobile dependency setup, analysis, the full mobile test suite, and
unconfigured-push tests passed. The native iOS simulator debug build
passed after integrating main, including the conflict resolution for the
new Buzz icon API. The current tree passed 3,076 mobile tests (four
skipped) and three unconfigured-push tests, and the updated native
simulator build passed.
- Exercised empty, three-reply, forty-reply, and older-reply deep-link
flows on an iPhone 17 Pro simulator against an isolated local relay with
a disposable identity. Confirmed correct head accessibility coordinates
and absence when offscreen. The local posterless-video fixture rendered
its attachment fallback; native decoding success is not claimed.
Resource ownership is covered through the production preview-loader
seam. [Updated simulator
captures](https://github.com/block/buzz/pull/8134#issuecomment-6047098230).
- Added delayed-hydration regressions for mouse-wheel and explicit
accessibility scrolling through the persistent head. Both preserve a
visible reading anchor across hydration and viewport resizing; removing
the user-scroll lifecycle callbacks makes both fail. Repeated empty,
populated, and older-reply deep-link opening with the updated native
build against the retained isolated relay.
- Entered and sent a reply from the disposable simulator identity
against the isolated local relay. The app displayed the reply and
cleared the composer, and the relay database confirmed persistence.
Software keyboard open/close remains unverified because the simulator
did not display it.
- Agent review feedback was addressed. Personal mobile testing is not
applicable under the author's standing exception.

Post-merge validation: repeat empty, populated, and deep-linked thread
opening in the integrated mobile candidate, including keyboard
open/close and sending a reply before release.

buzz-review-completed

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-09 08:05:55 -07:00
Tom Brow
e9269cbdf6 fix(mobile): simplify notification settings (#8199)
Keep notification settings as a separate card with a single
“Notifications” toggle. Remove the section heading, bell, all subtitles
(including permission/capability and pending opt-out messages), and the
iOS settings button.

Toggle callbacks, capability visibility, persisted opt-in/out, and relay
synchronization are unchanged. The disable failure is being investigated
separately. This intentionally removes the pending warning rather than
claiming to fix synchronization. Tom explicitly requested removal of the
immediate opt-out synchronization warning and all other status text;
this is an intentional exception to the pending-feedback guidance in
VISION_MOBILE.md. [Codex’s finding is answered against that agreed
scope](https://github.com/block/buzz/pull/8199#discussion_r4225426842).

No new tests. Delete tests for the removed permission/settings UI and
remove obsolete display assertions from existing recovery tests while
preserving their opt-out synchronization coverage.

Closest related issue:
[#7826](https://github.com/block/buzz/issues/7826). No duplicate cleanup
PR found.

### Validation

Mobile formatting/static analysis passed. The full existing mobile suite
passed (3,216 tests; 4 skipped), plus the three unconfigured-build tests
and gateway recipe checks. Existing opt-out recovery tests exercised the
switch, persisted tombstone, snapshot synchronization, and successful
relay retry.

Before/after renders use the existing Flutter widget fixture with
synthetic data and mocked native navigation. No simulator or TestFlight
run was performed. After merge, validate the settings row and on/off
flow in the next iOS candidate; remote disable failure remains a
separate investigation.

GitHub CI also passed the mobile tests, unconfigured-build tests, and
gateway recipe checks. The [security
review](https://github.com/block/buzz/actions/runs/37863885299)
completed successfully. The [Android
build](https://github.com/block/buzz/actions/runs/37863554077/job/113605025305)
passed. The iOS simulator build, native accessibility checks, and
unsigned iOS release build passed. [All required CI
checks](https://github.com/block/buzz/actions/runs/37863554077) are
green.

### Before and after

Headless Flutter widget fixture, with synthetic data and mocked native
navigation.

| Before | After |
| --- | --- |
| ![Notification settings
before](https://raw.githubusercontent.com/block/buzz/8334767435a2896656df7bcc497dcb64a1846d92/pr-8199--before.png)
| ![Notification settings
after](https://raw.githubusercontent.com/block/buzz/8334767435a2896656df7bcc497dcb64a1846d92/pr-8199--after.png)
|

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-08 18:47:42 -06:00
klopez4212
7e8aac4403 fix(mobile): polish channel management and iOS Settings (#8171)
## Summary

Use native iOS sheets for channel editing and Add Members, with profile
avatars, matching search, and destructive removal dialogs. Canvas
content becomes a tappable preview with Add/Remove actions.

Fix the Settings availability menu and clipped copy confirmation. Align
channel action messages with chat styling and keep agent identities
distinguishable across selection, attribution, and profile surfaces.

### Related issue

No duplicate found in the existing mobile Settings/channel issues and
PRs.

### Testing

- Flutter analysis, formatting, file-size checks, focused collision
regressions, and the full mobile test suite passed.
- Production UIKit picker simulator check passed: distinct
visible/accessibility labels, avatar updates, selected rows, and pubkey
callbacks. Wired into Mobile Swift CI.
- Original UI was built and tested on the physical iPhone with author
confirmation; profile/community data preserved.
- Review fixes await renewed author device confirmation before restoring
`buzz-review-completed` and ready-for-review status.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-08 14:32:55 -04:00
klopez4212
1ac0a1271b fix(mobile): preserve Markdown attachment destinations (#8168)
## Summary
Fix mobile “Image unavailable” errors caused by angle-bracketed Markdown
URLs. Preserve attachment destinations across images, galleries, videos,
and file links, including spaces, parentheses, and optional titles.

### Related issue
Related: #8086 (attachment filenames).

### Testing
- `just mobile-install mobile-check mobile-test`
- Regression coverage decodes PNG/JPEG/GIF/WebP/BMP and verifies gallery
viewers, video routing, and authenticated file links.
- Native device playback not tested.

---------

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

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

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

### Related issue

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

### Testing

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

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-08 10:40:12 -04:00
Tom Brow
70d2ca7760 fix(mobile): tolerate unrelated NIP-11 relay metadata (#8160)
Relay capability additions such as `read_state_snapshot` currently make
mobile push discovery reject otherwise valid NIP-11 documents. This
blocks descriptor-dependent enrollment, lease updates, and remote
revocation.

Read unrelated top-level metadata permissively while preserving
validation of the advertised `nip-pl` extension and the complete push
descriptor. Each returned descriptor value is covered by compatibility
tests; missing or malformed required push fields still fail.

### Validation

- [Codex code
review](https://github.com/block/buzz/pull/8160#issuecomment-6049355700)
and [security
review](https://github.com/block/buzz/pull/8160#issuecomment-6049486587)
completed without findings on the final code.
- Added 24 regression cases covering read-state metadata, arbitrary
future metadata, HTTP discovery, missing/malformed push fields, and
invalid extension advertisements. The six compatibility/discovery cases
fail on unchanged main and pass with the fix.
- All 55 focused descriptor, bootstrap, capability, and revocation tests
pass. Mobile formatting and analysis pass.
- Full mobile gate passes: 3,063 tests passed and four skipped with a
configured gateway, plus all three tests for an unconfigured gateway
build and the gateway recipe checks.
- [Current-head
CI](https://github.com/block/buzz/actions/runs/37706738403) passes,
including Flutter tests, Android debug APK compilation, the complete iOS
simulator app, and the unsigned iOS release build.
- Production HTTP descriptor discovery against live personal-relay
metadata passes without enrollment or relay changes.
- Device enrollment, delivery, notification tap, and opt-out remain to
be exercised in the updated iOS candidate. No relay or device state was
changed.




buzz-review-completed

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-07 17:37:02 -07:00
Tom Brow
f260ad7f95 fix(ios): use the shared glass capsule for thread titles (#8150)
The iOS thread header currently renders an unbacked “Thread” label over
scrolling messages. Supply the channel display name as its subtitle so
the header uses the same two-line glass capsule as channel
conversations. The title remains noninteractive and inherits the shared
sizing, truncation, text scaling, and accessibility behavior.

Channel and DM display names update with channel metadata; missing
metadata keeps the capsule without inventing a subtitle. App-bar
construction moves into a sibling file to keep the thread page within
the file-size limit.

### Validation

- [CI run](https://github.com/block/buzz/actions/runs/37654016581):
mobile tests, native iOS build, and all other executed jobs passed. The
overall workflow reports a GitHub internal server error; GitHub rejected
a retry. Required checks passed or were skipped.
- [Codex
review](https://github.com/block/buzz/pull/8150#issuecomment-6042500880)
completed without major findings on the current head.

- Mobile dependency setup, formatting, file-size checks, and analysis
passed.
- Full mobile suite: 3,039 tests passed, four skipped; all three
unconfigured-push tests passed.
- Widget regressions exercise real thread-page native parameters for
channel/DM labels, renames, missing metadata, and disabled title
interaction.
- Native iOS 26.5 simulator check confirms the shared capsule renders
“Thread” above “engineering,” with a single noninteractive accessibility
header. Before/after captures use synthetic content.

[Before/after simulator
captures](https://github.com/block/buzz/pull/8150#issuecomment-6042451532)

### Post-merge validation

During the mobile release baking period, open threads in channels and
DMs on a device, including long channel names and larger text settings;
verify the bubble stays clear of Back and has no tap action.

Human app-test confirmation is not applicable under the standing Buzz
mobile exception.

buzz-review-completed

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-07 20:05:03 +00:00
Logan Johnson
9322289364 feat(mobile): follow the portable mention rules, including DMs (#8102)
🤖
## Summary
The mobile app's @mention chooser now follows the same rules as the
desktop app (buzz-app). The same typed text finds the same people in the
same order on both apps, and sending works the same way.

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

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

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

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

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

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

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

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

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-05 18:54:49 -04:00
Logan Johnson
166d383ca7 feat(mobile): read web app catch-up marks for unread state (#8103)
🤖

## Summary
The web and desktop app (`buzz-app`) saves a "caught up" mark when you
scroll to the bottom of a channel and stay there, and another one for a
thread. Mobile did not understand these marks. It relied on the separate
per-message read marks that `buzz-app` also kept. `buzz-app` is about to
stop keeping those per-message marks under the "caught up" mark
(block/buzz-app#590), to save space in the synced read state.

With this change, mobile reads the "caught up" marks with the same rules
as `buzz-app`:
- `activity:<channel>` reads ordinary top-level messages up to its time.
It does not read thread replies, mentions, broadcasts or DM messages,
because reaching the bottom of the channel does not prove you saw those.
- `thread-activity:<root>` reads the replies in that thread up to its
time.

So a message you read in `buzz-app` stays read on mobile: in the app
badge, the channel list unread state, the "new messages" divider when
you open a channel, and the Mark read / Mark unread action.

Mobile does not write these marks. It only reads them.

### Related issue
None found. Companion to block/buzz-app#590. The old desktop app in this
repo is intentionally not changed.

### Testing
- New badge tests: `activity:` clears an ordinary top-level message, and
does not clear a mention, a thread reply or a DM. `thread-activity:`
clears replies in its own thread only. The two "clears" tests fail
without this change.
- New tests for the per-message read check and the "ordinary message"
rule.
- `flutter analyze`, `dart format` check and the full mobile test suite
pass locally.
- Not yet exercised on a device.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-05 18:52:59 -04:00
klopez4212
5173fad660 feat(pairing): enter desktop codes on mobile (#8085)
## Summary
Enter a desktop-only, random six-digit code on mobile to pair without a
desktop confirmation click. Verification uses the encrypted session,
limits guesses to five, and never treats the QR-derived transcript as
approval. Older clients retain desktop confirmation.

Adds digit-only error shakes and native haptics, a separate biometrics
choice, and a direct community-icon arrival animation. Fixes dropped
early offers and resets completed pairing so reopening or signing out
cannot get stuck.

### Related issue
Related: #8060; no duplicate code-entry PR found.
<img width="780" height="1688" alt="01-enter-code"
src="https://github.com/user-attachments/assets/a99ced3e-4305-4ac6-a6cc-d598ced423e7"
/>
<img width="780" height="1688" alt="02-incorrect-code"
src="https://github.com/user-attachments/assets/19cdb109-1f10-40a9-a661-1d7ba89c81c1"
/>
<img width="780" height="1688" alt="03-biometrics"
src="https://github.com/user-attachments/assets/af1b2924-9ab6-4d1a-b4b1-fccfdf446404"
/>
<img width="780" height="1688" alt="04-community-loading"
src="https://github.com/user-attachments/assets/84145d82-843a-4c25-aef7-6492fc241d4b"
/>
<img width="897" height="388" alt="05-desktop-code"
src="https://github.com/user-attachments/assets/d8a2e469-7e42-466a-988f-bab6b1df85fc"
/>

### Release order
Deploy the updated pairing relay first, then desktop, then mobile. The
relay now allows eight events per connection (seven for a successful
fifth guess plus room for cancellation), with a corresponding bounded
delivery budget. Existing clients remain supported; older client
combinations keep desktop confirmation.

### Testing
- 103 mobile pairing tests, 270 core tests, 51 relay integration tests,
and the new real-relay fifth-guess flow passed.
- The real-relay regression verifies proof, decrypted identity import,
and completion after four wrong guesses. Restoring the old six-event cap
makes it fail.
- A paused-clock desktop regression confirms slow readiness cannot
extend the visible QR beyond protocol expiry. Mobile regression covers
typing before negotiation completes.
- Core/pair-relay Clippy and full native desktop Clippy/workspace tests
passed. Mobile analysis passed; broad mobile runs each hit an unrelated
failure (a gateway-script timeout and a native-menu missing-plugin
error), and both affected test files passed separately.
- Repository-wide Rust testing exhausted local disk space while
compiling unrelated packages. The final push omitted that
resource-blocked hook and avoided repeating the desktop/mobile runs
above; hosted CI remains required.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-05 13:42:39 -04:00
klopez4212
8746bfebff Polish iOS header glass and scroll-edge blur (#8097)
Wrap channel and DM titles in native glass and remove the
temporary-channel clock. Apply a soft status-area fade across iPhone
navigation, and fade the large title with scroll progress and fade in
the centered title, with Reduce Motion support.

Validation: mobile formatting/analysis and the full mobile suite passed.
Native iOS 26/27 simulator checks covered actual text opacity, header
resizing, quick reversal, and restored heading position; signed iPhone
build, installation, and launch succeeded. `just ci` was blocked by disk
exhaustion during Rust compilation.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-05 11:55:11 -04:00
klopez4212
46e60206a4 fix(mobile): fill and stabilize content-aware loading placeholders (#8083)
Fill mobile loading views across the screen and use known image, video,
voice-note, and file shapes in message placeholders. Freeze each loading
cycle’s layout so arriving metadata cannot reshuffle the shimmer just
before content appears; preserve cached Home and forum content during
reconnects.

Validation: mobile analysis and full test suite passed (2,752 passed, 4
skipped), plus 3 unconfigured-push tests and file-size checks. Installed
and launched the loading changes on Pixel 10 before the latest main
merge. Full `just ci` was stopped during desktop checks due to low disk
space.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-05 11:26:48 -04:00
klopez4212
f0eb5575ff fix(mobile): polish photo carousels and video viewer controls (#8079)
## Summary
- Keep neighboring carousel photos visible, add page-change haptics, and
reuse appearance-style dots with native iOS glass and grouped actions.
- Refine full-screen video controls with fading transport, cycling
speeds, pinch-to-zoom, and double-tap reset before swipe-to-dismiss.

### Related issue
N/A. Checked recent open PRs; repository issue search was unavailable.

### Testing
- Mobile analysis/format checks, file-size guards, and full Flutter
suite passed (2,680 tests; 4 skipped).
- Built, installed, and manually verified on the connected iPhone.
- `just ci` was attempted but blocked fetching a Rust dependency because
this Mac could not connect to GitHub.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: klopez4212 <klopez4212@gmail.com>
2026-10-04 17:17:46 +01:00
klopez4212
5f2d801b8f fix(mobile): align Android conversation headers and reactions (#8082)
## Summary
Align Android channel and DM headers with iOS: centered, tappable titles
with presence or member counts and no avatars. Keep conversation
navigation frosted through keyboard and scroll changes, and run the
typing shimmer left to right.

Add an Android reaction details sheet with All/emoji tabs, swipe
navigation, and compact rows aligned to the tab gutters. Native iOS
reaction presentation is unchanged.

### Related issue
None found.

### Testing
- Mobile analysis, formatting, file-size checks, and full mobile tests
passed.
- Android debug build installed and launched on Pixel 10.
- Full `just ci` could not complete: disk space exhausted while fetching
an unrelated Rust dependency.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 16:49:12 +01:00
klopez4212
75a4efd0ca fix(mobile): unify sheet sections and bottom spacing (#8081)
## Summary
Use rounded, grouped rows consistently across mobile sheets and menus,
including Members and list pickers. Keep sheet content above the system
gesture area with 4dp of extra bottom padding on Android and iOS.

### Related issue
No duplicate found; related member-picker work: #4051.

### Testing
- Mobile analysis, formatting, file-size checks, and full mobile test
suite pass.
- Android build installed and launched on Pixel 10.
- Before/after Members previews use sample data (attached below).
- Full `just ci` stopped during Rust compilation because local disk
space ran out.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 16:20:01 +01:00
klopez4212
03f24a69bf feat(mobile): add iOS Liquid Glass tabs and quick actions (#8080)
## Summary

Use native Liquid Glass for the iOS tab bar and expanding + menu,
preserving the existing Lucide icons, icon-only layout, badges,
accessibility labels, and tab interactions. The + uses black-tinted
glass with white controls; the selected tab has a subtle theme accent
(6% blended into the existing selection color). Android retains its
current styling.

### Related issue

No duplicate issue found. Related prior navigation work: #7281 (closed).
This change covers the tab bar and quick actions; it does not add
page-to-page navigation-bar morphing.

### Testing

- `just mobile-check` passed (formatting, analysis, and file-size
checks).
- `just mobile-test` passed: 2,678 tests passed, 4 skipped; the separate
unconfigured-push suite also passed all 3 tests.
- Widget coverage verifies native glass parameters, black + tint,
original icons, icon-only layout, tab selection, badges, and quick-menu
open/close behavior.
- Signed Release iOS build installed and launched on a physical iPhone;
the final glass and tint styling was reviewed on device. No final-app
screenshot was captured.
- Full `just ci` was attempted but stopped during desktop compilation as
available disk space dropped to 3.5 GiB. The complete repository-wide
gate is not locally verified; hosted CI remains pending.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 14:24:09 +01:00
klopez4212
d2da104d94 feat(mobile): add animated community switcher (#8076)
## Summary

Replace the community switch sheet with a two-column picker. Selection
enlarges the avatar, waits with the voice-note loader for destination
channel content and layout to settle, then follows a curved path into
the header.

Cache raster icons at a maximum 512-pixel size and accept only
resource-free emoji SVGs. Cancel timed-out or disposed downloads without
delaying content readiness. Animate Edit badges in place, and open Add
Community on the pairing welcome page. Community removal lives only in
the switcher’s Edit mode. Keep the flow shared across Android/iOS, with
native iOS controls and removal confirmation, and clear navigation
backgrounds at rest.

### Related issue

Related to #8070 (community icon refresh/caching).

### Testing

- 122 focused Flutter regression tests and seven native iOS tests passed
after merging the latest navigation changes from `main`. Earlier feature
build installed and launched on a physical iPhone.
- Full mobile pre-push checks passed; regression coverage includes
gradient rendering, rejected/oversized icons, bounded persisted raster
decoding, SVG resource rejection and cache migration, timeout/disposal
cancellation, disconnected destination channels, degraded unread-history
settlement, and switching while icons refresh. Full `just ci` stopped
during Rust checks for low disk space. Hosted CI remains required.
- [Grid and Edit
previews](https://github.com/block/buzz/pull/8076#issuecomment-5977838957).

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 14:11:39 +01:00
klopez4212
05d1714912 fix(mobile): manage channel members from profile sheets (#8078)
Keep member administration reachable when small channels omit “See all.”
Tapping a member now opens profile-sheet rows to make a channel admin,
change an admin to member, or remove the person with confirmation, using
the existing grouped list style.

Actions follow the live roster, respect channel permissions, prevent
duplicate submissions, and surface failures. Existing guests can become
members or admins; owners, self, archived channels, and DMs do not
expose these controls.

Validation: mobile analysis, formatting, file-size checks, and the full
mobile suite passed. Release iOS build installed and launched on a
physical iPhone. Full repository `just ci` was previously stopped for
low disk space during desktop compilation.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 11:39:13 +01:00
klopez4212
a14107ab2e fix(mobile): refine iOS navigation and conversation details (#8075)
Fix iOS navigation title taps and truncation, theme-aware blur
(including chats opened at the newest message), and the search focus
animation with a plain, correctly spaced Cancel button.

Simplify DM details and header actions, move profile descriptions below
names, and remove redundant Settings/Add Community titles and
unnecessary “See all” links.

Validation: mobile analysis, formatting, file-size checks, and the full
mobile test suite passed in pre-push hooks; Release iOS build installed
and launched on a physical iPhone. Full `just ci` was stopped during
unrelated desktop compilation due to low disk space. Native XCTest
additions have not been run locally.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-04 10:36:59 +01:00
klopez4212
6cf4582002 Use native iOS navigation bars with large titles and avatars (#8052)
## Summary

Use UIKit navigation bars across iOS pages, with large titles that
collapse into the toolbar on scroll and a light native blur. Home uses
the active community name as its title and shows community and profile
avatars with centered emoji and stable loading, without flashing back to
icons. Channel headers retain the member count, tap-to-settings title,
and Huddle action. Their ultra-thin native material has a soft lower
edge and stays visible at the timeline bottom. The hidden title-layout
scroll view has its automatic edge effects disabled so it cannot add a
second backdrop.

| Expanded Home | Scrolled Home | Settings |
| --- | --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--home.png"
alt="home" width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--scrolled.png"
alt="scrolled" width="240"> | <img
src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--settings.png"
alt="settings" width="240"> |

Channel title, member count, and Huddle control:

<img
src="https://raw.githubusercontent.com/block/buzz/47a77d3ff1d30fc74f7488c1a4427e41bbb85c4e/pr-8052--channel.png"
alt="Channel navigation" width="280">

### Related issue

No matching open issue found.

### Testing

- Added causal regressions for Profile/Edit Photo native titles,
metrics-only collapse changes, and native-header suppression throughout
channel/thread reaction and action backdrops. Each regression fails when
its corresponding fix is removed.

- Mobile formatting and analysis passed. Full mobile test rerun passed:
2,622 tests, 4 skipped, plus 3 push-disabled build tests.
- Native iPhone Air regression passed: repeated title expansion/collapse
and scroll material behavior.
- Navigation and blur were accepted on the connected iPhone using the
existing profile. Simulator snapshots use real community data. The
subsequent review fixes have widget coverage; fresh physical-device
verification of Profile states, Dynamic Type changes, and message
backdrops is pending.
- Full repository `just ci` was blocked by local disk exhaustion while
compiling Rust dependencies.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-03 18:10:03 +01:00