fix(meeting): join from the home page, and make host controls act - #28
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:Navigating doesn't use that client.
/meeting/[id]mounts its ownStreamClientProviderand 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
/,StreamClientProviderpulls the SDK through adynamic()import and only initialises onceuseUserRolehas resolved and only for roles that might start a call. Soclientis 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.joinMeetingnow 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:
createInstantMeetingcreates a Stream call under a fresh UUID and writes nointerviewsrow, sogetInterviewByStreamCallIdresolves tonullandMeetingRoomrenders withinterview === 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 thetry, 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
MeetingModaldropped 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— cleannpm run test— 280/280 pass (15 new, acrosshostControlsandmeetingNavigation)npm run build— succeedsBoth new modules are import-free and live in
src/lib/so the barenode --testrunner 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.