Files
client/www/old/links.html
T

936 lines
35 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>Links</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"></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">
<!-- Links content will be loaded here -->
</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>
<!-- Add Link Form -->
<div style="padding: 10px; border-bottom: 1px solid var(--border-color);">
<h3 style="margin: 5px 0 10px 0;">Add Link</h3>
<input type="text" id="linkTitle" placeholder="Link Title"
style="width: calc(100% - 10px); margin-bottom: 5px; padding: 5px;" />
<input type="url" id="linkUrl" placeholder="https://example.com"
style="width: calc(100% - 10px); margin-bottom: 5px; padding: 5px;" />
<button id="btnAddLink" class="btn" style="width: 100%;">
Add Link
</button>
</div>
<!-- Upload HTML File -->
<div style="padding: 10px; border-bottom: 1px solid var(--border-color);">
<h3 style="margin: 5px 0 10px 0;">Upload HTML</h3>
<input type="file" id="myFile" accept=".html" style="width: 100%;" />
</div>
<!-- Download Bookmarks -->
<div style="padding: 10px; border-bottom: 1px solid var(--border-color);">
<h3 style="margin: 5px 0 10px 0;">Download</h3>
<button id="btnDownloadBookmarks" class="btn" style="width: 100%;">
Download Bookmarks
</button>
</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-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, subscribe, publishEvent, disconnect, getVersion, updateVersionDisplay } 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';
import { lzw_encode, lzw_decode } from './js/utilities.mjs';
// Version will be loaded asynchronously
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[links.html ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================
Track state for hamburger menu, relay status, and theme.
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
// Links-specific variables
let HTML_LINKS = "";
const D_TAG = "links";
/* ================================================================
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");
const UploadedFile = document.getElementById("myFile");
const linkTitle = document.getElementById("linkTitle");
const linkUrl = document.getElementById("linkUrl");
const btnAddLink = document.getElementById("btnAddLink");
const btnDownloadBookmarks = document.getElementById("btnDownloadBookmarks");
/* ================================================================
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();
}
}
/* ================================================================
LINKS FUNCTIONALITY
================================================================
Functions for saving and loading HTML links from Nostr events.
Uses kind 30078 (application-specific data) with encryption.
================================================================ */
/**
* Add a new link to the links page
*/
const addLink = async () => {
const title = linkTitle.value.trim();
const url = linkUrl.value.trim();
if (!title || !url) {
divFooterCenter.innerHTML = `<span style="color: orange;">⚠️ Please enter both title and URL</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
return;
}
// Validate URL
try {
new URL(url);
} catch (e) {
divFooterCenter.innerHTML = `<span style="color: orange;">⚠️ Please enter a valid URL</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
return;
}
// Create link HTML in Netscape bookmark format
const timestamp = Math.floor(Date.now() / 1000);
const linkHTML = ` <DT><A HREF="${url}" ADD_DATE="${timestamp}">${title}</A>\n`;
// If there's existing content, append to it
if (HTML_LINKS) {
// Try to find a good insertion point (before closing DL or H1)
if (HTML_LINKS.includes('</DL><p>')) {
HTML_LINKS = HTML_LINKS.replace('</DL><p>', linkHTML + ' </DL><p>');
} else if (HTML_LINKS.includes('</DL>')) {
HTML_LINKS = HTML_LINKS.replace('</DL>', linkHTML + ' </DL>');
} else {
HTML_LINKS += linkHTML;
}
} else {
// Create new Netscape bookmark format structure
HTML_LINKS = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3 ADD_DATE="${timestamp}" PERSONAL_TOOLBAR_FOLDER="true">My Bookmarks</H3>
<DL><p>
${linkHTML} </DL><p>
</DL><p>`;
}
// Update display
divBody.innerHTML = HTML_LINKS;
// Save to Nostr
await SaveFileAsGzipEncrypted30078(HTML_LINKS, D_TAG);
// Clear form
linkTitle.value = '';
linkUrl.value = '';
divFooterCenter.innerHTML = `<span style="color: green;">✅ Link added successfully!</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
};
/**
* Download bookmarks as HTML file
*/
const downloadBookmarks = () => {
if (!HTML_LINKS) {
divFooterCenter.innerHTML = `<span style="color: orange;">⚠️ No bookmarks to download</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
return;
}
// Create blob with the HTML content
const blob = new Blob([HTML_LINKS], { type: 'text/html' });
const url = URL.createObjectURL(blob);
// Create temporary link element
const a = document.createElement('a');
a.href = url;
a.download = 'bookmarks.html';
document.body.appendChild(a);
a.click();
// Clean up
document.body.removeChild(a);
URL.revokeObjectURL(url);
divFooterCenter.innerHTML = `<span style="color: green;">✅ Bookmarks downloaded!</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
};
/**
* Decrypt and decompress a 30078 event content
* @param {string} d - The d-tag identifier
* @returns {string} - Decrypted and decompressed content
*/
const strDecryptedUngzip30078File = async (d) => {
try {
console.log(`[links.html] Loading content for d-tag: ${d}`);
// Subscribe to kind 30078 events with the specific d-tag
const sub = subscribe(
{
kinds: [30078],
authors: [currentPubkey],
"#d": [d],
limit: 1
},
{ closeOnEose: true, cacheUsage: 'CACHE_FIRST' }
);
// Wait for the event
return new Promise((resolve) => {
const eventHandler = async (event) => {
const evt = event.detail;
if (evt.kind === 30078 && evt.tags.find(t => t[0] === 'd' && t[1] === d)) {
console.log(`[links.html] Found event for d-tag: ${d}`);
window.removeEventListener('ndkEvent', eventHandler);
try {
let strTemp = evt.content;
console.log(`[links.html] Encrypted size: ${strTemp.length}`);
// Decompress
strTemp = await lzw_decode(strTemp);
console.log(`[links.html] Decompressed size: ${strTemp.length}`);
// Decrypt using NIP-04
strTemp = await window.nostr.nip04.decrypt(currentPubkey, strTemp);
console.log(`[links.html] Decrypted successfully`);
resolve(strTemp);
} catch (error) {
console.error('[links.html] Error decrypting content:', error);
resolve("");
}
}
};
const eoseHandler = (event) => {
console.log(`[links.html] EOSE received, no content found for d-tag: ${d}`);
window.removeEventListener('ndkEvent', eventHandler);
window.removeEventListener('ndkEose', eoseHandler);
resolve("");
};
window.addEventListener('ndkEvent', eventHandler);
window.addEventListener('ndkEose', eoseHandler);
});
} catch (error) {
console.error('[links.html] Error loading content:', error);
return "";
}
};
/**
* Save HTML content as encrypted and compressed 30078 event
* @param {string} File - The HTML content to save
* @param {string} d - The d-tag identifier
*/
const SaveFileAsGzipEncrypted30078 = async (File, d) => {
try {
console.log(`[links.html] Original size: ${File.length}`);
let objEvent = {};
objEvent.created_at = Math.floor(Date.now() / 1000);
objEvent.kind = 30078;
objEvent.tags = [["d", d]];
// Encrypt using NIP-04
objEvent.content = await window.nostr.nip04.encrypt(currentPubkey, File);
console.log(`[links.html] Encrypted size: ${objEvent.content.length}`);
// Compress
objEvent.content = await lzw_encode(objEvent.content);
console.log(`[links.html] Compressed size: ${objEvent.content.length}`);
console.log(`[links.html] Overall size: ${JSON.stringify(objEvent).length}`);
// Publish the event
const result = await publishEvent(objEvent);
console.log(`[links.html] ✅ Published to:`, result.relayResults.successful);
console.log(`[links.html] ❌ Failed:`, result.relayResults.failed);
divFooterCenter.innerHTML = `<span style="color: green;">✅ Links saved successfully!</span>`;
setTimeout(() => {
divFooterCenter.innerHTML = '';
}, 3000);
} catch (error) {
console.error('[links.html] Error saving content:', error);
divFooterCenter.innerHTML = `<span style="color: red;">❌ Error saving links</span>`;
}
};
/**
* Upload HTML file and save to Nostr
*/
const UploadHTMLFile = async () => {
const file = UploadedFile.files[0];
if (!file) {
console.log('[links.html] No file selected');
return;
}
const reader = new FileReader();
reader.addEventListener("load", async () => {
HTML_LINKS = reader.result;
divBody.innerHTML = HTML_LINKS;
await SaveFileAsGzipEncrypted30078(HTML_LINKS, D_TAG);
}, false);
reader.readAsText(file);
};
/* ================================================================
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('[template.html] Error updating footer:', error);
}
};
/* ================================================================
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("[template.html] 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("[template.html] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
EVENT LISTENERS
================================================================
Wire up UI interactions.
Main hamburger button click handler is set up in main() after initialization.
================================================================ */
/* ================================================================
SUBSCRIPTION EXAMPLE
================================================================
Example of how to subscribe to Nostr events:
const sub = subscribe(
{ kinds: [1], authors: [pubkey], limit: 10 },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
Cache usage options:
- 'CACHE_FIRST' - Check cache first, then relays
- 'ONLY_RELAY' - Only query relays
- 'ONLY_CACHE' - Only query cache
- 'PARALLEL' - Query cache and relays simultaneously
Listen for events via window events:
window.addEventListener('ndkEvent', (event) => {
const evt = event.detail;
console.log('Received event:', evt);
});
================================================================ */
/* ================================================================
PUBLISH EXAMPLE
================================================================
Example of how to publish a Nostr event:
const event = {
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: "Hello, Nostr!"
};
try {
const result = await publishEvent(event);
console.log("✅ Published to:", result.relayResults.successful);
console.log("❌ Failed:", result.relayResults.failed);
console.log("Total relays:", result.totalRelays);
} catch (error) {
console.error("Publish error:", error);
}
Note: Events are automatically signed by the NDK worker using
the message-based signer (which calls window.nostr.signEvent).
================================================================ */
/* ================================================================
INITIALIZATION
================================================================
Main initialization sequence:
1. Initialize hamburger menu
2. Set up hamburger click handler
3. Initialize NDK (handles authentication automatically)
4. Get authenticated pubkey
5. Set up relay activity listeners
6. Set up version bar button listeners
7. Start update loop
The initNDKPage() function:
- Checks if already authenticated (via nostr-login-lite)
- If not authenticated, shows login modal
- Connects to NDK SharedWorker
- Returns when authentication is complete
Authentication persists across pages via nostr-login-lite's
localStorage, so users only need to login once.
================================================================ */
(async function main() {
console.log("[links.html] 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;
if (isDarkMode) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
// Save sidenav state before reload
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
window.location.reload();
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
await Logout();
} catch (error) {
console.error('Logout failed:', error);
}
});
}
// Set up file upload listener
if (UploadedFile) {
UploadedFile.addEventListener("change", UploadHTMLFile, false);
}
// Set up add link button listener
if (btnAddLink) {
btnAddLink.addEventListener("click", addLink);
}
// Set up download bookmarks button listener
if (btnDownloadBookmarks) {
btnDownloadBookmarks.addEventListener("click", downloadBookmarks);
}
// Allow Enter key in URL field to add link
if (linkUrl) {
linkUrl.addEventListener("keypress", (e) => {
if (e.key === 'Enter') {
addLink();
}
});
}
// Initialize NDK (handles authentication automatically)
await initNDKPage();
// Get authenticated pubkey
currentPubkey = await getPubkey();
await injectHeaderAvatar(currentPubkey);
console.log("[links.html] Authenticated as:", currentPubkey);
// Load saved links content
const savedContent = await strDecryptedUngzip30078File(D_TAG);
if (savedContent) {
divBody.innerHTML = savedContent;
HTML_LINKS = savedContent;
console.log("[links.html] Loaded saved links");
} else {
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<h2>No saved links found</h2>
<p>Upload an HTML file using the file input in the side navigation to save your links.</p>
</div>`;
}
// Initialize relay UI components
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
await UpdateFooter(); // Initial update before interval starts
// Listen for relay activity broadcasts from worker
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
console.log(`[template.html] Relay activity: ${relayUrl} - ${activity}`, stats);
setRelayActivityState(relayUrl, activity);
});
// Listen for relay activity broadcasts from worker (alternative message format)
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'relayActivity') {
const { relayUrl, activity } = event.data;
console.log(`[template.html] Relay activity: ${relayUrl} - ${activity}`);
setRelayActivityState(relayUrl, activity);
}
});
/* ============================================================
EXAMPLE: Subscribe to events
============================================================
Uncomment to subscribe to user's notes:
const notesSub = subscribe(
{ kinds: [1], authors: [currentPubkey], limit: 10 },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
console.log("[template.html] Subscribed to kind 1");
============================================================ */
/* ============================================================
EXAMPLE: Listen for events from worker
============================================================
Uncomment to handle incoming events:
window.addEventListener('ndkEvent', (event) => {
const evt = event.detail;
console.log("[template.html] Received event:", evt.kind, evt.pubkey);
if (evt.pubkey === currentPubkey) {
if (evt.kind === 1) {
// Handle note event
console.log("Note:", evt.content);
}
}
});
============================================================ */
/* ============================================================
EXAMPLE: Listen for cached profile
============================================================
Uncomment to handle cached profile data:
window.addEventListener('ndkProfile', (event) => {
console.log("[template.html] Cached profile:", event.detail);
// event.detail contains profile object (name, about, etc.)
});
============================================================ */
// Restore sidenav state if it was open before theme toggle
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
if (sidenavWasOpen === 'true') {
localStorage.removeItem('sidenavWasOpen');
openNav();
}
// Start update loop (updates footer every second)
updateIntervalId = setInterval(UpdateFooter, 1000);
// Update version display
await updateVersionDisplay();
console.log('[links.html] Initialization complete');
} catch (error) {
console.error('[links.html] 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. Each page can be distributed independently
- Copy template.html and customize
- No dependencies on other pages
- Works standalone or as part of suite
5. 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
6. 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>