clean up and fix up jumping
This commit is contained in:
parent
1c4bf99be2
commit
1e4aab08ad
1 changed files with 41 additions and 64 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
import "dart:async";
|
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
@ -26,28 +24,20 @@ final class ChatScroll({
|
||||||
required ValueNotifier<int?> contextualEvent,
|
required ValueNotifier<int?> contextualEvent,
|
||||||
}) {
|
}) {
|
||||||
final anchorId = useState<int?>(null);
|
final anchorId = useState<int?>(null);
|
||||||
|
final atBottom = useState(true);
|
||||||
|
final pendingJump = useState<int?>(null);
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
final centerKey = useMemoized(GlobalKey.new);
|
||||||
|
|
||||||
final itemKeys = useMemoized(() => <int, GlobalKey>{}, []);
|
final itemKeys = useMemoized(() => <int, GlobalKey>{}, []);
|
||||||
GlobalKey keyFor(int eventRowId) =>
|
GlobalKey keyFor(int eventRowId) =>
|
||||||
itemKeys.putIfAbsent(eventRowId, GlobalKey.new);
|
itemKeys.putIfAbsent(eventRowId, GlobalKey.new);
|
||||||
|
|
||||||
final scrollController = useScrollController();
|
|
||||||
final centerKey = useMemoized(GlobalKey.new);
|
|
||||||
|
|
||||||
final atBottom = useState(true);
|
|
||||||
|
|
||||||
final pendingAnchorTarget = useState<int?>(null);
|
|
||||||
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (anchorId.value == null) {
|
if (anchorId.value == null) {
|
||||||
if (controllerData case AsyncData(:final value?)
|
if (controllerData case AsyncData(:final value?)
|
||||||
when value.timeline.isNotEmpty) {
|
when value.timeline.isNotEmpty) {
|
||||||
final hasContextualEvent = value.timeline.any(
|
anchorId.value = value.timeline.contains(contextualEvent.value)
|
||||||
(event) => event == contextualEvent.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
anchorId.value = hasContextualEvent
|
|
||||||
? contextualEvent.value
|
? contextualEvent.value
|
||||||
: value.timeline.last;
|
: value.timeline.last;
|
||||||
}
|
}
|
||||||
|
|
@ -57,35 +47,34 @@ final class ChatScroll({
|
||||||
}, [controllerData, contextualEvent.value]);
|
}, [controllerData, contextualEvent.value]);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final target = pendingAnchorTarget.value;
|
final rowId = pendingJump.value;
|
||||||
if (target == null) return null;
|
if (rowId == null || anchorId.value != rowId) return null;
|
||||||
|
pendingJump.value = null;
|
||||||
|
|
||||||
final found =
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
controllerData.value?.timeline.any((event) => event == target) ??
|
if (!scrollController.hasClients) return;
|
||||||
false;
|
|
||||||
|
|
||||||
if (found || controllerData is AsyncError) {
|
final position = scrollController.position;
|
||||||
if (found) {
|
scrollController.jumpTo(
|
||||||
anchorId.value = target;
|
(-position.viewportDimension)
|
||||||
|
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
||||||
|
.toDouble(),
|
||||||
|
);
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
final context = keyFor(rowId).currentContext;
|
||||||
final context = keyFor(target).currentContext;
|
if (context?.mounted == true) {
|
||||||
if (context != null && context.mounted) {
|
await Scrollable.ensureVisible(
|
||||||
anchorMountedCompleter.value?.complete(context);
|
context!,
|
||||||
anchorMountedCompleter.value = null;
|
alignment: 0.5,
|
||||||
}
|
duration: const .new(milliseconds: 700),
|
||||||
});
|
curve: Curves.easeInOut,
|
||||||
} else {
|
|
||||||
anchorMountedCompleter.value?.completeError(
|
|
||||||
StateError("Failed to load context for $target"),
|
|
||||||
);
|
);
|
||||||
anchorMountedCompleter.value = null;
|
|
||||||
}
|
}
|
||||||
pendingAnchorTarget.value = null;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [controllerData, pendingAnchorTarget.value]);
|
}, [anchorId.value, pendingJump.value]);
|
||||||
|
|
||||||
final ({IList<int> history, IList<int> live}) split = useMemoized(() {
|
final ({IList<int> history, IList<int> live}) split = useMemoized(() {
|
||||||
final items = controllerData.value?.timeline;
|
final items = controllerData.value?.timeline;
|
||||||
|
|
@ -97,9 +86,7 @@ final class ChatScroll({
|
||||||
|
|
||||||
final anchorIndex = items.indexOf(anchor);
|
final anchorIndex = items.indexOf(anchor);
|
||||||
|
|
||||||
if (anchorIndex == -1) {
|
if (anchorIndex == -1) return (history: const .empty(), live: items);
|
||||||
return (history: const .empty(), live: items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
||||||
|
|
@ -138,7 +125,6 @@ final class ChatScroll({
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollController.addListener(checkPosition);
|
scrollController.addListener(checkPosition);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition());
|
WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition());
|
||||||
|
|
||||||
return () => scrollController.removeListener(checkPosition);
|
return () => scrollController.removeListener(checkPosition);
|
||||||
|
|
@ -158,35 +144,27 @@ final class ChatScroll({
|
||||||
centerKey: centerKey,
|
centerKey: centerKey,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
atBottom: atBottom.value,
|
atBottom: atBottom.value,
|
||||||
|
keyFor: keyFor,
|
||||||
jumpToEvent: (int rowId) async {
|
jumpToEvent: (int rowId) async {
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
|
|
||||||
final existing = keyFor(rowId).currentContext;
|
final context = keyFor(rowId).currentContext;
|
||||||
if (existing != null && existing.mounted) {
|
if (context != null) {
|
||||||
// Already mounted, just scroll
|
|
||||||
await Scrollable.ensureVisible(
|
|
||||||
existing,
|
|
||||||
alignment: 0.5,
|
|
||||||
duration: const .new(milliseconds: 700),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final completer = Completer<BuildContext>();
|
|
||||||
anchorMountedCompleter.value = completer;
|
|
||||||
pendingAnchorTarget.value = rowId;
|
|
||||||
contextualEvent.value = rowId;
|
|
||||||
|
|
||||||
final context = await completer.future;
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
await Scrollable.ensureVisible(context, alignment: 10);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
await Scrollable.ensureVisible(
|
await Scrollable.ensureVisible(
|
||||||
context,
|
context,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration: const .new(milliseconds: 700),
|
duration: const .new(milliseconds: 700),
|
||||||
curve: Curves.easeOutCirc,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingJump.value = rowId;
|
||||||
|
if (controllerData.value?.timeline.contains(rowId) ?? false) {
|
||||||
|
anchorId.value = rowId;
|
||||||
|
} else {
|
||||||
|
anchorId.value = null;
|
||||||
|
contextualEvent.value = rowId;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
jumpToBottom: () async {
|
jumpToBottom: () async {
|
||||||
|
|
@ -203,7 +181,6 @@ final class ChatScroll({
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
keyFor: keyFor,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue