fix: Lesebestätigung nur im Vordergrund, ohne Dauerfeuer, privat statt gar nicht

- Offener Chat meldete bei jedem Neuaufbau erneut "gelesen" – auch mit
  App im Hintergrund; die gerade gezeigte Benachrichtigung verschwand dann
  sofort wieder. Jetzt nur bei Fokus, je Event nur einmal.
- Zurück in der App: "Neue Nachrichten"-Trenner hinter dem zuletzt
  gelesenen Stand, danach gelesen melden.
- "Lesebestätigungen senden" aus: nur noch private Bestätigung
  (m.read.private) statt gar keiner – Ungelesen-Zähler stimmen wieder.
  Gilt auch für "Gelesen" aus der Benachrichtigung.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 21:32:36 +02:00
co-authored by Claude Opus 5.5
parent 223c9448f6
commit 1d0ccfde8b
6 changed files with 100 additions and 15 deletions
+57 -8
View File
@@ -28,6 +28,10 @@ class _ChatViewState extends ConsumerState<ChatView> {
// Fade-out of the "Neue Nachrichten" divider once the user reaches the bottom.
bool _unreadFading = false;
Timer? _unreadFadeTimer;
// Zuletzt von hier als gelesen gemeldetes Event. Verhindert, dass jeder
// Neuaufbau erneut eine Lesebestätigung schickt, und markiert nach dem
// Zurückkehren in die App, ab wo „Neue Nachrichten“ beginnen.
String? _lastMarkedEventId;
// Jump-to-message (from search): scroll to + briefly highlight a target event.
final _jumpKey = GlobalKey();
@@ -127,6 +131,11 @@ class _ChatViewState extends ConsumerState<ChatView> {
void initState() {
super.initState();
_scrollCtrl.addListener(_onScroll);
// App/Fenster wieder im Vordergrund: was inzwischen kam, jetzt als
// gelesen melden – mit „Neue Nachrichten“-Trenner davor.
ref.listenManual<bool>(windowFocusedProvider, (prev, focused) {
if (focused && prev == false) _onFocusRegained();
});
}
@override
@@ -142,6 +151,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
_historyExhausted = false;
_unreadFading = false;
_unreadFadeTimer?.cancel();
_lastMarkedEventId = null;
_jumpEventId = null;
_highlightEventId = null;
_highlightOn = false;
@@ -242,21 +252,60 @@ class _ChatViewState extends ConsumerState<ChatView> {
}
void _markRead([Timeline? tl]) {
// Nur als gelesen melden, was wirklich zu sehen ist: Läuft die App im
// Hintergrund (oder hat das Fenster keinen Fokus), bleibt alles ungelesen
// und die Benachrichtigung stehen – vorher wurde sie beim nächsten
// Neuaufbau sofort wieder entfernt und die Nachricht als gelesen gemeldet.
if (!ref.read(windowFocusedProvider)) return;
// Cancel any pending notification for this room (user is now reading it).
cancelNotificationForRoom(widget.roomId);
final sendReceipts = ref.read(privacyReadReceiptsProvider);
if (!sendReceipts) return;
final room = ref.read(roomProvider(widget.roomId));
if (room == null) return;
if (room.markedUnread) room.markUnread(false).catchError((_) {});
final timeline = tl ?? ref.read(timelineProvider(widget.roomId)).valueOrNull;
final latest = timeline?.events.firstOrNull;
if (latest != null && latest.status.isSynced) {
room.setReadMarker(latest.eventId, mRead: latest.eventId)
.catchError((_) {});
} else {
room.markUnread(false).catchError((_) {});
final latest = timeline?.events
.where((e) => e.status.isSynced)
.firstOrNull;
if (latest == null || latest.eventId == _lastMarkedEventId) return;
_lastMarkedEventId = latest.eventId;
markRoomReadUpTo(
room,
latest.eventId,
publicReceipt: ref.read(privacyReadReceiptsProvider),
).catchError((_) {
// Beim nächsten Anlass erneut versuchen.
if (_lastMarkedEventId == latest.eventId) _lastMarkedEventId = null;
});
}
/// Zurück in der App mit diesem Chat offen: Kamen inzwischen Nachrichten
/// von anderen, erscheint der „Neue Nachrichten“-Trenner hinter dem zuletzt
/// gelesenen Stand; danach wird bis zur neuesten Nachricht gelesen gemeldet.
void _onFocusRegained() {
if (!mounted || !_sessionCaptured) return;
final timeline = ref.read(timelineProvider(widget.roomId)).valueOrNull;
if (timeline == null) return;
final since = _lastMarkedEventId;
if (since != null && !_sessionHasUnread) {
final me = timeline.room.client.userID;
final events = timeline.events; // newest-first
final idx = events.indexWhere((e) => e.eventId == since);
final hasNew = idx > 0 &&
events
.take(idx)
.any((e) => _MessageList._isVisible(e) && e.senderId != me);
if (hasNew) {
_unreadFadeTimer?.cancel();
setState(() {
_sessionFullyReadId = since;
_sessionHasUnread = true;
_unreadFading = false;
});
}
}
_markRead(timeline);
}
Future<void> _onScroll() async {