feat: add YouTube live poll support

Add backend support for YouTube live polls including:
- Poll event handling and state tracking
- New `/poll/current` endpoint for current poll data
- Server-sent events stream at `/poll/stream` for real-time updates
- Poll emitter for broadcasting poll changes to clients
This commit is contained in:
Yusuf İpek
2025-10-12 23:04:57 +03:00
parent 81d4df5ff7
commit 4a4b2407b6
8 changed files with 165 additions and 22 deletions
+41 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import type { ChatMessage } from '@shared/chat';
import type { ChatMessage, Poll } from '@shared/chat';
import { useTimezone } from '../../lib/TimezoneContext';
import { formatTimestamp } from '../../lib/timezone';
import { proxyImageUrl } from '../../lib/imageProxy';
@@ -46,6 +46,7 @@ let globalConnectionListeners: Set<(payload: any) => void> = new Set();
export default function DashboardPage() {
const { messages, refresh, error: pollError } = useChatMessages();
const { selection, status: overlayStatus } = useOverlaySelection();
const { poll: currentPoll } = usePoll();
const { connected, liveId, connect, disconnect, connecting } = useConnection();
const [confirmUrl, setConfirmUrl] = useState<string | null>(null);
const [superChatPulse, setSuperChatPulse] = useState(false);
@@ -198,6 +199,12 @@ export default function DashboardPage() {
{connected && (
<>
<header className="dashboard__tabs">
{currentPoll && (
<div className="poll-indicator" title="Active poll on YouTube">
<span className="poll-indicator__icon">📊</span>
<span className="poll-indicator__text">Active Poll</span>
</div>
)}
<div className="tab">
Messages <span className="tab__count">{regularMessages.length}</span>
</div>
@@ -313,6 +320,7 @@ export default function DashboardPage() {
</div>
</div>
)}
</main>
);
}
@@ -455,6 +463,38 @@ function useConnection() {
return { connected, liveId, connect, disconnect, connecting };
}
function usePoll() {
const [poll, setPoll] = useState<Poll | null>(null);
useEffect(() => {
// Create SSE connection for polls
const eventSource = new EventSource(`${BACKEND_URL}/poll/stream`);
eventSource.addEventListener('poll', ((event: MessageEvent) => {
try {
const payload = JSON.parse(event.data);
setPoll(payload.poll);
} catch (error) {
console.error('Failed to parse poll payload', error);
}
}) as EventListener);
eventSource.addEventListener('heartbeat', () => {
// Heartbeat - connection is alive
});
eventSource.onerror = (error) => {
console.error('[Dashboard] Poll SSE connection error', error);
};
return () => {
eventSource.close();
};
}, []);
return { poll };
}
type ConnectionControlProps = {
connected: boolean;
liveId: string | null;
+33
View File
@@ -1357,3 +1357,36 @@ main {
background: rgba(248, 113, 113, 0.2);
color: #f87171;
}
/* Poll indicator */
.poll-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
background: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3);
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
color: #c4b5fd;
animation: pollPulse 2s ease-in-out infinite;
margin-right: 0.5rem;
}
.poll-indicator__icon {
font-size: 1rem;
}
.poll-indicator__text {
white-space: nowrap;
}
@keyframes pollPulse {
0%, 100% {
box-shadow: 0 0 0 rgba(139, 92, 246, 0);
}
50% {
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
}
}