Skip to content

Run the chat join step when the profile loads after the chat opens #292

Description

@HMarzban

Problem

A signed-in user can see "Join Channel" in place of the composer. This happens when the profile loads after the heading chat opened. The chat runs its join step only once, when it opens.

On 14ab7f9c1. Paths are under apps/webapp/src/.

  1. useChannelMetadata reads the user id once, inside its load effect (components/chatroom/hooks/useChannelMetadata.ts:24). The effect deps are the channel id and the workspace id (:76). The provider key is the heading id (components/chatroom/Chatroom.tsx:38). So nothing runs the effect again when the profile loads.
  2. With no user id, the effect skips the channel create step (:31) and the join step (:58). bootstrapChannel then stores the member flag isUserChannelMember from the server value, which is false (stores/chat/bootstrapStore.ts:25).
  3. While the profile is missing, AccessControl shows the composer (components/chatroom/components/ChannelComposer/ChannelComposer.tsx:39). When the profile loads, a PUBLIC chat with the member flag false shows "Join Channel" instead (:55).
  4. The only other writer of the member flag is the manual join (components/chatroom/hooks/useJoinChannel.ts:19). For an account that is not yet a member, one click on "Join Channel" brings the composer back. For an account that is already a member, the click fails, and the composer comes back only when the chat opens again. The user should not need that click.

Two paths open the chat before the profile loads:

  • Google One Tap signs in without a reload, on desktop only (components/pages/document/DocumentPage.tsx:56). It sets the profile at components/GoogleOneTapAuth.tsx:54, through components/auth/applySignedInProfile.ts:28. The pad provider is keyed on the document id only (hooks/useYdocAndProvider.ts:250), so the open chat stays mounted.
  • On a cold load, the session is set before the profile fetch ends (hooks/useOnAuthStateChange.ts:69-71). A chat deep link waits for the document, not the profile (hooks/useCheckUrlAndOpenHeadingChat.ts:41). This is a race. It needs a profile fetch that is slower than the document sync plus the 800 ms delay (hooks/useCheckUrlAndOpenHeadingChat.ts:53-59). Here the session is present, so only an account that is not yet a member sees "Join Channel".

In the One Tap path, even an account that is already a member of that chat sees "Join Channel". The first load ran as a visitor, so the server returned false.

Steps to reproduce

  1. Use Google Chrome 117 or later on desktop, signed in to a Google account. Sign out of docs.plus.
  2. Open a pad link that carries ?chatroom=<heading id>, for a heading chat that has messages. The link opens the chat without a click, so the One Tap prompt stays open.
  3. Wait for the chat to open. The composer shows.
  4. Sign in with the Google One Tap prompt. The page does not reload.
  5. See "Join Channel" in place of the composer.

Acceptance criteria

  • In the steps above, the composer shows after sign-in. The user does not click "Join Channel". This holds whether or not the account is already a member of that chat.
  • When the profile loads after the chat opened, the feed and the composer do not go back to a loading skeleton.
  • A workspace member opens a heading chat that has no channel row yet, before the profile loads. After it loads, their first message sends.
  • A visitor who stays signed out makes no channel create call and no join call. They see the same read-only view as today, with no error badge or toast.
  • When the profile is loaded before the chat opens, each chat open makes at most one join call, as today.

Agent Brief

Category: bug
Summary: When the profile loads after a chat opens, the chat must still run its skipped write steps, so a signed-in user gets the composer.

Current behavior:
useChannelMetadata reads the user id once, inside its load effect. The effect does not run again when the profile loads. With no user id, it skips upsertChannel and joinChannel. bootstrapChannel stores isUserChannelMember as false. When the profile loads, AccessControl swaps the composer for the "Join Channel" button.

Desired behavior:
The load may run without a user id. If a user id is present when that load ends, or appears later, the chat runs the skipped write steps once. It creates the channel row if it is missing. It joins only if the user is still not a member. It then updates isUserChannelMember and the user's member row in the store. The feed and the composer stay as they are, with no return to loading. A short "Join Channel" may show until the writes end. Visitors still skip both writes. The fix stays inside the chat. It does not change the auth state handler and does not reload the page.

Key interfaces:

  • useChannelMetadata — the load effect and its user id gate.
  • upsertChannel() and joinChannel() — the two skipped write steps.
  • bootstrapChannel() — writes isUserChannelMember and the member row.
  • useJoinChannel — the manual join. It sets the member flag through setWorkspaceChannelSetting.
  • AccessControl in ChannelComposer — chooses the skeleton, the composer, or "Join Channel".
  • isChannelDataLoaded and isFeedReady — the loading gate for the feed and the composer.
  • joinedWorkspace — the workspace setting that turns true once a workspace join ends. It never resets to false.

Out of scope

Notes

Do not make the fix only a new effect dep on the profile id. The effect clears isChannelDataLoaded at its start (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:21). isFeedReady needs it (apps/webapp/src/components/chatroom/ChatroomContext.tsx:288), so the feed and the composer would go back to their skeletons.

The workspaceId dep does not cover this (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:15, :76). It changes only with the document metadata, and an in-place sign-in does not change that.

Order the new writes after the workspace join. The join policy needs workspace membership (packages/supabase/scripts/13-RLS.sql:170-181). The hook's own comment says the channel create step is gated the same way (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:26-28). For a first-time visitor, join_workspace writes that membership. useJoinWorkspace starts that call only once a profile exists and the channel list has loaded, and sets joinedWorkspace when it ends (apps/webapp/src/hooks/useJoinWorkspace.ts:18-24). Nothing sets joinedWorkspace back to false (apps/webapp/src/stores/workspace.ts:64, apps/webapp/src/hooks/useJoinWorkspace.ts:24). After a sign-out and a document switch in the same tab, it can be true while the new join still runs. Do not use it alone as the gate.

The skipped channel create step causes a second failure. A heading chat with no channel row gets an empty result (packages/supabase/scripts/10-functions.sql:94-113). AccessControl then shows the composer (apps/webapp/src/components/chatroom/components/ChannelComposer/ChannelComposer.tsx:39). A send then fails. The message insert policy needs internal.can_read_channel(channel_id), and that check needs the channel row (packages/supabase/scripts/13-RLS.sql:211-216, packages/supabase/scripts/10-0-func-helpers.sql:143-162).

Do not reuse the missing-row check (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:29-31). It reads workspaceSettings.channels, and the first load writes an entry there even when the row is missing (apps/webapp/src/stores/chat/bootstrapStore.ts:24-27). Use channel_info from the first load. A null value means the row is missing.

The effect also passes no user id to bootstrapChannel (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:66). So the user's member row is not stored (apps/webapp/src/stores/chat/bootstrapStore.ts:29-36). The read cursor seed then finds no row (apps/webapp/src/components/chatroom/ChatroomContext.tsx:158-164). See the unread rule at apps/webapp/src/components/chatroom/CLAUDE.md:17. This part was not traced to a visible symptom.

Keep the chatroom rules. Writes stay gated on the user id at the entry point (apps/webapp/src/components/chatroom/CLAUDE.md:117). Sign-in call sites write their own state, and a guarded SIGNED_IN handler was rejected (:119).

joinChannel returns a policy error instead of throwing it (apps/webapp/src/api/channels/joinChannel.ts:15-17). So the hook marks a failed join as a member (apps/webapp/src/components/chatroom/hooks/useChannelMetadata.ts:59-61). Do not rely on its catch to detect a failed join.

Coordinate with #283. Both change what the composer area shows.

The evidence is a code trace on 14ab7f9c1, confirmed by two verifiers. It was not reproduced in a browser.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AuthChatRelated to chat featuresbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions