mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-08 14:40:08 +00:00
Premium: fix three issues from Adam's testing
1) Premium designs missing from the "New" tab
isPreviewBroken was flagging every premium cover / disc / jewel /
disc-design row as broken: those modes are in MODES_REQUIRE_IMAGE,
the stub payload has no slot/disc art, so row.preview was '' →
"broken". The New tab's sort puts broken rows last, so premium
rows sank past the 24-card pagination cutoff and disappeared.
Premium rows carry their own watermarked-JPEG preview inside the
envelope and always show it on the card, so they should never
count as "broken." isPreviewBroken now short-circuits on
row.premium and returns false unconditionally.
2) Upvote button felt unresponsive
The reaction handler awaited publishToRelays before changing the
button text — on slow relays that's a few seconds of no visible
feedback, so the click looked like it did nothing. Now it
optimistically swaps to "⋯" + disables the button as soon as you
click, then either commits to "▲ N" on success or rolls back to
the original text on failure. No more "did it work?"
3) My Designs → Purchased tab loaded inconsistently
Previously force-refetched the vault from relays every time the
tab opened, blocking the render on the slowest relay in the
pool. If the fetch failed, the whole wall went to an error
placeholder. Now:
- If we have an in-memory _vaultCache from the current
session, render those cards IMMEDIATELY (sync, before the
await), then re-fetch in the background and re-render with
the latest. Feels instant in the common case where the user
just unlocked something.
- Top-of-list refresh button so the user can force a relay
re-query on demand (useful after unlocking on another
device).
- Items missing their per-item event (the split-vault
_missing flag) render at .55 opacity with a click-to-toast
explanation instead of silently failing.
- On refresh failure, keep showing the cached cards — don't
blow them away just because relays timed out — and surface
a toastError with the actual reason.
Tested via CDP:
- Premium cover row → isPreviewBroken = false ✓
- Reaction button: click → text becomes "⋯", disabled = true.
On simulated failure response → rolls back to "▲", re-enabled.
- Existing vault round-trip + hydrate + Purchased-tab regression
still passes.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
b6b189848a
commit
e6588017ed
1 file changed
+80
-18
+80
-18
@@ -6117,30 +6117,76 @@
|
||||
return row;
|
||||
}
|
||||
|
||||
async function loadPurchasedDesignsIntoWall(wall) {
|
||||
function _renderVaultItemsIntoWall(wall, items, hadError = false, opts = {}) {
|
||||
wall.innerHTML = '';
|
||||
// Top toolbar: count + refresh button. Refresh forces a relay re-
|
||||
// query in case the user just bought something on another device.
|
||||
const topBar = document.createElement('div');
|
||||
topBar.style.cssText = 'grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px;font-size:.78rem;color:var(--muted)';
|
||||
const count = document.createElement('span');
|
||||
count.textContent = items.length
|
||||
? `${items.length} unlocked design${items.length === 1 ? '' : 's'}`
|
||||
: (hadError ? 'Vault load failed' : 'No premium designs unlocked yet');
|
||||
const refreshBtn = document.createElement('button');
|
||||
refreshBtn.type = 'button';
|
||||
refreshBtn.textContent = '↻ Refresh';
|
||||
refreshBtn.title = 'Re-query relays for the latest vault';
|
||||
refreshBtn.addEventListener('click', () => loadPurchasedDesignsIntoWall(wall, { force: true }));
|
||||
topBar.append(count, refreshBtn);
|
||||
wall.appendChild(topBar);
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'hint';
|
||||
empty.style.cssText = 'grid-column:1/-1';
|
||||
empty.textContent = hadError
|
||||
? 'Couldn’t load your purchase vault from relays. Try refresh.'
|
||||
: 'Visit the feed and zap a premium template to start your collection.';
|
||||
wall.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const row = _rowFromVaultItem(item);
|
||||
const card = buildFeedCard(row, () => {
|
||||
if (item._missing) {
|
||||
toastError('This vault entry is missing its per-item event on relays. Try Refresh, or re-unlock from the feed.');
|
||||
return;
|
||||
}
|
||||
try { loadProjectFromText(JSON.stringify(row.payload)); }
|
||||
catch (err) { toastError('Could not open: ' + (err?.message || err)); return; }
|
||||
$('nostrModal').classList.remove('open');
|
||||
});
|
||||
if (item._missing) card.style.opacity = '0.55';
|
||||
wall.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPurchasedDesignsIntoWall(wall, opts = {}) {
|
||||
if (!_supportsVault()) {
|
||||
wall.innerHTML = '<div class="hint" style="grid-column:1/-1">Your Nostr extension doesn’t support encryption (NIP-44 / NIP-04), so the purchase vault is unavailable.</div>';
|
||||
return;
|
||||
}
|
||||
wall.innerHTML = '<div class="hint" style="grid-column:1/-1">Loading purchased designs…</div>';
|
||||
// Render the in-memory cache immediately if we have one — feels
|
||||
// instant. Then re-fetch in the background and re-render with the
|
||||
// latest from relays. If we have no cache, show a placeholder while
|
||||
// the first fetch happens.
|
||||
const cached = _vaultCache;
|
||||
if (cached?.items?.length) {
|
||||
_renderVaultItemsIntoWall(wall, cached.items, false);
|
||||
} else {
|
||||
wall.innerHTML = '<div class="hint" style="grid-column:1/-1">Loading purchased designs…</div>';
|
||||
}
|
||||
try {
|
||||
const vault = await loadPurchaseVault({ force: true });
|
||||
const items = vault?.items || [];
|
||||
wall.innerHTML = '';
|
||||
if (!items.length) {
|
||||
wall.innerHTML = '<div class="hint" style="grid-column:1/-1">No premium designs unlocked yet. Visit the feed and zap a premium template to start your collection.</div>';
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const row = _rowFromVaultItem(item);
|
||||
wall.appendChild(buildFeedCard(row, () => {
|
||||
try { loadProjectFromText(JSON.stringify(row.payload)); }
|
||||
catch (err) { toastError('Could not open: ' + (err?.message || err)); return; }
|
||||
$('nostrModal').classList.remove('open');
|
||||
}));
|
||||
}
|
||||
const vault = await loadPurchaseVault({ force: opts.force !== false });
|
||||
_renderVaultItemsIntoWall(wall, vault?.items || [], false);
|
||||
} catch (err) {
|
||||
wall.innerHTML = `<div class="hint" style="grid-column:1/-1">Failed to load purchase vault: ${escapeHtml(err?.message || String(err))}</div>`;
|
||||
console.warn('[purchased tab] load failed', err);
|
||||
// Keep whatever we had cached on the wall; only fall back to the
|
||||
// error placeholder if we never had any cache.
|
||||
if (!cached?.items?.length) {
|
||||
_renderVaultItemsIntoWall(wall, [], true);
|
||||
} else {
|
||||
if (typeof toastError === 'function') toastError('Vault refresh failed: ' + (err?.message || err));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---- end Purchase vault ------------------------------------------------
|
||||
@@ -18375,6 +18421,13 @@
|
||||
|
||||
function isPreviewBroken(row) {
|
||||
if (!row) return false;
|
||||
// Premium rows carry their own watermarked-JPEG preview embedded in
|
||||
// the envelope (or the cached decrypted payload's mini-preview).
|
||||
// They never count as "broken" — without this exception, premium
|
||||
// cover/disc/jewel rows (whose synthesised stub has no slot art so
|
||||
// row.preview is '') would always sort to the back of the New tab
|
||||
// and get hidden past the chunk-size cutoff.
|
||||
if (row.premium) return false;
|
||||
const preview = row.preview;
|
||||
// No URL at all + this mode expects one → effectively a missing image.
|
||||
if (!preview && MODES_REQUIRE_IMAGE.has(row.mode)) return true;
|
||||
@@ -21061,11 +21114,20 @@
|
||||
btn.title = 'React to this template on Nostr';
|
||||
btn.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
if (btn.disabled || btn.classList.contains('reacted')) return;
|
||||
// Optimistic update so the button feels responsive even when the
|
||||
// relay publish takes a few seconds. We roll back on failure.
|
||||
const prevText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⋯';
|
||||
const ok = await reactToNostrEvent(row.e.id, row.e.pubkey);
|
||||
btn.disabled = false;
|
||||
if (ok) {
|
||||
row.reactions = _feedCache.reactions[row.e.id] || 1;
|
||||
btn.textContent = `▲ ${row.reactions}`;
|
||||
btn.classList.add('reacted');
|
||||
} else {
|
||||
btn.textContent = prevText;
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
|
||||
Reference in new issue
Block a user