mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 19:06:14 +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,56 @@
|
||||
'use client';
|
||||
|
||||
import type { ChatMessage, MessageRun } from '@shared/chat';
|
||||
import { proxyImageUrl } from '../lib/imageProxy';
|
||||
import { splitLinks } from '../lib/format';
|
||||
|
||||
type Props = {
|
||||
message: ChatMessage;
|
||||
className?: string;
|
||||
/** When given, links are clickable and call this instead of navigating. When omitted links render as text. */
|
||||
onLink?: (url: string) => void;
|
||||
};
|
||||
|
||||
function runsOf(message: ChatMessage): MessageRun[] {
|
||||
if (message.runs?.length) return message.runs;
|
||||
if (message.text && message.text !== 'N/A') return [{ text: message.text }];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function MessageBody({ message, className, onLink }: Props) {
|
||||
const runs = runsOf(message);
|
||||
if (!runs.length) return null;
|
||||
|
||||
const link = (url: string, label: string, key: number) =>
|
||||
onLink ? (
|
||||
<a
|
||||
key={key}
|
||||
href={url}
|
||||
className="link"
|
||||
title={url}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onLink(url);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<span key={key} className="link">{label}</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<p className={className}>
|
||||
{runs.map((run, i) => {
|
||||
if (run.emojiUrl) {
|
||||
return <img key={i} src={proxyImageUrl(run.emojiUrl)} alt={run.emojiAlt || 'emoji'} className="emoji" loading="lazy" />;
|
||||
}
|
||||
if (run.url) return link(run.url, run.text || run.url, i);
|
||||
return splitLinks(run.text ?? '').map((part, j) =>
|
||||
part.type === 'link' ? link(part.content, part.content, i * 1000 + j) : <span key={i * 1000 + j}>{part.content}</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user