diff --git a/.gitignore b/.gitignore index f81af0c..08752ba 100644 --- a/.gitignore +++ b/.gitignore @@ -49,8 +49,10 @@ android/ web/ linux/ macos/ +windows/ pubspec.lock .metadata flutter_notification_center.iml -dotenv \ No newline at end of file +dotenv +firebase_options.dart \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 66594f6..094bea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [5.0.0] - 24 September 2024 + +* Refactor package with the new component structure + ## [4.0.0] - 14 August 2024 * Fix overflow issue with long text in notification diff --git a/README.md b/README.md index d872611..c3c70a2 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,13 @@ A Flutter package for creating notification center displaying a list of notifica To use this package, add `flutter_notification_center` as a dependency in your pubspec.yaml file. +- Provide a `NotificationService` to the userstory to define and alter behaviour, this service accepts and `NotificationRepositoryInterface` as repository (data source) + - For custom notification styling provide the optional notificationWidgetBuilder with your own implementation. The `NotificationConfig` has its own parameters, as specified below: | Parameter | Explanation | |-----------|-------------| -| service | The notification service that will be used | | seperateNotificationsWithDivider | If true notifications will be seperated with dividers within the notification center | | translations | The translations that will be used | | notificationWidgetBuilder | The widget that defines the styles and logic for every notification | diff --git a/packages/flutter_notification_center_firebase/.gitignore b/packages/firebase_notification_center_repository/.gitignore similarity index 52% rename from packages/flutter_notification_center_firebase/.gitignore rename to packages/firebase_notification_center_repository/.gitignore index 46d0fec..ac5aa98 100644 --- a/packages/flutter_notification_center_firebase/.gitignore +++ b/packages/firebase_notification_center_repository/.gitignore @@ -22,31 +22,8 @@ migrate_working_dir/ #.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -.dart_tools/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# iOS related -/ios/ - -lib/config/ -pubspec.lock -dotenv - +build/ diff --git a/packages/firebase_notification_center_repository/CHANGELOG.md b/packages/firebase_notification_center_repository/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/firebase_notification_center_repository/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/firebase_notification_center_repository/LICENSE b/packages/firebase_notification_center_repository/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/firebase_notification_center_repository/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/firebase_notification_center_repository/README.md b/packages/firebase_notification_center_repository/README.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/packages/firebase_notification_center_repository/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/flutter_notification_center_firebase/analysis_options.yaml b/packages/firebase_notification_center_repository/analysis_options.yaml similarity index 94% rename from packages/flutter_notification_center_firebase/analysis_options.yaml rename to packages/firebase_notification_center_repository/analysis_options.yaml index 31b4b51..e2b30bf 100644 --- a/packages/flutter_notification_center_firebase/analysis_options.yaml +++ b/packages/firebase_notification_center_repository/analysis_options.yaml @@ -6,4 +6,4 @@ analyzer: exclude: linter: - rules: + rules: \ No newline at end of file diff --git a/packages/firebase_notification_center_repository/lib/firebase_notification_center_repository.dart b/packages/firebase_notification_center_repository/lib/firebase_notification_center_repository.dart new file mode 100644 index 0000000..0c331dc --- /dev/null +++ b/packages/firebase_notification_center_repository/lib/firebase_notification_center_repository.dart @@ -0,0 +1 @@ +export "src/firebase_notification_repository.dart"; diff --git a/packages/firebase_notification_center_repository/lib/src/firebase_notification_repository.dart b/packages/firebase_notification_center_repository/lib/src/firebase_notification_repository.dart new file mode 100644 index 0000000..6e9cf18 --- /dev/null +++ b/packages/firebase_notification_center_repository/lib/src/firebase_notification_repository.dart @@ -0,0 +1,151 @@ +import "package:cloud_firestore/cloud_firestore.dart"; +import "package:firebase_core/firebase_core.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; + +class FirebaseNotificationRepository + implements NotificationRepositoryInterface { + FirebaseNotificationRepository({ + FirebaseApp? firebaseApp, + this.activeNotificationsCollection = "active_notifications", + this.plannedNotificationsCollection = "planned_notifications", + }) : firebaseApp = firebaseApp ?? Firebase.app(); + + final FirebaseApp firebaseApp; + final String activeNotificationsCollection; + final String plannedNotificationsCollection; + + @override + Future addNotification( + String userId, + NotificationModel notification, + List recipientIds, + ) async { + var newNotification = notification; + + for (var recipientId in recipientIds) { + DocumentReference notifications; + if (notification.scheduledFor != null && + notification.scheduledFor!.isAfter(DateTime.now())) { + notifications = FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(plannedNotificationsCollection) + .doc(recipientId) + .collection(plannedNotificationsCollection) + .doc(notification.id); + } else { + newNotification = notification.copyWith( + id: "${notification.id}-${DateTime.now().millisecondsSinceEpoch}", + ); + + notifications = FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(activeNotificationsCollection) + .doc(recipientId) + .collection(activeNotificationsCollection) + .doc(newNotification.id); + } + + var currentDateTime = DateTime.now(); + newNotification.dateTimePushed = currentDateTime; + var notificationMap = newNotification.toMap(); + await notifications.set(notificationMap); + } + + return newNotification; + } + + @override + Future deleteNotification( + String userId, + String id, + bool planned, + ) async { + try { + if (planned) { + await FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(plannedNotificationsCollection) + .doc(userId) + .collection(plannedNotificationsCollection) + .doc(id) + .delete(); + } else { + await FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection) + .doc(id) + .delete(); + } + } catch (e) { + throw Exception("Failed to delete notification: $e"); + } + } + + @override + Stream getNotification(String userId, String id) { + var notificationStream = FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection) + .doc(id) + .snapshots() + .map((snapshot) { + if (snapshot.exists) { + if (snapshot.data() == null) { + return null; + } + + return NotificationModel.fromJson(snapshot.data()!); + } else { + return null; + } + }); + + return notificationStream; + } + + @override + Stream> getNotifications(String userId) { + var notificationsStream = FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection) + .snapshots() + .map( + (snapshot) => snapshot.docs + .map((doc) => NotificationModel.fromJson(doc.data())) + .toList(), + ); + + return notificationsStream; + } + + @override + Future updateNotification( + String userId, + NotificationModel notification, + ) async { + await FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection) + .doc(notification.id) + .update(notification.toMap()); + + return notification; + } + + @override + Stream> getPlannedNotifications(String userId) { + var notificationsStream = FirebaseFirestore.instanceFor(app: firebaseApp) + .collection(plannedNotificationsCollection) + .doc(userId) + .collection(plannedNotificationsCollection) + .snapshots() + .map( + (snapshot) => snapshot.docs + .map((doc) => NotificationModel.fromJson(doc.data())) + .toList(), + ); + + return notificationsStream; + } +} diff --git a/packages/firebase_notification_center_repository/pubspec.yaml b/packages/firebase_notification_center_repository/pubspec.yaml new file mode 100644 index 0000000..bd30a84 --- /dev/null +++ b/packages/firebase_notification_center_repository/pubspec.yaml @@ -0,0 +1,31 @@ +name: firebase_notification_center_repository +description: "A new Flutter package project." +version: 5.0.0 +homepage: +publish_to: 'none' + +environment: + sdk: ^3.5.3 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + # Firebase + cloud_firestore: ^5.4.2 + firebase_core: ^3.5.0 + + notification_center_repository_interface: + git: + url: https://github.com/Iconica-Development/flutter_notification_center.git + path: packages/notification_center_repository_interface + ref: 5.0.0 + +dev_dependencies: + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 + +flutter: diff --git a/packages/flutter_notification_center/.gitignore b/packages/flutter_notification_center/.gitignore index 29a3a50..ac5aa98 100644 --- a/packages/flutter_notification_center/.gitignore +++ b/packages/flutter_notification_center/.gitignore @@ -22,22 +22,8 @@ migrate_working_dir/ #.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release +build/ diff --git a/packages/flutter_notification_center/CHANGELOG.md b/packages/flutter_notification_center/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/flutter_notification_center/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/flutter_notification_center/LICENSE b/packages/flutter_notification_center/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/flutter_notification_center/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/flutter_notification_center/README.md b/packages/flutter_notification_center/README.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/packages/flutter_notification_center/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/flutter_notification_center/analysis_options.yaml b/packages/flutter_notification_center/analysis_options.yaml index 31b4b51..e2b30bf 100644 --- a/packages/flutter_notification_center/analysis_options.yaml +++ b/packages/flutter_notification_center/analysis_options.yaml @@ -6,4 +6,4 @@ analyzer: exclude: linter: - rules: + rules: \ No newline at end of file diff --git a/packages/flutter_notification_center/example/.gitignore b/packages/flutter_notification_center/example/.gitignore index 57cc60d..29a3a50 100644 --- a/packages/flutter_notification_center/example/.gitignore +++ b/packages/flutter_notification_center/example/.gitignore @@ -30,7 +30,6 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ -.dart_tools/ # Symbolication related app.*.symbols @@ -42,11 +41,3 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release - -# iOS related -/ios/ - -lib/config/ -pubspec.lock -dotenv -firebase_options.dart diff --git a/packages/flutter_notification_center/example/analysis_options.yaml b/packages/flutter_notification_center/example/analysis_options.yaml index 0439848..0d29021 100644 --- a/packages/flutter_notification_center/example/analysis_options.yaml +++ b/packages/flutter_notification_center/example/analysis_options.yaml @@ -23,5 +23,6 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_notification_center/example/firebase.json b/packages/flutter_notification_center/example/firebase.json new file mode 100644 index 0000000..58b7ecc --- /dev/null +++ b/packages/flutter_notification_center/example/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"dart":{"lib/firebase_options.dart":{"projectId":"appshell-demo","configurations":{"web":"1:431820621472:web:f4b27eea24be24fd1babc5"}}}}}} \ No newline at end of file diff --git a/packages/flutter_notification_center/example/lib/custom_notification.dart b/packages/flutter_notification_center/example/lib/custom_notification.dart deleted file mode 100644 index 454e67d..0000000 --- a/packages/flutter_notification_center/example/lib/custom_notification.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_notification_center/flutter_notification_center.dart'; -import 'package:flutter_notification_center_firebase/flutter_notification_center_firebase.dart'; - -class CustomNotificationWidget extends StatelessWidget { - final NotificationModel notification; - final FirebaseNotificationService notificationService; - final NotificationTranslations notificationTranslations; - final BuildContext context; - - const CustomNotificationWidget({ - required this.notification, - required this.notificationTranslations, - required this.notificationService, - required this.context, - super.key, - }); - - @override - Widget build(BuildContext context) { - return notification.isPinned - //Pinned notification - ? Dismissible( - key: Key('${notification.id}_pinned'), - onDismissed: (direction) async { - await unPinNotification(notificationService, notification, - notificationTranslations, context); - }, - background: Container( - color: const Color.fromRGBO(59, 213, 111, 1), - alignment: Alignment.centerLeft, - child: const Padding( - padding: EdgeInsets.only(left: 16.0), - child: Icon( - Icons.push_pin, - color: Colors.white, - ), - ), - ), - secondaryBackground: Container( - color: const Color.fromRGBO(59, 213, 111, 1), - alignment: Alignment.centerLeft, - child: const Padding( - padding: EdgeInsets.only(left: 16.0), - child: Icon( - Icons.push_pin, - color: Colors.white, - ), - ), - ), - child: GestureDetector( - onTap: () async => - _navigateToNotificationDetail(context, notification), - child: ListTile( - title: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - notification.title, - ), - ), - ], - ), - trailing: IconButton( - icon: const Icon(Icons.push_pin), - onPressed: () async => - _navigateToNotificationDetail(context, notification), - padding: const EdgeInsets.only(left: 60.0), - ), - ), - ), - ) - //Dismissable notification - : Dismissible( - key: Key(notification.id), - onDismissed: (direction) async { - if (direction == DismissDirection.endToStart) { - await dismissNotification(notificationService, notification, - notificationTranslations, context); - } else if (direction == DismissDirection.startToEnd) { - await pinNotification(notificationService, notification, - notificationTranslations, context); - } - }, - background: Container( - color: const Color.fromRGBO(59, 213, 111, 1), - alignment: Alignment.centerLeft, - child: const Padding( - padding: EdgeInsets.only(left: 16.0), - child: Icon( - Icons.push_pin, - color: Colors.white, - ), - ), - ), - secondaryBackground: Container( - color: const Color.fromRGBO(255, 131, 131, 1), - alignment: Alignment.centerRight, - child: const Padding( - padding: EdgeInsets.only(right: 16.0), - child: Icon( - Icons.delete, - color: Colors.black, - ), - ), - ), - child: GestureDetector( - onTap: () async => _navigateToNotificationDetail( - context, - notification, - ), - child: ListTile( - leading: Icon( - notification.icon, - color: Colors.grey, - ), - title: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - notification.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 16, - ), - ), - ), - ], - ), - trailing: !notification.isRead - ? Container( - margin: const EdgeInsets.only(right: 8.0), - width: 12.0, - height: 12.0, - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - ) - : null, - ), - ), - ); - } - - Future _navigateToNotificationDetail( - BuildContext context, - NotificationModel notification, - ) async { - unawaited(markNotificationAsRead(notificationService, notification)); - if (context.mounted) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NotificationDetailPage( - translations: notificationTranslations, - notification: notification, - ), - ), - ); - } - } -} diff --git a/packages/flutter_notification_center/example/lib/main.dart b/packages/flutter_notification_center/example/lib/main.dart index 04be4cf..c458429 100644 --- a/packages/flutter_notification_center/example/lib/main.dart +++ b/packages/flutter_notification_center/example/lib/main.dart @@ -1,10 +1,10 @@ -import 'package:example/custom_notification.dart'; +// import 'package:example/firebase_options.dart'; // import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_notification_center_repository/firebase_notification_center_repository.dart'; // import 'package:example/firebase_options.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter_notification_center_firebase/flutter_notification_center_firebase.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:flutter_notification_center/flutter_notification_center.dart'; @@ -22,6 +22,7 @@ void main() async { } Future _configureApp() async { + // Generate a FirebaseOptions and uncomment the following lines to initialize Firebase. // await Firebase.initializeApp( // options: DefaultFirebaseOptions.currentPlatform, // ); @@ -34,7 +35,8 @@ Future _configureApp() async { } Future _signInUser() async { - /// Implement your own sign in logic here + // Sign in, you could use the line below or implement your own sign in method. + // await FirebaseAuth.instance.signInAnonymously(); } class NotificationCenterDemo extends StatefulWidget { @@ -45,30 +47,39 @@ class NotificationCenterDemo extends StatefulWidget { } class _NotificationCenterDemoState extends State { - late NotificationConfig config; - late PopupHandler popupHandler; + late NotificationService service; + // Provide a user ID here. For Firebase you can use the commented line below. + String userId = ""; //FirebaseAuth.instance.currentUser!.uid; @override void initState() { super.initState(); - var service = FirebaseNotificationService( - newNotificationCallback: (notification) { - popupHandler.handleNotificationPopup(notification); - }, + + service = NotificationService( + userId: userId, + repository: FirebaseNotificationRepository(), ); - config = NotificationConfig( - service: service, - enableNotificationPopups: true, - showAsSnackBar: true, - notificationWidgetBuilder: (notification, context) => - CustomNotificationWidget( - notification: notification, - notificationService: service, - notificationTranslations: const NotificationTranslations.empty(), - context: context, + + // Uncomment the line below to send a test notification. + // Provide a user ID in the list to send the notification to. + _sendTestNotification([userId]); + } + + _sendTestNotification(List recipientIds) async { + await service.pushNotification( + NotificationModel( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: 'Test Notification', + body: 'This is a test notification.', + // For a scheduled message provide a scheduledFor date. + // For a recurring message provide a scheduledFor date, set recurring to true and provide an occuringInterval. + // + // scheduledFor: DateTime.now().add(const Duration(seconds: 5)), + // recurring: true, + // occuringInterval: OcurringInterval.debug, ), + recipientIds, ); - popupHandler = PopupHandler(context: context, config: config); } @override @@ -79,7 +90,8 @@ class _NotificationCenterDemoState extends State { centerTitle: true, actions: [ NotificationBellWidgetStory( - config: config, + userId: userId, + service: service, ), ], ), diff --git a/packages/flutter_notification_center/example/pubspec.yaml b/packages/flutter_notification_center/example/pubspec.yaml index d286274..344b657 100644 --- a/packages/flutter_notification_center/example/pubspec.yaml +++ b/packages/flutter_notification_center/example/pubspec.yaml @@ -2,38 +2,47 @@ name: example description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: "none" # Remove this line if you wish to publish to pub.dev +publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0 +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 environment: - sdk: ">=3.3.2 <4.0.0" + sdk: ^3.5.3 dependencies: flutter: sdk: flutter - intl: ^0.17.0 + + cupertino_icons: ^1.0.8 flutter_notification_center: - git: - url: https://github.com/Iconica-Development/flutter_notification_center - path: packages/flutter_notification_center - ref: 3.0.0 + path: ../ - flutter_notification_center_firebase: - git: - url: https://github.com/Iconica-Development/flutter_notification_center - path: packages/flutter_notification_center_firebase - ref: 3.0.0 + firebase_notification_center_repository: + path: ../../firebase_notification_center_repository + + intl: ^0.19.0 + + firebase_auth: any + firebase_core: any dev_dependencies: flutter_test: sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 -# The following section is specific to Flutter packages. + flutter_lints: ^4.0.0 + flutter: - uses-material-design: true + uses-material-design: true \ No newline at end of file diff --git a/packages/flutter_notification_center/firebase.json b/packages/flutter_notification_center/firebase.json new file mode 100644 index 0000000..58b7ecc --- /dev/null +++ b/packages/flutter_notification_center/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"dart":{"lib/firebase_options.dart":{"projectId":"appshell-demo","configurations":{"web":"1:431820621472:web:f4b27eea24be24fd1babc5"}}}}}} \ No newline at end of file diff --git a/packages/flutter_notification_center/lib/flutter_notification_center.dart b/packages/flutter_notification_center/lib/flutter_notification_center.dart index 5cc259e..4d2da03 100644 --- a/packages/flutter_notification_center/lib/flutter_notification_center.dart +++ b/packages/flutter_notification_center/lib/flutter_notification_center.dart @@ -1,17 +1,13 @@ -// SPDX-FileCopyrightText: 2024 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause +export "package:notification_center_repository_interface/notification_center_repository_interface.dart"; -export "package:flutter_animated_widgets/flutter_animated_widgets.dart"; - -export "src/models/notification.dart"; -export "src/models/notification_config.dart"; -export "src/models/notification_translation.dart"; -export "src/notification_bell.dart"; export "src/notification_bell_story.dart"; export "src/notification_center.dart"; -export "src/notification_detail.dart"; -export "src/notification_dialog.dart"; -export "src/notification_snackbar.dart"; -export "src/popup_handler.dart"; -export "src/services/notification_service.dart"; + +// Screens +export "src/screens/notification_bell.dart"; +export "src/screens/notification_detail.dart"; + +// Widgets +export "src/widgets/notification_dialog.dart"; +export "src/widgets/notification_snackbar.dart"; +export "src/widgets/popup_handler.dart"; diff --git a/packages/flutter_notification_center/lib/src/notification_bell_story.dart b/packages/flutter_notification_center/lib/src/notification_bell_story.dart index a279144..7072d62 100644 --- a/packages/flutter_notification_center/lib/src/notification_bell_story.dart +++ b/packages/flutter_notification_center/lib/src/notification_bell_story.dart @@ -1,27 +1,64 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; +import "package:flutter_animated_widgets/flutter_animated_widgets.dart"; +import "package:flutter_notification_center/src/notification_center.dart"; +import "package:flutter_notification_center/src/screens/notification_bell.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; /// A widget representing a notification bell. -class NotificationBellWidgetStory extends StatelessWidget { +class NotificationBellWidgetStory extends StatefulWidget { /// Creates a new [NotificationBellWidgetStory] instance. /// /// The [config] parameter specifies the notification configuration. const NotificationBellWidgetStory({ - required this.config, + required this.userId, + this.config, + this.service, + this.animatedIconStyle, super.key, }); + /// The user ID. + final String userId; + /// The notification configuration. - final NotificationConfig config; + final NotificationConfig? config; + + /// The notification service. + final NotificationService? service; + + final AnimatedNotificationBellStyle? animatedIconStyle; + + @override + State createState() => + _NotificationBellWidgetStoryState(); +} + +class _NotificationBellWidgetStoryState + extends State { + late NotificationConfig config; + late NotificationService service; + + @override + void initState() { + config = widget.config ?? const NotificationConfig(); + service = widget.service ?? + NotificationService( + userId: widget.userId, + ); + super.initState(); + } @override Widget build(BuildContext context) => NotificationBell( config: config, + service: service, + animatedIconStyle: widget.animatedIconStyle, onTap: () async { await Navigator.of(context).push( MaterialPageRoute( builder: (context) => NotificationCenter( config: config, + service: service, ), ), ); diff --git a/packages/flutter_notification_center/lib/src/notification_center.dart b/packages/flutter_notification_center/lib/src/notification_center.dart index 3180d9f..686d759 100644 --- a/packages/flutter_notification_center/lib/src/notification_center.dart +++ b/packages/flutter_notification_center/lib/src/notification_center.dart @@ -1,7 +1,8 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; +import "package:flutter_notification_center/src/screens/notification_detail.dart"; import "package:flutter_svg/svg.dart"; import "package:intl/intl.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; /// Widget for displaying the notification center. class NotificationCenter extends StatefulWidget { @@ -10,39 +11,31 @@ class NotificationCenter extends StatefulWidget { /// [config]: Configuration for the notification center. const NotificationCenter({ required this.config, + required this.service, super.key, }); /// Configuration for the notification center. final NotificationConfig config; + final NotificationService service; + @override NotificationCenterState createState() => NotificationCenterState(); } /// State for the notification center. class NotificationCenterState extends State { - late Future> _notificationsFuture; + late Stream> _notificationsStream; @override void initState() { super.initState(); - // ignore: discarded_futures - _notificationsFuture = widget.config.service.getActiveNotifications(); - widget.config.service.getActiveAmountStream().listen((amount) async { - _notificationsFuture = widget.config.service.getActiveNotifications(); + _notificationsStream = widget.service.getActiveNotifications(); + + widget.service.getActiveAmountStream().listen((data) { + setState(() {}); }); - widget.config.service.addListener(_listener); - } - - @override - void dispose() { - widget.config.service.removeListener(_listener); - super.dispose(); - } - - void _listener() { - setState(() {}); } @override @@ -67,8 +60,8 @@ class NotificationCenterState extends State { ), ), ), - body: FutureBuilder>( - future: _notificationsFuture, + body: StreamBuilder>( + stream: _notificationsStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -101,7 +94,7 @@ class NotificationCenterState extends State { await _navigateToNotificationDetail( context, notification, - widget.config.service, + widget.service, widget.config.translations, ); } @@ -111,7 +104,7 @@ class NotificationCenterState extends State { onDismissed: (direction) async { if (direction == DismissDirection.endToStart) { await unPinNotification( - widget.config.service, + widget.service, notification, widget.config.translations, context, @@ -119,7 +112,7 @@ class NotificationCenterState extends State { } else if (direction == DismissDirection.startToEnd) { await unPinNotification( - widget.config.service, + widget.service, notification, widget.config.translations, context, @@ -183,7 +176,7 @@ class NotificationCenterState extends State { await _navigateToNotificationDetail( context, notification, - widget.config.service, + widget.service, widget.config.translations, ); } @@ -193,7 +186,7 @@ class NotificationCenterState extends State { onDismissed: (direction) async { if (direction == DismissDirection.endToStart) { await dismissNotification( - widget.config.service, + widget.service, notification, widget.config.translations, context, @@ -201,7 +194,7 @@ class NotificationCenterState extends State { } else if (direction == DismissDirection.startToEnd) { await pinNotification( - widget.config.service, + widget.service, notification, widget.config.translations, context, @@ -270,8 +263,11 @@ Widget _notificationItem( NotificationConfig config, ) { var theme = Theme.of(context); - var dateTimePushed = - DateFormat("dd/MM/yyyy 'at' HH:mm").format(notification.dateTimePushed!); + String? dateTimePushed; + if (notification.dateTimePushed != null) { + dateTimePushed = DateFormat("dd/MM/yyyy 'at' HH:mm") + .format(notification.dateTimePushed!); + } return Padding( padding: const EdgeInsets.only(bottom: 8), child: Container( @@ -332,7 +328,7 @@ Widget _notificationItem( ], ), Text( - dateTimePushed, + dateTimePushed ?? "", style: theme.textTheme.labelSmall, ), ], diff --git a/packages/flutter_notification_center/lib/src/notification_bell.dart b/packages/flutter_notification_center/lib/src/screens/notification_bell.dart similarity index 72% rename from packages/flutter_notification_center/lib/src/notification_bell.dart rename to packages/flutter_notification_center/lib/src/screens/notification_bell.dart index f385304..59e9614 100644 --- a/packages/flutter_notification_center/lib/src/notification_bell.dart +++ b/packages/flutter_notification_center/lib/src/screens/notification_bell.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; +import "package:flutter_animated_widgets/flutter_animated_widgets.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; /// A bell icon widget that displays the number of active notifications. /// @@ -15,6 +16,8 @@ class NotificationBell extends StatefulWidget { /// [onTap]: Callback function to be invoked when the bell icon is tapped. const NotificationBell({ required this.config, + required this.service, + this.animatedIconStyle, this.onTap, super.key, }); @@ -23,6 +26,12 @@ class NotificationBell extends StatefulWidget { /// the notification service. final NotificationConfig config; + /// The notification service used to fetch active notifications. + final NotificationService service; + + /// The style of the animated bell icon. + final AnimatedNotificationBellStyle? animatedIconStyle; + /// Callback function to be invoked when the bell icon is tapped. final VoidCallback? onTap; @@ -37,7 +46,7 @@ class _NotificationBellState extends State { @override void initState() { super.initState(); - widget.config.service.getActiveAmountStream().listen((amount) { + widget.service.getActiveAmountStream().listen((amount) { setState(() { notificationAmount = amount; }); @@ -51,7 +60,8 @@ class _NotificationBellState extends State { icon: AnimatedNotificationBell( duration: const Duration(seconds: 1), notificationCount: notificationAmount, - style: widget.config.bellStyle, + style: + widget.animatedIconStyle ?? const AnimatedNotificationBellStyle(), ), ); } diff --git a/packages/flutter_notification_center/lib/src/notification_detail.dart b/packages/flutter_notification_center/lib/src/screens/notification_detail.dart similarity index 95% rename from packages/flutter_notification_center/lib/src/notification_detail.dart rename to packages/flutter_notification_center/lib/src/screens/notification_detail.dart index e506793..602a611 100644 --- a/packages/flutter_notification_center/lib/src/notification_detail.dart +++ b/packages/flutter_notification_center/lib/src/screens/notification_detail.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:intl/intl.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; /// A page displaying the details of a notification. class NotificationDetailPage extends StatelessWidget { diff --git a/packages/flutter_notification_center/lib/src/services/notification_service.dart b/packages/flutter_notification_center/lib/src/services/notification_service.dart deleted file mode 100644 index 397b31a..0000000 --- a/packages/flutter_notification_center/lib/src/services/notification_service.dart +++ /dev/null @@ -1,64 +0,0 @@ -import "dart:async"; - -import "package:flutter/material.dart"; -import "package:flutter_notification_center/src/models/notification.dart"; - -/// An abstract class representing a service for managing notifications. -abstract class NotificationService with ChangeNotifier { - /// Creates a new [NotificationService] instance. - /// - /// The [listOfActiveNotifications] parameter specifies the - /// list of active notifications, - /// with a default value of an empty list. - /// - /// The [listOfPlannedNotifications] parameter specifies the - /// list of planned notifications, - /// with a default value of an empty list. - NotificationService({ - this.listOfActiveNotifications = const [], - this.listOfPlannedNotifications = const [], - }); - - /// A list of active notifications. - List listOfActiveNotifications; - - /// A list of planned notifications. - List listOfPlannedNotifications; - - /// Pushes a notification to the service. - Future pushNotification( - NotificationModel notification, - List recipientIds, [ - Function(NotificationModel model)? onNewNotification, - ]); - - /// Retrieves the list of active notifications. - Future> getActiveNotifications(); - - /// Creates a scheduled notification. - Future createScheduledNotification(NotificationModel notification); - - /// Creates a recurring notification. - Future createRecurringNotification(NotificationModel notification); - - /// Deletes a scheduled notification. - Future deletePlannedNotification(NotificationModel notification); - - /// Dismisses an active notification. - Future dismissActiveNotification(NotificationModel notification); - - /// Pin an active notification. - Future pinActiveNotification(NotificationModel notification); - - /// Unpin an active notification. - Future unPinActiveNotification(NotificationModel notification); - - /// Marks a notification as read. - Future markNotificationAsRead(NotificationModel notification); - - /// Checks for scheduled notifications. - Future checkForScheduledNotifications(); - - /// Returns a stream of the number of active notifications. - Stream getActiveAmountStream(); -} diff --git a/packages/flutter_notification_center/lib/src/notification_dialog.dart b/packages/flutter_notification_center/lib/src/widgets/notification_dialog.dart similarity index 94% rename from packages/flutter_notification_center/lib/src/notification_dialog.dart rename to packages/flutter_notification_center/lib/src/widgets/notification_dialog.dart index caf3314..cc1ebb5 100644 --- a/packages/flutter_notification_center/lib/src/notification_dialog.dart +++ b/packages/flutter_notification_center/lib/src/widgets/notification_dialog.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:intl/intl.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; class NotificationDialog extends StatelessWidget { const NotificationDialog({ diff --git a/packages/flutter_notification_center/lib/src/notification_snackbar.dart b/packages/flutter_notification_center/lib/src/widgets/notification_snackbar.dart similarity index 93% rename from packages/flutter_notification_center/lib/src/notification_snackbar.dart rename to packages/flutter_notification_center/lib/src/widgets/notification_snackbar.dart index 5469ba8..da2c09c 100644 --- a/packages/flutter_notification_center/lib/src/notification_snackbar.dart +++ b/packages/flutter_notification_center/lib/src/widgets/notification_snackbar.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:intl/intl.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; class NotificationSnackbar extends SnackBar { NotificationSnackbar({ diff --git a/packages/flutter_notification_center/lib/src/popup_handler.dart b/packages/flutter_notification_center/lib/src/widgets/popup_handler.dart similarity index 81% rename from packages/flutter_notification_center/lib/src/popup_handler.dart rename to packages/flutter_notification_center/lib/src/widgets/popup_handler.dart index a6dc6ae..6194519 100644 --- a/packages/flutter_notification_center/lib/src/popup_handler.dart +++ b/packages/flutter_notification_center/lib/src/widgets/popup_handler.dart @@ -1,7 +1,8 @@ // Define a PopupHandler class to handle notification popups import "package:flutter/material.dart"; - -import "package:flutter_notification_center/flutter_notification_center.dart"; +import "package:flutter_notification_center/src/widgets/notification_dialog.dart"; +import "package:flutter_notification_center/src/widgets/notification_snackbar.dart"; +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; class PopupHandler { PopupHandler({ diff --git a/packages/flutter_notification_center/pubspec.yaml b/packages/flutter_notification_center/pubspec.yaml index 66c1b4b..0bfef6a 100644 --- a/packages/flutter_notification_center/pubspec.yaml +++ b/packages/flutter_notification_center/pubspec.yaml @@ -1,32 +1,37 @@ name: flutter_notification_center -description: "A Flutter package for displaying notifications in a notification center." -publish_to: "none" -version: 4.0.0 +description: "A new Flutter package project." +version: 5.0.0 +homepage: +publish_to: 'none' environment: - sdk: ">=3.3.2 <4.0.0" + sdk: ^3.5.3 + flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - intl: any + + intl: ^0.19.0 + flutter_svg: ^2.0.10+1 flutter_animated_widgets: git: url: https://github.com/Iconica-Development/flutter_animated_widgets ref: 0.3.1 - flutter_svg: ^2.0.10+1 + + notification_center_repository_interface: + git: + url: https://github.com/Iconica-Development/flutter_notification_center.git + path: packages/notification_center_repository_interface + ref: 5.0.0 dev_dependencies: - flutter_test: - sdk: flutter flutter_iconica_analysis: git: url: https://github.com/Iconica-Development/flutter_iconica_analysis ref: 7.0.0 -# The following section is specific to Flutter packages. flutter: - uses-material-design: true assets: - - assets/ + - assets/ \ No newline at end of file diff --git a/packages/flutter_notification_center/test/widget_test.dart b/packages/flutter_notification_center/test/widget_test.dart deleted file mode 100644 index d327631..0000000 --- a/packages/flutter_notification_center/test/widget_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-License-Identifier: GPL-3.0-or-later - -import "package:flutter_test/flutter_test.dart"; - -void main() { - test("test", () { - expect(true, true); - }); -} diff --git a/packages/flutter_notification_center_firebase/lib/flutter_notification_center_firebase.dart b/packages/flutter_notification_center_firebase/lib/flutter_notification_center_firebase.dart deleted file mode 100644 index c32078a..0000000 --- a/packages/flutter_notification_center_firebase/lib/flutter_notification_center_firebase.dart +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -export "src/services/firebase_notification_service.dart"; diff --git a/packages/flutter_notification_center_firebase/lib/src/services/firebase_notification_service.dart b/packages/flutter_notification_center_firebase/lib/src/services/firebase_notification_service.dart deleted file mode 100644 index 1cbc6ed..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/services/firebase_notification_service.dart +++ /dev/null @@ -1,420 +0,0 @@ -import "dart:async"; - -import "package:cloud_firestore/cloud_firestore.dart"; -import "package:firebase_auth/firebase_auth.dart"; -import "package:firebase_core/firebase_core.dart"; -import "package:flutter/material.dart"; -import "package:flutter_notification_center/flutter_notification_center.dart"; - -class FirebaseNotificationService - with ChangeNotifier - implements NotificationService { - FirebaseNotificationService({ - required this.newNotificationCallback, - this.firebaseApp, - this.activeNotificationsCollection = "active_notifications", - this.plannedNotificationsCollection = "planned_notifications", - this.listOfActiveNotifications = const [], - this.listOfPlannedNotifications = const [], - }) { - _firebaseApp = firebaseApp ?? Firebase.app(); - unawaited(_startTimer()); - } - final Function(NotificationModel) newNotificationCallback; - final FirebaseApp? firebaseApp; - final String activeNotificationsCollection; - final String plannedNotificationsCollection; - late FirebaseApp _firebaseApp; - - @override - List listOfActiveNotifications; - @override - List listOfPlannedNotifications; - - // ignore: unused_field - late Timer _timer; - - Future _startTimer() async { - _timer = Timer.periodic(const Duration(seconds: 15), (timer) async { - await checkForScheduledNotifications(); - }); - } - - @override - Future pushNotification( - NotificationModel notification, - List recipientIds, [ - Function(NotificationModel model)? onNewNotification, - ]) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - for (var recipientId in recipientIds) { - CollectionReference notifications = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(recipientId) - .collection(activeNotificationsCollection); - - var currentDateTime = DateTime.now(); - notification.dateTimePushed = currentDateTime; - var notificationMap = notification.toMap(); - await notifications.doc(notification.id).set(notificationMap); - } - if (recipientIds.contains(userId)) { - listOfActiveNotifications = [ - ...listOfActiveNotifications, - notification, - ]; - - //Show popup with notification conte - if (onNewNotification != null) { - onNewNotification(notification); - } else { - newNotificationCallback(notification); - } - } - - notifyListeners(); - } on Exception catch (e) { - debugPrint("Error creating document: $e"); - } - } - - @override - Future> getActiveNotifications() async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return []; - } - - CollectionReference activeNotificationsResult = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection); - - var querySnapshot = await activeNotificationsResult.get(); - - var activeNotifications = querySnapshot.docs.map((doc) { - var data = doc.data()! as Map; - data["id"] = doc.id; - return NotificationModel.fromJson(data); - }).toList(); - - listOfActiveNotifications = List.from(activeNotifications); - - listOfActiveNotifications.removeWhere((element) => element.isPinned); - activeNotifications - .sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!)); - - listOfActiveNotifications - .sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!)); - - listOfActiveNotifications.insertAll( - 0, - activeNotifications.where((element) => element.isPinned), - ); - - notifyListeners(); - return listOfActiveNotifications; - } on Exception catch (e) { - debugPrint("Error getting active notifications: $e"); - return []; - } - } - - @override - Future createRecurringNotification( - NotificationModel notification, - ) async { - if (notification.recurring) { - switch (notification.occuringInterval) { - case OcurringInterval.daily: - notification.scheduledFor = - DateTime.now().add(const Duration(days: 1)); - break; - case OcurringInterval.weekly: - notification.scheduledFor = - DateTime.now().add(const Duration(days: 7)); - break; - case OcurringInterval.monthly: - notification.scheduledFor = - DateTime.now().add(const Duration(days: 30)); - break; - case OcurringInterval.debug: - notification.scheduledFor = - DateTime.now().add(const Duration(seconds: 10)); - break; - case null: - } - await createScheduledNotification(notification); - } - } - - @override - Future createScheduledNotification( - NotificationModel notification, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - CollectionReference plannedNotifications = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(plannedNotificationsCollection) - .doc(userId) - .collection(plannedNotificationsCollection); - - var notificationMap = notification.toMap(); - await plannedNotifications.doc(notification.id).set(notificationMap); - } on Exception catch (e) { - debugPrint("Error creating document: $e"); - } - } - - @override - Future deletePlannedNotification( - NotificationModel notificationModel, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - DocumentReference documentReference = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(plannedNotificationsCollection) - .doc(userId) - .collection(plannedNotificationsCollection) - .doc(notificationModel.id); - await documentReference.delete(); - - QuerySnapshot querySnapshot = - await FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(plannedNotificationsCollection) - .doc(userId) - .collection(plannedNotificationsCollection) - .get(); - - if (querySnapshot.docs.isEmpty) { - debugPrint("The collection is now empty"); - } else { - debugPrint( - "Deleted planned notification with title: ${notificationModel.title}", - ); - } - } on Exception catch (e) { - debugPrint("Error deleting document: $e"); - } - } - - @override - Future dismissActiveNotification( - NotificationModel notificationModel, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - DocumentReference documentReference = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection) - .doc(notificationModel.id); - await documentReference.delete(); - listOfActiveNotifications - .removeWhere((element) => element.id == notificationModel.id); - notifyListeners(); - } on Exception catch (e) { - debugPrint("Error deleting document: $e"); - } - } - - @override - Future pinActiveNotification( - NotificationModel notificationModel, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - DocumentReference documentReference = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection) - .doc(notificationModel.id); - await documentReference.update({"isPinned": true}); - notificationModel.isPinned = true; - - listOfActiveNotifications - .removeAt(listOfActiveNotifications.indexOf(notificationModel)); - listOfActiveNotifications.insert(0, notificationModel); - - notifyListeners(); - } on Exception catch (e) { - debugPrint("Error updating document: $e"); - } - } - - @override - Future unPinActiveNotification( - NotificationModel notificationModel, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - DocumentReference documentReference = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection) - .doc(notificationModel.id); - await documentReference.update({"isPinned": false}); - notificationModel.isPinned = false; - - listOfActiveNotifications - .removeAt(listOfActiveNotifications.indexOf(notificationModel)); - - listOfActiveNotifications.add(notificationModel); - - notifyListeners(); - } on Exception catch (e) { - debugPrint("Error updating document: $e"); - } - } - - @override - Future markNotificationAsRead( - NotificationModel notificationModel, - ) async { - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - DocumentReference documentReference = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection) - .doc(notificationModel.id); - await documentReference.update({"isRead": true}); - notificationModel.isRead = true; - notifyListeners(); - } on Exception catch (e) { - debugPrint("Error updating document: $e"); - } - } - - @override - Future checkForScheduledNotifications() async { - var currentTime = DateTime.now(); - try { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - return; - } - - CollectionReference plannedNotificationsResult = - FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(plannedNotificationsCollection) - .doc(userId) - .collection(plannedNotificationsCollection); - - var querySnapshot = await plannedNotificationsResult.get(); - - if (querySnapshot.docs.isEmpty) { - return; - } - - var plannedNotifications = querySnapshot.docs.map((doc) { - var data = doc.data()! as Map; - return NotificationModel.fromJson(data); - }).toList(); - - for (var notification in plannedNotifications) { - if (notification.scheduledFor!.isBefore(currentTime) || - notification.scheduledFor!.isAtSameMomentAs(currentTime)) { - await pushNotification( - notification, - [userId], - newNotificationCallback, - ); - - await deletePlannedNotification(notification); - - //Plan new recurring notification instance - if (notification.recurring) { - var newNotification = NotificationModel( - id: UniqueKey().toString(), - title: notification.title, - body: notification.body, - recurring: true, - occuringInterval: notification.occuringInterval, - scheduledFor: DateTime.now().add(const Duration(seconds: 10)), - ); - await createScheduledNotification(newNotification); - } - } - } - } on Exception catch (e) { - debugPrint("Error getting planned notifications: $e"); - return; - } - } - - @override - Stream getActiveAmountStream() async* { - var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - - if (userId == null) { - debugPrint("User is not authenticated"); - yield 0; - } - - var amount = FirebaseFirestore.instanceFor(app: _firebaseApp) - .collection(activeNotificationsCollection) - .doc(userId) - .collection(activeNotificationsCollection) - .where("isRead", isEqualTo: false) - .snapshots() - .map((e) => e.docs.length); - yield* amount; - } -} diff --git a/packages/flutter_notification_center_firebase/pubspec.yaml b/packages/flutter_notification_center_firebase/pubspec.yaml deleted file mode 100644 index 4f60b98..0000000 --- a/packages/flutter_notification_center_firebase/pubspec.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: flutter_notification_center_firebase -description: "A new Flutter project." -publish_to: "none" -version: 4.0.0 - -environment: - sdk: ">=2.18.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - intl: any - - # Firebase - cloud_firestore: ^4.16.0 - firebase_auth: ^4.2.6 - firebase_core: ^2.5.0 - - cupertino_icons: ^1.0.2 - - flutter_notification_center: - git: - url: https://github.com/Iconica-Development/flutter_notification_center - ref: 4.0.0 - path: packages/flutter_notification_center - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: - uses-material-design: true diff --git a/packages/flutter_notification_center_firebase/test/widget_test.dart b/packages/flutter_notification_center_firebase/test/widget_test.dart deleted file mode 100644 index d1ac233..0000000 --- a/packages/flutter_notification_center_firebase/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// // This is a basic Flutter widget test. -// // -// // To perform an interaction with a widget in your test, use the WidgetTester -// // utility in the flutter_test package. For example, you can send tap and scroll -// // gestures. You can also use WidgetTester to find child widgets in the widget -// // tree, read text, and verify that the values of widget properties are correct. - -// import 'package:flutter/material.dart'; -// import 'package:flutter_test/flutter_test.dart'; - -// import 'package:example/main.dart'; - -// void main() { -// testWidgets('Counter increments smoke test', (WidgetTester tester) async { -// // Build our app and trigger a frame. -// await tester.pumpWidget(const MyApp()); - -// // Verify that our counter starts at 0. -// expect(find.text('0'), findsOneWidget); -// expect(find.text('1'), findsNothing); - -// // Tap the '+' icon and trigger a frame. -// await tester.tap(find.byIcon(Icons.add)); -// await tester.pump(); - -// // Verify that our counter has incremented. -// expect(find.text('0'), findsNothing); -// expect(find.text('1'), findsOneWidget); -// }); -// } diff --git a/packages/notification_center_repository_interface/.gitignore b/packages/notification_center_repository_interface/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/notification_center_repository_interface/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/notification_center_repository_interface/CHANGELOG.md b/packages/notification_center_repository_interface/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/notification_center_repository_interface/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/notification_center_repository_interface/LICENSE b/packages/notification_center_repository_interface/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/notification_center_repository_interface/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/notification_center_repository_interface/README.md b/packages/notification_center_repository_interface/README.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/packages/notification_center_repository_interface/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/notification_center_repository_interface/analysis_options.yaml b/packages/notification_center_repository_interface/analysis_options.yaml new file mode 100644 index 0000000..e2b30bf --- /dev/null +++ b/packages/notification_center_repository_interface/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/analysis_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: \ No newline at end of file diff --git a/packages/notification_center_repository_interface/lib/notification_center_repository_interface.dart b/packages/notification_center_repository_interface/lib/notification_center_repository_interface.dart new file mode 100644 index 0000000..300cce1 --- /dev/null +++ b/packages/notification_center_repository_interface/lib/notification_center_repository_interface.dart @@ -0,0 +1,13 @@ +// Interfaces +export "src/interfaces/notification_repository_interface.dart"; + +// Local +export "src/local/local_notification_repository.dart"; + +// Models +export "src/models/notification.dart"; +export "src/models/notification_config.dart"; +export "src/models/notification_translation.dart"; + +// Services +export "src/services/notification_service.dart"; diff --git a/packages/notification_center_repository_interface/lib/src/interfaces/notification_repository_interface.dart b/packages/notification_center_repository_interface/lib/src/interfaces/notification_repository_interface.dart new file mode 100644 index 0000000..c34fda5 --- /dev/null +++ b/packages/notification_center_repository_interface/lib/src/interfaces/notification_repository_interface.dart @@ -0,0 +1,27 @@ +import "package:notification_center_repository_interface/notification_center_repository_interface.dart"; + +abstract class NotificationRepositoryInterface { + Future addNotification( + String userId, + NotificationModel notification, + List recipientIds, + ); + + Stream getNotification(String userId, String id); + + Stream> getNotifications(String userId); + + Stream> getPlannedNotifications(String userId); + + Future updateNotification( + String userId, + NotificationModel notification, + ); + + Future deleteNotification( + String userId, + String id, + // ignore: avoid_positional_boolean_parameters + bool planned, + ); +} diff --git a/packages/notification_center_repository_interface/lib/src/local/local_notification_repository.dart b/packages/notification_center_repository_interface/lib/src/local/local_notification_repository.dart new file mode 100644 index 0000000..43afc49 --- /dev/null +++ b/packages/notification_center_repository_interface/lib/src/local/local_notification_repository.dart @@ -0,0 +1,92 @@ +import "dart:async"; + +import "package:notification_center_repository_interface/src/interfaces/notification_repository_interface.dart"; +import "package:notification_center_repository_interface/src/models/notification.dart"; +import "package:rxdart/rxdart.dart"; + +class LocalNotificationRepository implements NotificationRepositoryInterface { + final List _activeNotifications = []; + final List _plannedNotifications = []; + + final StreamController> _notificationsController = + BehaviorSubject>(); + + final StreamController> + _plannedNotificationsController = + BehaviorSubject>(); + + final StreamController _notificationController = + BehaviorSubject(); + + @override + Future addNotification( + String userId, + NotificationModel notification, + List recipientIds, + ) async { + if (notification.scheduledFor != null && + notification.scheduledFor!.isAfter(DateTime.now())) { + _plannedNotifications.add(notification); + } else { + _activeNotifications.add(notification); + } + + getNotifications(userId); + + return notification; + } + + @override + Future deleteNotification( + String userId, + String id, + bool planned, + ) async { + if (planned) { + _plannedNotifications.removeWhere((element) => element.id == id); + } else { + _activeNotifications.removeWhere((element) => element.id == id); + } + + getNotifications(userId); + } + + @override + Stream getNotification(String userId, String id) { + var notification = _activeNotifications.firstWhere( + (element) => element.id == id, + ); + + _notificationController.add(notification); + + return _notificationController.stream; + } + + @override + Stream> getNotifications(String userId) { + _notificationsController.add(_activeNotifications); + + return _notificationsController.stream; + } + + @override + Stream> getPlannedNotifications(String userId) { + _plannedNotificationsController.add(_plannedNotifications); + + return _plannedNotificationsController.stream; + } + + @override + Future updateNotification( + String userId, + NotificationModel notification, + ) async { + _activeNotifications + .removeWhere((element) => element.id == notification.id); + + _activeNotifications.add(notification); + getNotifications(userId); + + return notification; + } +} diff --git a/packages/flutter_notification_center/lib/src/models/notification.dart b/packages/notification_center_repository_interface/lib/src/models/notification.dart similarity index 90% rename from packages/flutter_notification_center/lib/src/models/notification.dart rename to packages/notification_center_repository_interface/lib/src/models/notification.dart index edcfbf1..b90de12 100644 --- a/packages/flutter_notification_center/lib/src/models/notification.dart +++ b/packages/notification_center_repository_interface/lib/src/models/notification.dart @@ -1,5 +1,3 @@ -import "package:flutter/material.dart"; - /// Enum representing the interval at which notifications occur. enum OcurringInterval { /// Notifications occur daily. @@ -39,7 +37,7 @@ class NotificationModel { this.occuringInterval, this.isPinned = false, this.isRead = false, - this.icon = Icons.notifications, + this.icon, }); /// Method to create a NotificationModel object from JSON data @@ -59,9 +57,7 @@ class NotificationModel { : null, isPinned = json["isPinned"] ?? false, isRead = json["isRead"] ?? false, - icon = json["icon"] != null - ? IconData(json["icon"], fontFamily: Icons.notifications.fontFamily) - : Icons.notifications; + icon = json["icon"] ?? 0xe44f; /// Unique identifier for the notification. final String id; @@ -91,14 +87,16 @@ class NotificationModel { bool isRead; /// Icon to be displayed with the notification. - final IconData icon; + final int? icon; /// Override toString() to provide custom string representation @override - String toString() => "NotificationModel{id: $id, title: $title, body: $body, " + String toString() => """ +NotificationModel{id: $id, title: $title, body: $body, " "dateTimePushed: $dateTimePushed, scheduledFor: $scheduledFor, " "recurring: $recurring, occuringInterval: $occuringInterval, " - "isPinned: $isPinned, icon: $icon}"; + "isPinned: $isPinned, icon: $icon + }"""; /// Convert the NotificationModel object to a Map. Map toMap() => { @@ -111,7 +109,7 @@ class NotificationModel { "occuringInterval": occuringInterval?.index, "isPinned": isPinned, "isRead": isRead, - "icon": icon.codePoint, + "icon": icon, }; /// Create a copy of the NotificationModel with some fields replaced. @@ -125,7 +123,7 @@ class NotificationModel { OcurringInterval? occuringInterval, bool? isPinned, bool? isRead, - IconData? icon, + int? icon, }) => NotificationModel( id: id ?? this.id, diff --git a/packages/flutter_notification_center/lib/src/models/notification_config.dart b/packages/notification_center_repository_interface/lib/src/models/notification_config.dart similarity index 80% rename from packages/flutter_notification_center/lib/src/models/notification_config.dart rename to packages/notification_center_repository_interface/lib/src/models/notification_config.dart index 6e3214e..05abb2d 100644 --- a/packages/flutter_notification_center/lib/src/models/notification_config.dart +++ b/packages/notification_center_repository_interface/lib/src/models/notification_config.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; - -import "package:flutter_notification_center/flutter_notification_center.dart"; +import "package:notification_center_repository_interface/src/models/notification.dart"; +import "package:notification_center_repository_interface/src/models/notification_translation.dart"; /// Configuration class for notifications. class NotificationConfig { @@ -11,20 +11,15 @@ class NotificationConfig { /// notification. The [translations] parameter is also optional and provides /// translations for notification messages. const NotificationConfig({ - required this.service, this.translations = const NotificationTranslations.empty(), this.notificationWidgetBuilder, this.showAsSnackBar = true, this.enableNotificationPopups = true, - this.bellStyle = const AnimatedNotificationBellStyle(), this.pinnedIconColor = Colors.black, this.emptyNotificationsBuilder, this.onNotificationTap, }); - /// The notification service to use for delivering notifications. - final NotificationService service; - /// Translations for notification messages. final NotificationTranslations translations; @@ -39,9 +34,6 @@ class NotificationConfig { /// Whether to show notification popups. final bool enableNotificationPopups; - /// The style of the notification bell. - final AnimatedNotificationBellStyle bellStyle; - /// The color of the trailing icon (if any) in the notification. final Color? pinnedIconColor; diff --git a/packages/flutter_notification_center/lib/src/models/notification_translation.dart b/packages/notification_center_repository_interface/lib/src/models/notification_translation.dart similarity index 100% rename from packages/flutter_notification_center/lib/src/models/notification_translation.dart rename to packages/notification_center_repository_interface/lib/src/models/notification_translation.dart diff --git a/packages/notification_center_repository_interface/lib/src/services/notification_service.dart b/packages/notification_center_repository_interface/lib/src/services/notification_service.dart new file mode 100644 index 0000000..d1bf19d --- /dev/null +++ b/packages/notification_center_repository_interface/lib/src/services/notification_service.dart @@ -0,0 +1,157 @@ +import "dart:async"; + +import "package:notification_center_repository_interface/src/interfaces/notification_repository_interface.dart"; +import "package:notification_center_repository_interface/src/local/local_notification_repository.dart"; +import "package:notification_center_repository_interface/src/models/notification.dart"; + +class NotificationService { + NotificationService({ + required this.userId, + this.pollingInterval = const Duration(seconds: 15), + NotificationRepositoryInterface? repository, + this.onNewNotification, + }) : repository = repository ?? LocalNotificationRepository() { + unawaited(_startTimer()); + } + + final NotificationRepositoryInterface repository; + final Function(NotificationModel)? onNewNotification; + final String userId; + final Duration pollingInterval; + + Timer? timer; + + Future _startTimer() async { + timer = Timer.periodic(pollingInterval, (timer) async { + await checkForScheduledNotifications(); + }); + } + + /// Pushes a notification to the service. + Future pushNotification( + NotificationModel notification, + List recipientIds, + ) { + var result = repository.addNotification( + userId, + notification, + recipientIds, + ); + + if (recipientIds.contains(userId)) { + getActiveNotifications(); + + //Show popup with notification conte + if (onNewNotification != null) { + onNewNotification!.call(notification); + } + } + + return result; + } + + /// Retrieves the list of active notifications. + Stream> getActiveNotifications() => + repository.getNotifications(userId); + + /// Retrieves the list of planned notifications. + Stream> getPlannedNotifications() => + repository.getPlannedNotifications(userId); + + /// Creates a scheduled notification. + Future createScheduledNotification( + NotificationModel notification, + List recipientIds, + ) async => + pushNotification( + notification, + recipientIds, + ); + + /// Creates a recurring notification. + Future createRecurringNotification( + NotificationModel notification, + List recipientIds, + ) async { + if (notification.recurring) { + switch (notification.occuringInterval) { + case OcurringInterval.daily: + notification.scheduledFor = + DateTime.now().add(const Duration(days: 1)); + + case OcurringInterval.weekly: + notification.scheduledFor = + DateTime.now().add(const Duration(days: 7)); + + case OcurringInterval.monthly: + notification.scheduledFor = + DateTime.now().add(const Duration(days: 30)); + + case OcurringInterval.debug: + notification.scheduledFor = + DateTime.now().add(const Duration(seconds: 10)); + + case null: + } + await createScheduledNotification(notification, recipientIds); + } + } + + /// Deletes a scheduled notification. + Future deletePlannedNotification(NotificationModel notification) => + repository.deleteNotification(userId, notification.id, true); + + /// Dismisses an active notification. + Future dismissActiveNotification(NotificationModel notification) => + repository.deleteNotification(userId, notification.id, false); + + /// Pin an active notification. + Future pinActiveNotification(NotificationModel notification) => + repository.updateNotification( + userId, + notification.copyWith(isPinned: true), + ); + + /// Unpin an active notification. + Future unPinActiveNotification(NotificationModel notification) => + repository.updateNotification( + userId, + notification.copyWith(isPinned: false), + ); + + /// Marks a notification as read. + Future markNotificationAsRead(NotificationModel notification) => + repository.updateNotification( + userId, + notification.copyWith(isRead: true), + ); + + /// Checks for scheduled notifications. + Future checkForScheduledNotifications() async { + var notifications = await repository.getPlannedNotifications(userId).first; + + for (var notification in notifications) { + if (notification.scheduledFor != null && + notification.scheduledFor!.isBefore(DateTime.now()) || + notification.scheduledFor!.isAtSameMomentAs(DateTime.now())) { + await pushNotification( + notification, + [userId], + ); + + await deletePlannedNotification(notification); + if (notification.recurring) { + await createRecurringNotification( + notification, + [userId], + ); + } + } + } + } + + /// Returns a stream of the number of active notifications. + Stream getActiveAmountStream() => repository + .getNotifications(userId) + .map((notifications) => notifications.length); +} diff --git a/packages/notification_center_repository_interface/pubspec.yaml b/packages/notification_center_repository_interface/pubspec.yaml new file mode 100644 index 0000000..bb15ec7 --- /dev/null +++ b/packages/notification_center_repository_interface/pubspec.yaml @@ -0,0 +1,20 @@ +name: notification_center_repository_interface +description: "A new Flutter package project." +version: 5.0.0 +homepage: + +environment: + sdk: ^3.5.3 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + rxdart: any + +dev_dependencies: + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0 \ No newline at end of file