mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
nip71: show creators' NIP-71 videos on their channel-profile wall (17b)
The channel-profile grid fetched only kind-30078, so a creator's NIP-71 videos (incl. their Nostube cross-posts) appeared in the main feed via ingest but were absent from their profile wall. New js/channelProfileVideos.js: - buildChannelVideoFilters(pubkey): native (kind 30078, #t=video) + NIP-71 (kinds 21/22/34235/34236) filters for the author. - convertChannelEvent(entry): routes NIP-71 kinds through buildVideoFromNip71Event (which rejects bitvid's own outbound mirrors so they don't duplicate the canonical note); kind-30078 uses the standard converter. channelProfile.js now uses these (net fewer lines, stays under size cap). Existing buildRenderableChannelVideos dedupe-by-root + canAccess handle precedence and whitelist/blacklist gating, so foreign videos inherit the same gating as native ones. Closes pre-launch TODO 17b. Tests + mutation-verified (NIP-71 routing, mirror skip, dual-filter build). 1253/1254 unit (known UploadModal flake). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
07b212943d
commit
7236496a16
4 files changed
+123
-20
No files matched your search
+6
-11
@@ -1,7 +1,7 @@
|
||||
// js/channelProfile.js
|
||||
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
||||
import { convertChannelEvent, buildChannelVideoFilters } from "./channelProfileVideos.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
@@ -4461,7 +4461,7 @@ function normalizeRenderableChannelVideo(entry) {
|
||||
typeof entry.kind === "number";
|
||||
|
||||
if (looksLikeRawEvent) {
|
||||
const converted = sharedConvertEventToVideo(entry);
|
||||
const converted = convertChannelEvent(entry);
|
||||
if (converted && !converted.invalid) {
|
||||
return converted;
|
||||
}
|
||||
@@ -5382,13 +5382,8 @@ async function loadUserVideos(pubkey) {
|
||||
});
|
||||
|
||||
try {
|
||||
// 1) Build filter for videos from this pubkey
|
||||
const filter = {
|
||||
kinds: [30078],
|
||||
authors: [pubkey],
|
||||
"#t": ["video"],
|
||||
limit: 200
|
||||
};
|
||||
// 1) Build filters for this pubkey's videos: native kind-30078 + NIP-71.
|
||||
const filters = buildChannelVideoFilters(pubkey);
|
||||
|
||||
// 2) Collect raw events from all relays
|
||||
const events = [];
|
||||
@@ -5401,7 +5396,7 @@ async function loadUserVideos(pubkey) {
|
||||
try {
|
||||
const fallbackEvents = await nostrClient.pool.list(
|
||||
Array.from(DEFAULT_RELAY_URLS),
|
||||
[filter]
|
||||
filters
|
||||
);
|
||||
if (Array.isArray(fallbackEvents)) {
|
||||
events.push(...fallbackEvents);
|
||||
@@ -5412,7 +5407,7 @@ async function loadUserVideos(pubkey) {
|
||||
} else {
|
||||
const settled = await pMapSettled(
|
||||
relayList,
|
||||
(url) => nostrClient.pool.list([url], [filter]),
|
||||
(url) => nostrClient.pool.list([url], filters),
|
||||
{ concurrency: RELAY_BACKGROUND_CONCURRENCY }
|
||||
);
|
||||
settled.forEach((result, index) => {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Channel-profile video sourcing: a creator's wall should show BOTH their
|
||||
// bitvid-native kind-30078 videos AND the NIP-71 videos they published via other
|
||||
// apps (Nostube etc.) — matching what the main feed shows via ingest. Previously
|
||||
// the profile fetched only kind-30078, so cross-posted NIP-71 videos were absent.
|
||||
|
||||
import { convertEventToVideo as sharedConvertEventToVideo } from "./nostr/index.js";
|
||||
import { buildVideoFromNip71Event, NIP71_KINDS } from "./nostr/nip71IngestAdapter.js";
|
||||
|
||||
// Two filters for one author: native video notes (kind 30078, tagged "video")
|
||||
// and their NIP-71 videos (kinds 21/22/34235/34236).
|
||||
export function buildChannelVideoFilters(pubkey, { limit = 200 } = {}) {
|
||||
return [
|
||||
{ kinds: [30078], authors: [pubkey], "#t": ["video"], limit },
|
||||
{ kinds: Array.from(NIP71_KINDS), authors: [pubkey], limit },
|
||||
];
|
||||
}
|
||||
|
||||
// Convert a raw channel event to a bitvid video object. Foreign NIP-71 kinds go
|
||||
// through the ingest adapter (which also rejects bitvid's own outbound mirrors so
|
||||
// they don't duplicate the canonical kind-30078 note); everything else uses the
|
||||
// standard kind-30078 converter. May return an { invalid: true } object — the
|
||||
// caller filters those out (same contract as convertEventToVideo).
|
||||
export function convertChannelEvent(entry) {
|
||||
const kind = Number(entry?.kind);
|
||||
if (NIP71_KINDS.has(kind)) {
|
||||
return buildVideoFromNip71Event(entry);
|
||||
}
|
||||
return sharedConvertEventToVideo(entry);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Channel-profile wall must source BOTH native kind-30078 videos and the
|
||||
// creator's NIP-71 videos (cross-posted via other apps), while skipping bitvid's
|
||||
// own outbound mirrors so they don't duplicate the canonical note.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
buildChannelVideoFilters,
|
||||
convertChannelEvent,
|
||||
} from "../js/channelProfileVideos.js";
|
||||
|
||||
const PK = "a".repeat(64);
|
||||
|
||||
test("builds a native (30078) filter AND a NIP-71 filter for the author", () => {
|
||||
const filters = buildChannelVideoFilters(PK);
|
||||
assert.equal(filters.length, 2);
|
||||
|
||||
const native = filters.find((f) => f.kinds.includes(30078));
|
||||
assert.deepEqual(native.authors, [PK]);
|
||||
assert.deepEqual(native["#t"], ["video"]);
|
||||
|
||||
const nip71 = filters.find((f) => !f.kinds.includes(30078));
|
||||
assert.deepEqual(nip71.kinds.slice().sort((a, b) => a - b), [21, 22, 34235, 34236]);
|
||||
assert.deepEqual(nip71.authors, [PK]);
|
||||
});
|
||||
|
||||
test("converts a foreign NIP-71 event into a renderable video", () => {
|
||||
const v = convertChannelEvent({
|
||||
id: "evt",
|
||||
pubkey: PK,
|
||||
kind: 34235,
|
||||
created_at: 1000,
|
||||
content: "desc",
|
||||
tags: [
|
||||
["d", "root"],
|
||||
["title", "Nostube clip"],
|
||||
["imeta", "url https://example.com/v.mp4", "m video/mp4"],
|
||||
],
|
||||
});
|
||||
assert.equal(v.invalid, false);
|
||||
assert.equal(v.title, "Nostube clip");
|
||||
assert.equal(v.source, "nip71-ingest");
|
||||
});
|
||||
|
||||
test("skips bitvid's own mirror (returns invalid so the caller drops it)", () => {
|
||||
const v = convertChannelEvent({
|
||||
id: "mirror",
|
||||
pubkey: PK,
|
||||
kind: 34235,
|
||||
created_at: 1000,
|
||||
content: "",
|
||||
tags: [
|
||||
["d", "root"],
|
||||
["client", "bitvid"],
|
||||
["title", "Our mirror"],
|
||||
["imeta", "url https://example.com/v.mp4", "m video/mp4"],
|
||||
],
|
||||
});
|
||||
assert.equal(v.invalid, true);
|
||||
assert.equal(v.reason, "bitvid-mirror");
|
||||
});
|
||||
|
||||
test("routes kind-30078 events through the standard converter", () => {
|
||||
const v = convertChannelEvent({
|
||||
id: "native",
|
||||
pubkey: PK,
|
||||
kind: 30078,
|
||||
created_at: 2000,
|
||||
content: JSON.stringify({
|
||||
version: 3,
|
||||
title: "Native bitvid video",
|
||||
videoRootId: "vr",
|
||||
url: "https://example.com/native.mp4",
|
||||
}),
|
||||
tags: [["d", "vr"], ["t", "video"]],
|
||||
});
|
||||
assert.equal(v.invalid, false);
|
||||
assert.equal(v.title, "Native bitvid video");
|
||||
// Not stamped as ingested — it's a native note.
|
||||
assert.notEqual(v.source, "nip71-ingest");
|
||||
});
|
||||
@@ -317,12 +317,10 @@ notes below. Summary:
|
||||
- Verified live: Goblinbox (Nostube) content renders; liveness check works.
|
||||
|
||||
### 17b. Channel Profile wall shows only bitvid videos, not NIP-71 (LAUNCH-BLOCKER)
|
||||
- [ ] The channel-profile grid (`js/channelProfile.js`, ~line 5387) fetches ONLY
|
||||
`{ kinds:[30078], authors:[pubkey], "#t":["video"] }`, so a creator's
|
||||
NIP-71 videos (kinds 21/22/34235/34236 — incl. their Nostube uploads) never
|
||||
appear on their profile wall even though they show in the main feed via
|
||||
ingest. Fix: also fetch the author's NIP-71 video kinds and merge them
|
||||
(convert via `buildVideoFromNip71Event`, skip bitvid mirrors, dedupe by
|
||||
videoRootId, native-30078 wins a shared root — same precedence as ingest).
|
||||
Consider reusing the ingested videos already in `nostrClient.allEvents` for
|
||||
that author rather than a fresh fetch.
|
||||
- [x] **Fixed** (`js/channelProfileVideos.js`): the channel grid now fetches the
|
||||
author's NIP-71 videos (kinds 21/22/34235/34236) alongside kind-30078 via
|
||||
`buildChannelVideoFilters`, and `convertChannelEvent` routes NIP-71 events
|
||||
through `buildVideoFromNip71Event` (skips bitvid mirrors). Existing
|
||||
dedupe-by-root + canAccess in `buildRenderableChannelVideos` handle
|
||||
precedence/gating. Logic extracted to a helper so channelProfile.js stayed
|
||||
under its size cap. Tests + mutation-verified.
|
||||
Reference in new issue
Block a user