diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index 5ee3449..67adb9f 100644 --- a/lib/controllers/notification.dart +++ b/lib/controllers/notification.dart @@ -37,6 +37,7 @@ class NotificationController MaterialPageRoute( builder: (_) => NotificationsPage( highlightedEventId: eventId, + jumpToEvent: (eventId) {}, defaultToAllNotifications: true, ), ), @@ -62,6 +63,7 @@ class NotificationController MaterialPageRoute( builder: (_) => NotificationsPage( highlightedEventId: eventId, + jumpToEvent: (eventId) {}, defaultToAllNotifications: true, ), ), diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart index bea65b9..3b1531eb 100644 --- a/lib/helpers/hooks/chat_scroll.dart +++ b/lib/helpers/hooks/chat_scroll.dart @@ -68,6 +68,14 @@ final class ChatScroll({ if (found || controllerData is AsyncError) { if (found) { anchorId.value = target; + + WidgetsBinding.instance.addPostFrameCallback((_) { + final context = keyFor(target).currentContext; + if (context != null && context.mounted) { + anchorMountedCompleter.value?.complete(context); + anchorMountedCompleter.value = null; + } + }); } else { anchorMountedCompleter.value?.completeError( StateError("Failed to load context for $target"), @@ -80,21 +88,6 @@ final class ChatScroll({ return null; }, [controllerData, pendingAnchorTarget.value]); - useEffect(() { - final completer = anchorMountedCompleter.value; - if (completer == null) return null; - - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = keyFor(anchorId.value!).currentContext; - if (context != null && context.mounted) { - anchorMountedCompleter.value?.complete(context); - anchorMountedCompleter.value = null; - } - }); - - return null; - }, [anchorId.value]); - final ({IList history, IList live}) split = useMemoized(() { final items = controllerData.value?.timeline; final anchor = anchorId.value; diff --git a/lib/widgets/pages/chat.dart b/lib/widgets/pages/chat.dart index 279f077..7cfec97 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/widgets/pages/chat.dart @@ -1,3 +1,4 @@ +import "package:flutter_hooks/flutter_hooks.dart"; import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/init_complete.dart"; @@ -7,45 +8,63 @@ import "package:nexus/widgets/sidebar.dart"; import "package:nexus/widgets/room_chat/room_chat.dart"; import "package:nexus/widgets/loading.dart"; -class const ChatPage({super.key}) extends ConsumerWidget { +class const ChatPage({super.key}) extends HookConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( - builder: (context, constraints) { - final isDesktop = constraints.maxWidth > 650; - final showMembersByDefault = constraints.maxWidth > 1000; - final initComplete = ref.watch(InitCompleteController.provider); - final roomId = ref - .watch(KeyController.provider(KeyController.roomKey)) - .requireValue; + Widget build(BuildContext context, WidgetRef ref) { + final initialHighlightedEvent = useState(null); - return SafeArea( - child: Scaffold( - appBar: initComplete ? null : Appbar(), - body: initComplete - ? Row( - children: [ - if (isDesktop) Sidebar(isDesktop: isDesktop), - Expanded( - child: RoomChat( - key: ValueKey(roomId), - roomId: roomId, - isDesktop: isDesktop, - showMembersByDefault: showMembersByDefault, + return LayoutBuilder( + builder: (context, constraints) { + final isDesktop = constraints.maxWidth > 650; + final showMembersByDefault = constraints.maxWidth > 1000; + final initComplete = ref.watch(InitCompleteController.provider); + final roomId = ref + .watch(KeyController.provider(KeyController.roomKey)) + .requireValue; + + return SafeArea( + child: Scaffold( + appBar: initComplete ? null : Appbar(), + body: initComplete + ? Row( + children: [ + if (isDesktop) + Sidebar( + isDesktop: isDesktop, + jumpToEvent: (eventId) => + initialHighlightedEvent.value = eventId, + ), + Expanded( + child: RoomChat( + key: ValueKey(( + roomId, + initialHighlightedEvent.value, + )), + roomId: roomId, + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, + initialHighlightedEvent: + initialHighlightedEvent.value, + ), ), + ], + ) + : Center( + child: Column( + mainAxisSize: .min, + children: [Loading(), Text("Syncing...")], ), - ], - ) - : Center( - child: Column( - mainAxisSize: .min, - children: [Loading(), Text("Syncing...")], ), - ), - drawer: isDesktop || !initComplete - ? null - : Sidebar(isDesktop: isDesktop), - ), - ); - }, - ); + drawer: isDesktop || !initComplete + ? null + : Sidebar( + isDesktop: isDesktop, + jumpToEvent: (eventId) => + initialHighlightedEvent.value = eventId, + ), + ), + ); + }, + ); + } } diff --git a/lib/widgets/pages/notifications.dart b/lib/widgets/pages/notifications.dart index 3e70104..56fee20 100644 --- a/lib/widgets/pages/notifications.dart +++ b/lib/widgets/pages/notifications.dart @@ -1,10 +1,13 @@ import "dart:async"; +import "package:collection/collection.dart"; import "package:m3e_buttons/m3e_buttons.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/notifications.dart"; +import "package:nexus/controllers/spaces.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/error_dialog.dart"; @@ -16,6 +19,7 @@ import "package:super_sliver_list/super_sliver_list.dart"; class const NotificationsPage({ final String? highlightedEventId, final bool defaultToAllNotifications = false, + required final void Function(String eventId) jumpToEvent, super.key, }) extends HookConsumerWidget { @override @@ -127,9 +131,48 @@ class const NotificationsPage({ child: HighlightWrapper( InkWell( onTap: () { - // TODO: Jump to event + final spaces = ref.read( + SpacesController.provider, + ); + final space = spaces.firstWhereOrNull( + (space) => + space.children.any( + (room) => + room.metadata?.id == + event.roomId, + ) || + space.subSpaces.any( + (subSpace) => + subSpace.children.any( + (room) => + room.metadata?.id == + event.roomId, + ), + ), + ); + if (space == null) return; + + ref + .read( + KeyController.provider( + KeyController.spaceKey, + ).notifier, + ) + .set(space.id); + ref + .read( + KeyController.provider( + KeyController.roomKey, + ).notifier, + ) + .set(event.roomId); + jumpToEvent(event.eventId); + + Navigator.of(context).pop(); }, - child: EventRenderer(event), + child: IgnorePointer( + child: EventRenderer(event), + ), ), isHighlighted: isHighlighted, ), diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index f58bda0..709d22d 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -32,7 +32,7 @@ final class const RoomChat({ final relatedEvent = useState(null); final relationType = useState(RelationType.reply); final contextualEvent = useState(initialHighlightedEvent); - final highlightedEvent = useState(initialHighlightedEvent); + final highlightedEvent = useState(null); final composerSize = useState(64); @@ -106,6 +106,24 @@ final class const RoomChat({ }); } + useEffect(() { + if (initialHighlightedEvent == null) return null; + + void check() { + if (!context.mounted) return; + + if (scroll.scrollController.hasClients) { + jumpToId(initialHighlightedEvent!); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) => check()); + } + } + + check(); + + return null; + }, [initialHighlightedEvent]); + IList getEventOptions(Event event) => event.buildEventOptions( context: context, diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 6689ce2..b691cd5 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -16,8 +16,11 @@ import "package:nexus/widgets/room_menu.dart"; // Needed for navigation_rail_m3e (#65). import "package:flutter/material.dart" as old_mat; -class const Sidebar({required final bool isDesktop, super.key}) - extends HookConsumerWidget { +class const Sidebar({ + required final bool isDesktop, + required final void Function(String eventId) jumpToEvent, + super.key, +}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedSpaceProvider = KeyController.provider( @@ -194,7 +197,8 @@ class const Sidebar({required final bool isDesktop, super.key}) tooltip: "Open notifications", onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => NotificationsPage(), + builder: (_) => + NotificationsPage(jumpToEvent: jumpToEvent), ), ), icon: Icon(Icons.notifications),