From d712bb3193f47bfb77bc8e7e765b517559891ce0 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 26 Sep 2026 14:52:24 -0400 Subject: [PATCH] tweak scroll behavior --- lib/helpers/hooks/chat_scroll.dart | 104 +++++++++++------------ lib/widgets/room_chat/chat_timeline.dart | 6 +- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index f278385..bea65b9 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -12,11 +12,11 @@ final class ChatScroll({ required final IList historyItems, required final IList liveItems, required final GlobalKey centerKey, - required final GlobalKey anchorItemKey, required final ScrollController scrollController, required final bool atBottom, required final Future Function(String id) jumpToId, required final Future Function() jumpToBottom, + required final GlobalKey Function(String eventId) keyFor, }) { factory use({ required AsyncValue controllerData, @@ -26,7 +26,9 @@ final class ChatScroll({ }) { final anchorId = useState(null); - final anchorItemKey = useMemoized(GlobalKey.new, [anchorId.value]); + final itemKeys = useMemoized(() => {}, []); + GlobalKey keyFor(String eventId) => + itemKeys.putIfAbsent(eventId, GlobalKey.new); final scrollController = useScrollController(); final centerKey = useMemoized(GlobalKey.new); @@ -83,7 +85,7 @@ final class ChatScroll({ if (completer == null) return null; WidgetsBinding.instance.addPostFrameCallback((_) { - final context = anchorItemKey.currentContext; + final context = keyFor(anchorId.value!).currentContext; if (context != null && context.mounted) { anchorMountedCompleter.value?.complete(context); anchorMountedCompleter.value = null; @@ -151,62 +153,58 @@ final class ChatScroll({ ], ); - Future jumpToId(String itemId) async { - if (!scrollController.hasClients) return; - - if (anchorId.value != itemId) { - final completer = Completer(); - anchorMountedCompleter.value = completer; - pendingAnchorTarget.value = itemId; - contextualEvent.value = itemId; - - final context = await completer.future; - if (!context.mounted) return; - - await Scrollable.ensureVisible( - context, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - return; - } - - final context = anchorItemKey.currentContext; - if (context != null && context.mounted) { - await Scrollable.ensureVisible( - context, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - } - } - - Future jumpToBottom() async { - if (contextualEvent.value != null) { - anchorId.value = null; - contextualEvent.value = null; - } - - if (!scrollController.hasClients) return; - - await scrollController.animateTo( - scrollController.position.minScrollExtent, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - } - return .new( historyItems: split.history, liveItems: split.live, centerKey: centerKey, - anchorItemKey: anchorItemKey, scrollController: scrollController, atBottom: atBottom.value, - jumpToId: jumpToId, - jumpToBottom: jumpToBottom, + jumpToId: (String itemId) async { + if (!scrollController.hasClients) return; + + final existing = keyFor(itemId).currentContext; + if (existing != null && existing.mounted) { + // Already mounted, just scroll + await Scrollable.ensureVisible( + existing, + alignment: 0.5, + duration: const .new(milliseconds: 700), + curve: Curves.easeInOut, + ); + } else { + final completer = Completer(); + anchorMountedCompleter.value = completer; + pendingAnchorTarget.value = itemId; + contextualEvent.value = itemId; + + final context = await completer.future; + if (!context.mounted) return; + + await Scrollable.ensureVisible(context, alignment: 10); + if (!context.mounted) return; + await Scrollable.ensureVisible( + context, + alignment: 0.5, + duration: const .new(milliseconds: 700), + curve: Curves.easeOutCirc, + ); + } + }, + jumpToBottom: () async { + if (contextualEvent.value != null) { + anchorId.value = null; + contextualEvent.value = null; + } + + if (!scrollController.hasClients) return; + + await scrollController.animateTo( + scrollController.position.minScrollExtent, + duration: const .new(milliseconds: 700), + curve: Curves.easeInOut, + ); + }, + keyFor: keyFor, ); } } diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index 049a09d..2cab4ca 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -59,9 +59,7 @@ class const ChatTimeline({ jumpToId: jumpToId, getEventOptions: getEventOptions, highlightedEvent: highlightedEvent, - key: index == 0 - ? scroll.anchorItemKey - : ValueKey(scroll.liveItems[index].eventId), + key: scroll.keyFor(scroll.liveItems[index].eventId), ), ), @@ -74,7 +72,7 @@ class const ChatTimeline({ jumpToId: jumpToId, getEventOptions: getEventOptions, highlightedEvent: highlightedEvent, - key: ValueKey(scroll.historyItems[index].eventId), + key: scroll.keyFor(scroll.historyItems[index].eventId), ), ), ],