refactor: new component structure

This commit is contained in:
Niels Gorter 2024-09-24 13:50:11 +02:00
parent ddf2a2bbb3
commit 367303aecf
51 changed files with 741 additions and 916 deletions

2
.gitignore vendored
View file

@ -49,8 +49,10 @@ android/
web/ web/
linux/ linux/
macos/ macos/
windows/
pubspec.lock pubspec.lock
.metadata .metadata
flutter_notification_center.iml flutter_notification_center.iml
dotenv dotenv
firebase_options.dart

View file

@ -1,3 +1,7 @@
## [5.0.0] - 24 September 2024
* Refactor package with the new component structure
## [4.0.0] - 14 August 2024 ## [4.0.0] - 14 August 2024
* Fix overflow issue with long text in notification * Fix overflow issue with long text in notification

View file

@ -15,12 +15,13 @@ A Flutter package for creating notification center displaying a list of notifica
To use this package, add `flutter_notification_center` as a dependency in your pubspec.yaml file. To use this package, add `flutter_notification_center` as a dependency in your pubspec.yaml file.
- Provide a `NotificationService` to the userstory to define and alter behaviour, this service accepts and `NotificationRepositoryInterface` as repository (data source)
- For custom notification styling provide the optional notificationWidgetBuilder with your own implementation. - For custom notification styling provide the optional notificationWidgetBuilder with your own implementation.
The `NotificationConfig` has its own parameters, as specified below: The `NotificationConfig` has its own parameters, as specified below:
| Parameter | Explanation | | Parameter | Explanation |
|-----------|-------------| |-----------|-------------|
| service | The notification service that will be used |
| seperateNotificationsWithDivider | If true notifications will be seperated with dividers within the notification center | | seperateNotificationsWithDivider | If true notifications will be seperated with dividers within the notification center |
| translations | The translations that will be used | | translations | The translations that will be used |
| notificationWidgetBuilder | The widget that defines the styles and logic for every notification | | notificationWidgetBuilder | The widget that defines the styles and logic for every notification |

View file

@ -22,31 +22,8 @@ migrate_working_dir/
#.vscode/ #.vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/ .dart_tool/
.flutter-plugins build/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
.dart_tools/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# iOS related
/ios/
lib/config/
pubspec.lock
dotenv

View file

@ -0,0 +1 @@
../../CHANGELOG.md

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -0,0 +1 @@
../../README.md

View file

@ -0,0 +1 @@
export "src/firebase_notification_repository.dart";

View file

@ -0,0 +1,151 @@
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_core/firebase_core.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
class FirebaseNotificationRepository
implements NotificationRepositoryInterface {
FirebaseNotificationRepository({
FirebaseApp? firebaseApp,
this.activeNotificationsCollection = "active_notifications",
this.plannedNotificationsCollection = "planned_notifications",
}) : firebaseApp = firebaseApp ?? Firebase.app();
final FirebaseApp firebaseApp;
final String activeNotificationsCollection;
final String plannedNotificationsCollection;
@override
Future<NotificationModel> addNotification(
String userId,
NotificationModel notification,
List<String> recipientIds,
) async {
var newNotification = notification;
for (var recipientId in recipientIds) {
DocumentReference notifications;
if (notification.scheduledFor != null &&
notification.scheduledFor!.isAfter(DateTime.now())) {
notifications = FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(plannedNotificationsCollection)
.doc(recipientId)
.collection(plannedNotificationsCollection)
.doc(notification.id);
} else {
newNotification = notification.copyWith(
id: "${notification.id}-${DateTime.now().millisecondsSinceEpoch}",
);
notifications = FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(activeNotificationsCollection)
.doc(recipientId)
.collection(activeNotificationsCollection)
.doc(newNotification.id);
}
var currentDateTime = DateTime.now();
newNotification.dateTimePushed = currentDateTime;
var notificationMap = newNotification.toMap();
await notifications.set(notificationMap);
}
return newNotification;
}
@override
Future<void> deleteNotification(
String userId,
String id,
bool planned,
) async {
try {
if (planned) {
await FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.doc(id)
.delete();
} else {
await FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(id)
.delete();
}
} catch (e) {
throw Exception("Failed to delete notification: $e");
}
}
@override
Stream<NotificationModel?> getNotification(String userId, String id) {
var notificationStream = FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(id)
.snapshots()
.map((snapshot) {
if (snapshot.exists) {
if (snapshot.data() == null) {
return null;
}
return NotificationModel.fromJson(snapshot.data()!);
} else {
return null;
}
});
return notificationStream;
}
@override
Stream<List<NotificationModel>> getNotifications(String userId) {
var notificationsStream = FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.snapshots()
.map(
(snapshot) => snapshot.docs
.map((doc) => NotificationModel.fromJson(doc.data()))
.toList(),
);
return notificationsStream;
}
@override
Future<NotificationModel> updateNotification(
String userId,
NotificationModel notification,
) async {
await FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notification.id)
.update(notification.toMap());
return notification;
}
@override
Stream<List<NotificationModel>> getPlannedNotifications(String userId) {
var notificationsStream = FirebaseFirestore.instanceFor(app: firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.snapshots()
.map(
(snapshot) => snapshot.docs
.map((doc) => NotificationModel.fromJson(doc.data()))
.toList(),
);
return notificationsStream;
}
}

View file

@ -0,0 +1,31 @@
name: firebase_notification_center_repository
description: "A new Flutter package project."
version: 5.0.0
homepage:
publish_to: 'none'
environment:
sdk: ^3.5.3
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
# Firebase
cloud_firestore: ^5.4.2
firebase_core: ^3.5.0
notification_center_repository_interface:
git:
url: https://github.com/Iconica-Development/flutter_notification_center.git
path: packages/notification_center_repository_interface
ref: 5.0.0
dev_dependencies:
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:

View file

@ -22,22 +22,8 @@ migrate_working_dir/
#.vscode/ #.vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/ .dart_tool/
.flutter-plugins build/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View file

@ -0,0 +1 @@
../../CHANGELOG.md

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -0,0 +1 @@
../../README.md

View file

@ -30,7 +30,6 @@ migrate_working_dir/
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
.dart_tools/
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols
@ -42,11 +41,3 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# iOS related
/ios/
lib/config/
pubspec.lock
dotenv
firebase_options.dart

View file

@ -23,5 +23,6 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1 @@
{"flutter":{"platforms":{"dart":{"lib/firebase_options.dart":{"projectId":"appshell-demo","configurations":{"web":"1:431820621472:web:f4b27eea24be24fd1babc5"}}}}}}

View file

@ -1,167 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:flutter_notification_center_firebase/flutter_notification_center_firebase.dart';
class CustomNotificationWidget extends StatelessWidget {
final NotificationModel notification;
final FirebaseNotificationService notificationService;
final NotificationTranslations notificationTranslations;
final BuildContext context;
const CustomNotificationWidget({
required this.notification,
required this.notificationTranslations,
required this.notificationService,
required this.context,
super.key,
});
@override
Widget build(BuildContext context) {
return notification.isPinned
//Pinned notification
? Dismissible(
key: Key('${notification.id}_pinned'),
onDismissed: (direction) async {
await unPinNotification(notificationService, notification,
notificationTranslations, context);
},
background: Container(
color: const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
secondaryBackground: Container(
color: const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
child: GestureDetector(
onTap: () async =>
_navigateToNotificationDetail(context, notification),
child: ListTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
notification.title,
),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.push_pin),
onPressed: () async =>
_navigateToNotificationDetail(context, notification),
padding: const EdgeInsets.only(left: 60.0),
),
),
),
)
//Dismissable notification
: Dismissible(
key: Key(notification.id),
onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) {
await dismissNotification(notificationService, notification,
notificationTranslations, context);
} else if (direction == DismissDirection.startToEnd) {
await pinNotification(notificationService, notification,
notificationTranslations, context);
}
},
background: Container(
color: const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
secondaryBackground: Container(
color: const Color.fromRGBO(255, 131, 131, 1),
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 16.0),
child: Icon(
Icons.delete,
color: Colors.black,
),
),
),
child: GestureDetector(
onTap: () async => _navigateToNotificationDetail(
context,
notification,
),
child: ListTile(
leading: Icon(
notification.icon,
color: Colors.grey,
),
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
notification.title,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16,
),
),
),
],
),
trailing: !notification.isRead
? Container(
margin: const EdgeInsets.only(right: 8.0),
width: 12.0,
height: 12.0,
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
)
: null,
),
),
);
}
Future<void> _navigateToNotificationDetail(
BuildContext context,
NotificationModel notification,
) async {
unawaited(markNotificationAsRead(notificationService, notification));
if (context.mounted) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotificationDetailPage(
translations: notificationTranslations,
notification: notification,
),
),
);
}
}
}

View file

@ -1,10 +1,10 @@
import 'package:example/custom_notification.dart'; // import 'package:example/firebase_options.dart';
// import 'package:firebase_auth/firebase_auth.dart'; // import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_notification_center_repository/firebase_notification_center_repository.dart';
// import 'package:example/firebase_options.dart'; // import 'package:example/firebase_options.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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:intl/date_symbol_data_local.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart'; import 'package:flutter_notification_center/flutter_notification_center.dart';
@ -22,6 +22,7 @@ void main() async {
} }
Future<void> _configureApp() async { Future<void> _configureApp() async {
// Generate a FirebaseOptions and uncomment the following lines to initialize Firebase.
// await Firebase.initializeApp( // await Firebase.initializeApp(
// options: DefaultFirebaseOptions.currentPlatform, // options: DefaultFirebaseOptions.currentPlatform,
// ); // );
@ -34,7 +35,8 @@ Future<void> _configureApp() async {
} }
Future<void> _signInUser() async { Future<void> _signInUser() async {
/// Implement your own sign in logic here // Sign in, you could use the line below or implement your own sign in method.
// await FirebaseAuth.instance.signInAnonymously();
} }
class NotificationCenterDemo extends StatefulWidget { class NotificationCenterDemo extends StatefulWidget {
@ -45,30 +47,39 @@ class NotificationCenterDemo extends StatefulWidget {
} }
class _NotificationCenterDemoState extends State<NotificationCenterDemo> { class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
late NotificationConfig config; late NotificationService service;
late PopupHandler popupHandler; // Provide a user ID here. For Firebase you can use the commented line below.
String userId = ""; //FirebaseAuth.instance.currentUser!.uid;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
var service = FirebaseNotificationService(
newNotificationCallback: (notification) { service = NotificationService(
popupHandler.handleNotificationPopup(notification); userId: userId,
}, repository: FirebaseNotificationRepository(),
); );
config = NotificationConfig(
service: service, // Uncomment the line below to send a test notification.
enableNotificationPopups: true, // Provide a user ID in the list to send the notification to.
showAsSnackBar: true, _sendTestNotification([userId]);
notificationWidgetBuilder: (notification, context) => }
CustomNotificationWidget(
notification: notification, _sendTestNotification(List<String> recipientIds) async {
notificationService: service, await service.pushNotification(
notificationTranslations: const NotificationTranslations.empty(), NotificationModel(
context: context, id: DateTime.now().millisecondsSinceEpoch.toString(),
title: 'Test Notification',
body: 'This is a test notification.',
// For a scheduled message provide a scheduledFor date.
// For a recurring message provide a scheduledFor date, set recurring to true and provide an occuringInterval.
//
// scheduledFor: DateTime.now().add(const Duration(seconds: 5)),
// recurring: true,
// occuringInterval: OcurringInterval.debug,
), ),
recipientIds,
); );
popupHandler = PopupHandler(context: context, config: config);
} }
@override @override
@ -79,7 +90,8 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
centerTitle: true, centerTitle: true,
actions: [ actions: [
NotificationBellWidgetStory( NotificationBellWidgetStory(
config: config, userId: userId,
service: service,
), ),
], ],
), ),

View file

@ -2,38 +2,47 @@ name: example
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0 # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment: environment:
sdk: ">=3.3.2 <4.0.0" sdk: ^3.5.3
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
intl: ^0.17.0
cupertino_icons: ^1.0.8
flutter_notification_center: flutter_notification_center:
git: path: ../
url: https://github.com/Iconica-Development/flutter_notification_center
path: packages/flutter_notification_center
ref: 3.0.0
flutter_notification_center_firebase: firebase_notification_center_repository:
git: path: ../../firebase_notification_center_repository
url: https://github.com/Iconica-Development/flutter_notification_center
path: packages/flutter_notification_center_firebase intl: ^0.19.0
ref: 3.0.0
firebase_auth: any
firebase_core: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# The following section is specific to Flutter packages. flutter_lints: ^4.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View file

@ -0,0 +1 @@
{"flutter":{"platforms":{"dart":{"lib/firebase_options.dart":{"projectId":"appshell-demo","configurations":{"web":"1:431820621472:web:f4b27eea24be24fd1babc5"}}}}}}

View file

@ -1,17 +1,13 @@
// SPDX-FileCopyrightText: 2024 Iconica export "package:notification_center_repository_interface/notification_center_repository_interface.dart";
//
// 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_translation.dart";
export "src/notification_bell.dart";
export "src/notification_bell_story.dart"; export "src/notification_bell_story.dart";
export "src/notification_center.dart"; export "src/notification_center.dart";
export "src/notification_detail.dart";
export "src/notification_dialog.dart"; // Screens
export "src/notification_snackbar.dart"; export "src/screens/notification_bell.dart";
export "src/popup_handler.dart"; export "src/screens/notification_detail.dart";
export "src/services/notification_service.dart";
// Widgets
export "src/widgets/notification_dialog.dart";
export "src/widgets/notification_snackbar.dart";
export "src/widgets/popup_handler.dart";

View file

@ -1,27 +1,64 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:flutter_animated_widgets/flutter_animated_widgets.dart";
import "package:flutter_notification_center/src/notification_center.dart";
import "package:flutter_notification_center/src/screens/notification_bell.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// A widget representing a notification bell. /// A widget representing a notification bell.
class NotificationBellWidgetStory extends StatelessWidget { class NotificationBellWidgetStory extends StatefulWidget {
/// Creates a new [NotificationBellWidgetStory] instance. /// Creates a new [NotificationBellWidgetStory] instance.
/// ///
/// The [config] parameter specifies the notification configuration. /// The [config] parameter specifies the notification configuration.
const NotificationBellWidgetStory({ const NotificationBellWidgetStory({
required this.config, required this.userId,
this.config,
this.service,
this.animatedIconStyle,
super.key, super.key,
}); });
/// The user ID.
final String userId;
/// The notification configuration. /// The notification configuration.
final NotificationConfig config; final NotificationConfig? config;
/// The notification service.
final NotificationService? service;
final AnimatedNotificationBellStyle? animatedIconStyle;
@override
State<NotificationBellWidgetStory> createState() =>
_NotificationBellWidgetStoryState();
}
class _NotificationBellWidgetStoryState
extends State<NotificationBellWidgetStory> {
late NotificationConfig config;
late NotificationService service;
@override
void initState() {
config = widget.config ?? const NotificationConfig();
service = widget.service ??
NotificationService(
userId: widget.userId,
);
super.initState();
}
@override @override
Widget build(BuildContext context) => NotificationBell( Widget build(BuildContext context) => NotificationBell(
config: config, config: config,
service: service,
animatedIconStyle: widget.animatedIconStyle,
onTap: () async { onTap: () async {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => NotificationCenter( builder: (context) => NotificationCenter(
config: config, config: config,
service: service,
), ),
), ),
); );

View file

@ -1,7 +1,8 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:flutter_notification_center/src/screens/notification_detail.dart";
import "package:flutter_svg/svg.dart"; import "package:flutter_svg/svg.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// Widget for displaying the notification center. /// Widget for displaying the notification center.
class NotificationCenter extends StatefulWidget { class NotificationCenter extends StatefulWidget {
@ -10,39 +11,31 @@ class NotificationCenter extends StatefulWidget {
/// [config]: Configuration for the notification center. /// [config]: Configuration for the notification center.
const NotificationCenter({ const NotificationCenter({
required this.config, required this.config,
required this.service,
super.key, super.key,
}); });
/// Configuration for the notification center. /// Configuration for the notification center.
final NotificationConfig config; final NotificationConfig config;
final NotificationService service;
@override @override
NotificationCenterState createState() => NotificationCenterState(); NotificationCenterState createState() => NotificationCenterState();
} }
/// State for the notification center. /// State for the notification center.
class NotificationCenterState extends State<NotificationCenter> { class NotificationCenterState extends State<NotificationCenter> {
late Future<List<NotificationModel>> _notificationsFuture; late Stream<List<NotificationModel>> _notificationsStream;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// ignore: discarded_futures _notificationsStream = widget.service.getActiveNotifications();
_notificationsFuture = widget.config.service.getActiveNotifications();
widget.config.service.getActiveAmountStream().listen((amount) async { widget.service.getActiveAmountStream().listen((data) {
_notificationsFuture = widget.config.service.getActiveNotifications(); setState(() {});
}); });
widget.config.service.addListener(_listener);
}
@override
void dispose() {
widget.config.service.removeListener(_listener);
super.dispose();
}
void _listener() {
setState(() {});
} }
@override @override
@ -67,8 +60,8 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
), ),
), ),
body: FutureBuilder<List<NotificationModel>>( body: StreamBuilder<List<NotificationModel>>(
future: _notificationsFuture, stream: _notificationsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -101,7 +94,7 @@ class NotificationCenterState extends State<NotificationCenter> {
await _navigateToNotificationDetail( await _navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service, widget.service,
widget.config.translations, widget.config.translations,
); );
} }
@ -111,7 +104,7 @@ class NotificationCenterState extends State<NotificationCenter> {
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) { if (direction == DismissDirection.endToStart) {
await unPinNotification( await unPinNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context, context,
@ -119,7 +112,7 @@ class NotificationCenterState extends State<NotificationCenter> {
} else if (direction == } else if (direction ==
DismissDirection.startToEnd) { DismissDirection.startToEnd) {
await unPinNotification( await unPinNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context, context,
@ -183,7 +176,7 @@ class NotificationCenterState extends State<NotificationCenter> {
await _navigateToNotificationDetail( await _navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service, widget.service,
widget.config.translations, widget.config.translations,
); );
} }
@ -193,7 +186,7 @@ class NotificationCenterState extends State<NotificationCenter> {
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) { if (direction == DismissDirection.endToStart) {
await dismissNotification( await dismissNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context, context,
@ -201,7 +194,7 @@ class NotificationCenterState extends State<NotificationCenter> {
} else if (direction == } else if (direction ==
DismissDirection.startToEnd) { DismissDirection.startToEnd) {
await pinNotification( await pinNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context, context,
@ -270,8 +263,11 @@ Widget _notificationItem(
NotificationConfig config, NotificationConfig config,
) { ) {
var theme = Theme.of(context); var theme = Theme.of(context);
var dateTimePushed = String? dateTimePushed;
DateFormat("dd/MM/yyyy 'at' HH:mm").format(notification.dateTimePushed!); if (notification.dateTimePushed != null) {
dateTimePushed = DateFormat("dd/MM/yyyy 'at' HH:mm")
.format(notification.dateTimePushed!);
}
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Container( child: Container(
@ -332,7 +328,7 @@ Widget _notificationItem(
], ],
), ),
Text( Text(
dateTimePushed, dateTimePushed ?? "",
style: theme.textTheme.labelSmall, style: theme.textTheme.labelSmall,
), ),
], ],

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:flutter_animated_widgets/flutter_animated_widgets.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// A bell icon widget that displays the number of active notifications. /// A bell icon widget that displays the number of active notifications.
/// ///
@ -15,6 +16,8 @@ class NotificationBell extends StatefulWidget {
/// [onTap]: Callback function to be invoked when the bell icon is tapped. /// [onTap]: Callback function to be invoked when the bell icon is tapped.
const NotificationBell({ const NotificationBell({
required this.config, required this.config,
required this.service,
this.animatedIconStyle,
this.onTap, this.onTap,
super.key, super.key,
}); });
@ -23,6 +26,12 @@ class NotificationBell extends StatefulWidget {
/// the notification service. /// the notification service.
final NotificationConfig config; final NotificationConfig config;
/// The notification service used to fetch active notifications.
final NotificationService service;
/// The style of the animated bell icon.
final AnimatedNotificationBellStyle? animatedIconStyle;
/// Callback function to be invoked when the bell icon is tapped. /// Callback function to be invoked when the bell icon is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
@ -37,7 +46,7 @@ class _NotificationBellState extends State<NotificationBell> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.config.service.getActiveAmountStream().listen((amount) { widget.service.getActiveAmountStream().listen((amount) {
setState(() { setState(() {
notificationAmount = amount; notificationAmount = amount;
}); });
@ -51,7 +60,8 @@ class _NotificationBellState extends State<NotificationBell> {
icon: AnimatedNotificationBell( icon: AnimatedNotificationBell(
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
notificationCount: notificationAmount, notificationCount: notificationAmount,
style: widget.config.bellStyle, style:
widget.animatedIconStyle ?? const AnimatedNotificationBellStyle(),
), ),
); );
} }

View file

@ -1,6 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// A page displaying the details of a notification. /// A page displaying the details of a notification.
class NotificationDetailPage extends StatelessWidget { class NotificationDetailPage extends StatelessWidget {

View file

@ -1,64 +0,0 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_notification_center/src/models/notification.dart";
/// An abstract class representing a service for managing notifications.
abstract class NotificationService with ChangeNotifier {
/// Creates a new [NotificationService] instance.
///
/// The [listOfActiveNotifications] parameter specifies the
/// list of active notifications,
/// with a default value of an empty list.
///
/// The [listOfPlannedNotifications] parameter specifies the
/// list of planned notifications,
/// with a default value of an empty list.
NotificationService({
this.listOfActiveNotifications = const [],
this.listOfPlannedNotifications = const [],
});
/// A list of active notifications.
List<NotificationModel> listOfActiveNotifications;
/// A list of planned notifications.
List<NotificationModel> listOfPlannedNotifications;
/// Pushes a notification to the service.
Future pushNotification(
NotificationModel notification,
List<String> recipientIds, [
Function(NotificationModel model)? onNewNotification,
]);
/// Retrieves the list of active notifications.
Future<List<NotificationModel>> getActiveNotifications();
/// Creates a scheduled notification.
Future createScheduledNotification(NotificationModel notification);
/// Creates a recurring notification.
Future createRecurringNotification(NotificationModel notification);
/// Deletes a scheduled notification.
Future deletePlannedNotification(NotificationModel notification);
/// Dismisses an active notification.
Future dismissActiveNotification(NotificationModel notification);
/// Pin an active notification.
Future pinActiveNotification(NotificationModel notification);
/// Unpin an active notification.
Future unPinActiveNotification(NotificationModel notification);
/// Marks a notification as read.
Future markNotificationAsRead(NotificationModel notification);
/// Checks for scheduled notifications.
Future checkForScheduledNotifications();
/// Returns a stream of the number of active notifications.
Stream<int> getActiveAmountStream();
}

View file

@ -1,6 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
class NotificationDialog extends StatelessWidget { class NotificationDialog extends StatelessWidget {
const NotificationDialog({ const NotificationDialog({

View file

@ -1,6 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
class NotificationSnackbar extends SnackBar { class NotificationSnackbar extends SnackBar {
NotificationSnackbar({ NotificationSnackbar({

View file

@ -1,7 +1,8 @@
// Define a PopupHandler class to handle notification popups // Define a PopupHandler class to handle notification popups
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_notification_center/src/widgets/notification_dialog.dart";
import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:flutter_notification_center/src/widgets/notification_snackbar.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
class PopupHandler { class PopupHandler {
PopupHandler({ PopupHandler({

View file

@ -1,32 +1,37 @@
name: flutter_notification_center name: flutter_notification_center
description: "A Flutter package for displaying notifications in a notification center." description: "A new Flutter package project."
publish_to: "none" version: 5.0.0
version: 4.0.0 homepage:
publish_to: 'none'
environment: environment:
sdk: ">=3.3.2 <4.0.0" sdk: ^3.5.3
flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
intl: any
intl: ^0.19.0
flutter_svg: ^2.0.10+1
flutter_animated_widgets: flutter_animated_widgets:
git: git:
url: https://github.com/Iconica-Development/flutter_animated_widgets url: https://github.com/Iconica-Development/flutter_animated_widgets
ref: 0.3.1 ref: 0.3.1
flutter_svg: ^2.0.10+1
notification_center_repository_interface:
git:
url: https://github.com/Iconica-Development/flutter_notification_center.git
path: packages/notification_center_repository_interface
ref: 5.0.0
dev_dependencies: dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis: flutter_iconica_analysis:
git: git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0 ref: 7.0.0
# The following section is specific to Flutter packages.
flutter: flutter:
uses-material-design: true
assets: assets:
- assets/ - assets/

View file

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

View file

@ -1,5 +0,0 @@
// SPDX-FileCopyrightText: 2024 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export "src/services/firebase_notification_service.dart";

View file

@ -1,420 +0,0 @@
import "dart:async";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_auth/firebase_auth.dart";
import "package:firebase_core/firebase_core.dart";
import "package:flutter/material.dart";
import "package:flutter_notification_center/flutter_notification_center.dart";
class FirebaseNotificationService
with ChangeNotifier
implements NotificationService {
FirebaseNotificationService({
required this.newNotificationCallback,
this.firebaseApp,
this.activeNotificationsCollection = "active_notifications",
this.plannedNotificationsCollection = "planned_notifications",
this.listOfActiveNotifications = const [],
this.listOfPlannedNotifications = const [],
}) {
_firebaseApp = firebaseApp ?? Firebase.app();
unawaited(_startTimer());
}
final Function(NotificationModel) newNotificationCallback;
final FirebaseApp? firebaseApp;
final String activeNotificationsCollection;
final String plannedNotificationsCollection;
late FirebaseApp _firebaseApp;
@override
List<NotificationModel> listOfActiveNotifications;
@override
List<NotificationModel> listOfPlannedNotifications;
// ignore: unused_field
late Timer _timer;
Future<void> _startTimer() async {
_timer = Timer.periodic(const Duration(seconds: 15), (timer) async {
await checkForScheduledNotifications();
});
}
@override
Future<void> pushNotification(
NotificationModel notification,
List<String> recipientIds, [
Function(NotificationModel model)? onNewNotification,
]) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
for (var recipientId in recipientIds) {
CollectionReference notifications =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(recipientId)
.collection(activeNotificationsCollection);
var currentDateTime = DateTime.now();
notification.dateTimePushed = currentDateTime;
var notificationMap = notification.toMap();
await notifications.doc(notification.id).set(notificationMap);
}
if (recipientIds.contains(userId)) {
listOfActiveNotifications = [
...listOfActiveNotifications,
notification,
];
//Show popup with notification conte
if (onNewNotification != null) {
onNewNotification(notification);
} else {
newNotificationCallback(notification);
}
}
notifyListeners();
} on Exception catch (e) {
debugPrint("Error creating document: $e");
}
}
@override
Future<List<NotificationModel>> getActiveNotifications() async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return [];
}
CollectionReference activeNotificationsResult =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection);
var querySnapshot = await activeNotificationsResult.get();
var activeNotifications = querySnapshot.docs.map((doc) {
var data = doc.data()! as Map<String, dynamic>;
data["id"] = doc.id;
return NotificationModel.fromJson(data);
}).toList();
listOfActiveNotifications = List.from(activeNotifications);
listOfActiveNotifications.removeWhere((element) => element.isPinned);
activeNotifications
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications.insertAll(
0,
activeNotifications.where((element) => element.isPinned),
);
notifyListeners();
return listOfActiveNotifications;
} on Exception catch (e) {
debugPrint("Error getting active notifications: $e");
return [];
}
}
@override
Future<void> createRecurringNotification(
NotificationModel notification,
) async {
if (notification.recurring) {
switch (notification.occuringInterval) {
case OcurringInterval.daily:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 1));
break;
case OcurringInterval.weekly:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 7));
break;
case OcurringInterval.monthly:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 30));
break;
case OcurringInterval.debug:
notification.scheduledFor =
DateTime.now().add(const Duration(seconds: 10));
break;
case null:
}
await createScheduledNotification(notification);
}
}
@override
Future<void> createScheduledNotification(
NotificationModel notification,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
CollectionReference plannedNotifications =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection);
var notificationMap = notification.toMap();
await plannedNotifications.doc(notification.id).set(notificationMap);
} on Exception catch (e) {
debugPrint("Error creating document: $e");
}
}
@override
Future<void> deletePlannedNotification(
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.doc(notificationModel.id);
await documentReference.delete();
QuerySnapshot querySnapshot =
await FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.get();
if (querySnapshot.docs.isEmpty) {
debugPrint("The collection is now empty");
} else {
debugPrint(
"Deleted planned notification with title: ${notificationModel.title}",
);
}
} on Exception catch (e) {
debugPrint("Error deleting document: $e");
}
}
@override
Future<void> dismissActiveNotification(
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.delete();
listOfActiveNotifications
.removeWhere((element) => element.id == notificationModel.id);
notifyListeners();
} on Exception catch (e) {
debugPrint("Error deleting document: $e");
}
}
@override
Future<void> pinActiveNotification(
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({"isPinned": true});
notificationModel.isPinned = true;
listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
listOfActiveNotifications.insert(0, notificationModel);
notifyListeners();
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> unPinActiveNotification(
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({"isPinned": false});
notificationModel.isPinned = false;
listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
listOfActiveNotifications.add(notificationModel);
notifyListeners();
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> markNotificationAsRead(
NotificationModel notificationModel,
) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({"isRead": true});
notificationModel.isRead = true;
notifyListeners();
} on Exception catch (e) {
debugPrint("Error updating document: $e");
}
}
@override
Future<void> checkForScheduledNotifications() async {
var currentTime = DateTime.now();
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
return;
}
CollectionReference plannedNotificationsResult =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection);
var querySnapshot = await plannedNotificationsResult.get();
if (querySnapshot.docs.isEmpty) {
return;
}
var plannedNotifications = querySnapshot.docs.map((doc) {
var data = doc.data()! as Map<String, dynamic>;
return NotificationModel.fromJson(data);
}).toList();
for (var notification in plannedNotifications) {
if (notification.scheduledFor!.isBefore(currentTime) ||
notification.scheduledFor!.isAtSameMomentAs(currentTime)) {
await pushNotification(
notification,
[userId],
newNotificationCallback,
);
await deletePlannedNotification(notification);
//Plan new recurring notification instance
if (notification.recurring) {
var newNotification = NotificationModel(
id: UniqueKey().toString(),
title: notification.title,
body: notification.body,
recurring: true,
occuringInterval: notification.occuringInterval,
scheduledFor: DateTime.now().add(const Duration(seconds: 10)),
);
await createScheduledNotification(newNotification);
}
}
}
} on Exception catch (e) {
debugPrint("Error getting planned notifications: $e");
return;
}
}
@override
Stream<int> getActiveAmountStream() async* {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint("User is not authenticated");
yield 0;
}
var amount = FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.where("isRead", isEqualTo: false)
.snapshots()
.map((e) => e.docs.length);
yield* amount;
}
}

View file

@ -1,36 +0,0 @@
name: flutter_notification_center_firebase
description: "A new Flutter project."
publish_to: "none"
version: 4.0.0
environment:
sdk: ">=2.18.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
intl: any
# Firebase
cloud_firestore: ^4.16.0
firebase_auth: ^4.2.6
firebase_core: ^2.5.0
cupertino_icons: ^1.0.2
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 4.0.0
path: packages/flutter_notification_center
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:
uses-material-design: true

View file

@ -1,30 +0,0 @@
// // This is a basic Flutter widget test.
// //
// // To perform an interaction with a widget in your test, use the WidgetTester
// // utility in the flutter_test package. For example, you can send tap and scroll
// // gestures. You can also use WidgetTester to find child widgets in the widget
// // tree, read text, and verify that the values of widget properties are correct.
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:example/main.dart';
// void main() {
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// // Build our app and trigger a frame.
// await tester.pumpWidget(const MyApp());
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
// }

View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -0,0 +1 @@
../../CHANGELOG.md

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -0,0 +1 @@
../../README.md

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -0,0 +1,13 @@
// Interfaces
export "src/interfaces/notification_repository_interface.dart";
// Local
export "src/local/local_notification_repository.dart";
// Models
export "src/models/notification.dart";
export "src/models/notification_config.dart";
export "src/models/notification_translation.dart";
// Services
export "src/services/notification_service.dart";

View file

@ -0,0 +1,27 @@
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
abstract class NotificationRepositoryInterface {
Future<NotificationModel> addNotification(
String userId,
NotificationModel notification,
List<String> recipientIds,
);
Stream<NotificationModel?> getNotification(String userId, String id);
Stream<List<NotificationModel>> getNotifications(String userId);
Stream<List<NotificationModel>> getPlannedNotifications(String userId);
Future<NotificationModel> updateNotification(
String userId,
NotificationModel notification,
);
Future<void> deleteNotification(
String userId,
String id,
// ignore: avoid_positional_boolean_parameters
bool planned,
);
}

View file

@ -0,0 +1,92 @@
import "dart:async";
import "package:notification_center_repository_interface/src/interfaces/notification_repository_interface.dart";
import "package:notification_center_repository_interface/src/models/notification.dart";
import "package:rxdart/rxdart.dart";
class LocalNotificationRepository implements NotificationRepositoryInterface {
final List<NotificationModel> _activeNotifications = [];
final List<NotificationModel> _plannedNotifications = [];
final StreamController<List<NotificationModel>> _notificationsController =
BehaviorSubject<List<NotificationModel>>();
final StreamController<List<NotificationModel>>
_plannedNotificationsController =
BehaviorSubject<List<NotificationModel>>();
final StreamController<NotificationModel> _notificationController =
BehaviorSubject<NotificationModel>();
@override
Future<NotificationModel> addNotification(
String userId,
NotificationModel notification,
List<String> recipientIds,
) async {
if (notification.scheduledFor != null &&
notification.scheduledFor!.isAfter(DateTime.now())) {
_plannedNotifications.add(notification);
} else {
_activeNotifications.add(notification);
}
getNotifications(userId);
return notification;
}
@override
Future<void> deleteNotification(
String userId,
String id,
bool planned,
) async {
if (planned) {
_plannedNotifications.removeWhere((element) => element.id == id);
} else {
_activeNotifications.removeWhere((element) => element.id == id);
}
getNotifications(userId);
}
@override
Stream<NotificationModel?> getNotification(String userId, String id) {
var notification = _activeNotifications.firstWhere(
(element) => element.id == id,
);
_notificationController.add(notification);
return _notificationController.stream;
}
@override
Stream<List<NotificationModel>> getNotifications(String userId) {
_notificationsController.add(_activeNotifications);
return _notificationsController.stream;
}
@override
Stream<List<NotificationModel>> getPlannedNotifications(String userId) {
_plannedNotificationsController.add(_plannedNotifications);
return _plannedNotificationsController.stream;
}
@override
Future<NotificationModel> updateNotification(
String userId,
NotificationModel notification,
) async {
_activeNotifications
.removeWhere((element) => element.id == notification.id);
_activeNotifications.add(notification);
getNotifications(userId);
return notification;
}
}

View file

@ -1,5 +1,3 @@
import "package:flutter/material.dart";
/// Enum representing the interval at which notifications occur. /// Enum representing the interval at which notifications occur.
enum OcurringInterval { enum OcurringInterval {
/// Notifications occur daily. /// Notifications occur daily.
@ -39,7 +37,7 @@ class NotificationModel {
this.occuringInterval, this.occuringInterval,
this.isPinned = false, this.isPinned = false,
this.isRead = false, this.isRead = false,
this.icon = Icons.notifications, this.icon,
}); });
/// Method to create a NotificationModel object from JSON data /// Method to create a NotificationModel object from JSON data
@ -59,9 +57,7 @@ class NotificationModel {
: null, : null,
isPinned = json["isPinned"] ?? false, isPinned = json["isPinned"] ?? false,
isRead = json["isRead"] ?? false, isRead = json["isRead"] ?? false,
icon = json["icon"] != null icon = json["icon"] ?? 0xe44f;
? IconData(json["icon"], fontFamily: Icons.notifications.fontFamily)
: Icons.notifications;
/// Unique identifier for the notification. /// Unique identifier for the notification.
final String id; final String id;
@ -91,14 +87,16 @@ class NotificationModel {
bool isRead; bool isRead;
/// Icon to be displayed with the notification. /// Icon to be displayed with the notification.
final IconData icon; final int? icon;
/// Override toString() to provide custom string representation /// Override toString() to provide custom string representation
@override @override
String toString() => "NotificationModel{id: $id, title: $title, body: $body, " String toString() => """
NotificationModel{id: $id, title: $title, body: $body, "
"dateTimePushed: $dateTimePushed, scheduledFor: $scheduledFor, " "dateTimePushed: $dateTimePushed, scheduledFor: $scheduledFor, "
"recurring: $recurring, occuringInterval: $occuringInterval, " "recurring: $recurring, occuringInterval: $occuringInterval, "
"isPinned: $isPinned, icon: $icon}"; "isPinned: $isPinned, icon: $icon
}""";
/// Convert the NotificationModel object to a Map. /// Convert the NotificationModel object to a Map.
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@ -111,7 +109,7 @@ class NotificationModel {
"occuringInterval": occuringInterval?.index, "occuringInterval": occuringInterval?.index,
"isPinned": isPinned, "isPinned": isPinned,
"isRead": isRead, "isRead": isRead,
"icon": icon.codePoint, "icon": icon,
}; };
/// Create a copy of the NotificationModel with some fields replaced. /// Create a copy of the NotificationModel with some fields replaced.
@ -125,7 +123,7 @@ class NotificationModel {
OcurringInterval? occuringInterval, OcurringInterval? occuringInterval,
bool? isPinned, bool? isPinned,
bool? isRead, bool? isRead,
IconData? icon, int? icon,
}) => }) =>
NotificationModel( NotificationModel(
id: id ?? this.id, id: id ?? this.id,

View file

@ -1,6 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:notification_center_repository_interface/src/models/notification.dart";
import "package:flutter_notification_center/flutter_notification_center.dart"; import "package:notification_center_repository_interface/src/models/notification_translation.dart";
/// Configuration class for notifications. /// Configuration class for notifications.
class NotificationConfig { class NotificationConfig {
@ -11,20 +11,15 @@ class NotificationConfig {
/// notification. The [translations] parameter is also optional and provides /// notification. The [translations] parameter is also optional and provides
/// translations for notification messages. /// translations for notification messages.
const NotificationConfig({ const NotificationConfig({
required this.service,
this.translations = const NotificationTranslations.empty(), this.translations = const NotificationTranslations.empty(),
this.notificationWidgetBuilder, this.notificationWidgetBuilder,
this.showAsSnackBar = true, this.showAsSnackBar = true,
this.enableNotificationPopups = true, this.enableNotificationPopups = true,
this.bellStyle = const AnimatedNotificationBellStyle(),
this.pinnedIconColor = Colors.black, this.pinnedIconColor = Colors.black,
this.emptyNotificationsBuilder, this.emptyNotificationsBuilder,
this.onNotificationTap, this.onNotificationTap,
}); });
/// The notification service to use for delivering notifications.
final NotificationService service;
/// Translations for notification messages. /// Translations for notification messages.
final NotificationTranslations translations; final NotificationTranslations translations;
@ -39,9 +34,6 @@ class NotificationConfig {
/// Whether to show notification popups. /// Whether to show notification popups.
final bool enableNotificationPopups; final bool enableNotificationPopups;
/// The style of the notification bell.
final AnimatedNotificationBellStyle bellStyle;
/// The color of the trailing icon (if any) in the notification. /// The color of the trailing icon (if any) in the notification.
final Color? pinnedIconColor; final Color? pinnedIconColor;

View file

@ -0,0 +1,157 @@
import "dart:async";
import "package:notification_center_repository_interface/src/interfaces/notification_repository_interface.dart";
import "package:notification_center_repository_interface/src/local/local_notification_repository.dart";
import "package:notification_center_repository_interface/src/models/notification.dart";
class NotificationService {
NotificationService({
required this.userId,
this.pollingInterval = const Duration(seconds: 15),
NotificationRepositoryInterface? repository,
this.onNewNotification,
}) : repository = repository ?? LocalNotificationRepository() {
unawaited(_startTimer());
}
final NotificationRepositoryInterface repository;
final Function(NotificationModel)? onNewNotification;
final String userId;
final Duration pollingInterval;
Timer? timer;
Future<void> _startTimer() async {
timer = Timer.periodic(pollingInterval, (timer) async {
await checkForScheduledNotifications();
});
}
/// Pushes a notification to the service.
Future<void> pushNotification(
NotificationModel notification,
List<String> recipientIds,
) {
var result = repository.addNotification(
userId,
notification,
recipientIds,
);
if (recipientIds.contains(userId)) {
getActiveNotifications();
//Show popup with notification conte
if (onNewNotification != null) {
onNewNotification!.call(notification);
}
}
return result;
}
/// Retrieves the list of active notifications.
Stream<List<NotificationModel>> getActiveNotifications() =>
repository.getNotifications(userId);
/// Retrieves the list of planned notifications.
Stream<List<NotificationModel>> getPlannedNotifications() =>
repository.getPlannedNotifications(userId);
/// Creates a scheduled notification.
Future<void> createScheduledNotification(
NotificationModel notification,
List<String> recipientIds,
) async =>
pushNotification(
notification,
recipientIds,
);
/// Creates a recurring notification.
Future<void> createRecurringNotification(
NotificationModel notification,
List<String> recipientIds,
) async {
if (notification.recurring) {
switch (notification.occuringInterval) {
case OcurringInterval.daily:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 1));
case OcurringInterval.weekly:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 7));
case OcurringInterval.monthly:
notification.scheduledFor =
DateTime.now().add(const Duration(days: 30));
case OcurringInterval.debug:
notification.scheduledFor =
DateTime.now().add(const Duration(seconds: 10));
case null:
}
await createScheduledNotification(notification, recipientIds);
}
}
/// Deletes a scheduled notification.
Future<void> deletePlannedNotification(NotificationModel notification) =>
repository.deleteNotification(userId, notification.id, true);
/// Dismisses an active notification.
Future<void> dismissActiveNotification(NotificationModel notification) =>
repository.deleteNotification(userId, notification.id, false);
/// Pin an active notification.
Future<void> pinActiveNotification(NotificationModel notification) =>
repository.updateNotification(
userId,
notification.copyWith(isPinned: true),
);
/// Unpin an active notification.
Future<void> unPinActiveNotification(NotificationModel notification) =>
repository.updateNotification(
userId,
notification.copyWith(isPinned: false),
);
/// Marks a notification as read.
Future<void> markNotificationAsRead(NotificationModel notification) =>
repository.updateNotification(
userId,
notification.copyWith(isRead: true),
);
/// Checks for scheduled notifications.
Future<void> checkForScheduledNotifications() async {
var notifications = await repository.getPlannedNotifications(userId).first;
for (var notification in notifications) {
if (notification.scheduledFor != null &&
notification.scheduledFor!.isBefore(DateTime.now()) ||
notification.scheduledFor!.isAtSameMomentAs(DateTime.now())) {
await pushNotification(
notification,
[userId],
);
await deletePlannedNotification(notification);
if (notification.recurring) {
await createRecurringNotification(
notification,
[userId],
);
}
}
}
}
/// Returns a stream of the number of active notifications.
Stream<int> getActiveAmountStream() => repository
.getNotifications(userId)
.map((notifications) => notifications.length);
}

View file

@ -0,0 +1,20 @@
name: notification_center_repository_interface
description: "A new Flutter package project."
version: 5.0.0
homepage:
environment:
sdk: ^3.5.3
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
rxdart: any
dev_dependencies:
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0