Files
pyramid/lib/features/chat/chat_provider.dart
T
Bernd Steckmeister 25ed765a03 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
2026-07-03 05:47:18 +02:00

148 lines
4.5 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/core/e2ee_diagnostics.dart';
import 'package:pyramid/core/matrix_client.dart';
// AsyncNotifier so onUpdate can refresh state in-place without recreating the
// timeline (which would reset the loaded event window back to the default 30).
class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
Timeline? _timeline;
StreamSubscription<String>? _roomUpdateSub;
Timer? _redecryptTimer;
@override
Future<Timeline> build(String arg) async {
final roomId = arg;
final client = await ref.watch(matrixClientProvider.future);
final room = client.getRoomById(roomId);
if (room == null) throw Exception('Raum nicht gefunden: $roomId');
_timeline?.cancelSubscriptions();
_roomUpdateSub?.cancel();
_redecryptTimer?.cancel();
final diag = ref.read(e2eeDiagnosticsProvider.notifier);
final timeline = await room.getTimeline(
onUpdate: _onUpdate,
);
_timeline = timeline;
if (timeline.events.length < 20) {
await timeline.requestHistory(historyCount: 60);
}
timeline.requestKeys(onlineKeyBackupOnly: false);
unawaited(_decryptLegacyEvents(client, timeline, roomId, diag));
// Re-decrypt whenever the room updates (e.g. after new keys arrive from
// key requests). Debounced to avoid redundant passes on rapid updates.
_roomUpdateSub = room.onUpdate.stream.listen((_) {
_redecryptTimer?.cancel();
_redecryptTimer = Timer(const Duration(milliseconds: 800), () {
final t = _timeline;
if (t != null) {
unawaited(_decryptLegacyEvents(client, t, roomId, diag));
}
});
});
// Ensure device keys are downloaded for all room members so outbound
// Megolm session key sharing works on the first send.
if (room.encrypted && client.encryptionEnabled) {
unawaited(_preloadDeviceKeys(client, room));
}
ref.onDispose(() {
_roomUpdateSub?.cancel();
_redecryptTimer?.cancel();
_timeline?.cancelSubscriptions();
_timeline = null;
});
return timeline;
}
void _onUpdate() {
final t = _timeline;
if (t != null) state = AsyncData(t);
}
}
final timelineProvider =
AsyncNotifierProvider.family<TimelineNotifier, Timeline, String>(
TimelineNotifier.new,
);
Future<void> _decryptLegacyEvents(
Client client,
Timeline timeline,
String roomId,
E2eeDiagnosticsNotifier diag,
) async {
final enc = client.encryption;
if (enc == null) return;
try {
await enc.keyManager.loadAllKeysFromRoom(roomId);
} catch (e) {
Logs().e('[ChatProvider] loadAllKeysFromRoom failed', e);
}
if (!enc.enabled) return;
var changed = false;
for (var i = 0; i < timeline.events.length; i++) {
final event = timeline.events[i];
if (event.type != EventTypes.Encrypted) continue;
try {
final decrypted = await enc.decryptRoomEvent(
event,
store: true,
updateType: EventUpdateType.history,
);
if (decrypted.type != EventTypes.Encrypted) {
timeline.events[i] = decrypted;
changed = true;
} else {
// Still encrypted — log for diagnostics
final sessionId = event.content.tryGet<String>('session_id') ?? '';
diag.add(E2eeDiagEntry(
timestamp: DateTime.now(),
roomId: roomId,
eventId: event.eventId,
sessionId: sessionId,
error: decrypted.content.tryGet<String>('body') ?? 'unknown',
));
}
} catch (e) {
final sessionId = event.content.tryGet<String>('session_id') ?? '';
diag.add(E2eeDiagEntry(
timestamp: DateTime.now(),
roomId: roomId,
eventId: event.eventId,
sessionId: sessionId,
error: e.toString(),
));
}
}
// Notify UI of in-place changes only when something actually decrypted
if (changed) timeline.onUpdate?.call();
}
Future<void> _preloadDeviceKeys(Client client, Room room) async {
try {
final members = await room.requestParticipants([Membership.join, Membership.invite]);
final userIds = members.map((m) => m.id).toSet();
await client.updateUserDeviceKeys(additionalUsers: userIds);
} catch (e) {
Logs().w('[ChatProvider] Device key preload failed: $e');
}
}
final roomProvider = Provider.family<Room?, String>((ref, roomId) {
final client = ref.watch(matrixClientProvider).valueOrNull;
return client?.getRoomById(roomId);
});