diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..9a8cebf --- /dev/null +++ b/src/app/api/chat/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { getLatestChatMessages } from "@/lib/clickhouse/actions"; + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const limit = parseInt(searchParams.get("limit") || "20", 10); + const before = searchParams.get("before") || undefined; + const messages = await getLatestChatMessages({ limit, before }); + return NextResponse.json(messages); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch chat messages" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index fb6be61..7498078 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -1,10 +1,78 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"; +interface ChatMessage { + ingest_timestamp: string; + origin: string; + mesh_timestamp: string; + packet: string; + path_len: number; + path: string; + channel_hash: string; + mac: string; + encrypted_message: string; +} + +const PAGE_SIZE = 20; + +function formatHex(hex: string): string { + // Add a space every 2 characters for readability + return hex.replace(/(.{2})/g, "$1 ").trim(); +} + +function formatLocalTime(utcString: string): string { + // Parse as UTC and display in local time + const utcDate = new Date(utcString + (utcString.endsWith('Z') ? '' : 'Z')); + return utcDate.toLocaleString(); +} + export default function ChatBox() { - const [value, setValue] = useState(""); const [minimized, setMinimized] = useState(true); + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [lastBefore, setLastBefore] = useState(undefined); + + useEffect(() => { + if (!minimized) { + setMessages([]); + setHasMore(true); + setLastBefore(undefined); + fetchMessages(undefined, true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [minimized]); + + const fetchMessages = async (before?: string, replace = false) => { + setLoading(true); + try { + let url = `/api/chat?limit=${PAGE_SIZE}`; + if (before) url += `&before=${encodeURIComponent(before)}`; + const res = await fetch(url); + const data = await res.json(); + if (Array.isArray(data)) { + setMessages((prev) => replace ? data : [...prev, ...data]); + setHasMore(data.length === PAGE_SIZE); + if (data.length > 0) { + setLastBefore(data[data.length - 1].ingest_timestamp); + } + } else { + setHasMore(false); + } + } catch { + setHasMore(false); + } finally { + setLoading(false); + } + }; + + const handleLoadMore = () => { + if (lastBefore) { + fetchMessages(lastBefore); + } + }; + return (
{!minimized && ( -
-
Chat coming soon...
+
+
+ {messages.length === 0 && !loading && ( +
No chat messages found.
+ )} + {messages.map((msg, i) => ( +
+
+ {formatLocalTime(msg.ingest_timestamp)} {msg.channel_hash} +
+
{formatHex(msg.encrypted_message)}
+
from: {msg.origin}
+
+ ))} + {hasMore && ( + + )} +
)}
diff --git a/src/lib/clickhouse/actions.ts b/src/lib/clickhouse/actions.ts index c35f527..2ff615b 100644 --- a/src/lib/clickhouse/actions.ts +++ b/src/lib/clickhouse/actions.ts @@ -43,4 +43,28 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp last_seen: string; type: string; }>; +} + +export async function getLatestChatMessages({ limit = 20, before }: { limit?: number, before?: string } = {}) { + let where = []; + const params: Record = { limit }; + if (before) { + where.push('ingest_timestamp < {before:DateTime64}'); + params.before = before; + } + const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; + const query = `SELECT ingest_timestamp, origin, mesh_timestamp, packet, path_len, path, channel_hash, mac, hex(encrypted_message) AS encrypted_message FROM meshcore_public_channel_messages ${whereClause} ORDER BY ingest_timestamp DESC LIMIT {limit:UInt32}`; + const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' }); + const rows = await resultSet.json(); + return rows as Array<{ + ingest_timestamp: string; + origin: string; + mesh_timestamp: string; + packet: string; + path_len: number; + path: string; + channel_hash: string; + mac: string; + encrypted_message: string; + }>; } \ No newline at end of file