From 29157c725214f894bac5b8acd71b41013a1aa3f0 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 2 Jun 2026 19:47:51 -0400 Subject: [PATCH 1/4] do not reverse the scrollview --- lib/controllers/room_chat_controller.dart | 13 ++--- lib/widgets/room_chat.dart | 60 +++++++++++++++-------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 03caa04..7e7eebc 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -38,16 +38,11 @@ class RoomChatController extends AsyncNotifier> { loadOlder(); } - return IMap.fromValues( - keyMapper: (id) => 9999999999 + (id ?? 0), - values: room.sticky, - ) - .addAll(room.timeline) - .toEntryIList(compare: (a, b) => (b?.key ?? 0).compareTo(a?.key ?? 0)) + return room.timeline + .toValueIList(sort: true, compare: (a, b) => (a ?? 0).compareTo(b ?? 0)) + .addAll(room.sticky) .map((entry) { - final foundEvent = entry.value == null - ? null - : room.events[entry.value!]; + final foundEvent = entry == null ? null : room.events[entry]; final editedEvent = foundEvent == null || foundEvent.lastEditRowId == 0 diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 95e7514..d8664b1 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -77,6 +77,29 @@ class RoomChat extends HookConsumerWidget { final listController = useRef(ListController()); final scrollController = useScrollController(); + final controllerData = ref.watch(controllerProvider); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + + return null; + }, [scrollController.hasClients]); + + useEffect(() { + if (scrollController.position.atEdge && + scrollController.position.pixels != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + } + return null; + }, [controllerData]); useEffect(() { Future listener() async { @@ -88,9 +111,9 @@ class RoomChat extends HookConsumerWidget { if (room == null) return; if (scrollController.position.pixels == 0) { - await client.markRead(room); - } else { if (room.hasMore) await notifier.loadOlder(); + } else { + await client.markRead(room); } } @@ -316,8 +339,6 @@ class RoomChat extends HookConsumerWidget { ].toIList(); } - final controllerData = ref.watch(controllerProvider); - return Scaffold( appBar: RoomAppbar( roomId: roomId, @@ -339,11 +360,20 @@ class RoomChat extends HookConsumerWidget { child: switch (controllerData) { AsyncData(:final value) || AsyncLoading(:final value?) => CustomScrollView( - reverse: true, controller: scrollController, slivers: [ - SliverPadding( - padding: .only(bottom: composerSize.value), + SliverToBoxAdapter( + child: Padding( + padding: .symmetric(vertical: 36), + child: Center( + child: ElevatedButton( + onPressed: controllerData is AsyncData + ? notifier.loadOlder + : null, + child: Text("Load More"), + ), + ), + ), ), SuperSliverList.builder( @@ -351,7 +381,7 @@ class RoomChat extends HookConsumerWidget { itemCount: value.length, itemBuilder: (_, index) { final event = value[index]; - final previousEvent = value.getOrNull(index + 1); + final previousEvent = value.getOrNull(index - 1); return FlashWrapper( EventRenderer( event, @@ -389,18 +419,8 @@ class RoomChat extends HookConsumerWidget { }, ), - SliverToBoxAdapter( - child: Padding( - padding: .symmetric(vertical: 36), - child: Center( - child: controllerData is AsyncLoading - ? Loading() - : ElevatedButton( - onPressed: notifier.loadOlder, - child: Text("Load More"), - ), - ), - ), + SliverPadding( + padding: .only(bottom: composerSize.value), ), ], ), -- 2.54.0 From ce4ade681507562849560738adeebd8f6c6ed892 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 2 Jun 2026 19:58:09 -0400 Subject: [PATCH 2/4] fix race condition when loading new chat --- lib/widgets/room_chat.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index d8664b1..92aa95a 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -90,7 +90,8 @@ class RoomChat extends HookConsumerWidget { }, [scrollController.hasClients]); useEffect(() { - if (scrollController.position.atEdge && + if (scrollController.hasClients && + scrollController.position.atEdge && scrollController.position.pixels != 0) { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { -- 2.54.0 From 1215be0b21fc18eb39886ea27cfcbdc813ab3d29 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 2 Jun 2026 20:42:04 -0400 Subject: [PATCH 3/4] attempt to remember position when loading history --- lib/controllers/room_chat_controller.dart | 4 ++- lib/widgets/room_chat.dart | 32 +++++++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 7e7eebc..84ac7b0 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -39,7 +39,9 @@ class RoomChatController extends AsyncNotifier> { } return room.timeline - .toValueIList(sort: true, compare: (a, b) => (a ?? 0).compareTo(b ?? 0)) + .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) + .map((element) => element.value) + .toIList() .addAll(room.sticky) .map((entry) { final foundEvent = entry == null ? null : room.events[entry]; diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 92aa95a..61fd3f8 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -79,6 +79,13 @@ class RoomChat extends HookConsumerWidget { final scrollController = useScrollController(); final controllerData = ref.watch(controllerProvider); + final heightBeforeLoad = useState(null); + + Future loadOlder() async { + heightBeforeLoad.value = scrollController.position.maxScrollExtent; + await notifier.loadOlder(); + } + useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { @@ -90,8 +97,19 @@ class RoomChat extends HookConsumerWidget { }, [scrollController.hasClients]); useEffect(() { - if (scrollController.hasClients && - scrollController.position.atEdge && + if (!scrollController.hasClients || controllerData is! AsyncData) return; + + if (heightBeforeLoad.value != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent - + heightBeforeLoad.value!, + ); + } + heightBeforeLoad.value = null; + }); + } else if (scrollController.position.atEdge && scrollController.position.pixels != 0) { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { @@ -104,7 +122,9 @@ class RoomChat extends HookConsumerWidget { useEffect(() { Future listener() async { - if (!scrollController.position.atEdge) return; + if (!scrollController.hasClients || !scrollController.position.atEdge) { + return; + } final room = ref.watch( RoomsController.provider.select((value) => value[roomId]), @@ -112,7 +132,9 @@ class RoomChat extends HookConsumerWidget { if (room == null) return; if (scrollController.position.pixels == 0) { - if (room.hasMore) await notifier.loadOlder(); + if (room.hasMore) { + await loadOlder(); + } } else { await client.markRead(room); } @@ -369,7 +391,7 @@ class RoomChat extends HookConsumerWidget { child: Center( child: ElevatedButton( onPressed: controllerData is AsyncData - ? notifier.loadOlder + ? loadOlder : null, child: Text("Load More"), ), -- 2.54.0 From cdcc34acaac4150662d34f3577ccbaf39119cd09 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 2 Jun 2026 21:04:23 -0400 Subject: [PATCH 4/4] remember position on load older based on event id instead of scroll progress --- lib/widgets/room_chat.dart | 60 +++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 61fd3f8..2d98ced 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -79,11 +79,14 @@ class RoomChat extends HookConsumerWidget { final scrollController = useScrollController(); final controllerData = ref.watch(controllerProvider); - final heightBeforeLoad = useState(null); + final topEventBeforeLoad = useState(null); Future loadOlder() async { - heightBeforeLoad.value = scrollController.position.maxScrollExtent; - await notifier.loadOlder(); + if (controllerData + case AsyncData(:final value) || AsyncLoading(:final value?)) { + topEventBeforeLoad.value = value.firstOrNull?.eventId; + await notifier.loadOlder(); + } } useEffect(() { @@ -97,26 +100,37 @@ class RoomChat extends HookConsumerWidget { }, [scrollController.hasClients]); useEffect(() { - if (!scrollController.hasClients || controllerData is! AsyncData) return; - - if (heightBeforeLoad.value != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent - - heightBeforeLoad.value!, - ); - } - heightBeforeLoad.value = null; - }); - } else if (scrollController.position.atEdge && - scrollController.position.pixels != 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } - }); + if (controllerData case AsyncData( + :final value, + ) when scrollController.hasClients) { + if (topEventBeforeLoad.value != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + final index = value.indexWhere( + (event) => event.eventId == topEventBeforeLoad.value, + ); + if (index != -1) { + listController.value.jumpToItem( + index: index, + scrollController: scrollController, + alignment: 0, + ); + } + } + topEventBeforeLoad.value = null; + }); + } else if (scrollController.position.atEdge && + scrollController.position.pixels != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent, + ); + } + }); + } } + return null; }, [controllerData]); @@ -142,7 +156,7 @@ class RoomChat extends HookConsumerWidget { scrollController.addListener(listener); return () => scrollController.removeListener(listener); - }, [roomId]); + }, [roomId, controllerData]); final composerNode = useFocusNode( onKeyEvent: (_, event) { -- 2.54.0