mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
Rewrite dashboard and overlay, single /events stream, static client served by the backend
Backend: one SSE stream for messages, selection, poll and connection status; reconnect with backoff; no on-disk Innertube session cache; mock only with MOCK_CHAT=1; binds to 127.0.0.1; serves client/out so production runs on one port. Full URLs for links YouTube truncates in chat. Client: components split out, Lucide icons, stream title and status, on-air strip, search, pause, keyboard shortcuts, overlay settings dialog. Overlay themes, position, size, animation and auto-hide via URL, sized for 1080p and scaled with the source. New blueprint theme for the brand background. Removed Tailwind, better-sqlite3, zod, the timezone helpers, Cline memory bank and AGENTS.md. Added ESLint, node:test tests and CI.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowDown } from 'lucide-react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import type { ChatMessage } from '@shared/chat';
|
||||
|
||||
type Props = {
|
||||
messages: ChatMessage[];
|
||||
paused?: boolean;
|
||||
render: (message: ChatMessage) => ReactNode;
|
||||
empty: string;
|
||||
};
|
||||
|
||||
const BOTTOM_THRESHOLD = 80;
|
||||
|
||||
/**
|
||||
* Scroll container that follows new messages while the user is at the bottom.
|
||||
* Scrolling up stops following and shows a jump button with the unseen count.
|
||||
* While paused the list is frozen at its current contents.
|
||||
*/
|
||||
export function ChatList({ messages, paused = false, render, empty }: Props) {
|
||||
const scroller = useRef<HTMLDivElement>(null);
|
||||
const following = useRef(true);
|
||||
const [unseen, setUnseen] = useState(0);
|
||||
const [frozen, setFrozen] = useState<ChatMessage[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFrozen(paused ? messages : null);
|
||||
// Only the pause flag should snapshot; message updates while paused must not refresh the snapshot.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paused]);
|
||||
|
||||
const shown = frozen ?? messages;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const node = scroller.current;
|
||||
if (!node) return;
|
||||
if (following.current) {
|
||||
node.scrollTop = node.scrollHeight;
|
||||
setUnseen(0);
|
||||
} else {
|
||||
setUnseen((count) => count + 1);
|
||||
}
|
||||
}, [shown]);
|
||||
|
||||
const onScroll = () => {
|
||||
const node = scroller.current;
|
||||
if (!node) return;
|
||||
const atBottom = node.scrollHeight - node.scrollTop - node.clientHeight <= BOTTOM_THRESHOLD;
|
||||
following.current = atBottom;
|
||||
if (atBottom) setUnseen(0);
|
||||
};
|
||||
|
||||
const jump = () => {
|
||||
const node = scroller.current;
|
||||
if (!node) return;
|
||||
following.current = true;
|
||||
node.scrollTop = node.scrollHeight;
|
||||
setUnseen(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="list">
|
||||
<div className="list__scroll" ref={scroller} onScroll={onScroll}>
|
||||
{shown.length ? shown.map(render) : <p className="list__empty">{empty}</p>}
|
||||
</div>
|
||||
{unseen > 0 && !following.current && (
|
||||
<button type="button" className="list__jump" onClick={jump}>
|
||||
<ArrowDown size={14} />
|
||||
{unseen} new
|
||||
</button>
|
||||
)}
|
||||
{paused && frozen && messages.length > frozen.length && (
|
||||
<span className="list__paused">Paused, {messages.length - frozen.length} waiting</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user