Chatty superseded by souveraine's Matrix sensorium. libcmatrix unused. qtpim contacts work stalled. All repos archived on Gitea. - STATE.md: remove component sections, update secrets consumers, decisions - Mark deprecated docs with headers - Update INVENTORY.md Tier 3 entries as deprecated - Update secrets.md consumer list
9.2 KiB
Chatty Matrix feature gaps — reactions, replies, redactions, edits (2026-07-16)
DEPRECATED (2026-07-21). Chatty abandoned. Matrix surface now lives in souveraine's sensorium (
--features matrix). Historical reference only.
Casey (he/him). Chatty fork ~/Projects/Chatty (branch identity-layer @
1b94cf9); libcmatrix fork ~/Projects/libcmatrix (branch sas-fixes @
0eb5f81). Ani + Aster = Annie.
The "fancy" Matrix features a modern chat has — reacting to a message, replying
with a quote, removing a message, editing one — are largely absent in Chatty.
This file records, per feature, exactly where the gap lives (library vs UI),
with file:line citations, and the subtask breakdown to land each.
No code touched yet. Design + scoping only.
The headline: this is greenfield across the whole ecosystem
Verified across GitHub code search + upstream GitLab (2026-07-16):
- libcmatrix upstream (source.puri.sm): 0 open MRs, 0 open issues for
reaction / redact / reply / edit. (The one
replyissue hit ism.thread, a different feature.) - agx/libcmatrix — a mirror, byte-identical to upstream.
- EionRobb/libcmatrix — same
event_parse_relations, only the enum carried. - FuriLabs/chatty (subprojects/libcmatrix) — identical parsing, same
/* todo */stubs at the same line numbers. - Any Chatty fork — only enum references, no consumers.
Nobody has done this work. There is nothing to cherry-pick. Our libcmatrix fork would be the reference implementation — and a candidate upstream contribution, since Chatty issue #939 shows upstream wants transport richness.
What libcmatrix has today (the ground truth)
cm_event_get_m_type() returns the parsed type; the relation parse lives in
event_parse_relations (src/events/cm-event.c:58-98). The room-level
outbound surface (src/cm-room.h) is: send_text, send_file,
set_typing_notice, set_read_marker, accept/reject_invite,
enable_encryption, leave, load_past_events, get_event. That is all.
No redact, no react, no reply, no edit.
The base cm_event_real_generate_json is g_assert_not_reached() (a crash
stub, cm-event.c:101-108); only CmRoomMessageEvent overrides generate_json
(cm-room-message-event.c:42). So only plain messages can be serialized
today — every relation-bearing outbound event needs a new path.
Per-feature gap
| Feature | Inbound (receive) | Outbound (send) | Where the gap is |
|---|---|---|---|
Edits (m.replace) |
✅ parsed — replaces_event_id (cm-event.c:74-89, incl unsigned fallback) |
❌ no API | outbound + UI (smallest) |
| Redactions | ⚠️ target captured via redacts into replaces_event_id (:95-96); encrypted redaction content is a known todo (:383) |
❌ no API | encrypted inbound + outbound + tombstone UI |
Replies (m.in_reply_to) |
❌ not parsed — reply_to_event_id field exists (:23), getter exists (:244), nothing populates it |
❌ no API | library-first: parse + outbound + UI |
Reactions (m.reaction) |
⚠️ type detected (:423-424), target event_id + emoji key not parsed — payload dropped |
❌ no API | library-first: parse + getter + outbound + UI |
File:line reference
- Edits parsed:
libcmatrix/src/events/cm-event.c:74(m.replace→replaces_event_id). - Redaction target:
cm-event.c:95-96(redactsfield). Encrypted-content todo:cm-event.c:383. - Reply field + getter:
cm-event.c:23(decl),:244(cm_event_get_reply_to_id),cm-event-private.h:46. No population site. - Reaction type detection:
cm-event.c:423-424. No target/key parse. - Outbound message-only:
cm-event.c:101-108(stub), overridden atcm-room-message-event.c:42. - Room API surface:
libcmatrix/src/cm-room.h:56-113(no relation verbs). - Chatty consumer:
Chatty/src/matrix/chatty-ma-chat.c,chatty-ma-key-chat.c— iterate events but never branch on relation types. - Chatty status enum (for completeness):
Chatty/src/chatty-enums.h:115-119(SENT/DELIVERED/READ/SENDING_FAILED/DELIVERY_FAILED); rendered atsrc/ui/chatty-message-row.c:224-230.
Design notes
event_parse_relationsis the single inbound insertion point. It already walkscontent.m.relates_to; replies and reactions are one more branch each in the same function. The field + getter for replies already exist and are waiting.- Outbound is the heavier lift because of the
generate_jsonstub. Each relation needs either a newCmEventsubclass with a realgenerate_json, or acm_room_*_asynchelper that builds the raw JSON and PUTs it (Matrix HTTP shapes are specced and simple). The helper approach is less GObject ceremony and matches howcm_room_send_text_asyncalready works. - Matrix HTTP shapes (reference, spec-stable):
- Redact:
PUT /_matrix/client/v3/rooms/{roomId}/redact/{eventId}/{txnId}body{reason?}. - Reaction: a
m.reactionevent withcontent.m.relates_to{rel_type:"m.annotation", event_id:<target>, key:<emoji>}. - Reply:
m.room.messagewhoseformatted_bodycarries the<mx-reply>fallback andm.relates_to.m.in_reply_to.event_id. - Edit:
m.room.messagewithcontent.m.relates_to{rel_type:"m.replace", event_id:<target>},bodynew text,m.new_content.
- Redact:
- Because nobody has done this, our outbound API shape is a de-facto interface if upstream ever adopts it. Argues for designing the verbs cleanly rather than quickest-path — but that's a build-time judgment, not now.
- Ties to the Person/unified-history layer: an edit/reaction/reply belongs to a message that belongs to a person. The relation-walking ("find the original this reacts/edits/replies to") is the same lookup the Person model needs. Natural order: contacts store + Person layer first, then these relation features on top — but each is independently shippable.
Sequencing (effort-ordered)
- Edits — near-pure UI; inbound already parses. Smallest win, proves the consumer pattern.
- Redactions — small library patch (encrypted-content todo + outbound) + tombstone UI.
- Replies — fill the parse stub (field waiting), outbound, quoting UI.
- Reactions — parse target+key, getter, outbound, aggregate UI.
Each lands independently in the libcmatrix sas-fixes fork, then the Chatty
identity-layer consumer. Subtasks below.
Pattern decision — RESOLVED (2026-07-16)
The "helper vs new CmEvent subclass" question posed earlier is a false dichotomy. The codebase already answers it.
How send_text_async actually works (cm-room.c): it does not build JSON
or call HTTP directly. It (1) constructs a CmRoomMessageEvent via
cm_room_message_event_new(), (2) sets body/txn_id/sender on the object,
(3) appends to the event list + DB (optimistic WAITING render), (4) queues a
GTask and calls room_send_message_from_queue(), which calls the event's own
generate_json to serialize on the wire.
room_message_generate_json (cm-room-message-event.c:42) is a clean JSON
builder: sets msgtype/body, branches file vs text, owns the encryption path,
returns the object. Adding a relation is ~two json_object_set_* calls in
this function, guarded by an optional relation field on the event struct.
So: the architecture is "event object owns its serialization; room verb
constructs + queues." The right pattern is to extend CmRoomMessageEvent to
carry an optional relation, emit it in generate_json, and make
cm_room_send_edit_async / _reply_async thin wrappers that set the relation
before queuing — exact mirror of send_text_async.
Why this is the only consistent choice:
- Edits and replies ARE message events (
m.room.message+m.relates_to). Modeling them as messages-with-relation keeps the encryption path (whichgenerate_jsonowns) working for free. Never re-plumb encryption. - Matches Souveraine's substrate instinct — the
Tooltrait (src/core/tools/defs.rs:350) is "object owns its behavior, registry routes." Same shape, different language. A future Annie Matrix-send tool wrapping these verbs gets a minimal FFI surface (the substrate has ~no C-FFI today;souveraine-secretsuses#[path]inclusion, not extern).
Two explicit design moves when building (not defaults)
- One optional relation field on the event struct, not a type-per-relation.
A single
CmEventRelation(enum: Edit / Reply + target id, and key for annotations) set before queueing.generate_jsonbranches on one field. This makes reactions (m.annotation) fall out of the same machinery. - Reactions stay their own event type (
CM_M_REACTION), not a message variant — a reaction is a distinctm.reactionevent, notm.room.message. So reactions get a smallCmReactionEvent(or a raw-JSON helper, they're tiny), while edits/replies share the message path. Mirrors upstream's existing type separation (CM_M_REACTIONvsCM_M_ROOM_MESSAGE).
Net: edits + replies land cheaply through the message-event extension;
redaction is a thin room verb over the existing HTTP client; reactions are the
one bespoke-ish path but still small. Task #1 (extend the message event +
generate_json for the edit relation) sets the template reply reuses.