Files
bitvid/tests/share-note-body.test.mjs
T
thePR0M3TH3ANandClaude Opus 5 38b51f13a5 [share] Give share-to-Nostr notes a real preview
"Share on Nostr" published a kind-1 note whose body ended with a bare thumbnail
URL. Many clients only inline such a URL when a NIP-92 `imeta` entry describes
it, so the viewer often saw a plain link instead of a preview image.

- buildShareEvent now emits an `imeta` entry for the thumbnail. The media type
  is inferred from the extension and OMITTED when unknown -- a wrong `m` is
  worse than a missing one, since clients use it to decide whether to render
  inline at all, so extensionless Blossom URLs ship url + alt only.
- inferImageMimeTypeFromUrl lives in a SEPARATE map from EXTENSION_MIME_MAP:
  that map gates video upload/publish paths, and folding image types into it
  would let an image pass a video check.
- shareNostrController was passing only { id, pubkey } to buildShareEvent, so
  the tag could never be built; it now forwards title and thumbnail.

When a video has a NIP-71 mirror, the note instead quotes it with
`nostr:naddr1...`, which nostr clients render as a native video quote card --
the closest thing to a rich preview that does not depend on any client fetching
OpenGraph tags (most never do).

- The mirror is addressable (kind 34235/34236, author = video pubkey,
  d = videoRootId), so js/nostr/mirrorPointer.js derives the coordinate with no
  relay lookup, and the pointer survives the author editing the video where a
  plain `nevent` would rot on the next replace.
- The pointer is only embedded after nip71MirrorService.findMirror() confirms
  the mirror exists. A pointer to a missing event renders as a broken/empty
  quote box, which is worse than the plain-link fallback, so a disabled
  service, a throwing lookup, a 2.5s timeout or a missing videoRootId all fall
  back to the thumbnail note.
- resolveMirrorKind prefers the kinds actually observed on relays and only then
  falls back to the same portrait rule the mirror builder uses, so a derived
  pointer matches what would be published rather than guessing independently.
- While quoting, the raw thumbnail URL is dropped from the body and the imeta
  tag is skipped: the quote card already shows the thumbnail, and emitting both
  rendered the same image twice. An `a` tag links the note to the coordinate.

Known limitation: a client rendering the quote card still has to play the
video, so mirrors whose imeta url is an .m3u8 hit the same HLS wall -- title and
thumbnail render, playback depends on the client.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 13:59:10 -04:00

80 lines
3.1 KiB
JavaScript

// SCN-share-body: what the viewer actually reads in the composed note.
//
// Two shapes, chosen by whether a NIP-71 mirror was confirmed on relays:
// mirror -> quote card via `nostr:naddr1…`, NO bare thumbnail URL
// no mirror -> bare thumbnail URL (described by the note's NIP-92 imeta)
//
// The "no bare thumbnail while quoting" rule is the one that matters visually:
// keeping both rendered the same image twice (once loose, once in the card).
import test from "node:test";
import assert from "node:assert/strict";
import { ShareNostrModal } from "../js/ui/components/ShareNostrModal.js";
// buildShareContent is a pure string builder; construct without touching the
// DOM-dependent parts of the modal.
const modal = Object.create(ShareNostrModal.prototype);
const build = (payload) => modal.buildShareContent(payload);
const BASE = {
title: "Why I'm (sort of) not worried about AI",
shareUrl: "https://bitvid.network/?v=nevent1abc",
thumbnail: "https://cdn.example/thumb.jpg",
};
test("without a mirror the body ends with the thumbnail URL", () => {
const body = build(BASE);
assert.equal(
body,
"Why I'm (sort of) not worried about AI — Check out this video on bitvid 👇\n\n" +
"https://bitvid.network/?v=nevent1abc\n\n" +
"https://cdn.example/thumb.jpg"
);
});
test("with a mirror the body quotes it and omits the thumbnail URL", () => {
const body = build({ ...BASE, mirrorNaddr: "naddr1qqxnzd3e" });
assert.match(body, /nostr:naddr1qqxnzd3e$/);
assert.equal(
body.includes("https://cdn.example/thumb.jpg"),
false,
"quote card already shows the thumbnail; a loose URL duplicates it"
);
// The bitvid link stays — attribution and traffic still matter.
assert.match(body, /https:\/\/bitvid\.network\/\?v=nevent1abc/);
});
test("the quote reference uses the nostr: URI scheme clients resolve", () => {
const body = build({ ...BASE, mirrorNaddr: "naddr1qqxnzd3e" });
assert.equal(body.includes("nostr:naddr1qqxnzd3e"), true);
// A bare naddr without the scheme is rendered as plain text by most clients.
assert.equal(/(^|\s)naddr1qqxnzd3e/.test(body), false);
});
test("an empty or whitespace mirror pointer falls back to the thumbnail", () => {
for (const mirrorNaddr of ["", " ", null, undefined]) {
const body = build({ ...BASE, mirrorNaddr });
assert.match(body, /https:\/\/cdn\.example\/thumb\.jpg$/);
assert.equal(body.includes("nostr:"), false);
}
});
test("missing pieces never leave dangling blank sections", () => {
const noThumb = build({ title: "T", shareUrl: "https://bitvid.network/?v=x" });
assert.equal(noThumb.endsWith("\n"), false);
assert.equal(/\n{3,}/.test(noThumb), false);
const titleOnly = build({ title: "T" });
assert.equal(titleOnly, "T — Check out this video on bitvid 👇");
const empty = build({});
assert.equal(empty, "Untitled video — Check out this video on bitvid 👇");
});
test("a missing title degrades to a placeholder rather than 'undefined'", () => {
const body = build({ ...BASE, title: " " });
assert.match(body, /^Untitled video — /);
assert.equal(body.includes("undefined"), false);
});