Repository navigation
feat(mobile): bring Activity feed and filters to mobile - #9186
klopez4212 wants to merge 4 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 065c133de0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| child: IosMessageCreateButton( | ||
| key: ValueKey(controlKey), | ||
| label: label, | ||
| labelFontSize: textScaler.scale(16), |
There was a problem hiding this comment.
Apply Dynamic Type only once to iOS filter labels
On iOS with a non-default accessibility text size, textScaler.scale(16) already contains the user's Dynamic Type scaling, but JumpToLatestGlassButton.swift subsequently feeds this value into UIFontMetrics.scaledValue, applying the same scaling again. At large text sizes the native label becomes much larger than the once-scaled width and height calculated here and is clipped; pass the unscaled base size (as other callers do) or stop scaling the supplied value natively.
AGENTS.md reference: AGENTS.md:L273-L281
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf53a84a06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _ActivityMenuButton<InboxFilter>( | ||
| controlKey: 'activity-type-menu', | ||
| popoverKey: 'activity-type-popover', | ||
| semanticLabel: 'Activity type', |
There was a problem hiding this comment.
Announce the selected Activity type
On iOS with VoiceOver, this constant accessibility label overrides the native button's visible title, so after selecting Threads, Mentions, DMs, or Reactions the control is still announced only as “Activity type” and the current filter cannot be determined without reopening the menu. Include the selected label in the semantic label, as the sender control already does.
AGENTS.md reference: AGENTS.md:L273-L281
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 582516275a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90935278da
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (e) => | ||
| (filter.value != InboxFilter.mention || | ||
| e.category == 'mention') && | ||
| (e.reactionTarget == null || includesSender(e.pubkey)), |
There was a problem hiding this comment.
Filter grouped events before selecting their sender
When a DM or thread contains messages from both people and agents, this predicate applies the sender filter before grouping only to reactions; the later check therefore classifies the whole conversation by its latest representative. For example, selecting People hides a human reply when a newer agent reply exists in the same group, while selecting Agents includes the human messages in that group. Apply includesSender to every event before buildInboxItems so each sender view is built from matching activity.
Useful? React with 👍 / 👎.
Keep Activity feed, filter controls, and Activity/Search content fade separate from Home layout and bell work in #9193. Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <[email protected]>
d7f5f48 to
1fbd0c7
Compare
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 700eca756a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a30196c9d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| continue; | ||
| } | ||
| final thread = event.threadReference; | ||
| final mentioned = !channel.isDm && tagsAddress(message.tags, viewer); |
There was a problem hiding this comment.
Keep original mention tags when applying edits
When another user edits a top-level message that mentioned the viewer, mobile's edit event carries only h, target e, and media tags, while activityMessages substitutes those edit tags into message. Testing relevance against message.tags therefore turns mentioned false and removes the mention from Activity even though only its content was edited; use the original event tags for addressing and thread relevance while projecting the edited content.
Useful? React with 👍 / 👎.
| for (final event in item.groupItems) { | ||
| if (!isThreadReply(event.tags)) { | ||
| notifier.markContextRead(msgContextKey(event.id), event.createdAt); | ||
| } |
There was a problem hiding this comment.
Advance the Solo activity marker once
For a Solo channel, the new grouping collapses every top-level entry into one row, but marking that row read performs one markContextRead(msg:...) call per grouped event. ReadStateManager persists on every call and deliberately treats msg: contexts as local-only, so a row containing up to the 500-event history cap causes hundreds of durable writes and does not synchronize the read action to other devices; advance activityContextKey(channelId) once to latestActivityAt, which inboxEventReadAt already uses for these channelCatchUp events.
AGENTS.md reference: AGENTS.md:L259-L264
Useful? React with 👍 / 👎.
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bbf000575
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await completeActivityColdWindow( | ||
| query, | ||
| filter, | ||
| received.where((e) => _matches(filter, e)).toList(), |
There was a problem hiding this comment.
Preserve derived-channel reactions while paging
When a cold or reconnect query returns a full page containing mobile-created reactions, this raw-tag recheck rejects those reactions because they carry only an e tag, even though the relay deliberately matches #h via the stored derived channel_id (crates/buzz-core/src/filter.rs:164-178). This can make the delta page appear shorter than its limit and stop pagination early, while later cold pages discard the reactions entirely, so older activity or reactions disappear after a busy offline interval. Page these filters without requiring a literal h tag, or resolve the derived channel from the reaction target. Fresh evidence beyond the earlier review is that the new regression fixture adds an h tag to every event at activity_history_test.dart:20-23, so its purported e-only reaction does not exercise the production representation.
AGENTS.md reference: AGENTS.md:L243-L247
Useful? React with 👍 / 👎.
| _liveRefreshTimer ??= Timer(const Duration(milliseconds: 250), () { | ||
| _liveRefreshTimer = null; | ||
| unawaited(_queueRefresh(generation)); | ||
| }); |
There was a problem hiding this comment.
🤖 P2 · Background refreshes exhaust the shared HTTP quota
With one joined stream and a warm cache, each unrelated incoming message schedules a refresh that issues three separate POST /query requests: addressed history, the channel delta, and update enrichment for the new message. Two messages per second, with reads completing before the next message, consume the default 300-call per-pubkey quota in about 50 seconds. The 250 ms timer bounds refresh operations without accounting for those nested requests.
User/system effect: Activity consumes the shared quota while its page is closed, delaying Activity and other relay reads.
| if (!remaining.contains(channel)) continue; | ||
| if (reads > 0) result.add(event); | ||
| counts[channel!] = (counts[channel] ?? 0) + 1; | ||
| if (counts[channel]! >= 100) remaining.remove(channel); |
There was a problem hiding this comment.
🤖 P2 · Cold paging can stop before any displayable DM
The cross-channel paging fix still retires each channel after 100 raw events. If a DM’s newest 500 events are reactions to a deleted message, that first page retires the DM even though projection produces no visible rows. Older untagged DM messages remain behind the cursor, and Activity offers no older-history action; refreshing reads the same prefix. The previous message-only DM query was not displaced by those reactions.
User/system effect: An existing DM can appear absent from Activity despite older readable messages.
| if (sessionState.status == SessionStatus.connected && | ||
| generation == _subscriptionGeneration) { | ||
| unawaited(_subscribeLive(generation)); | ||
| unawaited(_subscribeLive(generation, since)); |
There was a problem hiding this comment.
🤖 P2 · Successful Retry does not restore live subscriptions
If the initial HTTP query and WebSocket fallback fail, build() exits before _subscribeLive. A successful Retry only fetches and publishes a snapshot through the refresh queue. With no dependency rebuild, the skipped subscriptions remain uninstalled.
User/system effect: Activity looks recovered but receives no subsequent live updates, including hidden-DM resurfacing.
| if (count == 20) { | ||
| throw StateError( | ||
| 'Activity catch-up exceeds its budget. Refresh Activity.', | ||
| ); |
There was a problem hiding this comment.
🤖 P2 · An oversized delta leaves automatic recovery stuck
A retained session with 10,500 matching events in one channel batch fills the initial page and all 20 continuation pages. This exception leaves the saved cursor unchanged, so subsequent live refreshes and reconnects repeat the same oversized delta and fail again. The refresh queue retains the old feed without exposing that error.
User/system effect: Activity silently remains stale until the user manually resets history through refresh.
| } | ||
| } | ||
| } | ||
| _trimEvidence(events); |
There was a problem hiding this comment.
🤖 P2 · Trimming discards the participation proof just fetched
With 4,096 newer evidence events, this trim can evict an older viewer-authored reply returned by participation discovery. ownThreads and ownParents are computed afterward, while the corresponding interest can still be marked cached. Later deltas then skip rediscovering the discarded proof.
User/system effect: Unmentioned replies to a thread the viewer participated in disappear from Activity.
| // rows so deleted/edited or no-longer-admitted messages cannot survive. | ||
| byId.removeWhere((_, item) => activityMessageKinds.contains(item.kind)); | ||
| for (final item in conversationItems) { | ||
| byId[item.id] = item; |
There was a problem hiding this comment.
🤖 P2 · Offline hidden-DM history loses its recovery row
An addressed message in a hidden DM can be returned by the initial history query and then removed here. The replacement conversation projection uses a roster that excludes hidden DMs. If the message arrived offline more than five seconds before startup, the hidden-DM live replay also misses it.
User/system effect: The Activity row that previously opened and recovered that hidden conversation is unavailable.
| final pk = pubkey.toLowerCase(); | ||
| final isAgent = agents.contains(pk) || profiles[pk]?.ownerPubkey != null; | ||
| return sender.value == 'Anyone' || | ||
| (sender.value == 'Agents' ? isAgent : !isAgent); |
There was a problem hiding this comment.
🤖 P2 · The sender filter omits channel bot roles
A sender identified as an agent only by its channel bot role is classified as a person here. The row uses the channel-aware agent identity provider, which includes those roles, but this predicate uses only the global directory and cached profile owner.
User/system effect: The same sender renders as an agent under People and disappears under Agents.
| return target != null && | ||
| (update.channelId == null || | ||
| update.channelId == target.channelId) && | ||
| (update.kind == 9005 || update.pubkey == target.pubkey); |
There was a problem hiding this comment.
🤖 P2 · Owner-authorized updates leave stale agent content
The relay and mobile message actions permit a human owner to edit or delete their agent’s message. This signer-equality check rejects that accepted update because the human and agent have different keys. The original cached event consequently survives the update.
User/system effect: Activity retains deleted agent content or the old text after an authorized edit, while the conversation applies the update.
| for (final target in entry.value) '${entry.key}:$target', | ||
| }; | ||
| if (retained.interests.length > 4096) retained.interests.clear(); | ||
| if (!overflow) retained.since = startedAt - 5; |
There was a problem hiding this comment.
🤖 P2 · The delta cursor misses accepted timestamp skew
This advances the cursor to the reader’s wall clock minus five seconds, but the relay accepts event timestamps up to 900 seconds from its clock. A valid deletion from a sender one minute behind falls before the next delta. Unrestricted overlay reads only cover newly encountered targets, and live notifications trigger those same cursor-bounded queries.
User/system effect: Cached deleted content can remain visible, and valid edits or unaddressed messages can be omitted until a manual cold refresh.
| if (result.length > 4096) { | ||
| throw StateError( | ||
| 'Activity updates exceed the evidence budget. Refresh Activity.', | ||
| ); |
There was a problem hiding this comment.
🤖 P2 · One large edit history disables unrelated Activity
Auxiliary history is collected across retained messages before relevance filtering. More than 4,096 valid edits to a retained message, or across its target batch, triggers this exception and aborts the entire feed assembly. Retry and manual cold refresh encounter the same persisted history.
User/system effect: An unrelated message’s edit history can make DMs, approvals and all other Activity sources unavailable without a working refresh recovery.
The UI-only part of #9186 by @klopez4212, split out so it can land without the Activity sync changes. - The Inbox page becomes Activity, with header controls instead of menus: a type menu (All, Threads, Mentions, DMs, Reminders, Drafts), a sender menu (Anyone, People, Agents) and an Unread toggle. All of them filter the feed the app already loads. - Activity and Search get a top edge fade. Bulk mark-as-read and the "New" divider are gone. - DM thread replies group with their DM row, so the DMs filter includes them. Left out, for a separate sync PR: the Activity history cache, subscriptions to every joined channel, the refresh coalescer, thread-follow evidence, Reactions and Solo rows, and the read-state changes. The Activity provider and read-state writes are unchanged from main. One fix on top: the iOS filter labels pass an unscaled font size, since the native button already applies Dynamic Type. Before, large text was scaled twice. ### Validation - Kenny's added tests for the header controls (`tabs_journey`) and `TopEdgeFade`, adjusted for the removed Reactions option. Added tests for the DM filter, marking a DM row with replies under two thread roots read, and sender filtering per event. - Full mobile suite: see the latest CI run. Locally, 11 push and notification-toggle tests fail identically on an unchanged checkout. - Automated simulator run (iPhone 17 Pro, iOS 27) of a build combining this PR with its sibling split: Home rows measure 44 pt, the bell opens Activity and shows its dot after a row is marked unread, type/sender/Unread filters each narrow the feed, a row opens its conversation, Search results scroll under the fade, and filter labels fit at the largest text size. Screenshots are in Buzz rather than here because they show internal test data. Android was covered only by widget tests. ### Before merge - [x] Automated simulator run of Activity filters, row taps, read and unread, and Search, with screenshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <[email protected]> Co-authored-by: Tom Brow <[email protected]> Co-authored-by: kenny lopez <[email protected]> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
## Summary A reply in a thread you started, participated in, or followed can light a mobile channel's attention dot without appearing in Activity. Activity now includes eligible replies already observed by the channel unread pipeline in **All** and **Threads**. Tapping a row uses the existing oldest-unread reply and thread navigation. Retain the reply payload alongside its bounded unread evidence, then merge by event ID with the addressed feed. Mentions retain their category and do not duplicate. Deletion, eviction, and account/community resets share the same registry. This adds no relay queries or subscriptions. Coverage follows the existing channel unread catch-up and live subscriptions. This does not introduce complete historical thread discovery: replies outside that retained window, and replies not eligible for the channel attention dot, are not newly fetched. ### Related issue Related to #9271 (clearing channel rows) and closed #9186 (broader Activity feed expansion). #9213 supplied the existing Activity filters. This PR connects existing observed replies to those filters. ### Testing - Full mobile suite, push-unconfigured tests, analysis, formatting and file-size checks. - Production-provider regression: cold catch-up, live reply, mention deduplication, excluded unrelated/quiet replies, thread read markers, deletion/replay and identity reset. - Real Activity widget journey with fake relay/storage boundaries: show the unmentioned reply, select Threads, tap the row, and verify the reply ID and thread ID passed to the conversation page. - Mutation check: removing reply-payload retention makes the widget journey fail because the reply is absent. - Independent agent source review: no blockers. ### Preview Actual Flutter widgets at phone width with synthetic data and the new reply wiring disabled/enabled. These are widget-test captures, not installed-device or live-relay evidence. | Before | After | | --- | --- | |  |  | ### Human validation pending Draft per the repository readiness checklist. On a device, receive an unmentioned reply in a thread you started, replied in, or followed. Check Activity → All and Threads, tap the row to reach the reply, then read it and confirm Unread clears without reading unrelated channel messages. Native/live-relay testing and human confirmation remain pending. --------- Signed-off-by: kenny lopez <[email protected]> Signed-off-by: Tom Brow <[email protected]> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <[email protected]> Co-authored-by: Tom Brow <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
Summary
Replace mobile Inbox with Activity: All / Threads / Mentions / DMs / Reactions / Reminders / Drafts, Anyone / People / Agents, and an independent Unread toggle. Include participating/followed replies, Solo updates, grouped reactions, and independent DM history; reading a thread no longer reads unrelated channel activity.
Use native iOS glass controls and equal-height rounded Android filter pills with content-sized widths and consistent horizontal padding. Activity and Search keep fixed headers with a 16-point content fade at the top of their clipped lists. Shared navigation backdrops are unchanged. The bulk-read action and ellipsis are removed to match desktop.
Related issue
Related to #7779. Home layout, All / Solo / Team sizing, and the top-row bell are owned by #9193. This PR only renames the existing Inbox entry to Activity, including its accessibility label.
P1 review fixes
All includes approvals and agent lifecycle events. Cold conversation history uses aggregate channel batches, pages past saturated conversations to reach quieter channels, and cursor-pages participation evidence. Reminders and saved drafts remain reachable from the type menu. Activity reads shared conversation metadata and owns its message projection, including forum posts/replies and reactions without recipient tags.
Testing
6bbf00057: full Flutter suite passed (4,526 tests; 60 skipped), plus 4 unconfigured-push and 27 crypto tests. Analysis, formatting and file-size checks passed.Snapshots
Actual screen widgets with synthetic data, Flutter fallback rendering (not native iOS glass):