Files
bitvid/js/nostr/mirrorPointer.js
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

118 lines
3.9 KiB
JavaScript

// Addressable pointers to a video's NIP-71 mirror.
//
// The mirror (js/nostr/nip71Mirror.js) is an ADDRESSABLE event: kind
// 34235/34236, author = the video's pubkey, `d` = videoRootId. That makes its
// coordinate fully derivable from a video object with no relay lookup, and the
// pointer keeps resolving after the author edits the video (a plain `nevent`
// would rot on the next replace).
//
// Used by the share flow so a shared note can embed `nostr:naddr1…`, which
// nostr clients render as a native video quote card instead of a bare link.
import {
NIP71_NORMAL_VIDEO_KIND,
NIP71_SHORT_VIDEO_KIND,
} from "./nip71Mirror.js";
export { NIP71_NORMAL_VIDEO_KIND, NIP71_SHORT_VIDEO_KIND };
const str = (value) => (typeof value === "string" ? value.trim() : "");
/**
* Which mirror kind a video's pointer should address.
*
* Prefers `kinds` — the kinds actually observed on relays (nip71MirrorService
* `findMirror`), which is ground truth. Only when that is unavailable does it
* fall back to the SAME portrait heuristic the mirror builder uses
* (nip71Mirror.js: `hasDims && height > width`), so a derived pointer matches
* the event that would be published rather than guessing independently.
*
* Returns 0 when there is nothing to point at.
*/
export function resolveMirrorKind({ kinds = [], width, height } = {}) {
const observed = (Array.isArray(kinds) ? kinds : []).filter(
(kind) => kind === NIP71_NORMAL_VIDEO_KIND || kind === NIP71_SHORT_VIDEO_KIND
);
if (observed.length === 1) {
return observed[0];
}
if (observed.length > 1) {
// Cross-kind duplicate (nip71MirrorService flags this as `duplicate`).
// Prefer the normal kind so the pointer is stable rather than arbitrary.
return observed.includes(NIP71_NORMAL_VIDEO_KIND)
? NIP71_NORMAL_VIDEO_KIND
: observed[0];
}
const w = Number(width);
const h = Number(height);
const hasDims = Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0;
if (!hasDims) {
return NIP71_NORMAL_VIDEO_KIND;
}
return h > w ? NIP71_SHORT_VIDEO_KIND : NIP71_NORMAL_VIDEO_KIND;
}
/**
* Build the `a`-tag coordinate (`kind:pubkey:d`) for a video's mirror.
* Returns "" when the inputs cannot address anything.
*/
export function buildMirrorCoordinate({ pubkey, videoRootId, kind } = {}) {
const author = str(pubkey);
const identifier = str(videoRootId);
const resolvedKind = Number(kind);
if (!author || !identifier || !Number.isFinite(resolvedKind) || resolvedKind <= 0) {
return "";
}
return `${resolvedKind}:${author}:${identifier}`;
}
/**
* Build `naddr1…` for a video's mirror.
*
* `nip19` is injected so this stays testable without a browser; callers in the
* app pass `window.NostrTools.nip19`. Returns "" on any failure — a share note
* must never embed a malformed pointer, because clients render that as a broken
* quote box, which is strictly worse than the plain-link fallback.
*
* Relay hints matter here: without them a client that doesn't already have the
* mirror has nowhere to fetch it from, and the quote card renders empty.
*/
export function buildMirrorNaddr({
pubkey,
videoRootId,
kind,
relays = [],
nip19,
} = {}) {
const author = str(pubkey);
const identifier = str(videoRootId);
const resolvedKind = Number(kind);
if (!author || !identifier || !Number.isFinite(resolvedKind) || resolvedKind <= 0) {
return "";
}
if (!nip19 || typeof nip19.naddrEncode !== "function") {
return "";
}
const relayHints = (Array.isArray(relays) ? relays : [])
.map((entry) => str(entry))
.filter((entry) => /^wss?:\/\//i.test(entry))
// A handful is plenty; an naddr carrying a user's whole relay list gets
// unwieldy in note bodies and leaks more than it helps.
.slice(0, 3);
try {
return (
nip19.naddrEncode({
kind: resolvedKind,
pubkey: author,
identifier,
relays: relayHints,
}) || ""
);
} catch (err) {
return "";
}
}