diff --git a/README.md b/README.md index 0c18acf..ee55903 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,12 @@ dart scripts/generate.dart > export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" > ``` +Build webcrypto: + +```sh +flutter pub run webcrypto:setup +``` + Build generated files, and watch for new changes: ```sh diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9f9a087..9c0d96c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -20,16 +20,21 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } defaultConfig { applicationId = "nexus.federated.nexus" - minSdk = 29 + + minSdk = 29 targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode versionName = flutter.versionName + + multiDexEnabled = true } signingConfigs { @@ -65,6 +70,7 @@ android { dependencies { implementation("org.unifiedpush.android:embedded-fcm-distributor:3.1.0") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") } kotlin { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c30b367..c73d4ae 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -7,7 +7,8 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - return super.application(application, didFinishLaunchingWithOptions: launchOptions) + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { diff --git a/lib/controllers/notifications.dart b/lib/controllers/notifications.dart new file mode 100644 index 0000000..faf5e8c --- /dev/null +++ b/lib/controllers/notifications.dart @@ -0,0 +1,97 @@ +import "dart:io"; + +import "package:flutter_local_notifications/flutter_local_notifications.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +class NotificationsController + extends AsyncNotifier { + @override + Future build() async { + final notifications = FlutterLocalNotificationsPlugin(); + + final darwin = DarwinInitializationSettings(); + + await notifications.initialize( + settings: .new( + windows: .new( + appName: "Nexus", + appUserModelId: "nexus.federated.nexus", + guid: "dde78daf-130f-4e46-a80a-e31deeab45d7", + ), + android: .new("ic_launcher"), + iOS: darwin, + macOS: darwin, + linux: .new(defaultActionName: "Open"), + ), + onDidReceiveNotificationResponse: (details) { + print(details.data); + }, + ); + + return notifications; + } + + Future requestPermissions() async { + final controller = await future; + + if (Platform.isIOS) { + return await controller + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin + >() + ?.requestPermissions(alert: true, badge: true, sound: true) ?? + true; + } else if (Platform.isMacOS) { + return await controller + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin + >() + ?.requestPermissions(alert: true, badge: true, sound: true) ?? + true; + } else if (Platform.isAndroid) { + return await controller + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.requestNotificationsPermission() ?? + true; + } + + return true; + } + + Future send({ + required int id, + required String title, + String? body, + File? icon, + String? payload, + }) async { + final notificationDetails = NotificationDetails( + android: .new( + "messages", + "Messages", + largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path), + ), + iOS: .new(), + macOS: .new(), + linux: .new(icon: icon == null ? null : FilePathLinuxIcon(icon.path)), + windows: .new(), + ); + + final controller = await future; + await controller.show( + id: id, + title: title, + body: body, + notificationDetails: notificationDetails, + payload: payload, + ); + } + + static final provider = + AsyncNotifierProvider< + NotificationsController, + FlutterLocalNotificationsPlugin + >(NotificationsController.new); +} diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 8d67435..35753fe 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -8,6 +8,7 @@ import "package:m3e_buttons/m3e_buttons.dart"; import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/notifications.dart"; import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/unified_push.dart"; import "package:nexus/controllers/spec_versions.dart"; @@ -132,17 +133,30 @@ class SettingsSectionsController orElse: () => false, ) ? pusherRegistered.maybeWhen( - data: (_) => - (value) => value - ? ref + data: (_) => (value) async { + if (value) { + if (await ref + .watch( + NotificationsController + .provider + .notifier, + ) + .requestPermissions()) { + ref .watch( UnifiedPushController .provider .notifier, ) .register() - .onError(showError) - : /*deregeister*/ null, + .onError(showError); + } else { + // TODO: Handle not granted + } + } else { + // TODO: Deregister + } + }, orElse: () => null, ) : null, diff --git a/lib/controllers/unified_push.dart b/lib/controllers/unified_push.dart index f72e8ec..c4d32ac 100644 --- a/lib/controllers/unified_push.dart +++ b/lib/controllers/unified_push.dart @@ -3,9 +3,12 @@ import "dart:io"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:intl/intl.dart"; +import "package:nexus/controllers/notifications.dart"; +import "package:nexus/controllers/rooms.dart"; import "package:nexus/main.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; +import "package:nexus/models/content/message.dart"; import "package:nexus/models/requests/register_pusher.dart"; import "package:unifiedpush/unifiedpush.dart"; import "package:unifiedpush_storage_shared_preferences/storage.dart"; @@ -38,10 +41,29 @@ class UnifiedPushController extends AsyncNotifier { ); }, onMessage: (message, instance) async { + if (message.decrypted == false) { + // TODO: Handle more gracefully, show option to re-register pusher + throw Exception("Failed to decrypt notification"); + } final event = await client.handlePush( json.decode(String.fromCharCodes(message.content)), ); - print(event); + + final room = ref.read( + RoomsController.provider.select((rooms) => rooms[event.roomId]), + ); + + await ref + .read(NotificationsController.provider.notifier) + .send( + id: event.eventId.hashCode & 0x7fffffff, + title: room?.metadata?.name ?? "New Message", + body: switch (event.content) { + MessageContent(:final body) => body, + // TODO: Handle more types + _ => null, + }, + ); }, onRegistrationFailed: (error, instance) => throw error, onUnregistered: (instance) => ref.invalidateSelf(), diff --git a/lib/main.dart b/lib/main.dart index 2221151..aaed5a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import "package:media_kit/media_kit.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/multi_provider.dart"; +import "package:nexus/controllers/notifications.dart"; import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/shared_prefs.dart"; import "package:nexus/controllers/unified_push.dart"; @@ -146,6 +147,7 @@ class const App(final bool isInBackground, {super.key}) IListConst([ SharedPrefsController.provider, ClientController.provider, + NotificationsController.provider, UnifiedPushController.provider, ]), ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 603ea6a..70d9ba2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -37,6 +38,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) webcrypto_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin"); + webcrypto_plugin_register_with_registrar(webcrypto_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7d8f046..1d53ad7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever_linux url_launcher_linux + webcrypto window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f18d831..8b6bbb6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import app_links import dynamic_color import file_selector_macos +import flutter_local_notifications import media_kit_libs_macos_video import media_kit_video import package_info_plus @@ -15,12 +16,14 @@ import screen_retriever_macos import shared_preferences_foundation import url_launcher_macos import wakelock_plus +import webcrypto import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) @@ -28,5 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 6f957d9..9f951f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,14 +169,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2" - change_case: - dependency: transitive - description: - name: change_case - sha256: e41ef3df58521194ef8d7649928954805aeb08061917cf658322305e61568003 - url: "https://pub.dev" - source: hosted - version: "2.2.0" characters: dependency: transitive description: @@ -519,6 +511,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "1447ba911c60f2ba3f25dae1af151ec187162566b0f57e37771bf0b400f013ad" + url: "https://pub.dev" + source: hosted + version: "22.3.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd" + url: "https://pub.dev" + source: hosted + version: "12.2.0" + flutter_local_notifications_web: + dependency: transitive + description: + name: flutter_local_notifications_web + sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" + url: "https://pub.dev" + source: hosted + version: "3.1.1" flutter_localizations: dependency: "direct main" description: flutter @@ -1007,14 +1039,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.3" - native_toolchain_cmake: - dependency: transitive - description: - name: native_toolchain_cmake - sha256: de90e8952bad0684bdcff4d674c53d961f5cd1fdcf3a0255a669a35986c4ac1d - url: "https://pub.dev" - source: hosted - version: "0.2.7" navigation_rail_m3e: dependency: "direct main" description: @@ -1517,6 +1541,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" + url: "https://pub.dev" + source: hosted + version: "0.11.1" typed_data: dependency: transitive description: @@ -1750,13 +1782,13 @@ packages: source: hosted version: "3.0.3" webcrypto: - dependency: "direct overridden" + dependency: "direct dev" description: name: webcrypto - sha256: "9885ed99a846191542dd6ca820c83a098117df56be57453832233a3fef68f3f2" + sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f9a9d0..79a3378 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,8 +19,6 @@ environment: sdk: 3.13.0 dependency_overrides: - webcrypto: 0.6.1 - hooks: 2.2.0 dynamic_color: 2.1.0 path_provider_android: 2.2.23 # Pinned to avoid JNI linkify: @@ -83,6 +81,7 @@ dependencies: material_ui: 1.1.0 unifiedpush: 6.2.0 unifiedpush_storage_shared_preferences: 1.0.0 + flutter_local_notifications: ^22.3.0 dev_dependencies: build_runner: 2.16.0 @@ -91,6 +90,8 @@ dev_dependencies: riverpod_lint: 3.1.8 flutter_launcher_icons: 0.14.4 json_serializable: 6.14.1 + # Transitive dep but needed for commands + webcrypto: any flutter_launcher_icons: image_path: assets/icon.png diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0694802..5d2b53e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -30,6 +31,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WebcryptoPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WebcryptoPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 238ef93..c74b2f5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,10 +10,12 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever_windows url_launcher_windows + webcrypto window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES)