feat(live): polish — ended-state, chat, viewer actions, Private option
Restores the live streaming controllers / views / model from the
pre-audit WIP that got stashed on 2026-10-21, then applies the Phase 6
polish items:
## /live/new form
Add a Private visibility option that mirrors the upload modal's
wording — "Only you can see this stream. Perfect for testing without
an audience." Validator in LiveStreamController@store now permits
public|unlisted|private (was public|unlisted).
## Watch page ended-state
When the stream status is "ended", the overlay is a solid "Stream
ended" card showing the stream's duration (started_at → ended_at)
and, when a recording was published, a Watch Replay CTA linking to
the saved video. Previously an ended stream showed the generic
"Waiting for the broadcast to start" placeholder forever.
## Channel name link
The uploader's name in the meta row is now a real <a href> to the
channel page.
## Viewer action row
Subscribe + Share buttons below the player, visible to anyone except
the owner. Subscribe for guests uses the Phase 7 ?intended= pattern
so after sign-in they land back on the stream.
## Live chat (DB-backed polling)
New live_chat_messages table (migration 2026_10_21_000003). Viewers
GET /live/{stream}/chat?since=N every 3 seconds; signed-in users POST
to the same path (rate-limited at 60/min). Guests see the panel in
read-only mode with a "Sign in to chat" placeholder.
Private streams reject chat for anyone but the owner (404). Non-live
status rejects chat sends (422). Author name is snapshotted at post
time so a renamed account's old messages keep the name they used.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
639279f2a2
commit
5bf1128196
12 files changed
+2103
No files matched your search
@@ -0,0 +1,95 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Live')
|
||||
|
||||
@section('extra_styles')
|
||||
<style>
|
||||
.li-wrap { max-width: 1300px; margin: 0 auto; padding: 16px; }
|
||||
.li-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.li-head h1 { font-size: 22px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
|
||||
.li-head h1 .dot { width: 9px; height: 9px; border-radius: 50%; background: #ef4444; animation: liPulse 1.4s ease-in-out infinite; }
|
||||
@keyframes liPulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
|
||||
|
||||
.li-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
||||
|
||||
.li-card { display: block; text-decoration: none; color: inherit; border-radius: 14px; overflow: hidden; background: var(--yt-bg-secondary, #181818); border: 1px solid rgba(255,255,255,.08); transition: transform .15s ease, border-color .15s ease; }
|
||||
.li-card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.2); }
|
||||
.li-thumb { position: relative; aspect-ratio: 16/9; background: linear-gradient(135deg, #1f2937, #111827); display: grid; place-items: center; }
|
||||
.li-thumb i { font-size: 32px; opacity: .3; }
|
||||
.li-live { position: absolute; top: 8px; left: 8px; background: #ef4444; color: #fff; font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 4px; letter-spacing: .04em; }
|
||||
.li-viewers { position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,.75); color: #fff; font-size: 11px; padding: 3px 7px; border-radius: 4px; }
|
||||
.li-body { padding: 12px 14px; }
|
||||
.li-body h3 { font-size: 14px; font-weight: 600; margin: 0 0 5px; line-height: 1.35; }
|
||||
.li-body p { font-size: 12px; opacity: .6; margin: 0; }
|
||||
|
||||
.li-empty { text-align: center; padding: 60px 20px; opacity: .55; }
|
||||
.li-empty i { font-size: 40px; display: block; margin-bottom: 14px; opacity: .5; }
|
||||
|
||||
.li-section-title { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; opacity: .55; margin: 34px 0 14px; }
|
||||
.li-status { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; background: rgba(255,255,255,.1); }
|
||||
.li-status.live { background: #ef4444; color: #fff; }
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="li-wrap">
|
||||
|
||||
<div class="li-head">
|
||||
<h1><span class="dot"></span> Live now</h1>
|
||||
@auth
|
||||
<a class="action-btn action-btn-primary" href="{{ route('live.create') }}">
|
||||
<i class="bi bi-broadcast"></i> <span>Go Live</span>
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@if ($live->isEmpty())
|
||||
<div class="li-empty">
|
||||
<i class="bi bi-broadcast"></i>
|
||||
<div>Nobody is streaming right now.</div>
|
||||
@auth<div style="margin-top:8px;font-size:13px;">Be the first — hit <strong>Go Live</strong>.</div>@endauth
|
||||
</div>
|
||||
@else
|
||||
<div class="li-grid">
|
||||
@foreach ($live as $s)
|
||||
<a class="li-card" href="{{ route('live.watch', $s) }}">
|
||||
<div class="li-thumb">
|
||||
<i class="bi bi-broadcast"></i>
|
||||
<span class="li-live">LIVE</span>
|
||||
<span class="li-viewers"><i class="bi bi-eye"></i> {{ $s->current_viewers }}</span>
|
||||
</div>
|
||||
<div class="li-body">
|
||||
<h3>{{ $s->title }}</h3>
|
||||
<p>{{ $s->user->name ?? 'Unknown' }}</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@auth
|
||||
@if ($mine->isNotEmpty())
|
||||
<div class="li-section-title">Your streams</div>
|
||||
<div class="li-grid">
|
||||
@foreach ($mine as $s)
|
||||
<a class="li-card" href="{{ $s->status === 'ended' ? route('live.watch', $s) : route('live.broadcast', $s) }}">
|
||||
<div class="li-thumb">
|
||||
<i class="bi bi-{{ $s->isLive() ? 'broadcast' : 'camera-video-off' }}"></i>
|
||||
@if ($s->isLive())<span class="li-live">LIVE</span>@endif
|
||||
</div>
|
||||
<div class="li-body">
|
||||
<h3>{{ $s->title }}</h3>
|
||||
<p>
|
||||
<span class="li-status {{ $s->isLive() ? 'live' : '' }}">{{ ucfirst($s->status) }}</span>
|
||||
@if ($s->started_at)
|
||||
· {{ $s->formatted_duration }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
@endsection
|
||||
Reference in new issue
Block a user