Browse Source

neon: Adapt to push proxy changes and fix push notifications

pull/162/head
jld3103 2 years ago
parent
commit
961b5b0f0b
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 1
      packages/neon/integration_test/screenshot_test.dart
  2. 16
      packages/neon/lib/l10n/en.arb
  3. 26
      packages/neon/lib/l10n/localizations.dart
  4. 21
      packages/neon/lib/l10n/localizations_en.dart
  5. 1
      packages/neon/lib/main.dart
  6. 6
      packages/neon/lib/src/app.dart
  7. 57
      packages/neon/lib/src/blocs/push_notifications.dart
  8. 25
      packages/neon/lib/src/models/push_notification.dart
  9. 21
      packages/neon/lib/src/models/push_notification.g.dart
  10. 20
      packages/neon/lib/src/models/push_notification_with_account.dart
  11. 18
      packages/neon/lib/src/models/push_notification_with_account.g.dart
  12. 2
      packages/neon/lib/src/neon.dart
  13. 7
      packages/neon/lib/src/pages/settings.dart
  14. 2
      packages/neon/lib/src/utils/global.dart
  15. 33
      packages/neon/lib/src/utils/global_options.dart
  16. 23
      packages/neon/lib/src/utils/push_utils.dart

1
packages/neon/integration_test/screenshot_test.dart

@ -126,7 +126,6 @@ Future pumpAppPage(
accountsBloc,
sharedPreferences,
globalOptions,
null,
platform,
);

16
packages/neon/lib/l10n/en.arb

@ -81,16 +81,16 @@
"globalOptionsThemeModeAutomatic": "Automatic",
"globalOptionsThemeOLEDAsDark": "OLED theme as dark theme",
"globalOptionsThemeKeepOriginalAccentColor": "Keep the original accent color",
"globalOptionsPushNotificationsNotice": "External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.",
"globalOptionsPushNotificationsEnabled": "Enabled",
"globalOptionsPushNotificationsEnabledDisabledNotice": "No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications",
"globalOptionsPushNotificationsEnabledDisabledNotice": "No UnifiedPush distributor could be found or you denied the permission for showing notifications. Please go to the app settings and allow notifications and go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications",
"globalOptionsPushNotificationsDistributor": "UnifiedPush Distributor",
"globalOptionsPushNotificationsDistributorGotifyUP": "Gotify-UP",
"globalOptionsPushNotificationsDistributorFirebaseEmbedded": "Firebase (FOSS)",
"globalOptionsPushNotificationsDistributorNtfy": "ntfy",
"globalOptionsPushNotificationsDistributorFCMUP": "FCM-UP",
"globalOptionsPushNotificationsDistributorNextPush": "NextPush",
"globalOptionsPushNotificationsDistributorNoProvider2Push": "NoProvider2Push",
"globalOptionsPushNotificationsDistributorGotifyUP": "Gotify-UP (FOSS)",
"globalOptionsPushNotificationsDistributorFirebaseEmbedded": "Firebase (proprietary)",
"globalOptionsPushNotificationsDistributorNtfy": "ntfy (FOSS)",
"globalOptionsPushNotificationsDistributorFCMUP": "FCM-UP (proprietary)",
"globalOptionsPushNotificationsDistributorNextPush": "NextPush (FOSS)",
"globalOptionsPushNotificationsDistributorNoProvider2Push": "NoProvider2Push (FOSS)",
"globalOptionsPushNotificationsDistributorConversations": "Conversations",
"globalOptionsStartupMinimized": "Start minimized",
"globalOptionsStartupMinimizeInsteadOfExit": "Minimize instead of exit",
"globalOptionsSystemTrayEnabled": "Enable system tray",

26
packages/neon/lib/l10n/localizations.dart

@ -407,12 +407,6 @@ abstract class AppLocalizations {
/// **'Keep the original accent color'**
String get globalOptionsThemeKeepOriginalAccentColor;
/// No description provided for @globalOptionsPushNotificationsNotice.
///
/// In en, this message translates to:
/// **'External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.'**
String get globalOptionsPushNotificationsNotice;
/// No description provided for @globalOptionsPushNotificationsEnabled.
///
/// In en, this message translates to:
@ -422,7 +416,7 @@ abstract class AppLocalizations {
/// No description provided for @globalOptionsPushNotificationsEnabledDisabledNotice.
///
/// In en, this message translates to:
/// **'No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications'**
/// **'No UnifiedPush distributor could be found or you denied the permission for showing notifications. Please go to the app settings and allow notifications and go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications'**
String get globalOptionsPushNotificationsEnabledDisabledNotice;
/// No description provided for @globalOptionsPushNotificationsDistributor.
@ -434,39 +428,45 @@ abstract class AppLocalizations {
/// No description provided for @globalOptionsPushNotificationsDistributorGotifyUP.
///
/// In en, this message translates to:
/// **'Gotify-UP'**
/// **'Gotify-UP (FOSS)'**
String get globalOptionsPushNotificationsDistributorGotifyUP;
/// No description provided for @globalOptionsPushNotificationsDistributorFirebaseEmbedded.
///
/// In en, this message translates to:
/// **'Firebase (FOSS)'**
/// **'Firebase (proprietary)'**
String get globalOptionsPushNotificationsDistributorFirebaseEmbedded;
/// No description provided for @globalOptionsPushNotificationsDistributorNtfy.
///
/// In en, this message translates to:
/// **'ntfy'**
/// **'ntfy (FOSS)'**
String get globalOptionsPushNotificationsDistributorNtfy;
/// No description provided for @globalOptionsPushNotificationsDistributorFCMUP.
///
/// In en, this message translates to:
/// **'FCM-UP'**
/// **'FCM-UP (proprietary)'**
String get globalOptionsPushNotificationsDistributorFCMUP;
/// No description provided for @globalOptionsPushNotificationsDistributorNextPush.
///
/// In en, this message translates to:
/// **'NextPush'**
/// **'NextPush (FOSS)'**
String get globalOptionsPushNotificationsDistributorNextPush;
/// No description provided for @globalOptionsPushNotificationsDistributorNoProvider2Push.
///
/// In en, this message translates to:
/// **'NoProvider2Push'**
/// **'NoProvider2Push (FOSS)'**
String get globalOptionsPushNotificationsDistributorNoProvider2Push;
/// No description provided for @globalOptionsPushNotificationsDistributorConversations.
///
/// In en, this message translates to:
/// **'Conversations'**
String get globalOptionsPushNotificationsDistributorConversations;
/// No description provided for @globalOptionsStartupMinimized.
///
/// In en, this message translates to:

21
packages/neon/lib/l10n/localizations_en.dart

@ -174,37 +174,36 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get globalOptionsThemeKeepOriginalAccentColor => 'Keep the original accent color';
@override
String get globalOptionsPushNotificationsNotice =>
'External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.';
@override
String get globalOptionsPushNotificationsEnabled => 'Enabled';
@override
String get globalOptionsPushNotificationsEnabledDisabledNotice =>
'No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications';
'No UnifiedPush distributor could be found or you denied the permission for showing notifications. Please go to the app settings and allow notifications and go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications';
@override
String get globalOptionsPushNotificationsDistributor => 'UnifiedPush Distributor';
@override
String get globalOptionsPushNotificationsDistributorGotifyUP => 'Gotify-UP';
String get globalOptionsPushNotificationsDistributorGotifyUP => 'Gotify-UP (FOSS)';
@override
String get globalOptionsPushNotificationsDistributorFirebaseEmbedded => 'Firebase (proprietary)';
@override
String get globalOptionsPushNotificationsDistributorFirebaseEmbedded => 'Firebase (FOSS)';
String get globalOptionsPushNotificationsDistributorNtfy => 'ntfy (FOSS)';
@override
String get globalOptionsPushNotificationsDistributorNtfy => 'ntfy';
String get globalOptionsPushNotificationsDistributorFCMUP => 'FCM-UP (proprietary)';
@override
String get globalOptionsPushNotificationsDistributorFCMUP => 'FCM-UP';
String get globalOptionsPushNotificationsDistributorNextPush => 'NextPush (FOSS)';
@override
String get globalOptionsPushNotificationsDistributorNextPush => 'NextPush';
String get globalOptionsPushNotificationsDistributorNoProvider2Push => 'NoProvider2Push (FOSS)';
@override
String get globalOptionsPushNotificationsDistributorNoProvider2Push => 'NoProvider2Push';
String get globalOptionsPushNotificationsDistributorConversations => 'Conversations';
@override
String get globalOptionsStartupMinimized => 'Start minimized';

1
packages/neon/lib/main.dart

@ -52,7 +52,6 @@ Future main() async {
accountsBloc,
sharedPreferences,
globalOptions,
env,
platform,
);

6
packages/neon/lib/src/app.dart

@ -172,7 +172,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
final allAppImplementations = Provider.of<List<AppImplementation>>(context, listen: false);
final matchingAppImplementations =
allAppImplementations.where((final a) => a.id == pushNotificationWithAccountID.notification.subject.app);
allAppImplementations.where((final a) => a.id == pushNotificationWithAccountID.subject.app);
late AppImplementation appImplementation;
if (matchingAppImplementations.isNotEmpty) {
@ -191,7 +191,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
_accountsBloc
.getAppsBloc(account)
.getAppBloc<NotificationsBloc>(appImplementation)
.deleteNotification(pushNotificationWithAccountID.notification.subject.nid!);
.deleteNotification(pushNotificationWithAccountID.subject.nid!);
}
await _openAppFromExternal(account, appImplementation.id);
};
@ -199,7 +199,7 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver, tray.Tra
final details = await localNotificationsPlugin.getNotificationAppLaunchDetails();
if (details != null && details.didNotificationLaunchApp && details.notificationResponse?.payload != null) {
await Global.onPushNotificationClicked!(
PushNotificationWithAccountID.fromJson(
PushNotification.fromJson(
json.decode(details.notificationResponse!.payload!) as Map<String, dynamic>,
),
);

57
packages/neon/lib/src/blocs/push_notifications.dart

@ -3,7 +3,7 @@ part of '../neon.dart';
abstract class PushNotificationsBlocEvents {}
abstract class PushNotificationsBlocStates {
Stream<NextcloudPushNotification> get notifications;
Stream<PushNotification> get notifications;
}
class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents, PushNotificationsBlocStates {
@ -11,7 +11,6 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
this._accountsBloc,
this._sharedPreferences,
this._globalOptions,
this._env,
this._platform,
) {
if (_platform.canUsePushNotifications) {
@ -35,11 +34,10 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
final SharedPreferences _sharedPreferences;
late final _storage = AppStorage('notifications', _sharedPreferences);
final GlobalOptions _globalOptions;
final Env? _env;
late RSAKeypair _keypair;
bool? _pushNotificationsEnabled;
final _notificationsController = StreamController<NextcloudPushNotification>();
final _notificationsController = StreamController<PushNotification>();
@override
void dispose() {
@ -47,20 +45,14 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
}
@override
late Stream<NextcloudPushNotification> notifications = _notificationsController.stream.asBroadcastStream();
late Stream<PushNotification> notifications = _notificationsController.stream.asBroadcastStream();
String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}';
Future _setupUnifiedPush() async {
await UnifiedPush.initialize(
onNewEndpoint: (final endpoint, final instance) async {
Account? account;
for (final a in _accountsBloc.accounts.value) {
if (a.id == instance) {
account = a;
break;
}
}
final account = _accountsBloc.accounts.value.find(instance);
if (account == null) {
debugPrint('Account for $instance not found, can not process endpoint');
return;
@ -73,28 +65,17 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
debugPrint('Registering account $instance for push notifications on $endpoint');
var proxyServerForNextcloud = 'https://nc.proxy.neon.provokateurin.de/';
var proxyServerForClient = proxyServerForNextcloud;
if (_env != null) {
proxyServerForNextcloud = 'http://host.docker.internal:8080/';
proxyServerForClient = 'http://${_env!.testHost}:8080/';
}
final subscription = await account.client.notifications.registerDevice(
pushTokenHash: account.client.notifications.generatePushTokenHash(endpoint),
pushTokenHash: generatePushTokenHash(endpoint),
devicePublicKey: _keypair.publicKey.toFormattedPEM(),
proxyServer: proxyServerForNextcloud,
);
await account.client.notifications.registerDeviceAtPushProxy(
endpoint,
subscription.ocs.data,
proxyServerForClient,
proxyServer: '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint
);
await _storage.setString(_keyLastEndpoint(account), endpoint);
debugPrint('Account $instance registered for push notifications');
debugPrint(
'Account $instance registered for push notifications ${json.encode(subscription.ocs.data.toJson())}',
);
},
onMessage: PushUtils.onMessage,
);
@ -120,7 +101,13 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
Future _unregisterUnifiedPushInstances(final List<Account> accounts) async {
for (final account in accounts) {
try {
await account.client.notifications.removeDevice();
await UnifiedPush.unregister(account.client.id);
await _storage.remove(_keyLastEndpoint(account));
} catch (e) {
debugPrint('Failed to unregister device: $e');
}
}
}
@ -131,17 +118,3 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
}
}
}
class NextcloudPushNotification {
NextcloudPushNotification({
required this.instance,
required this.priority,
required this.type,
required this.subject,
});
final String instance;
final String priority;
final String type;
final NextcloudNotificationsPushNotificationDecryptedSubject subject;
}

25
packages/neon/lib/src/models/push_notification.dart

@ -0,0 +1,25 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:nextcloud/nextcloud.dart';
part 'push_notification.g.dart';
@JsonSerializable()
class PushNotification {
PushNotification({
required this.accountID,
required this.priority,
required this.type,
required this.subject,
});
factory PushNotification.fromJson(final Map<String, dynamic> json) => _$PushNotificationFromJson(json);
Map<String, dynamic> toJson() => _$PushNotificationToJson(this);
final String accountID;
final String priority;
final String type;
final NextcloudNotificationsNotificationDecryptedSubject subject;
}

21
packages/neon/lib/src/models/push_notification.g.dart

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'push_notification.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PushNotification _$PushNotificationFromJson(Map<String, dynamic> json) => PushNotification(
accountID: json['accountID'] as String,
priority: json['priority'] as String,
type: json['type'] as String,
subject: NextcloudNotificationsNotificationDecryptedSubject.fromJson(json['subject'] as Map<String, dynamic>),
);
Map<String, dynamic> _$PushNotificationToJson(PushNotification instance) => <String, dynamic>{
'accountID': instance.accountID,
'priority': instance.priority,
'type': instance.type,
'subject': instance.subject,
};

20
packages/neon/lib/src/models/push_notification_with_account.dart

@ -1,20 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:nextcloud/nextcloud.dart';
part 'push_notification_with_account.g.dart';
@JsonSerializable()
class PushNotificationWithAccountID {
PushNotificationWithAccountID({
required this.notification,
required this.accountID,
});
factory PushNotificationWithAccountID.fromJson(final Map<String, dynamic> json) =>
_$PushNotificationWithAccountIDFromJson(json);
Map<String, dynamic> toJson() => _$PushNotificationWithAccountIDToJson(this);
final NextcloudNotificationsPushNotification notification;
final String accountID;
}

18
packages/neon/lib/src/models/push_notification_with_account.g.dart

@ -1,18 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'push_notification_with_account.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PushNotificationWithAccountID _$PushNotificationWithAccountIDFromJson(Map<String, dynamic> json) =>
PushNotificationWithAccountID(
notification: NextcloudNotificationsPushNotification.fromJson(json['notification'] as Map<String, dynamic>),
accountID: json['accountID'] as String,
);
Map<String, dynamic> _$PushNotificationWithAccountIDToJson(PushNotificationWithAccountID instance) => <String, dynamic>{
'notification': instance.notification,
'accountID': instance.accountID,
};

2
packages/neon/lib/src/neon.dart

@ -25,7 +25,7 @@ import 'package:neon/src/apps/news/app.dart';
import 'package:neon/src/apps/notes/app.dart';
import 'package:neon/src/apps/notifications/app.dart';
import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/push_notification_with_account.dart';
import 'package:neon/src/models/push_notification.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;

7
packages/neon/lib/src/pages/settings.dart

@ -119,13 +119,6 @@ class _SettingsPageState extends State<SettingsPage> {
SettingsCategory(
title: Text(AppLocalizations.of(context).optionsCategoryPushNotifications),
tiles: [
TextSettingsTile(
text: AppLocalizations.of(context).globalOptionsPushNotificationsNotice,
style: const TextStyle(
fontWeight: FontWeight.w300,
fontStyle: FontStyle.italic,
),
),
if (pushNotificationsEnabledEnabledSnapshot.data != null &&
!pushNotificationsEnabledEnabledSnapshot.data!) ...[
TextSettingsTile(

2
packages/neon/lib/src/utils/global.dart

@ -2,5 +2,5 @@ part of '../neon.dart';
class Global {
static Function(String accountID)? onPushNotificationReceived;
static Function(PushNotificationWithAccountID notification)? onPushNotificationClicked;
static Function(PushNotification notification)? onPushNotificationClicked;
}

33
packages/neon/lib/src/utils/global_options.dart

@ -10,15 +10,25 @@ class GlobalOptions {
});
_pushNotificationsDistributorsSubject.listen((final distributors) async {
_pushNotificationsEnabledEnabledSubject.add(distributors.isNotEmpty);
await _setDefaultDistributor();
final allowed = distributors.isNotEmpty;
_pushNotificationsEnabledEnabledSubject.add(allowed);
if (!allowed) {
await pushNotificationsEnabled.set(false);
}
});
pushNotificationsEnabled.stream.listen((final enabled) async {
if (!enabled) {
if (enabled) {
final response = await Permission.notification.request();
if (response.isPermanentlyDenied) {
_pushNotificationsEnabledEnabledSubject.add(false);
}
if (!response.isGranted) {
await pushNotificationsEnabled.set(false);
}
} else {
await pushNotificationsDistributor.set(null);
}
await _setDefaultDistributor();
});
rememberLastUsedAccount.stream.listen((final remember) async {
@ -48,6 +58,8 @@ class GlobalOptions {
AppLocalizations.of(context).globalOptionsPushNotificationsDistributorFirebaseEmbedded,
'com.github.gotify.up': (final context) =>
AppLocalizations.of(context).globalOptionsPushNotificationsDistributorGotifyUP,
'eu.siacs.conversations': (final context) =>
AppLocalizations.of(context).globalOptionsPushNotificationsDistributorConversations,
'io.heckel.ntfy': (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsDistributorNtfy,
'org.unifiedpush.distributor.fcm': (final context) =>
AppLocalizations.of(context).globalOptionsPushNotificationsDistributorFCMUP,
@ -105,17 +117,6 @@ class GlobalOptions {
});
}
Future _setDefaultDistributor() async {
if ((pushNotificationsEnabled.enabled.valueOrNull ?? false) &&
pushNotificationsEnabled.value &&
pushNotificationsDistributor.values.hasValue &&
pushNotificationsDistributor.values.value.isNotEmpty &&
pushNotificationsDistributor.stream.hasValue &&
pushNotificationsDistributor.value == null) {
await pushNotificationsDistributor.set((await pushNotificationsDistributor.values.first).keys.toList()[0]);
}
}
late final themeMode = SelectOption<ThemeMode>(
storage: _storage,
key: 'theme-mode',
@ -147,7 +148,7 @@ class GlobalOptions {
storage: _storage,
key: 'push-notifications-enabled',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled,
defaultValue: BehaviorSubject.seeded(true),
defaultValue: BehaviorSubject.seeded(false),
enabled: _pushNotificationsEnabledEnabledSubject,
);

23
packages/neon/lib/src/utils/push_utils.dart

@ -27,7 +27,7 @@ class PushUtils {
android: const AndroidInitializationSettings('@mipmap/ic_launcher'),
linux: LinuxInitializationSettings(
defaultActionName: 'Open',
defaultIcon: AssetsLinuxIcon('assets/logo_harbour.svg'),
defaultIcon: AssetsLinuxIcon('assets/logo_neon.svg'),
),
),
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
@ -35,14 +35,14 @@ class PushUtils {
return localNotificationsPlugin;
}
static Future onMessage(final Uint8List message, final String instance) async {
static Future onMessage(final Uint8List messages, final String instance) async {
WidgetsFlutterBinding.ensureInitialized();
final localNotificationsPlugin = await initLocalNotifications(
onDidReceiveNotificationResponse: (final notification) async {
if (Global.onPushNotificationClicked != null && notification.payload != null) {
await Global.onPushNotificationClicked!(
PushNotificationWithAccountID.fromJson(
PushNotification.fromJson(
json.decode(notification.payload!) as Map<String, dynamic>,
),
);
@ -52,8 +52,10 @@ class PushUtils {
final sharedPreferences = await SharedPreferences.getInstance();
final keypair = await loadRSAKeypair(AppStorage('notifications', sharedPreferences));
final data = json.decode(utf8.decode(message)) as Map<String, dynamic>;
final notification = NextcloudNotificationsPushNotification(
for (final message in Uri(query: utf8.decode(messages)).queryParameters.values) {
final data = json.decode(message) as Map<String, dynamic>;
final notification = PushNotification(
accountID: instance,
priority: data['priority']! as String,
type: data['type']! as String,
@ -69,7 +71,7 @@ class PushUtils {
return;
}
if (notification.type == 'background') {
debugPrint('Got unknown background notification $notification.subject');
debugPrint('Got unknown background notification ${json.encode(notification.toJson())}');
return;
}
@ -115,19 +117,22 @@ class PushUtils {
),
),
payload: json.encode(
PushNotificationWithAccountID(
notification: notification,
PushNotification(
accountID: instance,
priority: notification.priority,
type: notification.type,
subject: notification.subject,
).toJson(),
),
);
Global.onPushNotificationReceived?.call(instance);
}
}
static int _getNotificationID(
final String instance,
final NextcloudNotificationsPushNotification notification,
final PushNotification notification,
) =>
sha256.convert(utf8.encode('$instance${notification.subject.nid}')).bytes.reduce((final a, final b) => a + b);
}

Loading…
Cancel
Save