Add default view event payload and cover recordVideoView

This commit is contained in:
thePR0M3TH3AN committed 2025-10-01 17:48:05 -04:00
1 parent c06233947f
commit cf99059564
3 files changed
+244 -3

No files matched your search

+44 -2
View File
@@ -2785,8 +2785,50 @@ class NostrClient {
? ["e", pointer.value, pointer.relay]
: ["e", pointer.value];
const content =
typeof options.content === "string" ? options.content : "";
let content = "";
if (typeof options.content === "string") {
content = options.content;
} else if (
options.content &&
typeof options.content === "object" &&
!Array.isArray(options.content)
) {
try {
content = JSON.stringify(options.content);
} catch (error) {
if (isDevMode) {
console.warn(
"[nostr] Failed to serialize custom view event content:",
error
);
}
content = "";
}
}
if (!content) {
const payload = {
target: {
type: pointer.type,
value: pointer.value,
},
created_at: createdAt,
};
if (pointer.relay) {
payload.target.relay = pointer.relay;
}
try {
content = JSON.stringify(payload);
} catch (error) {
if (isDevMode) {
console.warn(
"[nostr] Failed to serialize default view event content:",
error
);
}
content = "";
}
}
const event = buildViewEvent({
pubkey: actorPubkey,
+66 -1
View File
@@ -30,6 +30,69 @@ export const ADMIN_LIST_IDENTIFIERS = Object.freeze({
const DEFAULT_APPEND_TAGS = [];
let cachedUtf8Encoder = null;
function ensureValidUtf8Content(value) {
let normalized = "";
if (typeof value === "string") {
normalized = value;
} else if (value === undefined || value === null) {
normalized = "";
} else if (typeof value === "object") {
try {
const stringified = JSON.stringify(value);
normalized = typeof stringified === "string" ? stringified : "";
} catch (error) {
if (isDevMode) {
console.warn(
"[nostrEventSchemas] Failed to serialize view event content:",
error
);
}
normalized = "";
}
} else {
normalized = String(value);
}
if (!cachedUtf8Encoder && typeof TextEncoder !== "undefined") {
cachedUtf8Encoder = new TextEncoder();
}
if (cachedUtf8Encoder) {
try {
cachedUtf8Encoder.encode(normalized);
} catch (error) {
if (isDevMode) {
console.warn(
"[nostrEventSchemas] Dropping invalid UTF-8 characters from event content",
error
);
}
const sanitized = Array.from(normalized)
.filter((char) => {
const code = char.charCodeAt(0);
return code < 0xd800 || code > 0xdfff;
})
.join("");
try {
cachedUtf8Encoder.encode(sanitized);
normalized = sanitized;
} catch (encodeError) {
if (isDevMode) {
console.warn(
"[nostrEventSchemas] Failed to normalize view event content; defaulting to empty string",
encodeError
);
}
normalized = "";
}
}
}
return normalized;
}
const BASE_SCHEMAS = {
[NOTE_TYPES.VIDEO_POST]: {
type: NOTE_TYPES.VIDEO_POST,
@@ -477,12 +540,14 @@ export function buildViewEvent({
}
appendSchemaTags(tags, schema);
const resolvedContent = ensureValidUtf8Content(content);
return {
kind: schema?.kind ?? WATCH_HISTORY_KIND,
pubkey,
created_at,
tags,
content: typeof content === "string" ? content : String(content ?? ""),
content: resolvedContent,
};
}
+134
View File
@@ -506,6 +506,139 @@ async function testUnsubscribeStopsCallbacks() {
);
}
async function testRecordVideoViewEmitsJsonPayload() {
localStorage.clear();
harness.reset();
harness.resetMetrics();
const pointer = {
type: "a",
value: "30078:pub:view-json",
relay: "wss://relay.example",
};
const createdAt = Math.floor(Date.now() / 1000);
const originalPool = nostrClient.pool;
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalEnsureSessionActor = nostrClient.ensureSessionActor;
const originalSessionActor = nostrClient.sessionActor;
const originalPubkey = nostrClient.pubkey;
const originalUpdateHistory = nostrClient.updateWatchHistoryList;
const originalWarn = console.warn;
const originalNostrTools = window.NostrTools;
const warnings = [];
console.warn = (...args) => {
warnings.push(
args
.map((value) => {
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch (error) {
return String(value);
}
})
.join(" ")
);
};
window.NostrTools = {
...window.NostrTools,
getEventHash: () => "event-hash-record", // deterministic stub for tests
signEvent: () => "event-sig-record",
};
const publishCalls = [];
nostrClient.pool = {
publish(relayUrls, event) {
publishCalls.push({ relays: relayUrls, event });
return {
on(type, handler) {
if (type === "ok") {
setTimeout(() => handler(), 0);
}
return this;
},
};
},
};
nostrClient.relays = ["wss://relay.example"];
nostrClient.ensureSessionActor = async () => {
nostrClient.sessionActor = {
pubkey: "pub-record-json",
privateKey: "priv-record-json",
};
return "pub-record-json";
};
nostrClient.pubkey = "";
nostrClient.updateWatchHistoryList = async () => ({ ok: true });
try {
const result = await nostrClient.recordVideoView(pointer, {
created_at: createdAt,
});
assert.ok(result.ok, "recordVideoView should succeed with stubbed handlers");
assert.equal(
publishCalls.length,
1,
"publishViewEvent should be invoked once per recordVideoView call"
);
const emittedEvent = publishCalls[0]?.event || null;
assert.ok(emittedEvent, "publishViewEvent should enqueue an event for relays");
assert.equal(
typeof emittedEvent.content,
"string",
"recordVideoView should provide serialized view content"
);
const payload = JSON.parse(emittedEvent.content);
assert.deepEqual(
payload,
{
target: {
type: pointer.type,
value: pointer.value,
relay: pointer.relay,
},
created_at: createdAt,
},
"default view content should capture pointer metadata and timestamp"
);
assert.equal(
result.view.event.content,
emittedEvent.content,
"serialized payload should persist on the emitted event"
);
} finally {
nostrClient.pool = originalPool;
nostrClient.relays = originalRelays;
nostrClient.ensureSessionActor = originalEnsureSessionActor;
nostrClient.sessionActor = originalSessionActor;
nostrClient.pubkey = originalPubkey;
nostrClient.updateWatchHistoryList = originalUpdateHistory;
console.warn = originalWarn;
window.NostrTools = originalNostrTools;
}
const viewCounterWarnings = warnings.filter((message) =>
message.includes("[viewCounter] Failed to ingest local view event")
);
assert.equal(
viewCounterWarnings.length,
0,
"viewCounter.ingestLocalViewEvent should not reject JSON view payloads"
);
}
// Cached totals older than VIEW_COUNT_CACHE_TTL_MS should be discarded and rehydrated from relays.
async function testHydrationRefreshesAfterCacheTtl() {
harness.reset();
@@ -568,5 +701,6 @@ await testHydrationSkipsStaleEventsAndRollsOff();
await testLocalIngestNotifiesImmediately();
await testUnsubscribeStopsCallbacks();
await testRelayCountAggregationUsesBestEstimate();
await testRecordVideoViewEmitsJsonPayload();
console.log("View counter tests completed successfully.");