From 697dc976d05d39481d1bdd1bb4793e97ebb35f0b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 30 May 2026 23:58:17 -0400 Subject: [PATCH] Premium: self-author auto-unlock + paginated preview in preview pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes to the premium-deck experience the user just flagged: 1) Creator should never have to zap themselves to open their own premium design. New autoUnlockSelfAuthoredPremium walks the feed's gated events, picks the ones whose pubkey matches state.community.myPubkey, runs Premium.decryptPayload locally (the soft-gate KDF is deterministic — public material + the static client pepper — so the author needs no receipt), and stores the unlocked entry in the cache with source: 'self-authored'. Runs in parallel with the existing receipt fast-path; they target different events. 2) The preview pane's locked-premium branch was still rendering only the single watermarked JPEG. The earlier multi-slide work only updated the mini-preview / card path — the actual pane shown when you click into a row from the community browser kept the old single-image render. Now it detects row.premium.previewSlides.length > 1 and emits a Prev / Next pager with a "Slide N of M · K in deck" counter, identical UX to the mini-preview viewer. Single-image case unchanged. --- src/index.html | 89 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/index.html b/src/index.html index 62f3153..e4069c8 100644 --- a/src/index.html +++ b/src/index.html @@ -5780,6 +5780,43 @@ // sent for gated templates. Matching receipts auto-unlock without // re-payment, so a cache wipe / private window / new device feels // seamless. + + // Premium events authored by the current user should appear unlocked + // automatically — a creator should never have to zap themselves to + // open their own design. The soft-gate KDF is deterministic from + // public material + the static client pepper, so we can decrypt + // locally without involving relays / receipts. + async function autoUnlockSelfAuthoredPremium(gatedEvents) { + if (!Array.isArray(gatedEvents) || !gatedEvents.length) return; + const myPubkey = normalizePubkey(state.community?.myPubkey || ''); + if (!myPubkey) return; + for (const ev of gatedEvents) { + if (normalizePubkey(ev.pubkey) !== myPubkey) continue; + if (!isPremiumEvent(ev)) continue; + if (isUnlockedLocally(ev.id)) continue; + let envelope = null; + let decryptedPayload = null; + try { + envelope = JSON.parse(ev.content || '{}'); + decryptedPayload = await Premium.decryptPayload(envelope, { + creatorPubkey: ev.pubkey, + dTag: premiumEventDTag(ev), + schemaVersion: envelope?.payloadSchemaVersion || 5, + }); + } catch (err) { + console.warn('[premium] self-author auto-decrypt failed for', ev.id, err); + continue; + } + const addressCoord = premiumAddressCoord(ev); + markUnlocked(ev.id, { + source: 'self-authored', + address: addressCoord, + payload: decryptedPayload, + previewJpegDataUrl: envelope?.preview?.dataUrl || undefined, + }); + } + } + // Relay fast-path. Premium designs use address-based purchase scope // (PREMIUM_DESIGNS_FEATURE.md §13.2) so a creator's edit-in-place // doesn't invalidate prior unlocks. We query by `#a` first, then `#e` @@ -19180,6 +19217,9 @@ // tag-only events fall back to event-id receipts. const gatedEvents = templateEvents.filter((e) => isPremiumEvent(e) || isZapGated(e)); if (gatedEvents.length && state.community?.myPubkey) { + // Self-authored auto-unlock + receipt fast-path run in + // parallel — they target different events (mine vs. paid-for). + autoUnlockSelfAuthoredPremium(gatedEvents).then(() => notifyFeedProgress()).catch(() => {}); relayFastPathUnlockCheck(gatedEvents).then(() => notifyFeedProgress()).catch(() => {}); } } @@ -22311,17 +22351,62 @@ content.style.display = ''; // Locked premium row → render the watermarked JPEG instead of the - // (unavailable) plaintext design. Add an honest "soft-gate" note. + // (unavailable) plaintext design. For deck premium with multiple + // slide previews, paginate so the buyer can flip through. if (row.premium && !row.premium.decrypted) { const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:8px;padding:8px'; - if (row.premium.previewJpegDataUrl) { + + const previewSlides = Array.isArray(row.premium.previewSlides) + ? row.premium.previewSlides + : null; + + if (previewSlides && previewSlides.length > 1) { + // Paginated viewer for deck premium previews. + const img = document.createElement('img'); + img.alt = row.title || 'Premium deck preview'; + img.style.cssText = 'max-width:100%;height:auto;border:1px solid var(--line);background:#fff'; + const bar = document.createElement('div'); + bar.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:10px;font-size:.9rem;color:var(--ink);padding:4px 0'; + const prev = document.createElement('button'); + prev.type = 'button'; + prev.textContent = '‹ Prev'; + prev.style.cssText = 'padding:4px 12px;cursor:pointer'; + const counter = document.createElement('span'); + counter.style.cssText = 'min-width:11em;text-align:center;font-variant-numeric:tabular-nums'; + const next = document.createElement('button'); + next.type = 'button'; + next.textContent = 'Next ›'; + next.style.cssText = 'padding:4px 12px;cursor:pointer'; + bar.append(prev, counter, next); + wrap.append(img, bar); + const totalDeckSlides = row.premium.previewTotalSlides || previewSlides.length; + let idx = 0; + function render() { + const slide = previewSlides[idx]; + img.src = slide?.dataUrl || ''; + const shown = previewSlides.length; + counter.textContent = totalDeckSlides > shown + ? `Slide ${idx + 1} of ${shown} · ${totalDeckSlides} in deck` + : `Slide ${idx + 1} of ${shown}`; + } + prev.addEventListener('click', () => { + idx = (idx - 1 + previewSlides.length) % previewSlides.length; + render(); + }); + next.addEventListener('click', () => { + idx = (idx + 1) % previewSlides.length; + render(); + }); + render(); + } else if (row.premium.previewJpegDataUrl) { const img = document.createElement('img'); img.src = row.premium.previewJpegDataUrl; img.alt = row.title || 'Premium template preview'; img.style.cssText = 'max-width:100%;height:auto;border:1px solid var(--line);background:#fff'; wrap.appendChild(img); } + const note = document.createElement('p'); note.className = 'hint'; note.style.cssText = 'margin:0;text-align:center;max-width:520px';