diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index dcd48ee7..9a429f9f 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2526,5 +2526,6 @@ "placeholders": { "path": {} } - } + }, + "jumpToLastReadMessage": "Jump to last read message" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index dea5e88a..4ef1fb57 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -132,6 +132,11 @@ class ChatController extends State { bool showEmojiPicker = false; + bool get lastReadEventVisible => + timeline == null || + room!.fullyRead.isEmpty || + timeline!.events.any((event) => event.eventId == room!.fullyRead); + void recreateChat() async { final room = this.room; final userId = room?.directChatMatrixID; @@ -190,9 +195,13 @@ class ChatController extends State { } void requestFuture() async { - if (!timeline!.canRequestFuture) return; + final timeline = this.timeline; + if (timeline == null) return; + if (!timeline.canRequestFuture) return; try { - await timeline!.requestFuture(historyCount: _loadHistoryCount); + final mostRecentEventId = timeline.events.first.eventId; + await timeline.requestFuture(historyCount: _loadHistoryCount); + setReadMarker(eventId: mostRecentEventId); } catch (err) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -280,19 +289,29 @@ class ChatController extends State { Future? _setReadMarkerFuture; - void setReadMarker([_]) { - if (_setReadMarkerFuture == null && - (room!.hasNewMessages || room!.notificationCount > 0) && - timeline != null && - timeline!.events.isNotEmpty && - Matrix.of(context).webHasFocus) { - Logs().v('Set read marker...'); - // ignore: unawaited_futures - _setReadMarkerFuture = timeline!.setReadMarker().then((_) { - _setReadMarkerFuture = null; - }); - room!.client.updateIosBadge(); + void setReadMarker({String? eventId}) { + if (_setReadMarkerFuture != null) return; + if (lastReadEventVisible && + !room!.hasNewMessages && + room!.notificationCount == 0) { + return; } + if (!Matrix.of(context).webHasFocus) return; + + final timeline = this.timeline; + if (timeline == null || timeline.events.isEmpty) return; + + if (eventId == null && !lastReadEventVisible) { + return; + } + + eventId ??= timeline.events.first.eventId; + Logs().v('Set read marker...', eventId); + // ignore: unawaited_futures + _setReadMarkerFuture = timeline.setReadMarker(eventId).then((_) { + _setReadMarkerFuture = null; + }); + room!.client.updateIosBadge(); } @override @@ -759,6 +778,7 @@ class ChatController extends State { timeline = null; }); await getTimeline(); + setReadMarker(eventId: timeline!.events.first.eventId); } scrollController.jumpTo(0); } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index b3cf40a6..f4667edc 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -174,7 +174,7 @@ class ChatView extends StatelessWidget { } }, child: GestureDetector( - onTapDown: controller.setReadMarker, + onTapDown: (_) => controller.setReadMarker(), behavior: HitTestBehavior.opaque, child: StreamBuilder( stream: controller.room!.onUpdate.stream @@ -351,6 +351,31 @@ class ChatView extends StatelessWidget { ], ), ), + if (!controller.lastReadEventVisible) + Positioned( + top: 16, + left: 0, + right: 0, + child: Center( + child: FloatingActionButton.extended( + icon: const Icon(Icons.arrow_upward_outlined), + onPressed: () => controller + .scrollToEventId(controller.room!.fullyRead), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n.of(context)!.jumpToLastReadMessage), + IconButton( + onPressed: () => controller.setReadMarker( + eventId: controller.room!.fullyRead, + ), + icon: const Icon(Icons.close), + ), + ], + ), + ), + ), + ), if (controller.dragging) Container( color: Theme.of(context)