@micha.bigler/ui-core-micha 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORK_ORDERS.md +3 -1
- package/dev/entries.jsx +5 -1
- package/dist/i18n/messagingTranslations.js +27 -0
- package/dist/index.js +1 -0
- package/dist/messaging/Composer.js +41 -5
- package/dist/messaging/ConversationList.js +10 -3
- package/dist/messaging/DirectMessageLauncher.js +48 -0
- package/dist/messaging/MessageBubble.js +84 -4
- package/dist/messaging/MessagingProvider.js +102 -21
- package/dist/messaging/PollCard.js +22 -5
- package/dist/messaging/ReactionBar.js +1 -1
- package/dist/messaging/ReadTicks.js +7 -4
- package/dist/messaging/Thread.js +45 -8
- package/dist/messaging/api.js +3 -3
- package/dist/messaging/composerImageCompression.js +36 -0
- package/docs/messaging-deviations.md +108 -26
- package/package.json +1 -1
- package/src/i18n/messagingTranslations.ts +27 -0
- package/src/index.js +1 -0
- package/src/messaging/Composer.jsx +54 -8
- package/src/messaging/ConversationList.jsx +10 -4
- package/src/messaging/DirectMessageLauncher.jsx +96 -0
- package/src/messaging/MessageBubble.jsx +74 -7
- package/src/messaging/MessagingProvider.jsx +124 -15
- package/src/messaging/PollCard.jsx +19 -3
- package/src/messaging/ReactionBar.jsx +1 -1
- package/src/messaging/ReadTicks.jsx +5 -3
- package/src/messaging/Thread.jsx +39 -8
- package/src/messaging/api.js +3 -2
- package/src/messaging/composerImageCompression.js +35 -0
- package/tests/ConversationList.test.jsx +1 -1
- package/tests/Thread.test.jsx +1 -1
- package/tests/messagingChunk6.test.jsx +109 -0
- package/tests/messagingComposerChunk5.test.jsx +110 -0
- package/tests/messagingContractConformance.test.js +69 -0
- package/tests/messagingDirectMessageLauncher.test.jsx +78 -0
- package/tests/messagingExports.test.js +2 -1
- package/tests/messagingInteractions.test.jsx +3 -1
- package/tests/messagingLastMessagePreview.test.jsx +93 -0
- package/tests/messagingMessageActions.test.jsx +93 -0
- package/tests/messagingPollRendering.test.jsx +115 -0
- package/tests/messagingReactionMerge.test.jsx +79 -0
- package/tests/messagingThreadingQuoting.test.jsx +86 -0
- package/tests/messagingUnreadLifecycle.test.jsx +63 -0
- package/work-orders/MSG-3b.md +1002 -0
- package/work-orders/MSG-3c.md +378 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# MSG-3c — deliver the rows MSG-3b left blocked against a stale dcm version
|
|
2
|
+
|
|
3
|
+
Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
|
|
4
|
+
**Binding spec:** `django-core-micha/docs/design/messaging-platform.md`. Extends the landed MSG-3b
|
|
5
|
+
(published ucm 2.17.0), same convention as `MSG-2b` / `MSG-2c` / `NOTIF-8b`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Part A — Envelope (Expertenchat, 2026-07-31)
|
|
10
|
+
|
|
11
|
+
### Goal
|
|
12
|
+
|
|
13
|
+
Deliver the seven checklist rows MSG-3b correctly refused to fake — and which became buildable while
|
|
14
|
+
MSG-3b was still running, without the session ever learning.
|
|
15
|
+
|
|
16
|
+
### Why this WO exists
|
|
17
|
+
|
|
18
|
+
MSG-3b started against dcm **2.36.1** and pinned that version explicitly in every BLOCKED entry of
|
|
19
|
+
`docs/messaging-deviations.md`. dcm **2.37.0** (MSG-2c) landed mid-flight and delivered exactly what
|
|
20
|
+
those entries said was missing. The WO closed — six chunks, per-chunk reviews, a clean WO-end
|
|
21
|
+
`ui_reviewer` row-by-row cross-check — with seven rows recorded as blocked on "a future dcm work order"
|
|
22
|
+
that had in fact already shipped.
|
|
23
|
+
|
|
24
|
+
The WO-end review did not catch it because it cross-checked the checklist against **ucm's** landed code
|
|
25
|
+
and took the dependency's state from the WO text. That is the third occurrence of one pattern in this
|
|
26
|
+
workstream: a side verified against itself, with the seam assumed rather than read. See §5 of
|
|
27
|
+
`notifications-messaging-roadmap.md`.
|
|
28
|
+
|
|
29
|
+
**Credit where it is due, and the reason this was cheap to find:** MSG-3b wrote the dcm version into
|
|
30
|
+
every BLOCKED entry. Without that pin the staleness would have been invisible instead of a two-minute
|
|
31
|
+
check. Keep doing it — this WO makes it a rule.
|
|
32
|
+
|
|
33
|
+
### Verified as deliverable against dcm 2.37.0
|
|
34
|
+
|
|
35
|
+
Checked in `django-core-micha` at 2.37.0 before this WO was written:
|
|
36
|
+
|
|
37
|
+
| Row | What MSG-3b recorded | Actual state in 2.37.0 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 38 | "dcm does not serialize `last_message`" | `serialize_last_message` exists; `last_message` is on the conversation payload |
|
|
40
|
+
| 51 | "no poll read serialization" | `serialize_poll` exists, embedded in `serialize_message` for `kind == "poll"` |
|
|
41
|
+
| 52 | "per-option voter names absent" | `voters` is in the poll projection (user ids, every conversation kind) |
|
|
42
|
+
| 53 | "closed state depends on the absent poll contract" | `closed_at` is in the projection |
|
|
43
|
+
| 56 | "dcm 2.36.1 does not emit `reaction`" | emitted (MSG-2c wired 8 frames) |
|
|
44
|
+
| 57 | "dcm 2.36.1 does not emit `poll_updated`" | emitted |
|
|
45
|
+
| 58 | "reorder/preview need the missing `last_message`" | available per row 38 |
|
|
46
|
+
|
|
47
|
+
### Expected outcome
|
|
48
|
+
|
|
49
|
+
The seven rows above delivered per the MSG-3b checklist's original descriptions:
|
|
50
|
+
|
|
51
|
+
- **38 / 58** — real last-message preview in `ConversationList`, and list reorder plus preview refresh
|
|
52
|
+
on new / edited / deleted messages.
|
|
53
|
+
- **51-53** — `PollCard` renders question, options, per-option `vote_count` and `voters`, closed state
|
|
54
|
+
with voting disabled. Read `voted_option_ids` **only** from the three poll REST responses; it is
|
|
55
|
+
deliberately absent from `serialize_poll` and from every realtime frame, so do not look for it there
|
|
56
|
+
and do not reconstruct it from a frame.
|
|
57
|
+
- **56 / 57** — the existing `reaction` and `poll_updated` handlers stop being dead code; verify they
|
|
58
|
+
merge correctly now that frames actually arrive, rather than assuming the untested paths are right.
|
|
59
|
+
- `docs/messaging-deviations.md` corrected: these seven entries move from BLOCKED to delivered, and the
|
|
60
|
+
closing paragraph stops claiming they need a future dcm WO.
|
|
61
|
+
|
|
62
|
+
**Still legitimately BLOCKED, do not touch:** rows **27** (no readable thread reply state) and **42**
|
|
63
|
+
(`external_key` not serialized). Both are real dcm gaps that MSG-2c did not cover; both are addressed by
|
|
64
|
+
dcm `MSG-2d`, and a later ucm WO delivers them. MSG-3b assessed these two correctly.
|
|
65
|
+
|
|
66
|
+
### The rule this WO adds
|
|
67
|
+
|
|
68
|
+
**A BLOCKED row is re-verified against the live dependency at WO close, not at WO start.** Concretely:
|
|
69
|
+
|
|
70
|
+
1. Every BLOCKED entry in `docs/messaging-deviations.md` must name the dependency version it was
|
|
71
|
+
blocked at — MSG-3b already did this; make it explicit policy.
|
|
72
|
+
2. Extend the contract-conformance test from MSG-3b chunk 1: it must fail when a BLOCKED entry carries
|
|
73
|
+
no version pin. A test cannot check whether dcm has since shipped — it has no access to it — but it
|
|
74
|
+
can guarantee the pin exists, which is what makes the manual re-check a two-minute job instead of an
|
|
75
|
+
archaeology exercise.
|
|
76
|
+
3. The WO-end review instruction changes: cross-check BLOCKED rows against the **dependency repo at its
|
|
77
|
+
current version**, not against the WO text.
|
|
78
|
+
|
|
79
|
+
### Non-goals / do-not-touch
|
|
80
|
+
|
|
81
|
+
Rows 27 and 42 (blocked on dcm `MSG-2d`). Any dcm change. Any app-side code. The jg migration (MSG-5).
|
|
82
|
+
Search, typing, client→server WS. The decomposition and component boundaries established in MSG-3 and
|
|
83
|
+
held through MSG-3b — new rendering lands in the component that owns it, and the ~400 LOC soft trigger
|
|
84
|
+
and one-harness-entry-per-component rule stay in force. Do not re-open rows already delivered or
|
|
85
|
+
already recorded as deliberate deviations.
|
|
86
|
+
|
|
87
|
+
### Required tests to WRITE
|
|
88
|
+
|
|
89
|
+
- **Poll rendering** against a server-shaped payload — the projection as `serialize_poll` actually emits
|
|
90
|
+
it, not a client-constructed object. This is the MSG-3b lesson repeated: the threading tests passed
|
|
91
|
+
for a year of nobody noticing precisely because they used client-shaped fixtures.
|
|
92
|
+
- **`voted_option_ids` comes only from REST:** a `poll_updated` frame carrying no such key must not
|
|
93
|
+
clear or invert the viewer's own vote state.
|
|
94
|
+
- **`reaction` / `poll_updated` frame merge:** an arriving frame updates the rendered message for a
|
|
95
|
+
viewer who did not cause it; a duplicate `event_id` changes nothing.
|
|
96
|
+
- **Last-message preview and reorder:** a new message moves its conversation to the top and updates the
|
|
97
|
+
preview; an edit updates it; a delete empties it.
|
|
98
|
+
- **Conformance test extension:** a BLOCKED entry without a version pin fails the build — assert the
|
|
99
|
+
negative case, not only the positive.
|
|
100
|
+
- Existing ucm suites stay green.
|
|
101
|
+
|
|
102
|
+
### Risks
|
|
103
|
+
|
|
104
|
+
- **The dead handlers were never exercised.** Rows 56/57 are the paths most likely to contain latent
|
|
105
|
+
bugs, because they have never run against a real frame. Treat them as new code under test, not as
|
|
106
|
+
already-working code being unblocked.
|
|
107
|
+
- **Fixture shape** — see the poll test above; the single most repeated failure in this workstream is a
|
|
108
|
+
test that passes because it invented its own payload.
|
|
109
|
+
- Deviation-doc drift: correcting seven entries by hand while leaving two correct ones intact is exactly
|
|
110
|
+
where an over-eager edit removes a real blocker.
|
|
111
|
+
|
|
112
|
+
### Preconditions
|
|
113
|
+
|
|
114
|
+
dcm 2.37.0 published (met). Approval Gate #1 = operator go on this envelope. Rows 27/42 additionally
|
|
115
|
+
require dcm `MSG-2d`, which is **not** a precondition for this WO.
|
|
116
|
+
|
|
117
|
+
### Release
|
|
118
|
+
|
|
119
|
+
One version bump + npm publish at WO end.
|
|
120
|
+
|
|
121
|
+
### Execution directive
|
|
122
|
+
|
|
123
|
+
Implement through `codex exec` in the background — invoked **directly via Bash** (never the
|
|
124
|
+
`debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check` and
|
|
125
|
+
`--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file;
|
|
126
|
+
fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
|
|
127
|
+
|
|
128
|
+
### Mini-handover (pastable)
|
|
129
|
+
|
|
130
|
+
Orchestrator: implement `work-orders/MSG-3c.md` in `ui-core-micha` (main). `git pull` first, read the WO
|
|
131
|
+
+ `django-core-micha/docs/design/messaging-platform.md`, then follow `orchestrate-codex` (Codex-first,
|
|
132
|
+
own review per chunk, `ui_reviewer` at WO end, one publish at WO end). Rows 27 and 42 stay BLOCKED —
|
|
133
|
+
they wait on dcm `MSG-2d`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Part B — Implementation map (Orchestrator)
|
|
138
|
+
|
|
139
|
+
**Target repo working directory (all chunks):** `C:\Users\biglmi\Documents\webapps\ui-core-micha` (repo
|
|
140
|
+
root). **Chunk plan (Orchestrator's own — the envelope named rows, not chunks):**
|
|
141
|
+
|
|
142
|
+
1. Last-message preview + reorder (rows 38, 58).
|
|
143
|
+
2. Poll rendering (rows 51-53).
|
|
144
|
+
3. Reaction/poll_updated frame-merge fixes (row 56/57 verification) + the conformance-test extension +
|
|
145
|
+
the deviation-doc rewrite for all seven rows.
|
|
146
|
+
|
|
147
|
+
**Verified against dcm 2.37.0 while scoping** (do not re-derive these, they're settled): `serialize_last_message`/
|
|
148
|
+
`serialize_conversation_core` (`django-core-micha/src/django_core_micha/messaging/serializers.py:89-113`),
|
|
149
|
+
`serialize_poll`/`serialize_reactions` (`serializers.py:41-46,52-64`), `_poll_response` — the ONLY place
|
|
150
|
+
`voted_option_ids` is added, view-level, on top of `serialize_poll` (`views.py:32-36`, used by the
|
|
151
|
+
create/vote/close poll responses only — never by `serialize_message`'s embedded poll, never by the
|
|
152
|
+
`poll_updated` frame), and the three `conversation_upsert` publish sites (`services.py:95,120,165` —
|
|
153
|
+
**new-message send only**; `edit_message`/`soft_delete_message` publish `message_edited`/`message_deleted`
|
|
154
|
+
but never `conversation_upsert`, so a background conversation's list preview cannot live-update on an
|
|
155
|
+
edit/delete of its last message without a dcm change — out of scope, verify via REST refresh instead,
|
|
156
|
+
per chunk 1 below, do not invent a frame dcm doesn't send).
|
|
157
|
+
|
|
158
|
+
**Two additional bugs found during this scoping, in files this WO already touches — fix them here rather
|
|
159
|
+
than filing separately:**
|
|
160
|
+
- `MessagingProvider.jsx:144` (`applyFrame`'s `message_edited` case): `mergeById(state.messages, payload)`
|
|
161
|
+
where `payload` (no `.message` sub-key on this frame — it only ever carries `message_id`) falls through
|
|
162
|
+
to the raw `frame` object, and `idOf()` (`MessagingProvider.jsx:45`, `item?.id ?? item?.conversation_id
|
|
163
|
+
?? item?.message_id ?? item?.poll_id`) checks `.conversation_id` **before** `.message_id` — every frame
|
|
164
|
+
envelope carries `conversation_id`, so a `message_edited` frame gets keyed into the cache by the
|
|
165
|
+
conversation's id instead of the message's id, silently creating a bogus cache entry and never touching
|
|
166
|
+
the real message. Zero test coverage anywhere in this repo caught it. Fix in chunk 1 (same file as the
|
|
167
|
+
conversation-preview work): merge with an explicit `id: frame.message_id`, not the `idOf` fallback chain.
|
|
168
|
+
- `MessagingProvider.jsx:315-330` (`toggleReaction`) and the `reaction` frame case (`:145-149`): `serialize_reactions`
|
|
169
|
+
(verified above) returns `[{emoji, count}]` **only, never a per-viewer `reacted` flag, ever** — not in
|
|
170
|
+
REST responses, not in frames, by design (aggregate-only). `toggleReaction`'s optimistic update
|
|
171
|
+
correctly sets `reacted: true/false` locally on the user's own click, but then the REST-confirm step
|
|
172
|
+
(`:327`, `reactions: result?.reactions || ...`) and the frame handler (`:148`, `payload.reactions ||
|
|
173
|
+
frame.reactions || ...`) both prefer the server's `reacted`-less array over the optimistic one — so
|
|
174
|
+
confirming the user's own toggle, or literally any other viewer's reaction frame arriving for the same
|
|
175
|
+
message, silently wipes every emoji's locally-known `reacted` state. This is exactly what row 56 asks
|
|
176
|
+
to be verified rather than assumed. Fix in chunk 3 (bundled with the frame-merge verification, same
|
|
177
|
+
underlying fix in both places): merge the server's `{emoji,count}` list with previously-known `reacted`
|
|
178
|
+
flags by emoji key, never wholesale-replace.
|
|
179
|
+
|
|
180
|
+
### Chunk 1 — last-message preview and conversation reorder (rows 38, 58)
|
|
181
|
+
|
|
182
|
+
**Files:** `src/messaging/MessagingProvider.jsx` (the `message_edited` bug fix above; verify — do not
|
|
183
|
+
rebuild — the existing `conversation_upsert` handling at `:160` and `EMPTY_CACHE`/reducer shape),
|
|
184
|
+
`src/messaging/ConversationList.jsx` (verify — likely no change needed, see below), new tests.
|
|
185
|
+
|
|
186
|
+
**Much of rows 38/58 turns out to already be built.** MSG-3b wrote `applyFrame`'s `conversation_upsert`
|
|
187
|
+
case and `ConversationList`'s `last_message?.body` rendering + `last_message_at` sort defensively,
|
|
188
|
+
against the design doc's frame contract, before dcm actually sent the data. Now that dcm 2.37.0 does
|
|
189
|
+
(`conversation_upsert` on new-message send carries the real `serialize_conversation_core`, including
|
|
190
|
+
`last_message`), this is primarily a **verification + test-writing chunk**, not a rebuild — confirm this
|
|
191
|
+
by reading `ConversationList.jsx:11` (`ordered()`, sorts by `last_message_at`) and `:71`
|
|
192
|
+
(`conversation.last_message?.body`) before writing anything new. If you find either needs a real change
|
|
193
|
+
beyond the `message_edited` fix above, make it — but don't assume a rewrite is needed without checking.
|
|
194
|
+
|
|
195
|
+
**Row 58's "an edit updates it; a delete empties it"** must be tested via **REST refresh**
|
|
196
|
+
(`refreshConversations()`), not a live frame — dcm does not publish `conversation_upsert` on edit/delete
|
|
197
|
+
(see the verified call-site list above). `serialize_last_message` (`serializers.py:89-97`) already
|
|
198
|
+
returns `excerpt: ""` for a deleted last message — that's the server behavior a refresh test should
|
|
199
|
+
assert against, mocked with a server-shaped `listConversations` response, not invented.
|
|
200
|
+
|
|
201
|
+
**Required tests (chunk 1):**
|
|
202
|
+
- A `conversation_upsert` frame with a real `last_message` moves that conversation to the top of
|
|
203
|
+
`ConversationList` and updates its preview text — server-shaped fixture (`last_message: {body, ...}`,
|
|
204
|
+
not a client-invented shape).
|
|
205
|
+
- A REST refresh reflecting an edited last message updates the preview; reflecting a deleted one
|
|
206
|
+
(`last_message.excerpt: ""` — check the actual field name your fixture should use, verify against
|
|
207
|
+
`serialize_last_message`'s real key, which is `excerpt` not `body`, at the `last_message` level —
|
|
208
|
+
don't assume the `Conversation.last_message` shape matches `Message`'s own `body` field name) shows
|
|
209
|
+
the empty/no-preview state.
|
|
210
|
+
- The `message_edited` frame-keying regression: a `message_edited` frame with only `{message_id,
|
|
211
|
+
conversation_id}` (the real minimal shape — verified above, it carries no other fields) must patch the
|
|
212
|
+
message keyed by `message_id`, and must NOT create/touch any cache entry keyed by `conversation_id`.
|
|
213
|
+
- Existing ucm suites stay green.
|
|
214
|
+
|
|
215
|
+
**Progress contract / preamble:** identical to MSG-3b's chunks — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
|
|
216
|
+
git operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's
|
|
217
|
+
review.
|
|
218
|
+
|
|
219
|
+
**Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
|
|
220
|
+
`work-orders/MSG-3c.md` chunk 1 (Part B above). Follow `orchestrate-codex`.
|
|
221
|
+
|
|
222
|
+
**Note on chunk 1's actual execution:** Codex reported `workspace is out of credits` on invocation
|
|
223
|
+
(not a transient rate limit) — the orchestrator implemented chunk 1 directly per the Codex→Claude
|
|
224
|
+
fallback rule, and an independent reviewer ran (mandatory once the orchestrator is the author). Chunks
|
|
225
|
+
2/3 below should still attempt Codex first per the execution directive, but expect the same
|
|
226
|
+
out-of-credits failure and fall back the same way if it recurs — don't loop retrying it.
|
|
227
|
+
|
|
228
|
+
### Chunk 2 — poll rendering (rows 51-53)
|
|
229
|
+
|
|
230
|
+
**Files:** `src/messaging/PollCard.jsx` (all three rows land here), new tests. `PollCard.jsx` was built
|
|
231
|
+
in MSG-3 speculatively, before dcm's poll read contract existed — it guesses at field names, and two of
|
|
232
|
+
those guesses are confirmed wrong against the real server shape.
|
|
233
|
+
|
|
234
|
+
**Verified against dcm 2.37.0 (`django-core-micha/src/django_core_micha/messaging/serializers.py:52-64`,
|
|
235
|
+
`views.py:32-36`):** `serialize_poll` returns `{id, question, allow_multiple, closed_at, created_by_id,
|
|
236
|
+
options: [{id, text, order, vote_count, voters}]}` — `voters` is an array of user ids (row 52).
|
|
237
|
+
`voted_option_ids` is added **only** by `_poll_response` (`views.py:32-36`), a view-level enrichment
|
|
238
|
+
used **only** by the create/vote/close poll REST responses — it is never in `serialize_poll` itself,
|
|
239
|
+
never in `serialize_message`'s embedded poll (message list/thread loads), and never in the
|
|
240
|
+
`poll_updated` realtime frame. This is the WO's explicit "read `voted_option_ids` only from the three
|
|
241
|
+
poll REST responses" instruction, now with the exact citations.
|
|
242
|
+
|
|
243
|
+
**Two confirmed bugs in `PollCard.jsx` against this real shape — fix both:**
|
|
244
|
+
1. `PollCard.jsx:18` — `const creatorId = poll.created_by?.id ?? poll.created_by;` assumes a
|
|
245
|
+
`created_by` object or scalar; the real field is `created_by_id` (a plain id, verified above).
|
|
246
|
+
`poll.created_by` is always `undefined` against the real server, so `creatorId` is always
|
|
247
|
+
`undefined`, so `canClose`'s only non-host-supplied fallback (`creatorId === currentUser?.id`,
|
|
248
|
+
`:19`) can never be true for a genuine creator — close is effectively broken for every poll unless a
|
|
249
|
+
host explicitly passes `canClose`. Fix: read `poll.created_by_id` directly.
|
|
250
|
+
2. `PollCard.jsx:15` — `useEffect(() => setSelected((poll?.options || []).filter((option) =>
|
|
251
|
+
option.selected || option.voted)...` assumes per-option `selected`/`voted` flags that don't exist
|
|
252
|
+
anywhere in the real projection (verified above: options only ever have `{id, text, order,
|
|
253
|
+
vote_count, voters}`). Selection must instead derive from `poll.voted_option_ids` **when present**
|
|
254
|
+
(only true right after a create/vote/close REST call — the WO's explicit constraint) and otherwise
|
|
255
|
+
default to empty (a poll loaded via `listMessages`/`listThread`, or updated via a `poll_updated`
|
|
256
|
+
frame, correctly has no way to know the current viewer's vote state from that payload alone — do not
|
|
257
|
+
guess, do not carry over stale local state across a payload that structurally cannot contain the
|
|
258
|
+
answer).
|
|
259
|
+
|
|
260
|
+
**Row 52 (voters) — not rendered at all today:** add voter identification per option. dcm only gives
|
|
261
|
+
user **ids** (`option.voters: [user_id, ...]`), never names — there is no user-directory endpoint
|
|
262
|
+
(same constraint already established for the DM candidate list in `MSG-3b`). Render what's actually
|
|
263
|
+
available: a count (`voters.length`) is always correct; showing the *current viewer's own* vote as
|
|
264
|
+
"you voted for this" is derivable from `voted_option_ids` after a REST action, or otherwise left
|
|
265
|
+
unmarked. Do not invent a name lookup — this is a real, structural limit of the dcm contract, not
|
|
266
|
+
something to work around client-side. If host-supplied user resolution is later wanted, that is a
|
|
267
|
+
future WO's host-prop pattern, not this chunk's job.
|
|
268
|
+
|
|
269
|
+
**Row 53 (closed state, voting disabled)** — `PollCard.jsx:17` (`const closed =
|
|
270
|
+
Boolean(poll.closed_at)`) is already correct; verify the disabled-while-closed wiring on the vote
|
|
271
|
+
controls (`:32`) still works once selection is correctly derived from `voted_option_ids`, and add
|
|
272
|
+
coverage — this row was previously untested against a real payload shape too.
|
|
273
|
+
|
|
274
|
+
**Required tests (chunk 2):**
|
|
275
|
+
- Poll rendering against a genuinely server-shaped `serialize_poll` payload (all the real keys above,
|
|
276
|
+
not a client-invented one) — question, options, `vote_count`, some indication of `voters`.
|
|
277
|
+
- `voted_option_ids` present only in the vote/create/close REST response correctly drives selection
|
|
278
|
+
state; a `poll_updated` frame carrying no `voted_option_ids` key must not clear or invert the
|
|
279
|
+
viewer's own already-known vote state (this is the WO's explicitly named required test).
|
|
280
|
+
- Close permission: `created_by_id === currentUser.id` correctly enables the close control; a
|
|
281
|
+
non-creator without a host-supplied `canClose` override does not get it.
|
|
282
|
+
- Closed poll disables voting.
|
|
283
|
+
- Existing ucm suites (chunk 1 inclusive) stay green.
|
|
284
|
+
|
|
285
|
+
**Progress contract / preamble:** identical to chunk 1's — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no git
|
|
286
|
+
operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's review.
|
|
287
|
+
|
|
288
|
+
**Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
|
|
289
|
+
`work-orders/MSG-3c.md` chunk 2 (Part B above, after chunk 1 is committed). Follow `orchestrate-codex`.
|
|
290
|
+
|
|
291
|
+
**Note from chunk 2:** the same defect class row 56/57 ask to be verified for (a `poll_updated`/
|
|
292
|
+
`reaction` frame's viewer-independent projection silently wiping viewer-specific state the client
|
|
293
|
+
learned from a REST response) was ALSO found and fixed in `poll_updated`'s handler while writing
|
|
294
|
+
chunk 2's own tests (`MessagingProvider.jsx`'s `poll_updated` case now preserves a previously-known
|
|
295
|
+
`voted_option_ids` when the incoming frame lacks it). Chunk 3 must check whether the `reaction` case
|
|
296
|
+
has the analogous issue — it very likely does (see below), don't assume it's fine because it's a
|
|
297
|
+
similar shape to the code that turned out to be wrong.
|
|
298
|
+
|
|
299
|
+
### Chunk 3 — reaction frame-merge fix, conformance-test extension, deviation-doc rewrite (rows 56, 57 + the WO's new rule)
|
|
300
|
+
|
|
301
|
+
**Files:** `src/messaging/MessagingProvider.jsx` (`applyFrame`'s `reaction` case, and `toggleReaction`'s
|
|
302
|
+
REST-confirm path — same underlying bug, two call sites), `tests/messagingContractConformance.test.js`
|
|
303
|
+
(the version-pin extension), `docs/messaging-deviations.md` (full rewrite of the seven now-delivered
|
|
304
|
+
rows + the two still-BLOCKED rows, each BLOCKED entry now carrying an explicit version pin), new tests.
|
|
305
|
+
|
|
306
|
+
**The reaction bug — verified, not hypothetical:** `django-core-micha/src/django_core_micha/messaging/serializers.py:41-46`
|
|
307
|
+
(`serialize_reactions`) returns `[{emoji, count}]` **only, never a per-viewer `reacted` flag, in any
|
|
308
|
+
context** — not REST, not frames, by design (the design doc's "aggregate only" rule). `MessagingProvider.jsx`'s
|
|
309
|
+
`toggleReaction` (around `:315-330`) correctly sets `reacted: true/false` optimistically on the current
|
|
310
|
+
user's own click, but then:
|
|
311
|
+
- its own REST-confirm step merges `reactions: result?.reactions || result?.message?.reactions ||
|
|
312
|
+
nextReactions` — `result.reactions` (the real server array) is truthy and picked first, wiping the
|
|
313
|
+
optimistic `reacted` flags for every emoji on that message.
|
|
314
|
+
- `applyFrame`'s `reaction` case (`:154` area) merges `reactions: payload.reactions || frame.reactions
|
|
315
|
+
|| previous.reactions || []` — same problem: any `reaction` frame arriving for this message (the
|
|
316
|
+
user's own echoed toggle, or literally any other viewer reacting with a different emoji) wholesale-
|
|
317
|
+
replaces the reactions array with the server's `reacted`-less version, wiping every previously-known
|
|
318
|
+
`reacted` flag on that message, not just the one that changed.
|
|
319
|
+
|
|
320
|
+
**Fix both call sites the same way:** merge the server's `{emoji,count}` list with previously-known
|
|
321
|
+
`reacted` flags by emoji key — never wholesale-replace. For each emoji in the incoming server list, if
|
|
322
|
+
a previous entry for that same emoji had `reacted: true`, carry it forward (unless this merge IS the
|
|
323
|
+
direct result of the current user's own toggle action, in which case the just-computed optimistic value
|
|
324
|
+
is correct and already known — don't let the REST-confirm step regress what the optimistic update just
|
|
325
|
+
set). An emoji present in the previous list but absent from the incoming one means its count hit zero
|
|
326
|
+
and it should simply not appear (matches the existing pattern at `:320`, `.filter((reaction) =>
|
|
327
|
+
reaction.count !== 0)`).
|
|
328
|
+
|
|
329
|
+
**Required tests (chunk 3, reaction part):**
|
|
330
|
+
- A `reaction` frame for an emoji the viewer has NOT reacted with updates its count without touching
|
|
331
|
+
the viewer's `reacted` state on any OTHER emoji on the same message (the WO's explicitly named "an
|
|
332
|
+
arriving frame updates the rendered message for a viewer who did not cause it" case).
|
|
333
|
+
- The viewer's own optimistically-set `reacted: true` on one emoji survives (a) the REST confirmation
|
|
334
|
+
of that same toggle, and (b) an unrelated `reaction` frame for a different emoji on the same message
|
|
335
|
+
arriving afterward.
|
|
336
|
+
- A duplicate `event_id` changes nothing (verify the existing `seenEventsRef` de-dup in the provider's
|
|
337
|
+
`subscribe(...)` handler already covers this at the reducer-dispatch level — this may already be
|
|
338
|
+
correct and just need a test, not a code change; check before assuming otherwise).
|
|
339
|
+
|
|
340
|
+
**Conformance-test extension (the WO's new rule, required, not optional):** add a check to
|
|
341
|
+
`tests/messagingContractConformance.test.js` that reads `docs/messaging-deviations.md` and fails if any
|
|
342
|
+
line containing `**BLOCKED**` does not also carry an explicit dependency-version pin (e.g. a regex for
|
|
343
|
+
`dcm \d+\.\d+\.\d+` on that line or within its paragraph — check the actual final wording you land on
|
|
344
|
+
in the doc rewrite below and match the regex to it, don't write the regex first and hope the prose
|
|
345
|
+
matches). **Verified: this check does not currently exist, and would currently fail** — as of this
|
|
346
|
+
WO's start, rows 27/38/42/51/52/53/58's `**BLOCKED**` lines in `docs/messaging-deviations.md` do NOT
|
|
347
|
+
carry a version pin (only the doc's intro sentence and rows 56/57 happen to mention "dcm 2.36.1"); only
|
|
348
|
+
rewriting the doc's BLOCKED rows to each name the version fixes this. **Required negative-case test**
|
|
349
|
+
(the WO is explicit: "assert the negative case, not only the positive"): construct a small in-memory
|
|
350
|
+
BLOCKED line missing a version pin and assert the check's own logic flags it — don't only test that the
|
|
351
|
+
real file passes, prove the check can fail.
|
|
352
|
+
|
|
353
|
+
**`docs/messaging-deviations.md` rewrite — the WO's central deliverable for this chunk:**
|
|
354
|
+
- Rows 38, 51, 52, 53, 56, 57, 58 move from `BLOCKED` to their real delivered outcome (`OK`, matching
|
|
355
|
+
the pattern MSG-3b's own doc used for delivered rows) — each entry should reflect what was actually
|
|
356
|
+
built across chunks 1-3 of MSG-3c (last-message preview/reorder, poll rendering incl. the
|
|
357
|
+
`created_by_id`/`voted_option_ids` fixes, the `reaction`/`poll_updated` frame-merge fixes), not a
|
|
358
|
+
generic "delivered" note copy-pasted seven times.
|
|
359
|
+
- Rows **27 and 42 stay BLOCKED** — do not touch their substance, only add the now-required version pin
|
|
360
|
+
if missing (e.g. "as of dcm 2.37.0" or whatever phrasing matches the new conformance-test regex).
|
|
361
|
+
These are real, still-open gaps (thread-receipt read state; `external_key` serialization) waiting on
|
|
362
|
+
a future dcm `MSG-2d`.
|
|
363
|
+
- The closing "Required backend follow-up" paragraph (currently lists rows 27/38/42/51-53/56-58 as all
|
|
364
|
+
needing a future dcm WO) must be corrected to name only rows 27 and 42.
|
|
365
|
+
- **Risk named in the WO, read before editing:** "correcting seven entries by hand while leaving two
|
|
366
|
+
correct ones intact is exactly where an over-eager edit removes a real blocker" — do not touch rows
|
|
367
|
+
27/42's substantive claims, only add the version pin to them if the doc rewrite's new house style
|
|
368
|
+
requires it project-wide.
|
|
369
|
+
|
|
370
|
+
**Progress contract / preamble:** identical to prior chunks — `PLAN:`/`PROGRESS:`/`RESULT:` lines, no
|
|
371
|
+
git operations, write-and-run-only-the-new-tests, leave the diff uncommitted for the orchestrator's
|
|
372
|
+
review.
|
|
373
|
+
|
|
374
|
+
**Mini-handover:** repo `C:\Users\biglmi\Documents\webapps\ui-core-micha`, branch `main`, WO
|
|
375
|
+
`work-orders/MSG-3c.md` chunk 3 (Part B above, after chunk 2 is committed) — the WO's last content
|
|
376
|
+
chunk. After this chunk's independent review and commit, run the WO-end `ui_reviewer` pass per the
|
|
377
|
+
WO's changed instruction (cross-check BLOCKED rows against the dependency repo at its **current**
|
|
378
|
+
version, not against the WO text), then one version bump + npm publish.
|