import React, { useState, useEffect, useCallback } from "react"; import { useAppSelector, useAppDispatch } from "../hooks"; import { PacketRenderer } from "./packets/PacketRenderer"; import { StreamControl } from "./StreamControl"; import { Trash2, RefreshCw, Archive, Radio } from "lucide-react"; import { clearPackets, toggleStreamPause } from "../store/slices/packetSlice"; import { Packet } from "../lib/types"; import { Separator } from "./Separator"; // Number of packets to show per page const PACKETS_PER_PAGE = 10; export const PacketList: React.FC = () => { const { packets, bufferedPackets, loading, error, streamPaused } = useAppSelector((state) => state.packets); const dispatch = useAppDispatch(); const [currentPage, setCurrentPage] = useState(1); // Generate a reproducible hash code for a string const hashString = useCallback((str: string): string => { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } // Make sure hash is always positive and convert to string return Math.abs(hash).toString(36); }, []); // Create a packet key using data.id and from address // This should match the key generation logic in the reducer const createPacketKey = useCallback( (packet: Packet): string => { if (packet.data.id !== undefined && packet.data.from !== undefined) { // Use Meshtastic node ID format (! followed by lowercase hex) and packet ID const nodeId = `!${packet.data.from.toString(16).toLowerCase()}`; return `${nodeId}_${packet.data.id}`; } else { // Fallback to hash-based key if no ID or from (should be rare) return `hash_${hashString(JSON.stringify(packet))}`; } }, [hashString] ); // We don't need to track packet keys in state anymore since we use data.id // and it's deterministic - removing this effect to prevent the infinite loop issue if (loading) { return (
Loading...
); } if (error) { return (
Error: {error}
); } if (packets.length === 0 && bufferedPackets.length === 0) { return (
No packets received yet
); } // Calculate pagination const totalPages = Math.ceil(packets.length / PACKETS_PER_PAGE); const startIndex = (currentPage - 1) * PACKETS_PER_PAGE; const endIndex = startIndex + PACKETS_PER_PAGE; const currentPackets = packets.slice(startIndex, endIndex); const handleClearPackets = () => { if (window.confirm("Are you sure you want to clear all packets?")) { dispatch(clearPackets()); setCurrentPage(1); } }; const handleToggleStream = () => { dispatch(toggleStreamPause()); // When unpausing with buffered packets, reset to first page to see new content if (streamPaused && bufferedPackets.length > 0) { setCurrentPage(1); } }; // Get the key for a packet - directly use createPacketKey const getPacketKey = (packet: Packet, index: number): string => { return createPacketKey(packet) || `fallback_${index}`; }; return (
{packets.length} packets received, since 6:00am
{/* Show buffered count when paused */} {streamPaused && bufferedPackets.length > 0 && (
{bufferedPackets.length} new
)} {/* Stream control toggle */} {/* Clear button */}
{/* Empty state when no packets are visible but stream is paused */} {packets.length === 0 && streamPaused && bufferedPackets.length > 0 && (
Stream is paused with {bufferedPackets.length} buffered messages.
)} {/* Pagination */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
); };