From b01ea9b95ed7c98d1f59c72e23aadf50b77d8cbb Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Sat, 11 Mar 2023 13:40:36 -0500 Subject: [PATCH] Initial ActiveProfileSelector implementation using Notifications system --- lib/controls/app_bottom_nav_bar.dart | 23 +++- lib/controls/notifications_control.dart | 31 +++-- lib/controls/standard_app_drawer.dart | 4 +- lib/di_initialization.dart | 5 +- lib/main.dart | 7 +- lib/screens/notifications_screen.dart | 148 +++++++++++++----------- lib/utils/active_profile_selector.dart | 38 ++++++ 7 files changed, 172 insertions(+), 84 deletions(-) create mode 100644 lib/utils/active_profile_selector.dart diff --git a/lib/controls/app_bottom_nav_bar.dart b/lib/controls/app_bottom_nav_bar.dart index 2493ed6..c6a25f5 100644 --- a/lib/controls/app_bottom_nav_bar.dart +++ b/lib/controls/app_bottom_nav_bar.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:relatica/utils/snackbar_builder.dart'; import '../routes.dart'; import '../services/notifications_manager.dart'; +import '../utils/active_profile_selector.dart'; enum NavBarButtons { timelines, @@ -14,15 +16,26 @@ enum NavBarButtons { } class AppBottomNavBar extends StatelessWidget { + static final _logger = Logger('$AppBottomNavBar'); final NavBarButtons currentButton; const AppBottomNavBar({super.key, required this.currentButton}); @override Widget build(BuildContext context) { - final notificationManager = context.watch(); - final hasNotifications = - notificationManager.notifications.where((n) => !n.dismissed).isNotEmpty; + final nmResult = context + .watch>() + .activeEntry; + final hasNotifications = nmResult.fold( + onSuccess: (manager) => + manager.notifications + .where((n) => !n.dismissed) + .isNotEmpty, + onError: (error) { + _logger.info('Error getting notifications manager: $error'); + return false; + } + ); return BottomNavigationBar( onTap: (index) { final newButton = _indexToButton(index); @@ -89,8 +102,8 @@ class AppBottomNavBar extends StatelessWidget { throw ArgumentError('$index has no button type'); } - List _menuItems( - BuildContext context, bool hasNotifications) { + List _menuItems(BuildContext context, + bool hasNotifications) { return [ const BottomNavigationBarItem( label: 'Timelines', diff --git a/lib/controls/notifications_control.dart b/lib/controls/notifications_control.dart index a1c0c46..e15c5ce 100644 --- a/lib/controls/notifications_control.dart +++ b/lib/controls/notifications_control.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:relatica/utils/active_profile_selector.dart'; import '../globals.dart'; import '../models/user_notification.dart'; @@ -14,6 +16,7 @@ import '../utils/snackbar_builder.dart'; import 'image_control.dart'; class NotificationControl extends StatelessWidget { + static final _logger = Logger('$NotificationControl'); final UserNotification notification; const NotificationControl({ @@ -41,7 +44,17 @@ class NotificationControl extends StatelessWidget { @override Widget build(BuildContext context) { const iconSize = 50.0; - final manager = context.watch(); + final manager = context + .watch>() + .activeEntry + .fold( + onSuccess: (manager) => manager, + onError: (error) { + _logger.severe('Error getting notification manager: $error'); + return null; + }, + ); + final fromIcon = getIt().getById(notification.fromId).fold( onSuccess: (connection) => ImageControl( @@ -120,13 +133,15 @@ class NotificationControl extends StatelessWidget { notification.type == NotificationType.direct_message ? null : IconButton( - onPressed: () async { - final result = await manager.markSeen(notification); - if (result.isFailure) { - buildSnackbar( - context, 'Error marking notification: ${result.error}'); - } - }, + onPressed: manager == null + ? null + : () async { + final result = await manager.markSeen(notification); + if (result.isFailure) { + buildSnackbar(context, + 'Error marking notification: ${result.error}'); + } + }, icon: Icon(Icons.close_rounded)), ); } diff --git a/lib/controls/standard_app_drawer.dart b/lib/controls/standard_app_drawer.dart index 41bb9d2..ce3f236 100644 --- a/lib/controls/standard_app_drawer.dart +++ b/lib/controls/standard_app_drawer.dart @@ -17,7 +17,9 @@ class StandardAppDrawer extends StatelessWidget { (p) => ListTile( onTap: () async { await getIt().setActiveProfile(p); - clearCaches(); + if (context.mounted) { + context.pop(); + } }, leading: CircleAvatar( child: CachedNetworkImage(imageUrl: p.avatar)), diff --git a/lib/di_initialization.dart b/lib/di_initialization.dart index 4fe4038..51e3458 100644 --- a/lib/di_initialization.dart +++ b/lib/di_initialization.dart @@ -1,4 +1,5 @@ import 'package:logging/logging.dart'; +import 'package:relatica/utils/active_profile_selector.dart'; import 'data/interfaces/connections_repo_intf.dart'; import 'data/interfaces/groups_repo.intf.dart'; @@ -57,8 +58,8 @@ Future dependencyInjectionInitialization() async { getIt.registerSingleton(timelineManager); getIt.registerLazySingleton( () => MediaUploadAttachmentHelper()); - getIt.registerLazySingleton( - () => NotificationsManager()); + getIt.registerLazySingleton>( + () => ActiveProfileSelector((_) => NotificationsManager())); getIt.registerLazySingleton( () => DirectMessageService()); getIt.registerLazySingleton(() => InteractionsManager()); diff --git a/lib/main.dart b/lib/main.dart index c88df76..e417a56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; import 'package:provider/provider.dart'; +import 'package:relatica/utils/active_profile_selector.dart'; import 'app_theme.dart'; import 'di_initialization.dart'; @@ -75,8 +76,10 @@ class App extends StatelessWidget { ChangeNotifierProvider( create: (_) => getIt(), ), - ChangeNotifierProvider( - create: (_) => getIt(), + ChangeNotifierProvider< + ActiveProfileSelector>( + create: (_) => + getIt>(), ), ChangeNotifierProvider( create: (_) => getIt(), diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index 274dcfe..25920ff 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -10,6 +10,7 @@ import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; import '../services/network_status_service.dart'; import '../services/notifications_manager.dart'; +import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; class NotificationsScreen extends StatelessWidget { @@ -21,85 +22,100 @@ class NotificationsScreen extends StatelessWidget { Widget build(BuildContext context) { _logger.finest('Building'); final nss = getIt(); - final manager = context.watch(); - final notifications = manager.notifications; + final managerResult = context + .watch>() + .activeEntry; late final String title; late final Widget body; - if (notifications.isEmpty) { - manager.updateNotifications(); + late final List actions; + managerResult.match(onSuccess: (manager) { + final notifications = manager.notifications; + actions = [ + StatusAndRefreshButton( + valueListenable: nss.notificationsUpdateStatus, + refreshFunction: () async => manager.updateNotifications(), + busyColor: Theme.of(context).colorScheme.background, + ), + IconButton( + onPressed: () async => _clearAllNotifications(context, manager), + icon: const Icon(Icons.cleaning_services), + ), + ]; + if (notifications.isEmpty) { + manager.updateNotifications(); + title = 'Notifications'; + body = Center( + child: Column( + children: const [ + Center(child: Text('No notifications')), + ], + )); + } else { + final unreadCount = notifications.where((e) => !e.dismissed).length; + title = 'Notifications ($unreadCount)'; + body = RefreshIndicator( + onRefresh: () async { + manager.updateNotifications(); + }, + child: ListView.separated( + itemBuilder: (context, index) { + if (index == 0) { + return TextButton( + onPressed: () async { + final result = await manager.loadNewerNotifications(); + final noMore = result.fold( + onSuccess: (values) => values.isEmpty, + onError: (_) => true); + if (context.mounted && noMore) { + buildSnackbar( + context, 'No newer notifications to load'); + } + }, + child: const Text('Load newer notifications')); + } + if (index == notifications.length + 1) { + return TextButton( + onPressed: () async { + final result = await manager.loadOlderNotifications(); + final noMore = result.fold( + onSuccess: (values) => values.isEmpty, + onError: (_) => true); + if (context.mounted && noMore) { + buildSnackbar( + context, 'No older notifications to load'); + } + }, + child: const Text('Load older notifications')); + } + return NotificationControl( + notification: notifications[index - 1]); + }, + separatorBuilder: (context, index) { + return const Divider( + color: Colors.black54, + height: 0.0, + ); + }, + itemCount: notifications.length + 2), + ); + } + }, onError: (error) { title = 'Notifications'; + actions = []; body = Center( child: Column( - children: [ - const Center(child: Text('No notifications')), + children: const [ + Center(child: Text('Error getting notifications')), ], )); - } else { - final unreadCount = notifications.where((e) => !e.dismissed).length; - title = 'Notifications ($unreadCount)'; - body = RefreshIndicator( - onRefresh: () async { - manager.updateNotifications(); - }, - child: ListView.separated( - itemBuilder: (context, index) { - if (index == 0) { - return TextButton( - onPressed: () async { - final result = await manager.loadNewerNotifications(); - final noMore = result.fold( - onSuccess: (values) => values.isEmpty, - onError: (_) => true); - if (context.mounted && noMore) { - buildSnackbar( - context, 'No newer notifications to load'); - } - }, - child: const Text('Load newer notifications')); - } - if (index == notifications.length + 1) { - return TextButton( - onPressed: () async { - final result = await manager.loadOlderNotifications(); - final noMore = result.fold( - onSuccess: (values) => values.isEmpty, - onError: (_) => true); - if (context.mounted && noMore) { - buildSnackbar( - context, 'No older notifications to load'); - } - }, - child: const Text('Load older notifications')); - } - return NotificationControl( - notification: notifications[index - 1]); - }, - separatorBuilder: (context, index) { - return const Divider( - color: Colors.black54, - height: 0.0, - ); - }, - itemCount: notifications.length + 2), - ); - } + }); return Scaffold( appBar: StandardAppBar.build( context, title, withDrawer: true, - actions: [ - StatusAndRefreshButton( - valueListenable: nss.notificationsUpdateStatus, - refreshFunction: () async => manager.updateNotifications(), - busyColor: Theme.of(context).colorScheme.background, - ), - IconButton( - onPressed: () async => _clearAllNotifications(context, manager), - icon: const Icon(Icons.cleaning_services), - ), - ], + actions: actions, ), drawer: StandardAppDrawer(), body: body, diff --git a/lib/utils/active_profile_selector.dart b/lib/utils/active_profile_selector.dart new file mode 100644 index 0000000..189aa25 --- /dev/null +++ b/lib/utils/active_profile_selector.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; +import 'package:relatica/services/auth_service.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../globals.dart'; +import '../models/auth/profile.dart'; +import '../models/exec_error.dart'; + +class ActiveProfileSelector extends ChangeNotifier { + final _entries = {}; + + final T Function(Profile p) _entryBuilder; + + ActiveProfileSelector(T Function(Profile p) entryBuilder) + : _entryBuilder = entryBuilder; + + Result get activeEntry { + final service = getIt(); + if (!service.loggedIn) { + return buildErrorResult( + type: ErrorType.localError, + message: 'No Logged In User', + ); + } + + final p = service.currentProfile; + return runCatching(() { + final entry = _entries.putIfAbsent(p, () => _buildNewEntry(p)); + return Result.ok(entry).execErrorCast(); + }).execErrorCast(); + } + + T _buildNewEntry(Profile p) { + final newEntry = _entryBuilder(p); + newEntry.addListener(() => notifyListeners()); + return newEntry; + } +}