Promote unstable → beta: share popover copy buttons fix

Wire the video-modal share popover's Copy URL / Copy Magnet / Copy CDN / Share
on Nostr actions (were dead — no click delegation).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-09 12:47:17 -04:00
commit d8d1ccb1f6
3 files changed
+178

No files matched your search

+79
View File
@@ -3450,6 +3450,81 @@ export class VideoModal {
}
}
// Delegated click handler for the share popover's menu-action buttons
// (Copy URL / Copy Magnet / Copy CDN / Share on Nostr). The forced-source
// test links self-wire their own copy, so they are skipped here.
bindShareMenuActions(panel) {
if (!panel || typeof panel.addEventListener !== "function") {
return;
}
panel.addEventListener("click", (event) => {
const target = event.target;
const button =
target && typeof target.closest === "function"
? target.closest("button[data-action]")
: null;
if (!button || button.disabled) {
return;
}
const action = button.dataset?.action || "";
if (action === "copy-webtorrent-url" || action === "copy-cdn-url") {
return; // self-wired in createVideoShareMenuPanel
}
event.preventDefault();
event.stopPropagation();
this.handleShareMenuAction(action);
this.modalSharePopover?.close?.();
});
}
handleShareMenuAction(action) {
const video = this.activeVideo;
switch (action) {
case "copy-magnet":
this.dispatch("video:copy-magnet", { video });
break;
case "copy-cdn":
this.dispatch("video:copy-cdn", { video });
break;
case "share-nostr":
this.dispatch("video:share-nostr", {
video,
trigger: this.shareBtn || null,
});
break;
case "share":
// "Copy URL" — the shareable deep link to this video.
this.dispatch("video:copy-url", {
video,
url: this.buildShareUrl(video),
});
break;
default:
break;
}
}
// Plain `?v=<nevent>` share link (no forced playback source).
buildShareUrl(video) {
const eventId =
video && typeof video.id === "string" ? video.id.trim() : "";
if (!eventId) {
return "";
}
const win = this.window || globalThis;
const neventEncode = win?.NostrTools?.nip19?.neventEncode;
const loc = win?.location;
if (typeof neventEncode !== "function" || !loc) {
return "";
}
try {
const nevent = neventEncode({ id: eventId });
return `${loc.origin}${loc.pathname}?v=${encodeURIComponent(nevent)}`;
} catch (error) {
return "";
}
}
setupModalSharePopover() {
if (!this.playerModal || !this.shareBtn) {
return;
@@ -3483,6 +3558,10 @@ export class VideoModal {
this.buildForcedSourceShareUrl(this.activeVideo, playback),
});
if (panel) {
// The share menu's Copy URL / Copy Magnet / Copy CDN / Share on Nostr
// items only set `data-action` (like the ⋯ menu) — unlike the ⋯ menu,
// nothing wired their clicks, so they were dead. Delegate them here.
this.bindShareMenuActions(panel);
container.appendChild(panel);
}
return panel;
+26
View File
@@ -69,6 +69,10 @@ export default class VideoModalController {
this.handleCopyCdn(event);
});
this.videoModal.addEventListener("video:copy-url", (event) => {
this.handleCopyUrl(event);
});
this.videoModal.addEventListener("video:copy-magnet", () => {
if (this.handleCopyMagnetCallback) {
this.handleCopyMagnetCallback();
@@ -115,6 +119,28 @@ export default class VideoModalController {
});
}
handleCopyUrl(event) {
const url = event?.detail?.url || "";
if (!url) {
if (this.showError) {
this.showError("Could not generate a share link.");
}
return;
}
navigator.clipboard
.writeText(url)
.then(() => {
if (this.showSuccess) {
this.showSuccess("Video link copied to clipboard!");
}
})
.catch(() => {
if (this.showError) {
this.showError("Failed to copy the link.");
}
});
}
handleSourceSwitch(event) {
const detail = event?.detail || {};
const { source } = detail;
+73
View File
@@ -308,3 +308,76 @@ test("reaction click with no matching button does not dispatch", async (t) => {
modal.handleReactionClick({});
assert.equal(fired, false, "no reaction event for an unrelated/empty target");
});
// Share popover (reported: "Copy URL / Copy Magnet / Copy CDN don't work"). The
// share menu buttons only set data-action (like the ⋯ menu), but unlike the ⋯
// menu nothing wired their clicks, so they were dead. handleShareMenuAction now
// routes each to the event videoModalController already handles.
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-video-modal-share-menu-routing
// given: "a video modal with an active video"
// when: "handleShareMenuAction runs for a share-menu action"
// then: "it dispatches the matching video:* event; forced-source actions do not re-route"
// determinism_controls:
// - "JSDOM modal via setupModal; synchronous dispatch"
// relaxation:
// did_relax_any_assertion: false
test("share menu Copy Magnet routes to video:copy-magnet", async (t) => {
const { modal, cleanup } = await setupModal();
t.after(cleanup);
modal.activeVideo = { id: "e".repeat(64), url: "https://x/v.mp4", magnet: "" };
const events = [];
modal.addEventListener("video:copy-magnet", (e) => events.push(e.detail));
modal.handleShareMenuAction("copy-magnet");
assert.equal(events.length, 1, "Copy Magnet dispatches video:copy-magnet");
assert.equal(events[0].video.id, "e".repeat(64));
});
test("share menu Copy CDN routes to video:copy-cdn", async (t) => {
const { modal, cleanup } = await setupModal();
t.after(cleanup);
modal.activeVideo = { id: "e".repeat(64), url: "https://x/v.mp4" };
const events = [];
modal.addEventListener("video:copy-cdn", (e) => events.push(e.detail));
modal.handleShareMenuAction("copy-cdn");
assert.equal(events.length, 1, "Copy CDN dispatches video:copy-cdn");
assert.equal(events[0].video.url, "https://x/v.mp4");
});
test("share menu Copy URL routes to video:copy-url", async (t) => {
const { modal, cleanup } = await setupModal();
t.after(cleanup);
modal.activeVideo = { id: "e".repeat(64) };
const events = [];
modal.addEventListener("video:copy-url", (e) => events.push(e.detail));
modal.handleShareMenuAction("share");
assert.equal(events.length, 1, "Copy URL dispatches video:copy-url");
assert.equal(typeof events[0].url, "string");
});
test("share menu Share on Nostr routes to video:share-nostr", async (t) => {
const { modal, cleanup } = await setupModal();
t.after(cleanup);
modal.activeVideo = { id: "e".repeat(64) };
const events = [];
modal.addEventListener("video:share-nostr", (e) => events.push(e.detail));
modal.handleShareMenuAction("share-nostr");
assert.equal(events.length, 1, "Share on Nostr dispatches video:share-nostr");
});
test("share menu forced-source actions do not re-route through handleShareMenuAction", async (t) => {
const { modal, cleanup } = await setupModal();
t.after(cleanup);
modal.activeVideo = { id: "e".repeat(64) };
let fired = false;
["video:copy-magnet", "video:copy-cdn", "video:copy-url", "video:share-nostr"].forEach(
(name) => modal.addEventListener(name, () => { fired = true; })
);
modal.handleShareMenuAction("copy-cdn-url");
modal.handleShareMenuAction("copy-webtorrent-url");
assert.equal(fired, false, "forced-source links self-wire; not routed here");
});