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,366 @@
|
||||
# 12 — Schnellreferenz: Typen, Konstanten & Patterns
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Enum-Werte
|
||||
|
||||
```dart
|
||||
// Membership
|
||||
Membership.join // Mitglied
|
||||
Membership.invite // Eingeladen
|
||||
Membership.leave // Verlassen
|
||||
Membership.ban // Gesperrt
|
||||
Membership.knock // Klopf-Anfrage
|
||||
|
||||
// JoinRules
|
||||
JoinRules.public // Offen
|
||||
JoinRules.invite // Nur per Einladung
|
||||
JoinRules.knock // Beitrittsanfragen erlaubt
|
||||
JoinRules.restricted // Nur Mitglieder bestimmter Spaces/Räume
|
||||
JoinRules.knockRestricted
|
||||
|
||||
// HistoryVisibility
|
||||
HistoryVisibility.worldReadable // Ohne Login lesbar
|
||||
HistoryVisibility.shared // Alle Mitglieder sehen History
|
||||
HistoryVisibility.invited // Ab Einladungszeitpunkt
|
||||
HistoryVisibility.joined // Ab Beitrittszeitpunkt
|
||||
|
||||
// GuestAccess
|
||||
GuestAccess.canJoin // Gäste erlaubt
|
||||
GuestAccess.forbidden // Keine Gäste
|
||||
|
||||
// Visibility
|
||||
Visibility.public // Im Verzeichnis
|
||||
Visibility.private // Nicht im Verzeichnis
|
||||
|
||||
// CreateRoomPreset
|
||||
CreateRoomPreset.privateChat // invite + joined History
|
||||
CreateRoomPreset.publicChat // public + shared History
|
||||
CreateRoomPreset.trustedPrivateChat // wie private + alle Mitglieder sind Admins
|
||||
|
||||
// EventStatus
|
||||
EventStatus.sending // Wird übertragen
|
||||
EventStatus.sent // Server-ACK
|
||||
EventStatus.synced // Im Sync empfangen
|
||||
EventStatus.error // Fehlgeschlagen
|
||||
EventStatus.removed // Gelöscht
|
||||
|
||||
// PresenceType
|
||||
PresenceType.online
|
||||
PresenceType.unavailable
|
||||
PresenceType.offline
|
||||
|
||||
// ThumbnailMethod
|
||||
ThumbnailMethod.crop // Ausschneiden auf exakte Größe
|
||||
ThumbnailMethod.scale // Skalieren ohne Abschneiden
|
||||
|
||||
// LoginType
|
||||
LoginType.mLoginPassword
|
||||
LoginType.mLoginToken
|
||||
LoginType.mLoginSso
|
||||
|
||||
// PushRuleKind
|
||||
PushRuleKind.override
|
||||
PushRuleKind.content
|
||||
PushRuleKind.room
|
||||
PushRuleKind.sender
|
||||
PushRuleKind.underride
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EventTypes Konstanten
|
||||
|
||||
```dart
|
||||
EventTypes.Message // m.room.message
|
||||
EventTypes.Sticker // m.sticker
|
||||
EventTypes.Reaction // m.reaction
|
||||
EventTypes.RoomRedaction // m.room.redaction
|
||||
EventTypes.Encrypted // m.room.encrypted
|
||||
EventTypes.RoomCreate // m.room.create
|
||||
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.RoomPinnedEvents // m.room.pinned_events
|
||||
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
|
||||
EventTypes.CallCandidates // m.call.candidates
|
||||
EventTypes.CallReject // m.call.reject
|
||||
EventTypes.CallSelectAnswer // m.call.select_answer
|
||||
EventTypes.CallNegotiate // m.call.negotiate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Häufige Content-Zugriffe
|
||||
|
||||
```dart
|
||||
// Sicher auf Map-Inhalte zugreifen (Matrix SDK Extension)
|
||||
final text = event.content.tryGet<String>('body');
|
||||
final map = event.content.tryGetMap<String, dynamic>('info');
|
||||
final list = event.content.tryGetList<String>('aliases');
|
||||
|
||||
// Event-Inhalt strukturiert
|
||||
final msgtype = event.content['msgtype'] as String?;
|
||||
final body = event.content['body'] as String?;
|
||||
final formatted = event.content['formatted_body'] as String?;
|
||||
final format = event.content['format'] as String?; // 'org.matrix.custom.html'
|
||||
|
||||
// Relation
|
||||
final relatesTo = event.content
|
||||
.tryGetMap<String, dynamic>('m.relates_to');
|
||||
final relType = relatesTo?.tryGet<String>('rel_type');
|
||||
final eventId = relatesTo?.tryGet<String>('event_id');
|
||||
|
||||
// Verschlüsselte Medien
|
||||
final file = event.content.tryGetMap<String, dynamic>('file');
|
||||
final url = file?.tryGet<String>('url'); // mxc://
|
||||
final key = file?.tryGetMap<String, dynamic>('key');
|
||||
|
||||
// Mentions (MSC3952)
|
||||
final mentions = event.content.tryGetMap<String, dynamic>('m.mentions');
|
||||
final userIds = mentions?.tryGetList<String>('user_ids') ?? [];
|
||||
final roomMention = mentions?.tryGet<bool>('room') ?? false;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timeline-Widget Pattern (Flutter)
|
||||
|
||||
```dart
|
||||
class ChatView extends ConsumerStatefulWidget {
|
||||
final String roomId;
|
||||
const ChatView({required this.roomId, super.key});
|
||||
@override
|
||||
ConsumerState<ChatView> createState() => _ChatViewState();
|
||||
}
|
||||
|
||||
class _ChatViewState extends ConsumerState<ChatView> {
|
||||
Timeline? _timeline;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadTimeline();
|
||||
}
|
||||
|
||||
Future<void> _loadTimeline() async {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
final room = client.getRoomById(widget.roomId)!;
|
||||
_timeline = await room.getTimeline(
|
||||
onUpdate: () => setState(() {}),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timeline?.cancelSubscriptions(); // WICHTIG!
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final events = _timeline?.events ?? [];
|
||||
return ListView.builder(
|
||||
reverse: true, // Neueste unten
|
||||
itemCount: events.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final event = events[i];
|
||||
if (event.type != EventTypes.Message) return const SizedBox();
|
||||
return ListTile(
|
||||
title: Text(event.body),
|
||||
subtitle: Text(event.senderId),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Raum-Liste Widget Pattern
|
||||
|
||||
```dart
|
||||
// StreamProvider für reaktive Raumliste
|
||||
final roomsProvider = StreamProvider<List<Room>>((ref) async* {
|
||||
final client = await ref.watch(matrixClientProvider.future);
|
||||
yield _sortedRooms(client);
|
||||
await for (final _ in client.onSync.stream) {
|
||||
yield _sortedRooms(client);
|
||||
}
|
||||
});
|
||||
|
||||
List<Room> _sortedRooms(Client client) => client.rooms
|
||||
.where((r) => r.membership == Membership.join)
|
||||
.toList()
|
||||
..sort((a, b) =>
|
||||
(b.lastEvent?.originServerTs ?? DateTime(0))
|
||||
.compareTo(a.lastEvent?.originServerTs ?? DateTime(0)));
|
||||
|
||||
// In Widget:
|
||||
final rooms = ref.watch(roomsProvider);
|
||||
rooms.when(
|
||||
data: (list) => ListView.builder(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final room = list[i];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(child: Text(room.name[0])),
|
||||
title: Text(room.getLocalizedDisplayname()),
|
||||
subtitle: Text(room.lastEvent?.body ?? ''),
|
||||
trailing: room.notificationCount > 0
|
||||
? Badge(label: Text('${room.notificationCount}'))
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, s) => Text('Fehler: $e'),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nachrichten senden mit optimistischem UI
|
||||
|
||||
```dart
|
||||
// Das SDK fügt das Event sofort mit Status 'sending' in die Timeline ein.
|
||||
// UI kann es sofort anzeigen, ohne auf Server zu warten.
|
||||
|
||||
class MessageInput extends ConsumerWidget {
|
||||
final Room room;
|
||||
const MessageInput({required this.room, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = TextEditingController();
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: TextField(controller: controller)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () async {
|
||||
final text = controller.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
controller.clear();
|
||||
try {
|
||||
await room.sendTextEvent(text);
|
||||
} catch (e) {
|
||||
// Event bleibt mit Status 'error' in Timeline
|
||||
// User kann auf "Erneut senden" tippen
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nützliche Client-Extensions & Helpers
|
||||
|
||||
```dart
|
||||
// User-Erwähnung formatieren
|
||||
'@${userId.split(':').first.replaceFirst('@', '')}' // kurzform ohne Server
|
||||
|
||||
// Raum-Display-Name (FluffyChat-Stil)
|
||||
// Für DMs: Name des Gegenübers
|
||||
// Für Räume mit Name: Name
|
||||
// Für Räume ohne Name: kommagetrennte Mitgliedernamen
|
||||
room.getLocalizedDisplayname()
|
||||
|
||||
// Avatar-URL sicher holen
|
||||
user.avatarUrl?.getThumbnail(client, width: 56, height: 56)
|
||||
|
||||
// Event-Zeitstempel formatieren
|
||||
event.originServerTs.toLocal().toString()
|
||||
// oder mit intl:
|
||||
DateFormat.Hm().format(event.originServerTs.toLocal())
|
||||
|
||||
// Stable Notification-ID aus Raum-ID (deterministisch)
|
||||
int stableId(String roomId) {
|
||||
var h = 0;
|
||||
for (final c in roomId.codeUnits) {
|
||||
h = ((h * 31) + c) & 0x7FFFFFFF;
|
||||
}
|
||||
return h == 0 ? 1 : h;
|
||||
}
|
||||
|
||||
// Matrix-Größenangabe
|
||||
// event.sizeString → '2.3 MB', '512 KB'
|
||||
|
||||
// mxc:// URI in HTTP umwandeln
|
||||
mxcUri.getDownloadUri(client)
|
||||
mxcUri.getThumbnail(client, width: 300, height: 300)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehler-Handling
|
||||
|
||||
```dart
|
||||
// MatrixException — Fehler vom Server
|
||||
try {
|
||||
await client.joinRoom('!room:server');
|
||||
} on MatrixException catch (e) {
|
||||
switch (e.error) {
|
||||
case MatrixError.M_FORBIDDEN: // Kein Zugang
|
||||
case MatrixError.M_NOT_FOUND: // Raum existiert nicht
|
||||
case MatrixError.M_LIMIT_EXCEEDED: // Rate-Limit
|
||||
final retryAfter = e.retryAfterMs; // Wann wieder versuchen
|
||||
case MatrixError.M_UNKNOWN:
|
||||
default:
|
||||
print(e.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// TimeoutException — Keine Antwort vom Server
|
||||
// SocketException — Keine Netzwerkverbindung
|
||||
// FormatException — Ungültige Server-Antwort
|
||||
|
||||
// Verbindungsstatus überwachen
|
||||
client.onConnectionStateChanged.stream.listen((state) {
|
||||
switch (state) {
|
||||
case ConnectionState.connected: // Sync läuft
|
||||
case ConnectionState.connecting: // Verbindet
|
||||
case ConnectionState.reconnecting: // Verbindung wiederhergestellt
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Pakete für Pyramid
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
matrix: ^6.x.x # Matrix SDK (Haupt-SDK)
|
||||
flutter_vodozemac: ^x.x.x # E2EE (Rust-basiert, empfohlen)
|
||||
firebase_messaging: ^x.x.x # FCM Push (Android)
|
||||
firebase_core: ^x.x.x
|
||||
flutter_local_notifications: ^x.x.x # Lokale Notifications
|
||||
flutter_riverpod: ^x.x.x # State Management
|
||||
sqflite: ^x.x.x # Datenbank
|
||||
path_provider: ^x.x.x # App-Verzeichnisse
|
||||
shared_preferences: ^x.x.x # Einfache KV-Speicherung
|
||||
livekit_client: ^x.x.x # LiveKit für VoIP
|
||||
flutter_webrtc: ^x.x.x # WebRTC
|
||||
go_router: ^x.x.x # Navigation
|
||||
local_notifier: ^x.x.x # Windows Notifications
|
||||
windows_taskbar: ^x.x.x # Windows Badge
|
||||
```
|
||||
Reference in New Issue
Block a user