mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
zaps: fix badge never showing (array pointers) + zap total in the player modal
The zap-totals store's pointerKey only understood {type,value} objects, but
deriveVideoPointerInfo produces tag-style ARRAYS (["a", value, relay?]) — so
every card-badge lookup keyed to nothing, no receipt fetch was ever
scheduled, and the Most Zapped ranking metric silently read 0 for every
video. pointerKey now normalizes both shapes; tests use the array shape the
app actually passes.
Also adds the orange sats badge to the video player modal's meta row
(right side via the row's existing justify-between; views stay left),
driven by the same cache/change-signal as the cards and torn down with the
view-count subscription.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
ad62205afd
commit
cbe894969d
7 files changed
+106
-5
No files matched your search
@@ -211,6 +211,16 @@
|
||||
<span data-view-count-text>–</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
id="videoZapTotal"
|
||||
class="modal-meta video-modal__zaps flex items-center gap-1.5 hidden"
|
||||
title="Zap total (sats)"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
||||
</svg>
|
||||
<span data-zap-total-text></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="video-modal__actions-row">
|
||||
|
||||
@@ -2916,6 +2916,13 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Same treatment in the player modal: orange sats on the far side of the
|
||||
meta row (the row is justify-between, views stay left). */
|
||||
.video-modal__zaps {
|
||||
color: var(--color-zap);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* #43: always-visible active-identity pill. Rendered after the header in
|
||||
flow and pulled up with a negative block-start margin so it sits half-on /
|
||||
half-off the header divider — every pane shows who you're acting as. */
|
||||
|
||||
@@ -119,7 +119,12 @@ import {
|
||||
formatViewCount,
|
||||
ingestLocalViewEvent,
|
||||
} from "./viewCounter.js";
|
||||
import { initZapTotals } from "./zapTotals.js";
|
||||
import {
|
||||
initZapTotals,
|
||||
requestVideoZapTotal,
|
||||
getVideoZapTotalSnapshot,
|
||||
onZapTotalsChanged,
|
||||
} from "./zapTotals.js";
|
||||
import {
|
||||
formatAbsoluteTimestamp as formatAbsoluteTimestampUtil,
|
||||
formatAbsoluteDateWithOrdinal as formatAbsoluteDateWithOrdinalUtil,
|
||||
@@ -699,6 +704,9 @@ class Application {
|
||||
unsubscribeFromVideoViewCount,
|
||||
formatViewCount,
|
||||
ingestLocalViewEvent,
|
||||
requestVideoZapTotal,
|
||||
getVideoZapTotalSnapshot,
|
||||
onZapTotalsChanged,
|
||||
pointerArrayToKey,
|
||||
pointerKey: pointerKey,
|
||||
getCanonicalDesignSystemMode,
|
||||
|
||||
@@ -25,6 +25,9 @@ export function createModalCoordinator(deps) {
|
||||
unsubscribeFromVideoViewCount,
|
||||
formatViewCount,
|
||||
ingestLocalViewEvent,
|
||||
requestVideoZapTotal,
|
||||
getVideoZapTotalSnapshot,
|
||||
onZapTotalsChanged,
|
||||
pointerArrayToKey,
|
||||
pointerKey,
|
||||
getCanonicalDesignSystemMode,
|
||||
@@ -117,12 +120,66 @@ export function createModalCoordinator(deps) {
|
||||
}
|
||||
}
|
||||
this.modalViewCountUnsub = null;
|
||||
this.teardownModalZapTotal();
|
||||
if (this.videoModal) {
|
||||
this.videoModal.updateViewCountLabel("\u2013 views");
|
||||
this.videoModal.setViewCountPointer(null);
|
||||
}
|
||||
},
|
||||
|
||||
teardownModalZapTotal() {
|
||||
if (typeof this.modalZapTotalUnsub === "function") {
|
||||
try {
|
||||
this.modalZapTotalUnsub();
|
||||
} catch (error) {
|
||||
devLogger.warn("[zapTotals] Failed to tear down modal listener:", error);
|
||||
}
|
||||
}
|
||||
this.modalZapTotalUnsub = null;
|
||||
const el =
|
||||
typeof document !== "undefined"
|
||||
? document.getElementById("videoZapTotal")
|
||||
: null;
|
||||
if (el) {
|
||||
el.classList.add("hidden");
|
||||
const text = el.querySelector("[data-zap-total-text]");
|
||||
if (text) {
|
||||
text.textContent = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Orange sats badge on the modal's meta row (right side; views stay left).
|
||||
// Mirrors the card badge: cached total now (also schedules the batched
|
||||
// receipt fetch), re-rendered when a batch lands, hidden while zero.
|
||||
subscribeModalZapTotal(pointer) {
|
||||
if (typeof requestVideoZapTotal !== "function") {
|
||||
return;
|
||||
}
|
||||
const el =
|
||||
typeof document !== "undefined"
|
||||
? document.getElementById("videoZapTotal")
|
||||
: null;
|
||||
if (!el || !pointer) {
|
||||
return;
|
||||
}
|
||||
const render = () => {
|
||||
const sats = getVideoZapTotalSnapshot(pointer);
|
||||
const text = el.querySelector("[data-zap-total-text]");
|
||||
if (Number.isFinite(sats) && sats > 0) {
|
||||
if (text) {
|
||||
text.textContent = `${formatViewCount(sats)} sats`;
|
||||
}
|
||||
el.classList.remove("hidden");
|
||||
} else {
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
requestVideoZapTotal(pointer);
|
||||
render();
|
||||
this.modalZapTotalUnsub = onZapTotalsChanged(render);
|
||||
},
|
||||
|
||||
subscribeModalViewCount(pointer, pointerKey) {
|
||||
const viewEl = this.videoModal?.getViewCountElement() || null;
|
||||
if (!viewEl) {
|
||||
@@ -139,6 +196,7 @@ export function createModalCoordinator(deps) {
|
||||
this.videoModal.updateViewCountLabel("Loading views\u2026");
|
||||
this.videoModal.setViewCountPointer(pointerKey);
|
||||
}
|
||||
this.subscribeModalZapTotal(pointer);
|
||||
try {
|
||||
const token = subscribeToVideoViewCount(pointer, ({ total, status, partial }) => {
|
||||
const latestViewEl = this.videoModal?.getViewCountElement() || null;
|
||||
|
||||
+12
-2
@@ -29,9 +29,19 @@ function tagValues(event, name) {
|
||||
.map((tag) => tag[1]);
|
||||
}
|
||||
|
||||
// Accepts BOTH pointer shapes used in the app: the canonical tag-style array
|
||||
// ["a"|"e", value, relay?] (what deriveVideoPointerInfo/resolveVideoPointer
|
||||
// produce) and the object form { type, value }.
|
||||
export function pointerKey(pointer) {
|
||||
const type = pointer?.type === "e" ? "e" : pointer?.type === "a" ? "a" : "";
|
||||
const value = typeof pointer?.value === "string" ? pointer.value.trim() : "";
|
||||
let type = "";
|
||||
let value = "";
|
||||
if (Array.isArray(pointer)) {
|
||||
type = pointer[0] === "a" || pointer[0] === "e" ? pointer[0] : "";
|
||||
value = typeof pointer[1] === "string" ? pointer[1].trim() : "";
|
||||
} else if (pointer && typeof pointer === "object") {
|
||||
type = pointer.type === "a" || pointer.type === "e" ? pointer.type : "";
|
||||
value = typeof pointer.value === "string" ? pointer.value.trim() : "";
|
||||
}
|
||||
if (!type || !value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ test("binder: zero stays hidden, receipts reveal and format, prune drops detache
|
||||
const badge = card.querySelector("[data-zap-total]");
|
||||
const wrapper = card.querySelector(".video-card__zaps");
|
||||
|
||||
const pointerInfo = { key: `a:${A1}`, pointer: { type: "a", value: A1 } };
|
||||
// Tag-style ARRAY pointer — the shape VideoListView actually passes.
|
||||
const pointerInfo = { key: `a:${A1}`, pointer: ["a", A1] };
|
||||
binder.bind(card, pointerInfo);
|
||||
assert.equal(wrapper.classList.contains("hidden"), true, "unknown total stays hidden");
|
||||
assert.equal(badge.textContent, "");
|
||||
|
||||
@@ -65,7 +65,14 @@ test("amount extraction: bolt11 first, zap-request amount fallback, garbage →
|
||||
);
|
||||
});
|
||||
|
||||
test("pointerKey normalizes and rejects unusable pointers", () => {
|
||||
test("pointerKey accepts BOTH app shapes and rejects unusable pointers", () => {
|
||||
// Tag-style ARRAY — what deriveVideoPointerInfo/resolveVideoPointer actually
|
||||
// produce (regression: the object-only version silently zeroed every card
|
||||
// badge and the Most Zapped ranking).
|
||||
assert.equal(pointerKey(["a", A1]), `a:${A1}`);
|
||||
assert.equal(pointerKey(["e", "evt1", "wss://relay.example"]), "e:evt1");
|
||||
assert.equal(pointerKey(["x", "nope"]), "");
|
||||
// Object form.
|
||||
assert.equal(pointerKey({ type: "a", value: A1 }), `a:${A1}`);
|
||||
assert.equal(pointerKey({ type: "e", value: "evt1" }), "e:evt1");
|
||||
assert.equal(pointerKey({ type: "x", value: "nope" }), "");
|
||||
|
||||
Reference in new issue
Block a user