Browse Source

Enhance person visualizer with connection types and limits

- Limited person nodes to 20 by default to prevent graph overload
- Added "Displaying X of Y people found" message in Legend
- Implemented signed-by vs referenced connection tracking
- Added checkboxes to filter by connection type (signed by / referenced)
- Different link colors: green for signed-by, blue for referenced
- Removed "People (from follow lists)" from tag types (now handled by person visualizer)
- Consolidated all person connections into single node per pubkey
- Display count shows (Xs/Yr) for signed-by/referenced counts
- Disabled person toggle clicks when Show Person Nodes is off
- Cleaned up unused requirePublications code

This makes the person visualizer more manageable and informative while preventing performance issues from too many nodes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 9 months ago
parent
commit
46a5d8fe18
  1. 14
      src/app.css
  2. 99
      src/lib/navigator/EventNetwork/Legend.svelte
  3. 194
      src/lib/navigator/EventNetwork/index.svelte
  4. 145
      src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts

14
src/app.css

@ -210,6 +210,20 @@
.network-node-content { .network-node-content {
@apply fill-primary-100; @apply fill-primary-100;
} }
/* Person link colors */
.person-link-signed {
@apply stroke-green-500;
}
.person-link-referenced {
@apply stroke-blue-400;
}
/* Person anchor node */
.person-anchor-node {
@apply fill-green-400 stroke-green-600;
}
} }
/* Utilities can be applied via the @apply directive. */ /* Utilities can be applied via the @apply directive. */

99
src/lib/navigator/EventNetwork/Legend.svelte

@ -18,13 +18,16 @@
showTagAnchors = $bindable(false), showTagAnchors = $bindable(false),
selectedTagType = $bindable("t"), selectedTagType = $bindable("t"),
tagExpansionDepth = $bindable(0), tagExpansionDepth = $bindable(0),
requirePublications = $bindable(true),
onTagSettingsChange = () => {}, onTagSettingsChange = () => {},
showPersonNodes = $bindable(false), showPersonNodes = $bindable(false),
personAnchors = [], personAnchors = [],
disabledPersons = new Set<string>(), disabledPersons = new Set<string>(),
onPersonToggle = (pubkey: string) => {}, onPersonToggle = (pubkey: string) => {},
onPersonSettingsChange = () => {}, onPersonSettingsChange = () => {},
showSignedBy = $bindable(true),
showReferenced = $bindable(true),
totalPersonCount = 0,
displayedPersonCount = 0,
} = $props<{ } = $props<{
collapsedOnInteraction: boolean; collapsedOnInteraction: boolean;
className: string; className: string;
@ -38,13 +41,16 @@
showTagAnchors?: boolean; showTagAnchors?: boolean;
selectedTagType?: string; selectedTagType?: string;
tagExpansionDepth?: number; tagExpansionDepth?: number;
requirePublications?: boolean;
onTagSettingsChange?: () => void; onTagSettingsChange?: () => void;
showPersonNodes?: boolean; showPersonNodes?: boolean;
personAnchors?: any[]; personAnchors?: any[];
disabledPersons?: Set<string>; disabledPersons?: Set<string>;
onPersonToggle?: (pubkey: string) => void; onPersonToggle?: (pubkey: string) => void;
onPersonSettingsChange?: () => void; onPersonSettingsChange?: () => void;
showSignedBy?: boolean;
showReferenced?: boolean;
totalPersonCount?: number;
displayedPersonCount?: number;
}>(); }>();
let expanded = $state(true); let expanded = $state(true);
@ -200,32 +206,11 @@
> >
<option value="t">Hashtags</option> <option value="t">Hashtags</option>
<option value="author">Authors</option> <option value="author">Authors</option>
<option value="p">People (from follow lists)</option>
<option value="e">Event References</option> <option value="e">Event References</option>
<option value="title">Titles</option> <option value="title">Titles</option>
<option value="summary">Summaries</option> <option value="summary">Summaries</option>
</select> </select>
{#if selectedTagType === "p" && (!eventCounts[3] || eventCounts[3] === 0)}
<p class="text-xs text-orange-500 mt-1">
No follow lists loaded. Enable kind 3 events to see people tag anchors.
</p>
{/if}
{#if selectedTagType === "p" && eventCounts[3] > 0}
<div class="flex items-center space-x-2 mt-2">
<button
onclick={() => {
requirePublications = !requirePublications;
onTagSettingsChange();
}}
class="toggle-button small {requirePublications ? 'active' : ''}"
>
{requirePublications ? 'ON' : 'OFF'}
</button>
<span class="text-xs text-gray-600 dark:text-gray-400">Only show people with publications</span>
</div>
{/if}
</div> </div>
<!-- Expansion Depth --> <!-- Expansion Depth -->
@ -336,22 +321,51 @@
{#if personVisualizerExpanded} {#if personVisualizerExpanded}
<div class="space-y-3"> <div class="space-y-3">
<!-- Show Person Nodes Toggle --> <!-- Show Person Nodes Toggle -->
<div class="flex items-center space-x-2"> <div class="flex items-center justify-between">
<button <div class="flex items-center space-x-2">
onclick={() => { <button
showPersonNodes = !showPersonNodes; onclick={() => {
onPersonSettingsChange(); showPersonNodes = !showPersonNodes;
}} onPersonSettingsChange();
class="toggle-button {showPersonNodes ? 'active' : ''}" }}
> class="toggle-button {showPersonNodes ? 'active' : ''}"
{showPersonNodes ? 'ON' : 'OFF'} >
</button> {showPersonNodes ? 'ON' : 'OFF'}
<span class="text-sm">Show Person Nodes</span> </button>
<span class="text-sm">Show Person Nodes</span>
</div>
{#if showPersonNodes}
<div class="flex items-center space-x-3 text-xs">
<label class="flex items-center space-x-1">
<input
type="checkbox"
bind:checked={showSignedBy}
onchange={onPersonSettingsChange}
class="w-3 h-3"
/>
<span>Signed by</span>
</label>
<label class="flex items-center space-x-1">
<input
type="checkbox"
bind:checked={showReferenced}
onchange={onPersonSettingsChange}
class="w-3 h-3"
/>
<span>Referenced</span>
</label>
</div>
{/if}
</div> </div>
{#if showPersonNodes && personAnchors.length > 0} {#if showPersonNodes && personAnchors.length > 0}
<p class="text-xs text-gray-600 dark:text-gray-400"> <p class="text-xs text-gray-600 dark:text-gray-400">
{personAnchors.length} people found. Click to toggle visibility: {#if totalPersonCount > displayedPersonCount}
Displaying {displayedPersonCount} of {totalPersonCount} people found. Click to toggle visibility:
{:else}
{personAnchors.length} people found. Click to toggle visibility:
{/if}
</p> </p>
<div <div
class="tag-grid {personAnchors.length > 20 ? 'scrollable' : ''}" class="tag-grid {personAnchors.length > 20 ? 'scrollable' : ''}"
@ -361,8 +375,13 @@
{@const isDisabled = disabledPersons.has(person.pubkey)} {@const isDisabled = disabledPersons.has(person.pubkey)}
<button <button
class="tag-grid-item {isDisabled ? 'disabled' : ''}" class="tag-grid-item {isDisabled ? 'disabled' : ''}"
onclick={() => onPersonToggle(person.pubkey)} onclick={() => {
title={isDisabled ? `Click to show ${person.displayName || person.pubkey}` : `Click to hide ${person.displayName || person.pubkey}`} if (showPersonNodes) {
onPersonToggle(person.pubkey);
}
}}
disabled={!showPersonNodes}
title={!showPersonNodes ? 'Enable "Show Person Nodes" first' : isDisabled ? `Click to show ${person.displayName || person.pubkey}` : `Click to hide ${person.displayName || person.pubkey}`}
> >
<div class="legend-icon"> <div class="legend-icon">
<span <span
@ -372,8 +391,10 @@
</div> </div>
<span class="legend-text text-xs" style="opacity: {isDisabled ? 0.5 : 1};"> <span class="legend-text text-xs" style="opacity: {isDisabled ? 0.5 : 1};">
{person.displayName || person.pubkey.slice(0, 8) + '...'} {person.displayName || person.pubkey.slice(0, 8) + '...'}
{#if !isDisabled && person.eventCount} {#if !isDisabled}
<span class="text-gray-500">({person.eventCount})</span> <span class="text-gray-500">
({person.signedByCount || 0}s/{person.referencedCount || 0}r)
</span>
{/if} {/if}
</span> </span>
</button> </button>

194
src/lib/navigator/EventNetwork/index.svelte

@ -131,7 +131,6 @@
let selectedTagType = $state("t"); // Default to hashtags let selectedTagType = $state("t"); // Default to hashtags
let tagAnchorInfo = $state<any[]>([]); let tagAnchorInfo = $state<any[]>([]);
let tagExpansionDepth = $state(0); // Default to no expansion let tagExpansionDepth = $state(0); // Default to no expansion
let requirePublications = $state(true); // Default to only showing people with publications
// Store initial state to detect if component is being recreated // Store initial state to detect if component is being recreated
let componentId = Math.random(); let componentId = Math.random();
@ -161,6 +160,11 @@
let showPersonNodes = $state(false); let showPersonNodes = $state(false);
let personAnchorInfo = $state<any[]>([]); let personAnchorInfo = $state<any[]>([]);
let disabledPersons = $state(new Set<string>()); let disabledPersons = $state(new Set<string>());
let showSignedBy = $state(true);
let showReferenced = $state(true);
let personMap = $state<Map<string, any>>(new Map());
let totalPersonCount = $state(0);
let displayedPersonCount = $state(0);
// Debug function - call from browser console: window.debugTagAnchors() // Debug function - call from browser console: window.debugTagAnchors()
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@ -293,126 +297,12 @@
height height
}); });
// For "p" tags, we need to extract pubkeys from follow lists
// but only show anchors for pubkeys that have events in the visualization
let eventsForTags = events;
if (selectedTagType === "p" && followListEvents.length > 0) {
// Extract all pubkeys from follow lists
const followedPubkeys = new Set<string>();
followListEvents.forEach(event => {
event.tags.forEach(tag => {
if (tag[0] === "p" && tag[1]) {
followedPubkeys.add(tag[1]);
}
});
});
const syntheticEvents: NDKEvent[] = [];
// Create a map to track which events each followed pubkey is connected to
const pubkeyToEvents = new Map<string, Set<string>>();
// Find all connections for followed pubkeys
followedPubkeys.forEach(pubkey => {
const connectedEventIds = new Set<string>();
// Find events they authored
events.forEach(event => {
if (event.pubkey === pubkey && event.id) {
connectedEventIds.add(event.id);
}
});
// Find events where they're tagged with "p"
events.forEach(event => {
if (event.id && event.tags) {
event.tags.forEach(tag => {
if (tag[0] === 'p' && tag[1] === pubkey) {
connectedEventIds.add(event.id);
}
});
}
});
if (connectedEventIds.size > 0) {
pubkeyToEvents.set(pubkey, connectedEventIds);
}
});
if (requirePublications) {
// Only show people who have connections to events
pubkeyToEvents.forEach((eventIds, pubkey) => {
// Create synthetic events for each connection
eventIds.forEach(eventId => {
const syntheticEvent = {
id: eventId, // Use the actual event's ID so it connects properly
tags: [["p", pubkey]],
pubkey: "",
created_at: 0,
kind: 0,
content: "",
sig: ""
} as NDKEvent;
syntheticEvents.push(syntheticEvent);
});
});
} else {
// Show all people from follow lists
let syntheticId = 0;
// First, add people who have event connections
pubkeyToEvents.forEach((eventIds, pubkey) => {
eventIds.forEach(eventId => {
const syntheticEvent = {
id: eventId, // Use the actual event's ID so it connects properly
tags: [["p", pubkey]],
pubkey: "",
created_at: 0,
kind: 0,
content: "",
sig: ""
} as NDKEvent;
syntheticEvents.push(syntheticEvent);
});
});
// Then, add remaining people without any connections
followedPubkeys.forEach(pubkey => {
if (!pubkeyToEvents.has(pubkey)) {
const syntheticEvent = {
id: `synthetic-p-${syntheticId++}`, // Create unique IDs for those without events
tags: [["p", pubkey]],
pubkey: "",
created_at: 0,
kind: 0,
content: "",
sig: ""
} as NDKEvent;
syntheticEvents.push(syntheticEvent);
}
});
}
eventsForTags = syntheticEvents;
debug("Created synthetic events for p tags", {
followedPubkeys: followedPubkeys.size,
requirePublications,
syntheticEvents: syntheticEvents.length
});
}
// Get the display limit based on tag type // Get the display limit based on tag type
let displayLimit: number | undefined; let displayLimit: number | undefined;
if (selectedTagType === "p") {
// For people tags, use kind 0 (profiles) limit
const kind0Config = get(visualizationConfig).eventConfigs.find(ec => ec.kind === 0);
displayLimit = kind0Config?.limit || 50;
}
graphData = enhanceGraphWithTags( graphData = enhanceGraphWithTags(
graphData, graphData,
eventsForTags, events,
selectedTagType, selectedTagType,
width, width,
height, height,
@ -433,11 +323,6 @@
count: n.connectedNodes?.length || 0, count: n.connectedNodes?.length || 0,
color: getTagAnchorColor(n.tagType || ""), color: getTagAnchorColor(n.tagType || ""),
})); }));
// Add a message if People tag type is selected but no follow lists are loaded
if (selectedTagType === "p" && followListEvents.length === 0 && tagAnchors.length === 0) {
console.warn("[EventNetwork] No follow lists loaded. Enable kind 3 events with appropriate depth to see people tag anchors.");
}
} else { } else {
tagAnchorInfo = []; tagAnchorInfo = [];
} }
@ -447,31 +332,45 @@
debug("Creating person anchor nodes"); debug("Creating person anchor nodes");
// Extract unique persons from events // Extract unique persons from events
const personMap = extractUniquePersons(events); personMap = extractUniquePersons(events);
// Create person anchor nodes based on filters
const personResult = createPersonAnchorNodes(
personMap,
width,
height,
showSignedBy,
showReferenced
);
// Create person anchor nodes const personAnchors = personResult.nodes;
const personAnchors = createPersonAnchorNodes(personMap, width, height); totalPersonCount = personResult.totalCount;
displayedPersonCount = personAnchors.length;
// Create links between person anchors and their events // Create links between person anchors and their events
const personLinks = createPersonLinks(personAnchors, graphData.nodes); const personLinks = createPersonLinks(personAnchors, graphData.nodes, personMap);
// Add person anchors to the graph // Add person anchors to the graph
graphData.nodes = [...graphData.nodes, ...personAnchors]; graphData.nodes = [...graphData.nodes, ...personAnchors];
graphData.links = [...graphData.links, ...personLinks]; graphData.links = [...graphData.links, ...personLinks];
// Extract person info for legend // Extract person info for legend
personAnchorInfo = extractPersonAnchorInfo(personAnchors); personAnchorInfo = extractPersonAnchorInfo(personAnchors, personMap);
// Auto-disable all person nodes by default // Auto-disable all person nodes by default (only on first show)
personAnchors.forEach(anchor => { if (disabledPersons.size === 0) {
if (anchor.pubkey) { personAnchors.forEach(anchor => {
disabledPersons.add(anchor.pubkey); if (anchor.pubkey) {
} disabledPersons.add(anchor.pubkey);
}); }
});
}
debug("Person anchors created", { debug("Person anchors created", {
count: personAnchors.length, count: personAnchors.length,
disabled: disabledPersons.size disabled: disabledPersons.size,
showSignedBy,
showReferenced
}); });
} else { } else {
personAnchorInfo = []; personAnchorInfo = [];
@ -618,10 +517,26 @@
(enter: any) => (enter: any) =>
enter enter
.append("path") .append("path")
.attr("class", "link network-link-leather") .attr("class", (d: any) => {
let classes = "link network-link-leather";
if (d.connectionType === "signed-by") {
classes += " person-link-signed";
} else if (d.connectionType === "referenced") {
classes += " person-link-referenced";
}
return classes;
})
.attr("stroke-width", 2) .attr("stroke-width", 2)
.attr("marker-end", "url(#arrowhead)"), .attr("marker-end", "url(#arrowhead)"),
(update: any) => update, (update: any) => update.attr("class", (d: any) => {
let classes = "link network-link-leather";
if (d.connectionType === "signed-by") {
classes += " person-link-signed";
} else if (d.connectionType === "referenced") {
classes += " person-link-referenced";
}
return classes;
}),
(exit: any) => exit.remove(), (exit: any) => exit.remove(),
); );
@ -1013,7 +928,9 @@
tagType: selectedTagType, tagType: selectedTagType,
disabled: disabledTags.size, disabled: disabledTags.size,
persons: showPersonNodes, persons: showPersonNodes,
disabledPersons: disabledPersons.size disabledPersons: disabledPersons.size,
showSignedBy,
showReferenced
}; };
isUpdating = true; isUpdating = true;
@ -1234,7 +1151,6 @@
bind:showTagAnchors bind:showTagAnchors
bind:selectedTagType bind:selectedTagType
bind:tagExpansionDepth bind:tagExpansionDepth
bind:requirePublications
onTagSettingsChange={() => { onTagSettingsChange={() => {
// Trigger graph update when tag settings change // Trigger graph update when tag settings change
if (svg && events?.length) { if (svg && events?.length) {
@ -1251,6 +1167,10 @@
updateGraph(); updateGraph();
} }
}} }}
bind:showSignedBy
bind:showReferenced
{totalPersonCount}
{displayedPersonCount}
/> />
<!-- Settings Panel (shown when settings button is clicked) --> <!-- Settings Panel (shown when settings button is clicked) -->

145
src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts

@ -10,6 +10,7 @@ import { getDisplayNameSync } from "$lib/utils/profileCache";
const PERSON_ANCHOR_RADIUS = 15; const PERSON_ANCHOR_RADIUS = 15;
const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000; const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000;
const MAX_PERSON_NODES = 20; // Default limit for person nodes
/** /**
* Simple seeded random number generator * Simple seeded random number generator
@ -40,25 +41,52 @@ function createSeed(str: string): number {
return Math.abs(hash); return Math.abs(hash);
} }
export interface PersonConnection {
signedByEventIds: Set<string>;
referencedInEventIds: Set<string>;
}
/** /**
* Extracts unique persons (pubkeys) from events * Extracts unique persons (pubkeys) from events
* Tracks both signed-by (event.pubkey) and referenced (["p", pubkey] tags)
*/ */
export function extractUniquePersons( export function extractUniquePersons(
events: NDKEvent[] events: NDKEvent[]
): Map<string, Set<string>> { ): Map<string, PersonConnection> {
// Map of pubkey -> Set of event IDs // Map of pubkey -> PersonConnection
const personMap = new Map<string, Set<string>>(); const personMap = new Map<string, PersonConnection>();
console.log(`[PersonBuilder] Extracting persons from ${events.length} events`); console.log(`[PersonBuilder] Extracting persons from ${events.length} events`);
events.forEach((event) => { events.forEach((event) => {
if (!event.pubkey || !event.id) return; if (!event.id) return;
if (!personMap.has(event.pubkey)) { // Track signed-by connections
personMap.set(event.pubkey, new Set()); if (event.pubkey) {
if (!personMap.has(event.pubkey)) {
personMap.set(event.pubkey, {
signedByEventIds: new Set(),
referencedInEventIds: new Set()
});
}
personMap.get(event.pubkey)!.signedByEventIds.add(event.id);
} }
personMap.get(event.pubkey)!.add(event.id); // Track referenced connections from "p" tags
if (event.tags) {
event.tags.forEach(tag => {
if (tag[0] === "p" && tag[1]) {
const referencedPubkey = tag[1];
if (!personMap.has(referencedPubkey)) {
personMap.set(referencedPubkey, {
signedByEventIds: new Set(),
referencedInEventIds: new Set()
});
}
personMap.get(referencedPubkey)!.referencedInEventIds.add(event.id);
}
});
}
}); });
console.log(`[PersonBuilder] Found ${personMap.size} unique persons`); console.log(`[PersonBuilder] Found ${personMap.size} unique persons`);
@ -70,16 +98,56 @@ export function extractUniquePersons(
* Creates person anchor nodes * Creates person anchor nodes
*/ */
export function createPersonAnchorNodes( export function createPersonAnchorNodes(
personMap: Map<string, Set<string>>, personMap: Map<string, PersonConnection>,
width: number, width: number,
height: number height: number,
): NetworkNode[] { showSignedBy: boolean,
showReferenced: boolean,
limit: number = MAX_PERSON_NODES
): { nodes: NetworkNode[], totalCount: number } {
const anchorNodes: NetworkNode[] = []; const anchorNodes: NetworkNode[] = [];
const centerX = width / 2; const centerX = width / 2;
const centerY = height / 2; const centerY = height / 2;
Array.from(personMap.entries()).forEach(([pubkey, eventIds]) => { // Calculate eligible persons and their connection counts
const eligiblePersons: Array<{
pubkey: string;
connection: PersonConnection;
connectedEventIds: Set<string>;
totalConnections: number;
}> = [];
Array.from(personMap.entries()).forEach(([pubkey, connection]) => {
// Get all connected event IDs based on filters
const connectedEventIds = new Set<string>();
if (showSignedBy) {
connection.signedByEventIds.forEach(id => connectedEventIds.add(id));
}
if (showReferenced) {
connection.referencedInEventIds.forEach(id => connectedEventIds.add(id));
}
// Skip if no connections match the filter
if (connectedEventIds.size === 0) return;
eligiblePersons.push({
pubkey,
connection,
connectedEventIds,
totalConnections: connectedEventIds.size
});
});
// Sort by total connections (descending) and take only top N
eligiblePersons.sort((a, b) => b.totalConnections - a.totalConnections);
const limitedPersons = eligiblePersons.slice(0, limit);
// Create nodes for the limited set
limitedPersons.forEach(({ pubkey, connection, connectedEventIds }) => {
// Create seeded random generator for consistent positioning // Create seeded random generator for consistent positioning
const rng = new SeededRandom(createSeed(pubkey)); const rng = new SeededRandom(createSeed(pubkey));
@ -95,7 +163,7 @@ export function createPersonAnchorNodes(
const anchorNode: NetworkNode = { const anchorNode: NetworkNode = {
id: `person-anchor-${pubkey}`, id: `person-anchor-${pubkey}`,
title: displayName, title: displayName,
content: `${eventIds.size} events`, content: `${connection.signedByEventIds.size} signed, ${connection.referencedInEventIds.size} referenced`,
author: "", author: "",
kind: 0, // Special kind for anchors kind: 0, // Special kind for anchors
type: "PersonAnchor", type: "PersonAnchor",
@ -103,7 +171,7 @@ export function createPersonAnchorNodes(
isPersonAnchor: true, isPersonAnchor: true,
pubkey, pubkey,
displayName, displayName,
connectedNodes: Array.from(eventIds), connectedNodes: Array.from(connectedEventIds),
x, x,
y, y,
fx: x, // Fix position fx: x, // Fix position
@ -113,29 +181,50 @@ export function createPersonAnchorNodes(
anchorNodes.push(anchorNode); anchorNodes.push(anchorNode);
}); });
return anchorNodes; return {
nodes: anchorNodes,
totalCount: eligiblePersons.length
};
}
export interface PersonLink extends NetworkLink {
connectionType?: "signed-by" | "referenced";
} }
/** /**
* Creates links between person anchors and their events * Creates links between person anchors and their events
* Adds connection type for coloring
*/ */
export function createPersonLinks( export function createPersonLinks(
personAnchors: NetworkNode[], personAnchors: NetworkNode[],
nodes: NetworkNode[] nodes: NetworkNode[],
): NetworkLink[] { personMap: Map<string, PersonConnection>
const links: NetworkLink[] = []; ): PersonLink[] {
const links: PersonLink[] = [];
const nodeMap = new Map(nodes.map((n) => [n.id, n])); const nodeMap = new Map(nodes.map((n) => [n.id, n]));
personAnchors.forEach((anchor) => { personAnchors.forEach((anchor) => {
if (!anchor.connectedNodes) return; if (!anchor.connectedNodes || !anchor.pubkey) return;
const connection = personMap.get(anchor.pubkey);
if (!connection) return;
anchor.connectedNodes.forEach((nodeId) => { anchor.connectedNodes.forEach((nodeId) => {
const node = nodeMap.get(nodeId); const node = nodeMap.get(nodeId);
if (node) { if (node) {
// Determine connection type
let connectionType: "signed-by" | "referenced" | undefined;
if (connection.signedByEventIds.has(nodeId)) {
connectionType = "signed-by";
} else if (connection.referencedInEventIds.has(nodeId)) {
connectionType = "referenced";
}
links.push({ links.push({
source: anchor, source: anchor,
target: node, target: node,
isSequential: false, isSequential: false,
connectionType,
}); });
} }
}); });
@ -150,18 +239,24 @@ export function createPersonLinks(
export interface PersonAnchorInfo { export interface PersonAnchorInfo {
pubkey: string; pubkey: string;
displayName: string; displayName: string;
eventCount: number; signedByCount: number;
referencedCount: number;
} }
/** /**
* Extracts person info for Legend display * Extracts person info for Legend display
*/ */
export function extractPersonAnchorInfo( export function extractPersonAnchorInfo(
personAnchors: NetworkNode[] personAnchors: NetworkNode[],
personMap: Map<string, PersonConnection>
): PersonAnchorInfo[] { ): PersonAnchorInfo[] {
return personAnchors.map(anchor => ({ return personAnchors.map(anchor => {
pubkey: anchor.pubkey || "", const connection = personMap.get(anchor.pubkey || "");
displayName: anchor.displayName || "", return {
eventCount: anchor.connectedNodes?.length || 0, pubkey: anchor.pubkey || "",
})); displayName: anchor.displayName || "",
signedByCount: connection?.signedByEventIds.size || 0,
referencedCount: connection?.referencedInEventIds.size || 0,
};
});
} }
Loading…
Cancel
Save