mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-07-06 17:50:58 +02:00
messages
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<ChatMessage[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [lastBefore, setLastBefore] = useState<string | undefined>(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 (
|
||||
<div
|
||||
className={`w-80 bg-white dark:bg-neutral-900 rounded-lg shadow-lg flex flex-col ${
|
||||
@@ -26,8 +94,30 @@ export default function ChatBox() {
|
||||
</button>
|
||||
</div>
|
||||
{!minimized && (
|
||||
<div className="flex-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200">
|
||||
<div className="text-gray-400 text-center mt-8">Chat coming soon...</div>
|
||||
<div className="flex-1 overflow-y-auto text-sm text-gray-700 dark:text-gray-200 flex flex-col-reverse">
|
||||
<div className="flex flex-col-reverse gap-2">
|
||||
{messages.length === 0 && !loading && (
|
||||
<div className="text-gray-400 text-center mt-8">No chat messages found.</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.ingest_timestamp + msg.origin + i} className="border-b border-gray-200 dark:border-neutral-800 pb-2 mb-2">
|
||||
<div className="text-xs text-gray-400 flex items-center gap-2">
|
||||
{formatLocalTime(msg.ingest_timestamp)} <span className="text-xs text-blue-600 dark:text-blue-400 font-mono">{msg.channel_hash}</span>
|
||||
</div>
|
||||
<div className="font-mono break-all whitespace-pre-wrap">{formatHex(msg.encrypted_message)}</div>
|
||||
<div className="text-xs text-gray-300">from: {msg.origin}</div>
|
||||
</div>
|
||||
))}
|
||||
{hasMore && (
|
||||
<button
|
||||
className="w-full py-2 bg-gray-100 dark:bg-neutral-800 rounded text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-neutral-700 mt-2"
|
||||
onClick={handleLoadMore}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Load more"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<string, any> = { 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;
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user