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:
@@ -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();
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user