Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/chatty-matrix-features.md
Fimeg 19475878dd deprecate chatty/libcmatrix/qtpim, clean stale refs
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
2026-07-21 13:51:14 -04:00

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 reply issue hit is m.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 parsedreply_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.replacereplaces_event_id).
  • Redaction target: cm-event.c:95-96 (redacts field). 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 at cm-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 at src/ui/chatty-message-row.c:224-230.

Design notes

  • event_parse_relations is the single inbound insertion point. It already walks content.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_json stub. Each relation needs either a new CmEvent subclass with a real generate_json, or a cm_room_*_async helper that builds the raw JSON and PUTs it (Matrix HTTP shapes are specced and simple). The helper approach is less GObject ceremony and matches how cm_room_send_text_async already works.
  • Matrix HTTP shapes (reference, spec-stable):
    • Redact: PUT /_matrix/client/v3/rooms/{roomId}/redact/{eventId}/{txnId} body {reason?}.
    • Reaction: a m.reaction event with content.m.relates_to {rel_type:"m.annotation", event_id:<target>, key:<emoji>}.
    • Reply: m.room.message whose formatted_body carries the <mx-reply> fallback and m.relates_to.m.in_reply_to.event_id.
    • Edit: m.room.message with content.m.relates_to {rel_type:"m.replace", event_id:<target>}, body new text, m.new_content.
  • 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)

  1. Edits — near-pure UI; inbound already parses. Smallest win, proves the consumer pattern.
  2. Redactions — small library patch (encrypted-content todo + outbound) + tombstone UI.
  3. Replies — fill the parse stub (field waiting), outbound, quoting UI.
  4. 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 (which generate_json owns) working for free. Never re-plumb encryption.
  • Matches Souveraine's substrate instinct — the Tool trait (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-secrets uses #[path] inclusion, not extern).

Two explicit design moves when building (not defaults)

  1. 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_json branches on one field. This makes reactions (m.annotation) fall out of the same machinery.
  2. Reactions stay their own event type (CM_M_REACTION), not a message variant — a reaction is a distinct m.reaction event, not m.room.message. So reactions get a small CmReactionEvent (or a raw-JSON helper, they're tiny), while edits/replies share the message path. Mirrors upstream's existing type separation (CM_M_REACTION vs CM_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.