From 99270f4bd1052a5884e6d21a4504b32861869545 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 2 Jun 2026 21:09:35 -0400 Subject: [PATCH 1/5] don't reverse CustomScrollView Reviewed-on: https://git.federated.nexus/Nexus/nexus/pulls/34 --- lib/controllers/room_chat_controller.dart | 15 ++-- lib/widgets/room_chat.dart | 101 +++++++++++++++++----- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 03caa04..84ac7b0 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -38,16 +38,13 @@ 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 + .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) + .map((element) => element.value) + .toIList() + .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..2d98ced 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -77,10 +77,68 @@ class RoomChat extends HookConsumerWidget { final listController = useRef(ListController()); final scrollController = useScrollController(); + final controllerData = ref.watch(controllerProvider); + + final topEventBeforeLoad = useState(null); + + Future loadOlder() async { + if (controllerData + case AsyncData(:final value) || AsyncLoading(:final value?)) { + topEventBeforeLoad.value = value.firstOrNull?.eventId; + await notifier.loadOlder(); + } + } + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + + return null; + }, [scrollController.hasClients]); + + useEffect(() { + 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]); 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]), @@ -88,15 +146,17 @@ class RoomChat extends HookConsumerWidget { if (room == null) return; if (scrollController.position.pixels == 0) { - await client.markRead(room); + if (room.hasMore) { + await loadOlder(); + } } else { - if (room.hasMore) await notifier.loadOlder(); + await client.markRead(room); } } scrollController.addListener(listener); return () => scrollController.removeListener(listener); - }, [roomId]); + }, [roomId, controllerData]); final composerNode = useFocusNode( onKeyEvent: (_, event) { @@ -316,8 +376,6 @@ class RoomChat extends HookConsumerWidget { ].toIList(); } - final controllerData = ref.watch(controllerProvider); - return Scaffold( appBar: RoomAppbar( roomId: roomId, @@ -339,11 +397,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 + ? loadOlder + : null, + child: Text("Load More"), + ), + ), + ), ), SuperSliverList.builder( @@ -351,7 +418,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 +456,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), ), ], ), From 5fcc31b4279721fe441e9f9ae0856ceb71e91fbf Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 3 Jun 2026 10:18:20 -0400 Subject: [PATCH 2/5] add a slight offset to the maxScrollExtent jump to not accidentally mark room as read --- lib/widgets/room_chat.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 2d98ced..0c1f407 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -92,7 +92,9 @@ class RoomChat extends HookConsumerWidget { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); + scrollController.jumpTo( + scrollController.position.maxScrollExtent - .000001, + ); } }); From a87c9dc678bba174836341acbb110d298ed20cbf Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 3 Jun 2026 10:24:05 -0400 Subject: [PATCH 3/5] warm up emojis controller --- lib/controllers/emoji_controller.dart | 7 +++---- lib/pages/chat_page.dart | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/controllers/emoji_controller.dart b/lib/controllers/emoji_controller.dart index 7d00ac6..caea3de 100644 --- a/lib/controllers/emoji_controller.dart +++ b/lib/controllers/emoji_controller.dart @@ -78,8 +78,7 @@ class EmojiController extends AsyncNotifier { return (customCategories, customKeywords); } - static final provider = - AsyncNotifierProvider.autoDispose( - EmojiController.new, - ); + static final provider = AsyncNotifierProvider( + EmojiController.new, + ); } diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index d90d3d4..2c0ef97 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/emoji_controller.dart"; import "package:nexus/controllers/init_complete_controller.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/widgets/appbar.dart"; @@ -17,6 +18,7 @@ class ChatPage extends ConsumerWidget { final showMembersByDefault = constraints.maxWidth > 1000; final initComplete = ref.watch(InitCompleteController.provider); final roomId = ref.watch(KeyController.provider(KeyController.roomKey)); + ref.read(EmojiController.provider); return Scaffold( appBar: initComplete ? null : Appbar(), From e63e5a8c08acfc386a01bd4f39e5fd084bc43d3d Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 3 Jun 2026 10:41:34 -0400 Subject: [PATCH 4/5] fix auto scroll down sometimes not triggering --- lib/widgets/room_chat.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 0c1f407..9e3d281 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -82,21 +82,24 @@ class RoomChat extends HookConsumerWidget { final topEventBeforeLoad = useState(null); Future loadOlder() async { - if (controllerData - case AsyncData(:final value) || AsyncLoading(:final value?)) { + if (controllerData case AsyncData(:final value)) { topEventBeforeLoad.value = value.firstOrNull?.eventId; await notifier.loadOlder(); } } useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent - .000001, + ref + .read(controllerProvider.future) + .then( + (_) => WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent - .000001, + ); + } + }), ); - } - }); return null; }, [scrollController.hasClients]); From c898b2671aabe3484002a7137033c90307dca179 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 3 Jun 2026 10:47:42 -0400 Subject: [PATCH 5/5] add link to #35 from readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 702fd89..73f00de 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Member list - [ ] Colors based off of power level - [ ] Sort by power level -- [ ] Notifications using UnifiedPush +- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites - [ ] Settings