mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Linked Designs P2c: private (visibility-gated) linked designs
LINKED_DESIGNS_FEATURE.md §12.1 — private sources carry a NIP-44 / NIP-04 envelope addressed to specific pubkeys. Phase 1 treated them as unparseable junk; Phase 2 detects them, attempts decryption, and either renders the decrypted payload (if the viewer is in the envelope's recipients list) or paints a 🔒 placeholder otherwise. Resolver changes (_fetchPayload): - After the premium branch, detect isEncryptedEnvelope(parsed) and await decryptProjectEnvelope. On success return the decrypted payload like a public source; on any failure (not in recipients, no signing extension, malformed envelope) return { payload: null, private: { state: 'locked' } } so the renderer can paint the locked UI without re-fetching. Renderer changes: - New renderLockedPrivate helper paints a 🔒 + "Private design" + "Not shared with this account" on a dark gray background. No unlock CTA — access to a private design is granted at the source's publish time by adding the viewer to envelope.recipients, there's no buyer-side action to take. - Sync peek hit handles cached.private.state === 'locked'; async path handles entry.private.state === 'locked'. Source authors are always in their own envelope.recipients, so the existing self-decrypt path works for them without special-casing.
This commit is contained in:
1 parent
b592a69c20
commit
421104257e
1 file changed
+56
@@ -6612,6 +6612,39 @@
|
||||
premium: { state: 'locked', envelope, addressCoord },
|
||||
};
|
||||
}
|
||||
// Private (visibility) sources carry a NIP-44/04-wrapped
|
||||
// envelope addressed to specific recipients. The viewer can
|
||||
// see the source iff their pubkey is in envelope.recipients
|
||||
// — the source author is always self-addressed, so they
|
||||
// always succeed. decryptProjectEnvelope throws when the
|
||||
// viewer isn't in the recipients list (or has no signing
|
||||
// extension); we catch and surface as locked.
|
||||
if (typeof isEncryptedEnvelope === 'function' && isEncryptedEnvelope(parsed)) {
|
||||
if (typeof decryptProjectEnvelope === 'function') {
|
||||
try {
|
||||
const decrypted = await decryptProjectEnvelope(parsed);
|
||||
if (decrypted) {
|
||||
return {
|
||||
payload: decrypted,
|
||||
sourceEvent: ev,
|
||||
contentBytes: ev.content.length,
|
||||
private: { state: 'unlocked' },
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Fall through to the locked branch — the most common
|
||||
// path is "not in recipients" but we also land here if
|
||||
// the user isn't signed in or the extension can't
|
||||
// decrypt for any other reason.
|
||||
}
|
||||
}
|
||||
return {
|
||||
payload: null,
|
||||
sourceEvent: ev,
|
||||
contentBytes: ev.content.length,
|
||||
private: { state: 'locked' },
|
||||
};
|
||||
}
|
||||
return { payload: parsed, sourceEvent: ev, contentBytes: ev.content.length };
|
||||
}
|
||||
|
||||
@@ -11058,6 +11091,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Locked-private paint helper. No unlock CTA — access to a
|
||||
// private design is granted at the source's publish time
|
||||
// (the author adds the viewer to envelope.recipients), not
|
||||
// via a buyer-side action. The host just shows that the
|
||||
// layer exists but isn't readable from this account.
|
||||
const renderLockedPrivate = () => {
|
||||
fitWrap.innerHTML = '';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1f2937;color:#e5e7eb;padding:8px;text-align:center;gap:4px;font-size:.74rem;line-height:1.3';
|
||||
wrap.innerHTML = '<div style="font-size:1.4em">🔒</div><div>Private design</div><div style="font-size:.66rem;opacity:.7">Not shared with this account</div>';
|
||||
fitWrap.appendChild(wrap);
|
||||
};
|
||||
|
||||
// Locked-premium paint helper (LINKED_DESIGNS_FEATURE.md §12.1).
|
||||
// Shows the watermarked preview JPEG from the envelope + a
|
||||
// semi-transparent lock overlay + an inline unlock CTA. After
|
||||
@@ -11134,6 +11180,11 @@
|
||||
renderLockedPremium(cached);
|
||||
return;
|
||||
}
|
||||
if (cached?.private?.state === 'locked') {
|
||||
layer.design.lastResolvedAt = cached.fetchedAt;
|
||||
renderLockedPrivate();
|
||||
return;
|
||||
}
|
||||
|
||||
// No cache hit — show a placeholder (or the persisted fallback
|
||||
// preview if the user opted in to one) and kick off the async
|
||||
@@ -11155,6 +11206,11 @@
|
||||
renderLockedPremium(entry);
|
||||
return;
|
||||
}
|
||||
if (entry?.private?.state === 'locked') {
|
||||
layer.design.lastResolvedAt = entry.fetchedAt;
|
||||
renderLockedPrivate();
|
||||
return;
|
||||
}
|
||||
if (!entry?.payload) {
|
||||
// Source missing on relays. Keep the fallback preview if
|
||||
// one was already drawn; otherwise show an inline note.
|
||||
|
||||
Reference in new issue
Block a user