refactor chatview to have two sliver lists
Co-authored-by: Erwan Leboucher <erwanleboucher@gmail.com>
This commit is contained in:
parent
eeb2e2f54b
commit
110caff170
4 changed files with 161 additions and 152 deletions
|
|
@ -4,126 +4,148 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
final class ChatScroll({
|
||||
required final ListController listController,
|
||||
final class ChatScroll<T>({
|
||||
required final IList<T> historyItems,
|
||||
required final IList<T> liveItems,
|
||||
required final GlobalKey centerKey,
|
||||
required final ListController historyListController,
|
||||
required final ListController liveListController,
|
||||
required final ScrollController scrollController,
|
||||
required final bool hasMore,
|
||||
required final bool isLoadingOlder,
|
||||
required final Future<void> Function() loadOlder,
|
||||
required final Future<void> Function(String id) jumpToId,
|
||||
}) {
|
||||
static ChatScroll use<T>({
|
||||
static ChatScroll<T> use<T>({
|
||||
required AsyncValue<IList<T>?> controllerData,
|
||||
required String Function(T item) id,
|
||||
required Future<bool> Function() loadOlder,
|
||||
required bool Function() shouldLoadOlder,
|
||||
required Future<void> Function() onReachedBottom,
|
||||
}) {
|
||||
final listController = useRef(ListController());
|
||||
final historyListController = useRef(ListController());
|
||||
final liveListController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
final centerKey = useMemoized(GlobalKey.new);
|
||||
|
||||
final anchorId = useState<String?>(null);
|
||||
final hasMore = useState(true);
|
||||
final topItemBeforeLoad = useState<String?>(null);
|
||||
final loadingOlder = useRef(false);
|
||||
final initialized = useRef(false);
|
||||
final isLoadingOlder = useState(false);
|
||||
|
||||
Future<void> loadOlderItems() async {
|
||||
if (loadingOlder.value || !hasMore.value) return;
|
||||
|
||||
if (controllerData case AsyncData(:final value?)) {
|
||||
loadingOlder.value = true;
|
||||
topItemBeforeLoad.value = value.firstOrNull == null
|
||||
? null
|
||||
: id(value.first);
|
||||
|
||||
try {
|
||||
hasMore.value = await loadOlder();
|
||||
} finally {
|
||||
loadingOlder.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> jumpToId(String itemId) async {
|
||||
final index =
|
||||
controllerData.value?.indexWhere((item) => id(item) == itemId) ?? -1;
|
||||
|
||||
if (index == -1) return;
|
||||
|
||||
listController.value.animateToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
final anchorIdValue = anchorId.value;
|
||||
|
||||
useEffect(() {
|
||||
if (controllerData case AsyncData(:final value?)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
if (!initialized.value) {
|
||||
initialized.value = true;
|
||||
|
||||
if (value.isNotEmpty) {
|
||||
listController.value.jumpToItem(
|
||||
index: value.length - 1,
|
||||
scrollController: scrollController,
|
||||
alignment: 1,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final topItem = topItemBeforeLoad.value;
|
||||
|
||||
if (topItem != null) {
|
||||
final index = value.indexWhere((item) => id(item) == topItem);
|
||||
|
||||
if (index != -1) {
|
||||
listController.value.jumpToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
alignment: 0,
|
||||
);
|
||||
}
|
||||
|
||||
topItemBeforeLoad.value = null;
|
||||
} else if (scrollController.position.atEdge &&
|
||||
scrollController.position.pixels != 0) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
if (anchorId.value == null) {
|
||||
if (controllerData case AsyncData(:final value?)
|
||||
when value.isNotEmpty) {
|
||||
anchorId.value = id(value.last);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [controllerData]);
|
||||
|
||||
final ({IList<T> history, IList<T> live}) split = useMemoized(() {
|
||||
final items = controllerData.value;
|
||||
final anchor = anchorIdValue;
|
||||
|
||||
if (items == null || anchor == null) {
|
||||
return (history: const .empty(), live: const .empty());
|
||||
}
|
||||
|
||||
final anchorIndex = items.indexWhere((item) => id(item) == anchor);
|
||||
|
||||
if (anchorIndex == -1) {
|
||||
return (history: const .empty(), live: items);
|
||||
}
|
||||
|
||||
return (
|
||||
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
||||
live: items.skip(anchorIndex).toIList(),
|
||||
);
|
||||
}, [controllerData, anchorIdValue]);
|
||||
|
||||
Future<void> loadOlderItems() async {
|
||||
if (!hasMore.value || isLoadingOlder.value) return;
|
||||
|
||||
isLoadingOlder.value = true;
|
||||
|
||||
try {
|
||||
hasMore.value = await loadOlder();
|
||||
} finally {
|
||||
isLoadingOlder.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> jumpToId(String itemId) async {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
final historyIndex = split.history.indexWhere(
|
||||
(item) => id(item) == itemId,
|
||||
);
|
||||
|
||||
if (historyIndex != -1) {
|
||||
historyListController.value.animateToItem(
|
||||
index: historyIndex,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final liveIndex = split.live.indexWhere((item) => id(item) == itemId);
|
||||
|
||||
if (liveIndex != -1) {
|
||||
liveListController.value.animateToItem(
|
||||
index: liveIndex,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
Future<void> listener() async {
|
||||
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
||||
const loadThreshold = 500.0;
|
||||
const bottomThreshold = 50.0;
|
||||
|
||||
void checkPosition() {
|
||||
if (!scrollController.hasClients) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollController.position.pixels == 0) {
|
||||
if (shouldLoadOlder()) {
|
||||
await loadOlderItems();
|
||||
final position = scrollController.position;
|
||||
|
||||
if (position.extentAfter <= loadThreshold) {
|
||||
if (hasMore.value && !isLoadingOlder.value) {
|
||||
loadOlderItems();
|
||||
}
|
||||
} else {
|
||||
await onReachedBottom();
|
||||
}
|
||||
|
||||
if (position.extentBefore <= bottomThreshold) {
|
||||
onReachedBottom();
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(listener);
|
||||
scrollController.addListener(checkPosition);
|
||||
|
||||
return () => scrollController.removeListener(listener);
|
||||
}, [controllerData]);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition());
|
||||
|
||||
return () => scrollController.removeListener(checkPosition);
|
||||
}, [scrollController, onReachedBottom]);
|
||||
|
||||
return .new(
|
||||
listController: listController.value,
|
||||
historyItems: split.history,
|
||||
liveItems: split.live,
|
||||
centerKey: centerKey,
|
||||
historyListController: historyListController.value,
|
||||
liveListController: liveListController.value,
|
||||
scrollController: scrollController,
|
||||
hasMore: hasMore.value,
|
||||
isLoadingOlder: isLoadingOlder.value,
|
||||
loadOlder: loadOlderItems,
|
||||
jumpToId: jumpToId,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue