diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index f6948daf..e17226fa 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -297,13 +297,39 @@ class ChatController extends State { _loadDraft(); super.initState(); sendingClient = Matrix.of(context).client; - readMarkerEventId = room.fullyRead; - loadTimelineFuture = - _getTimeline(eventContextId: readMarkerEventId).onError( - ErrorReporter(context, 'Unable to load timeline').onErrorCallback, - ); + _tryLoadTimeline(); } + void _tryLoadTimeline() async { + loadTimelineFuture = _getTimeline(); + try { + await loadTimelineFuture; + final fullyRead = room.fullyRead; + if (fullyRead.isEmpty) return; + if (timeline!.events.any((event) => event.eventId == fullyRead)) { + Logs().v('Scroll up to visible event', fullyRead); + scrollToEventId(fullyRead); + setReadMarker(); + return; + } + if (!mounted) return; + _showScrollUpMaterialBanner(fullyRead); + } catch (e, s) { + ErrorReporter(context, 'Unable to load timeline').onErrorCallback(e, s); + rethrow; + } + } + + String? scrollUpBannerEventId; + + void discardScrollUpBannerEventId() => setState(() { + scrollUpBannerEventId = null; + }); + + void _showScrollUpMaterialBanner(String eventId) => setState(() { + scrollUpBannerEventId = eventId; + }); + void updateView() { if (!mounted) return; setState(() {}); @@ -313,7 +339,6 @@ class ChatController extends State { Future _getTimeline({ String? eventContextId, - Duration timeout = const Duration(seconds: 7), }) async { await Matrix.of(context).client.roomsLoading; await Matrix.of(context).client.accountDataLoading; @@ -322,34 +347,21 @@ class ChatController extends State { eventContextId = null; } try { - timeline = await room - .getTimeline( - onUpdate: updateView, - eventContextId: eventContextId, - ) - .timeout(timeout); + timeline = await room.getTimeline( + onUpdate: updateView, + eventContextId: eventContextId, + ); } catch (e, s) { Logs().w('Unable to load timeline on event ID $eventContextId', e, s); if (!mounted) return; timeline = await room.getTimeline(onUpdate: updateView); if (!mounted) return; if (e is TimeoutException || e is IOException) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.jumpToLastReadMessage), - action: SnackBarAction( - label: L10n.of(context)!.jump, - onPressed: () => scrollToEventId(eventContextId!), - ), - ), - ); + _showScrollUpMaterialBanner(eventContextId!); } } timeline!.requestKeys(onlineKeyBackupOnly: false); - if (timeline!.events.isNotEmpty) { - if (room.markedUnread) room.markUnread(false); - setReadMarker(); - } + if (room.markedUnread) room.markUnread(false); // when the scroll controller is attached we want to scroll to an event id, if specified // and update the scroll controller...which will trigger a request history, if the @@ -370,6 +382,7 @@ class ChatController extends State { void setReadMarker({String? eventId}) { if (_setReadMarkerFuture != null) return; + if (scrollUpBannerEventId != null) return; if (eventId == null && !room.hasNewMessages && room.notificationCount == 0) { @@ -380,7 +393,7 @@ class ChatController extends State { final timeline = this.timeline; if (timeline == null || timeline.events.isEmpty) return; - Logs().v('Set read marker...', eventId); + Logs().d('Set read marker...', eventId); // ignore: unawaited_futures _setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) { _setReadMarkerFuture = null; @@ -886,10 +899,7 @@ class ChatController extends State { setState(() { timeline = null; _scrolledUp = false; - loadTimelineFuture = _getTimeline( - eventContextId: eventId, - timeout: const Duration(seconds: 30), - ).onError( + loadTimelineFuture = _getTimeline(eventContextId: eventId).onError( ErrorReporter(context, 'Unable to load timeline after scroll to ID') .onErrorCallback, ); @@ -902,7 +912,7 @@ class ChatController extends State { } await scrollController.scrollToIndex( eventIndex, - preferPosition: AutoScrollPosition.middle, + preferPosition: AutoScrollPosition.end, ); _updateScrollController(); } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 85273c22..402daea6 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -146,6 +146,7 @@ class ChatView extends StatelessWidget { ); } final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; + final scrollUpBannerEventId = controller.scrollUpBannerEventId; return WillPopScope( onWillPop: () async { @@ -220,6 +221,39 @@ class ChatView extends StatelessWidget { child: Column( children: [ TombstoneDisplay(controller), + if (scrollUpBannerEventId != null) + Material( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + child: ListTile( + leading: IconButton( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + icon: const Icon(Icons.close), + tooltip: L10n.of(context)!.close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); + }, + ), + title: Text( + L10n.of(context)!.jumpToLastReadMessage, + ), + contentPadding: + const EdgeInsets.only(left: 8), + trailing: TextButton( + onPressed: () { + controller.scrollToEventId( + scrollUpBannerEventId, + ); + controller.discardScrollUpBannerEventId(); + }, + child: Text(L10n.of(context)!.jump), + ), + ), + ), PinnedEvents(controller), Expanded( child: GestureDetector(