diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index cd9dd3b..dce1234 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -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(undefined); + const messagesRef = useRef([]); 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); diff --git a/src/components/ChatMessageItem.tsx b/src/components/ChatMessageItem.tsx index 457583a..0e2c305 100644 --- a/src/components/ChatMessageItem.tsx +++ b/src/components/ChatMessageItem.tsx @@ -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(null); const [error, setError] = useState(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; diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index 5d68fb7..bcbd860 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -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(0); const fetchController = useRef(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 = { @@ -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 (