Skip to content

fix(meeting): join from the home page, and make host controls act - #28

Merged
kunjhirapara merged 1 commit into
mainfrom
fix/join-and-host-controls
Sep 15, 2026
Merged

kunjhirapara merged 1 commit into
mainfrom
fix/join-and-host-controls

Conversation

@kunjhirapara

Copy link
Copy Markdown
Owner

Two controls that rendered normally and did nothing when pressed. Both had the same shape: the condition gating the action was not the condition that governs whether the action can work.

1. "Join Meeting" failed on the home page

Reported: the interviewer clicked the join link on their main page and got a failure toast, but pasting the same link into the address bar let them in.

MeetingCard's button refused to navigate unless a Stream video client was already connected to the page the user was leaving:

if (!client) return toast.error("Failed to join meeting. Please try again.");
router.push(`/meeting/${callId}`);

Navigating doesn't use that client. /meeting/[id] mounts its own StreamClientProvider and waits for the connection there — which is exactly why the pasted link worked: it skipped a check that was never about whether joining was possible.

On /, StreamClientProvider pulls the SDK through a dynamic() import and only initialises once useUserRole has resolved and only for roles that might start a call. So client is null for as long as that chunk takes to fetch and connect, and never arrives at all for a role that doesn't qualify. Pressing the button in that window reported failure.

joinMeeting now resolves a route from the call id and nothing else (resolveJoinTarget).

2. Host controls were inert in instant meetings

"Mute all participants" and "Remove participant" rendered on the Stream capabilities — which the call's creator has, because Stream makes them its host — while both handlers opened with:

if (!call || !interview) return;

createInstantMeeting creates a Stream call under a fresh UUID and writes no interviews row, so getInterviewByStreamCallId resolves to null and MeetingRoom renders with interview === undefined. Every click was discarded silently — no effect, no toast, nothing in the console.

The interview is needed to log a host action, never to perform one. The handlers now require only the call, and the audit entry is written best-effort, matching how participant join/leave is already logged. That also fixes a second bug in the same lines: await logSessionEvent(...) sat inside the try, so a failed audit write turned a successful mute into "Unable to mute everyone."

Menu and handlers now read one predicate, getHostControlsAvailability, so the render gate and the guard can't drift apart again. It additionally hides the menu when removal is the only thing on offer and there's nobody to remove — previously that opened a heading with nothing beneath it.

Also

MeetingModal dropped an empty pasted meeting id (if (meetingId) joinMeeting(...)), so a URL ending in / produced the same silent no-op. It now goes through the error path.

Verification

  • npm run typecheck — clean
  • npm run test — 280/280 pass (15 new, across hostControls and meetingNavigation)
  • npm run build — succeeds

Both new modules are import-free and live in src/lib/ so the bare node --test runner can reach them, per CLAUDE.md.

Note

Neither fix can be exercised by the test suite end-to-end — there's no component test infrastructure here — so the extracted predicates are what's covered. Worth confirming in a real call: join from the home page, and the two host-control items in an instant meeting.

Two controls that rendered normally and did nothing when pressed. Both had
the same shape: the condition gating the action was not the condition that
governs whether the action can work.

Join Meeting
------------
MeetingCard's button refused to navigate unless a Stream video client was
already connected to the page the user was leaving:

    if (!client) return toast.error("Failed to join meeting. Please try again.");
    router.push(`/meeting/${callId}`);

Navigation does not use that client. /meeting/[id] mounts its own
StreamClientProvider and waits for the connection there, which is why pasting
the same link into the address bar worked while the button reported failure.
On "/" the client is loaded through a dynamic import and only once useUserRole
has resolved, so it is null for as long as that chunk takes to fetch and
connect — and never arrives at all for a role that does not qualify.

joinMeeting now resolves a route from the call id and nothing else.

Host controls
-------------
Mute all participants and Remove participant were gated on the Stream
capabilities, which their creator has, but both handlers opened with:

    if (!call || !interview) return;

An instant meeting writes no interviews row, so getInterviewByStreamCallId
resolves to null and MeetingRoom renders with interview === undefined. Every
click was discarded silently: no effect, no toast, nothing in the console.

The interview is needed to log a host action, never to perform one. The
handlers now require only the call, and the audit entry is written
best-effort — matching how participant join/leave is already logged, and
fixing a second case where a failed log turned a successful mute into an
error toast.

Both menu and handlers now read one predicate, getHostControlsAvailability,
so the render gate and the guard cannot drift apart again. It also hides the
menu when the only thing on offer is removal and there is nobody to remove,
which previously opened a heading with nothing under it.

Also passes an empty pasted meeting id through to the error path instead of
dropping it, which was the same silent no-op in MeetingModal.

Verified: typecheck clean, 280/280 tests pass (15 new), production build
succeeds.
Copilot AI lite review requested due to automatic review settings September 15, 2026 07:23

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kunjhirapara
kunjhirapara merged commit 1949532 into main Sep 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants