Files
client/build/.index-inline-check.mjs
2026-04-17 16:52:51 -04:00

505 lines
19 KiB
JavaScript

/* ================================================================
IMPORTS
================================================================ */
import { initNDKPage, getPubkey, injectHeaderAvatar, 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';
// Version will be loaded asynchronously
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[index.html ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
let startupStatusMessage = '';
let startupStatusUntil = 0;
let walletFooterStatusMessage = '';
let walletFooterStatusUntil = 0;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
/* ================================================================
DOM VARIABLES
================================================================ */
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");
/* ================================================================
APP DEFINITIONS
================================================================ */
const arrApps = [
{
id: `relays`,
svg: `<circle r="1" cx="7" cy="3" /> <circle r="1" cx="3" cy="3" /> <circle r="1" cx="3" cy="7" /> <circle r="1" cx="7" cy="7" />`,
name: `RELAYS`,
},
{
id: `db`,
svg: `<line x1="2" y1="2" x2="8" y2="2" /> <line x1="2" y1="5" x2="8" y2="5" /> <line x1="2" y1="8" x2="8" y2="8" />`,
name: `DB`
},
{
id: `tools`,
svg: `<path d="M5,8 L5,3 L8,4 L8,2 L5,3 L2,3 "/>`,
name: `TOOLS`,
},
// {
// id: `files`,
// svg: `<path d="M2,2 L2,8 "/> <path d="M4,2 L4,8 "/><path d="M6,2 L6,8"/><path d="M8,2 L8,8"/> `,
// name: `FILES`,
// },
{
id: `links`,
svg: `<path d="M2,2 L2,2 M2,5 L2,5 M2,8 L2,8 M5,2 L5,2 M5,5 L5,5 M5,8 L5,8 M8,2 L8,2 M8,5 L8,5 M8,8 L8,8 "/>`,
name: `LINKS`,
},
{
id: `profile`,
svg: `<circle cx=5 cy=5 r=3 fill="none" />`,
name: `P-FILE`,
},
{
id: `ws`,
svg: `<circle r="2.8284271247461903" cx="5" cy="5" />
<line x1="6" y1="6" x2="6" y2="6" />
<line x1="4" y1="4" x2="4" y2="4" /> `,
name: `WS`,
},
{
id: `post`,
svg: `<path d="M2,8 L8,2 L5,2 L8,2 L8,5 " /> `,
name: `POST`,
},
{
id: `feed`,
svg: `<path d="M2,5 L2,8 L5,8 L2,8 L8,2 " />`,
name: `FEED`,
},
{
id: `view`,
svg: `<path d="M2,5 L2,8 L5,8 L2,8 L8,2 " />`,
name: `VIEW`,
},
{
id: `love`,
svg: `<circle cx="3.5" cy="5" r="1.5" /><circle cx="7" cy="5" r="1.5" />`,
name: `LOVE`,
},
{
id: `msg`,
svg: `<path d="M5,2 L8,2 L8,5 L8,2 L2,8 L2,5 L2,8 L5,8 " /> `,
name: `MSG`,
},
{
id: `event`,
svg: `<circle cx=5 cy=5 r=3 fill="none" />`,
name: `EVENT`,
},
{
id: `note`,
svg: `<polygon fill="none" points="6,2 6,4 8,4 8,8 2,8 2,2 " />`,
name: `NOTE`,
},
{ id: `todo`, svg: `<path d=" M2,6 L4,8 L8,2"/>`, name: `TODO` },
{ id: `map`, svg: `<path d="M2,8 L2,2 L8,2 L8,8 L4,8 L4,4 L6,4 L6,6 "/>`, name: `MAP` },
{
id: `people`,
svg: `<path d="M2,2 L6,2 L8,6 L7,6 L7,8 L2,8 L2,2 " /> `,
name: `PEOPLE`,
},
{
id: `cal`,
svg: `<polygon points="2,3 3,6 3,8 7,8 7,6 8,3 6,8 5,8 " />
<line x1="6" y1="3" x2="6" y2="3" />
<line x1="4" y1="2" x2="4" y2="2" />
<line x1="5" y1="5" x2="5" y2="5" />
<rect x="3" y="6" width="4" height="2" />`,
name: `CAL`,
},
{
id: `block`,
svg: `<circle cx="5" cy="5" r="3" fill="none" /> <path d="M3,3 L7,7" />`,
name: `BLOCK`,
},
{
id: `conway`,
svg: `<circle cx="5" cy="2.45" r="0.85" fill="currentColor" stroke="none" />
<circle cx="7.55" cy="5" r="0.85" fill="currentColor" stroke="none" />
<circle cx="2.45" cy="7.55" r="0.85" fill="currentColor" stroke="none" />
<circle cx="5" cy="7.55" r="0.85" fill="currentColor" stroke="none" />
<circle cx="7.55" cy="7.55" r="0.85" fill="currentColor" stroke="none" />`,
name: `CONWAY`,
},
{
id: `slide-show`,
svg: `<circle cx="2" cy="2" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="5" cy="2" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="8" cy="2" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="2" cy="5" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="5" cy="5" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="8" cy="5" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="2" cy="8" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="5" cy="8" r="1.3" fill="currentColor" stroke="none"/>
<circle cx="8" cy="8" r="1.3" fill="currentColor" stroke="none"/>`,
name: `SLIDES`,
},
{
id: `blobs`,
// svg: `<rect fill="none" x="2" y="2" width="6" height="6" />`,
svg: `<rect fill="none" x="4" y="4" width="2" height="2" />`,
name: `BLOBS`,
},
{ id: `ai`, svg: `<rect x="3" y="4" width="4" height="4" /><line x1="4" y1="6" x2="4" y2="6" /><line x1="6" y1="6" x2="6" y2="6" /><line x1="5" y1="4" x2="5" y2="2" /><line x1="5" y1="2" x2="5" y2="2" />`, name: `AI` },
{ id: `skills-tv`, svg: `<rect fill="none" x="2" y="3" width="6" height="4" /><line x1="3" y1="8" x2="7" y2="8" /><line x1="5" y1="3" x2="5" y2="2" />`, name: `SKILLS TV` },
{ id: `skills-edit`, svg: ``, name: `SKILLS EDIT` },
{ id: `cashu`, svg: `<circle cx="5" cy="4" r="2.5" /><circle cx="5" cy="6" r="2.5" />`, name: `CASHU` },
{ id: `didactyl`, svg: `<path stroke-width="3.98" d="M3,3 L3,7 L7,7 L7,3" />`, name: `AGENT` },
{ id: `strudel`, svg: `<circle cx="4" cy="7" r="1" /><line x1="5" y1="7" x2="5" y2="2" /><path d="M5,2 C6,2 7,3 7,4" />`, name: `STRUDEL` },
{ id: `music`, svg: `<path d="M2,6 C2,3 3,2 5,2 C7,2 8,3 8,6" /><rect x="2" y="6" width="1" height="2" /><rect x="7" y="6" width="1" height="2" />`, name: `MUSIC` },
{ id: `keep-alive`, svg: ``, name: `KEEP ALIVE` },
{ id: `vj`, svg: ``, name: `VJ` },
{ id: `ai-tv`, svg: ``, name: `AI TV` },
];
/* ================================================================
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
================================================================ */
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
divSideNavBody.innerHTML = "Settings and options coming soon...";
// 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();
}
}
/* ================================================================
LOAD APP SCREEN
================================================================ */
const LoadAppScreen = async (providedPubkey = null) => {
const xmlns = "http://www.w3.org/2000/svg";
divBody.innerHTML = "";
const pubkey = providedPubkey || currentPubkey || null;
let divTitle = document.createElement(`div`);
divTitle.setAttribute(`id`, `divNpubTitle`);
divTitle.innerHTML = "NPUB";
let div = document.createElement(`a`);
div.setAttribute(`id`, `divNpub`);
div.setAttribute(`class`, `divAppButtons`);
if (pubkey) {
const isDark = document.body.classList.contains('dark-mode');
const svgNpub = QRCode({ msg: pubkey, dim: 170, ecl: 'L', pal: isDark ? ['#d9d9d9', '#000000'] : ['#000000', '#ffffff'] });
svgNpub.style.background = isDark ? '#000000' : '#ffffff';
svgNpub.style.padding = '6px';
svgNpub.style.borderRadius = '8px';
div.setAttribute(`href`, `./npub.html?npub=${pubkey}`);
div.setAttribute(`target`, `_blank`);
div.setAttribute(`rel`, `noopener noreferrer`);
div.appendChild(svgNpub);
div.appendChild(divTitle);
}
divBody.appendChild(div);
let htmlOut = "";
for (let Each of arrApps) {
const url = Each.id === 'feed'
? './feed.html'
: Each.id === 'view'
? './post.html?follows=true&post=false&viewed=true'
: Each.id === 'love'
? './notifications.html'
: Each.id === 'event'
? './event-management.html'
: `./${Each.id}.html`;
htmlOut += `<a id="div${Each.id}" class="divAppButtons" href="${url}" target="_blank" rel="noopener noreferrer">
<svg id="${Each.id}" class="svgAppButtons" viewBox="0 0 10 10" >
${Each.svg}
</svg>
${Each.name}
</a>`;
}
divBody.innerHTML = divBody.innerHTML + htmlOut;
divBody.style.justifyContent = `space-around`;
divBody.style.alignContent = `space-around`;
divBody.style.alignItems = ``;
divBody.style.height = "";
};
/* ================================================================
UPDATE FOOTER
================================================================ */
const UpdateFooter = async () => {
try {
// Update relay status visuals in footer and sidenav
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
// Keep startup/wallet status visible in footer center while active
const now = Date.now();
if (now < walletFooterStatusUntil && walletFooterStatusMessage) {
divFooterCenter.textContent = walletFooterStatusMessage;
} else if (now < startupStatusUntil && startupStatusMessage) {
divFooterCenter.textContent = startupStatusMessage;
} else {
divFooterCenter.innerHTML = '';
}
divFooterRight.innerHTML = '';
} catch (error) {
console.error('[index.html] Error updating footer:', error);
}
};
/* ================================================================
LOGOUT
================================================================ */
const Logout = async () => {
console.log("[index.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("[index.html] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
EVENT LISTENERS
================================================================ */
/* ================================================================
INITIALIZATION
================================================================ */
(async function main() {
console.log("[index.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);
}
});
}
// Render app shell immediately so worker startup does not block first paint
await LoadAppScreen();
// Initialize NDK (handles authentication automatically)
await initNDKPage();
// Get authenticated pubkey
currentPubkey = await getPubkey();
await injectHeaderAvatar(currentPubkey);
console.log("[index.html] Authenticated as:", currentPubkey);
// Initialize relay UI components
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
// Listen for relay activity broadcasts from worker
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
console.log(`[index.html] Relay activity: ${relayUrl} - ${activity}`, stats);
setRelayActivityState(relayUrl, activity);
});
// Show startup progress messages from worker in center footer section
window.addEventListener('ndkStartupStatus', (event) => {
const msg = String(event?.detail?.message || '').trim();
if (!msg) return;
console.log(`[index.html] Startup status: ${msg}`, event?.detail || {});
startupStatusMessage = msg;
startupStatusUntil = Date.now() + 12000;
divFooterCenter.textContent = msg;
});
// Show wallet balance/status updates from worker in center footer section
window.addEventListener('ndkWalletBalance', (event) => {
const balance = Number(event?.detail?.balance || 0);
const msg = `Wallet balance: ${balance.toLocaleString()} sats`;
walletFooterStatusMessage = msg;
walletFooterStatusUntil = Date.now() + 12000;
console.log(`[index.html] ${msg}`, event?.detail || {});
divFooterCenter.textContent = msg;
});
window.addEventListener('ndkWalletStatus', (event) => {
const status = String(event?.detail?.status || '').trim();
if (!status) return;
const msg = `Wallet status: ${status}`;
walletFooterStatusMessage = msg;
walletFooterStatusUntil = Date.now() + 12000;
console.log(`[index.html] ${msg}`, event?.detail || {});
divFooterCenter.textContent = msg;
});
// Hydrate app screen with authenticated data (NPUB QR)
await LoadAppScreen(currentPubkey);
// 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('[index.html] Initialization complete');
} catch (error) {
console.error('[index.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>`;
}
})();