[video-modal] Fix dead like/dislike buttons — dispatch video:reaction (#54)

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-02 09:40:46 -04:00
1 parent 757fca1059
commit 6e2894e3d8
3 files changed
+101 -3

No files matched your search

+30 -3
View File
@@ -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() {
+60
View File
@@ -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");
});
+11
View File
@@ -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