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
This commit is contained in:
Bernd Steckmeister
2026-07-03 05:47:18 +02:00
parent d706ace35f
commit 25ed765a03
195 changed files with 47784 additions and 1236 deletions
+221
View File
@@ -0,0 +1,221 @@
# 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();
}
```