aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/messages.js23
-rw-r--r--src/public-chat.js11
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);