From f4dee566502240f68e64941132e2c52671cee95f Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Sat, 19 Nov 2022 00:00:17 -0500 Subject: [PATCH] Initial rudimentary notifications viewer and back end --- lib/controls/notifications_control.dart | 28 +++++++ lib/friendica_client.dart | 19 +++++ lib/main.dart | 6 ++ lib/models/user_notification.dart | 32 ++++++++ lib/screens/notifications_screen.dart | 24 ++++-- .../notification_friendica_extension.dart | 16 ++++ lib/services/notifications_manager.dart | 74 +++++++++++++++++++ 7 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 lib/controls/notifications_control.dart create mode 100644 lib/models/user_notification.dart create mode 100644 lib/serializers/friendica/notification_friendica_extension.dart create mode 100644 lib/services/notifications_manager.dart diff --git a/lib/controls/notifications_control.dart b/lib/controls/notifications_control.dart new file mode 100644 index 0000000..dee7057 --- /dev/null +++ b/lib/controls/notifications_control.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; + +import '../models/user_notification.dart'; +import '../utils/dateutils.dart'; + +class NotificationControl extends StatelessWidget { + final UserNotification notification; + + const NotificationControl({ + super.key, + required this.notification, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + tileColor: notification.seen ? null : Colors.black12, + leading: Text(notification.fromName), + title: HtmlWidget(notification.content), + subtitle: + Text(ElapsedDateUtils.epochSecondsToString(notification.timestamp)), + trailing: notification.seen + ? null + : IconButton(onPressed: () {}, icon: Icon(Icons.close_rounded)), + ); + } +} diff --git a/lib/friendica_client.dart b/lib/friendica_client.dart index 79f5a9c..5884c0c 100644 --- a/lib/friendica_client.dart +++ b/lib/friendica_client.dart @@ -8,6 +8,8 @@ import 'models/TimelineIdentifiers.dart'; import 'models/credentials.dart'; import 'models/exec_error.dart'; import 'models/timeline_entry.dart'; +import 'models/user_notification.dart'; +import 'serializers/friendica/notification_friendica_extension.dart'; import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart'; class FriendicaClient { @@ -27,6 +29,23 @@ class FriendicaClient { _authHeader = "Basic $encodedAuthString"; } + FutureResult, ExecError> getNotifications() async { + final url = 'https://$serverName/api/friendica/notifications'; + final request = Uri.parse(url); + _logger.finest(() => 'Getting new notifications'); + return (await _getApiListRequest(request).andThenSuccessAsync( + (notificationsJson) async => notificationsJson + .map((json) => NotificationFriendicaExtension.fromJson(json)) + .toList())) + .mapError((error) { + if (error is ExecError) { + return error; + } + + return ExecError(type: ErrorType.localError, message: error.toString()); + }); + } + FutureResult, ExecError> getUserTimeline( {String userId = '', int page = 1, int count = 10}) async { _logger.finest(() => 'Getting user timeline for $userId'); diff --git a/lib/main.dart b/lib/main.dart index b513b45..45db895 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'routes.dart'; import 'services/auth_service.dart'; import 'services/connections_manager.dart'; import 'services/entry_manager_service.dart'; +import 'services/notifications_manager.dart'; import 'services/secrets_service.dart'; import 'services/timeline_manager.dart'; import 'utils/app_scrolling_behavior.dart'; @@ -31,6 +32,8 @@ void main() async { getIt.registerSingleton(secretsService); getIt.registerSingleton(authService); getIt.registerSingleton(timelineManager); + getIt.registerLazySingleton( + () => NotificationsManager()); await secretsService.initialize().andThenSuccessAsync((credentials) async { if (credentials.isEmpty) { return; @@ -71,6 +74,9 @@ class App extends StatelessWidget { ChangeNotifierProvider( create: (_) => getIt(), ), + ChangeNotifierProvider( + create: (_) => getIt(), + ), ], child: MaterialApp.router( theme: ThemeData( diff --git a/lib/models/user_notification.dart b/lib/models/user_notification.dart new file mode 100644 index 0000000..ba0dbe6 --- /dev/null +++ b/lib/models/user_notification.dart @@ -0,0 +1,32 @@ +enum NotificationType { + favorite, + follow, + follow_request, + mention, + reshare, + status +} + +class UserNotification { + final String id; + final String type; + final String fromName; + final String fromUrl; + final int timestamp; + final String iid; + final bool seen; + final String content; + final String link; + + UserNotification({ + required this.id, + required this.type, + required this.fromName, + required this.fromUrl, + required this.timestamp, + required this.iid, + required this.seen, + required this.content, + required this.link, + }); +} diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index 0026953..ed0c706 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -1,24 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../controls/app_bottom_nav_bar.dart'; +import '../controls/notifications_control.dart'; +import '../services/notifications_manager.dart'; class NotificationsScreen extends StatelessWidget { const NotificationsScreen({super.key}); @override Widget build(BuildContext context) { + final manager = context.watch(); + final notifications = manager.notifications; + if (notifications.isEmpty) { + manager.updateNotifications(); + } return Scaffold( appBar: AppBar( title: Text('Notifications'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Notifications'), - ], - ), - ), + body: ListView.separated( + itemBuilder: (context, index) { + return NotificationControl(notification: notifications[index]); + }, + separatorBuilder: (context, index) { + return Divider(); + }, + itemCount: notifications.length), bottomNavigationBar: AppBottomNavBar( currentButton: NavBarButtons.notifications, ), diff --git a/lib/serializers/friendica/notification_friendica_extension.dart b/lib/serializers/friendica/notification_friendica_extension.dart new file mode 100644 index 0000000..38d303d --- /dev/null +++ b/lib/serializers/friendica/notification_friendica_extension.dart @@ -0,0 +1,16 @@ +import '../../models/user_notification.dart'; + +extension NotificationFriendicaExtension on UserNotification { + static UserNotification fromJson(Map json) => + UserNotification( + id: json['id'].toString(), + type: json['type'].toString(), + fromName: json['name'], + fromUrl: json['url'], + timestamp: int.tryParse(json['timestamp'] ?? '') ?? 0, + iid: json['iid'].toString(), + seen: json['seen'] ?? false, + content: json['msg_html'], + link: json['link'], + ); +} diff --git a/lib/services/notifications_manager.dart b/lib/services/notifications_manager.dart new file mode 100644 index 0000000..1cce713 --- /dev/null +++ b/lib/services/notifications_manager.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../globals.dart'; +import '../models/exec_error.dart'; +import '../models/user_notification.dart'; +import 'auth_service.dart'; + +class NotificationsManager extends ChangeNotifier { + static final _logger = Logger('NotificationManager'); + final _notifications = {}; + + List get notifications { + final result = List.from(_notifications.values); + result.sort((n1, n2) { + if (n1.seen == n2.seen) { + return n2.timestamp.compareTo(n1.timestamp); + } + + if (n1.seen && !n2.seen) { + return 1; + } + + return -1; + }); + + return result; + } + + FutureResult, ExecError> updateNotifications() async { + final auth = getIt(); + final clientResult = auth.currentClient; + if (clientResult.isFailure) { + _logger.severe('Error getting Friendica client: ${clientResult.error}'); + return clientResult.errorCast(); + } + + final client = clientResult.value; + final result = await client.getNotifications(); + if (result.isFailure) { + return result.errorCast(); + } + + for (final n in result.value) { + _notifications[n.id] = n; + } + + notifyListeners(); + return Result.ok(notifications); + } + + FutureResult markSeen(String id) async { + final auth = getIt(); + final clientResult = auth.currentClient; + if (clientResult.isFailure) { + _logger.severe('Error getting Friendica client: ${clientResult.error}'); + return clientResult.errorCast(); + } + + final client = clientResult.value; + final result = await client.getNotifications(); + if (result.isFailure) { + return result.errorCast(); + } + + for (final n in result.value) { + _notifications[n.id] = n; + } + + notifyListeners(); + return Result.ok(notifications.first); + } +}