refactor: Enhance logic when to mark room as read

pull/920/head
krille-chan 1 year ago
parent 03511a1e8d
commit 0cf6a1d74a
No known key found for this signature in database

@ -252,6 +252,7 @@ class ChatController extends State<ChatPageWithRoom>
setState(() => _scrolledUp = true); setState(() => _scrolledUp = true);
} else if (scrollController.position.pixels <= 0 && _scrolledUp == true) { } else if (scrollController.position.pixels <= 0 && _scrolledUp == true) {
setState(() => _scrolledUp = false); setState(() => _scrolledUp = false);
setReadMarker();
} }
if (scrollController.position.pixels == 0 || if (scrollController.position.pixels == 0 ||
@ -275,6 +276,7 @@ class ChatController extends State<ChatPageWithRoom>
_loadDraft(); _loadDraft();
super.initState(); super.initState();
sendingClient = Matrix.of(context).client; sendingClient = Matrix.of(context).client;
WidgetsBinding.instance.addObserver(this);
_tryLoadTimeline(); _tryLoadTimeline();
} }
@ -286,7 +288,6 @@ class ChatController extends State<ChatPageWithRoom>
if (fullyRead.isEmpty) return; if (fullyRead.isEmpty) return;
if (timeline!.events.any((event) => event.eventId == fullyRead)) { if (timeline!.events.any((event) => event.eventId == fullyRead)) {
Logs().v('Scroll up to visible event', fullyRead); Logs().v('Scroll up to visible event', fullyRead);
setReadMarker();
return; return;
} }
if (!mounted) return; if (!mounted) return;
@ -317,6 +318,11 @@ class ChatController extends State<ChatPageWithRoom>
int? animateInEventIndex; int? animateInEventIndex;
void onInsert(int i) { 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 // setState will be called by updateView() anyway
animateInEventIndex = i; animateInEventIndex = i;
} }
@ -350,6 +356,7 @@ class ChatController extends State<ChatPageWithRoom>
} }
timeline!.requestKeys(onlineKeyBackupOnly: false); timeline!.requestKeys(onlineKeyBackupOnly: false);
if (room.markedUnread) room.markUnread(false); if (room.markedUnread) room.markUnread(false);
setReadMarker();
// when the scroll controller is attached we want to scroll to an event id, if specified // 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 // and update the scroll controller...which will trigger a request history, if the
@ -371,7 +378,6 @@ class ChatController extends State<ChatPageWithRoom>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
if (state != AppLifecycleState.resumed) return; if (state != AppLifecycleState.resumed) return;
if (!_scrolledUp) return;
setReadMarker(); setReadMarker();
} }
@ -379,13 +385,19 @@ class ChatController extends State<ChatPageWithRoom>
void setReadMarker({String? eventId}) { void setReadMarker({String? eventId}) {
if (_setReadMarkerFuture != null) return; if (_setReadMarkerFuture != null) return;
if (_scrolledUp) return;
if (scrollUpBannerEventId != null) return; if (scrollUpBannerEventId != null) return;
if (eventId == null && if (eventId == null &&
!room.hasNewMessages && !room.hasNewMessages &&
room.notificationCount == 0) { room.notificationCount == 0) {
return; 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; final timeline = this.timeline;
if (timeline == null || timeline.events.isEmpty) return; if (timeline == null || timeline.events.isEmpty) return;
@ -932,7 +944,6 @@ class ChatController extends State<ChatPageWithRoom>
); );
}); });
await loadTimelineFuture; await loadTimelineFuture;
setReadMarker();
} }
scrollController.jumpTo(0); scrollController.jumpTo(0);
} }
@ -1174,7 +1185,6 @@ class ChatController extends State<ChatPageWithRoom>
void onInputBarChanged(String text) { void onInputBarChanged(String text) {
if (_inputTextIsEmpty != text.isEmpty) { if (_inputTextIsEmpty != text.isEmpty) {
setReadMarker();
setState(() { setState(() {
_inputTextIsEmpty = text.isEmpty; _inputTextIsEmpty = text.isEmpty;
}); });
@ -1300,3 +1310,12 @@ class ChatController extends State<ChatPageWithRoom>
} }
enum EmojiPickerType { reaction, keyboard } enum EmojiPickerType { reaction, keyboard }
extension on List<Event> {
int get firstIndexWhereNotError {
if (isEmpty) return 0;
final index = indexWhere((event) => !event.status.isError);
if (index == -1) return length;
return index;
}
}

@ -150,238 +150,223 @@ class ChatView extends StatelessWidget {
controller.emojiPickerAction(); controller.emojiPickerAction();
} }
}, },
child: GestureDetector( child: StreamBuilder(
onTapDown: (_) => controller.setReadMarker(), stream: controller.room.onUpdate.stream
behavior: HitTestBehavior.opaque, .rateLimit(const Duration(seconds: 1)),
child: MouseRegion( builder: (context, snapshot) => FutureBuilder(
onEnter: (_) => controller.setReadMarker(), future: controller.loadTimelineFuture,
child: StreamBuilder( builder: (BuildContext context, snapshot) {
stream: controller.room.onUpdate.stream return Scaffold(
.rateLimit(const Duration(seconds: 1)), appBar: AppBar(
builder: (context, snapshot) => FutureBuilder( actionsIconTheme: IconThemeData(
future: controller.loadTimelineFuture, color: controller.selectedEvents.isEmpty
builder: (BuildContext context, snapshot) { ? null
return Scaffold( : Theme.of(context).colorScheme.primary,
appBar: AppBar( ),
actionsIconTheme: IconThemeData( leading: controller.selectMode
color: controller.selectedEvents.isEmpty ? IconButton(
? null icon: const Icon(Icons.close),
: Theme.of(context).colorScheme.primary, onPressed: controller.clearSelectedEvents,
), tooltip: L10n.of(context)!.close,
leading: controller.selectMode color: Theme.of(context).colorScheme.primary,
? IconButton( )
icon: const Icon(Icons.close), : UnreadRoomsBadge(
onPressed: controller.clearSelectedEvents, filter: (r) => r.id != controller.roomId,
tooltip: L10n.of(context)!.close, badgePosition: BadgePosition.topEnd(end: 8, top: 4),
color: Theme.of(context).colorScheme.primary, child: const Center(child: BackButton()),
) ),
: UnreadRoomsBadge( titleSpacing: 0,
filter: (r) => r.id != controller.roomId, title: ChatAppBarTitle(controller),
badgePosition: BadgePosition.topEnd(end: 8, top: 4), actions: _appBarActions(context),
child: const Center(child: BackButton()), ),
), floatingActionButton: controller.showScrollDownButton &&
titleSpacing: 0, controller.selectedEvents.isEmpty
title: ChatAppBarTitle(controller), ? Padding(
actions: _appBarActions(context), padding: const EdgeInsets.only(bottom: 56.0),
), child: FloatingActionButton(
floatingActionButton: controller.showScrollDownButton && onPressed: controller.scrollDown,
controller.selectedEvents.isEmpty heroTag: null,
? Padding( mini: true,
padding: const EdgeInsets.only(bottom: 56.0), child: const Icon(Icons.arrow_downward_outlined),
child: FloatingActionButton( ),
onPressed: controller.scrollDown, )
heroTag: null, : null,
mini: true, body: DropTarget(
child: const Icon(Icons.arrow_downward_outlined), onDragDone: controller.onDragDone,
), onDragEntered: controller.onDragEntered,
) onDragExited: controller.onDragExited,
: null, child: Stack(
body: DropTarget( children: <Widget>[
onDragDone: controller.onDragDone, if (accountConfig.wallpaperUrl != null)
onDragEntered: controller.onDragEntered, Opacity(
onDragExited: controller.onDragExited, opacity: accountConfig.wallpaperOpacity ?? 1,
child: Stack( child: MxcImage(
children: <Widget>[ uri: accountConfig.wallpaperUrl,
if (accountConfig.wallpaperUrl != null) fit: BoxFit.cover,
Opacity( isThumbnail: true,
opacity: accountConfig.wallpaperOpacity ?? 1, width: FluffyThemes.columnWidth * 4,
child: MxcImage( height: FluffyThemes.columnWidth * 4,
uri: accountConfig.wallpaperUrl, placeholder: (_) => Container(),
fit: BoxFit.cover, ),
isThumbnail: true, ),
width: FluffyThemes.columnWidth * 4, SafeArea(
height: FluffyThemes.columnWidth * 4, child: Column(
placeholder: (_) => Container(), children: <Widget>[
), TombstoneDisplay(controller),
), if (scrollUpBannerEventId != null)
SafeArea( Material(
child: Column( color:
children: <Widget>[ Theme.of(context).colorScheme.surfaceVariant,
TombstoneDisplay(controller), shape: Border(
if (scrollUpBannerEventId != null) bottom: BorderSide(
Material( width: 1,
color: Theme.of(context).dividerColor,
),
),
child: ListTile(
leading: IconButton(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.surfaceVariant, .onSurfaceVariant,
shape: Border( icon: const Icon(Icons.close),
bottom: BorderSide( tooltip: L10n.of(context)!.close,
width: 1, onPressed: () {
color: Theme.of(context).dividerColor, controller.discardScrollUpBannerEventId();
), controller.setReadMarker();
), },
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), title: Text(
Expanded( L10n.of(context)!.jumpToLastReadMessage,
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,
);
},
),
), ),
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 && alignment: Alignment.center,
controller.room.membership == Membership.join) child: Material(
Container( borderRadius: const BorderRadius.only(
margin: EdgeInsets.only( bottomLeft: Radius.circular(
bottom: bottomSheetPadding, AppConfig.borderRadius,
left: bottomSheetPadding,
right: bottomSheetPadding,
), ),
constraints: const BoxConstraints( bottomRight: Radius.circular(
maxWidth: FluffyThemes.columnWidth * 2.5, AppConfig.borderRadius,
), ),
alignment: Alignment.center, ),
child: Material( elevation: 4,
borderRadius: const BorderRadius.only( shadowColor: Colors.black.withAlpha(64),
bottomLeft: Radius.circular( clipBehavior: Clip.hardEdge,
AppConfig.borderRadius, color: Theme.of(context).brightness ==
), Brightness.light
bottomRight: Radius.circular( ? Colors.white
AppConfig.borderRadius, : Colors.black,
), child: controller.room.isAbandonedDMRoom == true
), ? Row(
elevation: 4, mainAxisAlignment:
shadowColor: Colors.black.withAlpha(64), MainAxisAlignment.spaceEvenly,
clipBehavior: Clip.hardEdge, children: [
color: Theme.of(context).brightness == TextButton.icon(
Brightness.light style: TextButton.styleFrom(
? Colors.white padding: const EdgeInsets.all(
: Colors.black, 16,
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,
),
), ),
TextButton.icon( foregroundColor: Theme.of(context)
style: TextButton.styleFrom( .colorScheme
padding: const EdgeInsets.all( .error,
16, ),
), icon: const Icon(
), Icons.archive_outlined,
icon: const Icon( ),
Icons.forum_outlined, onPressed: controller.leaveChat,
), label: Text(
onPressed: L10n.of(context)!.leave,
controller.recreateChat, ),
label: Text( ),
L10n.of(context)!.reopenChat, TextButton.icon(
), style: TextButton.styleFrom(
padding: const EdgeInsets.all(
16,
), ),
], ),
) icon: const Icon(
: Column( Icons.forum_outlined,
mainAxisSize: MainAxisSize.min, ),
children: [ onPressed: controller.recreateChat,
const ConnectionStatusHeader(), label: Text(
ReactionsPicker(controller), L10n.of(context)!.reopenChat,
ReplyDisplay(controller), ),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
), ),
), ],
), )
], : Column(
), mainAxisSize: MainAxisSize.min,
), children: [
if (controller.dragging) const ConnectionStatusHeader(),
Container( ReactionsPicker(controller),
color: Theme.of(context) ReplyDisplay(controller),
.scaffoldBackgroundColor ChatInputRow(controller),
.withOpacity(0.9), ChatEmojiPicker(controller),
alignment: Alignment.center, ],
child: const Icon( ),
Icons.upload_outlined, ),
size: 100,
), ),
), ],
], ),
), ),
), if (controller.dragging)
); Container(
}, color: Theme.of(context)
), .scaffoldBackgroundColor
), .withOpacity(0.9),
alignment: Alignment.center,
child: const Icon(
Icons.upload_outlined,
size: 100,
),
),
],
),
),
);
},
), ),
), ),
); );

Loading…
Cancel
Save