Files
client/www/fips-directory.html
T

959 lines
37 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>FIPS DIRECTORY</title>
<link rel="stylesheet" href="./css/client.css" />
<!-- Initialize theme BEFORE any components load -->
<script>
(function () {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-mode');
if (document.body) {
document.body.classList.add('dark-mode');
}
}
})();
</script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
<!-- SVG.js library (required by HamburgerMorphing) -->
<script src="./js/vendor/svg.min.js"></script>
</head>
<body>
<!-- ================================================================
HAMBURGER BUTTON (Fixed, separate from header)
================================================================
The hamburger button is a fixed element outside the header
to ensure it stays visible above the sidenav (z-index: 10 > 3).
================================================================ -->
<div id="divSvgHam" class="divHeaderButtons">
<!-- HamburgerMorphing will be injected here -->
</div>
<!-- ================================================================
HEADER
================================================================
Standard header with title (center).
================================================================ -->
<div id="divHeader">
<div id="divHeaderFlexLeft">
<!-- Hamburger is now separate fixed element -->
</div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText">FIPS DIRECTORY</div>
</div>
<div id="divHeaderFlexRight">
<!-- No button in header right - logout is in sidenav footer -->
</div>
</div>
<!-- ================================================================
BODY
================================================================
Main content area. Add your page-specific content here.
================================================================ -->
<div id="divBody">
</div>
<!-- ================================================================
FOOTER
================================================================
Three-section footer layout:
- Left: Relay status animations (HamburgerMorphing instances)
- Center: General status information
- Right: Additional information
================================================================ -->
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<!-- ================================================================
SIDENAV
================================================================
Slide-out navigation panel. Opens from left when hamburger clicked.
Uses flexbox layout to pin version bar to bottom.
Includes a version bar footer with theme toggle and logout buttons.
================================================================ -->
<div id="divSideNav">
<div id="divSideNavHeader">
<!-- No close button - use main hamburger to close -->
</div>
<div id="divSideNavBody">
<div id="divFiles"></div>
</div>
<div id="divAiSection" class="sidenavSection">
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
<div id="divAiList" class="sidenavSectionList">
<div id="divAiProvidersList">No saved providers yet.</div>
</div>
</div>
<div id="divRelaySection">
<div id="divRelaySectionTitle">
リレー
</div>
<div id="divRelayList">
Loading relays...
</div>
</div>
<div id="divBlossomSection">
<div id="divBlossomSectionTitle">ブロッサム</div>
<div id="divBlossomList">Loading blossom servers...</div>
</div>
<div id="divVersionBar">
<span id="versionDisplay">v0.0.1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
<div id="themeToggleHamburgerContainer"></div>
</button>
<button id="logoutButton" title="Logout">
<div id="logoutHamburgerContainer"></div>
</button>
</div>
</div>
</div>
<!-- ================================================================
REQUIRED SCRIPTS
================================================================
These scripts must be loaded in this order:
1. nostr.bundle.js - Nostr tools library
2. nostr-lite.js - Authentication modal (nostr-login-lite)
================================================================ -->
<script src="./nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<script type="module">
/* ================================================================
IMPORTS
================================================================
Import shared NDK functionality from init-ndk.mjs:
- initNDKPage() - Initialize authentication and worker
- getPubkey() - Get current user's pubkey
- subscribe() - Create NDK subscriptions
- publishEvent() - Publish events via NDK
- disconnect() - Disconnect from worker
- getRelayData() - Get relay connection data
- getRelayStats() - Get relay activity statistics
Import HamburgerMorphing for animated icons
================================================================ */
import {
initNDKPage,
getPubkey, injectHeaderAvatar, injectHeaderLoginButton,
subscribe,
publishEvent,
disconnect,
getVersion,
updateVersionDisplay,
getUserSettings,
patchUserSettings,
onUserSettings
} from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
// Version will be loaded asynchronously
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[fips-directory ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================
Track state for hamburger menu, relay status, and theme.
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
// FIPS directory state
let bookmarks = new Map(); // eventId -> { id, pubkey, url, title, description, createdAt }
let blocklist = new Set(); // Set of hex pubkeys blocked by current user
let myMuteListEventId = null; // event id of current user's kind 10000
let bookmarksLoaded = false;
let blocklistLoaded = false;
let bookmarkSub = null;
let muteListSub = null;
/*
AUTH STATE MODEL
------------------------------------------------------------------
This page uses optional auth: anyone can view the directory, but
login is needed to add/remove links and block publishers.
- optional (default):
Page can render public/read-only data without login, but can still
prompt login later for user actions (publish, settings, etc).
URL behavior:
- If ?auth=required|optional|none is present, it wins.
- Otherwise, defaults to optional.
*/
let isAuthenticated = false;
let authMode = 'optional';
let authedPageInitialized = false;
let relayActivityListenersBound = false;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
// App-wide user settings (NIP-78 kind 30078, d:user-settings)
let pageSettings = {};
let unsubscribeUserSettings = null;
/* ================================================================
DOM VARIABLES
================================================================
Cache DOM element references for better performance.
================================================================ */
const divBody = document.getElementById("divBody");
const divSideNav = document.getElementById("divSideNav");
const divSideNavBody = document.getElementById("divSideNavBody");
const divFooterCenter = document.getElementById("divFooterCenter");
const divFooterRight = document.getElementById("divFooterRight");
/* ================================================================
HAMBURGER MENU
================================================================
Initialize and control the animated hamburger menu.
================================================================ */
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
/* ================================================================
SIDENAV FUNCTIONS
================================================================
Open/close sidenav with hamburger morphing animation.
================================================================ */
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
// Initialize version bar buttons when sidenav opens (lazy load)
if (!logoutHamburger) {
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
logoutHamburger.animateTo('x');
}
if (!themeToggleHamburger) {
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
// Determine current theme
const savedTheme = localStorage.getItem('theme');
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
const initialShape = isDarkMode ? 'moon' : 'circle';
themeToggleHamburger.animateTo(initialShape);
}
}
function closeNav() {
divSideNav.style.width = "0vw";
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) {
hamburgerInstance.animateTo('burger');
}
}
function toggleNav() {
if (isNavOpen) {
closeNav();
} else {
openNav();
}
}
/* ================================================================
AUTH MODE HELPERS
================================================================ */
function hasTargetPubkeyInUrl() {
const params = new URLSearchParams(window.location.search || '');
const npub = String(params.get('npub') || '').trim();
const pubkey = String(params.get('pubkey') || '').trim();
return Boolean(npub || pubkey);
}
function resolveAuthModeFromUrl() {
const params = new URLSearchParams(window.location.search || '');
const explicitAuth = String(params.get('auth') || '').trim().toLowerCase();
if (explicitAuth === 'required' || explicitAuth === 'optional' || explicitAuth === 'none') {
return explicitAuth;
}
// Convention: explicit target profiles are public-readable by default.
if (hasTargetPubkeyInUrl()) {
return 'optional';
}
return 'optional';
}
function isAuthRequiredError(error) {
const message = String(error?.message || error || '').toLowerCase();
return message.includes('authentication required');
}
async function initializeAuthentication(mode) {
// required: existing behavior, throw if auth fails.
if (mode === 'required') {
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
return;
}
// none: public page, no login attempt on load.
if (mode === 'none') {
isAuthenticated = false;
currentPubkey = null;
return;
}
// optional: try silent/normal init; if auth required, continue public.
try {
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
} catch (error) {
if (isAuthRequiredError(error)) {
console.log('[fips-directory] Optional auth mode: continuing unauthenticated');
isAuthenticated = false;
currentPubkey = null;
return;
}
throw error;
}
}
async function initializeAuthenticatedPageFeatures() {
if (!isAuthenticated) {
await injectHeaderLoginButton();
return;
}
if (authedPageInitialized) return;
await injectHeaderAvatar(currentPubkey);
console.log('[fips-directory] Authenticated as:', currentPubkey);
// Hydrate app-wide user settings for this page
try {
pageSettings = await getUserSettings();
} catch (error) {
console.warn('[fips-directory] getUserSettings failed:', error);
pageSettings = {};
}
// Subscribe to live user settings updates (cross-tab + publish echoes)
if (!unsubscribeUserSettings) {
unsubscribeUserSettings = onUserSettings((settings) => {
pageSettings = settings || {};
});
}
// Initialize relay-dependent UI only once authenticated.
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
await UpdateFooter();
if (!updateIntervalId) {
updateIntervalId = setInterval(UpdateFooter, 1000);
}
// Relay activity listeners only matter after worker init/auth.
if (!relayActivityListenersBound) {
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
console.log(`[fips-directory] Relay activity: ${relayUrl} - ${activity}`, stats);
setRelayActivityState(relayUrl, activity);
});
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'relayActivity') {
const { relayUrl, activity } = event.data;
console.log(`[fips-directory] Relay activity: ${relayUrl} - ${activity}`);
setRelayActivityState(relayUrl, activity);
}
});
relayActivityListenersBound = true;
}
authedPageInitialized = true;
}
async function promptLoginIfNeeded() {
if (isAuthenticated) return true;
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
await initializeAuthenticatedPageFeatures();
return true;
}
/* ================================================================
UPDATE FOOTER
================================================================
Update footer sections with relay status, pubkey, and other info.
Called periodically by update loop.
================================================================ */
const UpdateFooter = async () => {
try {
// Update relay status visuals in footer and sidenav
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
// Clear center and right sections
divFooterCenter.innerHTML = '';
divFooterRight.innerHTML = '';
} catch (error) {
console.error('[fips-directory] Error updating footer:', error);
}
};
/* ================================================================
ESCAPE HELPER
================================================================ */
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
/* ================================================================
TAG VALUE HELPER
================================================================ */
function getTagValue(tags, name) {
const tag = tags.find(t => t[0] === name);
return tag && tag.length > 1 ? tag[1] : '';
}
/* ================================================================
FIPS BOOKMARK FUNCTIONS (NIP-B0, kind 39701)
================================================================ */
function parseBookmark(evt) {
const tags = evt.tags || [];
const dTag = getTagValue(tags, 'd') || '';
const title = getTagValue(tags, 'title') || dTag;
const url = dTag;
const description = evt.content || '';
return { id: evt.id, pubkey: evt.pubkey, url, title, description, createdAt: evt.created_at || 0 };
}
function subscribeFipsBookmarks() {
console.log('[fips-directory] Subscribing to kind 39701 bookmarks...');
bookmarkSub = subscribe(
{ kinds: [39701], '#t': ['fips-directory'], limit: 1000 },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
}
function initBookmarkListener() {
window.addEventListener('ndkEvent', (event) => {
const evt = event.detail;
if (evt.kind !== 39701) return;
const tags = evt.tags || [];
if (!tags.some(t => t[0] === 't' && t[1] === 'fips-directory')) return;
// Skip if we already have this event
if (bookmarks.has(evt.id)) return;
bookmarks.set(evt.id, parseBookmark(evt));
console.log('[fips-directory] Added bookmark:', bookmarks.get(evt.id).title);
if (bookmarksLoaded) renderDirectory();
});
window.addEventListener('ndkEose', (event) => {
if (bookmarkSub && event.detail && event.detail.subId !== bookmarkSub.subId) return;
if (!bookmarksLoaded) { bookmarksLoaded = true; renderDirectory(); }
});
}
/* ================================================================
MUTE LIST FUNCTIONS (NIP-51, kind 10000)
================================================================ */
function subscribeMuteLists() {
console.log('[fips-directory] Subscribing to kind 10000 mute lists...');
muteListSub = subscribe(
{ kinds: [10000], limit: 500 },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
}
function initMuteListListener() {
window.addEventListener('ndkEvent', (event) => {
const evt = event.detail;
if (evt.kind !== 10000) return;
// Only care about the current user's mute list
if (evt.pubkey !== currentPubkey) return;
const tags = evt.tags || [];
blocklist = new Set(tags.filter(t => t[0] === 'p').map(t => t[1]));
myMuteListEventId = evt.id;
console.log('[fips-directory] Blocklist updated:', blocklist.size, 'blocked pubkeys');
if (blocklistLoaded) renderDirectory();
});
window.addEventListener('ndkEose', (event) => {
if (muteListSub && event.detail && event.detail.subId !== muteListSub.subId) return;
if (!blocklistLoaded) { blocklistLoaded = true; renderDirectory(); }
});
}
/* ================================================================
RENDER DIRECTORY
================================================================ */
function renderDirectory() {
window.renderDirectory = renderDirectory;
// Group bookmarks by pubkey
const byPubkey = {};
bookmarks.forEach((bm) => {
if (!byPubkey[bm.pubkey]) byPubkey[bm.pubkey] = [];
byPubkey[bm.pubkey].push(bm);
});
// Sort pubkeys alphabetically by their first bookmark's title for stable order
const sortedPubkeys = Object.keys(byPubkey).sort((a, b) => {
const aName = byPubkey[a][0].title.toLowerCase();
const bName = byPubkey[b][0].title.toLowerCase();
return aName.localeCompare(bName);
});
// Add link form (only if logged in)
var addFormHtml = '';
if (isAuthenticated) {
addFormHtml =
'<div style="max-width:500px;margin-bottom:15px;border:1px solid var(--border-color);border-radius:var(--border-radius);padding:12px;">' +
'<div style="font-weight:bold;margin-bottom:10px;">Add Your FIPS Link</div>' +
'<div style="margin-bottom:8px;">' +
'<input id="fdLinkName" type="text" placeholder="Name (e.g. My Relay)" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'</div>' +
'<div style="margin-bottom:8px;">' +
'<input id="fdLinkUrl" type="text" placeholder="FIPS URL (e.g. http://npub1....fips/relay/)" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'</div>' +
'<div style="margin-bottom:8px;">' +
'<input id="fdLinkDesc" type="text" placeholder="Description (optional)" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'</div>' +
'<div><button class="btn" onclick="doAddLink()">Add Link</button></div>' +
'</div>';
}
// Build publisher sections
var contentHtml = '';
if (sortedPubkeys.length === 0) {
contentHtml = '<div style="text-align:center;padding:20px;color:var(--muted-color);">No FIPS links yet. ' +
(isAuthenticated ? 'Add one above!' : 'Sign in to add your links.') + '</div>';
} else {
sortedPubkeys.forEach(function(pubkey) {
var bms = byPubkey[pubkey];
var isOwn = isAuthenticated && pubkey === currentPubkey;
var isBlocked = blocklist.has(pubkey);
// Skip blocked publishers (unless it's the current user)
if (isBlocked && !isOwn) return;
// Publisher header
var headerHtml =
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);margin-bottom:10px;">' +
'<div style="font-weight:bold;font-size:14px;padding:10px 12px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;">' +
'<span>' + esc(pubkey.slice(0, 12) + '...' + pubkey.slice(-8)) + ' <span style="font-size:12px;color:var(--muted-color);">(' + bms.length + ')</span></span>' +
'<span>';
// Block/unblock button (only for other publishers, when logged in)
if (isAuthenticated && !isOwn) {
if (isBlocked) {
headerHtml += '<button class="btn" style="font-size:11px;padding:4px 8px;" onclick="doToggleBlock(\'' + pubkey + '\')">Unblock</button>';
} else {
headerHtml += '<button class="btn" style="font-size:11px;padding:4px 8px;" onclick="doToggleBlock(\'' + pubkey + '\')">Block</button>';
}
}
headerHtml += '</span></div>';
// Link items
bms.forEach(function(bm) {
headerHtml +=
'<div style="border-bottom:1px solid var(--border-color);font-size:12px;">' +
'<div style="padding:8px 12px;display:flex;justify-content:space-between;align-items:center;">' +
'<div style="flex:1;min-width:0;">' +
'<div><a href="' + esc(bm.url) + '" target="_blank" rel="noopener" style="font-weight:bold;color:var(--accent-color);text-decoration:none;">' + esc(bm.title) + '</a></div>' +
(bm.description ? '<div style="color:var(--muted-color);font-size:11px;margin-top:2px;">' + esc(bm.description) + '</div>' : '') +
'<div style="color:var(--muted-color);font-size:10px;margin-top:2px;word-break:break-all;">' + esc(bm.url) + '</div>' +
'</div>' +
(isOwn ? '<button class="btn" style="font-size:11px;padding:4px 8px;margin-left:8px;flex-shrink:0;" onclick="doRemoveLink(\'' + bm.id + '\')">Remove</button>' : '') +
'</div>' +
'</div>';
});
headerHtml += '</div>';
contentHtml += headerHtml;
});
}
divBody.innerHTML = '<div>' + addFormHtml + contentHtml + '</div>';
}
/* ================================================================
ADD LINK (publish kind 39701)
================================================================ */
async function doAddLink() {
if (!isAuthenticated) { var ok = await promptLoginIfNeeded(); if (!ok) { setStatus('Sign in to add links.'); return; } }
var name = document.getElementById('fdLinkName').value.trim();
var url = document.getElementById('fdLinkUrl').value.trim();
var desc = document.getElementById('fdLinkDesc').value.trim();
if (!name) { setStatus('Enter a name for the link.'); return; }
if (!url) { setStatus('Enter a FIPS URL.'); return; }
try {
var result = await publishEvent({
kind: 39701,
content: desc,
tags: [
['d', url],
['title', name],
['t', 'fips-directory'],
['published_at', String(Math.floor(Date.now() / 1000))]
],
created_at: Math.floor(Date.now() / 1000),
});
var n = (result?.relayResults?.successful || []).length;
setStatus('Link published to ' + n + ' relay(s)');
document.getElementById('fdLinkName').value = '';
document.getElementById('fdLinkUrl').value = '';
document.getElementById('fdLinkDesc').value = '';
} catch (e) { setStatus('Publish failed: ' + e.message); }
}
window.doAddLink = doAddLink;
/* ================================================================
REMOVE LINK (NIP-09 kind 5 deletion)
================================================================ */
async function doRemoveLink(eventId) {
if (!isAuthenticated) { var ok = await promptLoginIfNeeded(); if (!ok) return; }
try {
var result = await publishEvent({
kind: 5,
content: 'Removed from FIPS directory',
tags: [
['e', eventId],
['k', '39701']
],
created_at: Math.floor(Date.now() / 1000),
});
var n = (result?.relayResults?.successful || []).length;
setStatus('Link removed from ' + n + ' relay(s)');
bookmarks.delete(eventId);
renderDirectory();
} catch (e) { setStatus('Remove failed: ' + e.message); }
}
window.doRemoveLink = doRemoveLink;
/* ================================================================
TOGGLE BLOCK (NIP-51 kind 10000 mute list)
================================================================ */
async function doToggleBlock(pubkey) {
if (!isAuthenticated) { var ok = await promptLoginIfNeeded(); if (!ok) return; }
var isBlocked = blocklist.has(pubkey);
var newTags = [];
// Copy existing p tags, excluding the target pubkey if unblocking
blocklist.forEach(function(p) {
if (p !== pubkey) newTags.push(['p', p]);
});
if (!isBlocked) {
newTags.push(['p', pubkey]);
}
try {
var result = await publishEvent({
kind: 10000,
content: '',
tags: newTags,
created_at: Math.floor(Date.now() / 1000),
});
var n = (result?.relayResults?.successful || []).length;
setStatus((isBlocked ? 'Unblocked' : 'Blocked') + ' — published to ' + n + ' relay(s)');
// Update local state immediately
if (isBlocked) {
blocklist.delete(pubkey);
} else {
blocklist.add(pubkey);
}
myMuteListEventId = result?.eventId || myMuteListEventId;
renderDirectory();
} catch (e) { setStatus('Blocklist update failed: ' + e.message); }
}
window.doToggleBlock = doToggleBlock;
function setStatus(msg) {
divFooterCenter.textContent = msg;
}
/* ================================================================
LOGOUT
================================================================
Complete logout process:
1. Stop update loop
2. Disconnect from NDK worker
3. Logout from nostr-login-lite
4. Clear all storage (localStorage, sessionStorage, IndexedDB)
5. Reload page
================================================================ */
const Logout = async () => {
console.log("[fips-directory] Starting logout process...");
// Stop the update loop
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
// Disconnect from worker
disconnect();
// Logout from nostr-login-lite
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
// Clear all storage
localStorage.clear();
sessionStorage.clear();
// Clear IndexedDB
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
window.indexedDB.deleteDatabase(db.name);
}
}
}
console.log("[fips-directory] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
INITIALIZATION
================================================================
Main initialization sequence:
1. Initialize hamburger menu
2. Set up hamburger click handler
3. Resolve auth mode from URL/query policy
4. Initialize authentication based on mode
5. Initialize authenticated-only features (if signed in)
6. Set up version bar button listeners
7. Subscribe to FIPS bookmarks and mute lists
8. Update version display
================================================================ */
(async function main() {
console.log("[fips-directory] Starting initialization...");
try {
// Initialize hamburger menu first
initHamburgerMenu();
// Add click handler to hamburger
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) {
divSvgHam.addEventListener('click', toggleNav);
}
// Initialize version bar buttons
const themeToggleButton = document.getElementById('themeToggleButton');
const logoutButton = document.getElementById('logoutButton');
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
isDarkMode = !isDarkMode;
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
document.documentElement.classList.toggle('dark-mode', isDarkMode);
document.body.classList.toggle('dark-mode', isDarkMode);
if (themeToggleHamburger) {
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
}
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
// In optional/none modes this doubles as a "Sign in" entry point.
if (!isAuthenticated) {
await promptLoginIfNeeded();
return;
}
await Logout();
} catch (error) {
console.error('Logout/login action failed:', error);
}
});
}
// Resolve and initialize page auth policy.
authMode = resolveAuthModeFromUrl();
console.log('[fips-directory] Resolved auth mode:', authMode);
await initializeAuthentication(authMode);
// Initialize authenticated features only when signed in.
await initializeAuthenticatedPageFeatures();
// Set up bookmark listener and subscription (public + authenticated)
initBookmarkListener();
subscribeFipsBookmarks();
// Set up mute list listener and subscription (for blocklist)
initMuteListListener();
subscribeMuteLists();
// Optional UX note for public mode pages.
if (!isAuthenticated && (authMode === 'optional' || authMode === 'none')) {
divFooterCenter.textContent = 'Public mode';
divFooterRight.textContent = 'Sign in from side menu to add your links';
}
// Update version display
await updateVersionDisplay();
console.log('[fips-directory] Initialization complete');
} catch (error) {
console.error('[fips-directory] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Authentication Error</div>
<div style="font-size: 16px; color: #666;">${error.message}</div>
<div style="margin-top: 20px;">
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
</div>
</div>`;
}
})();
/* ================================================================
WORKER MESSAGE TYPES
================================================================
The NDK worker can send these message types:
1. 'response' - Response to init/subscribe/publish requests
- data.profile - User profile (from init)
- data.relays - User relays (from init)
- data.success - Publish success status
- data.relayResults - Relay publish results
2. 'event' - Nostr event from subscription
- Dispatched as 'ndkEvent' window event
- event.detail contains the Nostr event
3. 'eose' - End of stored events for subscription
- Dispatched as 'ndkEose' window event
- event.detail.subId contains subscription ID
4. 'signRequest' - Request to sign event/encrypt/decrypt
- Handled automatically by init-ndk.mjs
- Calls window.nostr methods and sends response
5. 'error' - Error from worker
- Logged to console automatically
6. 'relayActivity' - Relay read/write activity notification
- Dispatched as 'ndkRelayActivity' window event
- Used to animate relay status icons in footer
================================================================ */
/* ================================================================
DISTRIBUTED ARCHITECTURE NOTES
================================================================
Each page is independently accessible and self-contained:
1. Authentication persists via nostr-login-lite localStorage
- Login once on any page
- All other pages automatically authenticated
2. NDK SharedWorker is shared across all tabs/pages
- Single NDK instance manages all connections
- Subscriptions from all pages handled by one worker
- Events broadcast to all connected pages
3. Dexie cache is shared across all pages
- IndexedDB persists across sessions
- Cache-first queries are fast
- Reduces relay load
4. User settings are centralized and shared
- Worker hydrates kind 30078 (`d:user-settings`) on init
- Pages read via getUserSettings()
- Pages patch via patchUserSettings({ featureNamespace: ... })
- Pages subscribe via onUserSettings() for live updates
5. Each page can be distributed independently
- Copy template.html and customize
- No dependencies on other pages
- Works standalone or as part of suite
6. Message-based signer bridges worker and page
- Worker's NDK uses MessageBasedSigner
- Signer sends sign requests to page
- Page calls window.nostr.signEvent()
- Response sent back to worker
- NDK completes signing and publishing
7. Relay status visualization
- Footer left section shows connected relays
- Each relay has animated icon (HamburgerMorphing)
- Icons morph based on activity (read/write)
- Temporary animations show real-time activity
================================================================ */
</script>
</body>
</html>