Browse Source

Fix person node toggle persistence issue

- Add hasInitializedPersons flag to track first-time initialization
- Only auto-disable person nodes on first show, not on every update
- Clear disabled persons when person visualizer is hidden
- Update EventTypeConfig to show profile stats in format: [limit] of [total] fetched

This fixes the issue where person nodes would immediately disable after being toggled on when navigating from a publication page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 9 months ago
parent
commit
d620ff2a4c
  1. 24
      docs/event-types-panel-redesign.org
  2. 44
      src/lib/components/EventTypeConfig.svelte
  3. 11
      src/lib/navigator/EventNetwork/index.svelte
  4. 79
      src/routes/visualize/+page.svelte

24
docs/event-types-panel-redesign.org

@ -16,19 +16,19 @@ Clean implementation plan for the event network visualization, focusing on perfo
* Implementation Phases * Implementation Phases
** Phase 1: Tag Anchor Controls Migration ** Phase 1: Tag Anchor Controls Migration
- Move tag type selection from Settings to Legend - +Move tag type selection from Settings to Legend+
- Move expansion depth control from Settings to Legend - +Move expansion depth control from Settings to Legend+
- Move requirePublications checkbox from Settings to Legend - +Move requirePublications checkbox from Settings to Legend+
- Use native HTML button instead of flowbite Toggle component - +Use native HTML button instead of flowbite Toggle component+
- Clean up Settings panel - +Clean up Settings panel+
** Phase 2: Person Visualizer ** Phase 2: Person Visualizer
- Add collapsible "Person Visualizer" section in Legend - +Add collapsible "Person Visualizer" section in Legend+
- Display all event authors (pubkeys) as list items - +Display all event authors (pubkeys) as list items+
- Fetch display names from kind 0 events - +Fetch display names from kind 0 events+
- Render person nodes as diamond shapes in graph - +Render person nodes as diamond shapes in graph+
- Default all person nodes to disabled state - +Default all person nodes to disabled state+
- Click to toggle individual person visibility - +Click to toggle individual person visibility+
** Phase 3: State Management Fixes ** Phase 3: State Management Fixes
- Replace reactive Set with object/map for disabled states - Replace reactive Set with object/map for disabled states
@ -89,4 +89,4 @@ const contentEvents = await $ndkInstance.fetchEvents({
2. **Stability**: Avoids infinite loops and reactive state issues 2. **Stability**: Avoids infinite loops and reactive state issues
3. **UX**: Smooth, instant toggle without freezing 3. **UX**: Smooth, instant toggle without freezing
4. **Maintainability**: Clear separation of concerns 4. **Maintainability**: Clear separation of concerns
5. **Scalability**: Handles large numbers of nodes efficiently 5. **Scalability**: Handles large numbers of nodes efficiently

44
src/lib/components/EventTypeConfig.svelte

@ -6,10 +6,12 @@
let { let {
onReload = () => {}, onReload = () => {},
eventCounts = {} eventCounts = {},
profileStats = { totalFetched: 0, displayLimit: 50 }
} = $props<{ } = $props<{
onReload?: () => void; onReload?: () => void;
eventCounts?: { [kind: number]: number }; eventCounts?: { [kind: number]: number };
profileStats?: { totalFetched: number; displayLimit: number };
}>(); }>();
let newKind = $state(''); let newKind = $state('');
@ -122,16 +124,32 @@
<CloseCircleOutline class="w-4 h-4" /> <CloseCircleOutline class="w-4 h-4" />
</button> </button>
<!-- Limit input for all kinds --> <!-- Special format for kind 0 (profiles) -->
<input {#if config.kind === 0}
type="number" <input
value={config.limit} type="number"
min="1" value={profileStats.displayLimit}
max="1000" min="1"
class="w-16 px-2 py-1 text-xs border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white" max={profileStats.totalFetched || 1000}
oninput={(e) => handleLimitChange(config.kind, e.currentTarget.value)} class="w-16 px-2 py-1 text-xs border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
title="Max to display" oninput={(e) => handleLimitChange(config.kind, e.currentTarget.value)}
/> title="Max profiles to display"
/>
<span class="text-xs text-gray-600 dark:text-gray-400">
of {profileStats.totalFetched} fetched
</span>
{:else}
<!-- Limit input for other kinds -->
<input
type="number"
value={config.limit}
min="1"
max="1000"
class="w-16 px-2 py-1 text-xs border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
oninput={(e) => handleLimitChange(config.kind, e.currentTarget.value)}
title="Max to display"
/>
{/if}
<!-- Nested levels for 30040 --> <!-- Nested levels for 30040 -->
{#if config.kind === 30040} {#if config.kind === 30040}
@ -162,11 +180,11 @@
{/if} {/if}
<!-- Load indicator --> <!-- Load indicator -->
{#if isLoaded} {#if config.kind !== 0 && isLoaded}
<span class="text-xs text-green-600 dark:text-green-400"> <span class="text-xs text-green-600 dark:text-green-400">
({eventCounts[config.kind]}) ({eventCounts[config.kind]})
</span> </span>
{:else} {:else if config.kind !== 0}
<span class="text-xs text-red-600 dark:text-red-400"> <span class="text-xs text-red-600 dark:text-red-400">
(not loaded) (not loaded)
</span> </span>

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

@ -165,6 +165,7 @@
let personMap = $state<Map<string, any>>(new Map()); let personMap = $state<Map<string, any>>(new Map());
let totalPersonCount = $state(0); let totalPersonCount = $state(0);
let displayedPersonCount = $state(0); let displayedPersonCount = $state(0);
let hasInitializedPersons = $state(false);
// Debug function - call from browser console: window.debugTagAnchors() // Debug function - call from browser console: window.debugTagAnchors()
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@ -357,13 +358,14 @@
// Extract person info for legend // Extract person info for legend
personAnchorInfo = extractPersonAnchorInfo(personAnchors, personMap); personAnchorInfo = extractPersonAnchorInfo(personAnchors, personMap);
// Auto-disable all person nodes by default (only on first show) // Auto-disable all person nodes by default (only on first time showing)
if (disabledPersons.size === 0) { if (!hasInitializedPersons && personAnchors.length > 0) {
personAnchors.forEach(anchor => { personAnchors.forEach(anchor => {
if (anchor.pubkey) { if (anchor.pubkey) {
disabledPersons.add(anchor.pubkey); disabledPersons.add(anchor.pubkey);
} }
}); });
hasInitializedPersons = true;
} }
debug("Person anchors created", { debug("Person anchors created", {
@ -374,6 +376,11 @@
}); });
} else { } else {
personAnchorInfo = []; personAnchorInfo = [];
// Reset initialization flag when person nodes are hidden
if (hasInitializedPersons && personAnchorInfo.length === 0) {
hasInitializedPersons = false;
disabledPersons.clear();
}
} }
// Save current node positions before updating // Save current node positions before updating

79
src/routes/visualize/+page.svelte

@ -237,11 +237,15 @@
let allFetchedEvents: NDKEvent[] = []; let allFetchedEvents: NDKEvent[] = [];
// First, fetch non-publication events (like kind 0, 1, 3, etc.) // First, fetch non-publication events (like kind 1, 3, etc. but NOT kind 0)
if (otherConfigs.length > 0) { // We'll fetch kind 0 profiles after we know which pubkeys we need
debug("Fetching non-publication events:", otherConfigs); const kind0Config = otherConfigs.find(c => c.kind === 0);
const nonProfileConfigs = otherConfigs.filter(c => c.kind !== 0);
if (nonProfileConfigs.length > 0) {
debug("Fetching non-publication events (excluding profiles):", nonProfileConfigs);
for (const config of otherConfigs) { for (const config of nonProfileConfigs) {
try { try {
// Special handling for kind 3 (follow lists) // Special handling for kind 3 (follow lists)
if (config.kind === 3) { if (config.kind === 3) {
@ -483,26 +487,16 @@
baseEvents = [...allEvents]; // Store base events for tag expansion baseEvents = [...allEvents]; // Store base events for tag expansion
// Step 6: Fetch profiles (kind 0) // Step 6: Extract all pubkeys and fetch profiles
debug("Fetching profiles for events"); debug("Extracting pubkeys from all events");
// Get kind 0 config to respect its limit
const profileConfig = enabledConfigs.find(ec => ec.kind === 0);
const profileLimit = profileConfig?.limit || 50;
// Collect all pubkeys that need profiles
const allPubkeys = new Set<string>();
// Add event authors (these are the main content creators) // Use the utility function to extract ALL pubkeys (authors + p tags + content)
allEvents.forEach(event => { const allPubkeys = extractPubkeysFromEvents(allEvents);
if (event.pubkey) {
allPubkeys.add(event.pubkey);
}
});
// Add pubkeys from follow lists (for tag anchors) // Add pubkeys from follow lists if present
if (followListEvents.length > 0) { if (followListEvents.length > 0) {
followListEvents.forEach(event => { followListEvents.forEach(event => {
if (event.pubkey) allPubkeys.add(event.pubkey);
event.tags.forEach(tag => { event.tags.forEach(tag => {
if (tag[0] === 'p' && tag[1]) { if (tag[0] === 'p' && tag[1]) {
allPubkeys.add(tag[1]); allPubkeys.add(tag[1]);
@ -511,25 +505,38 @@
}); });
} }
// Limit the number of profiles to fetch based on kind 0 limit debug("Profile extraction complete:", {
const pubkeysArray = Array.from(allPubkeys);
const pubkeysToFetch = profileLimit === -1
? pubkeysArray
: pubkeysArray.slice(0, profileLimit);
debug("Profile fetch strategy:", {
totalPubkeys: allPubkeys.size, totalPubkeys: allPubkeys.size,
profileLimit, fromEvents: allEvents.length,
pubkeysToFetch: pubkeysToFetch.length, fromFollowLists: followListEvents.length
followListsLoaded: followListEvents.length
}); });
profileLoadingProgress = { current: 0, total: pubkeysToFetch.length }; // Fetch ALL profiles if kind 0 is enabled
await batchFetchProfiles(pubkeysToFetch, (fetched, total) => { if (kind0Config) {
profileLoadingProgress = { current: fetched, total }; debug("Fetching profiles for all discovered pubkeys");
});
profileLoadingProgress = null; // Clear progress when done // Update progress during fetch
debug("Profile fetch complete for", pubkeysToFetch.length, "pubkeys"); profileLoadingProgress = { current: 0, total: allPubkeys.size };
await batchFetchProfiles(
Array.from(allPubkeys),
(fetched, total) => {
profileLoadingProgress = { current: fetched, total };
}
);
profileLoadingProgress = null;
debug("Profile fetch complete");
// Store the total count for display
// The limit in kind0Config now controls display, not fetch
if (typeof window !== 'undefined' && window.profileStats) {
window.profileStats = {
totalFetched: allPubkeys.size,
displayLimit: kind0Config.limit
};
}
}
// Step 7: Apply display limits // Step 7: Apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig);

Loading…
Cancel
Save