mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
maybe fix chat
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore";
|
||||
@@ -51,6 +51,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [lastBefore, setLastBefore] = useState<string | undefined>(undefined);
|
||||
const messagesRef = useRef<ChatMessage[]>([]);
|
||||
|
||||
const selectedKey = allTabs[selectedTab];
|
||||
const channelId = selectedKey.isAllMessages ? undefined : getChannelIdFromKey(selectedKey.privateKey).toUpperCase();
|
||||
@@ -58,6 +59,59 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
|
||||
// Only show tabs if more than one channel (or if we have all messages tab)
|
||||
const showTabs = allTabs.length > 1;
|
||||
|
||||
// Update ref when messages change
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages;
|
||||
}, [messages]);
|
||||
|
||||
const fetchMessages = useCallback(async (before?: string, replace = false, fetchNewer = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = `/api/chat?limit=${PAGE_SIZE}`;
|
||||
if (channelId) url += `&channel_id=${channelId}`;
|
||||
|
||||
if (fetchNewer) {
|
||||
// Fetch newer messages using the most recent message timestamp
|
||||
const mostRecentTimestamp = messagesRef.current.length > 0 ? messagesRef.current[0].ingest_timestamp : undefined;
|
||||
if (mostRecentTimestamp) {
|
||||
// Ensure we use the exact UTC timestamp format for the API
|
||||
url += `&after=${encodeURIComponent(mostRecentTimestamp)}`;
|
||||
}
|
||||
} else if (before) {
|
||||
// Fetch older messages using before parameter
|
||||
url += `&before=${encodeURIComponent(before)}`;
|
||||
}
|
||||
|
||||
const res = await fetch(buildApiUrl(url));
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
if (fetchNewer && data.length > 0) {
|
||||
// Add newer messages to the beginning (most recent first)
|
||||
setMessages((prev) => [...data, ...prev]);
|
||||
} else {
|
||||
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 (error) {
|
||||
// Only set hasMore to false if we don't have a lastBefore value (can't load more)
|
||||
if (!lastBefore) {
|
||||
setHasMore(false);
|
||||
}
|
||||
if (fetchNewer) {
|
||||
// Silently fail for auto-refresh
|
||||
console.error('Auto-refresh failed:', error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [channelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!minimized) {
|
||||
setMessages([]);
|
||||
@@ -79,57 +133,6 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
|
||||
}
|
||||
}, [minimized, channelId]);
|
||||
|
||||
const fetchMessages = async (before?: string, replace = false, fetchNewer = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = `/api/chat?limit=${PAGE_SIZE}`;
|
||||
if (channelId) url += `&channel_id=${channelId}`;
|
||||
|
||||
if (fetchNewer) {
|
||||
// Fetch newer messages using the most recent message timestamp
|
||||
const mostRecentTimestamp = messages.length > 0 ? messages[0].ingest_timestamp : undefined;
|
||||
if (mostRecentTimestamp) {
|
||||
// Ensure we use the exact UTC timestamp format for the API
|
||||
url += `&after=${encodeURIComponent(mostRecentTimestamp)}`;
|
||||
}
|
||||
} else if (before) {
|
||||
// Fetch older messages using before parameter
|
||||
url += `&before=${encodeURIComponent(before)}`;
|
||||
}
|
||||
|
||||
const res = await fetch(buildApiUrl(url));
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
if (fetchNewer && data.length > 0) {
|
||||
// Filter out any duplicate messages by ingest_timestamp to prevent duplicates
|
||||
const existingTimestamps = new Set(messages.map(msg => msg.ingest_timestamp));
|
||||
const newMessages = data.filter(msg => !existingTimestamps.has(msg.ingest_timestamp));
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
// Add newer messages to the beginning (most recent first)
|
||||
setMessages((prev) => [...newMessages, ...prev]);
|
||||
}
|
||||
} else {
|
||||
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 (error) {
|
||||
setHasMore(false);
|
||||
if (fetchNewer) {
|
||||
// Silently fail for auto-refresh
|
||||
console.error('Auto-refresh failed:', error);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (lastBefore) {
|
||||
fetchMessages(lastBefore);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useConfig } from "./ConfigContext";
|
||||
import { decryptMeshcoreGroupMessage } from "../lib/meshcore";
|
||||
|
||||
@@ -26,10 +26,11 @@ function formatLocalTime(utcString: string): string {
|
||||
|
||||
export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow?: boolean }) {
|
||||
const { config } = useConfig();
|
||||
const knownKeys = [
|
||||
const knownKeys = useMemo(() => [
|
||||
...(config?.meshcoreKeys?.map((k: any) => k.privateKey) || []),
|
||||
"izOH6cXN6mrJ5e26oRXNcg==", // Always include public key
|
||||
];
|
||||
], [config?.meshcoreKeys]);
|
||||
const knownKeysString = knownKeys.join(",");
|
||||
const [parsed, setParsed] = useState<any | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [originsExpanded, setOriginsExpanded] = useState(false);
|
||||
@@ -62,7 +63,7 @@ export default function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessag
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [msg.encrypted_message, msg.mac, msg.channel_hash, knownKeys.join(",")]);
|
||||
}, [msg.encrypted_message, msg.mac, msg.channel_hash, knownKeysString, knownKeys]);
|
||||
|
||||
const originPathArray = msg.origin_path_array && msg.origin_path_array.length > 0 ? msg.origin_path_array : [];
|
||||
const originsCount = originPathArray.length;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { MapContainer, TileLayer, useMapEvents, Marker, Popup, MapContainerProps, useMap } from "react-leaflet";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from "leaflet";
|
||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
||||
@@ -33,7 +33,8 @@ type NodePosition = {
|
||||
type ClusteredMarkersProps = { nodes: NodePosition[] };
|
||||
function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
|
||||
const map = useMap();
|
||||
const { config } = useConfig ? useConfig() : { config: undefined };
|
||||
const configResult = useConfig();
|
||||
const config = configResult?.config;
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
// Remove any previous layers
|
||||
@@ -107,7 +108,8 @@ export default function MapView() {
|
||||
const [lastResultCount, setLastResultCount] = useState<number>(0);
|
||||
const fetchController = useRef<AbortController | null>(null);
|
||||
const lastRequestedBounds = useRef<[[number, number], [number, number]] | null>(null);
|
||||
const { config } = useConfig ? useConfig() : { config: undefined };
|
||||
const configResult = useConfig();
|
||||
const config = configResult?.config;
|
||||
|
||||
type TileLayerKey = 'openstreetmap' | 'opentopomap' | 'esri';
|
||||
const tileLayerOptions: Record<TileLayerKey, { url: string; attribution: string; maxZoom: number; subdomains?: string[] }> = {
|
||||
@@ -129,7 +131,7 @@ export default function MapView() {
|
||||
};
|
||||
const selectedTileLayer = tileLayerOptions[(config?.tileLayer as TileLayerKey) || 'openstreetmap'];
|
||||
|
||||
function fetchNodes(bounds?: [[number, number], [number, number]]) {
|
||||
const fetchNodes = useCallback((bounds?: [[number, number], [number, number]]) => {
|
||||
if (fetchController.current) {
|
||||
fetchController.current.abort();
|
||||
}
|
||||
@@ -169,7 +171,7 @@ export default function MapView() {
|
||||
if (err.name !== "AbortError") setNodePositions([]);
|
||||
if (fetchController.current === controller) setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [config?.nodeTypes, config?.lastSeen]);
|
||||
|
||||
function isBoundsInside(inner: [[number, number], [number, number]], outer: [[number, number], [number, number]]) {
|
||||
// inner: [[minLat, minLng], [maxLat, maxLng]]
|
||||
@@ -264,7 +266,7 @@ export default function MapView() {
|
||||
return () => {
|
||||
fetchController.current?.abort();
|
||||
};
|
||||
}, [bounds, config?.nodeTypes, config?.lastSeen]);
|
||||
}, [bounds, config?.nodeTypes, config?.lastSeen, fetchNodes]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
|
||||
Reference in New Issue
Block a user