import "dart:async"; 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/notifications.dart"; import "package:nexus/helpers/extensions/focus_room.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/nav_page.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; import "package:super_sliver_list/super_sliver_list.dart"; class const NotificationsList({ final int? highlightedEvent, final bool defaultToAllNotifications = false, super.key, }) extends HookConsumerWidget implements NavPage { @override String get title => "Notifications"; @override IconData get icon => Icons.notifications; // TODO: Show notification badge here @override Future? badgeBuilder(WidgetRef ref) => null; @override Widget build(BuildContext context, WidgetRef ref) { final options = { M3EToggleButtonGroupAction( checkedLabel: Text("Mentions"), icon: Icon(Icons.alternate_email), ): .highlight, M3EToggleButtonGroupAction( checkedLabel: Text("All Notifications"), icon: Icon(Icons.notifications), ): .notify, }; final unreadTypeIndex = useState(defaultToAllNotifications ? 1 : 0); final highlightedId = useState(highlightedEvent); final listController = useRef(ListController()); final scrollController = useScrollController(); final provider = NotificationsController.provider(( options.values.toList()[unreadTypeIndex.value], null, )); final notifications = ref.watch(provider); final notifier = ref.watch(provider.notifier); useEffect(() { if (highlightedId.value == null) return null; Timer? timer; void listener() => WidgetsBinding.instance.addPostFrameCallback((_) async { if (!listController.value.isAttached) return; final notifications = await ref.watch(provider.future); final index = notifications.indexWhere( (tuple) => tuple.$1 == highlightedId.value!, ); if (index == -1) return; listController.value.animateToItem( index: index, scrollController: scrollController, alignment: 0.5, duration: (_) => .new(milliseconds: 700), curve: (_) => Curves.easeInOut, ); timer = Timer(.new(seconds: 1), () { highlightedId.value = null; }); listController.value.removeListener(listener); }); listController.value.addListener(listener); return timer?.cancel; }, []); useEffect(() { Future listener() async { if (!scrollController.hasClients || notifications.isLoading) return; if (scrollController.position.pixels >= scrollController.position.maxScrollExtent) { await notifier.loadOlder(); } } scrollController.addListener(listener); return () => scrollController.removeListener(listener); }, [scrollController, notifications]); return Stack( children: [ Column( children: [ if (notifications is AsyncLoading && notifications.value != null) const LinearProgressIndicator(minHeight: 2), Expanded( child: switch (notifications) { AsyncData(:final value) || AsyncLoading(:final value?) => value.isEmpty ? Center( child: Text( "No notifications yet", style: Theme.of(context).textTheme.headlineMedium, ), ) : SuperListView.builder( listController: listController.value, controller: scrollController, itemCount: value.length, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 12, ), reverse: true, itemBuilder: (context, index) { final (rowId, roomId) = value[index]; final isHighlighted = rowId == highlightedId.value; return Padding( padding: .only(top: 8), child: HighlightWrapper( InkWell( onTap: () async { final navigator = Navigator.of(context); if (navigator.canPop()) navigator.pop(); await ref.focusRoom(roomId, rowId); }, child: IgnorePointer( child: EventRenderer(rowId, roomId: roomId), ), ), isHighlighted: isHighlighted, ), ); }, ), AsyncLoading() => const Loading(), AsyncError(:final error, :final stackTrace) => ErrorDialog( error, stackTrace, ), }, ), ], ), Align( alignment: .topRight, child: Padding( padding: .all(16), child: M3EToggleButtonGroup( selectedIndex: unreadTypeIndex.value, onSelectedIndexChanged: (index) => unreadTypeIndex.value = index ?? unreadTypeIndex.value, actions: options.keys.toList(), ), ), ), ], ); } }