25ed765a03
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
222 lines
5.4 KiB
Markdown
222 lines
5.4 KiB
Markdown
# 04 — Direct Messages (DMs)
|
|
|
|
---
|
|
|
|
## Inhaltsverzeichnis
|
|
- [DM starten oder finden](#dm-starten-oder-finden)
|
|
- [DM-Raum identifizieren](#dm-raum-identifizieren)
|
|
- [DM-Liste](#dm-liste)
|
|
- [Gruppen-DM](#gruppen-dm)
|
|
- [DM-Raum schließen / vergessen](#dm-raum-schließen--vergessen)
|
|
- [User-Profil für DM laden](#user-profil-für-dm-laden)
|
|
- [Verschlüsselung in DMs](#verschlüsselung-in-dms)
|
|
|
|
---
|
|
|
|
## DM starten oder finden
|
|
|
|
```dart
|
|
// Existierenden DM finden oder neuen erstellen
|
|
// Gibt immer einen joined Room zurück
|
|
final room = await client.startDirectChat('@alice:server');
|
|
print(room.id); // !roomId:server
|
|
|
|
// Mit expliziten Optionen
|
|
final room = await client.startDirectChat(
|
|
'@alice:server',
|
|
// enableEncryption: true, // Standard: true
|
|
);
|
|
|
|
// Navigieren zum DM-Raum
|
|
// (z.B. in Pyramid:)
|
|
ref.read(activeRoomIdProvider.notifier).state = room.id;
|
|
```
|
|
|
|
---
|
|
|
|
## DM-Raum identifizieren
|
|
|
|
```dart
|
|
final room = client.getRoomById('!roomId:server')!;
|
|
|
|
// Ist es ein DM?
|
|
final isDm = room.isDirectChat; // bool
|
|
|
|
// Mit wem?
|
|
final partner = room.directChatMatrixID; // '@alice:server' oder null
|
|
|
|
// DM zu einem bestimmten User finden
|
|
final existingDm = client.getDirectChatFromUserId('@alice:server');
|
|
// Gibt Room? zurück (null wenn kein DM vorhanden)
|
|
|
|
// Alle DMs des Users (Account-Data)
|
|
final dmMap = client.directChats;
|
|
// Map<String, List<String>>: { '@alice:server': ['!roomId:server'] }
|
|
```
|
|
|
|
---
|
|
|
|
## DM-Liste
|
|
|
|
```dart
|
|
// Alle DM-Räume (joined)
|
|
final dms = client.rooms.where((r) =>
|
|
r.isDirectChat &&
|
|
r.membership == Membership.join,
|
|
).toList();
|
|
|
|
// Sortiert nach letzter Aktivität
|
|
dms.sort((a, b) =>
|
|
(b.lastEvent?.originServerTs ?? DateTime(0))
|
|
.compareTo(a.lastEvent?.originServerTs ?? DateTime(0)));
|
|
|
|
// DMs mit ungelesenen Nachrichten
|
|
final unreadDms = dms.where((r) => r.notificationCount > 0).toList();
|
|
|
|
// Display-Info für DM-Liste
|
|
for (final dm in dms) {
|
|
final partner = client.getDirectChatFromUserId(dm.directChatMatrixID!);
|
|
final user = dm.unsafeGetUserFromMemoryOrFallback(dm.directChatMatrixID!);
|
|
print(user.displayName ?? dm.directChatMatrixID);
|
|
print(user.avatarUrl);
|
|
print(dm.lastEvent?.body); // Vorschautext
|
|
print(dm.lastEvent?.originServerTs); // Zeitstempel
|
|
print(dm.notificationCount); // Ungelesen
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Gruppen-DM
|
|
|
|
```dart
|
|
// Gruppen-DM mit mehreren Personen erstellen
|
|
final roomId = await client.createRoom(
|
|
name: 'Gruppenunterhaltung',
|
|
invite: ['@bob:server', '@charlie:server'],
|
|
isDirect: true, // Als DM markieren
|
|
preset: CreateRoomPreset.trustedPrivateChat,
|
|
enableEncryption: true,
|
|
);
|
|
|
|
// Oder mit startDirectChat (für Einzel-DMs) und manuell Teilnehmer hinzufügen
|
|
final room = await client.startDirectChat('@bob:server');
|
|
await room.invite('@charlie:server');
|
|
|
|
// Gruppe als DM im Account-Data kennzeichnen
|
|
// (passiert automatisch bei isDirect: true)
|
|
```
|
|
|
|
---
|
|
|
|
## DM-Raum schließen / vergessen
|
|
|
|
```dart
|
|
final room = client.getRoomById('!roomId:server')!;
|
|
|
|
// Verlassen (Gegenüber kann weiter chatten)
|
|
await room.leave();
|
|
|
|
// Aus Raumliste entfernen
|
|
await room.forget();
|
|
|
|
// DM aus Account-Daten entfernen
|
|
final dmMap = Map<String, dynamic>.from(client.directChats);
|
|
dmMap.remove('@alice:server');
|
|
// oder nur den spezifischen Raum entfernen:
|
|
final rooms = List<String>.from(dmMap['@alice:server'] ?? []);
|
|
rooms.remove(room.id);
|
|
if (rooms.isEmpty) {
|
|
dmMap.remove('@alice:server');
|
|
} else {
|
|
dmMap['@alice:server'] = rooms;
|
|
}
|
|
await client.setAccountData(
|
|
client.userID!,
|
|
'm.direct',
|
|
dmMap,
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## User-Profil für DM laden
|
|
|
|
```dart
|
|
// Profil des DM-Partners laden
|
|
final partnerMxid = room.directChatMatrixID!;
|
|
|
|
// Aus Room-Membership (gecacht, schnell)
|
|
final user = room.unsafeGetUserFromMemoryOrFallback(partnerMxid);
|
|
print(user.displayName);
|
|
print(user.avatarUrl);
|
|
|
|
// Frisch vom Server (genauer, aber langsamer)
|
|
final profile = await client.getProfileFromUserId(partnerMxid);
|
|
print(profile.displayname);
|
|
print(profile.avatarUrl);
|
|
|
|
// Avatar-URL in Widget verwenden
|
|
// (mxc:// URI → HTTP-URL via SDK)
|
|
final httpUrl = user.avatarUrl?.getThumbnail(
|
|
client,
|
|
width: 56,
|
|
height: 56,
|
|
method: ThumbnailMethod.crop,
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Verschlüsselung in DMs
|
|
|
|
```dart
|
|
// E2EE-Status prüfen
|
|
final isEncrypted = room.isEncrypted;
|
|
|
|
// E2EE aktivieren (einmalig, nicht mehr rückgängig!)
|
|
if (!room.isEncrypted) {
|
|
await room.enableEncryption();
|
|
}
|
|
|
|
// Geräte des Partners verifizieren
|
|
final partnerDevices = await client.getUserDeviceKeys('@alice:server');
|
|
for (final device in partnerDevices?.deviceKeys.values ?? []) {
|
|
print('${device.deviceId}: verified=${device.verified}');
|
|
}
|
|
|
|
// Fehlende Schlüssel für Entschlüsselung anfordern
|
|
await client.requestMissingSessionKeys();
|
|
|
|
// Schlechte Verschlüsselung erkennen
|
|
final badEvents = timeline.events
|
|
.where((e) => e.isBadEncrypted)
|
|
.toList();
|
|
```
|
|
|
|
---
|
|
|
|
## Einladungen (DM-Invite-Ansicht)
|
|
|
|
```dart
|
|
// Eingehende DM-Einladungen
|
|
final invites = client.rooms.where((r) =>
|
|
r.membership == Membership.invite &&
|
|
r.isDirectChat).toList();
|
|
|
|
// Einladung anzeigen
|
|
for (final invite in invites) {
|
|
final senderId = invite.getState(EventTypes.RoomMember, client.userID!)
|
|
?.senderId; // Wer hat eingeladen?
|
|
final sender = await client.getProfileFromUserId(senderId ?? '');
|
|
print('Einladung von ${sender.displayname}');
|
|
|
|
// Annehmen
|
|
await invite.join();
|
|
|
|
// Ablehnen
|
|
await invite.leave();
|
|
await invite.forget();
|
|
}
|
|
```
|