mirror of
https://github.com/krille-chan/fluffychat
synced 2024-10-05 16:32:51 +00:00
refactor: Enhance logic when to mark room as read
This commit is contained in:
parent
03511a1e8d
commit
0cf6a1d74a
2 changed files with 235 additions and 231 deletions
|
@ -252,6 +252,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
_loadDraft();
|
||||
super.initState();
|
||||
sendingClient = Matrix.of(context).client;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_tryLoadTimeline();
|
||||
}
|
||||
|
||||
|
@ -286,7 +288,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
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<ChatPageWithRoom>
|
|||
}
|
||||
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<ChatPageWithRoom>
|
|||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
if (!_scrolledUp) return;
|
||||
setReadMarker();
|
||||
}
|
||||
|
||||
|
@ -379,13 +385,19 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
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<ChatPageWithRoom>
|
|||
);
|
||||
});
|
||||
await loadTimelineFuture;
|
||||
setReadMarker();
|
||||
}
|
||||
scrollController.jumpTo(0);
|
||||
}
|
||||
|
@ -1174,7 +1185,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void onInputBarChanged(String text) {
|
||||
if (_inputTextIsEmpty != text.isEmpty) {
|
||||
setReadMarker();
|
||||
setState(() {
|
||||
_inputTextIsEmpty = text.isEmpty;
|
||||
});
|
||||
|
@ -1300,3 +1310,12 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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
|
||||
.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(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator
|
||||
.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
TombstoneDisplay(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceVariant,
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.dragging)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.9),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.upload_outlined,
|
||||
size: 100,
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue