[ui] #34(b): My Videos mirror toggle reflects relay truth, not just the local flag

The per-row "Share to apps" toggle read only the device-local opt-in flag
(nip71MirrorFlags), so a fresh device / cleared cache / other device reported a
video as un-shared even when its NIP-71 mirror events (34235/34236) exist on the
user's relays.

- nip71MirrorService: add batched findMirrors(videos) -> Map<videoRootId,
  {mirrored, kinds, duplicate}>, one lookup per root via the SubscriptionManager
  chokepoint (no direct pool.list).
- MyVideosController: tag each mirror button with data-mirror-root; after the
  synchronous (local-flag) render, refreshMirrorStates() looks up relay truth,
  reconciles the local flag to match (flag stays a cache hint), and corrects each
  button label. Fire-and-forget; detection failures fall back to the local flag.
- tests/my-videos-mirror-relay-truth.test.mjs: 4 jsdom behavioral tests
  (promote unmirrored->shared, clear stale shared, per-row batch reconcile,
  button tagging). Build + lint clean; mirror+my-videos suites 48/48.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-29 21:45:23 -04:00
1 parent 26d67d59b2
commit eb4d071203
4 files changed
+235 -7

No files matched your search

+57 -1
View File
@@ -336,6 +336,9 @@ export class MyVideosController {
for (const video of rows) {
this.listEl.appendChild(this.buildRow(video));
}
// After the synchronous render (which used the local flag), correct the mirror
// labels from relay truth in the background. Fire-and-forget.
this.refreshMirrorStates(rows);
}
buildRow(video) {
@@ -441,10 +444,63 @@ export class MyVideosController {
btn.classList.add("opacity-50");
return btn;
}
return this.buildActionButton(
const btn = this.buildActionButton(
enabled ? "Shared ✓" : "Share to apps",
(event) => this.handleToggleMirror(video, event?.currentTarget || null),
);
// Tag with the root so the batched relay-truth refresh can update this button.
const root = video?.videoRootId;
if (root) {
btn.dataset.mirrorRoot = String(root);
}
return btn;
}
// Relay-truth detection (#34): the per-row mirror label initially uses the
// device-local flag (synchronous), which can be stale/missing on a fresh device or
// after a cache clear. After render, look up the ACTUAL published mirror events
// (one batched query for the whole list), reconcile the local flag to match, and
// correct any button labels. Fire-and-forget — never blocks the render.
async refreshMirrorStates(videos) {
if (!FEATURE_NIP71_MIRROR || !this.pubkey) {
return;
}
if (typeof nip71MirrorService.findMirrors !== "function") {
return;
}
let states;
try {
states = await nip71MirrorService.findMirrors(videos);
} catch (error) {
// Detection is best-effort; the local flag remains as a hint.
return;
}
if (!(states instanceof Map) || !(this.listEl instanceof HTMLElement)) {
return;
}
for (const video of Array.isArray(videos) ? videos : []) {
const root = video?.videoRootId ? String(video.videoRootId) : "";
if (!root) {
continue;
}
const mirrored = states.get(root)?.mirrored === true;
// Reconcile the local flag so it's a correct cache going forward.
try {
setMirrorEnabled(this.pubkey, root, mirrored);
} catch (error) {
// ignore storage errors
}
const safeRoot =
typeof CSS !== "undefined" && typeof CSS.escape === "function"
? CSS.escape(root)
: root.replace(/["\\]/g, "\\$&");
const btn = this.listEl.querySelector(
`[data-mirror-root="${safeRoot}"]`,
);
if (btn instanceof HTMLElement && !btn.disabled) {
btn.textContent = mirrored ? "Shared ✓" : "Share to apps";
}
}
}
async handleToggleMirror(video, btn) {