Skip to content

fix(mobile): open unjoined channels from links and search results - #8923

Merged
brow merged 7 commits into
mainfrom
mobile-open-unlisted-channels
Nov 2, 2026
Merged

brow merged 7 commits into
mainfrom
mobile-open-unlisted-channels

Conversation

@brow

@brow brow commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Tapping a #channel link to an open channel you haven't joined showed "Channel could not be opened", because a tap could only open a channel already in the local list. A search hit had the same gap: it did nothing when its channel wasn't in the loaded list, for example when the open-channel directory failed to load or hadn't loaded yet.

Both taps now go through one resolver. It looks in the loaded list first, then reads the channel's own metadata, as buzz:// deep links already do.

  • An open channel you haven't joined opens in preview ("Join this channel from Manage to participate").
  • A private or withheld channel stays closed and shows "Channel not found here".
  • A failed lookup shows "Could not open the channel". So does a missing channel when the channel list failed to load, since it may be one you belong to.

The resolver also guards two cases:

  • A second tap during a slow lookup doesn't open the channel twice.
  • A lookup that finishes after you switch community or identity, or after you moved to another page, opens nothing and doesn't block a new tap.

Fixes AIDA-2324 and AIDA-2286. #8764 already made search load the open directory, which fixed the common search case, so the search part here covers the remaining fallback.

Before: a link to an unjoined open channel fails

before-link.mp4

After: the same link opens the channel in preview

after-link.mp4

Validation

  • Added tests:
    • A table-driven channel-link journey with four rows: an unjoined open channel opens, a withheld channel and a private channel show "Channel not found here", and a failed lookup shows "Could not open the channel". Each row double-taps during a gated lookup and asserts a single lookup and a single push. Failure rows then tap again to show the lock is released.
    • A lookup that settles after the user opened another page pushes nothing.
    • Search journeys: a message hit opens its unlisted channel (including after the channel list failed to load), or explains why it can't ("Could not open the channel" when the list failed).
    • Taps whose lookup will be discarded don't block the next tap: after a community switch away and back, or from another page after the first page closed. Each row fails if its part of the tap lock or the connection fence is removed.
    • I reverted the production files to main once and confirmed every new test fails there.
  • The full mobile suite and mobile-check pass.
  • I ran it on an iPhone 18 Pro simulator (iOS 27), signed in to Builderlab with an account that isn't a member of the open channel #speed-tests.
    • A link to that channel shows the error toast on main and opens a preview with this change (videos above).
    • A search hit in that channel opens it with this change. On main it also opens, through #8764's directory load.
  • Not covered by tests: waiting for a channel-list load still in flight, a short guard I checked by reading the code.
  • The human app-test step doesn't apply (Tom's standing exception for mobile client changes).

buzz-review-completed

🤖 Generated with Claude Code

A channel link or search hit only opened a channel already in the
local list, so a link to an open channel you hadn't joined showed
"Channel could not be opened" and a search hit outside the list did
nothing. Both taps now share one resolver that falls back to the
channel's own metadata, as buzz:// deep links do. A private or
withheld channel stays closed with a toast, a repeated tap during a
slow lookup opens once, and a lookup that finishes after a community
switch is dropped.

Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <[email protected]>
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@brow
brow marked this pull request as ready for review November 2, 2026 15:45
@brow
brow requested a review from a team as a code owner November 2, 2026 15:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T16:29:26.405915Z 37d61ab Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 3ae2b6a3771bf667d4f7015a95971140beba5fa7...4becf4a1dbaf1ea9c189e6af1bbd573ac7da1911
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability findings identified in the authorized PR changes.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only source and history inspection. Builds and tests were not executed, as instructed.

Generated by Codex Security Review |
Requested by: @brow |
Workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7e3dcc3f1

ℹ️ 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".

A lookup pending in one community blocked a tap on the same channel id
after a community switch, and the old lookup was then dropped, so
neither tap opened anything.

Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb43229041

ℹ️ 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".

A failed list load rethrew before the metadata lookup, so every tap on
a search hit showed an error instead of retrying.

Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

if (!context.mounted || _scope(ref) != startScope) return null;

P2 Badge Fence returning scopes with a generation

When a user switches A → B → A before A's original lookup settles, this equality passes again because the scope contains only the relay URL and pubkey, so the retired operation can show its cancellation error or navigate during the new visit, while _resolving also rejects the user's new tap in A. Fresh evidence beyond the prior one-way switch report is this returning-scope cycle, which the added test does not exercise. Fence both result acceptance and lock ownership with a monotonically changing community/session generation rather than stable scope equality.

AGENTS.md reference: AGENTS.md:L228-L238

ℹ️ 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".

A switch away and back to the same community kept the same scope
string, so a lookup from before the switch still blocked a new tap and
could land in the later visit. The relay config is replaced on every
switch, so its identity is the generation.

Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Agreed. Fixed in 37d61ab. The relay config is replaced on every community or identity switch, including a switch back to the same community, so both the result fence and the tap lock now key on that object's identity. The switch test now goes away and back, and it fails on the previous commit.

@brow

brow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37d61ab80f

ℹ️ 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".

A lookup from a page that closed still blocked the same channel's tap
on another page until it settled, though its result was dropped.

Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Nov 2, 2026

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Carl, an automated reviewer, commenting via Wes's GitHub account.

The resolver design holds up: lock cleanup in finally, the connection fence, and private/DM rejection all checked out, and the copy matches buzz:// links. One recommended fix before merge: a slow lookup can push its channel over a page the user opened while it was waiting (inline on :84). The other two inline comments don't block merge.

Validation gaps (optional, extend existing journeys rather than adding tests):

  • The success rows stop at page.channel.isMember == false. Asserting the visible preview banner and absent composer would prove the user-facing outcome.
  • No row retries after a failure toast, so nothing proves the lock is released.
  • The pending initial-list path is untested, as the PR says.

Reviewed at 83090be1 with source review across product, tests and concurrency. CI is green. We didn't re-run the app.

failure = error;
}
// The tap belongs to the connection it was made on.
if (!context.mounted ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Recommended fix: a late lookup pushes over whatever the user opened next.

This fence (and the context.mounted check at :19, and message_tile.dart:202) only checks that the tapping page still exists. A page with another route on top of it is still mounted, so a slow lookup passes the check and pushes anyway. The lookup can take up to 8s in fetchHistory, after any rate-limit wait. With no cached list it can wait even longer on the initial load at :75.

Repro: tap a link or search hit for unlisted channel A. Nothing visible happens. Tap a listed channel B, or open a thread, and it opens at once. Then A resolves and is pushed on top of B. A repeat tap on A is swallowed by _resolving, which makes "try something else" the likely next move.

Fix: also require ModalRoute.of(context)?.isCurrent ?? true here, so both callers drop a tap once the user has moved on. There's precedent in home_page.dart:54. To cover it, add a step to the gated table: tap A, push another route, release the gate, and assert A wasn't pushed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in 27bf22a. The resolver now opens nothing unless the tapping page is still the current route (ModalRoute.isCurrent), which covers chat bubbles, thread messages and search. New test: "a channel link lookup that settles after the user moved on opens nothing".

content: Text(
failure != null
? 'Could not open the channel'
: 'Channel not found here',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Non-blocking: a failed list load turns a temporary failure into "Channel not found here".

When the list load fails, :75 swallows the error and falls back to fetchOpenChannel. That function returns null for every private channel and DM by design (channels_provider.dart:980). So a private channel the user is a member of gets the final "not found" answer, even though membership just couldn't be checked.

Suggested: when the list has no value because it errored and the fallback returns null, show "Could not open the channel". Leaking whether a channel exists isn't a concern here, because that copy is already the generic failure message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done in 27bf22a. A failed list load is kept as the failure, so a channel that can't be found then shows "Could not open the channel". Covered by a new search-page row ("reports a failure when the channel list failed"). Toast rows also re-tap now to show the tap lock is released.

// A tap can arrive before the first list load finishes. A failed load
// still leaves the channel's own metadata, so the tap is a real retry.
if (!ref.read(channelsProvider).hasValue) {
await ref.read(channelsProvider.future).then((_) {}, onError: (_) {});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Optional: this wait has no deadline and no loading state.

With no cached snapshot, the initial list load waits for the connection with no time limit (channels_provider.dart:269-270). The tap gives no feedback while it waits, and its _resolving key stays held until the wait ends, even if the page is closed. We couldn't find a realistic journey where this hangs forever, so it doesn't block merge. With the isCurrent fence above, the worst outcome is a silent no-op. A short in-progress indicator would still make the wait visible to the user.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Leaving this one for now. With the current-page fence, the worst case is a tap that does nothing while the connection is still coming up, and a later tap after the list loads works. A deadline or spinner would be a separate UX change shared with the other open-channel paths.

A slow lookup pushed its channel over whatever page the user had opened
while it waited. A failed channel list now reports a missing channel
as a failure, since it may be one the user belongs to.

Signed-off-by: Tom Brow <[email protected]>
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 2, 2026

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Carl, an automated reviewer, commenting via Wes's GitHub account.

Follow-up on 4becf4a1 (base 8f83c632): no blockers. The two fixes from the last review hold up.

  • Late push: the ModalRoute.isCurrent fence drops a lookup once anything is on top of the tapping page. That covers links in chat bubbles and threads and search hits. The new "moved on" test fails without it.
  • Failed list load: that error now stays the reported failure. A member's private channel shows "Could not open the channel" instead of "not found", and a public channel still opens from its metadata.
  • Retry after a failure toast: the toast rows now tap again, which closes the lock-release gap.

Leaving the wait without a deadline as a follow-up is fine. With the fence, the worst case is a tap that silently does nothing. The merge from main doesn't touch the changed files, and CI is green. We reviewed the source only and didn't run the app.

@brow
brow merged commit 61b8455 into main Nov 2, 2026
53 checks passed
@brow
brow deleted the mobile-open-unlisted-channels branch November 2, 2026 20:56

This branch was previously deployed

1 inactive deployment
codex-review — 4becf4a1 Deployed Nov 2, 2026 by brow via Run Codex Security Review #11823
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants