620 lines
21 KiB
JavaScript
620 lines
21 KiB
JavaScript
/**
|
|
* Relay UI Module
|
|
* Shared relay visualization components for footer and sidenav
|
|
*
|
|
* VISUALIZATION RULES:
|
|
* + indicates a connected relay that is enabled for both reading and writing.
|
|
* up_arrow indicates a connected relay that is enabled for writing.
|
|
* down_arrow indicates a connected relay that is enabled for reading.
|
|
* up_carrot is a temporary indication that a relay is being written to.
|
|
* down_carrot is a temporary indication that a relay is being read from.
|
|
* circle with muted-color is a relay that is not currently connected.
|
|
*
|
|
*/
|
|
|
|
import { getRelayData, getRelayStats, reconnectRelay, walletGetBalance } from './init-ndk.mjs';
|
|
import { HamburgerMorphing } from "../hamburger_morphing/hamburger.mjs";
|
|
|
|
const VERSION = "0.0.2";
|
|
|
|
// Relay status visualization (footer)
|
|
let relayStatusInstances = new Map(); // relay URL -> HamburgerMorphing instance
|
|
let relayActivityStates = new Map(); // relay URL -> { activity: 'read'|'write', timeout: id }
|
|
let lastRelayShapes = new Map(); // relay URL -> last rendered shape
|
|
|
|
// Relay status visualization (sidenav)
|
|
let sidenavRelayInstances = new Map(); // relay URL -> HamburgerMorphing instance
|
|
let lastRelayListSignature = ''; // Track when relay list changes
|
|
|
|
let footerBalanceInitialized = false;
|
|
let footerBalanceLastValue = null;
|
|
let footerBalanceListenersAttached = false;
|
|
let relaySectionToggleInitialized = false;
|
|
let relaySectionToggleIcon = null;
|
|
|
|
let walletBootstrapRequested = false;
|
|
let walletBootstrapRetryTimer = null;
|
|
let footerBalanceHydrationInFlight = null;
|
|
let footerBalanceLastHydrationAt = 0;
|
|
|
|
function formatSats(amount, { approximate = false } = {}) {
|
|
const value = Number(amount || 0);
|
|
const prefix = approximate ? '~' : '';
|
|
return `${prefix}${value.toLocaleString()} sats`;
|
|
}
|
|
|
|
function ensureFooterBalanceElement() {
|
|
const divFooter = document.getElementById('divFooter');
|
|
if (!divFooter) return null;
|
|
|
|
let divFooterBalance = document.getElementById('divFooterBalance');
|
|
if (!divFooterBalance) {
|
|
divFooterBalance = document.createElement('div');
|
|
divFooterBalance.id = 'divFooterBalance';
|
|
divFooterBalance.className = 'divFooterBox';
|
|
divFooterBalance.textContent = '0 sats';
|
|
divFooter.appendChild(divFooterBalance);
|
|
}
|
|
|
|
return divFooterBalance;
|
|
}
|
|
|
|
function setFooterBalanceValue(rawAmount, meta = {}) {
|
|
const isApproximate = Boolean(meta?.isApproximate) || String(meta?.balanceSource || '') === 'lightweight';
|
|
const nextValue = formatSats(rawAmount || 0, { approximate: isApproximate });
|
|
const divFooterBalance = ensureFooterBalanceElement();
|
|
if (!divFooterBalance) return;
|
|
|
|
if (footerBalanceLastValue !== nextValue) {
|
|
divFooterBalance.textContent = nextValue;
|
|
footerBalanceLastValue = nextValue;
|
|
}
|
|
}
|
|
|
|
async function hydrateFooterBalanceFromWorker(force = false) {
|
|
const now = Date.now();
|
|
if (!force && now - footerBalanceLastHydrationAt < 4000) return;
|
|
if (footerBalanceHydrationInFlight) return footerBalanceHydrationInFlight;
|
|
|
|
footerBalanceHydrationInFlight = (async () => {
|
|
try {
|
|
const payload = await walletGetBalance();
|
|
if (typeof payload?.balance === 'number') {
|
|
setFooterBalanceValue(payload.balance, payload || {});
|
|
}
|
|
} catch (error) {
|
|
console.warn('[relay-ui] Footer balance hydration skipped:', error?.message || error);
|
|
} finally {
|
|
footerBalanceLastHydrationAt = Date.now();
|
|
footerBalanceHydrationInFlight = null;
|
|
}
|
|
})();
|
|
|
|
return footerBalanceHydrationInFlight;
|
|
}
|
|
|
|
export function initFooterBalance() {
|
|
ensureFooterBalanceElement();
|
|
setFooterBalanceValue(0);
|
|
|
|
if (!footerBalanceListenersAttached) {
|
|
window.addEventListener('ndkWalletBalance', (event) => {
|
|
const payload = event?.detail || {};
|
|
setFooterBalanceValue(payload?.balance || 0, payload);
|
|
});
|
|
|
|
window.addEventListener('ndkWalletStatus', (event) => {
|
|
if (event?.detail?.hasWallet === false) {
|
|
setFooterBalanceValue(0);
|
|
return;
|
|
}
|
|
|
|
if (typeof event?.detail?.balance === 'number') {
|
|
setFooterBalanceValue(event.detail.balance, event?.detail || {});
|
|
}
|
|
});
|
|
|
|
footerBalanceListenersAttached = true;
|
|
}
|
|
|
|
footerBalanceInitialized = true;
|
|
hydrateFooterBalanceFromWorker(true);
|
|
}
|
|
|
|
export function updateFooterBalance() {
|
|
ensureFooterBalanceElement();
|
|
if (!footerBalanceLastValue || footerBalanceLastValue === '0 sats' || footerBalanceLastValue === '~0 sats') {
|
|
hydrateFooterBalanceFromWorker();
|
|
}
|
|
}
|
|
|
|
function bootstrapWalletForFooter() {
|
|
if (walletBootstrapRequested) return;
|
|
walletBootstrapRequested = true;
|
|
|
|
// Lightweight wallet balance is now computed in worker startup flow.
|
|
// Footer will update via ndkWalletBalance events.
|
|
setTimeout(() => {
|
|
walletBootstrapRequested = false;
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Set temporary activity state for a relay (shows animation for 3 seconds)
|
|
*/
|
|
export function setRelayActivityState(relayUrl, activity) {
|
|
// Clear any existing timeout for this relay
|
|
const existingState = relayActivityStates.get(relayUrl);
|
|
if (existingState && existingState.timeout) {
|
|
clearTimeout(existingState.timeout);
|
|
}
|
|
|
|
relayActivityStates.set(relayUrl, {
|
|
activity: activity,
|
|
timeout: setTimeout(() => {
|
|
relayActivityStates.delete(relayUrl);
|
|
// Trigger visual update
|
|
updateFooterRelayStatus();
|
|
updateSidenavRelaySection();
|
|
}, 3000)
|
|
});
|
|
|
|
// Trigger immediate visual update
|
|
updateFooterRelayStatus();
|
|
updateSidenavRelaySection();
|
|
}
|
|
|
|
/**
|
|
* Compute visualization data for a single relay
|
|
* This centralizes the logic for determining shape and color
|
|
*
|
|
* @param {Object} relay - Relay data from getRelayData()
|
|
* @param {Object} stats - Stats for this relay from getRelayStats()
|
|
* @param {Object} activityState - Temporary activity state (if any)
|
|
* @returns {Object} { shape, color, isConnected, domain, tooltip }
|
|
*/
|
|
function computeRelayVisualization(relay, stats, activityState) {
|
|
const isConnected = relay.status >= 5; // 5+ = CONNECTED (includes auth states 6/7/8)
|
|
const relayType = relay.type || 'both'; // 'read', 'write', or 'both'
|
|
const domain = relay.url.replace('wss://', '').replace('ws://', '').split('/')[0];
|
|
|
|
// Helper to resolve CSS variables
|
|
const resolveCssVar = (cssVar) => {
|
|
if (cssVar && cssVar.startsWith('var(')) {
|
|
const match = cssVar.match(/var\(\s*(--[^,\s)]+)/);
|
|
if (match) {
|
|
const value = getComputedStyle(document.documentElement).getPropertyValue(match[1]).trim();
|
|
return value || cssVar;
|
|
}
|
|
}
|
|
return cssVar;
|
|
};
|
|
|
|
let shape = 'circle';
|
|
let color = resolveCssVar('var(--muted-color)');
|
|
|
|
if (isConnected) {
|
|
// Check for temporary activity states first (highest priority)
|
|
if (activityState) {
|
|
shape = activityState.activity === 'write' ? 'carrot_up' : 'carrot_down';
|
|
color = resolveCssVar('var(--accent-color)');
|
|
}
|
|
// Then use relay type to determine base shape
|
|
else if (relayType === 'both') {
|
|
shape = 'plus';
|
|
color = resolveCssVar('var(--primary-color)');
|
|
} else if (relayType === 'write') {
|
|
shape = 'arrow_up';
|
|
color = resolveCssVar('var(--primary-color)');
|
|
} else if (relayType === 'read') {
|
|
shape = 'arrow_down';
|
|
color = resolveCssVar('var(--primary-color)');
|
|
}
|
|
}
|
|
// Disconnected relays show as circle with muted-color (already set as default)
|
|
|
|
const statusText = isConnected ? 'Connected' : 'Disconnected';
|
|
const tooltip = `${relay.url}\nStatus: ${statusText}\nType: ${relayType}\nReads: ${stats.readCount}\nWrites: ${stats.writeCount}`;
|
|
|
|
return {
|
|
shape,
|
|
color,
|
|
isConnected,
|
|
domain,
|
|
tooltip,
|
|
relayType,
|
|
stats
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prepare relay visualization data for all relays
|
|
*
|
|
* @returns {Promise<Array>} Array of relay visualization objects
|
|
*/
|
|
async function prepareRelayVisualizationData() {
|
|
const relayData = await getRelayData();
|
|
const relayStats = await getRelayStats();
|
|
|
|
if (!relayData || relayData.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Sort relays: connected first, then by URL
|
|
const sortedRelays = [...relayData].sort((a, b) => {
|
|
if (a.status >= 5 && b.status < 5) return -1;
|
|
if (a.status < 5 && b.status >= 5) return 1;
|
|
return a.url.localeCompare(b.url);
|
|
});
|
|
|
|
// Compute visualization data for each relay
|
|
return sortedRelays.map(relay => {
|
|
const stats = relayStats[relay.url] || { readCount: 0, writeCount: 0 };
|
|
const activityState = relayActivityStates.get(relay.url);
|
|
const vizData = computeRelayVisualization(relay, stats, activityState);
|
|
|
|
return {
|
|
url: relay.url,
|
|
status: relay.status,
|
|
type: relay.type,
|
|
...vizData
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render footer relay section
|
|
*
|
|
* @param {Array} relayVizData - Array of relay visualization objects from prepareRelayVisualizationData()
|
|
*/
|
|
function drawFooterSection(relayVizData) {
|
|
const divFooterLeft = document.getElementById('divFooterLeft');
|
|
if (!divFooterLeft) return;
|
|
|
|
// Track which relays we currently have visuals for
|
|
const currentVisualRelays = new Set();
|
|
|
|
relayVizData.forEach((vizData) => {
|
|
const svgId = `relay-status-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
let svgContainer = document.getElementById(svgId);
|
|
|
|
// Create container if it doesn't exist
|
|
if (!svgContainer) {
|
|
const relayContainer = document.createElement('div');
|
|
relayContainer.className = 'relay-status';
|
|
relayContainer.style.cssText = `
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
`;
|
|
|
|
svgContainer = document.createElement('div');
|
|
svgContainer.id = svgId;
|
|
svgContainer.style.cssText = `
|
|
width: 100%;
|
|
height: 100%;
|
|
`;
|
|
|
|
relayContainer.appendChild(svgContainer);
|
|
divFooterLeft.appendChild(relayContainer);
|
|
}
|
|
|
|
// Create or update hamburger morphing instance
|
|
let instance = relayStatusInstances.get(vizData.url);
|
|
if (!instance) {
|
|
instance = new HamburgerMorphing(`#${svgId}`, {
|
|
size: 16,
|
|
foreground: vizData.color,
|
|
background: 'transparent',
|
|
hover: vizData.color
|
|
});
|
|
relayStatusInstances.set(vizData.url, instance);
|
|
instance.animateTo(vizData.shape);
|
|
lastRelayShapes.set(vizData.url, vizData.shape);
|
|
} else {
|
|
// Update color if changed
|
|
instance.options.foreground = vizData.color;
|
|
instance.options.hover = vizData.color;
|
|
// Animate only when shape changes
|
|
const prevShape = lastRelayShapes.get(vizData.url);
|
|
if (prevShape !== vizData.shape) {
|
|
instance.animateTo(vizData.shape);
|
|
lastRelayShapes.set(vizData.url, vizData.shape);
|
|
}
|
|
}
|
|
|
|
// Update tooltip
|
|
const relayContainer = svgContainer.parentElement;
|
|
relayContainer.title = vizData.tooltip;
|
|
|
|
currentVisualRelays.add(vizData.url);
|
|
});
|
|
|
|
// Clean up instances for relays that are no longer in the list
|
|
for (const [relayUrl, instance] of relayStatusInstances) {
|
|
if (!currentVisualRelays.has(relayUrl)) {
|
|
const svgContainer = document.getElementById(`relay-status-${relayUrl.replace(/[^a-zA-Z0-9]/g, '-')}`);
|
|
if (svgContainer && svgContainer.parentElement) {
|
|
svgContainer.parentElement.remove();
|
|
}
|
|
instance.destroy();
|
|
relayStatusInstances.delete(relayUrl);
|
|
lastRelayShapes.delete(relayUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render sidenav relay section
|
|
*
|
|
* @param {Array} relayVizData - Array of relay visualization objects from prepareRelayVisualizationData()
|
|
*/
|
|
function drawRelaySidebarSection(relayVizData) {
|
|
const divRelayList = document.getElementById('divRelayList');
|
|
if (!divRelayList) {
|
|
console.warn('[relay-ui] divRelayList element not found!');
|
|
return;
|
|
}
|
|
|
|
if (!relayVizData || relayVizData.length === 0) {
|
|
divRelayList.innerHTML = '<div style="color: var(--muted-color); font-style: italic; padding: 5px;">No relays configured</div>';
|
|
return;
|
|
}
|
|
|
|
// Create signature of current relay list (URLs + statuses + types)
|
|
const currentSignature = relayVizData.map(r => `${r.url}:${r.status}:${r.type}`).join('|');
|
|
|
|
// Only rebuild if relay list changed OR if divRelayList is empty (first load)
|
|
const isEmpty = divRelayList.children.length === 0;
|
|
const needsRebuild = currentSignature !== lastRelayListSignature || isEmpty;
|
|
|
|
if (needsRebuild) {
|
|
lastRelayListSignature = currentSignature;
|
|
|
|
// Clear and rebuild relay list
|
|
divRelayList.innerHTML = '';
|
|
|
|
// Clean up all existing instances since we're rebuilding
|
|
for (const [relayUrl, instance] of sidenavRelayInstances) {
|
|
instance.destroy();
|
|
}
|
|
sidenavRelayInstances.clear();
|
|
|
|
// Build relay list HTML first
|
|
for (const vizData of relayVizData) {
|
|
const fontWeight = vizData.isConnected ? 'bold' : 'normal';
|
|
const textColor = vizData.isConnected ? 'var(--primary-color)' : 'var(--muted-color)';
|
|
|
|
// Create relay row
|
|
const relayRow = document.createElement('div');
|
|
relayRow.className = 'sidenavRowEntry';
|
|
|
|
const relayRowInner = document.createElement('div');
|
|
relayRowInner.className = 'sidenavRow';
|
|
|
|
// Non-functional indent spacer to align with blossom drag grip
|
|
const indentSpacer = document.createElement('span');
|
|
indentSpacer.className = 'sidenavRowIndent relayRowIndent';
|
|
indentSpacer.setAttribute('aria-hidden', 'true');
|
|
indentSpacer.textContent = '≡';
|
|
|
|
// Relay name
|
|
const relayName = document.createElement('span');
|
|
relayName.className = 'sidenavRowName';
|
|
relayName.style.fontWeight = fontWeight;
|
|
relayName.style.color = textColor;
|
|
relayName.title = vizData.url;
|
|
relayName.textContent = vizData.domain;
|
|
|
|
// Read/Write counts
|
|
const countsSpan = document.createElement('span');
|
|
const countsId = `sidenav-counts-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
countsSpan.id = countsId;
|
|
countsSpan.className = 'sidenavRowMeta';
|
|
countsSpan.style.color = textColor;
|
|
countsSpan.textContent = `↓${vizData.stats.readCount} ↑${vizData.stats.writeCount}`;
|
|
|
|
// Status indicator container
|
|
const statusContainer = document.createElement('div');
|
|
const statusId = `sidenav-relay-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
statusContainer.id = statusId;
|
|
statusContainer.className = 'sidenavRowToggle';
|
|
|
|
// Add click handler for disconnected relays to attempt reconnection
|
|
if (!vizData.isConnected) {
|
|
statusContainer.style.cursor = 'pointer';
|
|
statusContainer.title = `${vizData.url}\nStatus: Disconnected\nClick to reconnect`;
|
|
statusContainer.addEventListener('click', () => {
|
|
console.log(`[relay-ui] Attempting to reconnect to relay: ${vizData.url}`);
|
|
reconnectRelay(vizData.url);
|
|
});
|
|
statusContainer.dataset.reconnectHandler = 'true';
|
|
}
|
|
|
|
relayRowInner.appendChild(indentSpacer);
|
|
relayRowInner.appendChild(relayName);
|
|
relayRowInner.appendChild(countsSpan);
|
|
relayRowInner.appendChild(statusContainer);
|
|
relayRow.appendChild(relayRowInner);
|
|
divRelayList.appendChild(relayRow);
|
|
}
|
|
|
|
// Capture relayVizData for the setTimeout callback
|
|
const vizDataToProcess = relayVizData;
|
|
|
|
// Now create HamburgerMorphing instances after DOM is ready
|
|
setTimeout(() => {
|
|
for (const vizData of vizDataToProcess) {
|
|
const statusId = `sidenav-relay-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
const statusContainer = document.getElementById(statusId);
|
|
|
|
if (!statusContainer) {
|
|
console.warn(`[relay-ui] Status container not found for ${vizData.url}, id: ${statusId}`);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Create HamburgerMorphing instance
|
|
const instance = new HamburgerMorphing(`#${statusId}`, {
|
|
size: 16,
|
|
foreground: vizData.color,
|
|
background: 'transparent'
|
|
});
|
|
sidenavRelayInstances.set(vizData.url, instance);
|
|
instance.animateTo(vizData.shape);
|
|
|
|
// Ensure SVG allows pointer events to pass through to container for disconnected relays
|
|
const svg = statusContainer.querySelector('svg');
|
|
if (svg && !vizData.isConnected) {
|
|
svg.style.pointerEvents = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error(`[relay-ui] Error creating HamburgerMorphing for ${vizData.url}:`, error);
|
|
}
|
|
}
|
|
}, 100);
|
|
} else {
|
|
// Relay list hasn't changed, just update existing instances and counts
|
|
for (const vizData of relayVizData) {
|
|
const instance = sidenavRelayInstances.get(vizData.url);
|
|
|
|
// Update counts display
|
|
const countsId = `sidenav-counts-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
const countsSpan = document.getElementById(countsId);
|
|
if (countsSpan) {
|
|
countsSpan.textContent = `↓${vizData.stats.readCount} ↑${vizData.stats.writeCount}`;
|
|
}
|
|
|
|
// Update click handler for disconnected relays
|
|
const statusId = `sidenav-relay-${vizData.url.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
const statusContainer = document.getElementById(statusId);
|
|
if (statusContainer) {
|
|
const svg = statusContainer.querySelector('svg');
|
|
if (!vizData.isConnected) {
|
|
statusContainer.style.cursor = 'pointer';
|
|
statusContainer.title = `${vizData.url}\nStatus: Disconnected\nClick to reconnect`;
|
|
// Only add click listener if not already added (check for data attribute)
|
|
if (!statusContainer.dataset.reconnectHandler) {
|
|
statusContainer.addEventListener('click', () => {
|
|
console.log(`[relay-ui] Attempting to reconnect to relay: ${vizData.url}`);
|
|
reconnectRelay(vizData.url);
|
|
});
|
|
statusContainer.dataset.reconnectHandler = 'true';
|
|
}
|
|
// Ensure SVG allows pointer events to pass through to container
|
|
if (svg) svg.style.pointerEvents = 'none';
|
|
} else {
|
|
// Relay is connected - remove cursor and update tooltip
|
|
statusContainer.style.cursor = 'default';
|
|
statusContainer.title = `${vizData.url}\nStatus: Connected\nType: ${vizData.relayType}`;
|
|
// Re-enable pointer events on SVG for connected relays
|
|
if (svg) svg.style.pointerEvents = 'auto';
|
|
}
|
|
}
|
|
|
|
if (!instance) continue;
|
|
|
|
// Update color on the instance
|
|
instance.options.foreground = vizData.color;
|
|
instance.options.hover = vizData.color;
|
|
|
|
instance.animateTo(vizData.shape);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize footer relay status visualization
|
|
* Call this once during page initialization
|
|
*/
|
|
export function initFooterRelayStatus() {
|
|
console.log(`[relay-ui v${VERSION}] Initializing footer relay status`);
|
|
initFooterBalance();
|
|
bootstrapWalletForFooter();
|
|
}
|
|
|
|
/**
|
|
* Update footer relay status visuals
|
|
* Call this periodically (e.g., every second) to update relay indicators
|
|
*/
|
|
export async function updateFooterRelayStatus() {
|
|
try {
|
|
if (!footerBalanceInitialized) initFooterBalance();
|
|
bootstrapWalletForFooter();
|
|
|
|
const relayVizData = await prepareRelayVisualizationData();
|
|
drawFooterSection(relayVizData);
|
|
|
|
updateFooterBalance();
|
|
} catch (error) {
|
|
console.error('[relay-ui] Error updating footer relay status:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize sidenav relay section
|
|
* Call this once during page initialization
|
|
*/
|
|
export function initSidenavRelaySection() {
|
|
console.log(`[relay-ui v${VERSION}] Initializing sidenav relay section`);
|
|
|
|
if (relaySectionToggleInitialized) return;
|
|
|
|
const sectionEl = document.getElementById('divRelaySection');
|
|
const titleEl = document.getElementById('divRelaySectionTitle');
|
|
if (!sectionEl || !titleEl) return;
|
|
|
|
const collapsedKey = 'sidenav-relay-collapsed';
|
|
if (localStorage.getItem(collapsedKey) === 'true') {
|
|
sectionEl.classList.add('collapsed');
|
|
}
|
|
|
|
let iconEl = document.getElementById('divRelaySectionToggleIcon');
|
|
if (!iconEl) {
|
|
iconEl = document.createElement('div');
|
|
iconEl.id = 'divRelaySectionToggleIcon';
|
|
iconEl.className = 'sidenavSectionToggleIcon';
|
|
titleEl.appendChild(iconEl);
|
|
}
|
|
|
|
try {
|
|
relaySectionToggleIcon = new HamburgerMorphing('#divRelaySectionToggleIcon', {
|
|
size: 16,
|
|
foreground: 'var(--muted-color)',
|
|
background: 'transparent',
|
|
hover: 'var(--primary-color)'
|
|
});
|
|
|
|
relaySectionToggleIcon.animateTo(sectionEl.classList.contains('collapsed') ? 'burger' : 'arrow_down');
|
|
} catch (error) {
|
|
console.warn('[relay-ui] Failed to initialize relay section toggle icon:', error);
|
|
}
|
|
|
|
titleEl.addEventListener('click', () => {
|
|
sectionEl.classList.toggle('collapsed');
|
|
const isCollapsed = sectionEl.classList.contains('collapsed');
|
|
localStorage.setItem(collapsedKey, isCollapsed ? 'true' : 'false');
|
|
if (relaySectionToggleIcon) {
|
|
relaySectionToggleIcon.animateTo(isCollapsed ? 'burger' : 'arrow_down');
|
|
}
|
|
});
|
|
|
|
relaySectionToggleInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* Update sidenav relay section
|
|
* Call this periodically (e.g., every second) to update relay list
|
|
*/
|
|
export async function updateSidenavRelaySection() {
|
|
try {
|
|
const relayVizData = await prepareRelayVisualizationData();
|
|
drawRelaySidebarSection(relayVizData);
|
|
} catch (error) {
|
|
console.error('[relay-ui] Error updating sidenav relay section:', error);
|
|
}
|
|
}
|
|
|
|
console.log(`[relay-ui v${VERSION}] Module loaded`);
|