multiple relayers

This commit is contained in:
ajvpot
2025-07-07 00:00:00 +00:00
parent b6b667d5b3
commit a482b1c8c0
6 changed files with 70 additions and 36 deletions
+3 -3
View File
@@ -65,8 +65,8 @@ export default function MessagesPage() {
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
// Only prepend messages that are not already present
const existingKeys = new Set(messages.map((m) => m.ingest_timestamp + m.origin));
const newMessages = data.filter((msg: any) => !existingKeys.has(msg.ingest_timestamp + msg.origin));
const existingKeys = new Set(messages.map((m) => m.ingest_timestamp + (m.origins?.join(',') ?? '')));
const newMessages = data.filter((msg: any) => !existingKeys.has(msg.ingest_timestamp + (msg.origins?.join(',') ?? '')));
if (newMessages.length > 0) {
setMessages((prev) => [...newMessages, ...prev]);
}
@@ -131,7 +131,7 @@ export default function MessagesPage() {
<div className="text-gray-400 text-center py-8">No chat messages found.</div>
)}
{messages.map((msg, i) => (
<ChatMessageItem key={msg.ingest_timestamp + msg.origin + i} msg={msg} showErrorRow showChannelId={true} />
<ChatMessageItem key={msg.ingest_timestamp + (msg.origins?.join(',') ?? '') + i} msg={msg} showErrorRow showChannelId={true} />
))}
{loading && (
<div className="text-center py-4 text-gray-400">Loading...</div>
+1 -1
View File
@@ -122,7 +122,7 @@ export default function ChatBox() {
<div className="text-gray-400 text-center mt-8">No chat messages found.</div>
)}
{messages.map((msg, i) => (
<ChatMessageItem key={msg.ingest_timestamp + msg.origin + i} msg={msg} />
<ChatMessageItem key={msg.ingest_timestamp + (msg.origins?.join(',') ?? '') + i} msg={msg} />
))}
{hasMore && (
<button
+4 -4
View File
@@ -5,7 +5,7 @@ import { decryptMeshcoreGroupMessage } from "../lib/meshcore_decrypt";
export interface ChatMessage {
ingest_timestamp: string;
origin: string;
origins: string[];
mesh_timestamp: string;
packet: string;
path_len: number;
@@ -71,7 +71,7 @@ export default function ChatMessageItem({ msg, showErrorRow, showChannelId }: {
{parsed.sender && ": "}
<span>{parsed.text}</span>
</div>
<div className="text-xs text-gray-300">Relayed by: {msg.origin}</div>
<div className="text-xs text-gray-300">Relayed by: {msg.origins && msg.origins.length > 0 ? msg.origins.map(o => o.slice(0, 6)).join(", ") : "-"}</div>
</div>
);
}
@@ -86,7 +86,7 @@ export default function ChatMessageItem({ msg, showErrorRow, showChannelId }: {
<span className="text-xs text-purple-600 dark:text-purple-300 ml-2">channel: {msg.channel_hash}</span>
)}
</div>
<div className="text-xs text-gray-300">Relayed by: {msg.origin}</div>
<div className="text-xs text-gray-300">Relayed by: {msg.origins && msg.origins.length > 0 ? msg.origins.map(o => o.slice(0, 6)).join(", ") : "-"}</div>
</div>
);
}else{
@@ -103,7 +103,7 @@ export default function ChatMessageItem({ msg, showErrorRow, showChannelId }: {
)}
</div>
<div className="w-full h-5 bg-gray-200 dark:bg-neutral-800 rounded animate-pulse my-2" />
<div className="text-xs text-gray-300">Relayed by: {msg.origin}</div>
<div className="text-xs text-gray-300">Relayed by: {msg.origins && msg.origins.length > 0 ? msg.origins.map(o => o.slice(0, 6)).join(", ") : "-"}</div>
</div>
);
}
+12
View File
@@ -15,6 +15,7 @@ export type Config = {
clustering?: boolean; // add clustering toggle
showNodeNames?: boolean; // add show node names toggle
meshcoreKeys?: MeshcoreKey[]; // meshcore private keys
showMeshcoreCoverageOverlay?: boolean; // meshcore overlay toggle
};
const TILE_LAYERS = [
@@ -30,6 +31,7 @@ const DEFAULT_CONFIG: Config = {
clustering: true, // default to clustering enabled
showNodeNames: true, // default to show node names
meshcoreKeys: [], // default empty
showMeshcoreCoverageOverlay: false, // meshcore overlay default
};
const LAST_SEEN_OPTIONS = [
@@ -214,6 +216,16 @@ function ConfigPopover({ config, setConfig, onClose, anchorRef, onOpenKeyModal }
<span>Show node names</span>
</label>
</div>
<div className="mb-2">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={config.showMeshcoreCoverageOverlay === true}
onChange={e => setConfig({ ...config, showMeshcoreCoverageOverlay: e.target.checked })}
/>
<span>Show meshcore coverage overlay</span>
</label>
</div>
<div className="mb-2">
<button
className="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 w-full"
+12
View File
@@ -397,6 +397,18 @@ export default function MapView() {
maxZoom={selectedTileLayer.maxZoom}
{...(selectedTileLayer.subdomains ? { subdomains: selectedTileLayer.subdomains } : {})}
/>
{config?.showMeshcoreCoverageOverlay && (
<TileLayer
url="https://tiles.w0z.is/tiles/{z}/{x}/{y}.png"
attribution="Meshcore Coverage &copy; <a href='https://w0z.is/'>w0z.is</a>"
minZoom={1}
maxZoom={22}
minNativeZoom={11}
maxNativeZoom={11}
zIndex={1000}
opacity={0.7}
/>
)}
<ClusteredMarkers nodes={nodePositions} />
</MapContainer>
</div>
+38 -28
View File
@@ -2,6 +2,7 @@
import { clickhouse } from "./clickhouse";
export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen }: { minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[], lastSeen?: string | null } = {}) {
try {
let where = [
"latitude IS NOT NULL",
"longitude IS NOT NULL"
@@ -43,36 +44,45 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
last_seen: string;
type: string;
}>;
} catch (error) {
console.error('ClickHouse error in getNodePositions:', error);
throw error;
}
}
export async function getLatestChatMessages({ limit = 20, before, after, channelId }: { limit?: number, before?: string, after?: string, channelId?: string } = {}) {
let where = [];
const params: Record<string, any> = { limit };
if (before) {
where.push('ingest_timestamp < {before:DateTime64}');
params.before = before;
try {
let where = [];
const params: Record<string, any> = { limit };
if (before) {
where.push('ingest_timestamp < {before:DateTime64}');
params.before = before;
}
if (after) {
where.push('ingest_timestamp > {after:DateTime64}');
params.after = after;
}
if (channelId) {
where.push('channel_hash = {channelId:String}');
params.channelId = channelId;
}
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
const query = `SELECT ingest_timestamp, origins, 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;
origins: string[];
mesh_timestamp: string;
packet: string;
path_len: number;
path: string;
channel_hash: string;
mac: string;
encrypted_message: string;
}>;
} catch (error) {
console.error('ClickHouse error in getLatestChatMessages:', error);
throw error;
}
if (after) {
where.push('ingest_timestamp > {after:DateTime64}');
params.after = after;
}
if (channelId) {
where.push('channel_hash = {channelId:String}');
params.channelId = channelId;
}
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;
}>;
}