760 lines
23 KiB
HTML
760 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<html lang="en" dir="ltr">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>BLOCK</title>
|
|
|
|
<link rel="stylesheet" href="./css/client.css" />
|
|
|
|
<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" />
|
|
<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;
|
|
gap: 10px !important;
|
|
overflow-y: auto !important;
|
|
}
|
|
|
|
#divBlockControls {
|
|
display: grid;
|
|
grid-template-columns: 150px 1fr auto auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
width: 100%;
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
padding: 10px;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
.blockInput,
|
|
.blockSelect {
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
padding: 8px 10px;
|
|
font-size: 90%;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
min-width: 0;
|
|
}
|
|
|
|
.blockButton {
|
|
border: var(--button-border-width) solid var(--button-border-color);
|
|
border-radius: var(--button-border-radius);
|
|
background: var(--button-background-color);
|
|
color: var(--button-color);
|
|
padding: 8px 10px;
|
|
font-size: 85%;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.blockButton:hover {
|
|
color: var(--button-hover-color);
|
|
border-color: var(--button-hover-color);
|
|
}
|
|
|
|
#lblPrivate {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 80%;
|
|
color: var(--muted-color);
|
|
user-select: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#divBlockStatus {
|
|
width: 100%;
|
|
font-size: 80%;
|
|
color: var(--muted-color);
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
padding: 8px 10px;
|
|
min-height: 34px;
|
|
display: flex;
|
|
align-items: center;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
#divBlockGroups {
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.blockGroup {
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
background: var(--secondary-color);
|
|
padding: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
min-height: 120px;
|
|
}
|
|
|
|
.blockGroupTitle {
|
|
font-size: 85%;
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
border-bottom: var(--border);
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.blockItem {
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
padding: 8px;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
.blockItemMain {
|
|
min-width: 0;
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.blockAvatar {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: var(--border-radius);
|
|
overflow: hidden;
|
|
flex: 0 0 34px;
|
|
border: var(--border);
|
|
background: transparent;
|
|
}
|
|
|
|
.blockAvatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
display: block;
|
|
filter: grayscale(var(--image-grayscale));
|
|
transition: filter 0.2s;
|
|
}
|
|
|
|
.blockAvatar img:hover {
|
|
filter: grayscale(var(--image-grayscale-hover));
|
|
}
|
|
|
|
.blockText {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.blockPrimary {
|
|
font-size: 82%;
|
|
color: var(--primary-color);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.blockSecondary {
|
|
font-size: 68%;
|
|
color: var(--muted-color);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.blockBadge {
|
|
font-size: 65%;
|
|
border: var(--border);
|
|
border-radius: var(--border-radius);
|
|
padding: 2px 6px;
|
|
color: var(--muted-color);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.blockRemove {
|
|
border: var(--button-border-width) solid var(--button-border-color);
|
|
border-radius: var(--button-border-radius);
|
|
background: var(--button-background-color);
|
|
color: var(--button-color);
|
|
padding: 4px 8px;
|
|
font-size: 70%;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.blockRemove:hover {
|
|
color: var(--button-hover-color);
|
|
border-color: var(--button-hover-color);
|
|
}
|
|
|
|
.blockEmpty {
|
|
font-size: 75%;
|
|
color: var(--muted-color);
|
|
opacity: 0.85;
|
|
}
|
|
|
|
@media (max-width: 800px) {
|
|
#divBlockControls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="divSvgHam" class="divHeaderButtons"></div>
|
|
|
|
<div id="divHeader">
|
|
<div id="divHeaderFlexLeft"></div>
|
|
<div id="divHeaderFlexCenter">
|
|
<div class="divHeaderText">BLOCK</div>
|
|
</div>
|
|
<div id="divHeaderFlexRight"></div>
|
|
</div>
|
|
|
|
<div id="divBody">
|
|
<div id="divBlockControls">
|
|
<select id="selMuteType" class="blockSelect">
|
|
<option value="p">Pubkey (p)</option>
|
|
<option value="t">Hashtag (t)</option>
|
|
<option value="word">Word (word)</option>
|
|
<option value="e">Thread/Event (e)</option>
|
|
</select>
|
|
<input id="inpMuteValue" class="blockInput" type="text" placeholder="npub/hex pubkey, hashtag, word, or event id" />
|
|
<label id="lblPrivate" for="chkPrivateMute">
|
|
<input id="chkPrivateMute" type="checkbox" checked />
|
|
Private (NIP-44)
|
|
</label>
|
|
<button id="btnAddMute" class="blockButton">Add Block</button>
|
|
</div>
|
|
|
|
<div id="divBlockStatus">Initializing...</div>
|
|
<div id="divBlockGroups"></div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<div id="divSideNav">
|
|
<div id="divSideNavHeader"></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>
|
|
|
|
<script src="./nostr.bundle.js"></script>
|
|
<script src="./nostr-lite.js"></script>
|
|
|
|
<script type="module">
|
|
import {
|
|
initNDKPage,
|
|
getPubkey,
|
|
injectHeaderAvatar,
|
|
fetchCachedProfile,
|
|
storeProfile,
|
|
ndkFetchEvents,
|
|
publishEvent,
|
|
queryCache,
|
|
subscribe,
|
|
disconnect,
|
|
getVersion,
|
|
updateVersionDisplay,
|
|
ensureMuteListLoaded,
|
|
getMuteList,
|
|
getMuteCount,
|
|
addMute,
|
|
removeMute,
|
|
onMuteListChanged
|
|
} from './js/init-ndk.mjs';
|
|
import { HamburgerMorphing } from './hamburger_morphing/hamburger.mjs';
|
|
import { createProfileCache } from './js/profile-cache.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';
|
|
|
|
const versionInfo = await getVersion();
|
|
const VERSION = versionInfo.VERSION;
|
|
console.log(`[block.html ${VERSION}] Loading...`);
|
|
|
|
let currentPubkey = null;
|
|
let updateIntervalId = null;
|
|
let isNavOpen = false;
|
|
let hamburgerInstance = null;
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = false;
|
|
let blockRenderToken = 0;
|
|
|
|
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 selMuteType = document.getElementById('selMuteType');
|
|
const inpMuteValue = document.getElementById('inpMuteValue');
|
|
const chkPrivateMute = document.getElementById('chkPrivateMute');
|
|
const btnAddMute = document.getElementById('btnAddMute');
|
|
const divBlockStatus = document.getElementById('divBlockStatus');
|
|
const divBlockGroups = document.getElementById('divBlockGroups');
|
|
|
|
const profileCacheApi = createProfileCache({
|
|
fetchCachedProfile,
|
|
ndkFetchEvents,
|
|
storeProfile,
|
|
queryCache
|
|
});
|
|
|
|
function setStatus(msg) {
|
|
divBlockStatus.textContent = msg;
|
|
}
|
|
|
|
function shortHex(value = '') {
|
|
if (!value || value.length < 16) return value;
|
|
return `${value.slice(0, 12)}…${value.slice(-8)}`;
|
|
}
|
|
|
|
function normalizeType(type) {
|
|
return ['p', 't', 'word', 'e'].includes(type) ? type : 'p';
|
|
}
|
|
|
|
function normalizeMuteValue(type, rawValue) {
|
|
const value = String(rawValue || '').trim();
|
|
if (!value) throw new Error('Enter a value');
|
|
|
|
if (type === 'p') {
|
|
if (/^[a-f0-9]{64}$/i.test(value)) return value.toLowerCase();
|
|
if (value.startsWith('npub1')) {
|
|
const decoded = window?.NostrTools?.nip19?.decode?.(value);
|
|
if (decoded?.type === 'npub' && typeof decoded.data === 'string' && decoded.data.length === 64) {
|
|
return decoded.data.toLowerCase();
|
|
}
|
|
}
|
|
throw new Error('Pubkey must be hex or npub1');
|
|
}
|
|
|
|
if (type === 't') {
|
|
const normalized = value.replace(/^#+/, '').trim().toLowerCase();
|
|
if (!normalized) throw new Error('Hashtag cannot be empty');
|
|
return normalized;
|
|
}
|
|
|
|
if (type === 'word') return value.toLowerCase();
|
|
|
|
if (type === 'e') {
|
|
if (/^[a-f0-9]{64}$/i.test(value)) return value.toLowerCase();
|
|
if (value.startsWith('note1')) {
|
|
const decoded = window?.NostrTools?.nip19?.decode?.(value);
|
|
if (decoded?.type === 'note' && typeof decoded.data === 'string' && decoded.data.length === 64) {
|
|
return decoded.data.toLowerCase();
|
|
}
|
|
}
|
|
throw new Error('Thread/Event must be hex event id or note1');
|
|
}
|
|
|
|
throw new Error('Unknown mute type');
|
|
}
|
|
|
|
function labelForType(type) {
|
|
if (type === 'p') return 'Pubkeys';
|
|
if (type === 't') return 'Hashtags';
|
|
if (type === 'word') return 'Words';
|
|
if (type === 'e') return 'Threads';
|
|
return type;
|
|
}
|
|
|
|
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...';
|
|
|
|
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)'
|
|
});
|
|
const savedTheme = localStorage.getItem('theme');
|
|
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
|
|
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
|
|
}
|
|
}
|
|
|
|
function closeNav() {
|
|
divSideNav.style.width = '0vw';
|
|
divSideNav.style.zIndex = -1;
|
|
isNavOpen = false;
|
|
if (hamburgerInstance) hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function toggleNav() {
|
|
if (isNavOpen) closeNav(); else openNav();
|
|
}
|
|
|
|
function initHamburgerMenu() {
|
|
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
async function updateFooter() {
|
|
try {
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
await updateBlossomSection();
|
|
divFooterCenter.innerHTML = '';
|
|
divFooterRight.innerHTML = '';
|
|
} catch (error) {
|
|
console.error('[block.html] Error updating footer:', error);
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
disconnect();
|
|
if (window.NOSTR_LOGIN_LITE?.logout) await window.NOSTR_LOGIN_LITE.logout();
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
if (window.indexedDB) {
|
|
const databases = await window.indexedDB.databases();
|
|
for (const db of databases) if (db.name) window.indexedDB.deleteDatabase(db.name);
|
|
}
|
|
location.reload(true);
|
|
}
|
|
|
|
async function getProfile(pubkey) {
|
|
return profileCacheApi.getCachedProfile(pubkey) || await fetchCachedProfile(pubkey) || null;
|
|
}
|
|
|
|
async function renderBlockGroups() {
|
|
const renderToken = ++blockRenderToken;
|
|
const list = getMuteList();
|
|
const groups = [
|
|
{ type: 'p', label: 'Pubkeys', publicValues: list.public.p, privateValues: list.private.p },
|
|
{ type: 't', label: 'Hashtags', publicValues: list.public.t, privateValues: list.private.t },
|
|
{ type: 'word', label: 'Words', publicValues: list.public.word, privateValues: list.private.word },
|
|
{ type: 'e', label: 'Threads', publicValues: list.public.e, privateValues: list.private.e }
|
|
];
|
|
|
|
const pubkeysToHydrate = [...new Set([...(list.public.p || []), ...(list.private.p || [])])];
|
|
if (pubkeysToHydrate.length) {
|
|
subscribe({ kinds: [0], authors: pubkeysToHydrate }, { closeOnEose: true, cacheUsage: 'CACHE_FIRST' });
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
for (const group of groups) {
|
|
if (renderToken !== blockRenderToken) return;
|
|
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'blockGroup';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'blockGroupTitle';
|
|
title.textContent = `${group.label} (${group.publicValues.length + group.privateValues.length})`;
|
|
wrap.appendChild(title);
|
|
|
|
const rows = [
|
|
...group.publicValues.map((value) => ({ value, private: false })),
|
|
...group.privateValues.map((value) => ({ value, private: true }))
|
|
];
|
|
|
|
if (rows.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'blockEmpty';
|
|
empty.textContent = `No ${group.label.toLowerCase()} muted`;
|
|
wrap.appendChild(empty);
|
|
fragment.appendChild(wrap);
|
|
continue;
|
|
}
|
|
|
|
for (const row of rows) {
|
|
if (renderToken !== blockRenderToken) return;
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'blockItem';
|
|
|
|
const main = document.createElement('div');
|
|
main.className = 'blockItemMain';
|
|
|
|
if (group.type === 'p') {
|
|
const avatarWrap = document.createElement('div');
|
|
avatarWrap.className = 'blockAvatar';
|
|
const img = document.createElement('img');
|
|
img.alt = '';
|
|
img.loading = 'lazy';
|
|
img.decoding = 'async';
|
|
img.referrerPolicy = 'no-referrer';
|
|
avatarWrap.appendChild(img);
|
|
main.appendChild(avatarWrap);
|
|
|
|
const profile = await getProfile(row.value);
|
|
if (renderToken !== blockRenderToken) return;
|
|
const picture = String(profile?.picture || '').trim();
|
|
if (picture) {
|
|
img.addEventListener('error', () => img.removeAttribute('src'), { once: true });
|
|
img.src = picture;
|
|
}
|
|
}
|
|
|
|
const text = document.createElement('div');
|
|
text.className = 'blockText';
|
|
|
|
const primary = document.createElement('div');
|
|
primary.className = 'blockPrimary';
|
|
|
|
const secondary = document.createElement('div');
|
|
secondary.className = 'blockSecondary';
|
|
|
|
if (group.type === 'p') {
|
|
const profile = await getProfile(row.value);
|
|
if (renderToken !== blockRenderToken) return;
|
|
primary.textContent = String(profile?.display_name || profile?.name || shortHex(row.value));
|
|
secondary.textContent = row.value;
|
|
} else if (group.type === 't') {
|
|
primary.textContent = `#${row.value}`;
|
|
secondary.textContent = row.value;
|
|
} else {
|
|
primary.textContent = row.value;
|
|
secondary.textContent = shortHex(row.value);
|
|
}
|
|
|
|
text.appendChild(primary);
|
|
text.appendChild(secondary);
|
|
main.appendChild(text);
|
|
|
|
const right = document.createElement('div');
|
|
right.style.display = 'flex';
|
|
right.style.alignItems = 'center';
|
|
right.style.gap = '8px';
|
|
|
|
const badge = document.createElement('div');
|
|
badge.className = 'blockBadge';
|
|
badge.textContent = row.private ? 'private' : 'public';
|
|
|
|
const btnRemove = document.createElement('button');
|
|
btnRemove.className = 'blockRemove';
|
|
btnRemove.textContent = 'Remove';
|
|
btnRemove.addEventListener('click', async (ev) => {
|
|
ev.stopPropagation();
|
|
const ok = window.confirm(`Remove ${row.value} from ${labelForType(group.type).toLowerCase()}?`);
|
|
if (!ok) return;
|
|
try {
|
|
setStatus('Removing mute entry...');
|
|
await removeMute(group.type, row.value);
|
|
setStatus(`Removed from ${labelForType(group.type).toLowerCase()}`);
|
|
await renderBlockGroups();
|
|
} catch (error) {
|
|
setStatus(`Remove failed: ${error?.message || String(error)}`);
|
|
}
|
|
});
|
|
|
|
right.appendChild(badge);
|
|
right.appendChild(btnRemove);
|
|
|
|
item.appendChild(main);
|
|
item.appendChild(right);
|
|
wrap.appendChild(item);
|
|
}
|
|
|
|
fragment.appendChild(wrap);
|
|
}
|
|
|
|
if (renderToken !== blockRenderToken) return;
|
|
divBlockGroups.innerHTML = '';
|
|
divBlockGroups.appendChild(fragment);
|
|
setStatus(`Loaded ${getMuteCount()} muted entries`);
|
|
}
|
|
|
|
async function addMuteFromInput() {
|
|
try {
|
|
const type = normalizeType(selMuteType.value);
|
|
const value = normalizeMuteValue(type, inpMuteValue.value);
|
|
const isPrivate = Boolean(chkPrivateMute.checked);
|
|
|
|
setStatus('Publishing mute list...');
|
|
await addMute(type, value, isPrivate);
|
|
|
|
inpMuteValue.value = '';
|
|
setStatus(`Added ${value} to ${labelForType(type).toLowerCase()} (${isPrivate ? 'private' : 'public'})`);
|
|
// renderBlockGroups() is triggered by onMuteListChanged listener
|
|
} catch (error) {
|
|
setStatus(`Add failed: ${error?.message || String(error)}`);
|
|
}
|
|
}
|
|
|
|
function wireEvents() {
|
|
btnAddMute.addEventListener('click', addMuteFromInput);
|
|
inpMuteValue.addEventListener('keydown', (ev) => {
|
|
if (ev.key === 'Enter') addMuteFromInput();
|
|
});
|
|
|
|
window.addEventListener('ndkEvent', async (event) => {
|
|
const evt = event.detail;
|
|
if (!evt || !evt.kind) return;
|
|
|
|
if (evt.kind === 0) {
|
|
try {
|
|
const parsed = JSON.parse(evt.content || '{}');
|
|
profileCacheApi.seedProfileCache(evt.pubkey, parsed);
|
|
await renderBlockGroups();
|
|
} catch (_e) {
|
|
// non-fatal
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (evt.kind === 10000 && evt.pubkey === currentPubkey) {
|
|
try {
|
|
await ensureMuteListLoaded({ forceReload: true });
|
|
// renderBlockGroups() is triggered by onMuteListChanged listener
|
|
} catch (err) {
|
|
console.warn('[block.html] Failed to refresh mute list from live event:', err?.message || err);
|
|
}
|
|
}
|
|
});
|
|
|
|
window.addEventListener('ndkRelayActivity', (event) => {
|
|
const { relayUrl, activity } = event.detail;
|
|
setRelayActivityState(relayUrl, activity);
|
|
});
|
|
|
|
onMuteListChanged(() => {
|
|
renderBlockGroups().catch((err) => {
|
|
console.warn('[block.html] mute-list change render failed:', err);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
initHamburgerMenu();
|
|
document.getElementById('divSvgHam')?.addEventListener('click', toggleNav);
|
|
|
|
const themeToggleButton = document.getElementById('themeToggleButton');
|
|
const logoutButton = document.getElementById('logoutButton');
|
|
|
|
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');
|
|
}
|
|
});
|
|
|
|
logoutButton?.addEventListener('click', async () => {
|
|
try { await logout(); } catch (error) { console.error(error); }
|
|
});
|
|
|
|
await initNDKPage();
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
|
|
setStatus('Loading mute list...');
|
|
await ensureMuteListLoaded();
|
|
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
await initBlossomSection();
|
|
initAiSectionWithLocalConfig();
|
|
await updateFooter();
|
|
|
|
wireEvents();
|
|
subscribe({ kinds: [10000], authors: [currentPubkey], limit: 1 }, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });
|
|
await renderBlockGroups();
|
|
|
|
|
|
updateIntervalId = setInterval(updateFooter, 1000);
|
|
await updateVersionDisplay();
|
|
} catch (error) {
|
|
console.error('[block.html] Initialization failed:', error);
|
|
divBody.innerHTML = `<div style="text-align:center;padding:40px;color:var(--accent-color);">${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
main();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|