diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 4043922..7e1aeb2 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -17,13 +17,13 @@ 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, String? withEventId) info) +class RoomChatController(final (String roomId, String? contextualEvent) info) extends AsyncNotifier { @override Future build() async { final (roomId, eventId) = info; final client = ref.read(ClientController.provider.notifier); - final room = ref.watch( + final room = ref.read( RoomsController.provider.select((rooms) => rooms[roomId]), ); @@ -34,31 +34,43 @@ class RoomChatController(final (String roomId, String? withEventId) info) await ref.read(RoomsController.provider.notifier).addState(roomId, state); } - 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 timeline = 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(), - hasMore: room.hasMore, - ); + return editedEvent == null + ? foundEvent + : foundEvent?.copyWith( + content: editedEvent.content, + localContent: editedEvent.localContent, + ); + }) + .nonNulls + .toIList(); + + if (info.$2 == null || timeline.map((e) => e.eventId).contains(info.$2)) { + ref.watch(RoomsController.provider.select((rooms) => rooms[roomId])); + + return .new(timeline: timeline, hasMore: room.hasMore); + } else { + final context = await client.getEventContext( + .new(roomId: roomId, eventId: info.$2!, limit: 20), + ); + return .new( + timeline: context.before.add(context.event).addAll(context.after), + hasMore: true, + historicalData: .new(start: context.start, end: context.end), + ); + } } Future deleteMessage(Event event, {String? reason}) => ref diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index 9efd3b2..065a215 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -32,8 +32,8 @@ final class ChatScroll({ useEffect(() { if (anchorId.value == null) { if (controllerData case AsyncData(:final value?) - when value.events.isNotEmpty) { - anchorId.value = value.events.last.eventId; + when value.timeline.isNotEmpty) { + anchorId.value = value.timeline.last.eventId; } } @@ -41,7 +41,7 @@ final class ChatScroll({ }, [controllerData]); final ({IList history, IList live}) split = useMemoized(() { - final items = controllerData.value?.events; + final items = controllerData.value?.timeline; final anchor = anchorIdValue; if (items == null || anchor == null) { @@ -92,19 +92,14 @@ final class ChatScroll({ (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 - final offset = historyListController.value.getOffsetToReveal( + offset = historyListController.value.getOffsetToReveal( historyIndex, 0.5, ); - - await scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 700), - curve: Curves.easeInOut, - ); } else { final liveIndex = split.live.indexWhere( (item) => item.eventId == itemId, @@ -112,18 +107,21 @@ final class ChatScroll({ if (liveIndex != -1) { // ignore: invalid_use_of_visible_for_testing_member - final offset = liveListController.value.getOffsetToReveal( - liveIndex, - 0.5, - ); - - await scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 700), - curve: Curves.easeInOut, - ); + offset = liveListController.value.getOffsetToReveal(liveIndex, 0.5); } } + + if (offset == null) { + // not in current timeline + } + + if (offset != null) { + await scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 700), + curve: Curves.easeInOut, + ); + } } return .new( diff --git a/lib/models/event_context.dart b/lib/models/event_context.dart index e7b1cb1..6103c7d 100644 --- a/lib/models/event_context.dart +++ b/lib/models/event_context.dart @@ -8,7 +8,7 @@ part "event_context.g.dart"; @Freezed(toJson: false, fromJson: false) @JsonSerializable() class const EventContext({ - required final IList events, + required final Event event, required final IList before, required final IList after, required final String start, diff --git a/lib/models/room.dart b/lib/models/room.dart index 6d7553f..96b29b4 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -12,17 +12,26 @@ part "room.g.dart"; class const Room({ @JsonKey(name: "meta") final RoomMetadata? metadata, + /// [timeline] is an IMap of timelineRowId to eventRowId @JsonKey(fromJson: Room.timelineTupleJsonToIMap) final IMap timeline = const IMap.empty(), + /// [clientSticky] is an ISet of eventRowId + @JsonKey(includeFromJson: false, includeToJson: false) final ISet clientSticky = const ISet.empty(), + /// [events] is an IMap of eventRowId to event @JsonKey(fromJson: Room.eventsJsonToIMap) final IMap events = const IMap.empty(), final bool reset = false, + + @JsonKey(includeFromJson: false, includeToJson: false) final bool hasFetchedState = false, + + @JsonKey(includeFromJson: false, includeToJson: false) final bool hasFetchedMembers = false, + final IMap> state = const IMap.empty(), final IMap> receipts = const IMap.empty(), @@ -32,9 +41,6 @@ class const Room({ // IMap accountData, // IList notifications, }) with _$Room { - /// [timeline] is an IMap of timelineRowId to eventRowId - /// [events] is an IMap of eventRowId to event - /// [clientSticky] is an ISet of eventRowId static IMap timelineTupleJsonToIMap(List json) => IMap.fromEntries( json.map( diff --git a/lib/models/room_chat.dart b/lib/models/room_chat.dart index e952d42..6044909 100644 --- a/lib/models/room_chat.dart +++ b/lib/models/room_chat.dart @@ -8,7 +8,7 @@ part "room_chat.g.dart"; @Freezed(toJson: false, fromJson: false) @JsonSerializable() class const RoomChat({ - required final IList events, + required final IList timeline, required final bool hasMore, final HistoricalData? historicalData, }) with _$RoomChat { @@ -21,8 +21,8 @@ class const RoomChat({ @Freezed(toJson: false, fromJson: false) @JsonSerializable() class const HistoricalData({ - required final String end, required final String start, + required final String end, }) with _$HistoricalData { Map toJson() => _$HistoricalDataToJson(this); diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index 186b2a6..2aee4a5 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -31,6 +31,7 @@ final class const RoomChat({ Widget build(BuildContext context, WidgetRef ref) { final relatedEvent = useState(null); final relationType = useState(RelationType.reply); + final contextualEvent = useState(initialHighlightedEvent); final highlightedEvent = useState(initialHighlightedEvent); final composerSize = useState(64); @@ -62,7 +63,7 @@ final class const RoomChat({ final controllerProvider = RoomChatController.provider(( roomId, - initialHighlightedEvent, + contextualEvent.value, )); final notifier = ref.watch(controllerProvider.notifier);