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:
co-authored by
Claude Opus 5.5
parent
223c9448f6
commit
1d0ccfde8b
@@ -9,6 +9,7 @@ import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/app_database.dart';
|
||||
import 'package:pyramid/core/notification_content.dart';
|
||||
import 'package:pyramid/core/read_markers.dart';
|
||||
import 'package:pyramid/core/soft_logout_guard.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -167,9 +168,11 @@ Future<bool> _bgMarkRead(String? roomId, String? eventId) async {
|
||||
if (client == null) return false;
|
||||
try {
|
||||
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
await room
|
||||
.setReadMarker(eventId, mRead: eventId)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
await markRoomReadUpTo(
|
||||
room,
|
||||
eventId,
|
||||
publicReceipt: await loadReadReceiptsPublic(),
|
||||
).timeout(const Duration(seconds: 15));
|
||||
debugPrint('[NOTIF-BGENGINE] read marker set for $roomId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:pyramid/core/background_push.dart';
|
||||
import 'package:pyramid/core/fcm_push_service.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/notification_content.dart';
|
||||
import 'package:pyramid/core/read_markers.dart';
|
||||
import 'package:pyramid/core/settings_prefs.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
@@ -205,9 +206,14 @@ Future<void> _initAndroid(WidgetRef ref) async {
|
||||
if (roomId != null && eventId != null) {
|
||||
try {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
await client
|
||||
.getRoomById(roomId)
|
||||
?.setReadMarker(eventId, mRead: eventId);
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room != null) {
|
||||
await markRoomReadUpTo(
|
||||
room,
|
||||
eventId,
|
||||
publicReceipt: ref.read(privacyReadReceiptsProvider),
|
||||
);
|
||||
}
|
||||
debugPrint('[NOTIF-FG] markReadFromNotification: $roomId bis $eventId');
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF-FG] markReadFromNotification error: $e');
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// SharedPreferences-Schlüssel der Einstellung „Lesebestätigungen senden“
|
||||
/// (siehe privacyReadReceiptsProvider). Auch der Hintergrund-Isolate der
|
||||
/// Benachrichtigungen liest ihn, dort gibt es keine Provider.
|
||||
const readReceiptsPrefKey = 'privacy_read_receipts';
|
||||
|
||||
Future<bool> loadReadReceiptsPublic() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(readReceiptsPrefKey) ?? true;
|
||||
}
|
||||
|
||||
/// Setzt Lesemarker und Lesebestätigung bis [eventId].
|
||||
///
|
||||
/// Mit [publicReceipt] = false (Einstellung „Lesebestätigungen senden“ aus)
|
||||
/// geht nur die private Bestätigung (m.read.private) raus: Der Server zählt
|
||||
/// Ungelesenes trotzdem herunter und andere eigene Geräte kennen den Stand,
|
||||
/// Chatpartner sehen aber kein „gelesen“. Früher wurde in diesem Fall gar
|
||||
/// nichts gesendet – Ungelesen-Zähler blieben dann stehen.
|
||||
Future<void> markRoomReadUpTo(
|
||||
Room room,
|
||||
String eventId, {
|
||||
required bool publicReceipt,
|
||||
}) => room.setReadMarker(eventId, mRead: eventId, public: publicReceipt);
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:pyramid/core/appearance.dart';
|
||||
import 'package:pyramid/core/read_markers.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
|
||||
// ── Generic SharedPreferences-backed bool notifier ──────────────────────────
|
||||
@@ -87,7 +88,7 @@ final privacyPresenceProvider =
|
||||
StateNotifierProvider<BoolPref, bool>((ref) => BoolPref('privacy_presence', true));
|
||||
|
||||
final privacyReadReceiptsProvider =
|
||||
StateNotifierProvider<BoolPref, bool>((ref) => BoolPref('privacy_read_receipts', true));
|
||||
StateNotifierProvider<BoolPref, bool>((ref) => BoolPref(readReceiptsPrefKey, true));
|
||||
|
||||
final privacyTypingProvider =
|
||||
StateNotifierProvider<BoolPref, bool>((ref) => BoolPref('privacy_typing', true));
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:pyramid/features/chat/message_group.dart';
|
||||
import 'package:pyramid/features/chat/pinned_messages_panel.dart';
|
||||
import 'package:pyramid/widgets/hover_region.dart';
|
||||
import 'package:pyramid/widgets/measure_size.dart';
|
||||
import 'package:pyramid/core/read_markers.dart';
|
||||
import 'package:pyramid/widgets/mxc_image.dart';
|
||||
|
||||
import 'package:pyramid/core/notification_service.dart';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user