eleven / messenger Public

web: stamp reactions with myUserId and pass identity ctx to folds — verified in-browser (#523)

Two more breaks in the encrypted-reactions path (#393), found by driving the
app in Chromium against a local build:

1. The react bridge stamped uid: session.uid — a field that is never assigned
   anywhere (identity lives in the module-level myUserId, roster-matched by
   pubkey; every other outbound payload stamps that). The reaction event went
   out uid-less, and the chat.reaction fold's guard drops uid-less events, so
   every client silently discarded every reaction. Stamp myUserId, and skip
   the send when it's empty (a uid-less reaction can't render or be
   un-reacted). Same fix for the SW's myUid in IndexedDB (sw.js uses it to
   show reaction pushes only to the reacted-to author) — it read session.uid
   too, so it was never stored.

2. makeReducer called fold handlers with (state, event, meta) — but the
   chat.reaction handler (and the event-vectors spec, whose web test
   reimplements the fold rather than exercising this path) expects a 4th ctx
   arg carrying ctx.me, from which the reaction's mine flag derives. ctx was
   always undefined, so after a reload no reaction was yours: the chip lost
   its 'mine' highlight and tapping it re-reacted instead of un-reacting
   (masked live by the optimistic myReactions set). makeReducer now passes a
   minimal identity ctx { me, meName } — identity only, reducers stay pure.

Verified end-to-end in Chromium (Playwright) against a local build: react →
chip renders live; reload → chip persists and is marked mine; tap again →
un-reacts and the chip disappears; no console errors.
Co-authored-by: Claude <noreply@anthropic.com>
James Adam b5b4be90ceea4b2e14e7f040fd3102338a68d662 parent c0dee9f
2 files changed, +10 −3
  • web/static/apps.js +4 −1
  • web/static/shell.js +6 −2
diff --git a/web/static/apps.js b/web/static/apps.js
index 14de905..7a9b1e8 100644
--- a/web/static/apps.js
+++ b/web/static/apps.js
@@ -228,7 +228,10 @@ function makeReducer(events, initialState) {
return (state, event, meta) => {
const s = state === undefined ? seed() : state;
const handler = events[event.type];
- return handler ? handler(s, event, meta || {}) : s;
+ // The 4th arg is a minimal identity ctx: some folds derive per-user state
+ // (chat.reaction's mine flag is uid === ctx.me). Only identity — a reducer
+ // must stay pure, so no emit/schedule/getState here.
+ return handler ? handler(s, event, meta || {}, { me: currentUser, meName: currentUserName }) : s;
};
}
diff --git a/web/static/shell.js b/web/static/shell.js
index 4f261f6..6ac8168 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -1410,7 +1410,7 @@ function syncThreadKeysToSW() {
idbSet("threadKeys", map).catch(() => {});
// The SW also needs to know who "me" is: reaction pushes are shown only to
// the reacted-to message's author (see sw.js), decided after decryption.
- if (session && session.uid) idbSet("myUid", session.uid).catch(() => {});
+ if (myUserId) idbSet("myUid", myUserId).catch(() => {});
}
// The frontend build this page loaded with (server-injected into the HTML).
@@ -3111,9 +3111,13 @@ setChatAppHost({
emoji,
on,
name: session.name || "",
- uid: session.uid,
+ uid: myUserId,
ts: Date.now(),
};
+ // The fold (chat.js "chat.reaction") keys reactions by (emoji, uid) and
+ // drops uid-less events — without an identity the reaction can't render
+ // or be un-reacted, so don't send one.
+ if (!myUserId) return false;
// The reacted-to message's author, riding inside the ciphertext: push
// handlers (sw.js, the iOS NSE) use it to show the reaction only to the
// author — the server never learns whose message it was. Absent for