# 03 — Nachrichten & Events --- ## Inhaltsverzeichnis - [Textnachricht senden](#textnachricht-senden) - [HTML / Markdown senden](#html--markdown-senden) - [Antworten (Reply)](#antworten-reply) - [Bearbeiten (Edit)](#bearbeiten-edit) - [Reaktionen](#reaktionen) - [Löschen (Redact)](#löschen-redact) - [Threads](#threads) - [Polls](#polls) - [Standort](#standort) - [Timeline lesen](#timeline-lesen) - [Event-Typen Referenz](#event-typen-referenz) - [Typing-Indikator](#typing-indikator) - [Read Markers / Lesebestätigung](#read-markers--lesebestätigung) - [Pinned Messages](#pinned-messages) - [Fehlgeschlagene Events erneut senden](#fehlgeschlagene-events-erneut-senden) --- ## Textnachricht senden ```dart 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 ```dart // Mit HTML-Formatierung await room.sendTextEvent( 'Fett und kursiv', // Fallback-Text (Plain) htmlBody: 'Fett und kursiv', parseMarkdown: false, // Kein automatisches Markdown-Parsing ); // Automatisches Markdown-Parsing (SDK parst ** → ) await room.sendTextEvent( '**Fett** und *kursiv*', parseMarkdown: true, // Standard: true ); ``` --- ## Antworten (Reply) ```dart // 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) ```dart // Nachricht bearbeiten await room.sendTextEvent( 'Korrigierter Text', editEventId: altesEvent.eventId, // Referenz auf Original htmlBody: 'Alt Korrigiert', // 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 ```dart // 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> // Eigene Reaktion entfernen (= Redact) final myReaction = reactions['👍'] ?.firstWhere((e) => e.senderId == client.userID); if (myReaction != null) { await myReaction.redactEvent(reason: null); } ``` --- ## Löschen (Redact) ```dart // 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 ```dart // 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 ```dart // 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 ```dart // 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 ```dart 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 ```dart // 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 ```dart // 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 // Automatisch aktuell durch Sync ``` --- ## Read Markers / Lesebestätigung ```dart // 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 ```dart // Events anpinnen final currentPinned = (room.getState(EventTypes.RoomPinnedEvents) ?.content['pinned'] as List?) ?.cast() ?? []; 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() ?? []; // Events laden final pinnedEvents = await Future.wait( pinned.map((id) => room.getEventById(id)), ); ``` --- ## Fehlgeschlagene Events erneut senden ```dart // 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 ); ```