# 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
);
```