diff options
| author | Adam Soltys <[email protected]> | 2026-09-15 22:39:05 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-15 22:39:05 -0700 |
| commit | 3d706acd1edccb5c4eb0f781b52b3d302bdafa2b (patch) | |
| tree | 3f43b2daf254b87a8f8dec58eeb2481f8b4678d7 /src/features | |
| parent | 12e069fc62bff8715de189aedc777b69d1e56377 (diff) | |
| download | coinosv3-3d706acd1edccb5c4eb0f781b52b3d302bdafa2b.tar.gz coinosv3-3d706acd1edccb5c4eb0f781b52b3d302bdafa2b.zip | |
A changed reaction stays changed: the fold keeps the newest, not the last to arrive.
One reaction per author was folded as "last one in wins". A reload's
backfill hands reactions back in relay order, so a flame swapped for
confetti came back as the flame. Rooms, DMs and the public page now keep
the newest by the rumor's own clock, and an optimistic send stamps its
time so an older one arriving afterwards stays out.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JuwQqNWtDJcxPWtEm9134z
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 23 |
1 files changed, 20 insertions, 3 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index e5646e9..0a87e15 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -479,6 +479,7 @@ export function messagesFeature(ctx) { // reaction that arrives before its message still lands. Ephemeral like the // community ones: the wrap backlog rebuilds them on reload. const dmReacts = new Map(); // rumorId -> Map(authorPk -> emoji) + const dmReactAt = new Map(); // `${rumorId}|${authorPk}` -> ms of the reaction that stands const pendingDirect = new Map(); // rumor id -> { bundle, from } const profiles = new Map(); // pubkey -> profile | null while loading const seenWraps = new Set(); @@ -920,6 +921,7 @@ export function messagesFeature(ctx) { edits: new Map(), deletes: new Set(), reactions: new Map(), + reactAt: new Map(), // `${target}|${author}` -> ms of the reaction that stands presence: new Map(), // pubkey -> ms of their last beat (this session only) typing: new Map(), // pubkey -> { ch, t } subbed: new Set(), @@ -1153,8 +1155,16 @@ export function messagesFeature(ctx) { } else if (rumor.kind === 7) { const target = tag('e')?.[1]; if (target) { - const r = room.reactions.get(target) || room.reactions.set(target, new Map()).get(target); - r.set(author, rumor.content); + // One reaction per author, the NEWEST by the rumor's own clock — not + // the last to arrive. A reload's backfill hands them back in relay + // order, and the flame swapped for confetti came back as the flame. + const key = target + '|' + author; + const at = eventMs(rumor) || rumor.created_at * 1000; + if (at >= (room.reactAt.get(key) || 0)) { + room.reactAt.set(key, at); + const r = room.reactions.get(target) || room.reactions.set(target, new Map()).get(target); + r.set(author, rumor.content); + } } } const tms = eventMs(rumor); @@ -1378,6 +1388,7 @@ export function messagesFeature(ctx) { const r = room.reactions.get(m.rumor.id) || room.reactions.set(m.rumor.id, new Map()).get(m.rumor.id); const prev = r.get(id.pubkey); r.set(id.pubkey, emoji); + room.reactAt.set(m.rumor.id + '|' + id.pubkey, eventMs(rumor)); // older ones arriving later stay out render(); try { const wrap = await wrapRumor(rumor, id.signer, room.chStream(chId)); @@ -2427,7 +2438,12 @@ export function messagesFeature(ctx) { const target = got.rumor.tags?.find((x) => x[0] === 'e')?.[1]; noteEmoji(got.rumor); if (target) { - (dmReacts.get(target) || dmReacts.set(target, new Map()).get(target)).set(got.author, got.rumor.content); + const key = target + '|' + got.author; + const at = got.rumor.created_at * 1000; + if (at >= (dmReactAt.get(key) || 0)) { + dmReactAt.set(key, at); + (dmReacts.get(target) || dmReacts.set(target, new Map()).get(target)).set(got.author, got.rumor.content); + } scheduleRepaint(); } } else if (got.rumor.kind === 3313 && !isMe(got.author)) { @@ -2629,6 +2645,7 @@ export function messagesFeature(ctx) { const r = dmReacts.get(m.rumor.id) || dmReacts.set(m.rumor.id, new Map()).get(m.rumor.id); const prev = r.get(id.pubkey); r.set(id.pubkey, emoji); + dmReactAt.set(m.rumor.id + '|' + id.pubkey, rumor.created_at * 1000); render(); try { const toPeer = await wrapDM(id.signer, peer, rumor); |
