feat: rework flutter_notification_center design

This commit is contained in:
mike doornenbal 2024-06-06 13:30:18 +02:00
parent b57bc7ce46
commit 169228152e
21 changed files with 496 additions and 471 deletions

View file

@ -1,3 +1,8 @@
## [2.0.0] - 6 June 2024
* Rework design for notification center
* Added iconica linter
## [1.4.1] - 4 June 2024
* Fix notification amount number to properly size and show plus icon when above certain amount

View file

@ -1,28 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
include: package:flutter_iconica_analysis/analysis_options.yaml
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
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

View file

@ -23,6 +23,5 @@ 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

View file

@ -82,7 +82,6 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
notificationTranslations: const NotificationTranslations.empty(),
context: context,
),
seperateNotificationsWithDivider: true,
);
popupHandler = PopupHandler(context: context, config: config);
}

View file

@ -2,12 +2,12 @@ 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
environment:
sdk: '>=3.3.2 <4.0.0'
sdk: ">=3.3.2 <4.0.0"
dependencies:
flutter:
@ -17,16 +17,14 @@ dependencies:
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center
ref: 2.0.0
flutter_notification_center_firebase:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center_firebase
ref: 2.0.0
dev_dependencies:
flutter_test:
@ -39,4 +37,3 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
uses-material-design: true

View file

@ -2,16 +2,17 @@
//
// SPDX-License-Identifier: BSD-3-Clause
export "package:flutter_animated_widgets/flutter_animated_widgets.dart";
export "src/models/notification.dart";
export "src/models/notification_config.dart";
export "src/models/notification_theme.dart";
export "src/models/notification_translation.dart";
export "src/notification_bell.dart";
export "src/notification_dialog.dart";
export "src/popup_handler.dart";
export "src/notification_snackbar.dart";
export "src/notification_detail.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";
export "package:flutter_animated_widgets/flutter_animated_widgets.dart";

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter/material.dart";
import '../../flutter_notification_center.dart';
import "package:flutter_notification_center/flutter_notification_center.dart";
/// Configuration class for notifications.
class NotificationConfig {
@ -12,7 +12,6 @@ class NotificationConfig {
/// translations for notification messages.
const NotificationConfig({
required this.service,
this.seperateNotificationsWithDivider = true,
this.translations = const NotificationTranslations.empty(),
this.notificationWidgetBuilder,
this.showAsSnackBar = true,
@ -23,9 +22,6 @@ class NotificationConfig {
/// The notification service to use for delivering notifications.
final NotificationService service;
/// Whether to seperate notifications with a divider.
final bool seperateNotificationsWithDivider;
/// Translations for notification messages.
final NotificationTranslations translations;
@ -33,7 +29,8 @@ class NotificationConfig {
final Widget Function(NotificationModel, BuildContext)?
notificationWidgetBuilder;
/// Whether to show notifications as snackbars. If false show notifications as a dialog.
/// Whether to show notifications as snackbars.
/// If false show notifications as a dialog.
final bool showAsSnackBar;
/// Whether to show notification popups.

View file

@ -14,7 +14,7 @@ class NotificationTranslations {
});
const NotificationTranslations.empty({
this.appBarTitle = "Notification Center",
this.appBarTitle = "Notifications",
this.noNotifications = "No unread notifications available.",
this.notificationDismissed = "Notification dismissed.",
this.notificationPinned = "Notification pinned.",
@ -63,8 +63,8 @@ class NotificationTranslations {
String? datePrefix,
String? notAvailable,
String? dissmissDialog,
}) {
return NotificationTranslations(
}) =>
NotificationTranslations(
appBarTitle: appBarTitle ?? this.appBarTitle,
noNotifications: noNotifications ?? this.noNotifications,
notificationDismissed:
@ -76,5 +76,4 @@ class NotificationTranslations {
notAvailable: notAvailable ?? this.notAvailable,
dissmissDialog: dissmissDialog ?? this.dissmissDialog,
);
}
}

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart";
import "../flutter_notification_center.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
/// A bell icon widget that displays the number of active notifications.
///

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart";
import "../flutter_notification_center.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
/// A widget representing a notification bell.
class NotificationBellWidgetStory extends StatelessWidget {

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../flutter_notification_center.dart';
import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart";
/// Widget for displaying the notification center.
class NotificationCenter extends StatefulWidget {
@ -27,7 +28,7 @@ class NotificationCenterState extends State<NotificationCenter> {
super.initState();
// ignore: discarded_futures
_notificationsFuture = widget.config.service.getActiveNotifications();
widget.config.service.getActiveAmountStream().listen((amount) {
widget.config.service.getActiveAmountStream().listen((amount) async {
_notificationsFuture = widget.config.service.getActiveNotifications();
});
widget.config.service.addListener(_listener);
@ -44,17 +45,26 @@ class NotificationCenterState extends State<NotificationCenter> {
}
@override
Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text(
widget.config.translations.appBarTitle,
style: theme.appBarTheme.titleTextStyle,
),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
leading: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Icon(
Icons.arrow_back_ios,
),
),
),
body: FutureBuilder<List<NotificationModel>>(
@ -64,47 +74,57 @@ class NotificationCenterState extends State<NotificationCenter> {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
debugPrint("Error: ${snapshot.error}");
return Center(
child: Text(widget.config.translations.errorMessage));
return Center(child: Text(widget.config.translations.errorMessage));
} else if (snapshot.data == null || snapshot.data!.isEmpty) {
return Center(
child: Text(widget.config.translations.noNotifications),
);
} else {
return ListView.builder(
itemCount: snapshot.data!.length * 2 - 1,
padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
if (index.isOdd) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: widget.config.seperateNotificationsWithDivider
? const Divider(
color: Colors.grey,
thickness: 1.0,
)
: Container(),
);
}
var notification = snapshot.data![index ~/ 2];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: widget.config.notificationWidgetBuilder != null
? widget.config.notificationWidgetBuilder!(
notification, context)
: notification.isPinned
//Pinned notification
? Dismissible(
key: Key('${notification.id}_pinned'),
var notification = snapshot.data![index];
return notification.isPinned
? GestureDetector(
onTap: () async => _navigateToNotificationDetail(
context,
notification,
widget.config.service,
widget.config.translations,
const NotificationStyle(),
),
child: Dismissible(
key: Key("${notification.id}_pinned"),
onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) {
await unPinNotification(
widget.config.service,
notification,
widget.config.translations,
context);
context,
);
} else if (direction ==
DismissDirection.startToEnd) {
await unPinNotification(
widget.config.service,
notification,
widget.config.translations,
context,
);
}
},
background: Container(
color:
const Color.fromRGBO(59, 213, 111, 1),
decoration: const BoxDecoration(
color: Color.fromRGBO(59, 213, 111, 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
@ -115,85 +135,64 @@ class NotificationCenterState extends State<NotificationCenter> {
),
),
secondaryBackground: Container(
color:
const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
decoration: const BoxDecoration(
color: Color.fromRGBO(59, 213, 111, 1),
borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
padding: EdgeInsets.only(right: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
child: GestureDetector(
onTap: () async =>
_navigateToNotificationDetail(
child: _notificationItem(
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),
),
),
),
)
//Dismissable notification
: Dismissible(
: GestureDetector(
onTap: () async => _navigateToNotificationDetail(
context,
notification,
widget.config.service,
widget.config.translations,
const NotificationStyle(),
),
child: Dismissible(
key: Key(notification.id),
onDismissed: (direction) async {
if (direction ==
DismissDirection.endToStart) {
if (direction == DismissDirection.endToStart) {
await dismissNotification(
widget.config.service,
notification,
widget.config.translations,
context);
context,
);
} else if (direction ==
DismissDirection.startToEnd) {
await pinNotification(
widget.config.service,
notification,
widget.config.translations,
context);
context,
);
}
},
background: Container(
color:
const Color.fromRGBO(59, 213, 111, 1),
decoration: const BoxDecoration(
color: Color.fromRGBO(59, 213, 111, 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
@ -203,9 +202,16 @@ class NotificationCenterState extends State<NotificationCenter> {
),
),
),
secondaryBackground: Container(
color:
const Color.fromRGBO(255, 131, 131, 1),
secondaryBackground: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 131, 131, 1),
borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 16.0),
@ -215,56 +221,93 @@ class NotificationCenterState extends State<NotificationCenter> {
),
),
),
child: GestureDetector(
onTap: () async =>
_navigateToNotificationDetail(
),
child: _notificationItem(
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: !notification.isRead
? Container(
margin: const EdgeInsets.only(
right: 8.0),
width: 12.0,
height: 12.0,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
)
: null,
),
),
));
);
},
);
}
},
),
);
}
}
Widget _notificationItem(
BuildContext context,
NotificationModel notification,
) {
var theme = Theme.of(context);
var dateTimePushed =
DateFormat("dd-MM-yyyy HH:mm").format(notification.dateTimePushed!);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (!notification.isPinned) ...[
if (!notification.isRead) ...[
const SizedBox(
width: 8,
),
const Icon(
Icons.circle_rounded,
color: Colors.black,
size: 10,
),
const SizedBox(
width: 8,
),
],
] else ...[
const SizedBox(
width: 8,
),
Transform.rotate(
angle: 0.5,
child: Icon(
notification.isRead
? Icons.push_pin_outlined
: Icons.push_pin,
color: Colors.black,
size: 30,
),
),
const SizedBox(
width: 8,
),
],
Text(
notification.title,
style: notification.isRead
? theme.textTheme.bodyMedium
: theme.textTheme.bodyLarge,
),
],
),
Text(
dateTimePushed,
style: theme.textTheme.bodyMedium,
),
],
),
),
),
);
}
Future<void> _navigateToNotificationDetail(
@ -274,7 +317,6 @@ Future<void> _navigateToNotificationDetail(
NotificationTranslations notificationTranslations,
NotificationStyle style,
) async {
await markNotificationAsRead(notificationService, notification);
if (context.mounted) {
await Navigator.push(
context,
@ -287,6 +329,7 @@ Future<void> _navigateToNotificationDetail(
),
);
}
await markNotificationAsRead(notificationService, notification);
}
Future<void> dismissNotification(

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart";
import "../flutter_notification_center.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart";
/// A page displaying the details of a notification.
@ -27,13 +27,27 @@ class NotificationDetailPage extends StatelessWidget {
final NotificationTranslations translations;
@override
Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text(
translations.appBarTitle,
style: notificationStyle.appTitleTextStyle,
style: theme.appBarTheme.titleTextStyle,
),
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
centerTitle: true,
leading: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Icon(
Icons.arrow_back_ios,
),
),
),
body: SingleChildScrollView(
child: Padding(
@ -48,12 +62,12 @@ class NotificationDetailPage extends StatelessWidget {
const SizedBox(height: 10),
Text(
notification.body,
style:
notificationStyle.subtitleTextStyle ?? const TextStyle(),
style: notificationStyle.subtitleTextStyle ?? const TextStyle(),
),
const SizedBox(height: 10),
Text(
'${translations.datePrefix} ${DateFormat('yyyy-MM-dd HH:mm').format(
"${translations.datePrefix}"
' ${DateFormat('yyyy-MM-dd HH:mm').format(
notification.dateTimePushed ?? DateTime.now(),
)}',
style: const TextStyle(
@ -66,4 +80,5 @@ class NotificationDetailPage extends StatelessWidget {
),
),
);
}
}

View file

@ -1,25 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:intl/intl.dart';
import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart";
class NotificationDialog extends StatelessWidget {
const NotificationDialog({
required this.title,
required this.body,
required this.translations,
super.key,
this.datetimePublished,
});
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,
});
@override
Widget build(BuildContext context) {
String formattedDateTime = datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished!)
var formattedDateTime = datetimePublished != null
? DateFormat("dd MMM HH:mm").format(datetimePublished!)
: translations.notAvailable;
return AlertDialog(

View file

@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:intl/intl.dart';
import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart";
class NotificationSnackbar extends SnackBar {
NotificationSnackbar({
super.key,
required String title,
required String body,
required NotificationTranslations translations,
required VoidCallback onDismiss,
super.key,
DateTime? datetimePublished,
}) : super(
content: Column(
@ -33,7 +33,7 @@ class NotificationSnackbar extends SnackBar {
const SizedBox(height: 4),
Text(
datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished)
? DateFormat("dd-MM-yyyy HH:mm").format(datetimePublished)
: translations.notAvailable,
style: const TextStyle(
fontSize: 12.0,

View file

@ -1,18 +1,17 @@
// Define a PopupHandler class to handle notification popups
import 'package:flutter/material.dart';
import "package:flutter/material.dart";
import 'package:flutter_notification_center/flutter_notification_center.dart';
import "package:flutter_notification_center/flutter_notification_center.dart";
class PopupHandler {
final BuildContext context;
final NotificationConfig config;
PopupHandler({
required this.context,
required this.config,
});
final BuildContext context;
final NotificationConfig config;
void handleNotificationPopup(NotificationModel notification) {
Future<void> handleNotificationPopup(NotificationModel notification) async {
if (!config.enableNotificationPopups) return;
if (config.showAsSnackBar) {
@ -31,7 +30,7 @@ class PopupHandler {
} else {
if (ModalRoute.of(context)?.isCurrent != true) return;
showDialog(
await showDialog(
context: context,
builder: (context) => NotificationDialog(
translations: config.translations,

View file

@ -1,7 +1,7 @@
import "dart:async";
import "package:flutter/material.dart";
import "../models/notification.dart";
import "package:flutter_notification_center/src/models/notification.dart";
/// An abstract class representing a service for managing notifications.
abstract class NotificationService with ChangeNotifier {
@ -26,8 +26,10 @@ abstract class NotificationService with ChangeNotifier {
List<NotificationModel> listOfPlannedNotifications;
/// Pushes a notification to the service.
Future pushNotification(NotificationModel notification,
[Function(NotificationModel model)? onNewNotification]);
Future pushNotification(
NotificationModel notification, [
Function(NotificationModel model)? onNewNotification,
]);
/// Retrieves the list of active notifications.
Future<List<NotificationModel>> getActiveNotifications();

View file

@ -1,11 +1,10 @@
name: flutter_notification_center
description: "A Flutter package for displaying notifications in a notification center."
publish_to: 'none'
version: 1.4.1
publish_to: "none"
version: 2.0.0
environment:
sdk: '>=3.3.2 <4.0.0'
sdk: ">=3.3.2 <4.0.0"
dependencies:
flutter:
@ -15,7 +14,7 @@ dependencies:
flutter_animated_widgets:
git:
url: https://github.com/Iconica-Development/flutter_animated_widgets
ref: 0.2.0
ref: 0.3.0
dev_dependencies:
flutter_test:

View file

@ -3,10 +3,10 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: GPL-3.0-or-later
import 'package:flutter_test/flutter_test.dart';
import "package:flutter_test/flutter_test.dart";
void main() {
test('test', () {
test("test", () {
expect(true, true);
});
}

View file

@ -1,28 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
include: package:flutter_iconica_analysis/analysis_options.yaml
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
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

View file

@ -1,14 +1,25 @@
import 'dart:async';
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 "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;
@ -23,33 +34,23 @@ class FirebaseNotificationService
// ignore: unused_field
late Timer _timer;
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();
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 15), (timer) {
debugPrint('Checking for scheduled notifications');
checkForScheduledNotifications();
Future<void> _startTimer() async {
_timer = Timer.periodic(const Duration(seconds: 15), (timer) async {
debugPrint("Checking for scheduled notifications");
await checkForScheduledNotifications();
});
}
@override
Future<void> pushNotification(NotificationModel notification,
[Function(NotificationModel model)? onNewNotification]) async {
Future<void> pushNotification(
NotificationModel notification, [
Function(NotificationModel model)? onNewNotification,
]) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -59,9 +60,9 @@ class FirebaseNotificationService
.doc(userId)
.collection(activeNotificationsCollection);
DateTime currentDateTime = DateTime.now();
var currentDateTime = DateTime.now();
notification.dateTimePushed = currentDateTime;
Map<String, dynamic> notificationMap = notification.toMap();
var notificationMap = notification.toMap();
await notifications.doc(notification.id).set(notificationMap);
listOfActiveNotifications = [...listOfActiveNotifications, notification];
@ -74,8 +75,8 @@ class FirebaseNotificationService
}
notifyListeners();
} catch (e) {
debugPrint('Error creating document: $e');
} on Exception catch (e) {
debugPrint("Error creating document: $e");
}
}
@ -85,7 +86,7 @@ class FirebaseNotificationService
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return [];
}
@ -95,12 +96,11 @@ class FirebaseNotificationService
.doc(userId)
.collection(activeNotificationsCollection);
QuerySnapshot querySnapshot = await activeNotificationsResult.get();
var querySnapshot = await activeNotificationsResult.get();
List<NotificationModel> activeNotifications =
querySnapshot.docs.map((doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
data['id'] = doc.id;
var activeNotifications = querySnapshot.docs.map((doc) {
var data = doc.data()! as Map<String, dynamic>;
data["id"] = doc.id;
return NotificationModel.fromJson(data);
}).toList();
@ -114,19 +114,22 @@ class FirebaseNotificationService
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications.insertAll(
0, activeNotifications.where((element) => element.isPinned));
0,
activeNotifications.where((element) => element.isPinned),
);
notifyListeners();
return listOfActiveNotifications;
} catch (e) {
debugPrint('Error getting active notifications: $e');
} on Exception catch (e) {
debugPrint("Error getting active notifications: $e");
return [];
}
}
@override
Future<void> createRecurringNotification(
NotificationModel notification) async {
NotificationModel notification,
) async {
if (notification.recurring) {
switch (notification.occuringInterval) {
case OcurringInterval.daily:
@ -147,18 +150,19 @@ class FirebaseNotificationService
break;
case null:
}
createScheduledNotification(notification);
await createScheduledNotification(notification);
}
}
@override
Future<void> createScheduledNotification(
NotificationModel notification) async {
NotificationModel notification,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -168,21 +172,22 @@ class FirebaseNotificationService
.doc(userId)
.collection(plannedNotificationsCollection);
Map<String, dynamic> notificationMap = notification.toMap();
var notificationMap = notification.toMap();
await plannedNotifications.doc(notification.id).set(notificationMap);
} catch (e) {
debugPrint('Error creating document: $e');
} on Exception catch (e) {
debugPrint("Error creating document: $e");
}
}
@override
Future<void> deletePlannedNotification(
NotificationModel notificationModel) async {
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -202,24 +207,26 @@ class FirebaseNotificationService
.get();
if (querySnapshot.docs.isEmpty) {
debugPrint('The collection is now empty');
debugPrint("The collection is now empty");
} else {
debugPrint(
'Deleted planned notification with title: ${notificationModel.title}');
"Deleted planned notification with title: ${notificationModel.title}",
);
}
} catch (e) {
debugPrint('Error deleting document: $e');
} on Exception catch (e) {
debugPrint("Error deleting document: $e");
}
}
@override
Future<void> dismissActiveNotification(
NotificationModel notificationModel) async {
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -233,19 +240,20 @@ class FirebaseNotificationService
listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
notifyListeners();
} catch (e) {
debugPrint('Error deleting document: $e');
} on Exception catch (e) {
debugPrint("Error deleting document: $e");
}
}
@override
Future<void> pinActiveNotification(
NotificationModel notificationModel) async {
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -255,7 +263,7 @@ class FirebaseNotificationService
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isPinned': true});
await documentReference.update({"isPinned": true});
notificationModel.isPinned = true;
listOfActiveNotifications
@ -263,19 +271,20 @@ class FirebaseNotificationService
listOfActiveNotifications.insert(0, notificationModel);
notifyListeners();
} catch (e) {
debugPrint('Error updating document: $e');
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> unPinActiveNotification(
NotificationModel notificationModel) async {
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -285,7 +294,7 @@ class FirebaseNotificationService
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isPinned': false});
await documentReference.update({"isPinned": false});
notificationModel.isPinned = false;
listOfActiveNotifications
@ -294,19 +303,20 @@ class FirebaseNotificationService
listOfActiveNotifications.add(notificationModel);
notifyListeners();
} catch (e) {
debugPrint('Error updating document: $e');
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> markNotificationAsRead(
NotificationModel notificationModel) async {
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -316,22 +326,22 @@ class FirebaseNotificationService
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isRead': true});
await documentReference.update({"isRead": true});
notificationModel.isRead = true;
notifyListeners();
} catch (e) {
debugPrint('Error updating document: $e');
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> checkForScheduledNotifications() async {
DateTime currentTime = DateTime.now();
var currentTime = DateTime.now();
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
return;
}
@ -341,20 +351,19 @@ class FirebaseNotificationService
.doc(userId)
.collection(plannedNotificationsCollection);
QuerySnapshot querySnapshot = await plannedNotificationsResult.get();
var querySnapshot = await plannedNotificationsResult.get();
if (querySnapshot.docs.isEmpty) {
debugPrint('No scheduled notifications to be pushed');
debugPrint("No scheduled notifications to be pushed");
return;
}
List<NotificationModel> plannedNotifications =
querySnapshot.docs.map((doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
var plannedNotifications = querySnapshot.docs.map((doc) {
var data = doc.data()! as Map<String, dynamic>;
return NotificationModel.fromJson(data);
}).toList();
for (NotificationModel notification in plannedNotifications) {
for (var notification in plannedNotifications) {
if (notification.scheduledFor!.isBefore(currentTime) ||
notification.scheduledFor!.isAtSameMomentAs(currentTime)) {
await pushNotification(notification, newNotificationCallback);
@ -363,7 +372,7 @@ class FirebaseNotificationService
//Plan new recurring notification instance
if (notification.recurring) {
NotificationModel newNotification = NotificationModel(
var newNotification = NotificationModel(
id: UniqueKey().toString(),
title: notification.title,
body: notification.body,
@ -375,8 +384,8 @@ class FirebaseNotificationService
}
}
}
} catch (e) {
debugPrint('Error getting planned notifications: $e');
} on Exception catch (e) {
debugPrint("Error getting planned notifications: $e");
return;
}
}
@ -386,7 +395,7 @@ class FirebaseNotificationService
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
debugPrint("User is not authenticated");
yield 0;
}

View file

@ -1,8 +1,7 @@
name: flutter_notification_center_firebase
description: "A new Flutter project."
publish_to: "none"
version: 1.4.1
version: 2.0.0
environment:
sdk: ">=2.18.0 <3.0.0"
@ -22,15 +21,16 @@ dependencies:
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
ref: 2.0.0
path: packages/flutter_notification_center
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:
uses-material-design: true