Feed cards: rebuild when the author's profile arrives (so the Tip button shows)

Adam reported the ⚡ Tip button was missing from cards in the
template browser even though the author had a Lightning address.

Root cause: diffWallCards reuses an existing card by event id and
never rebuilds it. The author's profile is fetched AFTER the
template events in fetchFeedAndReactions (the enrichment step), so
the first card render runs when _feedCache.profiles[author] is
still undefined. makeFeedCardTipButton returns null in that case
(no lud16 → no tip button). When the profile arrived later,
notifyFeedProgress kicked off a rerender, but diffWallCards reused
the tip-less DOM card unchanged, and the button never appeared.

Fix
  - buildFeedCardForRow now stamps each card with a small string
    encoding the row's mutable view-state: profile-has-LN flag,
    reaction count, zap-total bucket, premium decrypted/locked
    state, broken-preview flag.
  - diffWallCards reads the stamp on the existing card and the
    fresh stamp on the new row. If they differ, it rebuilds the
    card and replaceWith() into place. Cards built through paths
    that don't set a stamp (legacy code paths) are reused as
    before — backwards-compatible.

Same mechanism naturally handles other "card refreshes when…"
cases that previously didn't: when zap receipts stream in (zap
bucket changes), when a reaction count ticks up (rx changes),
when a premium row gets unlocked (pd/pg flip), when a previously-
broken image gets retried and works (b flips).

Tested via CDP: card built with empty profiles cache → no tip
button, stamp ln=0. Profile pushed into the cache with lud16 →
rerender → stamp ln=1, card rebuilt, "⚡ Tip" button now present.

The same root cause was theoretically present in the preview pane
and profile page, but in practice those open AFTER the user has
been on the feed for a few seconds (profiles already loaded), so
this card-side fix is the user-visible one. If the preview pane
ever lands first, the openDesignPreview path already reads the
latest _feedCache.profiles every time, so it picks up the LN
address from the next render.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-29 09:53:52 -04:00
1 parent 34d5918002
commit 1ea611b44e
1 file changed
+31
+31
View File
@@ -21117,6 +21117,24 @@
}
// Card factory used by both the initial render and the streaming-update diff.
// Encode the bits of a row that affect what gets drawn into the card,
// so diffWallCards can detect when an existing card needs to be
// rebuilt rather than reused. Most importantly: the author's profile
// (and therefore the Tip button's presence) arrives async AFTER the
// initial card render — without this stamp, the card stays in its
// tip-less state forever.
function _feedCardStateStamp(row) {
const pk = row?.e?.pubkey || '';
const profile = _feedCache.profiles?.[pk] || null;
const hasLn = !!(profile?.lud16 || profile?.lud06);
const reactions = row?.reactions || 0;
const zapBucket = Math.floor((zapTotalForEvent(row?.e?.id || '') || 0) / 1000);
const premiumDecrypted = !!(row?.premium && row.premium.decrypted);
const premiumGated = !!(row?.zapGate && row.zapGate.gated && !row.zapGate.unlocked);
const broken = isPreviewBroken(row) ? 1 : 0;
return `ln=${hasLn ? 1 : 0}|rx=${reactions}|z=${zapBucket}|pd=${premiumDecrypted ? 1 : 0}|pg=${premiumGated ? 1 : 0}|b=${broken}`;
}
function buildFeedCardForRow(row) {
const card = buildFeedCard(row, () => {
if (tryDesignLayerImportRow(row)) return;
@@ -21135,6 +21153,7 @@
$('nostrModal').classList.remove('open');
});
card.dataset.eventId = row.e?.id || '';
card.dataset.cardStamp = _feedCardStateStamp(row);
return card;
}
@@ -21158,6 +21177,18 @@
let card = existing.get(id);
if (card) {
existing.delete(id);
// Rebuild the card if its state stamp changed (e.g. the author's
// profile finally arrived → tip button now applicable). The
// stamp is set by buildFeedCardForRow; absent on cards built
// through a different path → always reuse those, to preserve
// legacy behaviour.
const currentStamp = card.dataset?.cardStamp;
const newStamp = (typeof _feedCardStateStamp === 'function') ? _feedCardStateStamp(row) : currentStamp;
if (currentStamp && newStamp && currentStamp !== newStamp) {
const fresh = buildCardFn(row);
card.replaceWith(fresh);
card = fresh;
}
} else {
card = buildCardFn(row);
}