From 0cf6a1d74a08bbdfb2e862e838d5dcfd54d8d2f3 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 29 Feb 2024 18:06:06 +0100 Subject: [PATCH] refactor: Enhance logic when to mark room as read --- lib/pages/chat/chat.dart | 29 ++- lib/pages/chat/chat_view.dart | 425 ++++++++++++++++------------------ 2 files changed, 229 insertions(+), 225 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ccd677e45..3d75263e3 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -252,6 +252,7 @@ class ChatController extends State setState(() => _scrolledUp = true); } else if (scrollController.position.pixels <= 0 && _scrolledUp == true) { setState(() => _scrolledUp = false); + setReadMarker(); } if (scrollController.position.pixels == 0 || @@ -275,6 +276,7 @@ class ChatController extends State _loadDraft(); super.initState(); sendingClient = Matrix.of(context).client; + WidgetsBinding.instance.addObserver(this); _tryLoadTimeline(); } @@ -286,7 +288,6 @@ class ChatController extends State if (fullyRead.isEmpty) return; if (timeline!.events.any((event) => event.eventId == fullyRead)) { Logs().v('Scroll up to visible event', fullyRead); - setReadMarker(); return; } if (!mounted) return; @@ -317,6 +318,11 @@ class ChatController extends State int? animateInEventIndex; void onInsert(int i) { + if (timeline?.events[i].status == EventStatus.synced) { + final index = timeline!.events.firstIndexWhereNotError; + if (i == index) setReadMarker(eventId: timeline?.events[i].eventId); + } + // setState will be called by updateView() anyway animateInEventIndex = i; } @@ -350,6 +356,7 @@ class ChatController extends State } timeline!.requestKeys(onlineKeyBackupOnly: false); if (room.markedUnread) room.markUnread(false); + setReadMarker(); // 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 @@ -371,7 +378,6 @@ class ChatController extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed) return; - if (!_scrolledUp) return; setReadMarker(); } @@ -379,13 +385,19 @@ class ChatController extends State void setReadMarker({String? eventId}) { if (_setReadMarkerFuture != null) return; + if (_scrolledUp) return; if (scrollUpBannerEventId != null) return; if (eventId == null && !room.hasNewMessages && room.notificationCount == 0) { return; } - if (!Matrix.of(context).webHasFocus) return; + + // Do not send read markers when app is not in foreground + if (!Matrix.of(context).webHasFocus || + WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { + return; + } final timeline = this.timeline; if (timeline == null || timeline.events.isEmpty) return; @@ -932,7 +944,6 @@ class ChatController extends State ); }); await loadTimelineFuture; - setReadMarker(); } scrollController.jumpTo(0); } @@ -1174,7 +1185,6 @@ class ChatController extends State void onInputBarChanged(String text) { if (_inputTextIsEmpty != text.isEmpty) { - setReadMarker(); setState(() { _inputTextIsEmpty = text.isEmpty; }); @@ -1300,3 +1310,12 @@ class ChatController extends State } enum EmojiPickerType { reaction, keyboard } + +extension on List { + int get firstIndexWhereNotError { + if (isEmpty) return 0; + final index = indexWhere((event) => !event.status.isError); + if (index == -1) return length; + return index; + } +} diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 18be19219..e5cbaa2ae 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -150,238 +150,223 @@ class ChatView extends StatelessWidget { controller.emojiPickerAction(); } }, - child: GestureDetector( - onTapDown: (_) => controller.setReadMarker(), - behavior: HitTestBehavior.opaque, - child: MouseRegion( - onEnter: (_) => controller.setReadMarker(), - child: StreamBuilder( - stream: controller.room.onUpdate.stream - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) => FutureBuilder( - future: controller.loadTimelineFuture, - builder: (BuildContext context, snapshot) { - return Scaffold( - appBar: AppBar( - actionsIconTheme: IconThemeData( - color: controller.selectedEvents.isEmpty - ? null - : Theme.of(context).colorScheme.primary, - ), - leading: controller.selectMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context)!.close, - color: Theme.of(context).colorScheme.primary, - ) - : UnreadRoomsBadge( - filter: (r) => r.id != controller.roomId, - badgePosition: BadgePosition.topEnd(end: 8, top: 4), - child: const Center(child: BackButton()), - ), - titleSpacing: 0, - title: ChatAppBarTitle(controller), - actions: _appBarActions(context), - ), - floatingActionButton: controller.showScrollDownButton && - controller.selectedEvents.isEmpty - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - heroTag: null, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), - ), - ) - : null, - body: DropTarget( - onDragDone: controller.onDragDone, - onDragEntered: controller.onDragEntered, - onDragExited: controller.onDragExited, - child: Stack( - children: [ - if (accountConfig.wallpaperUrl != null) - Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), - ), - ), - SafeArea( - child: Column( - children: [ - TombstoneDisplay(controller), - if (scrollUpBannerEventId != null) - Material( + child: StreamBuilder( + stream: controller.room.onUpdate.stream + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) => FutureBuilder( + future: controller.loadTimelineFuture, + builder: (BuildContext context, snapshot) { + return Scaffold( + appBar: AppBar( + actionsIconTheme: IconThemeData( + color: controller.selectedEvents.isEmpty + ? null + : Theme.of(context).colorScheme.primary, + ), + leading: controller.selectMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context)!.close, + color: Theme.of(context).colorScheme.primary, + ) + : UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), + ), + titleSpacing: 0, + title: ChatAppBarTitle(controller), + actions: _appBarActions(context), + ), + floatingActionButton: controller.showScrollDownButton && + controller.selectedEvents.isEmpty + ? Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + heroTag: null, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), + ), + ) + : null, + body: DropTarget( + onDragDone: controller.onDragDone, + onDragEntered: controller.onDragEntered, + onDragExited: controller.onDragExited, + child: Stack( + children: [ + if (accountConfig.wallpaperUrl != null) + Opacity( + opacity: accountConfig.wallpaperOpacity ?? 1, + child: MxcImage( + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 4, + height: FluffyThemes.columnWidth * 4, + placeholder: (_) => Container(), + ), + ), + SafeArea( + child: Column( + children: [ + TombstoneDisplay(controller), + if (scrollUpBannerEventId != null) + Material( + color: + Theme.of(context).colorScheme.surfaceVariant, + shape: Border( + bottom: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, + ), + ), + child: ListTile( + leading: IconButton( color: Theme.of(context) .colorScheme - .surfaceVariant, - shape: Border( - bottom: BorderSide( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), - 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), - ), - ), + .onSurfaceVariant, + icon: const Icon(Icons.close), + tooltip: L10n.of(context)!.close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); + }, ), - PinnedEvents(controller), - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator - .adaptive( - strokeWidth: 2, - ), - ); - } - return ChatEventList( - controller: controller, - ); - }, - ), + 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( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: (context) { + if (controller.timeline == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + return ChatEventList( + controller: controller, + ); + }, + ), + ), + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2.5, ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular( + AppConfig.borderRadius, ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, + bottomRight: Radius.circular( + AppConfig.borderRadius, ), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular( - AppConfig.borderRadius, - ), - bottomRight: Radius.circular( - AppConfig.borderRadius, - ), - ), - elevation: 4, - shadowColor: Colors.black.withAlpha(64), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.white - : Colors.black, - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), + ), + elevation: 4, + shadowColor: Colors.black.withAlpha(64), + clipBehavior: Clip.hardEdge, + color: Theme.of(context).brightness == + Brightness.light + ? Colors.white + : Colors.black, + child: controller.room.isAbandonedDMRoom == true + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context)!.reopenChat, - ), + foregroundColor: Theme.of(context) + .colorScheme + .error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: controller.recreateChat, + label: Text( + L10n.of(context)!.reopenChat, + ), ), - ), - ), - ], - ), - ), - if (controller.dragging) - Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), + ), ), - ), - ], + ], + ), ), - ), - ); - }, - ), - ), + if (controller.dragging) + Container( + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.9), + alignment: Alignment.center, + child: const Icon( + Icons.upload_outlined, + size: 100, + ), + ), + ], + ), + ), + ); + }, ), ), );