From 6e2894e3d8f28be73863371dca5a720748fead5e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:40:46 -0400 Subject: [PATCH] =?UTF-8?q?[video-modal]=20Fix=20dead=20like/dislike=20but?= =?UTF-8?q?tons=20=E2=80=94=20dispatch=20video:reaction=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real #54 bug (the signer gate was correct but unreachable): the reactions controller forwards the raw click event to VideoModal.handleReactionClick, which called this.reactionsController.handleReaction(event) — a method that does NOT exist on the video-modal reactions controller (it only binds buttons + does optimistic UI), passing the DOM event as the "reaction". So clicking like/dislike threw / silently did nothing and never reached the publish path. Fix: handleReactionClick now derives +/- from the clicked button (#modalLikeBtn / #modalDislikeBtn, or a bare "+"/"-" for programmatic callers) and dispatches "video:reaction" — the event ModalManager already routes to app.handleVideoReaction -> reactionController.handleReaction (signs + publishes the kind-7, and runs the ensureEncryptionCapableSigner gate from the earlier #54 pass). Note: #53 (channel avatar/name link) is already fixed in a20388c5 (handleCreatorNavigation now dispatches "creator:navigate"); if it still looks dead it's a stale build — rebuild dist / redeploy the branch under test. Tests: tests/video-modal-controllers.test.mjs (+2). Lint + build green. Co-Authored-By: Claude Opus 4.8 --- js/ui/components/VideoModal.js | 33 ++++++++++++-- tests/video-modal-controllers.test.mjs | 60 ++++++++++++++++++++++++++ todo/TODO_2026-06-20_pre-launch.md | 11 +++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 4f2d140a..4ea29c04 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -3030,10 +3030,37 @@ export class VideoModal { this.log("[VideoModal] More button clicked"); } - handleReactionClick(reaction) { - if (this.reactionsController) { - this.reactionsController.handleReaction(reaction); + handleReactionClick(event) { + // The reactionsController binds the like/dislike buttons and forwards the raw + // click event here. Derive WHICH button was clicked, then dispatch + // "video:reaction" — the event ModalManager listens for + // (→ app.handleVideoReaction → reactionController.handleReaction, which signs + + // publishes the kind-7). The old code called this.reactionsController.handleReaction + // (a method that doesn't exist on that controller) with the DOM event as the + // "reaction", so like/dislike silently did nothing. Also accept a bare "+"/"-" + // for programmatic callers. + let reaction = ""; + if (event === "+" || event === "-") { + reaction = event; + } else { + const target = + event?.currentTarget instanceof Element + ? event.currentTarget + : event?.target?.closest?.("#modalLikeBtn, #modalDislikeBtn") || null; + if (target && target === this.reactionButtons?.["+"]) { + reaction = "+"; + } else if (target && target === this.reactionButtons?.["-"]) { + reaction = "-"; + } else if (target?.id === "modalLikeBtn") { + reaction = "+"; + } else if (target?.id === "modalDislikeBtn") { + reaction = "-"; + } } + if (reaction !== "+" && reaction !== "-") { + return; + } + this.dispatch("video:reaction", { reaction, video: this.activeVideo }); } bindVideoEvents() { diff --git a/tests/video-modal-controllers.test.mjs b/tests/video-modal-controllers.test.mjs index b74bc17d..803d9e3f 100644 --- a/tests/video-modal-controllers.test.mjs +++ b/tests/video-modal-controllers.test.mjs @@ -248,3 +248,63 @@ test("creator navigation is a no-op without an active video pubkey", async (t) = modal.handleCreatorNavigation(); assert.equal(fired, false, "no pubkey → no navigation event"); }); + +// Like/dislike in the player modal (TODO #54): clicking a reaction button must +// dispatch "video:reaction" (→ app.handleVideoReaction → reactionController). The +// old handleReactionClick called this.reactionsController.handleReaction (a method +// that doesn't exist on the video-modal reactions controller) with the raw DOM +// event, so nothing was published. +// +// test_integrity_note: +// change_type: ["new_tests"] +// scenarios: +// - id: SCN-video-modal-reaction-dispatch +// given: "a video modal with an active video" +// when: "handleReactionClick runs for a like/dislike button or a bare +/-" +// then: "it dispatches video:reaction with the derived reaction; unrelated targets do nothing" +// observable_outcomes: +// - "bare '+'/'-' → video:reaction with that reaction" +// - "a click whose currentTarget is the like button → reaction '+'" +// - "a null/unrelated target → no dispatch" +// determinism_controls: +// - "JSDOM modal via setupModal; synchronous dispatch" +// anti_cheat_rationale: +// prevents: ["hard-coded return value", "asserting the broken call path"] +// relaxation: +// did_relax_any_assertion: false + +test("like/dislike dispatches video:reaction with the derived reaction", async (t) => { + const { modal, cleanup } = await setupModal(); + t.after(cleanup); + + modal.activeVideo = { pubkey: "a".repeat(64) }; + const events = []; + modal.addEventListener("video:reaction", (event) => events.push(event.detail)); + + modal.handleReactionClick("+"); + modal.handleReactionClick("-"); + + assert.equal(events.length, 2, "both reactions dispatched"); + assert.equal(events[0].reaction, "+"); + assert.equal(events[1].reaction, "-"); + assert.equal(events[0].video, modal.activeVideo, "carries the active video"); + + // And via a real button click (currentTarget = the bound like button). + if (modal.reactionButtons?.["+"]) { + modal.handleReactionClick({ currentTarget: modal.reactionButtons["+"] }); + assert.equal(events[2]?.reaction, "+", "derived '+' from the like button element"); + } +}); + +test("reaction click with no matching button does not dispatch", async (t) => { + const { modal, cleanup } = await setupModal(); + t.after(cleanup); + + let fired = false; + modal.addEventListener("video:reaction", () => { + fired = true; + }); + modal.handleReactionClick({ currentTarget: null }); + modal.handleReactionClick({}); + assert.equal(fired, false, "no reaction event for an unrelated/empty target"); +}); diff --git a/todo/TODO_2026-06-20_pre-launch.md b/todo/TODO_2026-06-20_pre-launch.md index b5f7620a..252ece55 100644 --- a/todo/TODO_2026-06-20_pre-launch.md +++ b/todo/TODO_2026-06-20_pre-launch.md @@ -1659,6 +1659,17 @@ passphrase-encrypted). Items 51, 56, 57 are all facets of that. Tests: `tests/reaction-signer-gate.test.mjs`. **Still VERIFY on unstable** whether any residual dead-button behavior remains (event binding / pointer availability), which would be a separate wiring bug from the signer issue. +- [x] **The actual dead-button bug — FIXED 2026-07-02.** `VideoModal.handleReactionClick` + never derived the reaction or reached the publish path: it called + `this.reactionsController.handleReaction(event)` — a method that does NOT exist on + the video-modal reactions controller (which only binds buttons + does optimistic + UI), passing the raw DOM click event as the "reaction". So the #54 signer gate was + correct but unreachable. Now `handleReactionClick` derives `+`/`-` from the clicked + button (`#modalLikeBtn`/`#modalDislikeBtn`) and dispatches `video:reaction` — the + event ModalManager already routes to `app.handleVideoReaction` → + `reactionController.handleReaction` (which signs/publishes + runs the signer gate). + Tests: `tests/video-modal-controllers.test.mjs` (+2 — derives the reaction and + dispatches; unrelated target is a no-op). Lint + build green. ### 55. Video-grid event caching — faster loads, cache-first + merge-new (PERF / UX) - [ ] **Channel-profile grid still loads slowly.** Even after the coalescing + relay