fix: Haken live, Einladung im Chat annehmen, Trenner verschwindet zuverlässig

Bernds Test mit der PC-Testinstanz:
- ✓✓ erschien erst nach erneutem Öffnen: eine Lesebestätigung ohne neue
  Nachricht zeichnete nichts neu (Raum-Update kommt im SDK vor der
  Bestätigung). Jetzt nach jedem Sync Fingerabdruck der Lesepositionen
  prüfen und bei Änderung neu zeichnen.
- Einladung: Chat zeigte leeren Verlauf, Annehmen ging nur in der Liste, und
  danach blieb der Chat leer bis zum App-Neustart. Jetzt Annehmen/Ablehnen
  direkt im Chat; Annehmen wartet auf den Beitritts-Sync, der Verlauf baut
  sich bei invite→join selbst neu auf.
- Trenner blieb stehen, wenn nach dem Öffnen/Zurückkommen nicht gescrollt
  wurde. Jetzt: neueste Nachrichten sichtbar (≤ halbe Bildschirmhöhe
  hochgescrollt) → nach 6 s weich ausblenden, auch ohne Scrollen.
- Live-Test mit beiden Testkonten (Gegenprobe: ohne Fix rot) in
  scripts/test.ps1 -Ui; Testlauf-Sperre erkennt nur noch eigene Testläufe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 23:33:59 +02:00
co-authored by Claude Opus 5.5
parent a6461fe23c
commit c817de924a
7 changed files with 479 additions and 16 deletions
+56
View File
@@ -10,7 +10,9 @@ import 'package:pyramid/core/matrix_client.dart';
class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
Timeline? _timeline;
StreamSubscription<String>? _roomUpdateSub;
StreamSubscription<SyncUpdate>? _syncSub;
Timer? _redecryptTimer;
String _receiptSig = '';
@override
Future<Timeline> build(String arg) async {
@@ -21,6 +23,7 @@ class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
_timeline?.cancelSubscriptions();
_roomUpdateSub?.cancel();
_syncSub?.cancel();
_redecryptTimer?.cancel();
final diag = ref.read(e2eeDiagnosticsProvider.notifier);
@@ -49,6 +52,28 @@ class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
});
});
// Nach jedem Sync (erst dann sind Lesebestätigungen verarbeitet – das
// Raum-Update kommt im SDK VORHER):
// • Einladung angenommen (invite → join): Verlauf neu aufbauen; der beim
// Einladen gebaute blieb sonst leer, bis man die App neu startete.
// • Lesebestätigung eines anderen hat sich geändert: neu zeichnen, damit
// ✓✓ live erscheint. Neue Nachrichten allein lösen _onUpdate schon aus,
// eine Bestätigung ohne neue Nachricht bisher nicht.
final membershipAtBuild = room.membership;
_receiptSig = _receiptSignature(room);
_syncSub = client.onSync.stream.listen((_) {
if (membershipAtBuild != Membership.join &&
room.membership == Membership.join) {
ref.invalidateSelf();
return;
}
final sig = _receiptSignature(room);
if (sig != _receiptSig) {
_receiptSig = sig;
_onUpdate();
}
});
// 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) {
@@ -57,6 +82,7 @@ class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
ref.onDispose(() {
_roomUpdateSub?.cancel();
_syncSub?.cancel();
_redecryptTimer?.cancel();
_timeline?.cancelSubscriptions();
_timeline = null;
@@ -70,6 +96,36 @@ class TimelineNotifier extends FamilyAsyncNotifier<Timeline, String> {
}
}
/// Kurzer Fingerabdruck der Lesepositionen der anderen Mitglieder.
String _receiptSignature(Room room) {
final b = StringBuffer();
for (final users in [
room.receiptState.global.otherUsers,
room.receiptState.mainThread?.otherUsers ?? const {},
]) {
users.forEach((userId, data) => b
..write(userId)
..write('=')
..write(data.eventId)
..write(';'));
}
return b.toString();
}
/// Einladung annehmen und warten, bis der Raum als beigetreten
/// synchronisiert ist – erst dann hat der Verlauf Inhalt.
Future<void> acceptInvite(WidgetRef ref, Room room) async {
final client = room.client;
// Vor dem Beitreten anfangen zu warten, sonst kann der Sync durchrutschen.
final synced = client
.waitForRoomInSync(room.id, join: true)
.timeout(const Duration(seconds: 20))
.then<void>((_) {}, onError: (_) {});
await room.join();
await synced;
ref.invalidate(timelineProvider(room.id));
}
final timelineProvider =
AsyncNotifierProvider.family<TimelineNotifier, Timeline, String>(
TimelineNotifier.new,