Skip to content

fix(mobile): join an open channel from the chat footer - #9253

Merged
brow merged 6 commits into
mainfrom
mobile-join-footer
Nov 10, 2026
Merged

brow merged 6 commits into
mainfrom
mobile-join-footer

Conversation

@brow

@brow brow commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

A nonmember viewing an open channel on mobile saw "Join this channel from Manage to participate." in the footer, with nothing to tap and no obvious "Manage". The footer now matches desktop's join notice: Join channel to send messages with a primary-styled Join button (the same primary colors as the composer's send button). A private channel preview says "You're previewing this channel" with no Join, and the archived notice is unchanged.

Making the join show up

The relay acknowledges a join (kind 9021) before its read replicas see the new roster; on the Builderlab relay the lag ran to tens of seconds. So:

  • Confirm, then show. Like desktop's channel lifecycle, joinChannel confirms the roster with consistency: "strong" (writer-pool) reads at 0/250/750/1500 ms. It then settles only once the channel list itself shows the membership.
  • Accepted but unconfirmed is not a rejection. If the roster never confirms, or the list read fails or is retired, joinChannel throws ChannelJoinUnconfirmed. The footer then says "Join sent, but not confirmed yet" with Check again, which rereads (recheckJoin) without sending another join. A refused join still shows "Couldn't join channel. Try again." and keeps Join. The Channel details "Join channel" row gets the same distinction.
  • Read-your-writes without hiding removals. ChannelsNotifier records each join it confirmed. While an ordinary membership read from a replica lacks one, the list rereads memberships from the writer pool, whose answer is authoritative: a join removed since is dropped there and forgotten. Once a replica read includes every recorded join, reads go back to replicas only. A community or identity switch clears the recorded joins.

Guest-join channels

Self-joining a channel whose join_role is guest gives a role that can't post. Like desktop, the footer reads This channel is read-only with no Join, so it never promises sending. Channel details offers no "Join channel" row for them either. ChannelData parses join_role the way desktop does: a missing tag means member, and anything but a single member value fails closed to guest.

Before / after

Before (main) After

New states (widget-test renders):

Join accepted, not confirmed yet Guest-join channel

Demo video (search for an open channel, open it, tap Join; the footer becomes the composer): shared in Buzz rather than here, since it shows real member names from the test community.

Validation

  • Footer journey: a refused join keeps Join with an error toast; an unconfirmed join shows Check again, which rechecks without resending and then shows the composer. Further tests cover a guest-join channel (read-only, no Join), a private preview, and the member join.
  • ChannelsNotifier with a fake relay whose writer pool sees joins before replicas:
    • a confirmed join survives later stale replica reads;
    • reads stop pinning once replicas catch up;
    • a join removed on the writer is not kept;
    • a join recorded for one identity doesn't pin another's reads;
    • a failed roster read, a list that can't show the channel yet, or a failed list read each reports the join unconfirmed, and a recheck then shows it with no second 9021;
    • a list rebuild mid-join still shows the membership.
      A table test covers join_role parsing.
  • Breaking the writer reread, the forget-on-removal step, the scope-switch reset, or the retired-read retry each fails these tests.
  • Full mobile suite: 4611 pass.
  • Automated sim run (iPhone 17 Pro, Builderlab test account, live relay): left an open channel, found it in search, opened it, and tapped Join. The composer replaced the footer, and it stayed after backgrounding and relaunching the app.

Leave's ordinary refresh and the relay's missing-roster repair are left for separate follow-ups.

buzz-review-completed

🤖 Generated with Claude Code

@brow

brow commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

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

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T22:53:02.011459Z 1b61947 Draft marked ready
ℹ️ 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.

@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: a503618e7f

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

@brow

brow commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: fae660296e

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

@brow
brow marked this pull request as ready for review November 8, 2026 21:47
@brow
brow requested a review from a team as a code owner November 8, 2026 21:47
@brow
brow marked this pull request as draft November 8, 2026 21:47
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 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: e2dc54e22fc113f298889d76c3acf674cb46db4b...3c7fe36a19597b164da00c2c8666c55ec82bfefe
  • 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 regressions identified in the authorized PR range.

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: 5b6064229d

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

brow pushed a commit that referenced this pull request Nov 8, 2026
@brow

brow commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

Footer before and after captures for the PR description.

footer-after

footer-before

@brow
brow force-pushed the mobile-join-footer branch from 5b60642 to 1b61947 Compare November 8, 2026 22:49
brow pushed a commit that referenced this pull request Nov 8, 2026
@brow

brow commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

Updated after shot: primary-styled Join.

footer-after-primary

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 8, 2026
@brow
brow marked this pull request as ready for review November 8, 2026 22:50
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 8, 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: 1b619472cd

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

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 :bot: Jude’s code review agent — REQUEST CHANGES

Reviewed base 8cce12f591988f29cc2beab5ff0c89d51862f31b to head 1b619472cda1182296c897a33e07c6facd4b9b15. This joins a channel from the mobile footer and changes the read-after-write membership contract. The following source-traced cases need attention:

  1. [P2] A confirmed join can settle without updating the UI. channel_management_actions.dart:238–240,392–400 calls the best-effort refresh; channels_provider.dart:1033–1057 returns normally when disconnected, retired, or a fetch fails. After a confirmed roster, the footer can therefore re-enable Join without feedback while its cached channel remains a nonmember; the details-page caller can also close as if successful. Propagate or reconcile the result and verify that membership was installed before treating the operation as complete. Add a production-seam regression for failed/retired refresh and visible recovery, plus a successful strong-membership refresh assertion. The current footer success test supplies fake actions, and the action test only exercises exhausted confirmation.

  2. [P2] A later replica read can undo the confirmed join. The join refresh requests strong membership reads, but a later notice, foreground resume, or reconnect may trigger an ordinary list read: channels_provider.dart:980–989, channels_provider_lifecycle.dart:285–301. If the replica still lacks the new roster, that newer request installs a nonmember snapshot and the composer disappears again; the read can also be marked as covering the notice. Preserve read-your-writes through subsequent refreshes without hiding a genuine later removal, and test a writer-visible/replica-stale join followed by a later ordinary refresh and eventual reconciliation.

  3. [P2] Distinguish acceptance from failure. _confirmJoined runs only after submit returns, but a failed query or exhausted confirmation is caught by the new footer and shown as Couldn’t join … Try again. The membership may already exist. In particular, the relay commits membership before best-effort roster emission, and an already-member retry returns before re-emission (side_effects.rs:2966–2985,3014–3016); resending is not a universal recovery. Represent accepted-but-unconfirmed separately from an actual rejection, give accurate next-step guidance, and test both outcomes. A durable repair for a missing relay roster is a pre-existing relay follow-up, not a demand to redesign the relay in this mobile PR.

  4. [P2] The new posting promise is false for guest-join channels. banners.dart:11–12,31–35 promises sending for every open channel, but self-join can grant guest (side_effects.rs:2975–2984), and guests cannot post (ingest.rs:996–1026). The pre-existing composer checks membership rather than posting role, so this new one-tap path can invite users to compose only to have the relay reject the send. Make the new copy and resulting affordance accurate for guest joins; cover guest and ordinary-member journeys.

Scope: Leave’s ordinary refresh and the relay’s missing-roster repair gap predate this PR; consider them separate follow-ups, not independent blockers here.

Evidence and verification: source review at the clean pinned head; git diff --check 8cce12f591988f29cc2beab5ff0c89d51862f31b 1b619472cda1182296c897a33e07c6facd4b9b15 passed. GitHub CI run 42532167759 passed at the exact head, including mobile checks and three mobile test shards. The PR body separately reports a local full-suite failure that passed when rerun alone. The failure scenarios above are source-traced, not independently reproduced; this review did not run a local suite, simulator, accessibility, or mutation checks. Author owns fixes and causal regressions; reviewer/CI owns independent full-suite and exact-artifact native/accessibility verification. Host capacity and simulator availability are verification gaps, not additional defects.

@tlongwell-block

Copy link
Copy Markdown
Collaborator

🤖 Meli, Tyler’s AI agent, posting on his explicit behalf.

The new footer conflates an accepted-but-unconfirmed join with a rejected join by showing “Couldn’t join. Try again.” Please distinguish those outcomes, as the current desktop flow does, and cover the unconfirmed state.

A separate, pre-existing relay recovery gap can make retries ineffective when membership has committed but discovery/roster emission failed and no later repair occurs: the already-member path returns without re-emitting it. That deserves a follow-up; an unconfirmed result alone does not establish a permanent dead end. Leave confirmation parity is recommended as a separate follow-up, not an independent blocker for this PR. The full mobile suite passed; simulator failure-state validation remains incomplete.

Reviewed head: 1b619472cda1182296c897a33e07c6facd4b9b15.

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Kyle reviewed this PR and the Nexus review-tracker notes and approves.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

square-tomb and others added 5 commits November 10, 2026 07:08
A nonmember viewing an open channel saw "Join this channel from Manage to
participate." with no way to act on it there. The footer now matches
desktop: "Join channel to send messages" with a Join button. A private
channel preview says it is a preview and offers no Join.

Joining also has to show up right away. The relay acknowledges the join
before its replicas see the new roster, so the refresh that followed still
listed the user as a nonmember and the page stayed on Join. Like desktop's
channel lifecycle, joinChannel now confirms the roster with strong
(writer-pool) reads, then refreshes memberships with a strong read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
Only the relay's /query bridge honors consistency: strong. A WebSocket REQ
reads replicas, so the confirmation could miss a just-accepted join.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
A join now settles only when the channel list shows the membership. An
accepted join that can't be confirmed or shown yet is reported apart from
a refusal, and the footer offers Check again, which rereads without a
second join. While a confirmed join is missing from a replica read, the
list reads the writer pool, whose answer also keeps a later removal.
Guest-join channels read as read-only with no Join, as on desktop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
@brow
brow force-pushed the mobile-join-footer branch from 1b61947 to f2c216f Compare November 10, 2026 15:33
@brow

brow commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Screenshots

pr-9253--footer-guest

pr-9253--footer-unconfirmed

brow pushed a commit that referenced this pull request Nov 10, 2026
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 10, 2026
A community or identity switch clears this client's recorded joins, so
another scope's join never pins reads to the writer pool. Join failures
now log their cause, and Channel details offers no Join for a guest-join
channel, matching the footer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Tom Brow <[email protected]>
@brow

brow commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Thanks, Jude. All four are addressed as of 3c7fe36:

  1. Settling without the UI. joinChannel now settles only once the channel list shows the membership (installConfirmedJoin, which reads through fetchFresh and propagates its failures). A confirmed join that a failed or retired list read can't show throws ChannelJoinUnconfirmed. Regressions cover a failed list read, a list that can't show the channel yet, a list rebuild mid-join, and the successful install.
  2. Later replica reads. ChannelsNotifier records its confirmed joins. While a replica membership read lacks one, it rereads from the writer pool (consistency: strong), and that answer is authoritative, so a genuine removal is dropped rather than hidden. The pin clears once replicas include the join, and on a community or identity switch. Tests cover all three.
  3. Unconfirmed vs rejected. An accepted but unconfirmed join shows "Join sent, but not confirmed yet" with Check again, which rereads without resending. A refusal keeps the "Couldn't join" toast and Join. Both are steps of one footer journey; Channel details distinguishes them too.
  4. Guest joins. join_role is parsed as desktop does it (missing means member; anything but a single member fails closed to guest). Guest-join channels show "This channel is read-only" with no Join in the footer or Channel details. Covered by a guest journey and a parsing table, next to the member journey.

Leave's refresh and the roster repair stay out of scope as you suggested. Full mobile suite passes, and an automated sim join against a live relay showed the composer and kept it across an app relaunch.

@brow
brow requested a review from jedwards27 November 10, 2026 15:40
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 10, 2026
@brow
brow dismissed jedwards27’s stale review November 10, 2026 23:28

🤖 All four points addressed in 3c7fe36 (see reply above).

@brow
brow merged commit 0aca758 into main Nov 10, 2026
53 checks passed
@brow
brow deleted the mobile-join-footer branch November 10, 2026 23:32
philazar added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main:
  feat(mobile/ios): show the message's own text in notifications (#9346)
  feat(mobile): prefill the agents you last tagged in a conversation (#9248)
  feat(mobile): start a Solo chat from a message or inbox row (#9251)
  feat(mobile): link to Builderlab web from Settings (#9300)
  fix(relay): bound invite preview host lookup (#9308)
  fix(relay): saturate filter limits instead of casting (#9230)
  fix(mobile): tell same-name agents apart in Solo (#9321)
  fix(mobile): join an open channel from the chat footer (#9253)
  fix(relay): fence pending subscriptions on close or replacement (#2382)
  fix(mobile): hide archived people and agents from choices; give them their own members section (#9320)
  Add video stacks and native iOS zoom playback (#9316)
  Support PNG and HEIC photos in the native iOS zoom gallery (#9314)
  fix(mobile): list known people in recipient pickers before a name is typed (#9311)
  feat(mobile): mark edited messages after the body (#8863)

Signed-off-by: Philip Azar <[email protected]>
philazar added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main: (22 commits)
  chore(deps): bump hickory-proto to 0.26.2 for RUSTSEC-2026-0487..0491 (#9351)
  feat(mobile/ios): show the message's own text in notifications (#9346)
  feat(mobile): prefill the agents you last tagged in a conversation (#9248)
  feat(mobile): start a Solo chat from a message or inbox row (#9251)
  feat(mobile): link to Builderlab web from Settings (#9300)
  fix(relay): bound invite preview host lookup (#9308)
  fix(relay): saturate filter limits instead of casting (#9230)
  fix(mobile): tell same-name agents apart in Solo (#9321)
  fix(mobile): join an open channel from the chat footer (#9253)
  fix(relay): fence pending subscriptions on close or replacement (#2382)
  fix(mobile): hide archived people and agents from choices; give them their own members section (#9320)
  Add video stacks and native iOS zoom playback (#9316)
  Support PNG and HEIC photos in the native iOS zoom gallery (#9314)
  fix(mobile): list known people in recipient pickers before a name is typed (#9311)
  feat(mobile): mark edited messages after the body (#8863)
  Add swipeable photo stacks and native iOS gallery preview (#9259)
  ci(docker): skip relay image publishes for main pushes outside the build context (#9310)
  Add manual-only relay image publishing (#9303)
  fix(mobile): bump Flutter to 3.47.9 for Android TLS fix (#9309)
  Serve video and poster previews for invite links (#9306)
  ...

Signed-off-by: Philip Azar <[email protected]>

This branch was previously deployed

1 inactive deployment
codex-review — 3c7fe36a Deployed Nov 10, 2026 by brow via Run Codex Security Review #14239
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.

6 participants