mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
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.
57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
'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>
|
|
);
|
|
}
|