diff options
| -rw-r--r-- | src/features/messages.js | 23 | ||||
| -rw-r--r-- | src/public-chat.js | 11 |
2 files changed, 30 insertions, 4 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); diff --git a/src/public-chat.js b/src/public-chat.js index 91ded5b..9cce6f5 100644 --- a/src/public-chat.js +++ b/src/public-chat.js @@ -84,6 +84,7 @@ export function mountPublicChat() { const deletes = new Map(); // rumorId -> Set(author) const reactions = new Map(); // rumorId -> Map(author -> emoji) const reactEmoji = new Map(); // shortcode -> url, from reactions' own emoji tags + const reactAt = new Map(); // `${rumorId}|${author}` -> ms of the reaction that stands const controlEntries = []; let folded = null; // foldControl result: banned set + channel renames let foldT = 0; // fold-once-per-burst debounce, same as the app's rooms @@ -157,7 +158,15 @@ export function mountPublicChat() { } } else if (rumor.kind === 7) { const target2 = tag('e')?.[1]; - if (target2) (reactions.get(target2) || reactions.set(target2, new Map()).get(target2)).set(author, rumor.content); + if (target2) { + // newest by the rumor's clock, not last to arrive (see the app) + const key = target2 + '|' + author; + const at = eventMs(rumor) || rumor.created_at * 1000; + if (at >= (reactAt.get(key) || 0)) { + reactAt.set(key, at); + (reactions.get(target2) || reactions.set(target2, new Map()).get(target2)).set(author, rumor.content); + } + } // a :code: reaction names its picture in its own tag; kept by code so // the chip can show it for (const [code, url] of emojiTagMap(rumor.tags)) reactEmoji.set(code, url); |
