diff --git a/www/css/messaging-ui.css b/www/css/messaging-ui.css index 786f4df..0c80ced 100644 --- a/www/css/messaging-ui.css +++ b/www/css/messaging-ui.css @@ -1,7 +1,7 @@ .msg-thread-pane { flex: 1; min-height: 0; - border: 2px solid var(--primary-color); + /* border: 2px solid var(--primary-color); */ border-radius: 10px; background: var(--secondary-color); display: flex; diff --git a/www/document.html b/www/document.html index 1093a55..2afcc0a 100644 --- a/www/document.html +++ b/www/document.html @@ -372,6 +372,21 @@ min-height: 40px; } + .docEventPre { + margin: 0; + min-height: 40px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-family); + font-size: 84%; + line-height: 1.35; + border: 1px solid var(--muted-color); + border-radius: 8px; + padding: 10px; + background: color-mix(in srgb, var(--secondary-color) 90%, var(--muted-color) 10%); + } + #divDocumentInfo { font-size: 72%; color: var(--muted-color); @@ -1286,17 +1301,46 @@ user: `; } + function buildEventViewJson() { + const d = String(CURRENT_NOTE || '').trim(); + const note = d ? (OBJ_NOTES[d] || null) : null; + const fallbackKind = Number(note?.kind) === 30023 ? 30023 : 30024; + const tags = collectDocumentTags(); + return { + id: String(note?.id || ''), + pubkey: String(note?.pubkey || currentPubkey || ''), + kind: fallbackKind, + created_at: Number(note?.created_at || Math.floor(Date.now() / 1000)), + tags, + content: String(taDocument?.value || '') + }; + } + function updateDocumentView() { if (!taDocument || !divDocumentPreview) return; - if (documentViewMode === 'markdown') { + const metaGridEl = document.querySelector('#divAiDocumentPane .docPaneMetaGrid'); + const isMarkdown = documentViewMode === 'markdown'; + const isEvent = documentViewMode === 'event'; + + if (isMarkdown || isEvent) { taDocument.style.display = 'none'; + if (metaGridEl) metaGridEl.style.display = 'none'; divDocumentPreview.style.display = 'block'; - const metaHtml = renderDocumentMetaPreview(); - const bodyHtml = renderDocumentPreview(taDocument.value || ''); - divDocumentPreview.innerHTML = `${metaHtml}
${escapeHtml(eventJson)}`;
return;
}
+
taDocument.style.display = 'block';
+ if (metaGridEl) metaGridEl.style.display = 'grid';
divDocumentPreview.style.display = 'none';
}
@@ -4279,6 +4323,13 @@ user:
updateDocumentView();
}
},
+ {
+ label: 'View Event',
+ onClick: () => {
+ documentViewMode = 'event';
+ updateDocumentView();
+ }
+ },
{
label: 'Copy document',
onClick: async () => {
diff --git a/www/js/version.json b/www/js/version.json
index 9ca460c..c6d477b 100644
--- a/www/js/version.json
+++ b/www/js/version.json
@@ -1,5 +1,5 @@
{
- "VERSION": "v0.7.10",
- "VERSION_NUMBER": "0.7.10",
- "BUILD_DATE": "2026-04-28T16:33:28.943Z"
+ "VERSION": "v0.7.11",
+ "VERSION_NUMBER": "0.7.11",
+ "BUILD_DATE": "2026-05-03T11:00:46.559Z"
}
diff --git a/www/js/vj-stream.mjs b/www/js/vj-stream.mjs
index b938804..1e700a5 100644
--- a/www/js/vj-stream.mjs
+++ b/www/js/vj-stream.mjs
@@ -9,6 +9,7 @@ const STATS_POLL_INTERVAL = 10000;
const DEFAULT_STREAMING_SITE = Object.freeze({
name: 'laantungir.net',
streamBaseUrl: 'https://laantungir.net',
+ streamUrlTemplate: '{base}/stream/{slug}/stream.m3u8',
rtmpServer: 'rtmp://laantungir.net:1935/publish',
obsKeyTemplate: '{slug}/src/{SECRET_KEY}',
});
@@ -18,11 +19,13 @@ function normalizeStreamingSiteConfig(site) {
const input = site && typeof site === 'object' ? site : {};
const name = String(input.name || '').trim() || DEFAULT_STREAMING_SITE.name;
const streamBaseUrl = String(input.streamBaseUrl || '').trim() || DEFAULT_STREAMING_SITE.streamBaseUrl;
+ const streamUrlTemplate = String(input.streamUrlTemplate || '').trim() || DEFAULT_STREAMING_SITE.streamUrlTemplate;
const rtmpServer = String(input.rtmpServer || '').trim() || DEFAULT_STREAMING_SITE.rtmpServer;
const obsKeyTemplate = String(input.obsKeyTemplate || '').trim() || DEFAULT_STREAMING_SITE.obsKeyTemplate;
return {
name,
streamBaseUrl: streamBaseUrl.replace(/\/+$/, ''),
+ streamUrlTemplate,
rtmpServer,
obsKeyTemplate,
};
@@ -85,8 +88,12 @@ function deriveShowUrls(slug, siteConfig = DEFAULT_STREAMING_SITE) {
}
const base = site.streamBaseUrl;
+ const masterPlaylist = String(site.streamUrlTemplate || '').trim()
+ .replace(/\{base\}/gi, base)
+ .replace(/\{slug\}/gi, safeSlug)
+ || `${base}/stream/${safeSlug}/stream.m3u8`;
return {
- masterPlaylist: `${base}/stream/${safeSlug}/stream.m3u8`,
+ masterPlaylist,
viewerPage: `${base}/stream/${safeSlug}`,
stats: `${base}/api/stream/stats?show=${encodeURIComponent(safeSlug)}`,
obsServer: site.rtmpServer,
@@ -220,10 +227,11 @@ function parseStreamTargetFromUrl() {
}
function buildStreamTags({ dTag, title, summary, image, streamingUrl, status, episodeId, episodeDescription, webUrl }) {
+ const safeStatus = String(status || 'planned').trim() || 'planned';
const tags = [
['d', dTag],
['title', String(title || '').trim() || 'Untitled stream'],
- ['status', String(status || 'planned').trim() || 'planned']
+ ['status', safeStatus]
];
const safeSummary = String(summary || '').trim();
@@ -232,6 +240,7 @@ function buildStreamTags({ dTag, title, summary, image, streamingUrl, status, ep
const safeEpisodeId = String(episodeId || '').trim();
const safeEpisodeDescription = String(episodeDescription || '').trim();
const safeWeb = String(webUrl || '').trim();
+ const nowUnix = String(Math.floor(Date.now() / 1000));
if (safeSummary) tags.push(['summary', safeSummary]);
if (safeImage) tags.push(['image', safeImage]);
@@ -240,6 +249,14 @@ function buildStreamTags({ dTag, title, summary, image, streamingUrl, status, ep
if (safeEpisodeDescription) tags.push(['episode_description', safeEpisodeDescription]);
if (safeWeb) tags.push(['web', safeWeb]);
+ // NIP-53 compatibility: include timestamps so clients don't default to Unix epoch.
+ // Prefer episodeId timestamp when available because it is set when the episode starts.
+ const startsAt = /^\d+$/.test(safeEpisodeId) ? safeEpisodeId : nowUnix;
+ tags.push(['starts', startsAt]);
+ if (safeStatus === 'ended') {
+ tags.push(['ends', nowUnix]);
+ }
+
return tags;
}
@@ -279,6 +296,7 @@ export function initVjStreamPanel({
streamingSiteFormPanel: document.getElementById('streamingSiteFormPanel'),
inputSiteName: document.getElementById('inputSiteName'),
inputSiteStreamBaseUrl: document.getElementById('inputSiteStreamBaseUrl'),
+ inputSiteStreamUrlTemplate: document.getElementById('inputSiteStreamUrlTemplate'),
inputSiteRtmpServer: document.getElementById('inputSiteRtmpServer'),
inputSiteObsKeyTemplate: document.getElementById('inputSiteObsKeyTemplate'),
btnSaveStreamingSite: document.getElementById('btnSaveStreamingSite'),
@@ -435,6 +453,9 @@ export function initVjStreamPanel({
if (els.inputSiteStreamBaseUrl) {
els.inputSiteStreamBaseUrl.value = String(seedSite?.streamBaseUrl || '').trim();
}
+ if (els.inputSiteStreamUrlTemplate) {
+ els.inputSiteStreamUrlTemplate.value = String(seedSite?.streamUrlTemplate || '').trim();
+ }
if (els.inputSiteRtmpServer) {
els.inputSiteRtmpServer.value = String(seedSite?.rtmpServer || '').trim();
}
@@ -450,7 +471,7 @@ export function initVjStreamPanel({
function upsertStreamingSite(draft, { mode = 'add', editingName = '' } = {}) {
const normalizedDraft = normalizeStreamingSiteConfig(draft);
- if (!normalizedDraft.name || !normalizedDraft.streamBaseUrl || !normalizedDraft.rtmpServer || !normalizedDraft.obsKeyTemplate) {
+ if (!normalizedDraft.name || !normalizedDraft.streamBaseUrl || !normalizedDraft.streamUrlTemplate || !normalizedDraft.rtmpServer || !normalizedDraft.obsKeyTemplate) {
return false;
}
@@ -1780,6 +1801,7 @@ export function initVjStreamPanel({
showStreamingSiteForm('add', {
name: '',
streamBaseUrl: 'https://',
+ streamUrlTemplate: '{base}/stream/{slug}/stream.m3u8',
rtmpServer: 'rtmp://',
obsKeyTemplate: '{slug}/src/{SECRET_KEY}',
});
@@ -1807,6 +1829,7 @@ export function initVjStreamPanel({
const draft = normalizeStreamingSiteConfig({
name: String(els.inputSiteName?.value || '').trim(),
streamBaseUrl: String(els.inputSiteStreamBaseUrl?.value || '').trim(),
+ streamUrlTemplate: String(els.inputSiteStreamUrlTemplate?.value || '').trim(),
rtmpServer: String(els.inputSiteRtmpServer?.value || '').trim(),
obsKeyTemplate: String(els.inputSiteObsKeyTemplate?.value || '').trim(),
});
diff --git a/www/note.html b/www/note.html
index 9767482..299308b 100644
--- a/www/note.html
+++ b/www/note.html
@@ -253,6 +253,24 @@
#divHeaderText {
outline: none;
}
+
+ #txtDecryptTrace {
+ font-family: var(--font-mono);
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+
+ #divDiagnosticBadge {
+ display: inline-block;
+ margin-left: 10px;
+ padding: 4px 8px;
+ border: 1px solid #b45309;
+ border-radius: 4px;
+ color: #b45309;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ }
@@ -330,10 +348,15 @@
| NOTES | |||||||||
|---|---|---|---|---|---|---|---|---|---|
|   | |||||||||
| NOTES | |||||||||
|   | |||||||||
| Evt | Del | @@ -573,21 +790,26 @@ const versionInfo = await getVersion();Edited | Type | Encrypted | +Layers | 📋 |
✕ |
${svgPub} | -${OBJ_NOTES[Each].objTags?.title || 'Untitled'} | +${rowTitle} | ${dTagValue} | ${dCreated} | ${dEdited} | -${OBJ_NOTES[Each].kind} | -${OBJ_NOTES[Each].encrypted ? 'Yes' : 'No'} | +${note?.kind} | +${note?.encrypted ? 'Yes' : 'No'} | +${note?.kind == 30024 ? Number(note?.decryptDiagnostics?.passes || 0) : '-'} | `; } @@ -656,9 +879,14 @@ const versionInfo = await getVersion(); // Save when switching from edit to view mode if there are unsaved changes if (taNote.value.trim() !== LastText) { - console.log('[autosave] Switching to view mode with unsaved changes - triggering save'); - await Publish30024Note(); - LastText = taNote.value.trim(); + if (DIAGNOSTIC_ACTIVE) { + console.log('[autosave] Diagnostic view active - skipping implicit save'); + LastText = taNote.value.trim(); + } else { + console.log('[autosave] Switching to view mode with unsaved changes - triggering save'); + await Publish30024Note(); + LastText = taNote.value.trim(); + } } } }; @@ -672,15 +900,24 @@ const versionInfo = await getVersion(); return; } - taNote.value = OBJ_NOTES[id].content || ''; - LastText = OBJ_NOTES[id].content || ''; - txtTitle.textContent = OBJ_NOTES[id].objTags?.title || ''; - divTitle.textContent = OBJ_NOTES[id].objTags?.title || ''; - txtSummary.textContent = OBJ_NOTES[id].objTags?.summary || ''; - txtTags.textContent = OBJ_NOTES[id].objTags?.t?.join(' ') || ''; + const note = OBJ_NOTES[id]; + await ensureNoteDiagnostics(note); + + const displayTags = note.decryptedObjTags || note.objTags || {}; + const displayContent = Number(note.kind) === 30024 + ? String(note.decryptedContent || note.content || '') + : String(note.content || ''); + + taNote.value = displayContent; + LastText = taNote.value.trim(); + txtTitle.textContent = displayTags?.title || ''; + divTitle.textContent = displayTags?.title || ''; + txtSummary.textContent = displayTags?.summary || ''; + txtTags.textContent = displayTags?.t?.join(' ') || ''; CURRENT_NOTE = id; txtID.textContent = CURRENT_NOTE; + setDiagnosticUI(note); // Parse markdown for preview console.log('[note.html] LoadNote - window.marked exists:', !!window.marked); @@ -699,11 +936,11 @@ const versionInfo = await getVersion(); console.log('[note.html] No content to display - reason:', reason); } - if (OBJ_NOTES[id].objTags?.image === "" || !OBJ_NOTES[id].objTags?.image) { + if (displayTags?.image === "" || !displayTags?.image) { txtImg.textContent = ""; imgMain.className = "clsHidden"; } else { - txtImg.textContent = OBJ_NOTES[id].objTags.image; + txtImg.textContent = displayTags.image; imgMain.className = "clsVisible"; } @@ -932,7 +1169,7 @@ const versionInfo = await getVersion(); ); // Listen for incoming note events - window.addEventListener('ndkEvent', (event) => { + window.addEventListener('ndkEvent', async (event) => { const evt = event.detail; if (evt.kind === 30023 || evt.kind === 30024) { console.log("[note.html] Received note:", evt); @@ -957,6 +1194,14 @@ const versionInfo = await getVersion(); objTags: objTags, encrypted: evt.kind === 30024 }; + + if (evt.kind === 30024) { + await ensureNoteDiagnostics(OBJ_NOTES[dTag]); + } + + if (isNavOpen) { + LoadSidenav(); + } } }); @@ -982,6 +1227,7 @@ const versionInfo = await getVersion(); txtSummary.textContent = ""; txtTags.textContent = ""; divHeaderText.textContent = "TITLE"; + setDiagnosticUI(null); SetEditMode(true); closeNav(); }); @@ -1042,6 +1288,11 @@ const versionInfo = await getVersion(); return; } + if (DIAGNOSTIC_ACTIVE) { + console.log('[autosave] Skipping: diagnostic mode active'); + return; + } + // Skip if not enough time passed since last edit const secondsSinceEdit = Math.floor(Date.now() / 1000) - NUM_LAST_EDIT_TIME; // console.log('[autosave] Seconds since edit:', secondsSinceEdit, '| required:', autoSaveSetting); diff --git a/www/vj.html b/www/vj.html index 23178a1..52026e4 100644 --- a/www/vj.html +++ b/www/vj.html @@ -1535,6 +1535,7 @@