diff --git a/lib/controllers/contextual_event.dart b/lib/controllers/contextual_event.dart index 2bc6b3a..42c5a26 100644 --- a/lib/controllers/contextual_event.dart +++ b/lib/controllers/contextual_event.dart @@ -1,17 +1,16 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/event.dart"; -class ContextualEventController(String? _) extends Notifier { +class ContextualEventController(String? _) extends Notifier { @override - Event? build() => null; + int? build() => null; - void set(Event? event) => state = event; + void set(int? event) => state = event; @override bool updateShouldNotify(_, _) => true; static final provider = NotifierProvider.family - .autoDispose( + .autoDispose( ContextualEventController.new, ); } diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index d93775e..04cb5ae 100644 --- a/lib/controllers/notification.dart +++ b/lib/controllers/notification.dart @@ -31,12 +31,14 @@ class NotificationController macOS: darwin, ), onDidReceiveNotificationResponse: (details) { - if (details.payload case final eventId?) { + if (details.payload == null) return; + + if (int.tryParse(details.payload!) case final rowId?) { if (navigatorKey.currentContext case final context?) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => NotificationsPage( - highlightedEventId: eventId, + highlightedEvent: rowId, defaultToAllNotifications: true, ), ), @@ -55,17 +57,17 @@ class NotificationController return; } - final eventId = call.arguments as String; - - if (navigatorKey.currentContext case final context?) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsPage( - highlightedEventId: eventId, - defaultToAllNotifications: true, + if (int.tryParse(call.arguments) case int rowId) { + if (navigatorKey.currentContext case final context?) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => NotificationsPage( + highlightedEvent: rowId, + defaultToAllNotifications: true, + ), ), - ), - ); + ); + } } }); diff --git a/lib/controllers/notifications.dart b/lib/controllers/notifications.dart index da4312d..33e1cb3 100644 --- a/lib/controllers/notifications.dart +++ b/lib/controllers/notifications.dart @@ -1,33 +1,61 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/event.dart"; typedef NotificationsRequest = (UnreadType? unreadType, String? roomId); class NotificationsController([final NotificationsRequest? request]) - extends AsyncNotifier> { + extends AsyncNotifier> { + static const virtualRoomId = "!notifications"; + @override - Future> build() async { + Future> build() async { final client = ref.read(ClientController.provider.notifier); final (unreadType, roomId) = request ?? (null, null); - return await client.getMentions( + final mentions = await client.getMentions( .new( maxTimestamp: .now(), unreadType: unreadType ?? .highlight, roomId: roomId, ), ); + + ref + .watch(RoomsController.provider.notifier) + .update( + .new({ + virtualRoomId: .new( + events: IMap.fromIterable( + mentions, + keyMapper: (event) => event.rowId, + ), + ), + }), + .new(), + ); + + return .new(mentions.map((event) => (event.rowId, event.roomId))); } Future loadOlder() async { final currentNotifications = await future; state = .loading(); state = await .guard(() async { - final lastTs = currentNotifications.lastOrNull?.timestamp; - if (lastTs == null) return const .empty(); + final lastNotification = currentNotifications.lastOrNull?.$1; + final lastTs = lastNotification == null + ? null + : ref.watch( + RoomsController.provider.select( + (rooms) => + rooms[virtualRoomId]?.events[lastNotification]?.timestamp, + ), + ); + + if (lastTs == null) return currentNotifications; final client = ref.read(ClientController.provider.notifier); final (unreadType, roomId) = request ?? (null, null); @@ -40,14 +68,30 @@ class NotificationsController([final NotificationsRequest? request]) ), ); - return currentNotifications.addAll(newNotifications); + ref + .watch(RoomsController.provider.notifier) + .update( + .new({ + virtualRoomId: .new( + events: IMap.fromIterable( + newNotifications, + keyMapper: (event) => event.rowId, + ), + ), + }), + .new(), + ); + + return currentNotifications.addAll( + newNotifications.map((event) => (event.rowId, event.roomId)), + ); }); } static final provider = AsyncNotifierProvider.family .autoDispose< NotificationsController, - IList, + IList<(int, String)>, NotificationsRequest? >(NotificationsController.new); } diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 60de47a..b100f93 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -18,10 +18,7 @@ import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/room_chat.dart"; -typedef ChatParams = ({ - String roomId, - ({int rowId, String eventId})? contextualEvent, -}); +typedef ChatParams = ({String roomId, int? contextualEvent}); class RoomChatController(final ChatParams info) extends AsyncNotifier { @@ -47,7 +44,7 @@ class RoomChatController(final ChatParams info) .nonNulls .toIList(); if (info.contextualEvent == null || - timeline.contains(info.contextualEvent!.rowId)) { + timeline.contains(info.contextualEvent)) { ref.watch( RoomsController.provider.select( (rooms) => @@ -62,7 +59,10 @@ class RoomChatController(final ChatParams info) ); } else { final context = await client.getEventContext( - .new(roomId: info.roomId, eventId: info.contextualEvent!.eventId), + .new( + roomId: info.roomId, + eventId: room.events[info.contextualEvent!]!.eventId, + ), ); final events = context.before.add(context.event).addAll(context.after); diff --git a/lib/controllers/unified_push.dart b/lib/controllers/unified_push.dart index db99868..cc0fce0 100644 --- a/lib/controllers/unified_push.dart +++ b/lib/controllers/unified_push.dart @@ -91,7 +91,7 @@ class UnifiedPushController extends AsyncNotifier { id: event.eventId.hashCode & 0x7fffffff, title: roomMetadata.name ?? "New Event", icon: icon, - payload: event.eventId, + payload: event.rowId.toString(), body: switch (event.content) { MessageContent(:final body?) || StickerContent(:final body) => body, diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index 856f0e0..faaa887 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -5,16 +5,17 @@ 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/direction.dart"; -import "package:nexus/models/event.dart"; import "package:nexus/models/room_chat.dart"; +typedef JumpToEvent = Future Function(int rowId); + final class ChatScroll({ required final IList historyRows, required final IList liveRows, required final GlobalKey centerKey, required final ScrollController scrollController, required final bool atBottom, - required final Future Function(Event event) jumpToEvent, + required final JumpToEvent jumpToEvent, required final Future Function() jumpToBottom, required final GlobalKey Function(int eventRowId) keyFor, }) { @@ -22,7 +23,7 @@ final class ChatScroll({ required AsyncValue controllerData, required Future Function(Direction direction) paginate, required Future Function() markRead, - required ValueNotifier contextualEvent, + required ValueNotifier contextualEvent, }) { final anchorId = useState(null); @@ -43,11 +44,11 @@ final class ChatScroll({ if (controllerData case AsyncData(:final value?) when value.timeline.isNotEmpty) { final hasContextualEvent = value.timeline.any( - (event) => event == contextualEvent.value?.rowId, + (event) => event == contextualEvent.value, ); anchorId.value = hasContextualEvent - ? contextualEvent.value?.rowId + ? contextualEvent.value : value.timeline.last; } } @@ -157,10 +158,10 @@ final class ChatScroll({ centerKey: centerKey, scrollController: scrollController, atBottom: atBottom.value, - jumpToEvent: (Event event) async { + jumpToEvent: (int rowId) async { if (!scrollController.hasClients) return; - final existing = keyFor(event.rowId).currentContext; + final existing = keyFor(rowId).currentContext; if (existing != null && existing.mounted) { // Already mounted, just scroll await Scrollable.ensureVisible( @@ -172,8 +173,8 @@ final class ChatScroll({ } else { final completer = Completer(); anchorMountedCompleter.value = completer; - pendingAnchorTarget.value = event.rowId; - contextualEvent.value = event; + pendingAnchorTarget.value = rowId; + contextualEvent.value = rowId; final context = await completer.future; if (!context.mounted) return; diff --git a/lib/widgets/pages/chat.dart b/lib/widgets/pages/chat.dart index 52e5cc8..0a57351 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/widgets/pages/chat.dart @@ -36,7 +36,7 @@ class const ChatPage({super.key}) extends HookConsumerWidget { key: ValueKey((roomId, initialHighlight)), roomId: roomId, isDesktop: isDesktop, - initialHighlightedEvent: initialHighlight, + initialHighlight: initialHighlight, showMembersByDefault: showMembersByDefault, ); }, diff --git a/lib/widgets/pages/notifications.dart b/lib/widgets/pages/notifications.dart index 269f13f..3347e09 100644 --- a/lib/widgets/pages/notifications.dart +++ b/lib/widgets/pages/notifications.dart @@ -16,7 +16,7 @@ import "package:nexus/widgets/renderers/event.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const NotificationsPage({ - final String? highlightedEventId, + final int? highlightedEvent, final bool defaultToAllNotifications = false, super.key, }) extends HookConsumerWidget { @@ -34,7 +34,7 @@ class const NotificationsPage({ }; final unreadTypeIndex = useState(defaultToAllNotifications ? 1 : 0); - final highlightedId = useState(highlightedEventId); + final highlightedId = useState(highlightedEvent); final listController = useRef(ListController()); final scrollController = useScrollController(); @@ -56,7 +56,7 @@ class const NotificationsPage({ final notifications = await ref.watch(provider.future); final index = notifications.indexWhere( - (element) => element.eventId == highlightedId.value!, + (tuple) => tuple.$1 == highlightedId.value!, ); if (index == -1) return; @@ -120,23 +120,23 @@ class const NotificationsPage({ ), reverse: true, itemBuilder: (context, index) { - final event = value[index]; + final (rowId, roomId) = value[index]; final isHighlighted = - event.eventId == highlightedId.value; + rowId == highlightedId.value; return Padding( padding: .only(top: 8), child: HighlightWrapper( InkWell( onTap: () async { - await ref.focusRoom(event.roomId); + await ref.focusRoom(roomId); ref .watch( ContextualEventController.provider( - event.roomId, + roomId, ).notifier, ) - .set(event); + .set(rowId); if (context.mounted) { Navigator.of(context).pop(); @@ -144,8 +144,9 @@ class const NotificationsPage({ }, child: IgnorePointer( child: EventRenderer( - event.rowId, - roomId: event.roomId, + rowId, + roomId: NotificationsController + .virtualRoomId, ), ), ), diff --git a/lib/widgets/pinned_events_drawer.dart b/lib/widgets/pinned_events_drawer.dart index cb91985..f6bf9d2 100644 --- a/lib/widgets/pinned_events_drawer.dart +++ b/lib/widgets/pinned_events_drawer.dart @@ -2,6 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/pinned_events.dart"; +import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; @@ -10,7 +11,7 @@ import "package:nexus/widgets/renderers/event.dart"; final class const PinnedEventsDrawer( final String roomId, { required final IList Function(Event event) getEventOptions, - required final Future Function(Event event) jumpToEvent, + required final JumpToEvent jumpToEvent, super.key, }) extends HookConsumerWidget { @override @@ -62,7 +63,7 @@ final class const PinnedEventsDrawer( borderRadius: .circular(12), onTap: () { Navigator.of(context).pop(); - jumpToEvent(event); + jumpToEvent(event.rowId); }, child: Padding( padding: .symmetric(vertical: 4), diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index f5ffe8b..c295510 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -6,6 +6,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; +import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/canonical_alias.dart"; import "package:nexus/models/content/content.dart"; @@ -35,7 +36,7 @@ class const EventRenderer( final int? previousEventRowId, final bool textOnly = false, final int? maxLines, - final Future Function(Event event)? jumpToEvent, + final JumpToEvent? jumpToEvent, final IList Function(Event event)? getEventOptions, super.key, }) extends HookConsumerWidget { @@ -106,7 +107,8 @@ class const EventRenderer( .new(roomId: roomId, eventId: event.replyTo!), ).future, ); - if (replyEvent != null) await jumpToEvent!(replyEvent); + if (replyEvent != null) + await jumpToEvent!(replyEvent.rowId); }, isGrouped: isGrouped, maxLines: maxLines, diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart index 6ae61eb..254c782 100644 --- a/lib/widgets/room_chat/chat_timeline.dart +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -9,7 +9,7 @@ import "package:super_sliver_list/super_sliver_list.dart"; class const ChatTimeline({ required final String roomId, required final ChatScroll scroll, - required final Future Function(Event event) jumpToEvent, + required final JumpToEvent jumpToEvent, required final IList Function(Event) getEventOptions, required final int? highlightedEvent, required final double composerHeight, diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index ab4d35d..45c7e8a 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -24,14 +24,14 @@ final class const RoomChat({ required final String? roomId, required final bool isDesktop, required final bool showMembersByDefault, - final Event? initialHighlightedEvent, + final int? initialHighlight, super.key, }) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final relatedEvent = useState(null); final relationType = useState(RelationType.reply); - final contextualEvent = useState(initialHighlightedEvent); + final contextualEvent = useState(initialHighlight); final highlightedEvent = useState(null); final composerSize = useState(64); @@ -63,12 +63,7 @@ final class const RoomChat({ final controllerProvider = RoomChatController.provider(( roomId: roomId, - contextualEvent: contextualEvent.value == null - ? null - : ( - rowId: contextualEvent.value!.rowId, - eventId: contextualEvent.value!.eventId, - ), + contextualEvent: contextualEvent.value, )); final notifier = ref.watch(controllerProvider.notifier); @@ -102,25 +97,25 @@ final class const RoomChat({ }, ); - Future jumpToEvent(Event event) async { - highlightedEvent.value = event.rowId; + Future jumpToEvent(int rowId) async { + highlightedEvent.value = rowId; - await scroll.jumpToEvent(event); + await scroll.jumpToEvent(rowId); await Future.delayed(.new(milliseconds: 700), () { - if (highlightedEvent.value == event.rowId) { + if (highlightedEvent.value == rowId) { highlightedEvent.value = null; } }); } useEffect(() { - if (initialHighlightedEvent == null) return null; + if (initialHighlight == null) return null; void check() { if (!context.mounted) return; if (scroll.scrollController.hasClients) { - jumpToEvent(initialHighlightedEvent!); + jumpToEvent(initialHighlight!); } else { WidgetsBinding.instance.addPostFrameCallback((_) => check()); } @@ -129,7 +124,7 @@ final class const RoomChat({ check(); return null; - }, [initialHighlightedEvent]); + }, [initialHighlight]); IList getEventOptions(Event event) => event.buildEventOptions(