extract scroll helpers into their own hook
This commit is contained in:
parent
529a2e35c4
commit
eeb2e2f54b
3 changed files with 160 additions and 115 deletions
131
lib/helpers/hooks/chat_scroll.dart
Normal file
131
lib/helpers/hooks/chat_scroll.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
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,
|
||||||
|
required final ScrollController scrollController,
|
||||||
|
required final bool hasMore,
|
||||||
|
required final Future<void> Function() loadOlder,
|
||||||
|
required final Future<void> Function(String id) jumpToId,
|
||||||
|
}) {
|
||||||
|
static ChatScroll 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 scrollController = useScrollController();
|
||||||
|
|
||||||
|
final hasMore = useState(true);
|
||||||
|
final topItemBeforeLoad = useState<String?>(null);
|
||||||
|
final loadingOlder = useRef(false);
|
||||||
|
final initialized = useRef(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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [controllerData]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> listener() async {
|
||||||
|
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollController.position.pixels == 0) {
|
||||||
|
if (shouldLoadOlder()) {
|
||||||
|
await loadOlderItems();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await onReachedBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
}, [controllerData]);
|
||||||
|
|
||||||
|
return .new(
|
||||||
|
listController: listController.value,
|
||||||
|
scrollController: scrollController,
|
||||||
|
hasMore: hasMore.value,
|
||||||
|
loadOlder: loadOlderItems,
|
||||||
|
jumpToId: jumpToId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ class const ChatPage({super.key}) extends ConsumerWidget {
|
||||||
if (isDesktop) Sidebar(isDesktop: isDesktop),
|
if (isDesktop) Sidebar(isDesktop: isDesktop),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RoomChat(
|
child: RoomChat(
|
||||||
|
key: ValueKey(roomId),
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
showMembersByDefault: showMembersByDefault,
|
showMembersByDefault: showMembersByDefault,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/member_list_opened.dart";
|
import "package:nexus/controllers/member_list_opened.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/room_chat.dart";
|
import "package:nexus/controllers/room_chat.dart";
|
||||||
|
import "package:nexus/helpers/hooks/chat_scroll.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/composer.dart";
|
import "package:nexus/widgets/composer/composer.dart";
|
||||||
|
|
@ -16,7 +17,6 @@ import "package:nexus/widgets/pinned_events_drawer.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:super_sliver_list/super_sliver_list.dart";
|
|
||||||
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
import "package:nexus/widgets/room_chat/chat_timeline.dart";
|
||||||
import "package:nexus/helpers/extensions/build_event_options.dart";
|
import "package:nexus/helpers/extensions/build_event_options.dart";
|
||||||
|
|
||||||
|
|
@ -64,123 +64,27 @@ final class const RoomChat({
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
final listController = useRef(ListController());
|
|
||||||
final scrollController = useScrollController();
|
|
||||||
final controllerData = ref.watch(controllerProvider);
|
final controllerData = ref.watch(controllerProvider);
|
||||||
|
|
||||||
final topEventBeforeLoad = useState<String?>(null);
|
final scroll = ChatScroll.use(
|
||||||
final hasMore = useState<bool>(true);
|
controllerData: controllerData,
|
||||||
final loadingOlder = useRef(false);
|
id: (event) => event.eventId,
|
||||||
|
loadOlder: notifier.loadOlder,
|
||||||
Future<void> jumpToId(String eventId) async {
|
shouldLoadOlder: () => ref.read(
|
||||||
final index =
|
RoomsController.provider.select(
|
||||||
controllerData.value?.indexWhere(
|
(rooms) => rooms[roomId]?.hasMore ?? false,
|
||||||
(element) => element.eventId == eventId,
|
),
|
||||||
) ??
|
),
|
||||||
-1;
|
onReachedBottom: () async {
|
||||||
if (index == -1) return;
|
|
||||||
|
|
||||||
listController.value.animateToItem(
|
|
||||||
index: index,
|
|
||||||
scrollController: scrollController,
|
|
||||||
alignment: 0.5,
|
|
||||||
duration: (_) => .new(milliseconds: 700),
|
|
||||||
curve: (_) => Curves.easeInOut,
|
|
||||||
);
|
|
||||||
highlightedEvent.value = eventId;
|
|
||||||
await Future.delayed(.new(seconds: 1), () {
|
|
||||||
if (highlightedEvent.value == eventId) {
|
|
||||||
highlightedEvent.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadOlder() async {
|
|
||||||
if (loadingOlder.value || !hasMore.value) return;
|
|
||||||
if (controllerData case AsyncData(:final value?)) {
|
|
||||||
loadingOlder.value = true;
|
|
||||||
topEventBeforeLoad.value = value.firstOrNull?.eventId;
|
|
||||||
try {
|
|
||||||
hasMore.value = await notifier.loadOlder();
|
|
||||||
} finally {
|
|
||||||
loadingOlder.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
ref
|
|
||||||
.read(controllerProvider.future)
|
|
||||||
.then(
|
|
||||||
(_) => WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (scrollController.hasClients) {
|
|
||||||
scrollController.jumpTo(
|
|
||||||
scrollController.position.maxScrollExtent - .000001,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
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<void> listener() async {
|
|
||||||
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final room = ref.read(
|
final room = ref.read(
|
||||||
RoomsController.provider.select((value) => value[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
);
|
);
|
||||||
if (room == null) return;
|
|
||||||
|
|
||||||
if (scrollController.position.pixels == 0) {
|
if (room != null) {
|
||||||
if (room.hasMore) {
|
|
||||||
await loadOlder();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await client.markRead(room);
|
await client.markRead(room);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
);
|
||||||
scrollController.addListener(listener);
|
|
||||||
return () => scrollController.removeListener(listener);
|
|
||||||
}, [roomId, controllerData]);
|
|
||||||
|
|
||||||
final composerNode = useFocusNode(
|
final composerNode = useFocusNode(
|
||||||
onKeyEvent: (_, event) {
|
onKeyEvent: (_, event) {
|
||||||
|
|
@ -193,6 +97,15 @@ final class const RoomChat({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<void> jumpToId(String eventId) async {
|
||||||
|
highlightedEvent.value = eventId;
|
||||||
|
|
||||||
|
await scroll.jumpToId(eventId);
|
||||||
|
await Future.delayed(.new(seconds: 1), () {
|
||||||
|
if (highlightedEvent.value == eventId) highlightedEvent.value = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
IList<PopupMenuEntry> getEventOptions(Event event) =>
|
IList<PopupMenuEntry> getEventOptions(Event event) =>
|
||||||
event.buildEventOptions(
|
event.buildEventOptions(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -237,10 +150,10 @@ final class const RoomChat({
|
||||||
padding: .symmetric(horizontal: 4),
|
padding: .symmetric(horizontal: 4),
|
||||||
child: ChatTimeline(
|
child: ChatTimeline(
|
||||||
controllerData: controllerData,
|
controllerData: controllerData,
|
||||||
scrollController: scrollController,
|
scrollController: scroll.scrollController,
|
||||||
listController: listController.value,
|
listController: scroll.listController,
|
||||||
hasMore: hasMore.value,
|
hasMore: scroll.hasMore,
|
||||||
loadOlder: loadOlder,
|
loadOlder: scroll.loadOlder,
|
||||||
jumpToId: jumpToId,
|
jumpToId: jumpToId,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
highlightedEvent: highlightedEvent.value,
|
highlightedEvent: highlightedEvent.value,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue