diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index f0d4f1d..065a215 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -1,5 +1,3 @@ -import "dart:async"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; @@ -12,7 +10,6 @@ final class ChatScroll({ required final IList historyItems, required final IList liveItems, required final GlobalKey centerKey, - required final GlobalKey anchorItemKey, required final ListController historyListController, required final ListController liveListController, required final ScrollController scrollController, @@ -22,85 +19,30 @@ final class ChatScroll({ required AsyncValue controllerData, required Future Function() loadOlder, required Future Function() markRead, - required String? contextualEvent, - required void Function(String id) setContextualEvent, }) { - final anchorId = useState(null); - - final anchorItemKey = useMemoized(GlobalKey.new, [anchorId.value]); - - final historyListController = useMemoized(ListController.new, [ - anchorId.value, - ]); - final liveListController = useMemoized(ListController.new, [ - anchorId.value, - ]); + final historyListController = useRef(ListController()); + final liveListController = useRef(ListController()); final scrollController = useScrollController(); final centerKey = useMemoized(GlobalKey.new); - final pendingAnchorTarget = useState(null); - final anchorMountedCompleter = useRef?>(null); + final anchorId = useState(null); + + final anchorIdValue = anchorId.value; useEffect(() { if (anchorId.value == null) { if (controllerData case AsyncData(:final value?) when value.timeline.isNotEmpty) { - final hasContextualEvent = value.timeline.any( - (event) => event.eventId == contextualEvent, - ); - - anchorId.value = hasContextualEvent - ? contextualEvent - : value.timeline.last.eventId; + anchorId.value = value.timeline.last.eventId; } } return null; - }, [controllerData, contextualEvent]); - - useEffect(() { - final target = pendingAnchorTarget.value; - if (target == null) return null; - - final found = - controllerData.value?.timeline.any( - (event) => event.eventId == target, - ) ?? - false; - - if (found || controllerData is AsyncError) { - if (found) { - anchorId.value = target; - } else { - anchorMountedCompleter.value?.completeError( - StateError("Failed to load context for $target"), - ); - anchorMountedCompleter.value = null; - } - pendingAnchorTarget.value = null; - } - - return null; - }, [controllerData, pendingAnchorTarget.value]); - - useEffect(() { - final completer = anchorMountedCompleter.value; - if (completer == null) return null; - - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = anchorItemKey.currentContext; - if (context != null && context.mounted) { - anchorMountedCompleter.value?.complete(context); - anchorMountedCompleter.value = null; - } - }); - - return null; - }, [anchorId.value]); + }, [controllerData]); final ({IList history, IList live}) split = useMemoized(() { final items = controllerData.value?.timeline; - final anchor = anchorId.value; + final anchor = anchorIdValue; if (items == null || anchor == null) { return (history: const .empty(), live: const .empty()); @@ -116,58 +58,62 @@ final class ChatScroll({ history: items.take(anchorIndex).toIList().reversed.toIList(), live: items.skip(anchorIndex).toIList(), ); - }, [controllerData, anchorId.value]); + }, [controllerData, anchorIdValue]); - useEffect( - () { - const topThreshold = 500.0; - const bottomThreshold = 50.0; + useEffect(() { + const topThreshold = 500.0; + const bottomThreshold = 50.0; - Future checkPosition() async { - if (!scrollController.hasClients) return; - if (contextualEvent != null) return; + Future checkPosition() async { + if (!scrollController.hasClients) return; - final position = scrollController.position; + final position = scrollController.position; - if (position.extentAfter <= topThreshold) { - await loadOlder(); - } else if (position.extentBefore <= bottomThreshold) { - await markRead(); - } + if (position.extentAfter <= topThreshold) { + await loadOlder(); + } else if (position.extentBefore <= bottomThreshold) { + await markRead(); } - - scrollController.addListener(checkPosition); - - WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition()); - - return () => scrollController.removeListener(checkPosition); - }, - [scrollController, controllerData, loadOlder, markRead, contextualEvent], - ); - - double? resolveOffset(String itemId) { - final historyIndex = split.history.indexWhere( - (item) => item.eventId == itemId, - ); - if (historyIndex != -1) { - // TODO: Replace SuperSliverView because of the bug that requires this: #94 - // ignore: invalid_use_of_visible_for_testing_member - return historyListController.getOffsetToReveal(historyIndex, 0.5); } - final liveIndex = split.live.indexWhere((item) => item.eventId == itemId); - if (liveIndex != -1) { - // ignore: invalid_use_of_visible_for_testing_member - return liveListController.getOffsetToReveal(liveIndex, 0.5); - } + scrollController.addListener(checkPosition); - return null; - } + WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition()); + + return () { + scrollController.removeListener(checkPosition); + }; + }, [scrollController, controllerData, loadOlder, markRead]); Future jumpToId(String itemId) async { if (!scrollController.hasClients) return; - final offset = resolveOffset(itemId); + final historyIndex = split.history.indexWhere( + (item) => item.eventId == itemId, + ); + + double? offset; + if (historyIndex != -1) { + // TODO: Replace SuperSliverView because of the bug that requires this: #94 + // ignore: invalid_use_of_visible_for_testing_member + offset = historyListController.value.getOffsetToReveal( + historyIndex, + 0.5, + ); + } else { + final liveIndex = split.live.indexWhere( + (item) => item.eventId == itemId, + ); + + if (liveIndex != -1) { + // ignore: invalid_use_of_visible_for_testing_member + offset = liveListController.value.getOffsetToReveal(liveIndex, 0.5); + } + } + + if (offset == null) { + // not in current timeline + } if (offset != null) { await scrollController.animateTo( @@ -175,23 +121,6 @@ final class ChatScroll({ duration: const Duration(milliseconds: 700), curve: Curves.easeInOut, ); - return; - } - - final completer = Completer(); - anchorMountedCompleter.value = completer; - pendingAnchorTarget.value = itemId; - setContextualEvent(itemId); - - final context = await completer.future; - - if (context.mounted) { - await Scrollable.ensureVisible( - context, - alignment: 0.5, - duration: const Duration(milliseconds: 700), - curve: Curves.easeInOut, - ); } } @@ -199,9 +128,8 @@ final class ChatScroll({ historyItems: split.history, liveItems: split.live, centerKey: centerKey, - anchorItemKey: anchorItemKey, - historyListController: historyListController, - liveListController: liveListController, + historyListController: historyListController.value, + liveListController: liveListController.value, scrollController: scrollController, jumpToId: jumpToId, ); diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index 77164c4..3bc601c 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -28,7 +28,6 @@ class const ChatTimeline({ required Future Function(String) jumpToId, required IList Function(Event) getEventOptions, required String? highlightedEvent, - required Key key, }) => HighlightWrapper( EventRenderer( event, @@ -36,7 +35,7 @@ class const ChatTimeline({ getEventOptions: getEventOptions, isGrouped: isGrouped(event, previousEvent), ), - key: key, + key: ValueKey(event.eventId), isHighlighted: highlightedEvent == event.eventId, ); @@ -60,9 +59,6 @@ class const ChatTimeline({ jumpToId: jumpToId, getEventOptions: getEventOptions, highlightedEvent: highlightedEvent, - key: index == 0 - ? scroll.anchorItemKey - : ValueKey(scroll.liveItems[index].eventId), ), ), @@ -76,7 +72,6 @@ class const ChatTimeline({ jumpToId: jumpToId, getEventOptions: getEventOptions, highlightedEvent: highlightedEvent, - key: ValueKey(scroll.historyItems[index].eventId), ), ), ], diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index be0a606..2aee4a5 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -74,8 +74,6 @@ final class const RoomChat({ final scroll = ChatScroll.use( controllerData: controllerData, loadOlder: notifier.loadOlder, - contextualEvent: contextualEvent.value, - setContextualEvent: (id) => contextualEvent.value = id, markRead: () async { final room = ref.read( RoomsController.provider.select((rooms) => rooms[roomId]),