Skip to content

Commit

Permalink
fix(neon_framework): Migrate to notifications_push_repository
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <[email protected]>
  • Loading branch information
provokateurin committed Sep 12, 2024
1 parent 0f0a799 commit d3ef216
Show file tree
Hide file tree
Showing 27 changed files with 143 additions and 533 deletions.
7 changes: 7 additions & 0 deletions packages/neon_framework/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,13 @@ packages:
relative: true
source: path
version: "1.0.0"
notifications_push_repository:
dependency: "direct overridden"
description:
path: "../packages/notifications_push_repository"
relative: true
source: path
version: "0.1.0"
open_filex:
dependency: transitive
description:
Expand Down
4 changes: 3 additions & 1 deletion packages/neon_framework/example/pubspec_overrides.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# melos_managed_dependency_overrides: cookie_store,dashboard_app,dynamite_runtime,files_app,interceptor_http_client,neon_framework,neon_http_client,neon_lints,news_app,nextcloud,notes_app,notifications_app,sort_box,talk_app
# melos_managed_dependency_overrides: cookie_store,dashboard_app,dynamite_runtime,files_app,interceptor_http_client,neon_framework,neon_http_client,neon_lints,news_app,nextcloud,notes_app,notifications_app,notifications_push_repository,sort_box,talk_app
dependency_overrides:
cookie_store:
path: ../../cookie_store
Expand All @@ -24,6 +24,8 @@ dependency_overrides:
path: ../packages/notes_app
notifications_app:
path: ../packages/notifications_app
notifications_push_repository:
path: ../packages/notifications_push_repository
sort_box:
path: ../packages/sort_box
talk_app:
Expand Down
5 changes: 2 additions & 3 deletions packages/neon_framework/lib/l10n/en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,8 @@
"globalOptionsThemeOLEDAsDark": "OLED theme as dark theme",
"globalOptionsThemeUseNextcloudTheme": "Use Nextcloud theme",
"globalOptionsThemeCustomBackground": "Custom background",
"globalOptionsPushNotificationsEnabled": "Enabled",
"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",
"globalOptionsPushNotifications": "UnifiedPush",
"globalOptionsPushNotificationsDisabled": "Disabled",
"globalOptionsPushNotificationsDistributorGotifyUP": "Gotify-UP (FOSS)",
"globalOptionsPushNotificationsDistributorFirebaseEmbedded": "Firebase (proprietary)",
"globalOptionsPushNotificationsDistributorNtfy": "ntfy (FOSS)",
Expand Down
18 changes: 6 additions & 12 deletions packages/neon_framework/lib/l10n/localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -559,23 +559,17 @@ abstract class NeonLocalizations {
/// **'Custom background'**
String get globalOptionsThemeCustomBackground;

/// No description provided for @globalOptionsPushNotificationsEnabled.
/// No description provided for @globalOptionsPushNotifications.
///
/// In en, this message translates to:
/// **'Enabled'**
String get globalOptionsPushNotificationsEnabled;
/// **'UnifiedPush'**
String get globalOptionsPushNotifications;

/// No description provided for @globalOptionsPushNotificationsEnabledDisabledNotice.
/// No description provided for @globalOptionsPushNotificationsDisabled.
///
/// In en, this message translates to:
/// **'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.
///
/// In en, this message translates to:
/// **'UnifiedPush Distributor'**
String get globalOptionsPushNotificationsDistributor;
/// **'Disabled'**
String get globalOptionsPushNotificationsDisabled;

/// No description provided for @globalOptionsPushNotificationsDistributorGotifyUP.
///
Expand Down
8 changes: 2 additions & 6 deletions packages/neon_framework/lib/l10n/localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,10 @@ class NeonLocalizationsEn extends NeonLocalizations {
String get globalOptionsThemeCustomBackground => 'Custom background';

@override
String get globalOptionsPushNotificationsEnabled => 'Enabled';
String get globalOptionsPushNotifications => 'UnifiedPush';

@override
String get 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';

@override
String get globalOptionsPushNotificationsDistributor => 'UnifiedPush Distributor';
String get globalOptionsPushNotificationsDisabled => 'Disabled';

@override
String get globalOptionsPushNotificationsDistributorGotifyUP => 'Gotify-UP (FOSS)';
Expand Down
35 changes: 30 additions & 5 deletions packages/neon_framework/lib/neon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:logging/logging.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/app.dart';
import 'package:neon_framework/src/blocs/accounts.dart';
import 'package:neon_framework/src/blocs/first_launch.dart';
import 'package:neon_framework/src/blocs/next_push.dart';
import 'package:neon_framework/src/blocs/push_notifications.dart';
import 'package:neon_framework/src/models/app_implementation.dart';
import 'package:neon_framework/src/models/disposable.dart';
import 'package:neon_framework/src/platform/platform.dart';
import 'package:neon_framework/src/storage/keys.dart';
import 'package:neon_framework/src/storage/sqlite_persistence.dart';
import 'package:neon_framework/src/theme/neon.dart';
import 'package:neon_framework/src/utils/global_options.dart';
import 'package:neon_framework/src/utils/provider.dart';
import 'package:neon_framework/src/utils/push_utils.dart';
import 'package:neon_framework/src/utils/timezone.dart';
import 'package:neon_framework/src/utils/user_agent.dart';
import 'package:neon_framework/storage.dart';
import 'package:notifications_push_repository/notifications_push_repository.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:timezone/timezone.dart' as tz;

Expand Down Expand Up @@ -53,19 +58,39 @@ Future<void> runNeon({
final packageInfo = await PackageInfo.fromPlatform();
buildUserAgent(packageInfo);

final notificationsPushStorage = NotificationsPushStorage(
devicePrivateKeyPersistence: NeonStorage().singleValueStore(StorageKeys.notificationsDevicePrivateKey),
pushSubscriptionsPersistence: SQLiteCachedPersistence(
prefix: 'notifications-push-subscriptions',
),
);

// TODO: THIS DOES NOT WORK YET DUE TO A CYCLIC DEPENDENCY, THE ACCOUNT REPOSITORY IS NEEDED
final accountsSubject = BehaviorSubject<BuiltList<Account>>.seeded(BuiltList());

final notificationsPushRepository = NotificationsPushRepository(
accountsSubject: accountsSubject,
storage: notificationsPushStorage,
onMessage: PushUtils.onMessage,
);

final distributors = await notificationsPushRepository.distributors;
final globalOptions = GlobalOptions(
packageInfo,
distributors,
);

final accountsBloc = AccountsBloc(
PushNotificationsBloc(
globalOptions: globalOptions,
allAppImplementations: appImplementations,
notificationsPushRepository: notificationsPushRepository,
);

PushNotificationsBloc(
accountsSubject: accountsBloc.accounts,
final accountsBloc = AccountsBloc(
globalOptions: globalOptions,
allAppImplementations: appImplementations,
);
unawaited(accountsBloc.accounts.pipe(accountsSubject));

final firstLaunchBloc = FirstLaunchBloc();
final nextPushBloc = NextPushBloc(
accountsSubject: accountsBloc.accounts,
Expand Down
2 changes: 1 addition & 1 deletion packages/neon_framework/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import 'package:neon_framework/src/blocs/unified_search.dart';
import 'package:neon_framework/src/models/account.dart';
import 'package:neon_framework/src/models/app_implementation.dart';
import 'package:neon_framework/src/models/notifications_interface.dart';
import 'package:neon_framework/src/models/push_notification.dart';
import 'package:neon_framework/src/platform/platform.dart';
import 'package:neon_framework/src/router.dart';
import 'package:neon_framework/src/theme/neon.dart';
Expand All @@ -28,6 +27,7 @@ import 'package:neon_framework/src/utils/provider.dart';
import 'package:neon_framework/src/utils/push_utils.dart';
import 'package:neon_framework/src/widgets/options_collection_builder.dart';
import 'package:nextcloud/notifications.dart' as notifications;
import 'package:notifications_push_repository/notifications_push_repository.dart';
import 'package:provider/provider.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:universal_io/io.dart';
Expand Down
11 changes: 4 additions & 7 deletions packages/neon_framework/lib/src/blocs/next_push.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,12 @@ class _NextPushBloc extends Bloc implements NextPushBloc {
if (disabled) {
return;
}
changesSubscription = Rx.merge([
globalOptions.pushNotificationsEnabled.stream,
changesSubscription = Rx.merge<dynamic>([
globalOptions.pushNotificationsDistributor.stream,
accountsSubject,
]).debounceTime(const Duration(milliseconds: 100)).listen((_) async {
if (!globalOptions.pushNotificationsEnabled.enabled || !globalOptions.pushNotificationsEnabled.value) {
return;
}
if (globalOptions.pushNotificationsDistributor.value != null) {
]).listen((_) async {
final distributor = await globalOptions.pushNotificationsDistributor.stream.first;
if (distributor != null) {
return;
}
if (globalOptions.pushNotificationsDistributor.values.containsKey(unifiedPushNextPushID)) {
Expand Down
127 changes: 21 additions & 106 deletions packages/neon_framework/lib/src/blocs/push_notifications.dart
Original file line number Diff line number Diff line change
@@ -1,145 +1,60 @@
import 'dart:async';
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/bloc/bloc.dart';
import 'package:neon_framework/src/platform/platform.dart';
import 'package:neon_framework/src/storage/keys.dart';
import 'package:neon_framework/src/utils/findable.dart';
import 'package:neon_framework/src/utils/global_options.dart';
import 'package:neon_framework/src/utils/push_utils.dart';
import 'package:neon_framework/storage.dart';
import 'package:nextcloud/notifications.dart' as notifications;
import 'package:rxdart/rxdart.dart';
import 'package:unifiedpush/unifiedpush.dart';
import 'package:notifications_push_repository/notifications_push_repository.dart';
import 'package:permission_handler/permission_handler.dart';

/// Bloc for managing push notifications and registration.
/// Bloc for managing push subscriptions.
sealed class PushNotificationsBloc {
@internal
factory PushNotificationsBloc({
required BehaviorSubject<BuiltList<Account>> accountsSubject,
required GlobalOptions globalOptions,
required NotificationsPushRepository notificationsPushRepository,
}) = _PushNotificationsBloc;
}

class _PushNotificationsBloc extends Bloc implements PushNotificationsBloc {
_PushNotificationsBloc({
required this.accountsSubject,
required this.globalOptions,
required this.notificationsPushRepository,
}) {
if (NeonPlatform.instance.canUsePushNotifications) {
unawaited(UnifiedPush.getDistributors().then(globalOptions.updateDistributors));

globalOptions.pushNotificationsEnabled.addListener(pushNotificationsEnabledListener);
// Call the listener to update everything
unawaited(pushNotificationsEnabledListener());
unawaited(changeDistributor());
globalOptions.pushNotificationsDistributor.addListener(changeDistributor);
}
}

@override
final log = Logger('PushNotificationsBloc');

final BehaviorSubject<BuiltList<Account>> accountsSubject;
late final storage = NeonStorage().settingsStore(StorageKeys.lastEndpoint);
final GlobalOptions globalOptions;

StreamSubscription<BuiltList<Account>>? accountsListener;
final NotificationsPushRepository notificationsPushRepository;
String? oldDistributor;

@override
void dispose() {
unawaited(accountsListener?.cancel());
globalOptions.pushNotificationsEnabled.removeListener(pushNotificationsEnabledListener);
globalOptions.pushNotificationsDistributor.removeListener(changeDistributor);
}

Future<void> pushNotificationsEnabledListener() async {
if (globalOptions.pushNotificationsEnabled.value) {
await setupUnifiedPush();

globalOptions.pushNotificationsDistributor.addListener(distributorListener);
accountsListener = accountsSubject.listen(registerUnifiedPushInstances);
} else {
globalOptions.pushNotificationsDistributor.removeListener(distributorListener);
unawaited(accountsListener?.cancel());
Future<void> changeDistributor() async {
final newDistributor = globalOptions.pushNotificationsDistributor.value;
if (newDistributor == oldDistributor) {
return;
}
}

Future<void> setupUnifiedPush() async {
// We just use a single RSA keypair for all accounts
final keypair = PushUtils.loadRSAKeypair();

await UnifiedPush.initialize(
onNewEndpoint: (endpoint, instance) async {
final account = accountsSubject.value.tryFind(instance);
if (account == null) {
log.fine('Account for $instance not found, can not process endpoint');
return;
}

if (storage.getString(account.id) == endpoint) {
log.fine('Endpoint not changed');
return;
}

log.fine('Registering account $instance for push notifications on $endpoint');

final subscription = await account.client.notifications.push.registerDevice(
$body: notifications.PushRegisterDeviceRequestApplicationJson(
(b) => b
..pushTokenHash = notifications.generatePushTokenHash(endpoint)
..devicePublicKey = keypair.publicKey.toFormattedPEM()
..proxyServer = '$endpoint#', // This is a hack to make the Nextcloud server directly push to the endpoint
),
);
oldDistributor = newDistributor;

await storage.setString(account.id, endpoint);
final response = await Permission.notification.request();
if (!response.isGranted) {
log.fine('Notifications permission denied, disabling push notifications');

log.fine(
'Account $instance registered for push notifications ${json.encode(subscription.body.ocs.data.toJson())}',
);
},
onMessage: PushUtils.onMessage,
);
}

Future<void> distributorListener() async {
final distributor = globalOptions.pushNotificationsDistributor.value;
final disabled = distributor == null;
final sameDistributor = distributor == await UnifiedPush.getDistributor();
final accounts = accountsSubject.value;
if (disabled || !sameDistributor) {
await unregisterUnifiedPushInstances(accounts);
}
if (!disabled && !sameDistributor) {
log.fine('UnifiedPush distributor changed to $distributor');
await UnifiedPush.saveDistributor(distributor);
globalOptions.pushNotificationsDistributor.reset();
return;
}
if (!disabled) {
await registerUnifiedPushInstances(accounts);
}
}

Future<void> unregisterUnifiedPushInstances(BuiltList<Account> accounts) async {
for (final account in accounts) {
try {
await account.client.notifications.push.removeDevice();
await UnifiedPush.unregister(account.id);
await storage.remove(account.id);
} on Exception catch (error) {
log.warning(
'Failed to unregister device.',
error,
);
}
}
}

Future<void> registerUnifiedPushInstances(BuiltList<Account> accounts) async {
// Notifications will only work on accounts with app password
for (final account in accounts.where((a) => a.password != null)) {
await UnifiedPush.registerApp(account.id);
}
await notificationsPushRepository.changeDistributor(newDistributor);
}
}
Loading

0 comments on commit d3ef216

Please sign in to comment.