Files
pyramid/docs/matrix-sdk/03-messages.md
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

11 KiB

03 — Nachrichten & Events


Inhaltsverzeichnis


Textnachricht senden

final room = client.getRoomById('!roomId:server')!;

// Einfacher Text
final eventId = await room.sendTextEvent('Hallo Welt!');

// Gibt ein temporäres Event zurück (für optimistische UI)
// Das Event bekommt Status EventStatus.sending → .sent → .synced

HTML / Markdown senden

// Mit HTML-Formatierung
await room.sendTextEvent(
  'Fett und kursiv',               // Fallback-Text (Plain)
  htmlBody: '<b>Fett</b> und <em>kursiv</em>',
  parseMarkdown: false,            // Kein automatisches Markdown-Parsing
);

// Automatisches Markdown-Parsing (SDK parst ** → <b>)
await room.sendTextEvent(
  '**Fett** und *kursiv*',
  parseMarkdown: true,             // Standard: true
);

Antworten (Reply)

// Event, auf das geantwortet wird
final replyTo = timeline.events.first; // Event-Objekt

await room.sendTextEvent(
  'Das sehe ich auch so!',
  inReplyTo: replyTo,
);

// Prüfen ob ein Event eine Antwort ist
final isReply = event.isReply;           // bool
final repliedId = event.inReplyTo;       // eventId als String?

// Das referenzierte Event laden
final original = await room.getEventById(repliedId!);

Bearbeiten (Edit)

// Nachricht bearbeiten
await room.sendTextEvent(
  'Korrigierter Text',
  editEventId: altesEvent.eventId,       // Referenz auf Original
  htmlBody: '<s>Alt</s> <b>Korrigiert</b>', // optional
);

// Prüfen ob ein Event bearbeitet wurde
final isEdit = event.isEdit;             // bool

// Gerendertes Event (mit letzter Bearbeitung) holen
final displayed = event.getDisplayEvent(); // gibt die letzte Version zurück
print(displayed.body);                    // aktuelle Nachricht

// Alle Bearbeitungen eines Events
final edits = timeline.events
    .where((e) => e.isEdit && e.relationshipEventId == originalEvent.eventId)
    .toList();

Reaktionen

// Reaktion senden (Emoji oder Text)
await room.sendReaction(event.eventId, '👍');
await room.sendReaction(event.eventId, '❤️');

// Alle Reaktionen eines Events lesen
final reactions = event.aggregatedEvents(
  timeline,
  RelationshipTypes.reaction,
);
// Gruppiert nach Emoji-Key:
// Map<String, List<Event>>

// Eigene Reaktion entfernen (= Redact)
final myReaction = reactions['👍']
    ?.firstWhere((e) => e.senderId == client.userID);
if (myReaction != null) {
  await myReaction.redactEvent(reason: null);
}

Löschen (Redact)

// Event löschen
await room.redactEvent(
  event.eventId,
  reason: 'Unpassender Inhalt',     // optional
);

// Über das Event-Objekt direkt
await event.redactEvent(reason: 'Spam');

// Prüfen ob redacted
final redacted = event.isRedacted;   // bool

Threads

// Thread-Antwort senden
await room.sendEvent(
  {
    'm.relates_to': {
      'rel_type': 'm.thread',
      'event_id': rootEvent.eventId,
      'is_falling_back': true,       // true wenn kein echter Thread-Client
      'm.in_reply_to': {
        'event_id': rootEvent.eventId,
      },
    },
    'msgtype': 'm.text',
    'body': 'Antwort im Thread',
  },
  EventTypes.Message,
);

// Thread-Events eines Root-Events laden
final threadEvents = timeline.events
    .where((e) =>
        e.relationshipType == RelationshipTypes.thread &&
        e.relationshipEventId == rootEvent.eventId)
    .toList();

Polls

// Poll erstellen (MSC3381)
await room.sendEvent(
  {
    'org.matrix.msc3381.poll.start': {
      'question': {
        'msgtype': 'm.text',
        'body': 'Was ist dein Lieblingsessen?',
      },
      'kind': 'org.matrix.msc3381.poll.disclosed', // oder .undisclosed
      'max_selections': 1,
      'answers': [
        {'id': '1', 'org.matrix.msc3381.poll.answer': {'msgtype': 'm.text', 'body': 'Pizza'}},
        {'id': '2', 'org.matrix.msc3381.poll.answer': {'msgtype': 'm.text', 'body': 'Pasta'}},
        {'id': '3', 'org.matrix.msc3381.poll.answer': {'msgtype': 'm.text', 'body': 'Salat'}},
      ],
    },
    'msgtype': 'm.text',
    'body': 'Was ist dein Lieblingsessen? (Poll)',
  },
  'org.matrix.msc3381.poll.start',
);

// Abstimmen
await room.sendEvent(
  {
    'm.relates_to': {
      'rel_type': 'm.reference',
      'event_id': pollEvent.eventId,
    },
    'org.matrix.msc3381.poll.response': {
      'answers': ['1'],              // Ausgewählte IDs
    },
    'msgtype': 'm.text',
    'body': 'Meine Antwort: Pizza',
  },
  'org.matrix.msc3381.poll.response',
);

// Poll beenden
await room.sendEvent(
  {
    'm.relates_to': {
      'rel_type': 'm.reference',
      'event_id': pollEvent.eventId,
    },
    'msgtype': 'm.text',
    'body': 'Poll beendet.',
  },
  'org.matrix.msc3381.poll.end',
);

Standort

// Standort senden
await room.sendEvent(
  {
    'msgtype': 'm.location',
    'body': 'Mein Standort: geo:52.5200,13.4050',
    'geo_uri': 'geo:52.5200,13.4050',
    'org.matrix.msc3488.location': {
      'uri': 'geo:52.5200,13.4050',
      'description': 'Berlin',
    },
  },
  EventTypes.Message,
);

Timeline lesen

final room = client.getRoomById('!roomId:server')!;

// Timeline erstellen (Callbacks werden bei Änderungen gerufen)
Timeline? timeline;
timeline = await room.getTimeline(
  onUpdate: () {
    // Vollständiges UI-Rebuild
    setState(() {});
  },
  onChange: (int index) {
    // Einzelnes Event geändert (z.B. Reaktion, Edit, Status)
    setState(() {});
  },
  onInsert: (int index) {
    // Neues Event eingefügt
    setState(() {});
  },
  onRemove: (int index) {
    // Event entfernt (Redaction)
    setState(() {});
  },
);

// WICHTIG: Immer aufräumen!
@override
void dispose() {
  timeline?.cancelSubscriptions();
  super.dispose();
}

// Events abrufen (neueste zuerst, Index 0 = aktuellstes)
final events = timeline.events;

// Ältere Nachrichten nachladen (z.B. beim Scrollen nach oben)
await timeline.requestHistory(count: 30);
// Gibt true zurück wenn es noch mehr gibt
final hasMore = timeline.canRequestHistory;

// Bestimmtes Event aus Timeline oder Server laden
final event = await room.getEventById('$eventId');

// Neuere Events nachladen (nach Gap)
await timeline.requestFuture(count: 20);

Event-Typen Referenz

// Nachrichten-Typen (msgtype)
'm.text'       // Textnachricht
'm.emote'      // /me Aktion
'm.notice'     // Bot-Nachricht (dezenter)
'm.image'      // Bild
'm.video'      // Video
'm.audio'      // Audio/Sprachnachricht
'm.file'       // Datei
'm.location'   // Standort
'm.sticker'    // Sticker (eigener Event-Typ: m.sticker)

// Event-Typen (type)
EventTypes.Message          // m.room.message
EventTypes.Sticker          // m.sticker
EventTypes.Reaction         // m.reaction
EventTypes.RoomRedaction    // m.room.redaction
EventTypes.Encrypted        // m.room.encrypted
EventTypes.RoomName         // m.room.name
EventTypes.RoomTopic        // m.room.topic
EventTypes.RoomAvatar       // m.room.avatar
EventTypes.RoomMember       // m.room.member
EventTypes.RoomPowerLevels  // m.room.power_levels
EventTypes.RoomCanonicalAlias // m.room.canonical_alias
EventTypes.RoomJoinRules    // m.room.join_rules
EventTypes.RoomGuestAccess  // m.room.guest_access
EventTypes.HistoryVisibility // m.room.history_visibility
EventTypes.RoomCreate       // m.room.create
EventTypes.RoomTombstone    // m.room.tombstone
EventTypes.Encryption       // m.room.encryption
EventTypes.SpaceChild       // m.space.child
EventTypes.SpaceParent      // m.space.parent
EventTypes.CallInvite        // m.call.invite
EventTypes.CallAnswer        // m.call.answer
EventTypes.CallHangup        // m.call.hangup

// Event-Status prüfen
event.status == EventStatus.sending    // Wird gesendet (optimistisch)
event.status == EventStatus.sent       // Vom Server bestätigt
event.status == EventStatus.synced     // Im eigenen Sync empfangen
event.status == EventStatus.error      // Fehlgeschlagen
event.status == EventStatus.removed    // Gelöscht

// Nützliche Event-Properties
event.type               // String: Event-Typ
event.eventId            // String: ID
event.senderId           // String: MXID des Senders
event.originServerTs     // DateTime: Sendezeitpunkt
event.body               // String: Nachrichtentext (entschlüsselt)
event.messageType        // String?: msgtype
event.formattedBody      // String?: HTML-Inhalt
event.isReply            // bool
event.isEdit             // bool
event.isReacted          // bool
event.isRedacted         // bool
event.isEncrypted        // bool
event.isBadEncrypted     // bool: Entschlüsselung fehlgeschlagen
event.isState            // bool: State-Event?
event.relationshipEventId // String?: ID des referenzierten Events
event.relationshipType   // String?: Beziehungstyp (m.replace, m.thread, etc.)

// Sender-User holen
final sender = room.unsafeGetUserFromMemoryOrFallback(event.senderId);
print(sender.displayName);
print(sender.avatarUrl);

Typing-Indikator

// Schreiben anzeigen
await room.setTyping(true, timeout: 30000); // 30 Sekunden

// Schreiben beenden
await room.setTyping(false);

// Tippende User im Raum beobachten
final typing = room.typingUsers; // List<User>
// Automatisch aktuell durch Sync

Read Markers / Lesebestätigung

// Gelesenes Event markieren
await room.setReadMarker(
  event.eventId,
  mRead: event.eventId,   // private receipt (unsichtbar für andere)
  // public receipt weglassen für private-only
);

// Öffentliche + private Lesebestätigung
await room.setReadMarker(
  event.eventId,
  mRead: event.eventId,
);

// Letztes gelesenes Event
final lastRead = room.lastReadEventId;

// Ungelesen-Badge
final count = room.notificationCount;   // Notifications
final hl    = room.highlightCount;      // Mentions/Highlights

Pinned Messages

// Events anpinnen
final currentPinned = (room.getState(EventTypes.RoomPinnedEvents)
    ?.content['pinned'] as List?)
    ?.cast<String>() ?? [];

await room.client.sendState(
  room.id,
  EventTypes.RoomPinnedEvents,
  {'pinned': [...currentPinned, eventId]},
  stateKey: '',
);

// Angepinnte Events lesen
final pinned = (room.getState(EventTypes.RoomPinnedEvents)
    ?.content['pinned'] as List?)
    ?.cast<String>() ?? [];

// Events laden
final pinnedEvents = await Future.wait(
  pinned.map((id) => room.getEventById(id)),
);

Fehlgeschlagene Events erneut senden

// Events mit Status error finden
final failed = timeline.events
    .where((e) => e.status == EventStatus.error)
    .toList();

// Erneut senden
for (final event in failed) {
  await event.sendAgain();
}

// Oder aus dem Timeline-Controller:
await room.sendEvent(
  event.content,
  event.type,
  txnId: event.eventId, // Gleiche txnId → überschreibt fehlgeschlagenes
);