From 0addd59226a1feed3c8880cc45fd6f780dca122a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:50:03 -0400 Subject: [PATCH 1/5] feat: prioritize video root pointer for view tracking --- js/app.js | 87 ++++++++----------------------------- js/utils/videoPointer.js | 51 ++++++++++++++++++++++ tests/view-counter.test.mjs | 76 ++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 70 deletions(-) create mode 100644 js/utils/videoPointer.js diff --git a/js/app.js b/js/app.js index adf62dd1..08d52500 100644 --- a/js/app.js +++ b/js/app.js @@ -71,6 +71,7 @@ import ProfileModalController from "./ui/profileModalController.js"; import ZapController from "./ui/zapController.js"; import { MediaLoader } from "./utils/mediaLoader.js"; import { pointerArrayToKey } from "./utils/pointer.js"; +import { resolveVideoPointer } from "./utils/videoPointer.js"; import { isValidMagnetUri } from "./utils/magnetValidators.js"; import { dedupeToNewestByRoot } from "./utils/videoDeduper.js"; import { @@ -1309,34 +1310,14 @@ class Application { } const dTagValue = (this.extractDTagValue(video.tags) || "").trim(); - const normalizedPubkey = - typeof video.pubkey === "string" ? video.pubkey.trim() : ""; - const kind = - typeof video.kind === "number" && Number.isFinite(video.kind) - ? video.kind - : VIDEO_EVENT_KIND; - if (dTagValue && normalizedPubkey) { - const pointer = ["a", `${kind}:${normalizedPubkey}:${dTagValue}`]; - const key = pointerArrayToKey(pointer); - if (key) { - return { pointer, key }; - } - } - - const fallbackId = - typeof video.id === "string" && video.id.trim() - ? video.id.trim() - : ""; - if (fallbackId) { - const pointer = ["e", fallbackId]; - const key = pointerArrayToKey(pointer); - if (key) { - return { pointer, key }; - } - } - - return null; + return resolveVideoPointer({ + kind: video.kind, + pubkey: video.pubkey, + videoRootId: video.videoRootId, + dTag: dTagValue, + fallbackEventId: video.id, + }); } formatViewCountLabel(total) { @@ -6257,50 +6238,16 @@ class Application { }; const dTagValue = (this.extractDTagValue(video.tags) || "").trim(); - const normalizedPubkey = - typeof video.pubkey === "string" ? video.pubkey.trim() : ""; - const primaryPointer = - dTagValue && normalizedPubkey - ? [ - "a", - `${ - typeof video.kind === "number" && Number.isFinite(video.kind) - ? video.kind - : 30078 - }:${normalizedPubkey}:${dTagValue}`, - ] - : null; - const fallbackId = - typeof (video.id || eventId) === "string" - ? (video.id || eventId).trim() - : ""; - const fallbackPointer = fallbackId ? ["e", fallbackId] : null; + const pointerInfo = resolveVideoPointer({ + kind: video.kind, + pubkey: video.pubkey, + videoRootId: video.videoRootId, + dTag: dTagValue, + fallbackEventId: video.id || eventId, + }); - let resolvedPointer = null; - let resolvedPointerKey = ""; - const pointerCandidates = []; - if (primaryPointer) { - pointerCandidates.push(primaryPointer); - } - if (fallbackPointer) { - pointerCandidates.push(fallbackPointer); - } - - for (const candidate of pointerCandidates) { - const key = pointerArrayToKey(candidate); - if (key) { - resolvedPointer = candidate; - resolvedPointerKey = key; - break; - } - } - - this.currentVideoPointer = resolvedPointer && resolvedPointerKey - ? resolvedPointer - : null; - this.currentVideoPointerKey = this.currentVideoPointer - ? resolvedPointerKey - : null; + this.currentVideoPointer = pointerInfo?.pointer || null; + this.currentVideoPointerKey = pointerInfo?.key || null; if (this.currentVideo) { this.currentVideo.pointer = this.currentVideoPointer; diff --git a/js/utils/videoPointer.js b/js/utils/videoPointer.js new file mode 100644 index 00000000..35193e92 --- /dev/null +++ b/js/utils/videoPointer.js @@ -0,0 +1,51 @@ +// js/utils/videoPointer.js + +import { pointerArrayToKey } from "./pointer.js"; + +const DEFAULT_VIDEO_KIND = 30078; + +function normalizeString(value) { + return typeof value === "string" ? value.trim() : ""; +} + +export function resolveVideoPointer({ + kind, + pubkey, + videoRootId, + dTag, + fallbackEventId, +} = {}) { + const normalizedPubkey = normalizeString(pubkey).toLowerCase(); + const normalizedKind = + typeof kind === "number" && Number.isFinite(kind) ? kind : DEFAULT_VIDEO_KIND; + const pointerCandidates = []; + + const rootId = normalizeString(videoRootId); + if (rootId && normalizedPubkey) { + pointerCandidates.push(["a", `${normalizedKind}:${normalizedPubkey}:${rootId}`]); + } + + const normalizedDTag = normalizeString(dTag); + if (normalizedDTag && normalizedPubkey) { + pointerCandidates.push([ + "a", + `${normalizedKind}:${normalizedPubkey}:${normalizedDTag}`, + ]); + } + + const fallbackId = normalizeString(fallbackEventId); + if (fallbackId) { + pointerCandidates.push(["e", fallbackId]); + } + + for (const pointer of pointerCandidates) { + const key = pointerArrayToKey(pointer); + if (key) { + return { pointer, key }; + } + } + + return null; +} + +export default resolveVideoPointer; diff --git a/tests/view-counter.test.mjs b/tests/view-counter.test.mjs index ac78e4a4..a797e44d 100644 --- a/tests/view-counter.test.mjs +++ b/tests/view-counter.test.mjs @@ -47,6 +47,14 @@ if (!globalThis.window.NostrTools) { globalThis.window.NostrTools = {}; } +if (typeof globalThis.window.NostrTools.getEventHash !== "function") { + globalThis.window.NostrTools.getEventHash = () => "test-event-hash"; +} + +if (typeof globalThis.window.NostrTools.signEvent !== "function") { + globalThis.window.NostrTools.signEvent = () => "test-event-sig"; +} + setNostrEventSchemaOverrides({}); const canonicalViewEvent = buildViewEvent({ @@ -275,11 +283,79 @@ const { unsubscribeFromVideoViewCount, ingestLocalViewEvent, } = await import("../js/viewCounter.js"); +const { resolveVideoPointer } = await import("../js/utils/videoPointer.js"); initViewCounter({ nostrClient }); const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); +function testPointerPrefersVideoRootId() { + const info = resolveVideoPointer({ + pubkey: "ABCDEF1234", + videoRootId: "ROOT-POINTER", + dTag: "legacy-d-tag", + fallbackEventId: "event-fallback", + kind: 30078, + }); + + assert.ok(info, "expected pointer info for root-backed video"); + assert.deepEqual( + info.pointer, + ["a", "30078:abcdef1234:ROOT-POINTER"], + "videos with a videoRootId should resolve to the root pointer" + ); + assert.equal( + info.key, + "a:30078:abcdef1234:root-pointer", + "root pointer key should be normalized for view counter lookups" + ); +} + +function testPointerFallsBackToDTag() { + const info = resolveVideoPointer({ + pubkey: "ABCDEF1234", + dTag: "legacy-pointer", + fallbackEventId: "event-fallback", + kind: 30078, + }); + + assert.ok(info, "expected pointer info when only d-tag is present"); + assert.deepEqual( + info.pointer, + ["a", "30078:abcdef1234:legacy-pointer"], + "videos without a root should fall back to the d-tag pointer" + ); + assert.equal( + info.key, + "a:30078:abcdef1234:legacy-pointer", + "d-tag pointer key should match the normalized value" + ); +} + +function testPointerFallsBackToEventId() { + const info = resolveVideoPointer({ + pubkey: "ABCDEF1234", + fallbackEventId: "legacy-event-id", + kind: 30078, + }); + + assert.ok(info, "expected pointer info when only event id is present"); + assert.deepEqual( + info.pointer, + ["e", "legacy-event-id"], + "videos without root or d-tag should fall back to the event pointer" + ); + assert.equal( + info.key, + "e:legacy-event-id", + "event pointer key should normalize correctly" + ); +} + +testPointerPrefersVideoRootId(); +testPointerFallsBackToDTag(); +testPointerFallsBackToEventId(); + async function testDedupesWithinWindow() { localStorage.clear(); harness.reset(); From 7d389bb33d56aafad411898355328ff944c103b3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:35:29 -0400 Subject: [PATCH 2/5] Speed up video subscription flushes --- js/nostr.js | 130 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 51 deletions(-) diff --git a/js/nostr.js b/js/nostr.js index 4e7ed67f..928c0a08 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -6004,67 +6004,89 @@ export class NostrClient { // We'll collect events here instead of processing them instantly let eventBuffer = []; + const EVENT_FLUSH_DEBOUNCE_MS = 75; + let flushTimerId = null; - // 1) On each incoming event, just push to the buffer - sub.on("event", (event) => { - eventBuffer.push(event); - }); + const flushEventBuffer = () => { + if (!eventBuffer.length) { + return; + } - // 2) Process buffered events on a setInterval (e.g., every second) - const processInterval = setInterval(() => { - if (eventBuffer.length > 0) { - // Copy and clear the buffer - const toProcess = eventBuffer.slice(); - eventBuffer = []; + const toProcess = eventBuffer; + eventBuffer = []; - // Now handle each event - for (const evt of toProcess) { - try { - const video = convertEventToVideo(evt); + for (const evt of toProcess) { + try { + const video = convertEventToVideo(evt); - if (video.invalid) { - invalidDuringSub.push({ id: video.id, reason: video.reason }); - continue; - } + if (video.invalid) { + invalidDuringSub.push({ id: video.id, reason: video.reason }); + continue; + } - this.mergeNip71MetadataIntoVideo(video); + this.mergeNip71MetadataIntoVideo(video); - // Store in allEvents - this.allEvents.set(evt.id, video); + // Store in allEvents + this.allEvents.set(evt.id, video); - // If it's a "deleted" note, remove from activeMap - if (video.deleted) { - const activeKey = getActiveKey(video); - this.activeMap.delete(activeKey); - continue; - } - - // Otherwise, if it's newer than what we have, update activeMap + // If it's a "deleted" note, remove from activeMap + if (video.deleted) { const activeKey = getActiveKey(video); - const prevActive = this.activeMap.get(activeKey); - if (!prevActive || video.created_at > prevActive.created_at) { - this.activeMap.set(activeKey, video); - onVideo(video); // Trigger the callback that re-renders - this.populateNip71MetadataForVideos([video]).catch((error) => { - if (isDevMode) { - console.warn( - "[nostr] Failed to hydrate NIP-71 metadata for live video:", - error - ); - } - }); - } - } catch (err) { - if (isDevMode) { - console.error("[subscribeVideos] Error processing event:", err); - } + this.activeMap.delete(activeKey); + continue; + } + + // Otherwise, if it's newer than what we have, update activeMap + const activeKey = getActiveKey(video); + const prevActive = this.activeMap.get(activeKey); + if (!prevActive || video.created_at > prevActive.created_at) { + this.activeMap.set(activeKey, video); + onVideo(video); // Trigger the callback that re-renders + this.populateNip71MetadataForVideos([video]).catch((error) => { + if (isDevMode) { + console.warn( + "[nostr] Failed to hydrate NIP-71 metadata for live video:", + error + ); + } + }); + } + } catch (err) { + if (isDevMode) { + console.error("[subscribeVideos] Error processing event:", err); } } - - // Optionally, save data to local storage after processing the batch - this.saveLocalData(); } - }, 1000); + + // Persist processed events after each flush so reloads warm quickly. + this.saveLocalData(); + }; + + const scheduleFlush = (immediate = false) => { + if (flushTimerId) { + if (!immediate) { + return; + } + clearTimeout(flushTimerId); + flushTimerId = null; + } + + if (immediate) { + flushEventBuffer(); + return; + } + + flushTimerId = setTimeout(() => { + flushTimerId = null; + flushEventBuffer(); + }, EVENT_FLUSH_DEBOUNCE_MS); + }; + + // 1) On each incoming event, just push to the buffer and schedule a flush + sub.on("event", (event) => { + eventBuffer.push(event); + scheduleFlush(false); + }); // You can still use sub.on("eose") if needed sub.on("eose", () => { @@ -6079,6 +6101,7 @@ export class NostrClient { "[subscribeVideos] Reached EOSE for all relays (historical load done)" ); } + scheduleFlush(true); }); // Return the subscription object if you need to unsub manually later @@ -6090,7 +6113,12 @@ export class NostrClient { return; } unsubscribed = true; - clearInterval(processInterval); + if (flushTimerId) { + clearTimeout(flushTimerId); + flushTimerId = null; + } + // Ensure any straggling events are flushed before tearing down. + flushEventBuffer(); try { return originalUnsub(); } catch (err) { From c99f6834cde62a571a82f89fc83d870d051af3a9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:45:59 -0400 Subject: [PATCH 3/5] Show posted and edited times from root history --- components/video-modal.html | 10 +- js/app.js | 198 ++++++++++++++++++++++++++++++++- js/ui/components/VideoModal.js | 46 ++++++-- 3 files changed, 242 insertions(+), 12 deletions(-) diff --git a/components/video-modal.html b/components/video-modal.html index 3045c03f..741289ff 100644 --- a/components/video-modal.html +++ b/components/video-modal.html @@ -237,10 +237,14 @@
-