A work-in-progress chat bot for Streamplace with chat overlay functionality
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Working chat overlay yippie

+418 -379
+132
islands/ChatMessage.tsx
··· 1 + import { useEffect, useRef, useState } from "preact/hooks"; 2 + 3 + interface ChatMessageProps { 4 + data: EnrichedChatMessage; 5 + onExpire?: (id: string) => void; 6 + } 7 + 8 + interface RGB { 9 + red: number; 10 + green: number; 11 + blue: number; 12 + } 13 + 14 + // Customizable defaults 15 + const MESSAGE_BG_COLOR = "rgba(50, 50, 50, 0.5)"; 16 + const MESSAGE_BORDER_COLOR = "rgba(255, 255, 255, 0.3)"; 17 + const MESSAGE_BORDER_WIDTH = "2px"; 18 + const MESSAGE_BORDER_RADIUS = "8px"; 19 + const MESSAGE_BOX_SHADOW = "0 0 6px rgba(0, 0, 0, 0.5)"; 20 + const MESSAGE_SPACING = "8px"; 21 + const MESSAGE_TIMEOUT_SECONDS = 60; // Message lifetime in seconds 22 + const FADE_TIME_SECONDS = 1; 23 + const USE_BANNER_BACKGROUND = false; // Set to false to use solid color 24 + const BANNER_OPACITY = 0.50; // Opacity of banner background 25 + 26 + export default function ChatMessage({ data, onExpire }: ChatMessageProps) { 27 + const [isFadingOut, setIsFadingOut] = useState(false); 28 + const messageRef = useRef<HTMLDivElement>(null); 29 + 30 + useEffect(() => { 31 + // Handle message timeout 32 + if (MESSAGE_TIMEOUT_SECONDS <= 0) { 33 + return; 34 + } 35 + 36 + const timeoutId = setTimeout(() => { 37 + setIsFadingOut(true); 38 + setTimeout(() => { 39 + if (onExpire) { 40 + onExpire(data.id); 41 + } 42 + }, FADE_TIME_SECONDS * 1000); 43 + }, MESSAGE_TIMEOUT_SECONDS * 1000); 44 + 45 + return () => clearTimeout(timeoutId); 46 + }, [data.id, onExpire]); 47 + 48 + // Get colors from author profile 49 + const chatColor = data.author.color 50 + ? rgbaToString(data.author.color, 1) 51 + : "#ffffff"; 52 + const borderColor = data.author.color 53 + ? rgbaToString(data.author.color, 0.8) 54 + : MESSAGE_BORDER_COLOR; 55 + 56 + // Background: use banner if available, otherwise use color or default 57 + let backgroundStyle: Record<string, string> = {}; 58 + 59 + if (USE_BANNER_BACKGROUND && data.author.bannerUrl) { 60 + // Use banner as background with overlay 61 + const overlayColor = data.author.color 62 + ? rgbaToString(data.author.color, 0.3) 63 + : MESSAGE_BG_COLOR; 64 + 65 + backgroundStyle = { 66 + backgroundImage: 67 + `linear-gradient(${overlayColor}, ${overlayColor}), url(${data.author.bannerUrl})`, 68 + backgroundSize: "cover", 69 + backgroundPosition: "center", 70 + backgroundBlendMode: "overlay", 71 + }; 72 + } else if (data.author.color) { 73 + // Use solid color background 74 + backgroundStyle = { 75 + backgroundColor: rgbaToString(data.author.color, 0.5), 76 + }; 77 + } else { 78 + // Use default background 79 + backgroundStyle = { 80 + backgroundColor: MESSAGE_BG_COLOR, 81 + }; 82 + } 83 + 84 + return ( 85 + <div 86 + ref={messageRef} 87 + class={`chat-message ${isFadingOut ? "fading-out" : ""}`} 88 + data-message-id={data.id} 89 + style={{ 90 + ...backgroundStyle, 91 + border: `${MESSAGE_BORDER_WIDTH} solid ${borderColor}`, 92 + borderRadius: MESSAGE_BORDER_RADIUS, 93 + boxShadow: MESSAGE_BOX_SHADOW, 94 + }} 95 + > 96 + {data.author.avatarUrl && ( 97 + <img 98 + src={data.author.avatarUrl} 99 + alt={data.author.displayName || data.author.handle} 100 + class="chat-message-avatar" 101 + /> 102 + )} 103 + 104 + <div class="chat-message-content"> 105 + <div class="chat-message-header"> 106 + <span 107 + class="chat-message-name" 108 + style={{ color: chatColor }} 109 + > 110 + {data.author.displayName || data.author.handle} 111 + </span> 112 + {data.author.pronouns && data.author.pronouns.length > 0 && 113 + ( 114 + <span class="chat-message-pronouns"> 115 + {data.author.pronouns.join(" ")} 116 + </span> 117 + )} 118 + </div> 119 + 120 + <div class="chat-message-text"> 121 + {data.text} 122 + </div> 123 + </div> 124 + </div> 125 + ); 126 + } 127 + 128 + // Convert RGB to rgba() string with optional alpha 129 + export function rgbaToString(color: RGB, alpha: number = 1): string { 130 + const clampedAlpha = Math.max(0, Math.min(1, alpha)); 131 + return `rgba(${color.red}, ${color.green}, ${color.blue}, ${clampedAlpha})`; 132 + }
+108 -46
islands/ChatOverlay.tsx
··· 1 - import { useEffect, useRef, useState } from "preact/hooks"; 1 + import { useCallback, useEffect, useRef, useState } from "preact/hooks"; 2 + import ChatMessage from "./ChatMessage.tsx"; 2 3 3 - interface ChatOverlayProps { 4 - wsUrl: string; 5 - streamerHandle: Handle; 4 + interface ChatBoxProps { 5 + streamerHandle: string; 6 6 maxMessages?: number; 7 + backgroundColor?: string; 8 + borderColor?: string; 9 + borderWidth?: string; 10 + borderRadius?: string; 11 + boxShadow?: string; 7 12 } 8 13 9 - export function ChatOverlay({ 10 - wsUrl, 14 + export default function ChatBox({ 11 15 streamerHandle, 12 16 maxMessages = 50, 13 - }: ChatOverlayProps) { 17 + backgroundColor = "rgba(0, 0, 0, 0.0)", 18 + borderColor = "rgba(0, 0, 0, 0.0)", 19 + borderWidth = "2px", 20 + borderRadius = "12px", 21 + boxShadow = "0 0 10px rgba(0,0,0,0.0)", 22 + }: ChatBoxProps) { 14 23 const [messages, setMessages] = useState<EnrichedChatMessage[]>([]); 24 + const [pendingDeletes, setPendingDeletes] = useState< 25 + Array<{ id: string; timestamp: number }> 26 + >([]); 27 + const chatContainerRef = useRef<HTMLDivElement>(null); 15 28 const [connected, setConnected] = useState(false); 16 29 const [isClient, setIsClient] = useState(false); 17 30 const wsRef = useRef<WebSocket | null>(null); 18 - const messagesEndRef = useRef<HTMLDivElement>(null); 31 + 32 + // Prune old pending deletes (older than 60 seconds) 33 + const prunePendingDeletes = useCallback( 34 + (list: Array<{ id: string; timestamp: number }>) => { 35 + const now = Date.now(); 36 + return list.filter((item) => now - item.timestamp < 60000); 37 + }, 38 + [], 39 + ); 19 40 20 41 // This effect runs only on the client after hydration 21 42 useEffect(() => { 22 43 setIsClient(true); 23 44 }, []); 24 45 46 + // Handle message expiration 47 + const handleExpire = useCallback((id: string) => { 48 + setMessages((current) => current.filter((msg) => msg.id !== id)); 49 + }, []); 50 + 51 + // Memoize handleAddMessage to prevent recreating on every render 52 + const handleAddMessage = useCallback( 53 + (data: EnrichedChatMessage) => { 54 + // Filter empty messages 55 + if ( 56 + !data.text || typeof data.text !== "string" || 57 + !data.text.trim() 58 + ) { 59 + return; 60 + } 61 + 62 + // Check if message is in pending deletes 63 + setPendingDeletes((current) => { 64 + const pruned = prunePendingDeletes(current); 65 + const pendingIndex = pruned.findIndex((item) => 66 + item.id === data.id 67 + ); 68 + 69 + if (pendingIndex !== -1) { 70 + // Message was deleted before it arrived, don't add it 71 + const updated = [...pruned]; 72 + updated.splice(pendingIndex, 1); 73 + return updated; 74 + } 75 + 76 + return pruned; 77 + }); 78 + 79 + setMessages((current) => { 80 + // Check for duplicates 81 + if (current.some((msg) => msg.id === data.id)) { 82 + return current; 83 + } 84 + 85 + // Add new message and limit total messages 86 + const updated = [...current, data]; 87 + return updated.slice(-maxMessages); 88 + }); 89 + }, 90 + [maxMessages, prunePendingDeletes], 91 + ); 92 + 25 93 // This effect runs only when we're on the client 26 94 useEffect(() => { 27 95 if (!isClient) { 28 96 return; 29 97 } 30 98 99 + const protocol = globalThis.location.protocol === "https:" 100 + ? "wss:" 101 + : "ws:"; 102 + const wsUrl = 103 + `${protocol}//${globalThis.location.host}/api/websocket/${streamerHandle}`; 104 + 105 + console.log(`Attempting to connect to: ${wsUrl}`); 106 + 31 107 const ws = new WebSocket(wsUrl); 32 108 wsRef.current = ws; 33 109 ··· 39 115 ws.onmessage = (event) => { 40 116 try { 41 117 const data = JSON.parse(event.data); 118 + console.log("Received message:", data); 42 119 43 120 if (data.type === "chat_message") { 44 121 const message: EnrichedChatMessage = { ··· 46 123 timestamp: new Date(data.data.timestamp), 47 124 }; 48 125 49 - setMessages((prev) => { 50 - const newMessages = [...prev, message]; 51 - return newMessages.slice(-maxMessages); 52 - }); 126 + handleAddMessage(message); 127 + } else if (data.type === "subscribed") { 128 + console.log("Successfully subscribed to:", data.streamer); 53 129 } 54 130 } catch (error) { 55 131 console.error("Error parsing message:", error); 56 132 } 57 133 }; 58 134 59 - ws.onclose = () => { 60 - console.log("Disconnected from WebSocket"); 135 + ws.onclose = (event) => { 136 + console.log( 137 + "Disconnected from WebSocket:", 138 + event.code, 139 + event.reason, 140 + ); 61 141 setConnected(false); 62 142 }; 63 143 ··· 70 150 ws.close(); 71 151 } 72 152 }; 73 - }, [isClient, wsUrl, streamerHandle, maxMessages]); 74 - 75 - // Auto-scroll to bottom on new messages 76 - useEffect(() => { 77 - if (messages.length > 0) { 78 - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 79 - } 80 - }, [messages]); 153 + }, [isClient, streamerHandle, handleAddMessage]); 81 154 82 155 return ( 83 - <div class="chat-overlay"> 84 - <div class="chat-status"> 85 - {connected ? "🟢 Connected" : "🔴 Disconnected"} 86 - </div> 87 - <div class="chat-messages"> 88 - {messages.map((msg) => ( 89 - <div key={msg.id} class="chat-message"> 90 - <span 91 - class="chat-author" 92 - style={{ 93 - color: 94 - `rgb(${msg.color.red}, ${msg.color.green}, ${msg.color.blue})`, 95 - }} 96 - > 97 - {msg.author.displayName || msg.author.handle}: 98 - </span> 99 - <span class="chat-text">{msg.text}</span> 100 - {msg.isReply && ( 101 - <span class="chat-reply-indicator">↩️</span> 102 - )} 103 - </div> 104 - ))} 105 - <div ref={messagesEndRef} /> 106 - </div> 156 + <div 157 + ref={chatContainerRef} 158 + class="chat-box" 159 + style={{ 160 + backgroundColor, 161 + border: `${borderWidth} solid ${borderColor}`, 162 + borderRadius, 163 + boxShadow, 164 + }} 165 + > 166 + {messages.slice().reverse().map((msg) => ( 167 + <ChatMessage key={msg.id} data={msg} onExpire={handleExpire} /> 168 + ))} 107 169 </div> 108 170 ); 109 171 }
+38 -40
routes/api/websocket/[streamerHandle].ts
··· 1 1 import { define } from "../../../utils.ts"; 2 - import { StreamplaceWebSocketService } from "../../../utils/websocket.ts"; 2 + import { streamplaceWS } from "../../../utils/websocket.ts"; 3 3 import { resolveHandle } from "../../../utils/atcuteUtils.ts"; 4 4 5 - // Global WebSocket service instance 6 - let wsService: StreamplaceWebSocketService | null = null; 7 - 8 - function getWebSocketService() { 9 - if (!wsService) { 10 - wsService = new StreamplaceWebSocketService(); 11 - wsService.start(); 12 - } 13 - return wsService; 14 - } 15 - 16 5 export const handler = define.handlers({ 17 6 async GET(ctx) { 18 7 const { streamerHandle } = ctx.params; ··· 21 10 return new Response("WebSocket connections only", { status: 426 }); 22 11 } 23 12 24 - const { socket, response } = Deno.upgradeWebSocket(ctx.req); 25 - const wsService = getWebSocketService(); 26 - const streamerDid = await resolveHandle(streamerHandle as Handle); 13 + try { 14 + const { socket, response } = Deno.upgradeWebSocket(ctx.req); 15 + const streamerDid = await resolveHandle(streamerHandle as Handle); 27 16 28 - socket.onopen = () => { 29 - console.log(`New client connected for streamer: ${streamerHandle}`); 30 - // Store the streamerDid with the socket 31 - (socket as any).streamerDid = streamerDid; 32 - wsService.handleNewConnection(socket, streamerDid); 33 - }; 34 - 35 - socket.onmessage = (event) => { 36 - try { 37 - const message = JSON.parse(event.data); 38 - wsService.handleClientMessage(socket, message); 39 - } catch (error) { 40 - console.error("Error handling client message:", error); 41 - socket.send( 42 - JSON.stringify({ error: "Invalid message format" }), 17 + socket.onopen = () => { 18 + console.log( 19 + `✅ New websocket client connected for streamer: ${streamerHandle}`, 43 20 ); 44 - } 45 - }; 21 + // Store the streamerDid with the socket 22 + (socket as any).streamerDid = streamerDid; 23 + streamplaceWS.handleNewConnection(socket, streamerDid); 24 + }; 46 25 47 - socket.onclose = () => { 48 - console.log(`Client disconnected for streamer: ${streamerDid}`); 49 - wsService.removeClient(socket); 50 - }; 26 + socket.onmessage = (event) => { 27 + try { 28 + console.log(`Received client message: ${event.data}`); 29 + const message = JSON.parse(event.data); 30 + streamplaceWS.handleClientMessage(socket, message); 31 + } catch (error) { 32 + console.error("Error handling client message:", error); 33 + socket.send( 34 + JSON.stringify({ error: "Invalid message format" }), 35 + ); 36 + } 37 + }; 38 + 39 + socket.onclose = () => { 40 + console.log( 41 + `❎ Websocket client disconnected for streamer: ${streamerHandle}`, 42 + ); 43 + streamplaceWS.removeClient(socket); 44 + }; 51 45 52 - socket.onerror = (error) => { 53 - console.error("WebSocket error:", error); 54 - }; 46 + socket.onerror = (error) => { 47 + console.error("WebSocket error:", error); 48 + }; 55 49 56 - return response; 50 + return response; 51 + } catch (error) { 52 + console.error("Error upgrading WebSocket:", error); 53 + return new Response("Failed to upgrade WebSocket", { status: 500 }); 54 + } 57 55 }, 58 56 });
+5 -11
routes/chat/[streamerHandle].tsx
··· 1 - import { PageProps } from "fresh"; 2 - import { ChatOverlay } from "../../islands/ChatOverlay.tsx"; 1 + import { define } from "../../utils.ts"; 2 + import ChatOverlay from "../../islands/ChatOverlay.tsx"; 3 3 4 - export default function ChatPage(props: PageProps) { 5 - const { streamerHandle } = props.params; 6 - const wsUrl = typeof window !== "undefined" 7 - ? `${ 8 - location.protocol === "https:" ? "wss:" : "ws:" 9 - }//${location.host}/api/websocket/${streamerHandle}` 10 - : `ws://localhost:8000/api/websocket/${streamerHandle}`; 4 + export default define.page(function ChatPage(ctx) { 5 + const { streamerHandle } = ctx.params; 11 6 12 7 return ( 13 8 <div> 14 9 <ChatOverlay 15 - wsUrl={wsUrl} 16 10 streamerHandle={streamerHandle as Handle} 17 11 maxMessages={50} 18 12 /> 19 13 </div> 20 14 ); 21 - } 15 + });
+105
static/chat-message.css
··· 1 + .chat-message { 2 + /* Layout */ 3 + display: flex; 4 + align-items: flex-start; 5 + gap: 8px; 6 + padding: 10px 12px; 7 + margin-top: 6px; 8 + 9 + /* Text handling */ 10 + word-break: break-word; 11 + overflow-wrap: break-word; 12 + 13 + /* Transitions and animations */ 14 + opacity: 1; 15 + animation: slideUp 0.3s ease-out; 16 + 17 + /* Ensure content is visible over background */ 18 + position: relative; 19 + } 20 + 21 + /* Slide up animation for new messages */ 22 + @keyframes slideUp { 23 + from { 24 + opacity: 0; 25 + transform: translateY(20px); 26 + } 27 + to { 28 + opacity: 1; 29 + transform: translateY(0); 30 + } 31 + } 32 + 33 + /* Fade out animation for expiring messages */ 34 + .chat-message.fading-out { 35 + opacity: 0; 36 + transition: opacity 1s ease-out; 37 + } 38 + 39 + /* Avatar */ 40 + .chat-message-avatar { 41 + width: 40px; 42 + height: 40px; 43 + border-radius: 50%; 44 + flex-shrink: 0; 45 + object-fit: cover; 46 + border: 2px solid rgba(255, 255, 255, 0.2); 47 + background-color: rgba(0, 0, 0, 0.2); 48 + } 49 + 50 + /* Content container */ 51 + .chat-message-content { 52 + display: flex; 53 + flex-direction: column; 54 + flex: 1; 55 + min-width: 0; /* Allows text to wrap properly */ 56 + gap: 4px; 57 + } 58 + 59 + /* Header (name + pronouns) */ 60 + .chat-message-header { 61 + display: flex; 62 + align-items: baseline; 63 + gap: 8px; 64 + flex-wrap: wrap; 65 + } 66 + 67 + /* Name */ 68 + .chat-message-name { 69 + font-weight: bold; 70 + font-size: 1em; 71 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); 72 + line-height: 1.2; 73 + } 74 + 75 + /* Pronouns */ 76 + .chat-message-pronouns { 77 + font-size: 0.85em; 78 + opacity: 0.8; 79 + color: rgba(255, 255, 255, 0.9); 80 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); 81 + } 82 + 83 + /* Message text */ 84 + .chat-message-text { 85 + line-height: 1.4; 86 + color: rgba(255, 255, 255, 0.95); 87 + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); 88 + font-size: 0.95em; 89 + } 90 + 91 + /* Emotes in messages */ 92 + .chat-message-emote { 93 + width: 28px; 94 + height: auto; 95 + vertical-align: middle; 96 + display: inline-block; 97 + margin: 0 2px; 98 + } 99 + 100 + /* Badges (if you add them later) */ 101 + .chat-message-badge { 102 + width: 18px; 103 + height: 18px; 104 + margin-right: 4px; 105 + }
+18 -72
static/chat-overlay.css
··· 1 - * { 2 - margin: 0; 3 - padding: 0; 1 + .chat-box { 2 + /* Layout - fill entire viewport and stack messages from bottom */ 3 + display: flex; 4 + flex-direction: column-reverse; 5 + justify-content: flex-start; 6 + min-height: 100vh; 7 + max-height: 100vh; 8 + width: 100%; 9 + overflow: hidden; /* Messages can overflow at top */ 10 + padding: 12px; 4 11 box-sizing: border-box; 5 - } 6 12 7 - body { 13 + /* Typography */ 14 + font-size: 1em; 8 15 font-family: 9 - system-ui, 10 - -apple-system, 16 + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 17 + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 11 18 sans-serif; 12 - background: transparent; 13 - } 14 - 15 - .chat-overlay { 16 - width: 400px; 17 - height: 600px; 18 - display: flex; 19 - flex-direction: column; 20 - background: rgba(0, 0, 0, 0.7); 21 - color: white; 22 - padding: 10px; 23 - } 24 - 25 - .chat-status { 26 - font-size: 12px; 27 - padding: 5px; 28 - margin-bottom: 10px; 29 - background: rgba(0, 0, 0, 0.5); 30 - border-radius: 4px; 31 - } 32 - 33 - .chat-messages { 34 - flex: 1; 35 - overflow-y: auto; 36 - display: flex; 37 - flex-direction: column; 38 - gap: 8px; 39 - } 19 + -webkit-font-smoothing: antialiased; 20 + -moz-osx-font-smoothing: grayscale; 40 21 41 - .chat-message { 42 - padding: 4px 8px; 43 - background: rgba(0, 0, 0, 0.3); 44 - border-radius: 4px; 45 - word-wrap: break-word; 46 - } 47 - 48 - .chat-author { 49 - font-weight: bold; 50 - margin-right: 6px; 51 - } 52 - 53 - .chat-text { 54 - color: white; 55 - } 56 - 57 - .chat-reply-indicator { 58 - margin-left: 6px; 59 - font-size: 12px; 60 - } 61 - 62 - /* Scrollbar styling */ 63 - .chat-messages::-webkit-scrollbar { 64 - width: 6px; 65 - } 66 - 67 - .chat-messages::-webkit-scrollbar-track { 68 - background: rgba(0, 0, 0, 0.2); 69 - } 70 - 71 - .chat-messages::-webkit-scrollbar-thumb { 72 - background: rgba(255, 255, 255, 0.3); 73 - border-radius: 3px; 74 - } 75 - 76 - .chat-messages::-webkit-scrollbar-thumb:hover { 77 - background: rgba(255, 255, 255, 0.5); 22 + /* Ensure messages are at bottom */ 23 + gap: 0; 78 24 }
+1 -177
static/styles.css
··· 1 1 @import "chat-overlay.css"; 2 - 3 - *, 4 - *::before, 5 - *::after { 6 - box-sizing: border-box; 7 - } 8 - 9 - * { 10 - margin: 0; 11 - } 12 - 13 - button { 14 - color: inherit; 15 - } 16 - 17 - button, 18 - [role="button"] { 19 - cursor: pointer; 20 - } 21 - 22 - code { 23 - font-family: 24 - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", 25 - "Courier New", monospace; 26 - font-size: 1em; 27 - } 28 - 29 - img, 30 - svg { 31 - display: block; 32 - } 33 - 34 - img, 35 - video { 36 - max-width: 100%; 37 - height: auto; 38 - } 39 - 40 - html { 41 - line-height: 1.5; 42 - -webkit-text-size-adjust: 100%; 43 - font-family: 44 - ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", 45 - Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 46 - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 47 - "Noto Color Emoji"; 48 - } 49 - 50 - .transition-colors { 51 - transition-property: background-color, border-color, color, fill, stroke; 52 - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 53 - transition-duration: 150ms; 54 - } 55 - 56 - .my-6 { 57 - margin-bottom: 1.5rem; 58 - margin-top: 1.5rem; 59 - } 60 - 61 - .text-4xl { 62 - font-size: 2.25rem; 63 - line-height: 2.5rem; 64 - } 65 - 66 - .mx-2 { 67 - margin-left: 0.5rem; 68 - margin-right: 0.5rem; 69 - } 70 - 71 - .my-4 { 72 - margin-bottom: 1rem; 73 - margin-top: 1rem; 74 - } 75 - 76 - .mx-auto { 77 - margin-left: auto; 78 - margin-right: auto; 79 - } 80 - 81 - .px-4 { 82 - padding-left: 1rem; 83 - padding-right: 1rem; 84 - } 85 - 86 - .py-8 { 87 - padding-bottom: 2rem; 88 - padding-top: 2rem; 89 - } 90 - 91 - .bg-\[\#86efac\] { 92 - background-color: #86efac; 93 - } 94 - 95 - .text-3xl { 96 - font-size: 1.875rem; 97 - line-height: 2.25rem; 98 - } 99 - 100 - .py-6 { 101 - padding-bottom: 1.5rem; 102 - padding-top: 1.5rem; 103 - } 104 - 105 - .px-2 { 106 - padding-left: 0.5rem; 107 - padding-right: 0.5rem; 108 - } 109 - 110 - .py-1 { 111 - padding-bottom: 0.25rem; 112 - padding-top: 0.25rem; 113 - } 114 - 115 - .border-gray-500 { 116 - border-color: #6b7280; 117 - } 118 - 119 - .bg-white { 120 - background-color: #fff; 121 - } 122 - 123 - .flex { 124 - display: flex; 125 - } 126 - 127 - .gap-8 { 128 - grid-gap: 2rem; 129 - gap: 2rem; 130 - } 131 - 132 - .font-bold { 133 - font-weight: 700; 134 - } 135 - 136 - .max-w-screen-md { 137 - max-width: 768px; 138 - } 139 - 140 - .flex-col { 141 - flex-direction: column; 142 - } 143 - 144 - .items-center { 145 - align-items: center; 146 - } 147 - 148 - .justify-center { 149 - justify-content: center; 150 - } 151 - 152 - .border-2 { 153 - border-width: 2px; 154 - } 155 - 156 - .rounded-sm { 157 - border-radius: 0.25rem; 158 - } 159 - 160 - .hover\:bg-gray-200:hover { 161 - background-color: #e5e7eb; 162 - } 163 - 164 - .tabular-nums { 165 - font-variant-numeric: tabular-nums; 166 - } 167 - 168 - .min-h-screen { 169 - min-height: 100vh; 170 - } 171 - 172 - .fresh-gradient { 173 - background-color: rgb(134, 239, 172); 174 - background-image: linear-gradient(to right bottom, 175 - rgb(219, 234, 254), 176 - rgb(187, 247, 208), 177 - rgb(254, 249, 195)); 178 - } 2 + @import "chat-message.css";
+1 -1
utils/atcuteUtils.ts
··· 55 55 56 56 this.initialized = true; 57 57 console.log( 58 - `Streamplace client initialized for ${this.config.username}`, 58 + `Atproto client initialized for ${this.config.username}`, 59 59 ); 60 60 } 61 61
+6 -21
utils/globals.d.ts
··· 88 88 displayName?: string, 89 89 } 90 90 91 - // Label for display (pronouns, roles, etc.) 92 - interface MessageLabel { 91 + // User chat role 92 + interface Role { 93 93 text: string; 94 94 icon?: string; // SVG string or icon identifier 95 95 color?: string; // Hex color for label styling 96 - type?: "pronoun" | "role" | "badge" | "custom"; 96 + type?: "broadcaster" | "moderator" | "VIP" | "admin"; 97 97 } 98 98 99 99 // Enriched message for overlay display 100 100 interface EnrichedChatMessage { 101 101 id: string; 102 102 text: string; 103 - author: { 104 - did: Did; 105 - handle: Handle; 106 - displayName?: string; // Streamplace doesn't have display names yet but might one day 107 - }; 103 + author: UserProfile; 108 104 streamer: Did; 109 105 timestamp: Date; // Parsed createdAt 110 - color: { 111 - red: number; 112 - green: number; 113 - blue: number; 114 - }; 115 - labels?: MessageLabel[]; // Pronouns, roles, VIP status, etc. 116 - facets?: Array<{ 117 - features: unknown[]; 118 - index: { 119 - byteStart: number; 120 - byteEnd: number; 121 - }; 122 - }>; // Keep facets for mentions, links, etc. 106 + roles?: Role[]; 107 + facets?: Facet[]; 123 108 isReply?: boolean; // Simplified reply indicator 124 109 }
+2 -1
utils/streamplaceBot.ts
··· 54 54 // Register default commands 55 55 this.registerDefaultCommands(); 56 56 57 + const streamer = await this.getUserProfile(this.streamerDid); 57 58 console.log( 58 - `StreamplaceBot initialized for streamer: ${this.streamerDid}`, 59 + `StreamplaceBot initialized for streamer: ${streamer.handle}`, 59 60 ); 60 61 } 61 62
+2 -10
utils/websocket.ts
··· 90 90 } 91 91 this.streamerClients.get(message.streamer)!.add(client); 92 92 93 - console.log( 94 - `Client subscribed to streamer: ${message.streamer}`, 95 - ); 96 93 client.send(JSON.stringify({ 97 94 type: "subscribed", 98 95 streamer: message.streamer, ··· 181 178 return { 182 179 id: jetstreamMessage.commit.cid, 183 180 text: record.text, 184 - author: { 185 - did: jetstreamMessage.did, 186 - handle: profile.handle, 187 - // displayName if implemented 188 - }, 181 + author: profile, 189 182 timestamp: new Date(record.createdAt), 190 - color: profile.color!, 191 - facets: record.facets, 183 + facets: undefined,//record.facets || undefined, 192 184 isReply: !!record.reply, 193 185 streamer: record.streamer, 194 186 };