diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 7b076f7..2db473e 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -15,11 +15,11 @@ import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/room.dart"; +import "package:nexus/models/room_chat.dart"; -class RoomChatController(final String roomId) - extends AsyncNotifier?> { +class RoomChatController(final String roomId) extends AsyncNotifier { @override - Future?> build() async { + Future build() async { final client = ref.read(ClientController.provider.notifier); final room = ref.watch( RoomsController.provider.select((rooms) => rooms[roomId]), @@ -32,28 +32,31 @@ class RoomChatController(final String roomId) await ref.read(RoomsController.provider.notifier).addState(roomId, state); } - return room.timeline - .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) - .map((element) => element.value) - .toIList() - .addAll(room.clientSticky) - .map((entry) { - final foundEvent = entry == null ? null : room.events[entry]; + return .new( + events: room.timeline + .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) + .map((element) => element.value) + .toIList() + .addAll(room.clientSticky) + .map((entry) { + final foundEvent = entry == null ? null : room.events[entry]; - final editedEvent = - foundEvent == null || foundEvent.lastEditRowId == 0 - ? null - : room.events[foundEvent.lastEditRowId]; + final editedEvent = + foundEvent == null || foundEvent.lastEditRowId == 0 + ? null + : room.events[foundEvent.lastEditRowId]; - return editedEvent == null - ? foundEvent - : foundEvent?.copyWith( - content: editedEvent.content, - localContent: editedEvent.localContent, - ); - }) - .nonNulls - .toIList(); + return editedEvent == null + ? foundEvent + : foundEvent?.copyWith( + content: editedEvent.content, + localContent: editedEvent.localContent, + ); + }) + .nonNulls + .toIList(), + hasMore: room.hasMore, + ); } Future deleteMessage(Event event, {String? reason}) => ref @@ -67,7 +70,7 @@ class RoomChatController(final String roomId) ); Future loadOlder() async { - if (state.isLoading) return; + if (state.isLoading || state.value?.hasMore == false) return; state = .loading(); @@ -224,7 +227,7 @@ class RoomChatController(final String roomId) } static final provider = AsyncNotifierProvider.family - .autoDispose?, String>( + .autoDispose( RoomChatController.new, ); } diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index efc023e..d8bfae8 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -2,22 +2,23 @@ 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:nexus/models/event.dart"; +import "package:nexus/models/room_chat.dart"; import "package:super_sliver_list/super_sliver_list.dart"; -final class ChatScroll({ - required final IList historyItems, - required final IList liveItems, +final class ChatScroll({ + required final IList historyItems, + required final IList liveItems, required final GlobalKey centerKey, required final ListController historyListController, required final ListController liveListController, required final ScrollController scrollController, required final Future Function(String id) jumpToId, }) { - static ChatScroll use({ - required AsyncValue?> controllerData, - required String Function(T item) id, + static ChatScroll use({ + required AsyncValue controllerData, required Future Function() loadOlder, - required Future Function() onReachedBottom, + required Future Function() markRead, }) { final historyListController = useRef(ListController()); final liveListController = useRef(ListController()); @@ -31,23 +32,23 @@ final class ChatScroll({ useEffect(() { if (anchorId.value == null) { if (controllerData case AsyncData(:final value?) - when value.isNotEmpty) { - anchorId.value = id(value.last); + when value.events.isNotEmpty) { + anchorId.value = value.events.last.eventId; } } return null; }, [controllerData]); - final ({IList history, IList live}) split = useMemoized(() { - final items = controllerData.value; + final ({IList history, IList live}) split = useMemoized(() { + final items = controllerData.value?.events; final anchor = anchorIdValue; if (items == null || anchor == null) { return (history: const .empty(), live: const .empty()); } - final anchorIndex = items.indexWhere((item) => id(item) == anchor); + final anchorIndex = items.indexWhere((item) => item.eventId == anchor); if (anchorIndex == -1) { return (history: const .empty(), live: items); @@ -71,7 +72,7 @@ final class ChatScroll({ if (position.extentAfter <= topThreshold) { await loadOlder(); } else if (position.extentBefore <= bottomThreshold) { - await onReachedBottom(); + await markRead(); } } @@ -82,13 +83,13 @@ final class ChatScroll({ return () { scrollController.removeListener(checkPosition); }; - }, [scrollController, controllerData, loadOlder, onReachedBottom]); + }, [scrollController, controllerData, loadOlder, markRead]); Future jumpToId(String itemId) async { if (!scrollController.hasClients) return; final historyIndex = split.history.indexWhere( - (item) => id(item) == itemId, + (item) => item.eventId == itemId, ); if (historyIndex != -1) { @@ -105,7 +106,9 @@ final class ChatScroll({ curve: Curves.easeInOut, ); } else { - final liveIndex = split.live.indexWhere((item) => id(item) == itemId); + final liveIndex = split.live.indexWhere( + (item) => item.eventId == itemId, + ); if (liveIndex != -1) { // ignore: invalid_use_of_visible_for_testing_member diff --git a/lib/models/room_chat.dart b/lib/models/room_chat.dart new file mode 100644 index 0000000..e952d42 --- /dev/null +++ b/lib/models/room_chat.dart @@ -0,0 +1,31 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/event.dart"; + +part "room_chat.freezed.dart"; +part "room_chat.g.dart"; + +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const RoomChat({ + required final IList events, + required final bool hasMore, + final HistoricalData? historicalData, +}) with _$RoomChat { + Map toJson() => _$RoomChatToJson(this); + + factory RoomChat.fromJson(Map json) => + _$RoomChatFromJson(json); +} + +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const HistoricalData({ + required final String end, + required final String start, +}) with _$HistoricalData { + Map toJson() => _$HistoricalDataToJson(this); + + factory HistoricalData.fromJson(Map json) => + _$HistoricalDataFromJson(json); +} diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index 434145c..3bc601c 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -8,7 +8,7 @@ import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const ChatTimeline({ - required final ChatScroll scroll, + required final ChatScroll scroll, required final Future Function(String) jumpToId, required final IList Function(Event) getEventOptions, required final String? highlightedEvent, diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index 951d9e3..9d81c16 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -68,9 +68,8 @@ final class const RoomChat({ final scroll = ChatScroll.use( controllerData: controllerData, - id: (event) => event.eventId, loadOlder: notifier.loadOlder, - onReachedBottom: () async { + markRead: () async { final room = ref.read( RoomsController.provider.select((rooms) => rooms[roomId]), );