diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..19f6059 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 + +updates: + - package-ecosystem: "pub" + directory: "/packages/flutter_notification_center" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "/packages/flutter_notification_center_firebase" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "/packages/flutter_notification_center/example" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/document-component.yml b/.github/workflows/document-component.yml new file mode 100644 index 0000000..4bd69a0 --- /dev/null +++ b/.github/workflows/document-component.yml @@ -0,0 +1,14 @@ +name: Iconica Standard Component Documentation Workflow +# Workflow Caller version: 1.0.0 + +on: + release: + types: [published] + + workflow_dispatch: + +jobs: + call-iconica-component-documentation-workflow: + uses: Iconica-Development/.github/.github/workflows/component-documentation.yml@master + secrets: inherit + permissions: write-all \ No newline at end of file diff --git a/.github/workflows/melos-ci.yml b/.github/workflows/melos-ci.yml new file mode 100644 index 0000000..9b5ed36 --- /dev/null +++ b/.github/workflows/melos-ci.yml @@ -0,0 +1,14 @@ +name: Iconica Standard Melos CI Workflow +# Workflow Caller version: 1.0.0 + +on: + pull_request: + workflow_dispatch: + +jobs: + call-global-iconica-workflow: + uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master + secrets: inherit + permissions: write-all + with: + subfolder: '.' # add optional subfolder to run workflow in \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f2812..57099d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.4.0] - 29 May 2024 + +* Make notifications user bound and not app bound (https://github.com/Iconica-Development/flutter_notification_center/issues/8) +* Fix indicator amount of bell icon (https://github.com/Iconica-Development/flutter_notification_center/issues/10) +* Add unpin functionality (https://github.com/Iconica-Development/flutter_notification_center/issues/15) +* Change color of bin icon (https://github.com/Iconica-Development/flutter_notification_center/issues/19) +* Fix multiple dialog and snackbars stacking on top +* Fix notification center updating when new notifications come in and the screen is open +* Fix sorting of all notifications +* Fix pinned notifications should remain on top + ## [1.3.1] - 30 April 2024 * Fix Animationcontroller not disposing correctly. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c21bd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2024 Iconica, All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index bbf71fe..0000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/components_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: \ No newline at end of file diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..af2a73a --- /dev/null +++ b/melos.yaml @@ -0,0 +1,39 @@ +name: flutter_notification_center + +packages: + - packages/** + +command: + version: + branch: master + +scripts: + lint:all: + run: dart run melos run analyze && dart run melos run format-check + description: Run all static analysis checks. + + get: + run: | + melos exec -c 1 -- "flutter pub get" + melos exec --scope="*example*" -c 1 -- "flutter pub get" + + upgrade: + run: melos exec -c 1 -- "flutter pub upgrade" + + create: + # run create in the example folder of flutter_chat_view + run: melos exec --scope="*example*" -c 1 -- "flutter create ." + + analyze: + run: | + dart run melos exec -c 1 -- \ + flutter analyze --fatal-infos + description: Run `flutter analyze` for all packages. + + format: + run: dart run melos exec dart format . + description: Run `dart format` for all packages. + + format-check: + run: dart run melos exec dart format . --set-exit-if-changed + description: Run `dart format` checks for all packages. \ No newline at end of file diff --git a/packages/flutter_notification_center/example/.gitignore b/packages/flutter_notification_center/example/.gitignore index 46d0fec..57cc60d 100644 --- a/packages/flutter_notification_center/example/.gitignore +++ b/packages/flutter_notification_center/example/.gitignore @@ -49,4 +49,4 @@ app.*.map.json lib/config/ pubspec.lock dotenv - +firebase_options.dart diff --git a/packages/flutter_notification_center/example/lib/custom_notification.dart b/packages/flutter_notification_center/example/lib/custom_notification.dart index 9561e79..d1d2185 100644 --- a/packages/flutter_notification_center/example/lib/custom_notification.dart +++ b/packages/flutter_notification_center/example/lib/custom_notification.dart @@ -1,3 +1,5 @@ +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'; @@ -22,33 +24,62 @@ class CustomNotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return notification.isPinned //Pinned notification - ? GestureDetector( - onTap: () async => - _navigateToNotificationDetail(context, notification), - child: ListTile( - leading: style.showNotificationIcon != null - ? Icon( - notification.icon, - color: style.leadingIconColor, - ) - : null, - title: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - notification.title, - style: style.titleTextStyle, - ), - ), - ], + ? 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, + ), ), - trailing: IconButton( - icon: const Icon(Icons.push_pin), - color: style.pinnedIconColor, - onPressed: () async => - _navigateToNotificationDetail(context, notification), - padding: const EdgeInsets.only(left: 60.0), + ), + 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( + leading: style.showNotificationIcon != null + ? Icon( + notification.icon, + color: style.leadingIconColor, + ) + : null, + title: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + notification.title, + style: style.titleTextStyle, + ), + ), + ], + ), + trailing: IconButton( + icon: const Icon(Icons.push_pin), + color: style.pinnedIconColor, + onPressed: () async => + _navigateToNotificationDetail(context, notification), + padding: const EdgeInsets.only(left: 60.0), + ), ), ), ) @@ -57,10 +88,11 @@ class CustomNotificationWidget extends StatelessWidget { key: Key(notification.id), onDismissed: (direction) async { if (direction == DismissDirection.endToStart) { - await dismissNotification(notificationService, notification); + await dismissNotification(notificationService, notification, + notificationTranslations, context); } else if (direction == DismissDirection.startToEnd) { - await pinNotification( - notificationService, notification, context); + await pinNotification(notificationService, notification, + notificationTranslations, context); } }, background: Container( @@ -81,7 +113,7 @@ class CustomNotificationWidget extends StatelessWidget { padding: EdgeInsets.only(right: 16.0), child: Icon( Icons.delete, - color: Colors.white, + color: Colors.black, ), ), ), @@ -130,7 +162,7 @@ class CustomNotificationWidget extends StatelessWidget { BuildContext context, NotificationModel notification, ) async { - await markNotificationAsRead(notificationService, notification); + unawaited(markNotificationAsRead(notificationService, notification)); if (context.mounted) { await Navigator.push( context, @@ -144,25 +176,4 @@ class CustomNotificationWidget extends StatelessWidget { ); } } - - Future dismissNotification( - FirebaseNotificationService notificationService, - NotificationModel notification, - ) async { - await notificationService.dismissActiveNotification(notification); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Notification dismissed"), - ), - ); - } - } - - Future markNotificationAsRead( - FirebaseNotificationService notificationService, - NotificationModel notification, - ) async { - await notificationService.markNotificationAsRead(notification); - } } diff --git a/packages/flutter_notification_center/example/lib/main.dart b/packages/flutter_notification_center/example/lib/main.dart index b7a96a8..7c904d0 100644 --- a/packages/flutter_notification_center/example/lib/main.dart +++ b/packages/flutter_notification_center/example/lib/main.dart @@ -1,8 +1,9 @@ import 'package:example/custom_notification.dart'; +// import 'package:firebase_auth/firebase_auth.dart'; +// import 'package:example/firebase_options.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:firebase_core/firebase_core.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'; @@ -21,14 +22,9 @@ void main() async { } Future _configureApp() async { - try { - await dotenv.load(fileName: 'dotenv'); - } catch (e) { - debugPrint('Failed to load dotenv file: $e'); - } - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + // await Firebase.initializeApp( + // options: DefaultFirebaseOptions.currentPlatform, + // ); await SystemChrome.setPreferredOrientations( [ DeviceOrientation.portraitDown, @@ -55,14 +51,15 @@ class _NotificationCenterDemoState extends State { @override void initState() { super.initState(); - var service = - FirebaseNotificationService(newNotificationCallback: (notification) { - popupHandler.handleNotificationPopup(notification); - }); + var service = FirebaseNotificationService( + newNotificationCallback: (notification) { + popupHandler.handleNotificationPopup(notification); + }, + ); config = NotificationConfig( service: service, enableNotificationPopups: true, - showAsSnackBar: false, + showAsSnackBar: true, notificationWidgetBuilder: (notification, context) => CustomNotificationWidget( notification: notification, @@ -82,7 +79,7 @@ class _NotificationCenterDemoState extends State { showNotificationIcon: true, ), notificationService: service, - notificationTranslations: const NotificationTranslations(), + notificationTranslations: const NotificationTranslations.empty(), context: context, ), seperateNotificationsWithDivider: true, diff --git a/packages/flutter_notification_center/example/pubspec.yaml b/packages/flutter_notification_center/example/pubspec.yaml index 1484957..8344608 100644 --- a/packages/flutter_notification_center/example/pubspec.yaml +++ b/packages/flutter_notification_center/example/pubspec.yaml @@ -14,24 +14,17 @@ dependencies: sdk: flutter intl: ^0.17.0 - flutter_animated_widgets: - git: - url: https://github.com/Iconica-Development/flutter_animated_widgets - ref: 0.0.1 - cloud_firestore: ^4.16.1 - - flutter_dotenv: ^5.0.2 - firebase_auth: ^4.2.6 - firebase_core: ^2.5.0 - firebase_storage: ^11.0.14 - flutter_notification_center: git: url: https://github.com/Iconica-Development/flutter_notification_center - ref: 1.3.0 + ref: 1.4.0 path: packages/flutter_notification_center + flutter_notification_center_firebase: - path: ../../flutter_notification_center_firebase + git: + url: https://github.com/Iconica-Development/flutter_notification_center + ref: 1.4.0 + path: packages/flutter_notification_center_firebase @@ -46,6 +39,4 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: uses-material-design: true - assets: - - dotenv diff --git a/packages/flutter_notification_center/lib/src/models/notification_config.dart b/packages/flutter_notification_center/lib/src/models/notification_config.dart index 8ab4527..e09070d 100644 --- a/packages/flutter_notification_center/lib/src/models/notification_config.dart +++ b/packages/flutter_notification_center/lib/src/models/notification_config.dart @@ -13,7 +13,7 @@ class NotificationConfig { const NotificationConfig({ required this.service, this.seperateNotificationsWithDivider = true, - this.translations = const NotificationTranslations(), + this.translations = const NotificationTranslations.empty(), this.notificationWidgetBuilder, this.showAsSnackBar = true, this.enableNotificationPopups = true, diff --git a/packages/flutter_notification_center/lib/src/models/notification_translation.dart b/packages/flutter_notification_center/lib/src/models/notification_translation.dart index cc7ef2d..dc7602d 100644 --- a/packages/flutter_notification_center/lib/src/models/notification_translation.dart +++ b/packages/flutter_notification_center/lib/src/models/notification_translation.dart @@ -1,17 +1,28 @@ /// Defines translations for notification messages. class NotificationTranslations { /// Creates a new [NotificationTranslations] instance. - /// - /// The [appBarTitle] parameter specifies the title to be displayed in the - /// app bar of the notification center. The default value - /// is 'Notification Center'. - /// - /// The [noNotifications] parameter specifies the message to be displayed when - /// there are no unread notifications available. The default value is - /// 'No unread notifications available.'. const NotificationTranslations({ + required this.appBarTitle, + required this.noNotifications, + required this.notificationDismissed, + required this.notificationPinned, + required this.notificationUnpinned, + required this.errorMessage, + required this.datePrefix, + required this.notAvailable, + required this.dissmissDialog, + }); + + const NotificationTranslations.empty({ this.appBarTitle = "Notification Center", this.noNotifications = "No unread notifications available.", + this.notificationDismissed = "Notification dismissed.", + this.notificationPinned = "Notification pinned.", + this.notificationUnpinned = "Notification unpinned.", + this.errorMessage = "An error occurred. Please try again later.", + this.datePrefix = "Date:", + this.notAvailable = "N/A", + this.dissmissDialog = "Dismiss", }); /// The title to be displayed in the app bar of the notification center. @@ -20,4 +31,50 @@ class NotificationTranslations { /// The message to be displayed when there are no unread /// notifications available. final String noNotifications; + + /// The message to be displayed when a notification is dismissed. + final String notificationDismissed; + + /// The message to be displayed when a notification is pinned. + final String notificationPinned; + + /// The message to be displayed when a notification is unpinned. + final String notificationUnpinned; + + /// The message to be displayed when an error occurs. + final String errorMessage; + + /// The message to be displayed before the date of a notification. + final String datePrefix; + + /// The message to be displayed when parsing of the date fails + final String notAvailable; + + /// The message to be displayed on the dismiss dialog / snackbar + final String dissmissDialog; + + NotificationTranslations copyWith({ + String? appBarTitle, + String? noNotifications, + String? notificationDismissed, + String? notificationPinned, + String? notificationUnpinned, + String? errorMessage, + String? datePrefix, + String? notAvailable, + String? dissmissDialog, + }) { + return NotificationTranslations( + appBarTitle: appBarTitle ?? this.appBarTitle, + noNotifications: noNotifications ?? this.noNotifications, + notificationDismissed: + notificationDismissed ?? this.notificationDismissed, + notificationPinned: notificationPinned ?? this.notificationPinned, + notificationUnpinned: notificationUnpinned ?? this.notificationUnpinned, + errorMessage: errorMessage ?? this.errorMessage, + datePrefix: datePrefix ?? this.datePrefix, + notAvailable: notAvailable ?? this.notAvailable, + dissmissDialog: dissmissDialog ?? this.dissmissDialog, + ); + } } diff --git a/packages/flutter_notification_center/lib/src/notification_bell.dart b/packages/flutter_notification_center/lib/src/notification_bell.dart index a0247d8..ebaf229 100644 --- a/packages/flutter_notification_center/lib/src/notification_bell.dart +++ b/packages/flutter_notification_center/lib/src/notification_bell.dart @@ -37,13 +37,9 @@ class _NotificationBellState extends State { @override void initState() { super.initState(); - - // Fetch active notifications and update the notification count - WidgetsBinding.instance.addPostFrameCallback((_) async { - var amount = await widget.config.service.getActiveNotifications(); - + widget.config.service.getActiveAmountStream().listen((amount) { setState(() { - notificationAmount = amount.length; + notificationAmount = amount; }); }); } diff --git a/packages/flutter_notification_center/lib/src/notification_center.dart b/packages/flutter_notification_center/lib/src/notification_center.dart index 91f4223..2378155 100644 --- a/packages/flutter_notification_center/lib/src/notification_center.dart +++ b/packages/flutter_notification_center/lib/src/notification_center.dart @@ -27,6 +27,9 @@ class NotificationCenterState extends State { super.initState(); // ignore: discarded_futures _notificationsFuture = widget.config.service.getActiveNotifications(); + widget.config.service.getActiveAmountStream().listen((amount) { + _notificationsFuture = widget.config.service.getActiveNotifications(); + }); widget.config.service.addListener(_listener); } @@ -60,10 +63,12 @@ class NotificationCenterState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { - return Center(child: Text("Error: ${snapshot.error}")); + debugPrint("Error: ${snapshot.error}"); + return Center( + child: Text(widget.config.translations.errorMessage)); } else if (snapshot.data == null || snapshot.data!.isEmpty) { - return const Center( - child: Text("No unread notifications available."), + return Center( + child: Text(widget.config.translations.noNotifications), ); } else { return ListView.builder( @@ -88,47 +93,81 @@ class NotificationCenterState extends State { notification, context) : notification.isPinned //Pinned notification - ? GestureDetector( - onTap: () async => - _navigateToNotificationDetail( - context, - notification, - widget.config.service, - widget.config.translations, - const NotificationStyle()), - child: ListTile( - leading: Icon( - notification.icon, - color: Colors.grey, + ? Dismissible( + key: Key('${notification.id}_pinned'), + onDismissed: (direction) async { + await unPinNotification( + widget.config.service, + notification, + widget.config.translations, + 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, + ), ), - title: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - notification.title, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 16, + ), + 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, + widget.config.service, + widget.config.translations, + const NotificationStyle()), + 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: IconButton( - icon: const Icon(Icons.push_pin), - color: Colors.grey, - onPressed: () async => - _navigateToNotificationDetail( - context, - notification, - widget.config.service, - widget.config.translations, - const NotificationStyle()), - padding: - const EdgeInsets.only(left: 60.0), + ], + ), + trailing: IconButton( + icon: const Icon(Icons.push_pin), + color: Colors.grey, + onPressed: () async => + _navigateToNotificationDetail( + context, + notification, + widget.config.service, + widget.config.translations, + const NotificationStyle()), + padding: + const EdgeInsets.only(left: 60.0), + ), ), ), ) @@ -141,12 +180,14 @@ class NotificationCenterState extends State { await dismissNotification( widget.config.service, notification, + widget.config.translations, context); } else if (direction == DismissDirection.startToEnd) { await pinNotification( widget.config.service, notification, + widget.config.translations, context); } }, @@ -170,7 +211,7 @@ class NotificationCenterState extends State { padding: EdgeInsets.only(right: 16.0), child: Icon( Icons.delete, - color: Colors.white, + color: Colors.black, ), ), ), @@ -251,13 +292,15 @@ Future _navigateToNotificationDetail( Future dismissNotification( NotificationService notificationService, NotificationModel notification, + NotificationTranslations notificationTranslations, BuildContext context, ) async { await notificationService.dismissActiveNotification(notification); if (context.mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Notification dismissed"), + SnackBar( + content: Text(notificationTranslations.notificationDismissed), ), ); } @@ -266,13 +309,32 @@ Future dismissNotification( Future pinNotification( NotificationService notificationService, NotificationModel notification, + NotificationTranslations notificationTranslations, BuildContext context, ) async { await notificationService.pinActiveNotification(notification); if (context.mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Notification pinned"), + SnackBar( + content: Text(notificationTranslations.notificationPinned), + ), + ); + } +} + +Future unPinNotification( + NotificationService notificationService, + NotificationModel notification, + NotificationTranslations notificationTranslations, + BuildContext context, +) async { + await notificationService.unPinActiveNotification(notification); + if (context.mounted) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(notificationTranslations.notificationUnpinned), ), ); } diff --git a/packages/flutter_notification_center/lib/src/notification_detail.dart b/packages/flutter_notification_center/lib/src/notification_detail.dart index d5ff647..433a9e5 100644 --- a/packages/flutter_notification_center/lib/src/notification_detail.dart +++ b/packages/flutter_notification_center/lib/src/notification_detail.dart @@ -53,7 +53,7 @@ class NotificationDetailPage extends StatelessWidget { ), const SizedBox(height: 10), Text( - 'Date: ${DateFormat('yyyy-MM-dd HH:mm').format( + '${translations.datePrefix} ${DateFormat('yyyy-MM-dd HH:mm').format( notification.dateTimePushed ?? DateTime.now(), )}', style: const TextStyle( diff --git a/packages/flutter_notification_center/lib/src/notification_dialog.dart b/packages/flutter_notification_center/lib/src/notification_dialog.dart index 209785c..fcdffba 100644 --- a/packages/flutter_notification_center/lib/src/notification_dialog.dart +++ b/packages/flutter_notification_center/lib/src/notification_dialog.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_notification_center/flutter_notification_center.dart'; import 'package:intl/intl.dart'; class NotificationDialog extends StatelessWidget { final String title; final String body; final DateTime? datetimePublished; + final NotificationTranslations translations; const NotificationDialog({ super.key, required this.title, required this.body, + required this.translations, this.datetimePublished, }); @@ -17,7 +20,7 @@ class NotificationDialog extends StatelessWidget { Widget build(BuildContext context) { String formattedDateTime = datetimePublished != null ? DateFormat('dd MMM HH:mm').format(datetimePublished!) - : 'N/A'; + : translations.notAvailable; return AlertDialog( title: Text( @@ -55,9 +58,9 @@ class NotificationDialog extends StatelessWidget { onPressed: () { Navigator.of(context).pop(); }, - child: const Text( - 'Dismiss', - style: TextStyle( + child: Text( + translations.dissmissDialog, + style: const TextStyle( color: Colors.red, ), ), diff --git a/packages/flutter_notification_center/lib/src/notification_snackbar.dart b/packages/flutter_notification_center/lib/src/notification_snackbar.dart index 264936d..db8515f 100644 --- a/packages/flutter_notification_center/lib/src/notification_snackbar.dart +++ b/packages/flutter_notification_center/lib/src/notification_snackbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_notification_center/flutter_notification_center.dart'; import 'package:intl/intl.dart'; class NotificationSnackbar extends SnackBar { @@ -6,6 +7,8 @@ class NotificationSnackbar extends SnackBar { super.key, required String title, required String body, + required NotificationTranslations translations, + required VoidCallback onDismiss, DateTime? datetimePublished, }) : super( content: Column( @@ -31,7 +34,7 @@ class NotificationSnackbar extends SnackBar { Text( datetimePublished != null ? DateFormat('dd MMM HH:mm').format(datetimePublished) - : 'N/A', + : translations.notAvailable, style: const TextStyle( fontSize: 12.0, color: Colors.white, @@ -39,10 +42,10 @@ class NotificationSnackbar extends SnackBar { ), ], ), - duration: const Duration(seconds: 8), + duration: const Duration(seconds: 50), action: SnackBarAction( - label: 'Dismiss', - onPressed: () {}, + label: translations.dissmissDialog, + onPressed: onDismiss, textColor: Colors.white, ), ); diff --git a/packages/flutter_notification_center/lib/src/popup_handler.dart b/packages/flutter_notification_center/lib/src/popup_handler.dart index 863178f..d755b00 100644 --- a/packages/flutter_notification_center/lib/src/popup_handler.dart +++ b/packages/flutter_notification_center/lib/src/popup_handler.dart @@ -16,17 +16,25 @@ class PopupHandler { if (!config.enableNotificationPopups) return; if (config.showAsSnackBar) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( NotificationSnackbar( title: notification.title, body: notification.body, + translations: config.translations, + onDismiss: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, datetimePublished: DateTime.now(), ), ); } else { + if (ModalRoute.of(context)?.isCurrent != true) return; + showDialog( context: context, builder: (context) => NotificationDialog( + translations: config.translations, title: notification.title, body: notification.body, datetimePublished: notification.dateTimePushed, diff --git a/packages/flutter_notification_center/lib/src/services/notification_service.dart b/packages/flutter_notification_center/lib/src/services/notification_service.dart index 22b7f15..473a0dc 100644 --- a/packages/flutter_notification_center/lib/src/services/notification_service.dart +++ b/packages/flutter_notification_center/lib/src/services/notification_service.dart @@ -47,9 +47,15 @@ abstract class NotificationService with ChangeNotifier { /// 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/pubspec.yaml b/packages/flutter_notification_center/pubspec.yaml index bf353af..9095e08 100644 --- a/packages/flutter_notification_center/pubspec.yaml +++ b/packages/flutter_notification_center/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_notification_center description: "A Flutter package for displaying notifications in a notification center." publish_to: 'none' -version: 1.3.1 +version: 1.4.0 environment: sdk: '>=3.3.2 <4.0.0' 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 index d5d82dd..c32078a 100644 --- a/packages/flutter_notification_center_firebase/lib/flutter_notification_center_firebase.dart +++ b/packages/flutter_notification_center_firebase/lib/flutter_notification_center_firebase.dart @@ -3,7 +3,3 @@ // SPDX-License-Identifier: BSD-3-Clause export "src/services/firebase_notification_service.dart"; -export "src/config/firebase_collections.dart"; -export "src/config/environment.dart"; -export "src/config/firebase_options.dart"; -export "src/config/firebase.dart"; diff --git a/packages/flutter_notification_center_firebase/lib/src/config/environment.dart b/packages/flutter_notification_center_firebase/lib/src/config/environment.dart deleted file mode 100644 index 2da7a2c..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/config/environment.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -const _errorMessage = 'Unable to fetch dotenv, did you make sure to generate ' - 'your build environment config?\nUse the command: ' - 'dart pub run environment_config:generate\n' - 'For more information, look at the readme\n' - 'Using default now...'; - -/// This environment config is used for the features inside the package -/// The project that uses this package should have their own environment config -/// the values in the dotenv should atleast include the following: -mixin SharedEnvironmentConfig {} - -/// This environment config is used only for the firebase configuration -mixin SharedFirebaseEnvironmentConfig { - static String get firebaseAppName { - var firebaseAppName = dotenv.env['FIREBASE_APP_NAME']; - if (firebaseAppName == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAppName; - } -} diff --git a/packages/flutter_notification_center_firebase/lib/src/config/environment_config.dart b/packages/flutter_notification_center_firebase/lib/src/config/environment_config.dart deleted file mode 100644 index 9fb5f9a..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/config/environment_config.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -const _errorMessage = 'Unable to fetch dotenv, did you make sure to generate ' - 'your build environment config?\nUse the command: ' - 'flutter pub run environment_config:generate\n' - 'For more information, look at the readme\n' - 'Using default now...'; - -class EnvironmentConfig { - String get firebaseProjectId { - var firebaseProjectId = dotenv.env['FIREBASE_PROJECT_ID']; - if (firebaseProjectId == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseProjectId; - } - - String get firebaseMessageId { - var firebaseMessageId = dotenv.env['FIREBASE_MESSAGE_ID']; - if (firebaseMessageId == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseMessageId; - } - - String get firebaseAuthDomain { - var firebaseAuthDomain = dotenv.env['FIREBASE_AUTH_DOMAIN']; - if (firebaseAuthDomain == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAuthDomain; - } - - String get firebaseStorageUrl { - var firebaseStorageUrl = dotenv.env['FIREBASE_STORAGE_URL']; - if (firebaseStorageUrl == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseStorageUrl; - } - - String get firebaseDatabaseUrl { - var firebaseDatabaseUrl = dotenv.env['FIREBASE_DATABASE_URL']; - if (firebaseDatabaseUrl == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseDatabaseUrl; - } - - String get firebaseWebApiKey { - var firebaseKey = dotenv.env['FIREBASE_WEB_API_KEY']; - if (firebaseKey == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseKey; - } - - String get firebaseIosApiKey { - var firebaseKey = dotenv.env['FIREBASE_IOS_API_KEY']; - if (firebaseKey == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseKey; - } - - String get firebaseAndroidApiKey { - var firebaseKey = dotenv.env['FIREBASE_ANDROID_API_KEY']; - if (firebaseKey == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseKey; - } - - String get firebaseAppIdAndroid { - var firebaseAppIdAndroid = dotenv.env['FIREBASE_APP_ID_ANDROID']; - if (firebaseAppIdAndroid == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAppIdAndroid; - } - - String get firebaseAppIdIos { - var firebaseAppIdIos = dotenv.env['FIREBASE_APP_ID_IOS']; - if (firebaseAppIdIos == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAppIdIos; - } - - String get firebaseAppIdMacos { - var firebaseAppIdMacos = dotenv.env['FIREBASE_APP_ID_MACOS']; - if (firebaseAppIdMacos == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAppIdMacos; - } - - String get firebaseAppIdWeb { - var firebaseAppIdWeb = dotenv.env['FIREBASE_APP_ID_WEB']; - if (firebaseAppIdWeb == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseAppIdWeb; - } - - String get firebaseClientIdIos { - var firebaseClientIdIos = dotenv.env['FIREBASE_CLIENT_ID_IOS']; - if (firebaseClientIdIos == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseClientIdIos; - } - - String get firebaseClientIdMacos { - var firebaseClientIdMacos = dotenv.env['FIREBASE_CLIENT_ID_MACOS']; - if (firebaseClientIdMacos == null) { - debugPrint(_errorMessage); - throw Exception(_errorMessage); - } - return firebaseClientIdMacos; - } -} diff --git a/packages/flutter_notification_center_firebase/lib/src/config/firebase.dart b/packages/flutter_notification_center_firebase/lib/src/config/firebase.dart deleted file mode 100644 index eb35030..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/config/firebase.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'environment.dart'; - -mixin FirebaseInstance { - static FirebaseApp instance() => - SharedFirebaseEnvironmentConfig.firebaseAppName.isEmpty - ? Firebase.app() - : Firebase.app(SharedFirebaseEnvironmentConfig.firebaseAppName); -} - -mixin Database { - static FirebaseFirestore ref() => FirebaseFirestore.instanceFor( - app: FirebaseInstance.instance(), - ); -} - -mixin Storage { - static Reference ref({bool prefixed = true}) => - FirebaseStorage.instanceFor(app: FirebaseInstance.instance()).ref(); -} diff --git a/packages/flutter_notification_center_firebase/lib/src/config/firebase_collections.dart b/packages/flutter_notification_center_firebase/lib/src/config/firebase_collections.dart deleted file mode 100644 index e0ca12d..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/config/firebase_collections.dart +++ /dev/null @@ -1,4 +0,0 @@ -mixin FirebaseCollectionNames { - static const String activeNotifications = 'active_notifications'; - static const String plannedNotifications = 'planned_notifications'; -} diff --git a/packages/flutter_notification_center_firebase/lib/src/config/firebase_options.dart b/packages/flutter_notification_center_firebase/lib/src/config/firebase_options.dart deleted file mode 100644 index 05be768..0000000 --- a/packages/flutter_notification_center_firebase/lib/src/config/firebase_options.dart +++ /dev/null @@ -1,84 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members - -import 'environment_config.dart'; -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show TargetPlatform, defaultTargetPlatform, kIsWeb; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static FirebaseOptions web = FirebaseOptions( - apiKey: EnvironmentConfig().firebaseWebApiKey, - appId: EnvironmentConfig().firebaseAppIdWeb, - messagingSenderId: EnvironmentConfig().firebaseMessageId, - projectId: EnvironmentConfig().firebaseProjectId, - authDomain: EnvironmentConfig().firebaseAuthDomain, - storageBucket: EnvironmentConfig().firebaseStorageUrl, - ); - - static FirebaseOptions android = FirebaseOptions( - apiKey: EnvironmentConfig().firebaseAndroidApiKey, - appId: EnvironmentConfig().firebaseAppIdAndroid, - messagingSenderId: EnvironmentConfig().firebaseMessageId, - projectId: EnvironmentConfig().firebaseProjectId, - storageBucket: EnvironmentConfig().firebaseStorageUrl, - ); - - static FirebaseOptions ios = FirebaseOptions( - apiKey: EnvironmentConfig().firebaseIosApiKey, - appId: EnvironmentConfig().firebaseAppIdIos, - messagingSenderId: EnvironmentConfig().firebaseMessageId, - projectId: EnvironmentConfig().firebaseProjectId, - storageBucket: EnvironmentConfig().firebaseStorageUrl, - iosClientId: EnvironmentConfig().firebaseClientIdIos, - iosBundleId: 'nl.iconica.appshellDemo', - ); - - static FirebaseOptions macos = FirebaseOptions( - apiKey: EnvironmentConfig().firebaseIosApiKey, - appId: EnvironmentConfig().firebaseAppIdMacos, - messagingSenderId: EnvironmentConfig().firebaseMessageId, - projectId: EnvironmentConfig().firebaseProjectId, - storageBucket: EnvironmentConfig().firebaseStorageUrl, - iosClientId: EnvironmentConfig().firebaseClientIdMacos, - iosBundleId: 'nl.iconica.appshellDemo.RunnerTests', - ); -} 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 index 6b6711c..5da6914 100644 --- 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 @@ -1,14 +1,19 @@ 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'; -import '../config/firebase_collections.dart'; class FirebaseNotificationService with ChangeNotifier implements NotificationService { final Function(NotificationModel) newNotificationCallback; + final FirebaseApp? firebaseApp; + final String activeNotificationsCollection; + final String plannedNotificationsCollection; + late FirebaseApp _firebaseApp; @override List listOfActiveNotifications; @@ -18,10 +23,15 @@ class FirebaseNotificationService // ignore: unused_field late Timer _timer; - FirebaseNotificationService( - {required this.newNotificationCallback, - this.listOfActiveNotifications = const [], - this.listOfPlannedNotifications = const []}) { + FirebaseNotificationService({ + required this.newNotificationCallback, + this.firebaseApp, + this.activeNotificationsCollection = 'active_notifications', + this.plannedNotificationsCollection = 'planned_notifications', + this.listOfActiveNotifications = const [], + this.listOfPlannedNotifications = const [], + }) { + _firebaseApp = firebaseApp ?? Firebase.app(); _startTimer(); } @@ -36,15 +46,25 @@ class FirebaseNotificationService Future pushNotification(NotificationModel notification, [Function(NotificationModel model)? onNewNotification]) async { try { - CollectionReference notifications = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.activeNotifications); + var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; + + if (userId == null) { + debugPrint('User is not authenticated'); + return; + } + + CollectionReference notifications = + FirebaseFirestore.instanceFor(app: _firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection); DateTime currentDateTime = DateTime.now(); notification.dateTimePushed = currentDateTime; Map notificationMap = notification.toMap(); await notifications.doc(notification.id).set(notificationMap); - listOfActiveNotifications.add(notification); + listOfActiveNotifications = [...listOfActiveNotifications, notification]; //Show popup with notification conte if (onNewNotification != null) { @@ -62,11 +82,20 @@ class FirebaseNotificationService @override Future> getActiveNotifications() async { try { - CollectionReference activeNotificationsCollection = FirebaseFirestore - .instance - .collection(FirebaseCollectionNames.activeNotifications); + var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - QuerySnapshot querySnapshot = await activeNotificationsCollection.get(); + if (userId == null) { + debugPrint('User is not authenticated'); + return []; + } + + CollectionReference activeNotificationsResult = + FirebaseFirestore.instanceFor(app: _firebaseApp) + .collection(activeNotificationsCollection) + .doc(userId) + .collection(activeNotificationsCollection); + + QuerySnapshot querySnapshot = await activeNotificationsResult.get(); List activeNotifications = querySnapshot.docs.map((doc) { @@ -75,7 +104,18 @@ class FirebaseNotificationService return NotificationModel.fromJson(data); }).toList(); - listOfActiveNotifications = activeNotifications; + 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; } catch (e) { @@ -115,8 +155,19 @@ class FirebaseNotificationService Future createScheduledNotification( NotificationModel notification) async { try { - CollectionReference plannedNotifications = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.plannedNotifications); + 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); + Map notificationMap = notification.toMap(); await plannedNotifications.doc(notification.id).set(notificationMap); } catch (e) { @@ -128,14 +179,27 @@ class FirebaseNotificationService Future deletePlannedNotification( NotificationModel notificationModel) async { try { - DocumentReference documentReference = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.plannedNotifications) - .doc(notificationModel.id); + 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.instance - .collection(FirebaseCollectionNames.plannedNotifications) - .get(); + QuerySnapshot querySnapshot = + await FirebaseFirestore.instanceFor(app: _firebaseApp) + .collection(plannedNotificationsCollection) + .doc(userId) + .collection(plannedNotificationsCollection) + .get(); if (querySnapshot.docs.isEmpty) { debugPrint('The collection is now empty'); @@ -152,9 +216,19 @@ class FirebaseNotificationService Future dismissActiveNotification( NotificationModel notificationModel) async { try { - DocumentReference documentReference = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.activeNotifications) - .doc(notificationModel.id); + 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 .removeAt(listOfActiveNotifications.indexOf(notificationModel)); @@ -168,11 +242,57 @@ class FirebaseNotificationService Future pinActiveNotification( NotificationModel notificationModel) async { try { - DocumentReference documentReference = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.activeNotifications) - .doc(notificationModel.id); + 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(); + } 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(); } catch (e) { debugPrint('Error updating document: $e'); @@ -183,9 +303,19 @@ class FirebaseNotificationService Future markNotificationAsRead( NotificationModel notificationModel) async { try { - DocumentReference documentReference = FirebaseFirestore.instance - .collection(FirebaseCollectionNames.activeNotifications) - .doc(notificationModel.id); + 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(); @@ -198,11 +328,20 @@ class FirebaseNotificationService Future checkForScheduledNotifications() async { DateTime currentTime = DateTime.now(); try { - CollectionReference plannedNotificationsCollection = FirebaseFirestore - .instance - .collection(FirebaseCollectionNames.plannedNotifications); + var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; - QuerySnapshot querySnapshot = await plannedNotificationsCollection.get(); + if (userId == null) { + debugPrint('User is not authenticated'); + return; + } + + CollectionReference plannedNotificationsResult = + FirebaseFirestore.instanceFor(app: _firebaseApp) + .collection(plannedNotificationsCollection) + .doc(userId) + .collection(plannedNotificationsCollection); + + QuerySnapshot querySnapshot = await plannedNotificationsResult.get(); if (querySnapshot.docs.isEmpty) { debugPrint('No scheduled notifications to be pushed'); @@ -241,4 +380,22 @@ class FirebaseNotificationService 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) + .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 index db6b98f..98f0747 100644 --- a/packages/flutter_notification_center_firebase/pubspec.yaml +++ b/packages/flutter_notification_center_firebase/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_notification_center_firebase description: "A new Flutter project." publish_to: "none" -version: 1.3.1 +version: 1.4.0 environment: sdk: ">=2.18.0 <3.0.0" @@ -10,21 +10,19 @@ environment: dependencies: flutter: sdk: flutter - flutter_dotenv: ^5.0.2 intl: any # Firebase cloud_firestore: ^4.16.0 firebase_auth: ^4.2.6 firebase_core: ^2.5.0 - firebase_storage: ^11.0.14 cupertino_icons: ^1.0.2 flutter_notification_center: git: url: https://github.com/Iconica-Development/flutter_notification_center - ref: 1.3.1 + ref: 1.4.0 path: packages/flutter_notification_center dev_dependencies: diff --git a/pubspec.yaml b/pubspec.yaml index 9c548d9..bb095eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,32 +1,6 @@ -name: flutter_notification_center -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 - -version: 1.2.0 +name: flutter_nofication_center_workspace environment: - sdk: '>=3.3.2 <4.0.0' - -dependencies: - flutter: - sdk: flutter - intl: any - - flutter_animated_widgets: - git: - url: https://github.com/Iconica-Development/flutter_animated_widgets - ref: 0.1.1 - + sdk: ">=3.1.0 <4.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 + melos: ^3.0.1 \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index ca731c8..0000000 --- a/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:flutter_notification_center/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); -// }); -// }