Problem
Split out of #23 after real-device testing: with a seat visibly open and nobody queued, the app still required Request the mic → type a "why should you get the mic?" message → submit → Claim your seat — high-friction for a literally uncontested seat. #23's own body already treats this as separable (its dependency on #22 is explicitly soft: "direct-join doesn't need it"). This issue is that separable, independent piece, pulled forward because it's cheap and the friction is real today — not a bureaucratic split.
Design
Direct join on a truly uncontested seat: if a seat is open and no pending speaker_requests exist for it, the empty tile itself reads "Join the conversation" instead of "Seat open," and tapping it attempts to occupy that seat directly:
- No request message.
- No separate "Claim your seat" button/step.
- One tap, one server round-trip.
If a legitimate queue exists, the tile falls back to today's existing request flow unchanged — a bystander tapping the empty tile must never let them cut ahead of a real pending request.
Server-authoritative, race-safe: a new, thin server action — verify no pending request exists for the seat, then call the existing claim_speaker_seat (issue #13, already concurrency-safe: two simultaneous taps on a genuinely empty seat still resolve to exactly one winner, the same DB-level atomicity every other seat-claim path already relies on). No new database-level primitive needed — this is a new entry point onto an existing, already-safe one, not new concurrency logic.
Explicit non-goal
No ranking, no eligibility/readiness signal, no time-graduated fallback — those only matter once a queue exists, which by definition this issue's path doesn't have. That's #23's job.
Depends on
Nothing — no dependency on #22 (candidate readiness) or #23 (automatic promotion for contested seats). Depends only on what already exists (claim_speaker_seat, speaker_requests).
Blocks
Nothing new. Removes one class of friction #23 would otherwise have left in place until it landed.
Problem
Split out of #23 after real-device testing: with a seat visibly open and nobody queued, the app still required Request the mic → type a "why should you get the mic?" message → submit → Claim your seat — high-friction for a literally uncontested seat. #23's own body already treats this as separable (its dependency on #22 is explicitly soft: "direct-join doesn't need it"). This issue is that separable, independent piece, pulled forward because it's cheap and the friction is real today — not a bureaucratic split.
Design
Direct join on a truly uncontested seat: if a seat is open and no pending
speaker_requestsexist for it, the empty tile itself reads "Join the conversation" instead of "Seat open," and tapping it attempts to occupy that seat directly:If a legitimate queue exists, the tile falls back to today's existing request flow unchanged — a bystander tapping the empty tile must never let them cut ahead of a real pending request.
Server-authoritative, race-safe: a new, thin server action — verify no pending request exists for the seat, then call the existing
claim_speaker_seat(issue #13, already concurrency-safe: two simultaneous taps on a genuinely empty seat still resolve to exactly one winner, the same DB-level atomicity every other seat-claim path already relies on). No new database-level primitive needed — this is a new entry point onto an existing, already-safe one, not new concurrency logic.Explicit non-goal
No ranking, no eligibility/readiness signal, no time-graduated fallback — those only matter once a queue exists, which by definition this issue's path doesn't have. That's #23's job.
Depends on
Nothing — no dependency on #22 (candidate readiness) or #23 (automatic promotion for contested seats). Depends only on what already exists (
claim_speaker_seat,speaker_requests).Blocks
Nothing new. Removes one class of friction #23 would otherwise have left in place until it landed.