936 lines
35 KiB
HTML
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-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, 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>
|