Compare commits

..

17 commits

Author SHA1 Message Date
Freek van de Ven
463a745053 feat: add component release workflow 2025-02-13 19:21:05 +01:00
Freek van de Ven
006a83dcde feat: add hosted version of iconica packages 2025-02-13 19:21:05 +01:00
Freek van de Ven
17350efe91 chore: add pubspec_overrides.yaml to .gitignore 2025-02-13 19:21:05 +01:00
dependabot[bot]
5e3e22d870 chore(deps): bump intl in /packages/flutter_notification_center
Bumps [intl](https://github.com/dart-lang/i18n/tree/main/pkgs) from 0.19.0 to 0.20.1.
- [Release notes](https://github.com/dart-lang/i18n/releases)
- [Commits](https://github.com/dart-lang/i18n/commits/intl-v0.20.1/pkgs)

---
updated-dependencies:
- dependency-name: intl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 19:21:05 +01:00
dependabot[bot]
c246abdbc9 build(deps): bump flutter_lints
Bumps [flutter_lints](https://github.com/flutter/packages/tree/main/packages) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/flutter/packages/releases)
- [Commits](https://github.com/flutter/packages/commits/flutter_lints-v5.0.0/packages)

---
updated-dependencies:
- dependency-name: flutter_lints
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 18:19:45 +01:00
HammadAsiif
dce9ac0b4f chore: update the documentation 2025-02-13 18:19:45 +01:00
mike doornenbal
406589194f
Merge pull request #28 from Iconica-Development/5.0.0
refactor: new component structure
2024-09-24 14:40:47 +02:00
367303aecf refactor: new component structure 2024-09-24 13:50:11 +02:00
mike doornenbal
ddf2a2bbb3
Merge pull request #27 from Iconica-Development/bugfix/feedback_v2
fix: unread notification count, title style, onTap notification
2024-09-05 11:33:34 +02:00
mike doornenbal
90738db549 fix: notification center 2024-08-14 16:08:03 +02:00
Gorter-dev
43a7f44e88
Merge pull request #26 from Iconica-Development/bugfix/feedback
fix: change default style
2024-07-30 13:41:04 +02:00
mike doornenbal
dbf83cc8d6 fix: remove debugprints 2024-07-30 13:30:35 +02:00
mike doornenbal
0b14e46833 fix: change default style 2024-07-29 13:52:56 +02:00
mike doornenbal
2acb617512
Merge pull request #25 from Iconica-Development/bugfix/notification_center
fix: styling and service
2024-07-18 13:12:33 +02:00
mike doornenbal
b929ff5af9 fix: delete notification bug 2024-07-12 14:39:34 +02:00
mike doornenbal
91621bde96 fix: default style 2024-07-12 14:20:42 +02:00
mike doornenbal
169228152e feat: rework flutter_notification_center design 2024-06-06 21:56:13 +02:00
56 changed files with 1467 additions and 1429 deletions

14
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: Iconica Standard Component Release Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/component-release.yml@master
secrets: inherit
permissions: write-all

3
.gitignore vendored
View file

@ -49,8 +49,11 @@ 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
pubspec_overrides.yaml

View file

@ -1,3 +1,22 @@
## [5.1.0] - 13 February 2025
* Update intl to 0.20.1
## [5.0.0] - 24 September 2024
* Refactor package with the new component structure
## [4.0.0] - 14 August 2024
* Fix overflow issue with long text in notification
* Fix new notification only sending to the person that triggered the notification
* Added `onNotificationTap` to a notification
## [2.0.0] - 6 June 2024
* Rework design for notification center
* Added iconica linter
## [1.4.1] - 4 June 2024 ## [1.4.1] - 4 June 2024
* Fix notification amount number to properly size and show plus icon when above certain amount * Fix notification amount number to properly size and show plus icon when above certain amount

323
README.md
View file

@ -1,59 +1,300 @@
# flutter_notification_center # flutter_notification_center
A Flutter package for creating notification center displaying a list of notifications.
`flutter_notification_center` is a comprehensive and flexible notification management package for Flutter applications. It allows developers to integrate real-time notifications, schedule messages, and provide an interactive notification center, with support for Firebase and local data storage. The package is highly customizable, enabling developers to adjust the UI, translate notification messages, and configure notifications to enhance user engagement.
---
## Table of Contents
- [Features](#features)
- [Getting Started](#getting-started)
- [Installation](#installation)
- [Usage](#usage)
- [Setting Up Firebase](#setting-up-firebase)
- [Configuring the Notification Center](#configuring-the-notification-center)
- [Displaying Notifications](#displaying-notifications)
- [Customization](#customization)
- [UI Customization](#ui-customization)
- [Translations](#translations)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Issues](#issues)
- [Contributing](#contribute)
- [Author](#author)
---
## Features ## Features
- Notification center: A page containing a list of notifications. - **Real-time Notifications**: Fetch and display active notifications in real-time.
- Notification bell: A clickable bell icon that can be placed anywere showing the amound of unread notifications. Clicking the bell takes the user to the notification center. - **Scheduled and Recurring Notifications**: Set notifications for future delivery or create recurring notifications.
- Pinned notifications: A pinned notification can't be dismissed by the user from the notification center. - **Interactive Notification Center**: Display notifications with interactive options to dismiss, pin, or mark them as read.
- Dismissable notifications: A dismissable notification can be dismissed by the user from the notification center. - **Customizable UI**: Adjust UI elements like icons, colors, and layout of the notification center.
- Notification detail page: Clicking on a notification takes the user to a notification detail page. - **Localization**: Easily translate notification messages for multiple languages.
- Notification types: Notification types that can be created are: instant, scheduled, recurring. - **Modular Architecture**: Compatible with Firebase and local storage solutions, allowing flexibility in backend configuration.
- Notification popups: If a notification is pushed a popup can appear in form of a dialog or snackbar.
## Setup ---
To use this package, add `flutter_notification_center` as a dependency in your pubspec.yaml file. ## Getting Started
- For custom notification styling provide the optional notificationWidgetBuilder with your own implementation. ### Core Components
1. **Notification Center**: Provides a UI for displaying notifications.
2. **Notification Bell**: Displays the count of active notifications with animation support.
3. **Notification Service**: Manages notifications, including adding, deleting, and retrieving notifications.
The `NotificationConfig` has its own parameters, as specified below: ### Requirements
| Parameter | Explanation | 1. **Firebase**: Firebase should be initialized for storing notifications in Firestore.
|-----------|-------------| 2. **User Identification**: Each user requires a unique `userId` for personalized notifications.
| service | The notification service that will be used |
| seperateNotificationsWithDivider | If true notifications will be seperated with dividers within the notification center |
| translations | The translations that will be used |
| notificationWidgetBuilder | The widget that defines the styles and logic for every notification |
| enableNotificationPopups | If set to false no popups will be shown if a new notification is pushed |
| showAsSnackBar | If true notifications popups will show as snackbar. If false shown as dialog|
If you set `enableNotificationPopups` to true, you can use `PopupHandler` in the `newNotificationCallback` to display popups in case a new notification is pushed. ---
The `notificationWidgetBuilder` expects the following parameters, as specified below: ## Installation
| Parameter | Explanation |
|-----------|-------------| 1. **Add Dependencies**: Add required dependencies in `pubspec.yaml`.
| notification | The notification that is being defined |
| style | The styling that will be used for the notification | ```yaml
| notificationService | The notification service that will be used | dependencies:
| notificationTranslations | The translations that will be used for the notification| firebase_notification_center_repository:
| context | The provided context | git:
url: https://github.com/Iconica-Development/flutter_notification_center.git
path: packages/firebase_notification_center_repository
ref: 5.0.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
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center.git
path: packages/flutter_notification_center
ref: 5.0.0
firebase_core: ^latest_version
cloud_firestore: ^latest_version
intl: ^latest_version
```
2. **Firebase Setup**:
- Follow [Firebase setup instructions](https://firebase.google.com/docs/flutter/setup) for both Android and iOS.
- Ensure Firestore is configured with `active_notifications` and `planned_notifications` collections for immediate and scheduled notifications, respectively.
3. **Asset Configuration**:
- Some icons, like `unpin_icon.svg`, may need to be referenced in `pubspec.yaml`.
```yaml
flutter:
assets:
- assets/unpin_icon.svg
```
4. **Initialize Firebase**:
- Initialize Firebase in the `main.dart` file.
```dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
```
---
## Usage ## Usage
Create instant notification:
- Make a call to pushNotification() and provide a NotificationModel with the required attributes.
Create scheduled notification: ### Setting Up Firebase
- Make a call to createScheduledNotification() and provide a NotificationModel with the required attributes + scheduledFor.
Create recurring notification: 1. **Initialize FirebaseNotificationRepository**:
- Make a call to createRecurringNotification() and provide a NotificationModel with the required attributes and the following additional attributes: - This repository interacts with Firebase Firestore for managing notifications.
- scheduledFor
- recurring = true
- occuringInterval with the pre defined interval (daily, weekly, monthly)
To create a pinned notification, set isPinned = true when creating the notification. ```dart
final notificationRepository = FirebaseNotificationRepository(
activeNotificationsCollection: "active_notifications",
plannedNotificationsCollection: "planned_notifications",
);
```
### Example 2. **Firestore Configuration**:
- Ensure Firestore is correctly set up with appropriate collections.
### Configuring the Notification Center
1. **NotificationBell Widget**:
- Displays the count of active notifications.
```dart
final notificationBell = NotificationBell(
config: NotificationConfig(),
service: notificationService, // An instance of NotificationService
onTap: () {
// Define behavior when tapped, e.g., navigate to NotificationCenter
},
);
```
2. **NotificationCenter Widget**:
- Displays a list of notifications, allowing users to interact with each one.
```dart
final notificationCenter = NotificationCenter(
config: NotificationConfig(
translations: NotificationTranslations(appBarTitle: "Notifications"),
showAsSnackBar: true,
enableNotificationPopups: true,
),
service: notificationService,
);
```
### Displaying Notifications
1. **Adding Notifications**:
- Use `addNotification` to add new notifications to the repository.
```dart
final newNotification = NotificationModel(
id: "123",
title: "New Alert",
body: "You have a new notification",
scheduledFor: DateTime.now().add(Duration(days: 1)),
);
notificationRepository.addNotification("user_id", newNotification, ["recipient_id"]);
```
2. **Streaming Notifications**:
- Use `getNotifications` to listen to real-time updates of active notifications.
```dart
notificationRepository.getNotifications("user_id").listen((notifications) {
for (var notification in notifications) {
print(notification.title);
}
});
```
3. **Deleting Notifications**:
- Remove a notification by calling `deleteNotification`.
```dart
notificationRepository.deleteNotification("user_id", "notification_id", false);
```
---
## Customization
### UI Customization
1. **Notification Bell Styling**:
- Customize the `NotificationBell` appearance with `AnimatedNotificationBellStyle`.
```dart
final notificationBell = NotificationBell(
animatedIconStyle: AnimatedNotificationBellStyle(
color: Colors.blue,
size: 24,
),
onTap: () { /* Navigate to NotificationCenter */ },
);
```
2. **Notification Display Layout**:
- Customize the `NotificationCenter` layout with `NotificationConfig`, adjusting elements like the app bar title, icon color, and UI interactions.
```dart
final notificationConfig = NotificationConfig(
showAsSnackBar: true,
enableNotificationPopups: true,
pinnedIconColor: Colors.green,
);
```
### Translations
1. **Localized Translations**:
- `NotificationTranslations` provides customizable text for various notification messages.
```dart
final translations = NotificationTranslations(
appBarTitle: "Mis Notificaciones",
notificationDismissed: "Notificación descartada",
notificationPinned: "Notificación fijada",
notificationUnpinned: "Notificación desmarcada",
);
final notificationConfig = NotificationConfig(translations: translations);
```
2. **Dialog Customization**:
- Adjust dialog text within `NotificationDialog` using custom translations, ideal for supporting multiple languages.
---
## API Reference
### Classes
- **NotificationService**: Manages notifications, with methods for adding, updating, deleting, and scheduling.
- **NotificationModel**: Represents a notification structure with fields like `id`, `title`, `body`, and `scheduledFor`.
- **NotificationConfig**: Customization class for notifications, allowing display options like snackbar or dialog.
- **NotificationRepositoryInterface**: Defines the required methods for a notification repository, enabling various backend implementations.
- **LocalNotificationRepository**: In-memory implementation, useful for testing.
### Key Methods
- `addNotification(userId, notification, recipientIds)`: Adds a new notification.
- `deleteNotification(userId, id, planned)`: Deletes a notification.
- `getNotifications(userId)`: Streams active notifications.
- `getPlannedNotifications(userId)`: Streams scheduled notifications.
- `updateNotification(userId, notification)`: Updates an existing notification.
---
## Examples
### Basic Usage
```dart
final firebaseRepo = FirebaseNotificationRepository();
final notification = NotificationModel(
id: "notif_01",
title: "Welcome!",
body: "Thank you for signing up.",
scheduledFor: DateTime.now().add(Duration(hours: 1)),
);
// Adding a notification
await firebaseRepo.addNotification("user_123", notification, ["user_123"]);
// Listening to active notifications
firebaseRepo.getNotifications("user_123").listen((notifications) {
notifications.forEach((notif) => print(notif.title));
});
```
---
### Advanced Usage With Custom Translations
```dart
final translations = NotificationTranslations(
appBarTitle: "My Custom Notifications",
noNotifications: "No notifications at this time.",
);
final notificationConfig = NotificationConfig(
translations: translations,
showAsSnackBar: true,
);
// Using the config in a custom notification center setup
final notificationCenter = NotificationCenter(
config: notificationConfig,
service: firebaseRepo,
);
```
See [Example Code](example/lib/main.dart) for more info. See [Example Code](example/lib/main.dart) for more info.
@ -61,7 +302,7 @@ See [Example Code](example/lib/main.dart) for more info.
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_notification_center/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_notification_center/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute ## Contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_notification_center/pulls). If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_notification_center/pulls).

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

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,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 @@
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,26 @@
name: firebase_notification_center_repository
description: "A new Flutter package project."
version: 5.1.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:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^5.1.0
dev_dependencies:
flutter_iconica_analysis:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^7.0.0

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

@ -1,28 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:flutter_iconica_analysis/analysis_options.yaml
# 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`.
# The following line activates a set of recommended lints for Flutter apps, # Possible to overwrite the rules from the package
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml analyzer:
exclude:
linter: 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: 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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M680-840v80h-40v327l-80-80v-247H400v87l-87-87-33-33v-47h400ZM480-40l-40-40v-240H240v-80l80-80v-46L56-792l56-56 736 736-58 56-264-264h-6v240l-40 40ZM354-400h92l-44-44-2-2-46 46Zm126-193Zm-78 149Z"/></svg>

After

Width:  |  Height:  |  Size: 319 B

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

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

View file

@ -1,179 +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 NotificationStyle style;
final FirebaseNotificationService notificationService;
final NotificationTranslations notificationTranslations;
final BuildContext context;
const CustomNotificationWidget({
required this.notification,
required this.style,
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(
leading: style.showNotificationIcon != null
? Icon(
notification.icon,
color: style.leadingIconColor,
)
: null,
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
notification.title,
style: style.titleTextStyle,
),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.push_pin),
color: style.pinnedIconColor,
onPressed: () async =>
_navigateToNotificationDetail(context, notification),
padding: const EdgeInsets.only(left: 60.0),
),
),
),
)
//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: BoxDecoration(
shape: BoxShape.circle,
color: style.isReadDotColor,
),
)
: 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,
notificationStyle: style,
),
),
);
}
}
}

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,46 +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 {
style: const NotificationStyle( await service.pushNotification(
appTitleTextStyle: TextStyle( NotificationModel(
color: Colors.black, id: DateTime.now().millisecondsSinceEpoch.toString(),
fontSize: 20, 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,
), ),
titleTextStyle: TextStyle( recipientIds,
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16,
),
leadingIconColor: Colors.grey,
pinnedIconColor: Colors.grey,
isReadDotColor: Colors.red,
showNotificationIcon: true,
),
notificationService: service,
notificationTranslations: const NotificationTranslations.empty(),
context: context,
),
seperateNotificationsWithDivider: true,
); );
popupHandler = PopupHandler(context: context, config: config);
} }
@override @override
@ -95,7 +90,8 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
centerTitle: true, centerTitle: true,
actions: [ actions: [
NotificationBellWidgetStory( NotificationBellWidgetStory(
config: config, userId: userId,
service: service,
), ),
], ],
), ),

View file

@ -4,39 +4,45 @@ description: "A new Flutter project."
# 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
ref: 1.4.0
path: packages/flutter_notification_center
flutter_notification_center_firebase: firebase_notification_center_repository:
git: path: ../../firebase_notification_center_repository
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center_firebase
intl: ^0.20.1
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: ^5.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 "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_bell_story.dart";
export "src/notification_center.dart"; export "src/notification_center.dart";
export "src/services/notification_service.dart";
export "package:flutter_animated_widgets/flutter_animated_widgets.dart"; // Screens
export "src/screens/notification_bell.dart";
export "src/screens/notification_detail.dart";
// Widgets
export "src/widgets/notification_dialog.dart";
export "src/widgets/notification_snackbar.dart";
export "src/widgets/popup_handler.dart";

View file

@ -1,71 +0,0 @@
import "package:flutter/material.dart";
/// Defines the appearance customization for notifications.
class NotificationStyle {
/// Creates a new [NotificationStyle] instance.
///
/// Each parameter is optional and allows customizing various aspects
/// of the notification appearance.
const NotificationStyle({
this.titleTextStyle,
this.subtitleTextStyle,
this.backgroundColor,
this.leadingIconColor,
this.pinnedIconColor,
this.contentPadding,
this.titleTextAlign,
this.subtitleTextAlign,
this.dense,
this.tileDecoration,
this.emptyNotificationsBuilder,
this.appTitleTextStyle,
this.dividerColor,
this.isReadDotColor,
this.showNotificationIcon,
});
/// The text style for the title of the notification.
final TextStyle? titleTextStyle;
/// The text style for the subtitle of the notification.
final TextStyle? subtitleTextStyle;
/// The background color of the notification.
final Color? backgroundColor;
/// The color of the leading icon (if any) in the notification.
final Color? leadingIconColor;
/// The color of the trailing icon (if any) in the notification.
final Color? pinnedIconColor;
/// The padding around the content of the notification.
final EdgeInsets? contentPadding;
/// The alignment of the title text within the notification.
final TextAlign? titleTextAlign;
/// The alignment of the subtitle text within the notification.
final TextAlign? subtitleTextAlign;
/// Whether the notification should be dense or not.
final bool? dense;
/// The decoration to apply to the tile of the notification.
final BoxDecoration? tileDecoration;
/// A builder function to display when there are no notifications.
final Widget Function()? emptyNotificationsBuilder;
/// The text style for the application title.
final TextStyle? appTitleTextStyle;
/// The color of the divider.
final Color? dividerColor;
/// The color of the dot that shows that anotification has not been read.
final Color? isReadDotColor;
/// Whether to show the notification icon.
final bool? showNotificationIcon;
}

View file

@ -1,27 +1,64 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../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,5 +1,8 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import '../flutter_notification_center.dart'; import "package:flutter_notification_center/src/screens/notification_detail.dart";
import "package:flutter_svg/svg.dart";
import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// Widget for displaying the notification center. /// Widget for displaying the notification center.
class NotificationCenter extends StatefulWidget { class NotificationCenter extends StatefulWidget {
@ -8,257 +11,243 @@ 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) {
_notificationsFuture = widget.config.service.getActiveNotifications();
});
widget.config.service.addListener(_listener);
}
@override widget.service.getActiveAmountStream().listen((data) {
void dispose() {
widget.config.service.removeListener(_listener);
super.dispose();
}
void _listener() {
setState(() {}); setState(() {});
});
} }
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text( title: Text(
widget.config.translations.appBarTitle, widget.config.translations.appBarTitle,
style: theme.textTheme.headlineLarge,
), ),
centerTitle: true, centerTitle: true,
leading: IconButton( iconTheme: theme.appBarTheme.iconTheme ??
icon: const Icon(Icons.arrow_back), const IconThemeData(color: Colors.white),
onPressed: () { leading: GestureDetector(
onTap: () {
Navigator.pop(context); Navigator.pop(context);
}, },
child: const Icon(
Icons.arrow_back_ios,
), ),
), ),
body: FutureBuilder<List<NotificationModel>>( ),
future: _notificationsFuture, body: StreamBuilder<List<NotificationModel>>(
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());
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
debugPrint("Error: ${snapshot.error}"); debugPrint("Error: ${snapshot.error}");
return Center( return Center(child: Text(widget.config.translations.errorMessage));
child: Text(widget.config.translations.errorMessage));
} else if (snapshot.data == null || snapshot.data!.isEmpty) { } else if (snapshot.data == null || snapshot.data!.isEmpty) {
return Center( return widget.config.emptyNotificationsBuilder?.call() ??
child: Text(widget.config.translations.noNotifications), Center(
child: Text(
widget.config.translations.noNotifications,
style: theme.textTheme.bodyMedium,
),
); );
} else { } else {
return ListView.builder( return ListView.builder(
itemCount: snapshot.data!.length * 2 - 1, padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index.isOdd) { var notification = snapshot.data![index];
return Padding( return notification.isPinned
padding: const EdgeInsets.symmetric(horizontal: 24.0), ? GestureDetector(
child: widget.config.seperateNotificationsWithDivider onTap: () async {
? const Divider( if (widget.config.onNotificationTap != null) {
color: Colors.grey, widget.config.onNotificationTap!.call(notification);
thickness: 1.0, } else {
) await _navigateToNotificationDetail(
: Container(), context,
notification,
widget.service,
widget.config.translations,
); );
} }
var notification = snapshot.data![index ~/ 2]; },
return Padding( child: Dismissible(
padding: const EdgeInsets.symmetric(horizontal: 12.0), key: Key("${notification.id}_pinned"),
child: widget.config.notificationWidgetBuilder != null
? widget.config.notificationWidgetBuilder!(
notification, context)
: notification.isPinned
//Pinned notification
? Dismissible(
key: Key('${notification.id}_pinned'),
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) {
await unPinNotification( await unPinNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context); context,
);
} else if (direction ==
DismissDirection.startToEnd) {
await unPinNotification(
widget.service,
notification,
widget.config.translations,
context,
);
}
}, },
background: Container( background: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color(0xFF88CB33),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: const Padding( child: Padding(
padding: EdgeInsets.only(left: 16.0), padding: const EdgeInsets.only(left: 16.0),
child: Icon( child: SvgPicture.asset(
Icons.push_pin, "assets/unpin_icon.svg",
color: Colors.white, package: "flutter_notification_center",
theme: const SvgTheme(
currentColor: Colors.white,
),
height: 24,
), ),
), ),
), ),
secondaryBackground: Container( secondaryBackground: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color(0xFF88CB33),
alignment: Alignment.centerLeft, borderRadius: BorderRadius.only(
child: const Padding( topRight: Radius.circular(6),
padding: EdgeInsets.only(left: 16.0), bottomRight: Radius.circular(6),
child: Icon( ),
Icons.push_pin, ),
color: Colors.white, alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: SvgPicture.asset(
"assets/unpin_icon.svg",
package: "flutter_notification_center",
theme: const SvgTheme(
currentColor: Colors.white,
),
height: 24,
), ),
), ),
), ),
child: GestureDetector( child: _notificationItem(
onTap: () async =>
_navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service, widget.config,
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 : GestureDetector(
: Dismissible( onTap: () async {
if (widget.config.onNotificationTap != null) {
widget.config.onNotificationTap!.call(notification);
} else {
await _navigateToNotificationDetail(
context,
notification,
widget.service,
widget.config.translations,
);
}
},
child: Dismissible(
key: Key(notification.id), key: Key(notification.id),
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == if (direction == DismissDirection.endToStart) {
DismissDirection.endToStart) {
await dismissNotification( await dismissNotification(
widget.config.service, widget.service,
notification, notification,
widget.config.translations, widget.config.translations,
context); context,
);
} 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,
);
} }
}, },
background: Container( background: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color(0xFF88CB33),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: const Padding( child: Padding(
padding: EdgeInsets.only(left: 16.0), padding: const EdgeInsets.only(left: 16.0),
child: Icon( child: Transform.rotate(
Icons.push_pin, angle: 0.5,
child: const Icon(
Icons.push_pin_outlined,
color: Colors.white, color: Colors.white,
), ),
), ),
), ),
secondaryBackground: Container( ),
color: secondaryBackground: Padding(
const Color.fromRGBO(255, 131, 131, 1), padding: const EdgeInsets.only(bottom: 8),
child: Container(
decoration: const BoxDecoration(
color: Color(0xFFFF6161),
borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(right: 16.0), padding: EdgeInsets.only(right: 16.0),
child: Icon( child: Icon(
Icons.delete, Icons.delete,
color: Colors.black, color: Colors.white,
), ),
), ),
), ),
child: GestureDetector( ),
onTap: () async => child: _notificationItem(
_navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service, widget.config,
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,
),
),
));
}, },
); );
} }
@ -266,15 +255,95 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
); );
} }
}
Widget _notificationItem(
BuildContext context,
NotificationModel notification,
NotificationConfig config,
) {
var theme = Theme.of(context);
String? dateTimePushed;
if (notification.dateTimePushed != null) {
dateTimePushed = DateFormat("dd/MM/yyyy 'at' HH:mm")
.format(notification.dateTimePushed!);
}
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!notification.isPinned) ...[
if (!notification.isRead) ...[
const SizedBox(
width: 8,
),
const Padding(
padding: EdgeInsets.only(top: 8),
child: Icon(
Icons.circle_rounded,
color: Colors.black,
size: 8,
),
),
const SizedBox(
width: 8,
),
],
] else ...[
const SizedBox(
width: 8,
),
Transform.rotate(
angle: 0.5,
child: Icon(
Icons.push_pin_outlined,
color: config.pinnedIconColor,
size: 30,
),
),
const SizedBox(
width: 8,
),
],
Flexible(
child: Text(
notification.title,
style: notification.isRead && !notification.isPinned
? theme.textTheme.bodyMedium
: theme.textTheme.titleMedium,
),
),
],
),
Text(
dateTimePushed ?? "",
style: theme.textTheme.labelSmall,
),
],
),
),
),
);
}
Future<void> _navigateToNotificationDetail( Future<void> _navigateToNotificationDetail(
BuildContext context, BuildContext context,
NotificationModel notification, NotificationModel notification,
NotificationService notificationService, NotificationService notificationService,
NotificationTranslations notificationTranslations, NotificationTranslations notificationTranslations,
NotificationStyle style,
) async { ) async {
await markNotificationAsRead(notificationService, notification);
if (context.mounted) { if (context.mounted) {
await Navigator.push( await Navigator.push(
context, context,
@ -282,11 +351,11 @@ Future<void> _navigateToNotificationDetail(
builder: (context) => NotificationDetailPage( builder: (context) => NotificationDetailPage(
translations: notificationTranslations, translations: notificationTranslations,
notification: notification, notification: notification,
notificationStyle: style,
), ),
), ),
); );
} }
await markNotificationAsRead(notificationService, notification);
} }
Future<void> dismissNotification( Future<void> dismissNotification(

View file

@ -1,69 +0,0 @@
import "package:flutter/material.dart";
import "../flutter_notification_center.dart";
import "package:intl/intl.dart";
/// A page displaying the details of a notification.
class NotificationDetailPage extends StatelessWidget {
/// Creates a new [NotificationDetailPage] instance.
///
/// The [notificationStyle] parameter specifies the notification style.
///
/// The [notification] parameter specifies the notification
/// to display details for.
const NotificationDetailPage({
required this.notificationStyle,
required this.notification,
required this.translations,
super.key,
});
/// The notification style.
final NotificationStyle notificationStyle;
/// The notification to display details for.
final NotificationModel notification;
/// The translations for the notification detail page.
final NotificationTranslations translations;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(
translations.appBarTitle,
style: notificationStyle.appTitleTextStyle,
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification.title,
style: notificationStyle.titleTextStyle ?? const TextStyle(),
),
const SizedBox(height: 10),
Text(
notification.body,
style:
notificationStyle.subtitleTextStyle ?? const TextStyle(),
),
const SizedBox(height: 10),
Text(
'${translations.datePrefix} ${DateFormat('yyyy-MM-dd HH:mm').format(
notification.dateTimePushed ?? DateTime.now(),
)}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
),
);
}

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../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;
}); });
@ -46,11 +55,13 @@ class _NotificationBellState extends State<NotificationBell> {
@override @override
Widget build(BuildContext context) => IconButton( Widget build(BuildContext context) => IconButton(
padding: EdgeInsets.zero,
onPressed: widget.onTap, onPressed: widget.onTap,
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

@ -0,0 +1,76 @@
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:notification_center_repository_interface/notification_center_repository_interface.dart";
/// A page displaying the details of a notification.
class NotificationDetailPage extends StatelessWidget {
/// Creates a new [NotificationDetailPage] instance.
///
/// The [notificationStyle] parameter specifies the notification style.
///
/// The [notification] parameter specifies the notification
/// to display details for.
const NotificationDetailPage({
required this.notification,
required this.translations,
super.key,
});
/// The notification to display details for.
final NotificationModel notification;
/// The translations for the notification detail page.
final NotificationTranslations translations;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text(
translations.appBarTitle,
style: theme.textTheme.headlineLarge,
),
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(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification.title,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 10),
Text(
notification.body,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 10),
Text(
"${translations.datePrefix}"
' ${DateFormat('yyyy-MM-dd HH:mm').format(
notification.dateTimePushed ?? DateTime.now(),
)}',
style: theme.textTheme.labelSmall,
),
],
),
),
),
);
}
}

View file

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

View file

@ -1,14 +1,14 @@
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({
super.key,
required String title, required String title,
required String body, required String body,
required NotificationTranslations translations, required NotificationTranslations translations,
required VoidCallback onDismiss, required VoidCallback onDismiss,
super.key,
DateTime? datetimePublished, DateTime? datetimePublished,
}) : super( }) : super(
content: Column( content: Column(
@ -33,7 +33,7 @@ class NotificationSnackbar extends SnackBar {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
datetimePublished != null datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished) ? DateFormat("dd-MM-yyyy HH:mm").format(datetimePublished)
: translations.notAvailable, : translations.notAvailable,
style: const TextStyle( style: const TextStyle(
fontSize: 12.0, fontSize: 12.0,

View file

@ -1,18 +1,18 @@
// 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 {
final BuildContext context;
final NotificationConfig config;
PopupHandler({ PopupHandler({
required this.context, required this.context,
required this.config, 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.enableNotificationPopups) return;
if (config.showAsSnackBar) { if (config.showAsSnackBar) {
@ -31,7 +31,7 @@ class PopupHandler {
} else { } else {
if (ModalRoute.of(context)?.isCurrent != true) return; if (ModalRoute.of(context)?.isCurrent != true) return;
showDialog( await showDialog(
context: context, context: context,
builder: (context) => NotificationDialog( builder: (context) => NotificationDialog(
translations: config.translations, translations: config.translations,

View file

@ -1,30 +1,33 @@
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."
version: 5.1.0
homepage:
publish_to: 'none' publish_to: 'none'
version: 1.4.1
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.20.1
flutter_svg: ^2.0.10+1
flutter_animated_widgets: flutter_animated_widgets:
git: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
url: https://github.com/Iconica-Development/flutter_animated_widgets version: ^0.3.1
ref: 0.2.0
notification_center_repository_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^5.1.0
dev_dependencies: dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis: flutter_iconica_analysis:
git: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
url: https://github.com/Iconica-Development/flutter_iconica_analysis version: ^7.0.0
ref: 7.0.0
# The following section is specific to Flutter packages.
flutter: flutter:
uses-material-design: true 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,28 +0,0 @@
# 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`.
# 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
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,5 +0,0 @@
// SPDX-FileCopyrightText: 2024 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
export "src/services/firebase_notification_service.dart";

View file

@ -1,401 +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 {
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;
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();
});
}
@override
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');
return;
}
CollectionReference notifications =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection);
DateTime currentDateTime = DateTime.now();
notification.dateTimePushed = currentDateTime;
Map<String, dynamic> notificationMap = notification.toMap();
await notifications.doc(notification.id).set(notificationMap);
listOfActiveNotifications = [...listOfActiveNotifications, notification];
//Show popup with notification conte
if (onNewNotification != null) {
onNewNotification(notification);
} else {
newNotificationCallback(notification);
}
notifyListeners();
} 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);
QuerySnapshot 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;
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;
} 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:
}
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);
Map<String, dynamic> notificationMap = notification.toMap();
await plannedNotifications.doc(notification.id).set(notificationMap);
} 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}');
}
} 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
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
notifyListeners();
} 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();
} 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();
} 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();
} catch (e) {
debugPrint('Error updating document: $e');
}
}
@override
Future<void> checkForScheduledNotifications() async {
DateTime 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);
QuerySnapshot querySnapshot = await plannedNotificationsResult.get();
if (querySnapshot.docs.isEmpty) {
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>;
return NotificationModel.fromJson(data);
}).toList();
for (NotificationModel notification in plannedNotifications) {
if (notification.scheduledFor!.isBefore(currentTime) ||
notification.scheduledFor!.isAtSameMomentAs(currentTime)) {
await pushNotification(notification, newNotificationCallback);
await deletePlannedNotification(notification);
//Plan new recurring notification instance
if (notification.recurring) {
NotificationModel 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);
}
}
}
} 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)
.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: 1.4.1
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: 1.4.0
path: packages/flutter_notification_center
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.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 '../../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,21 +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.seperateNotificationsWithDivider = true,
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.emptyNotificationsBuilder,
this.onNotificationTap,
}); });
/// 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. /// Translations for notification messages.
final NotificationTranslations translations; final NotificationTranslations translations;
@ -33,12 +27,18 @@ class NotificationConfig {
final Widget Function(NotificationModel, BuildContext)? final Widget Function(NotificationModel, BuildContext)?
notificationWidgetBuilder; 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; final bool showAsSnackBar;
/// Whether to show notification popups. /// Whether to show notification popups.
final bool enableNotificationPopups; final bool enableNotificationPopups;
/// The style of the notification bell. /// The color of the trailing icon (if any) in the notification.
final AnimatedNotificationBellStyle bellStyle; final Color? pinnedIconColor;
/// A builder function to display when there are no notifications.
final Widget Function()? emptyNotificationsBuilder;
final Function(NotificationModel)? onNotificationTap;
} }

View file

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

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,19 @@
name: notification_center_repository_interface
description: "A new Flutter package project."
version: 5.1.0
homepage:
environment:
sdk: ^3.5.3
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
rxdart: any
dev_dependencies:
flutter_iconica_analysis:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^7.0.0