From b173351011f78c319f850198a655fc72a6bd3d61 Mon Sep 17 00:00:00 2001 From: ajvpot <553597+ajvpot@users.noreply.github.com> Date: Tue, 23 Sep 2025 04:04:17 +0200 Subject: [PATCH] path display overhaul --- .../(app)/meshcore/node/[publicKey]/page.tsx | 1 - src/app/(app)/search/page.tsx | 27 +++- src/app/api/meshcore/search/route.ts | 4 +- src/components/AdvertDetails.tsx | 3 +- src/components/ChatMessageItem.tsx | 5 +- src/components/NodeLinkWithHover.tsx | 25 +++- src/components/PathDisplay.tsx | 29 ---- src/components/PathVisualization.tsx | 141 +++++------------- src/hooks/useQueryParams.ts | 4 +- src/lib/pathUtils.ts | 94 ++++++++++++ 10 files changed, 191 insertions(+), 142 deletions(-) delete mode 100644 src/components/PathDisplay.tsx create mode 100644 src/lib/pathUtils.ts diff --git a/src/app/(app)/meshcore/node/[publicKey]/page.tsx b/src/app/(app)/meshcore/node/[publicKey]/page.tsx index 8fb7f79..6f420ff 100644 --- a/src/app/(app)/meshcore/node/[publicKey]/page.tsx +++ b/src/app/(app)/meshcore/node/[publicKey]/page.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import moment from "moment"; import { formatPublicKey } from "@/lib/meshcore"; import { getNameIconLabel } from "@/lib/meshcore-map-nodeutils"; -import PathDisplay from "@/components/PathDisplay"; import AdvertDetails from "@/components/AdvertDetails"; import ContactQRCode from "@/components/ContactQRCode"; import { useConfig, LAST_SEEN_OPTIONS } from "@/components/ConfigContext"; diff --git a/src/app/(app)/search/page.tsx b/src/app/(app)/search/page.tsx index 51d1572..3f3ab45 100644 --- a/src/app/(app)/search/page.tsx +++ b/src/app/(app)/search/page.tsx @@ -13,11 +13,14 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline'; function SearchPageContent() { const { config } = useConfig(); - const { query, setQuery, setLimit, setExact } = useSearchQuery(); + const { query, setQuery, setLimit, setExact, setIsRepeater } = useSearchQuery(); const [showFilters, setShowFilters] = useState(false); // Helper function to check if exact search is enabled const isExactEnabled = query.exact === true || (typeof query.exact === 'string' && (query.exact === 'true' || query.exact === '')); + + // Helper function to check if is_repeater search is enabled + const isRepeaterEnabled = query.is_repeater === true || (typeof query.is_repeater === 'string' && (query.is_repeater === 'true' || query.is_repeater === '')); // Always use config values for region and lastSeen const searchParams = { @@ -26,6 +29,7 @@ function SearchPageContent() { lastSeen: config.lastSeen, limit: query.limit || 50, exact: isExactEnabled, + is_repeater: isRepeaterEnabled, }; const { data, isLoading, error } = useMeshcoreSearch({ @@ -82,7 +86,7 @@ function SearchPageContent() { {showFilters && (
-
+
{/* Region Filter */}
+ {/* Repeater Filter */} +
+ +
+ setIsRepeater(e.target.checked)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> + +
+
+
)} diff --git a/src/app/api/meshcore/search/route.ts b/src/app/api/meshcore/search/route.ts index 3a4650d..40d4864 100644 --- a/src/app/api/meshcore/search/route.ts +++ b/src/app/api/meshcore/search/route.ts @@ -30,9 +30,9 @@ export async function POST(req: Request) { }); } - if (body.queries.length > 50) { + if (body.queries.length > 500) { return NextResponse.json({ - error: "Maximum 50 queries allowed per batch", + error: "Maximum 500 queries allowed per batch", code: "TOO_MANY_QUERIES" }, { status: 400 }); } diff --git a/src/components/AdvertDetails.tsx b/src/components/AdvertDetails.tsx index 03ad0a4..52edea6 100644 --- a/src/components/AdvertDetails.tsx +++ b/src/components/AdvertDetails.tsx @@ -2,7 +2,8 @@ import { useState } from "react"; import moment from "moment"; -import PathVisualization, { PathData } from "./PathVisualization"; +import PathVisualization from "./PathVisualization"; +import { PathData } from "@/lib/pathUtils"; interface AdvertDetailsProps { advert: { diff --git a/src/components/ChatMessageItem.tsx b/src/components/ChatMessageItem.tsx index 146f4a2..01ed42e 100644 --- a/src/components/ChatMessageItem.tsx +++ b/src/components/ChatMessageItem.tsx @@ -2,7 +2,8 @@ import React, { useMemo } from "react"; import { useConfig } from "./ConfigContext"; import { useMessageDecryption } from "@/hooks/useMessageDecryption"; -import PathVisualization, { PathData } from "./PathVisualization"; +import PathVisualization from "./PathVisualization"; +import { PathData } from "@/lib/pathUtils"; import NodeLinkWithHover from "./NodeLinkWithHover"; import { findNodeMentions } from "@/lib/node-utils"; @@ -90,6 +91,7 @@ function ChatMessageContent({ text }: { text: string }) { @{nodeName} @@ -149,6 +151,7 @@ function ChatMessageItem({ msg, showErrorRow }: { msg: ChatMessage, showErrorRow {parsed.sender ? ( {parsed.sender} diff --git a/src/components/NodeLinkWithHover.tsx b/src/components/NodeLinkWithHover.tsx index 7360a08..b13034e 100644 --- a/src/components/NodeLinkWithHover.tsx +++ b/src/components/NodeLinkWithHover.tsx @@ -11,11 +11,15 @@ import NodeCard from '@/components/NodeCard'; interface NodeLinkWithHoverProps { nodeName: string; children: React.ReactNode; + exact?: boolean; + is_repeater?: boolean; } export default function NodeLinkWithHover({ nodeName, - children + children, + exact = false, + is_repeater = false }: NodeLinkWithHoverProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isWaitingForSearch, setIsWaitingForSearch] = useState(false); @@ -32,7 +36,8 @@ export default function NodeLinkWithHover({ region: config.selectedRegion, lastSeen: config.lastSeen, limit: 10, - exact: true, + exact: exact, + is_repeater: is_repeater, enabled: !!nodeName }); @@ -49,7 +54,11 @@ export default function NodeLinkWithHover({ if (foundNode) return `/meshcore/node/${foundNode.public_key}`; // If no results or multiple results, link to search page - return `/search?q=${encodeURIComponent(nodeName)}&exact`; + const searchUrl = `/search?q=${encodeURIComponent(nodeName)}`; + const params = []; + if (exact) params.push('exact'); + if (is_repeater) params.push('is_repeater'); + return searchUrl + (params.length > 0 ? '&' + params.join('&') : ''); })(); // Handle click behavior @@ -75,11 +84,17 @@ export default function NodeLinkWithHover({ // Calculate navigation URL directly here since linkHref might still be "#" const navigationUrl = foundNode ? `/meshcore/node/${foundNode.public_key}` - : `/search?q=${encodeURIComponent(nodeName)}&exact`; + : (() => { + const searchUrl = `/search?q=${encodeURIComponent(nodeName)}`; + const params = []; + if (exact) params.push('exact'); + if (is_repeater) params.push('is_repeater'); + return searchUrl + (params.length > 0 ? '&' + params.join('&') : ''); + })(); router.push(navigationUrl); } - }, [isWaitingForSearch, isSearchLoading, foundNode, router, nodeName, searchData]); + }, [isWaitingForSearch, isSearchLoading, foundNode, router, nodeName, searchData, exact, is_repeater]); // Popover content component const PopoverContent = () => { diff --git a/src/components/PathDisplay.tsx b/src/components/PathDisplay.tsx deleted file mode 100644 index ff6f0b8..0000000 --- a/src/components/PathDisplay.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import React from "react"; - -interface PathDisplayProps { - path: string; - origin_pubkey: string; - className?: string; -} - -export default function PathDisplay({ - path, - origin_pubkey, - className = "" -}: PathDisplayProps) { - // Parse path into 2-character slices - const pathSlices = path.match(/.{1,2}/g) || []; - const formattedPath = pathSlices.join(' '); - - // Get first 2 characters of the pubkey for display - const pubkeyPrefix = origin_pubkey.substring(0, 2); - - return ( -
- {formattedPath} - ({pubkeyPrefix}) -
- ); -} diff --git a/src/components/PathVisualization.tsx b/src/components/PathVisualization.tsx index 85f1774..e0a1b97 100644 --- a/src/components/PathVisualization.tsx +++ b/src/components/PathVisualization.tsx @@ -6,27 +6,18 @@ import Link from "next/link"; import Tree from 'react-d3-tree'; import { ArrowsPointingOutIcon, ArrowsPointingInIcon } from "@heroicons/react/24/outline"; import { ExternalLink } from "lucide-react"; -import PathDisplay from "./PathDisplay"; +import NodeLinkWithHover from "./NodeLinkWithHover"; import { useMeshcoreSearches } from "@/hooks/useMeshcoreSearch"; import type { MeshcoreSearchResult } from "@/hooks/useMeshcoreSearch"; import { useConfigWithRegion } from "@/hooks/useConfigWithRegion"; - -export interface PathData { - origin: string; - pubkey: string; - path: string; -} - -interface PathGroup { - path: string; - pathSlices: string[]; - indices: number[]; -} - -interface TreeNode { - name: string; - children?: TreeNode[]; -} +import { + PathData, + PathGroup, + TreeNode, + groupPathsByStructure, + buildTreeFromPathGroups, + extractUniquePrefixes +} from "@/lib/pathUtils"; interface PathVisualizationProps { paths: PathData[]; @@ -53,77 +44,23 @@ export default function PathVisualization({ const { config } = useConfigWithRegion(); const pathsCount = paths.length; + // Group paths by structure + const pathGroups = useMemo(() => + groupPathsByStructure(paths), + [paths] + ); + // Process data for tree visualization const treeData = useMemo(() => { if (!showGraph || pathsCount === 0) return null; - - // Group messages by path similarity - const pathGroups: PathGroup[] = []; - - paths.forEach(({ origin, pubkey, path }, index) => { - // Parse path into 2-character slices and include pubkey as final hop - const pathSlices = path.match(/.{1,2}/g) || []; - const pubkeyPrefix = pubkey.substring(0, 2); - const fullPathSlices = [...pathSlices, pubkeyPrefix]; - - // Find existing group with same path structure - const existingGroup = pathGroups.find(group => - group.pathSlices.length === fullPathSlices.length && - group.pathSlices.every((slice, i) => slice === fullPathSlices[i]) - ); - - if (existingGroup) { - existingGroup.indices.push(index); - } else { - pathGroups.push({ - path: path + pubkeyPrefix, - pathSlices: fullPathSlices, - indices: [index] - }); - } - }); - - // Build tree structure for react-d3-tree - const buildTree = (): TreeNode => { - const rootName = initiatingNodeKey ? initiatingNodeKey.substring(0, 2) : "??"; - const root: TreeNode = { name: rootName, children: [] }; - - pathGroups.forEach(group => { - let currentNode = root; - - group.pathSlices.forEach((slice, level) => { - let child = currentNode.children?.find(c => c.name === slice); - - if (!child) { - child = { name: slice, children: [] }; - if (!currentNode.children) currentNode.children = []; - currentNode.children.push(child); - } - - currentNode = child; - }); - }); - - return root; - }; - - return buildTree(); - }, [showGraph, paths, pathsCount, initiatingNodeKey]); + return buildTreeFromPathGroups(pathGroups, initiatingNodeKey); + }, [showGraph, pathsCount, pathGroups, initiatingNodeKey]); // Extract unique prefixes from tree data for name lookups - const uniquePrefixes = useMemo(() => { - if (!treeData) return []; - - const prefixes = new Set(); - - const extractPrefixes = (node: TreeNode) => { - prefixes.add(node.name); - node.children?.forEach(extractPrefixes); - }; - - extractPrefixes(treeData); - return Array.from(prefixes); - }, [treeData]); + const uniquePrefixes = useMemo(() => + extractUniquePrefixes(treeData), + [treeData] + ); // Use the new useMeshcoreSearches hook to handle multiple prefix searches // Filter out "??" prefix and only search for valid hex prefixes @@ -187,23 +124,27 @@ export default function PathVisualization({ const PathsList = useCallback(() => (
- {paths.map(({ origin, pubkey, path }, index) => ( -
- - {origin} - - + {pathGroups.map((group, groupIndex) => ( +
+
+ {group.pathSlices.map((slice, sliceIndex) => ( + + + {slice} + + + ))} + {group.count > 1 && ( + + (x{group.count}) + {/* TODO: this doesnt work? */} + + )} +
))}
- ), [paths]); + ), [pathGroups]); // Memoize the render function to prevent unnecessary re-renders const renderCustomNodeElement = useCallback(({ nodeDatum, toggleNode }: any) => { @@ -333,7 +274,7 @@ export default function PathVisualization({
- {pathsCount} path{pathsCount !== 1 ? 's' : ''} + Heard {pathsCount} time{pathsCount !== 1 ? 's' : ''} {pathsCount > 0 && (