From 9f070fcdba101a695ea4b3f2afa3d79c347520fc Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 26 Sep 2026 18:06:51 -0400 Subject: [PATCH 1/3] reorganize pages into being under widgets --- lib/controllers/notification.dart | 2 +- lib/main.dart | 8 ++++---- lib/{ => widgets}/pages/chat.dart | 0 lib/{ => widgets}/pages/notifications.dart | 0 lib/{ => widgets}/pages/select_server.dart | 2 +- lib/{ => widgets}/pages/settings.dart | 2 +- lib/{ => widgets}/pages/settings_category.dart | 0 lib/{ => widgets}/pages/verify.dart | 2 +- lib/widgets/sidebar.dart | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) rename lib/{ => widgets}/pages/chat.dart (100%) rename lib/{ => widgets}/pages/notifications.dart (100%) rename lib/{ => widgets}/pages/select_server.dart (99%) rename lib/{ => widgets}/pages/settings.dart (99%) rename lib/{ => widgets}/pages/settings_category.dart (100%) rename lib/{ => widgets}/pages/verify.dart (98%) diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index 681c625..5ee34499 100644 --- a/lib/controllers/notification.dart +++ b/lib/controllers/notification.dart @@ -7,7 +7,7 @@ import "package:nexus/controllers/portal.dart"; import "package:nexus/main.dart"; import "package:flutter_local_notifications/flutter_local_notifications.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/pages/notifications.dart"; +import "package:nexus/widgets/pages/notifications.dart"; import "package:xdg_desktop_portal/xdg_desktop_portal.dart"; class NotificationController diff --git a/lib/main.dart b/lib/main.dart index bbc8a4e..5586cbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,10 +14,10 @@ import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/unified_push.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/font_licenses.dart"; -import "package:nexus/pages/chat.dart"; -import "package:nexus/pages/select_server.dart"; -import "package:nexus/pages/settings.dart"; -import "package:nexus/pages/verify.dart"; +import "package:nexus/widgets/pages/chat.dart"; +import "package:nexus/widgets/pages/select_server.dart"; +import "package:nexus/widgets/pages/settings.dart"; +import "package:nexus/widgets/pages/verify.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; diff --git a/lib/pages/chat.dart b/lib/widgets/pages/chat.dart similarity index 100% rename from lib/pages/chat.dart rename to lib/widgets/pages/chat.dart diff --git a/lib/pages/notifications.dart b/lib/widgets/pages/notifications.dart similarity index 100% rename from lib/pages/notifications.dart rename to lib/widgets/pages/notifications.dart diff --git a/lib/pages/select_server.dart b/lib/widgets/pages/select_server.dart similarity index 99% rename from lib/pages/select_server.dart rename to lib/widgets/pages/select_server.dart index a903cf6..34f1a50 100644 --- a/lib/pages/select_server.dart +++ b/lib/widgets/pages/select_server.dart @@ -9,7 +9,7 @@ import "package:nexus/controllers/client_id.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/main.dart"; import "package:nexus/models/homeserver.dart"; -import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/divider_text.dart"; diff --git a/lib/pages/settings.dart b/lib/widgets/pages/settings.dart similarity index 99% rename from lib/pages/settings.dart rename to lib/widgets/pages/settings.dart index ed9dc59..50eb7f0 100644 --- a/lib/pages/settings.dart +++ b/lib/widgets/pages/settings.dart @@ -8,7 +8,7 @@ import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/controllers/settings_sections.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_about_dialog.dart"; -import "package:nexus/pages/settings_category.dart"; +import "package:nexus/widgets/pages/settings_category.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; diff --git a/lib/pages/settings_category.dart b/lib/widgets/pages/settings_category.dart similarity index 100% rename from lib/pages/settings_category.dart rename to lib/widgets/pages/settings_category.dart diff --git a/lib/pages/verify.dart b/lib/widgets/pages/verify.dart similarity index 98% rename from lib/pages/verify.dart rename to lib/widgets/pages/verify.dart index 42a8dfc..a4f95ec 100644 --- a/lib/pages/verify.dart +++ b/lib/widgets/pages/verify.dart @@ -2,7 +2,7 @@ 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/client.dart"; -import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/helpers/required_validator_helper.dart"; diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index b2d8295..6689ce2 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -6,8 +6,8 @@ import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/spaces.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/pages/notifications.dart"; -import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/pages/notifications.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_widget.dart"; import "package:nexus/widgets/join_dialog.dart"; From c90a1c41b01b4f40708d982e62733999a2c77bd5 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 26 Sep 2026 19:13:30 -0400 Subject: [PATCH 2/3] working jumps from notif page Doesn't work from notif page pushed from notif controller --- lib/controllers/notification.dart | 2 + lib/helpers/hooks/chat_scroll.dart | 23 +++---- lib/widgets/pages/chat.dart | 91 +++++++++++++++++----------- lib/widgets/pages/notifications.dart | 47 +++++++++++++- lib/widgets/room_chat/room_chat.dart | 20 +++++- lib/widgets/sidebar.dart | 10 ++- 6 files changed, 136 insertions(+), 57 deletions(-) diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index 5ee34499..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), From 1c1f585362a6bbb6ff3e9b89439318aaccde44c9 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 26 Sep 2026 19:37:47 -0400 Subject: [PATCH 3/3] add JumpToEventController to allow jumping to events from the NotificationsPage when it has been pushed from the NotificationController --- lib/controllers/jump_to_event.dart | 15 +++++ lib/controllers/notification.dart | 2 - lib/widgets/pages/chat.dart | 98 +++++++++++++--------------- lib/widgets/pages/notifications.dart | 10 ++- lib/widgets/sidebar.dart | 10 +-- 5 files changed, 70 insertions(+), 65 deletions(-) create mode 100644 lib/controllers/jump_to_event.dart diff --git a/lib/controllers/jump_to_event.dart b/lib/controllers/jump_to_event.dart new file mode 100644 index 0000000..389f077 --- /dev/null +++ b/lib/controllers/jump_to_event.dart @@ -0,0 +1,15 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; + +class JumpToEventController extends Notifier { + @override + String? build() => null; + + void set(String? eventId) => state = eventId; + + @override + bool updateShouldNotify(_, _) => true; + + static final provider = NotifierProvider( + JumpToEventController.new, + ); +} diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index 67adb9f..5ee34499 100644 --- a/lib/controllers/notification.dart +++ b/lib/controllers/notification.dart @@ -37,7 +37,6 @@ class NotificationController MaterialPageRoute( builder: (_) => NotificationsPage( highlightedEventId: eventId, - jumpToEvent: (eventId) {}, defaultToAllNotifications: true, ), ), @@ -63,7 +62,6 @@ class NotificationController MaterialPageRoute( builder: (_) => NotificationsPage( highlightedEventId: eventId, - jumpToEvent: (eventId) {}, defaultToAllNotifications: true, ), ), diff --git a/lib/widgets/pages/chat.dart b/lib/widgets/pages/chat.dart index 7cfec97..34efe5c 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/widgets/pages/chat.dart @@ -1,7 +1,7 @@ -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"; +import "package:nexus/controllers/jump_to_event.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/sidebar.dart"; @@ -10,61 +10,51 @@ import "package:nexus/widgets/loading.dart"; class const ChatPage({super.key}) extends HookConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) { - final initialHighlightedEvent = useState(null); + 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; - 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, - ), + return SafeArea( + child: Scaffold( + appBar: initComplete ? null : Appbar(), + body: initComplete + ? Row( + children: [ + if (isDesktop) Sidebar(isDesktop: isDesktop), + Expanded( + child: Consumer( + builder: (context, ref, _) { + final initialHighlight = ref.watch( + JumpToEventController.provider, + ); + return RoomChat( + key: ValueKey((roomId, initialHighlight)), + roomId: roomId, + isDesktop: isDesktop, + initialHighlightedEvent: initialHighlight, + showMembersByDefault: showMembersByDefault, + ); + }, ), - ], - ) - : 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, - jumpToEvent: (eventId) => - initialHighlightedEvent.value = eventId, - ), - ), - ); - }, - ); - } + ), + drawer: isDesktop || !initComplete + ? null + : Sidebar(isDesktop: isDesktop), + ), + ); + }, + ); } diff --git a/lib/widgets/pages/notifications.dart b/lib/widgets/pages/notifications.dart index 56fee20..c8cfe76 100644 --- a/lib/widgets/pages/notifications.dart +++ b/lib/widgets/pages/notifications.dart @@ -5,6 +5,7 @@ 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/jump_to_event.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/notifications.dart"; import "package:nexus/controllers/spaces.dart"; @@ -19,7 +20,6 @@ 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 @@ -166,7 +166,13 @@ class const NotificationsPage({ ).notifier, ) .set(event.roomId); - jumpToEvent(event.eventId); + ref + .read( + JumpToEventController + .provider + .notifier, + ) + .set(event.eventId); Navigator.of(context).pop(); }, diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index b691cd5..6689ce2 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -16,11 +16,8 @@ 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, - required final void Function(String eventId) jumpToEvent, - super.key, -}) extends HookConsumerWidget { +class const Sidebar({required final bool isDesktop, super.key}) + extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedSpaceProvider = KeyController.provider( @@ -197,8 +194,7 @@ class const Sidebar({ tooltip: "Open notifications", onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => - NotificationsPage(jumpToEvent: jumpToEvent), + builder: (_) => NotificationsPage(), ), ), icon: Icon(Icons.notifications),