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 29, 2024
1 parent 8b905a0 commit ffb6bbf
Show file tree
Hide file tree
Showing 26 changed files with 149 additions and 538 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 @@ -917,6 +917,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: account_repository,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: account_repository,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:
account_repository:
path: ../packages/account_repository
Expand Down Expand Up @@ -26,6 +26,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 @@ -162,9 +162,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
46 changes: 37 additions & 9 deletions packages/neon_framework/lib/neon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:http/http.dart' as http;
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:timezone/data/latest.dart' as tzdata;
Expand Down Expand Up @@ -57,10 +60,6 @@ Future<void> runNeon({

final packageInfo = await PackageInfo.fromPlatform();

final globalOptions = GlobalOptions(
packageInfo,
);

final accountStorage = AccountStorage(
accountsPersistence: NeonStorage().singleValueStore(StorageKeys.accounts),
lastAccountPersistence: NeonStorage().singleValueStore(StorageKeys.lastUsedAccount),
Expand All @@ -72,20 +71,49 @@ Future<void> runNeon({
storage: accountStorage,
);

NotificationsPushRepository? notificationsPushRepository;
var distributors = BuiltList<String>();
if (NeonPlatform.instance.canUsePushNotifications) {
final notificationsPushStorage = NotificationsPushStorage(
devicePrivateKeyPersistence: NeonStorage().singleValueStore(StorageKeys.notificationsDevicePrivateKey),
pushSubscriptionsPersistence: SQLiteCachedPersistence(
prefix: 'notifications-push-subscriptions',
),
);

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

distributors = await notificationsPushRepository.distributors;
}

final globalOptions = GlobalOptions(
packageInfo,
distributors,
);

await accountRepository.loadAccounts(
initialAccount: globalOptions.initialAccount.value,
rememberLastUsedAccount: globalOptions.rememberLastUsedAccount.value,
);

if (notificationsPushRepository != null) {
await notificationsPushRepository.initialize();

PushNotificationsBloc(
globalOptions: globalOptions,
notificationsPushRepository: notificationsPushRepository,
);
}

final accountsBloc = AccountsBloc(
allAppImplementations: appImplementations,
accountRepository: accountRepository,
);

PushNotificationsBloc(
globalOptions: globalOptions,
accountRepository: accountRepository,
);
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 @@ -13,7 +13,6 @@ import 'package:neon_framework/models.dart';
import 'package:neon_framework/src/blocs/accounts.dart';
import 'package:neon_framework/src/blocs/maintenance_mode.dart';
import 'package:neon_framework/src/blocs/unified_search.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 @@ -27,6 +26,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
10 changes: 3 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,11 @@ 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 {
if (globalOptions.pushNotificationsDistributor.value == null) {
return;
}
if (globalOptions.pushNotificationsDistributor.values.containsKey(unifiedPushNextPushID)) {
Expand Down
126 changes: 19 additions & 107 deletions packages/neon_framework/lib/src/blocs/push_notifications.dart
Original file line number Diff line number Diff line change
@@ -1,143 +1,55 @@
import 'dart:async';
import 'dart:convert';

import 'package:account_repository/account_repository.dart';
import 'package:built_collection/built_collection.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.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/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: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 GlobalOptions globalOptions,
required AccountRepository accountRepository,
required NotificationsPushRepository notificationsPushRepository,
}) = _PushNotificationsBloc;
}

class _PushNotificationsBloc extends Bloc implements PushNotificationsBloc {
_PushNotificationsBloc({
required this.globalOptions,
required this.accountRepository,
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');

late final storage = NeonStorage().settingsStore(StorageKeys.lastEndpoint);
final GlobalOptions globalOptions;
final AccountRepository accountRepository;

StreamSubscription<({Account? active, BuiltList<Account> accounts})>? accountsListener;
final NotificationsPushRepository notificationsPushRepository;
String? oldDistributor;

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

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

globalOptions.pushNotificationsDistributor.addListener(distributorListener);
accountsListener = accountRepository.accounts.listen(registerUnifiedPushInstances);
} else {
globalOptions.pushNotificationsDistributor.removeListener(distributorListener);
unawaited(accountsListener?.cancel());
}
}

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 = accountRepository.accountByID(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
),
);

await storage.setString(account.id, endpoint);

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

Future<void> distributorListener() async {
Future<void> changeDistributor() async {
final distributor = globalOptions.pushNotificationsDistributor.value;
final disabled = distributor == null;
final sameDistributor = distributor == await UnifiedPush.getDistributor();
final accounts = await accountRepository.accounts.first;
if (disabled || !sameDistributor) {
await unregisterUnifiedPushInstances(accounts.accounts);
}
if (!disabled && !sameDistributor) {
log.fine('UnifiedPush distributor changed to $distributor');
await UnifiedPush.saveDistributor(distributor);
}
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,
);
if (distributor != null) {
final response = await Permission.notification.request();
if (!response.isGranted) {
log.fine('Notifications permission denied, disabling push notifications');

globalOptions.pushNotificationsDistributor.reset();
return;
}
}
}

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

0 comments on commit ffb6bbf

Please sign in to comment.