1376 lines
46 KiB
HTML
1376 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<html lang="en" dir="ltr">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>TEMPLATE</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>
|
|
<style>
|
|
#divBody {
|
|
flex-direction: column !important;
|
|
flex-wrap: nowrap !important;
|
|
align-items: stretch !important;
|
|
justify-content: flex-start !important;
|
|
align-content: flex-start !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
#divConwayShell {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
#conwayCanvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
cursor: crosshair;
|
|
touch-action: none;
|
|
}
|
|
|
|
#divConwayControlsFooter {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
white-space: nowrap;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
.conwayControlWord {
|
|
color: var(--primary-color);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
font-size: 80%;
|
|
line-height: 1;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.conwayControlWord:hover {
|
|
color: var(--button-hover-color);
|
|
}
|
|
|
|
.conwayControlWord.is-disabled {
|
|
opacity: 0.45;
|
|
}
|
|
</style>
|
|
</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">
|
|
<div id="divConwayShell">
|
|
<canvas id="conwayCanvas"></canvas>
|
|
</div>
|
|
</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 id="divConwayControlsFooter">
|
|
<span class="conwayControlWord" data-action="play">play</span>
|
|
<span class="conwayControlWord" data-action="pause">pause</span>
|
|
<span class="conwayControlWord" data-action="step">step</span>
|
|
<span class="conwayControlWord" data-action="seed">seed</span>
|
|
<span class="conwayControlWord" data-action="clear">clear</span>
|
|
<span class="conwayControlWord" data-action="full">full</span>
|
|
</div>
|
|
</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-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,
|
|
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(`[conway.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;
|
|
|
|
// 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");
|
|
|
|
const divConwayShell = document.getElementById('divConwayShell');
|
|
const conwayCanvas = document.getElementById('conwayCanvas');
|
|
const divConwayControlsFooter = document.getElementById('divConwayControlsFooter');
|
|
|
|
/* ================================================================
|
|
CONWAY GAME OF LIFE
|
|
================================================================ */
|
|
const CELL_SIZE = 24;
|
|
const BASE_STEP_INTERVAL_MS = 100;
|
|
const NOSTR_IMPRINT_COOLDOWN_MS = 5000;
|
|
|
|
let conwayCols = 0;
|
|
let conwayRows = 0;
|
|
let conwayGrid = [];
|
|
let conwayNextGrid = [];
|
|
let conwayCtx = null;
|
|
let isConwayRunning = true;
|
|
let isPointerDown = false;
|
|
let lastConwayStep = 0;
|
|
let conwayAnimationFrame = null;
|
|
let currentStepIntervalMs = BASE_STEP_INTERVAL_MS;
|
|
let conwaySpeedRamp = null;
|
|
|
|
let followedPubkeys = [];
|
|
let followedPubkeySet = new Set();
|
|
let followListSub = null;
|
|
let followedNotesSub = null;
|
|
let firehoseNotesSub = null;
|
|
let useFirehoseMode = false;
|
|
let conwayNostrListenerAttached = false;
|
|
let imprintInProgress = false;
|
|
let lastNostrImprintAt = 0;
|
|
const seenImprintEventIds = new Set();
|
|
|
|
function createConwayGrid(c, r) {
|
|
const g = [];
|
|
for (let y = 0; y < r; y++) {
|
|
g[y] = new Uint8Array(c);
|
|
}
|
|
return g;
|
|
}
|
|
|
|
function resizeConway() {
|
|
if (!conwayCanvas || !divConwayShell) {
|
|
return;
|
|
}
|
|
|
|
const { clientWidth, clientHeight } = divConwayShell;
|
|
conwayCanvas.width = clientWidth;
|
|
conwayCanvas.height = clientHeight;
|
|
|
|
const nextCols = Math.ceil(clientWidth / CELL_SIZE);
|
|
const nextRows = Math.ceil(clientHeight / CELL_SIZE);
|
|
const resizedGrid = createConwayGrid(nextCols, nextRows);
|
|
|
|
if (conwayGrid.length > 0) {
|
|
for (let y = 0; y < Math.min(conwayRows, nextRows); y++) {
|
|
for (let x = 0; x < Math.min(conwayCols, nextCols); x++) {
|
|
resizedGrid[y][x] = conwayGrid[y][x];
|
|
}
|
|
}
|
|
}
|
|
|
|
conwayCols = nextCols;
|
|
conwayRows = nextRows;
|
|
conwayGrid = resizedGrid;
|
|
conwayNextGrid = createConwayGrid(conwayCols, conwayRows);
|
|
}
|
|
|
|
function countConwayNeighbors(g, x, y) {
|
|
let count = 0;
|
|
for (let dy = -1; dy <= 1; dy++) {
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
if (dx === 0 && dy === 0) continue;
|
|
const nx = (x + dx + conwayCols) % conwayCols;
|
|
const ny = (y + dy + conwayRows) % conwayRows;
|
|
count += g[ny][nx];
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function stepConway() {
|
|
for (let y = 0; y < conwayRows; y++) {
|
|
for (let x = 0; x < conwayCols; x++) {
|
|
const neighbors = countConwayNeighbors(conwayGrid, x, y);
|
|
if (conwayGrid[y][x]) {
|
|
conwayNextGrid[y][x] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
|
|
} else {
|
|
conwayNextGrid[y][x] = (neighbors === 3) ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
[conwayGrid, conwayNextGrid] = [conwayNextGrid, conwayGrid];
|
|
}
|
|
|
|
function drawConway() {
|
|
if (!conwayCtx) {
|
|
return;
|
|
}
|
|
|
|
const styles = getComputedStyle(document.body);
|
|
const secondary = styles.getPropertyValue('--secondary-color').trim() || '#ffffff';
|
|
const primary = styles.getPropertyValue('--primary-color').trim() || '#000000';
|
|
const accent = styles.getPropertyValue('--accent-color').trim() || '#ff0000';
|
|
|
|
conwayCtx.fillStyle = secondary;
|
|
conwayCtx.fillRect(0, 0, conwayCanvas.width, conwayCanvas.height);
|
|
|
|
conwayCtx.strokeStyle = primary;
|
|
conwayCtx.globalAlpha = 0.12;
|
|
conwayCtx.lineWidth = 0.5;
|
|
conwayCtx.beginPath();
|
|
for (let x = 0; x <= conwayCols; x++) {
|
|
conwayCtx.moveTo(x * CELL_SIZE, 0);
|
|
conwayCtx.lineTo(x * CELL_SIZE, conwayRows * CELL_SIZE);
|
|
}
|
|
for (let y = 0; y <= conwayRows; y++) {
|
|
conwayCtx.moveTo(0, y * CELL_SIZE);
|
|
conwayCtx.lineTo(conwayCols * CELL_SIZE, y * CELL_SIZE);
|
|
}
|
|
conwayCtx.stroke();
|
|
conwayCtx.globalAlpha = 1;
|
|
|
|
conwayCtx.fillStyle = accent;
|
|
for (let y = 0; y < conwayRows; y++) {
|
|
for (let x = 0; x < conwayCols; x++) {
|
|
if (conwayGrid[y][x]) {
|
|
conwayCtx.fillRect(x * CELL_SIZE + 1, y * CELL_SIZE + 1, CELL_SIZE - 1, CELL_SIZE - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function conwayPointToCell(clientX, clientY) {
|
|
const rect = conwayCanvas.getBoundingClientRect();
|
|
const x = Math.floor((clientX - rect.left) / CELL_SIZE);
|
|
const y = Math.floor((clientY - rect.top) / CELL_SIZE);
|
|
return { x, y };
|
|
}
|
|
|
|
function paintConwayCell(clientX, clientY) {
|
|
const { x, y } = conwayPointToCell(clientX, clientY);
|
|
if (x >= 0 && x < conwayCols && y >= 0 && y < conwayRows) {
|
|
conwayGrid[y][x] = 1;
|
|
}
|
|
}
|
|
|
|
function clearConway() {
|
|
conwayGrid = createConwayGrid(conwayCols, conwayRows);
|
|
conwayNextGrid = createConwayGrid(conwayCols, conwayRows);
|
|
}
|
|
|
|
function addConwayPatternAtRandom() {
|
|
const rx = Math.floor(Math.random() * conwayCols);
|
|
const ry = Math.floor(Math.random() * conwayRows);
|
|
const patterns = [
|
|
[[0, 0], [1, 0], [2, 0], [2, -1], [1, -2]],
|
|
[[0, 0], [1, 0], [-1, 0], [0, -1], [1, -1]],
|
|
[[0, 0], [1, 0], [2, 0], [3, 0], [4, -1], [4, -3], [0, -3], [1, -3]],
|
|
[[0, 0], [1, 0], [1, -2], [3, -1], [4, 0], [5, 0], [6, 0]]
|
|
];
|
|
|
|
const pattern = patterns[Math.floor(Math.random() * patterns.length)];
|
|
for (const [dx, dy] of pattern) {
|
|
const nx = (rx + dx + conwayCols) % conwayCols;
|
|
const ny = (ry + dy + conwayRows) % conwayRows;
|
|
conwayGrid[ny][nx] = 1;
|
|
}
|
|
}
|
|
|
|
function addConwayRandomSeed() {
|
|
const centerX = Math.floor(conwayCols / 2);
|
|
const centerY = Math.floor(conwayRows / 2);
|
|
const spread = 5;
|
|
for (let i = 0; i < 15; i++) {
|
|
const x = centerX + Math.floor(Math.random() * spread * 2 - spread);
|
|
const y = centerY + Math.floor(Math.random() * spread * 2 - spread);
|
|
if (x >= 0 && x < conwayCols && y >= 0 && y < conwayRows) {
|
|
conwayGrid[y][x] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
function addConwayGliderGun(ox, oy) {
|
|
const cells = [
|
|
[0, 4], [0, 5], [1, 4], [1, 5],
|
|
[10, 4], [10, 5], [10, 6], [11, 3], [11, 7], [12, 2], [12, 8], [13, 2], [13, 8],
|
|
[14, 5], [15, 3], [15, 7], [16, 4], [16, 5], [16, 6], [17, 5],
|
|
[20, 2], [20, 3], [20, 4], [21, 2], [21, 3], [21, 4], [22, 1], [22, 5],
|
|
[24, 0], [24, 1], [24, 5], [24, 6],
|
|
[34, 2], [34, 3], [35, 2], [35, 3]
|
|
];
|
|
|
|
for (const [cx, cy] of cells) {
|
|
const nx = ox + cx;
|
|
const ny = oy + cy;
|
|
if (nx >= 0 && nx < conwayCols && ny >= 0 && ny < conwayRows) {
|
|
conwayGrid[ny][nx] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
function syncConwayControlState() {
|
|
if (!divConwayControlsFooter) return;
|
|
const playWord = divConwayControlsFooter.querySelector('[data-action="play"]');
|
|
const pauseWord = divConwayControlsFooter.querySelector('[data-action="pause"]');
|
|
if (playWord) {
|
|
playWord.classList.toggle('is-disabled', isConwayRunning);
|
|
}
|
|
if (pauseWord) {
|
|
pauseWord.classList.toggle('is-disabled', !isConwayRunning);
|
|
}
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function startConwaySpeedRamp(targetIntervalMs, durationMs) {
|
|
conwaySpeedRamp = {
|
|
startTime: performance.now(),
|
|
from: currentStepIntervalMs,
|
|
to: targetIntervalMs,
|
|
duration: Math.max(1, durationMs)
|
|
};
|
|
}
|
|
|
|
function updateConwaySpeedRamp(timestamp) {
|
|
if (!conwaySpeedRamp) {
|
|
return;
|
|
}
|
|
const elapsed = timestamp - conwaySpeedRamp.startTime;
|
|
const t = Math.max(0, Math.min(1, elapsed / conwaySpeedRamp.duration));
|
|
currentStepIntervalMs = conwaySpeedRamp.from + (conwaySpeedRamp.to - conwaySpeedRamp.from) * t;
|
|
if (t >= 1) {
|
|
currentStepIntervalMs = conwaySpeedRamp.to;
|
|
conwaySpeedRamp = null;
|
|
}
|
|
}
|
|
|
|
function isLikelyKind1Repost(evt) {
|
|
const content = String(evt?.content || '').trim();
|
|
if (!content) {
|
|
return false;
|
|
}
|
|
|
|
if (/^(nostr:)?(note1|nevent1)[a-z0-9]+$/i.test(content)) {
|
|
return true;
|
|
}
|
|
|
|
const hasKind1Tag = Array.isArray(evt?.tags) && evt.tags.some((tag) => tag?.[0] === 'k' && tag?.[1] === '1');
|
|
if (hasKind1Tag && /^(nostr:)?(note1|nevent1)/i.test(content)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function extractImprintText(rawContent) {
|
|
return String(rawContent || '')
|
|
.toUpperCase()
|
|
.replace(/[^A-Z0-9]/g, '')
|
|
.slice(0, 5);
|
|
}
|
|
|
|
function imprintTextIntoConwayGrid(rawContent) {
|
|
const text = extractImprintText(rawContent);
|
|
if (!text || conwayCols <= 0 || conwayRows <= 0) {
|
|
return false;
|
|
}
|
|
|
|
const offscreen = document.createElement('canvas');
|
|
offscreen.width = conwayCols;
|
|
offscreen.height = conwayRows;
|
|
const offCtx = offscreen.getContext('2d');
|
|
if (!offCtx) {
|
|
return false;
|
|
}
|
|
|
|
offCtx.clearRect(0, 0, conwayCols, conwayRows);
|
|
offCtx.textAlign = 'center';
|
|
offCtx.textBaseline = 'middle';
|
|
offCtx.fillStyle = '#ffffff';
|
|
|
|
let fontSize = Math.max(8, Math.floor(conwayRows * 0.38));
|
|
while (fontSize > 6) {
|
|
offCtx.font = `700 ${fontSize}px pageMono, monospace`;
|
|
const width = offCtx.measureText(text).width;
|
|
if (width <= conwayCols * 0.9) {
|
|
break;
|
|
}
|
|
fontSize -= 1;
|
|
}
|
|
|
|
offCtx.font = `700 ${fontSize}px pageMono, monospace`;
|
|
offCtx.fillText(text, Math.floor(conwayCols / 2), Math.floor(conwayRows / 2));
|
|
|
|
const { data, width, height } = offCtx.getImageData(0, 0, conwayCols, conwayRows);
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const alpha = data[(y * width + x) * 4 + 3];
|
|
if (alpha > 120) {
|
|
conwayGrid[y][x] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function performNostrImprint(rawContent) {
|
|
if (imprintInProgress) {
|
|
return;
|
|
}
|
|
|
|
const text = extractImprintText(rawContent);
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (now - lastNostrImprintAt < NOSTR_IMPRINT_COOLDOWN_MS) {
|
|
return;
|
|
}
|
|
|
|
imprintInProgress = true;
|
|
lastNostrImprintAt = now;
|
|
|
|
try {
|
|
isConwayRunning = true;
|
|
syncConwayControlState();
|
|
startConwaySpeedRamp(320, 900);
|
|
await sleep(950);
|
|
|
|
isConwayRunning = false;
|
|
syncConwayControlState();
|
|
imprintTextIntoConwayGrid(text);
|
|
drawConway();
|
|
await sleep(1400);
|
|
|
|
isConwayRunning = true;
|
|
currentStepIntervalMs = 360;
|
|
startConwaySpeedRamp(BASE_STEP_INTERVAL_MS, 1800);
|
|
syncConwayControlState();
|
|
} catch (error) {
|
|
console.warn('[conway.html] performNostrImprint failed:', error);
|
|
} finally {
|
|
imprintInProgress = false;
|
|
}
|
|
}
|
|
|
|
function parseFollowedPubkeysFromKind3(evt) {
|
|
if (!evt || evt.kind !== 3 || !Array.isArray(evt.tags)) {
|
|
return [];
|
|
}
|
|
|
|
const pTags = evt.tags.filter((tag) => tag?.[0] === 'p' && typeof tag?.[1] === 'string' && tag[1].length > 0);
|
|
return [...new Set(pTags.map((tag) => tag[1]))];
|
|
}
|
|
|
|
function setConwayNoteSourceMode(mode) {
|
|
if (mode === 'firehose') {
|
|
useFirehoseMode = true;
|
|
|
|
if (followedNotesSub?.unsubscribe) {
|
|
followedNotesSub.unsubscribe();
|
|
followedNotesSub = null;
|
|
}
|
|
|
|
if (!firehoseNotesSub) {
|
|
firehoseNotesSub = subscribe(
|
|
{ kinds: [1], limit: 500 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
console.log('[conway.html] Using kind-1 firehose mode');
|
|
}
|
|
return;
|
|
}
|
|
|
|
useFirehoseMode = false;
|
|
|
|
if (firehoseNotesSub?.unsubscribe) {
|
|
firehoseNotesSub.unsubscribe();
|
|
firehoseNotesSub = null;
|
|
}
|
|
|
|
if (followedNotesSub?.unsubscribe) {
|
|
followedNotesSub.unsubscribe();
|
|
}
|
|
|
|
if (!followedPubkeys.length) {
|
|
followedNotesSub = null;
|
|
return;
|
|
}
|
|
|
|
followedNotesSub = subscribe(
|
|
{ kinds: [1], authors: followedPubkeys, limit: 500 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
console.log('[conway.html] Subscribed to followed kind 1 notes:', followedPubkeys.length);
|
|
}
|
|
|
|
function resubscribeFollowedNotes() {
|
|
if (!followedPubkeys.length) {
|
|
setConwayNoteSourceMode('firehose');
|
|
return;
|
|
}
|
|
|
|
setConwayNoteSourceMode('followed');
|
|
}
|
|
|
|
function updateFollowListFromKind3(evt, source = 'event') {
|
|
const nextPubkeys = parseFollowedPubkeysFromKind3(evt);
|
|
const nextSet = new Set(nextPubkeys);
|
|
const sameSize = nextSet.size === followedPubkeySet.size;
|
|
const unchanged = sameSize && [...nextSet].every((pk) => followedPubkeySet.has(pk));
|
|
|
|
if (unchanged) {
|
|
return;
|
|
}
|
|
|
|
followedPubkeys = nextPubkeys;
|
|
followedPubkeySet = nextSet;
|
|
console.log('[conway.html] Updated follows from kind 3:', { source, count: followedPubkeys.length });
|
|
resubscribeFollowedNotes();
|
|
}
|
|
|
|
function rememberImprintEventId(evtId) {
|
|
if (!evtId) return false;
|
|
if (seenImprintEventIds.has(evtId)) {
|
|
return true;
|
|
}
|
|
seenImprintEventIds.add(evtId);
|
|
if (seenImprintEventIds.size > 5000) {
|
|
const iter = seenImprintEventIds.values();
|
|
const first = iter.next().value;
|
|
if (first) {
|
|
seenImprintEventIds.delete(first);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function handleConwayNostrEvent(event) {
|
|
const evt = event?.detail;
|
|
if (!evt || typeof evt !== 'object') {
|
|
return;
|
|
}
|
|
|
|
if (evt.kind === 3 && evt.pubkey === currentPubkey) {
|
|
updateFollowListFromKind3(evt, 'live');
|
|
return;
|
|
}
|
|
|
|
if (evt.kind !== 1) {
|
|
return;
|
|
}
|
|
|
|
if (!useFirehoseMode && !followedPubkeySet.has(evt.pubkey)) {
|
|
return;
|
|
}
|
|
|
|
if (rememberImprintEventId(evt.id)) {
|
|
return;
|
|
}
|
|
|
|
if (isLikelyKind1Repost(evt)) {
|
|
return;
|
|
}
|
|
|
|
performNostrImprint(evt.content || '');
|
|
}
|
|
|
|
function initConwayNostrBridge() {
|
|
if (!conwayNostrListenerAttached) {
|
|
window.addEventListener('ndkEvent', handleConwayNostrEvent);
|
|
conwayNostrListenerAttached = true;
|
|
}
|
|
|
|
if (!currentPubkey) {
|
|
setConwayNoteSourceMode('firehose');
|
|
return;
|
|
}
|
|
|
|
// Start with firehose until follows load; auto-switches to followed mode when non-empty.
|
|
setConwayNoteSourceMode('firehose');
|
|
|
|
if (followListSub?.unsubscribe) {
|
|
followListSub.unsubscribe();
|
|
}
|
|
|
|
followListSub = subscribe(
|
|
{ kinds: [3], authors: [currentPubkey], limit: 1 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
console.log('[conway.html] Subscribed to current user kind 3 follow list');
|
|
}
|
|
|
|
async function toggleConwayFullscreen() {
|
|
if (!divConwayShell) return;
|
|
if (document.fullscreenElement === divConwayShell) {
|
|
await document.exitFullscreen();
|
|
} else if (!document.fullscreenElement) {
|
|
await divConwayShell.requestFullscreen();
|
|
}
|
|
resizeConway();
|
|
drawConway();
|
|
}
|
|
|
|
function conwayLoop(timestamp) {
|
|
updateConwaySpeedRamp(timestamp);
|
|
if (timestamp - lastConwayStep >= currentStepIntervalMs) {
|
|
if (isConwayRunning) {
|
|
stepConway();
|
|
}
|
|
lastConwayStep = timestamp;
|
|
}
|
|
drawConway();
|
|
conwayAnimationFrame = requestAnimationFrame(conwayLoop);
|
|
}
|
|
|
|
function initConway() {
|
|
if (!conwayCanvas) {
|
|
return;
|
|
}
|
|
|
|
conwayCtx = conwayCanvas.getContext('2d');
|
|
resizeConway();
|
|
clearConway();
|
|
addConwayRandomSeed();
|
|
if (conwayCols > 40 && conwayRows > 20) {
|
|
addConwayGliderGun(2, 2);
|
|
}
|
|
|
|
conwayCanvas.addEventListener('pointerdown', (event) => {
|
|
isPointerDown = true;
|
|
paintConwayCell(event.clientX, event.clientY);
|
|
});
|
|
|
|
conwayCanvas.addEventListener('pointermove', (event) => {
|
|
if (!isPointerDown) return;
|
|
paintConwayCell(event.clientX, event.clientY);
|
|
});
|
|
|
|
window.addEventListener('pointerup', () => {
|
|
isPointerDown = false;
|
|
});
|
|
|
|
window.addEventListener('keydown', () => {
|
|
addConwayPatternAtRandom();
|
|
});
|
|
|
|
window.addEventListener('resize', resizeConway);
|
|
document.addEventListener('fullscreenchange', () => {
|
|
resizeConway();
|
|
drawConway();
|
|
});
|
|
|
|
if (divConwayControlsFooter) {
|
|
divConwayControlsFooter.addEventListener('click', async (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement)) return;
|
|
const action = target.getAttribute('data-action');
|
|
if (!action) return;
|
|
|
|
if (action === 'play') {
|
|
isConwayRunning = true;
|
|
syncConwayControlState();
|
|
return;
|
|
}
|
|
|
|
if (action === 'pause') {
|
|
isConwayRunning = false;
|
|
syncConwayControlState();
|
|
return;
|
|
}
|
|
|
|
if (action === 'step') {
|
|
stepConway();
|
|
drawConway();
|
|
return;
|
|
}
|
|
|
|
if (action === 'seed') {
|
|
addConwayRandomSeed();
|
|
addConwayPatternAtRandom();
|
|
drawConway();
|
|
return;
|
|
}
|
|
|
|
if (action === 'clear') {
|
|
clearConway();
|
|
drawConway();
|
|
return;
|
|
}
|
|
|
|
if (action === 'full') {
|
|
try {
|
|
await toggleConwayFullscreen();
|
|
} catch (error) {
|
|
console.warn('[conway.html] Fullscreen toggle failed:', error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
syncConwayControlState();
|
|
conwayAnimationFrame = requestAnimationFrame(conwayLoop);
|
|
}
|
|
|
|
/* ================================================================
|
|
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();
|
|
}
|
|
}
|
|
|
|
|
|
/* ================================================================
|
|
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();
|
|
// Keep center controls intact; clear right status area only
|
|
divFooterRight.innerHTML = '';
|
|
} catch (error) {
|
|
console.error('[conway.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("[conway.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (followListSub?.unsubscribe) {
|
|
followListSub.unsubscribe();
|
|
followListSub = null;
|
|
}
|
|
if (followedNotesSub?.unsubscribe) {
|
|
followedNotesSub.unsubscribe();
|
|
followedNotesSub = null;
|
|
}
|
|
if (firehoseNotesSub?.unsubscribe) {
|
|
firehoseNotesSub.unsubscribe();
|
|
firehoseNotesSub = null;
|
|
}
|
|
|
|
console.log("[conway.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).
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
USER SETTINGS EXAMPLE (NIP-78)
|
|
================================================================
|
|
Read/subscribe/write helper pattern for all pages:
|
|
|
|
// Read latest merged settings (cache + relay hydrated by worker)
|
|
const settings = await getUserSettings();
|
|
|
|
// Subscribe to cross-tab updates
|
|
const unsubscribe = onUserSettings((nextSettings) => {
|
|
// Re-render page from nextSettings
|
|
});
|
|
|
|
// Patch only your feature namespace
|
|
await patchUserSettings({
|
|
myFeature: {
|
|
someFlag: true
|
|
}
|
|
});
|
|
|
|
// On page teardown (if applicable)
|
|
// unsubscribe();
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
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. Hydrate + subscribe user settings
|
|
6. Set up relay activity listeners
|
|
7. Set up version bar button listeners
|
|
8. 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("[conway.html] Starting initialization...");
|
|
|
|
try {
|
|
// Initialize hamburger menu first
|
|
initHamburgerMenu();
|
|
initConway();
|
|
|
|
// 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 {
|
|
await Logout();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize NDK (handles authentication automatically)
|
|
await initNDKPage();
|
|
|
|
// Get authenticated pubkey
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
console.log("[conway.html] Authenticated as:", currentPubkey);
|
|
initConwayNostrBridge();
|
|
|
|
// Hydrate app-wide user settings for this page
|
|
try {
|
|
pageSettings = await getUserSettings();
|
|
} catch (error) {
|
|
console.warn('[conway.html] getUserSettings failed:', error);
|
|
pageSettings = {};
|
|
}
|
|
|
|
// Subscribe to live user settings updates (cross-tab + publish echoes)
|
|
if (!unsubscribeUserSettings) {
|
|
unsubscribeUserSettings = onUserSettings((settings) => {
|
|
pageSettings = settings || {};
|
|
// TODO: Re-render page-specific UI from pageSettings here.
|
|
});
|
|
}
|
|
|
|
// 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(`[conway.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(`[conway.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.)
|
|
});
|
|
============================================================ */
|
|
|
|
|
|
// Start update loop (updates footer every second)
|
|
updateIntervalId = setInterval(UpdateFooter, 1000);
|
|
|
|
// Update version display
|
|
await updateVersionDisplay();
|
|
|
|
console.log('[conway.html] Initialization complete');
|
|
} catch (error) {
|
|
console.error('[conway.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. 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>
|