mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 12:25:11 +00:00
feat: better scroll to last read message handling
This commit is contained in:
parent
89493f364a
commit
9b21c0d951
2 changed files with 75 additions and 31 deletions
|
@ -297,13 +297,39 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
_loadDraft();
|
_loadDraft();
|
||||||
super.initState();
|
super.initState();
|
||||||
sendingClient = Matrix.of(context).client;
|
sendingClient = Matrix.of(context).client;
|
||||||
readMarkerEventId = room.fullyRead;
|
_tryLoadTimeline();
|
||||||
loadTimelineFuture =
|
|
||||||
_getTimeline(eventContextId: readMarkerEventId).onError(
|
|
||||||
ErrorReporter(context, 'Unable to load timeline').onErrorCallback,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
void updateView() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
@ -313,7 +339,6 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
|
|
||||||
Future<void> _getTimeline({
|
Future<void> _getTimeline({
|
||||||
String? eventContextId,
|
String? eventContextId,
|
||||||
Duration timeout = const Duration(seconds: 7),
|
|
||||||
}) async {
|
}) async {
|
||||||
await Matrix.of(context).client.roomsLoading;
|
await Matrix.of(context).client.roomsLoading;
|
||||||
await Matrix.of(context).client.accountDataLoading;
|
await Matrix.of(context).client.accountDataLoading;
|
||||||
|
@ -322,34 +347,21 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
eventContextId = null;
|
eventContextId = null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
timeline = await room
|
timeline = await room.getTimeline(
|
||||||
.getTimeline(
|
onUpdate: updateView,
|
||||||
onUpdate: updateView,
|
eventContextId: eventContextId,
|
||||||
eventContextId: eventContextId,
|
);
|
||||||
)
|
|
||||||
.timeout(timeout);
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
|
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
timeline = await room.getTimeline(onUpdate: updateView);
|
timeline = await room.getTimeline(onUpdate: updateView);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (e is TimeoutException || e is IOException) {
|
if (e is TimeoutException || e is IOException) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
_showScrollUpMaterialBanner(eventContextId!);
|
||||||
SnackBar(
|
|
||||||
content: Text(L10n.of(context)!.jumpToLastReadMessage),
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: L10n.of(context)!.jump,
|
|
||||||
onPressed: () => scrollToEventId(eventContextId!),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
||||||
if (timeline!.events.isNotEmpty) {
|
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
|
||||||
|
@ -370,6 +382,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
|
|
||||||
void setReadMarker({String? eventId}) {
|
void setReadMarker({String? eventId}) {
|
||||||
if (_setReadMarkerFuture != null) return;
|
if (_setReadMarkerFuture != null) return;
|
||||||
|
if (scrollUpBannerEventId != null) return;
|
||||||
if (eventId == null &&
|
if (eventId == null &&
|
||||||
!room.hasNewMessages &&
|
!room.hasNewMessages &&
|
||||||
room.notificationCount == 0) {
|
room.notificationCount == 0) {
|
||||||
|
@ -380,7 +393,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
final timeline = this.timeline;
|
final timeline = this.timeline;
|
||||||
if (timeline == null || timeline.events.isEmpty) return;
|
if (timeline == null || timeline.events.isEmpty) return;
|
||||||
|
|
||||||
Logs().v('Set read marker...', eventId);
|
Logs().d('Set read marker...', eventId);
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
_setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) {
|
_setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) {
|
||||||
_setReadMarkerFuture = null;
|
_setReadMarkerFuture = null;
|
||||||
|
@ -886,10 +899,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
setState(() {
|
setState(() {
|
||||||
timeline = null;
|
timeline = null;
|
||||||
_scrolledUp = false;
|
_scrolledUp = false;
|
||||||
loadTimelineFuture = _getTimeline(
|
loadTimelineFuture = _getTimeline(eventContextId: eventId).onError(
|
||||||
eventContextId: eventId,
|
|
||||||
timeout: const Duration(seconds: 30),
|
|
||||||
).onError(
|
|
||||||
ErrorReporter(context, 'Unable to load timeline after scroll to ID')
|
ErrorReporter(context, 'Unable to load timeline after scroll to ID')
|
||||||
.onErrorCallback,
|
.onErrorCallback,
|
||||||
);
|
);
|
||||||
|
@ -902,7 +912,7 @@ class ChatController extends State<ChatPageWithRoom> {
|
||||||
}
|
}
|
||||||
await scrollController.scrollToIndex(
|
await scrollController.scrollToIndex(
|
||||||
eventIndex,
|
eventIndex,
|
||||||
preferPosition: AutoScrollPosition.middle,
|
preferPosition: AutoScrollPosition.end,
|
||||||
);
|
);
|
||||||
_updateScrollController();
|
_updateScrollController();
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,7 @@ class ChatView extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||||
|
final scrollUpBannerEventId = controller.scrollUpBannerEventId;
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
|
@ -220,6 +221,39 @@ class ChatView extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
TombstoneDisplay(controller),
|
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),
|
PinnedEvents(controller),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
|
Loading…
Reference in a new issue