Push Notifications (#66)
Fixes #35. Things to do: - [x] Register with local distributor - [x] Register with homeserver - [x] Recieve messages - [x] Show notifications - [ ] Notifications page (could come later) Reviewed-on: #66
This commit is contained in:
parent
6e42902322
commit
ff9bf3bec4
30 changed files with 772 additions and 36 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -11,6 +11,8 @@
|
||||||
"muks",
|
"muks",
|
||||||
"prefs",
|
"prefs",
|
||||||
"unban",
|
"unban",
|
||||||
"unredact"
|
"unifiedpush",
|
||||||
|
"unredact",
|
||||||
|
"webpush"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,12 @@ dart scripts/generate.dart
|
||||||
> export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"
|
> 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:
|
Build generated files, and watch for new changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,21 @@ android {
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "nexus.federated.nexus"
|
applicationId = "nexus.federated.nexus"
|
||||||
|
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
@ -63,6 +68,11 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.unifiedpush.android:embedded-fcm-distributor:3.1.0")
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import UIKit
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "dart:async";
|
||||||
import "dart:ffi";
|
import "dart:ffi";
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
|
|
@ -15,11 +16,13 @@ import "package:nexus/controllers/sync_status.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces.dart";
|
import "package:nexus/controllers/top_level_spaces.dart";
|
||||||
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/models/capabilities.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/oauth_auth_code_response.dart";
|
import "package:nexus/models/oauth_auth_code_response.dart";
|
||||||
import "package:nexus/models/open_graph_data.dart";
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
import "package:nexus/models/paginate.dart";
|
import "package:nexus/models/paginate.dart";
|
||||||
|
import "package:nexus/models/requests/deregister_pusher.dart";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
import "package:nexus/models/requests/download_media.dart";
|
||||||
import "package:nexus/models/requests/get_event.dart";
|
import "package:nexus/models/requests/get_event.dart";
|
||||||
import "package:nexus/models/requests/get_related_events.dart";
|
import "package:nexus/models/requests/get_related_events.dart";
|
||||||
|
|
@ -31,6 +34,7 @@ import "package:nexus/models/requests/oauth/get_auth_url.dart";
|
||||||
import "package:nexus/models/requests/oauth/register_client.dart";
|
import "package:nexus/models/requests/oauth/register_client.dart";
|
||||||
import "package:nexus/models/requests/paginate.dart";
|
import "package:nexus/models/requests/paginate.dart";
|
||||||
import "package:nexus/models/requests/redact_event.dart";
|
import "package:nexus/models/requests/redact_event.dart";
|
||||||
|
import "package:nexus/models/requests/register_pusher.dart";
|
||||||
import "package:nexus/models/requests/report.dart";
|
import "package:nexus/models/requests/report.dart";
|
||||||
import "package:nexus/models/requests/send_event.dart";
|
import "package:nexus/models/requests/send_event.dart";
|
||||||
import "package:nexus/models/requests/send_message.dart";
|
import "package:nexus/models/requests/send_message.dart";
|
||||||
|
|
@ -156,18 +160,15 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
throw Exception("GomuksStart returned error code $errorCode");
|
throw Exception("GomuksStart returned error code $errorCode");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _sendCommand(
|
Future<dynamic> callGomuksMethod(
|
||||||
String command, [
|
Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data = const {},
|
FutureOr<GomuksResponse> Function(int handle, GomuksBorrowedBuffer data)
|
||||||
]) async {
|
callback,
|
||||||
|
) async {
|
||||||
final bufferPointer = data.toGomuksBufferPtr();
|
final bufferPointer = data.toGomuksBufferPtr();
|
||||||
final handle = await future;
|
final handle = await future;
|
||||||
final response = await Isolate.run(
|
final response = await Isolate.run(
|
||||||
() => GomuksSubmitCommand(
|
() => callback(handle, bufferPointer.ref),
|
||||||
handle,
|
|
||||||
command.toNativeUtf8().cast<Char>(),
|
|
||||||
bufferPointer.ref,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
calloc.free(bufferPointer);
|
calloc.free(bufferPointer);
|
||||||
|
|
@ -180,6 +181,24 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Event?> handlePush(Map<String, dynamic> data) async {
|
||||||
|
final response = await callGomuksMethod(
|
||||||
|
data,
|
||||||
|
(handle, data) async => GomuksHandlePush(handle, data),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response == null ? null : .fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _sendCommand(
|
||||||
|
String command, [
|
||||||
|
Map<String, dynamic> data = const {},
|
||||||
|
]) => callGomuksMethod(
|
||||||
|
data,
|
||||||
|
(handle, data) async =>
|
||||||
|
GomuksSubmitCommand(handle, command.toNativeUtf8().cast<Char>(), data),
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> redactEvent(RedactEventRequest report) =>
|
Future<void> redactEvent(RedactEventRequest report) =>
|
||||||
_sendCommand("redact_event", report.toJson());
|
_sendCommand("redact_event", report.toJson());
|
||||||
|
|
||||||
|
|
@ -281,6 +300,15 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<void> setAccountData(SetAccountDataRequest request) =>
|
Future<void> setAccountData(SetAccountDataRequest request) =>
|
||||||
_sendCommand("set_account_data", request.toJson());
|
_sendCommand("set_account_data", request.toJson());
|
||||||
|
|
||||||
|
Future<void> registerPusher(RegisterPusherRequest request) =>
|
||||||
|
_sendCommand("register_homeserver_push", request.toJson());
|
||||||
|
|
||||||
|
Future<void> deregisterPusher(DeregisterPusherRequest request) =>
|
||||||
|
_sendCommand("register_homeserver_push", {
|
||||||
|
...request.toJson(),
|
||||||
|
"kind": null,
|
||||||
|
});
|
||||||
|
|
||||||
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
||||||
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
||||||
|
|
||||||
|
|
@ -318,6 +346,10 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<SpecVersionsResponse> getSpecVersions() async =>
|
Future<SpecVersionsResponse> getSpecVersions() async =>
|
||||||
.fromJson(await _sendCommand("get_versions"));
|
.fromJson(await _sendCommand("get_versions"));
|
||||||
|
|
||||||
|
Future<Capabilities> getCapabilities() async => Capabilities.fromJson(
|
||||||
|
(await _sendCommand("get_capabilities"))["capabilities"],
|
||||||
|
);
|
||||||
|
|
||||||
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
||||||
try {
|
try {
|
||||||
final response = await _sendCommand("discover_homeserver", {
|
final response = await _sendCommand("discover_homeserver", {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import "package:nexus/controllers/shared_prefs.dart";
|
||||||
class KeyController(final String key) extends Notifier<String?> {
|
class KeyController(final String key) extends Notifier<String?> {
|
||||||
static const String spaceKey = "space";
|
static const String spaceKey = "space";
|
||||||
static const String roomKey = "room";
|
static const String roomKey = "room";
|
||||||
|
static const String pushKeyKey = "pushKey";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? build() =>
|
String? build() =>
|
||||||
|
|
|
||||||
120
lib/controllers/notifications.dart
Normal file
120
lib/controllers/notifications.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
import "package:flutter_local_notifications/flutter_local_notifications.dart";
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import "package:flutter_local_notifications_linux/src/model/hint.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/pages/notifications.dart";
|
||||||
|
|
||||||
|
class NotificationsController
|
||||||
|
extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
|
||||||
|
@override
|
||||||
|
Future<FlutterLocalNotificationsPlugin> 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_foreground"),
|
||||||
|
iOS: darwin,
|
||||||
|
macOS: darwin,
|
||||||
|
linux: .new(defaultActionName: "Open"),
|
||||||
|
),
|
||||||
|
onDidReceiveNotificationResponse: (details) {
|
||||||
|
if (details.payload case final eventId?) {
|
||||||
|
if (navigatorKey.currentContext case final context?) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => NotificationsPage(eventId: eventId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<void> 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),
|
||||||
|
),
|
||||||
|
linux: .new(
|
||||||
|
customHints: icon == null
|
||||||
|
? null
|
||||||
|
: [
|
||||||
|
.new(
|
||||||
|
name: "image-path",
|
||||||
|
value: LinuxHintStringValue(icon.path),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// TODO: See if icons can be added to iOS, macOS, and Windows notifications (#68)
|
||||||
|
iOS: .new(),
|
||||||
|
macOS: .new(),
|
||||||
|
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);
|
||||||
|
}
|
||||||
30
lib/controllers/push_key.dart
Normal file
30
lib/controllers/push_key.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import "dart:convert";
|
||||||
|
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/key.dart";
|
||||||
|
|
||||||
|
class PushKeyController(final String instance) extends Notifier<String?> {
|
||||||
|
@override
|
||||||
|
String? build() => json.decode(
|
||||||
|
ref.watch(KeyController.provider(KeyController.pushKeyKey)) ?? "{}",
|
||||||
|
)[instance];
|
||||||
|
|
||||||
|
Future<void> set(String? value) async {
|
||||||
|
final provider = KeyController.provider(KeyController.pushKeyKey);
|
||||||
|
final notifier = ref.watch(provider.notifier);
|
||||||
|
final prefs = IMap(json.decode(ref.watch(provider) ?? "{}"));
|
||||||
|
|
||||||
|
state = value;
|
||||||
|
notifier.set(
|
||||||
|
json.encode(
|
||||||
|
prefs.add(instance, value).where((_, value) => value != null).unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
NotifierProvider.family<PushKeyController, String?, String>(
|
||||||
|
PushKeyController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:m3e_buttons/m3e_buttons.dart";
|
import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
import "package:nexus/controllers/account_data.dart";
|
import "package:nexus/controllers/account_data.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/notifications.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
import "package:nexus/controllers/spec_versions.dart";
|
import "package:nexus/controllers/spec_versions.dart";
|
||||||
|
import "package:nexus/controllers/unified_push_allowed.dart";
|
||||||
import "package:nexus/models/account_data.dart";
|
import "package:nexus/models/account_data.dart";
|
||||||
import "package:nexus/models/settings_category.dart";
|
import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -95,6 +99,90 @@ class SettingsSectionsController
|
||||||
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
||||||
"Account": .new([
|
"Account": .new([
|
||||||
.new(title: "Profile", icon: Icons.person, settings: .new([])),
|
.new(title: "Profile", icon: Icons.person, settings: .new([])),
|
||||||
|
.new(
|
||||||
|
title: "Notifications",
|
||||||
|
icon: Icons.notifications,
|
||||||
|
settings: .new([
|
||||||
|
.new(
|
||||||
|
title: "Push notifications",
|
||||||
|
description: "Enable push notifications using Web Push",
|
||||||
|
builder: (title, description, icon) => HookConsumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final loading = useState(false);
|
||||||
|
final pusherRegistered = ref.watch(
|
||||||
|
UnifiedPushController.provider,
|
||||||
|
);
|
||||||
|
final unifiedPushAllowed = ref.watch(
|
||||||
|
UnifiedPushAllowedController.provider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SwitchListTile(
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(
|
||||||
|
unifiedPushAllowed.maybeWhen(
|
||||||
|
data: (data) => data,
|
||||||
|
orElse: () => null,
|
||||||
|
) ??
|
||||||
|
description,
|
||||||
|
),
|
||||||
|
secondary: Icon(icon),
|
||||||
|
value: pusherRegistered.maybeWhen(
|
||||||
|
data: (value) => value,
|
||||||
|
orElse: () => false,
|
||||||
|
),
|
||||||
|
onChanged: loading.value
|
||||||
|
? null
|
||||||
|
: unifiedPushAllowed.maybeWhen(
|
||||||
|
data: (data) => data == null,
|
||||||
|
orElse: () => false,
|
||||||
|
)
|
||||||
|
? pusherRegistered.maybeWhen(
|
||||||
|
data: (_) => (value) async {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
if (value) {
|
||||||
|
if (await ref
|
||||||
|
.watch(
|
||||||
|
NotificationsController
|
||||||
|
.provider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.requestPermissions()) {
|
||||||
|
await ref
|
||||||
|
.watch(
|
||||||
|
UnifiedPushController
|
||||||
|
.provider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.register();
|
||||||
|
} else {
|
||||||
|
// TODO: Handle not granted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref
|
||||||
|
.watch(
|
||||||
|
UnifiedPushController
|
||||||
|
.provider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.deregister();
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
showError(error, stackTrace);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orElse: () => null,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
icon: Icons.notification_add,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
.new(
|
.new(
|
||||||
title: "Safety",
|
title: "Safety",
|
||||||
icon: Icons.gpp_good,
|
icon: Icons.gpp_good,
|
||||||
|
|
@ -166,6 +254,8 @@ class SettingsSectionsController
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.popUntil((route) => route.isFirst);
|
.popUntil((route) => route.isFirst);
|
||||||
|
Navigator.of(context)
|
||||||
|
.popUntil((route) => route.isFirst);
|
||||||
|
|
||||||
await WidgetsBinding.instance.endOfFrame;
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
|
||||||
|
|
|
||||||
164
lib/controllers/unified_push.dart
Normal file
164
lib/controllers/unified_push.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import "dart:convert";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:intl/intl.dart";
|
||||||
|
import "package:nexus/controllers/key.dart";
|
||||||
|
import "package:nexus/controllers/notifications.dart";
|
||||||
|
import "package:nexus/controllers/push_key.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/content/sticker.dart";
|
||||||
|
import "package:nexus/models/requests/register_pusher.dart";
|
||||||
|
import "package:unifiedpush/unifiedpush.dart";
|
||||||
|
import "package:unifiedpush_storage_shared_preferences/storage.dart";
|
||||||
|
import "package:window_manager/window_manager.dart";
|
||||||
|
|
||||||
|
class UnifiedPushController extends AsyncNotifier<bool> {
|
||||||
|
@override
|
||||||
|
Future<bool> build() async {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
final registered = await UnifiedPush.initialize(
|
||||||
|
linuxOptions: .new(
|
||||||
|
dbusName: "nexus.federated.nexus.UnifiedPush",
|
||||||
|
storage: UnifiedPushStorageSharedPreferences(),
|
||||||
|
background: isInBackground,
|
||||||
|
),
|
||||||
|
onNewEndpoint: (endpoint, instance) async {
|
||||||
|
final pushKey = endpoint.pubKeySet!.pubKey;
|
||||||
|
await ref
|
||||||
|
.watch(PushKeyController.provider(instance).notifier)
|
||||||
|
.set(pushKey);
|
||||||
|
|
||||||
|
await client.registerPusher(
|
||||||
|
.new(
|
||||||
|
appDisplayName: "Nexus",
|
||||||
|
appId: "nexus.federated.nexus",
|
||||||
|
data: PusherData.webPush(
|
||||||
|
url: Uri.parse(endpoint.url),
|
||||||
|
auth: endpoint.pubKeySet!.auth,
|
||||||
|
),
|
||||||
|
deviceDisplayName:
|
||||||
|
"Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}",
|
||||||
|
kind: .webPush,
|
||||||
|
lang: "en",
|
||||||
|
pushKey: pushKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMessage: (message, instance) async {
|
||||||
|
if (message.decrypted == false) {
|
||||||
|
throw Exception(
|
||||||
|
"Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final event = await client.handlePush(
|
||||||
|
json.decode(String.fromCharCodes(message.content)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event == null ||
|
||||||
|
event.unreadType?.shouldNotify != true ||
|
||||||
|
(!isInBackground &&
|
||||||
|
await windowManager.isFocused().onError((_, _) => false) &&
|
||||||
|
ref.watch(KeyController.provider(KeyController.roomKey)) ==
|
||||||
|
event.roomId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = ref.read(
|
||||||
|
RoomsController.provider.select((rooms) => rooms[event.roomId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
final avatar = room?.metadata?.avatar;
|
||||||
|
final icon = avatar == null
|
||||||
|
? null
|
||||||
|
: await ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.downloadMedia(.new(mxc: avatar, isAvatar: true));
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(NotificationsController.provider.notifier)
|
||||||
|
.send(
|
||||||
|
id: event.eventId.hashCode & 0x7fffffff,
|
||||||
|
title: room?.metadata?.name ?? "New Event",
|
||||||
|
icon: icon,
|
||||||
|
payload: event.eventId,
|
||||||
|
body: switch (event.content) {
|
||||||
|
MessageContent(:final body?) ||
|
||||||
|
StickerContent(:final body) => body,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isInBackground) exit(0);
|
||||||
|
},
|
||||||
|
onRegistrationFailed: (error, instance) => throw error,
|
||||||
|
onUnregistered: (instance) async {
|
||||||
|
await ref
|
||||||
|
.watch(PushKeyController.provider(instance).notifier)
|
||||||
|
.set(null);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (registered) {
|
||||||
|
// Needs to be registered every startup
|
||||||
|
await register(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> register([bool alreadyRegistered = false]) async {
|
||||||
|
final clientState = ref.watch(ClientStateController.provider);
|
||||||
|
if (clientState?.deviceId == null) ref.invalidateSelf();
|
||||||
|
|
||||||
|
final capabilities = await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getCapabilities();
|
||||||
|
|
||||||
|
if (capabilities.webpush?.vapid == null) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
"Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyRegistered &&
|
||||||
|
!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
|
||||||
|
throw Exception("No UnifiedPush distributors found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await UnifiedPush.register(
|
||||||
|
instance: clientState!.deviceId!,
|
||||||
|
vapid: capabilities.webpush?.vapid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyRegistered) ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deregister() async {
|
||||||
|
final clientState = ref.watch(ClientStateController.provider);
|
||||||
|
final key = ref.watch(PushKeyController.provider(clientState!.deviceId!));
|
||||||
|
|
||||||
|
if (key != null) {
|
||||||
|
await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.deregisterPusher(.new(appId: "nexus.federated.nexus", pushKey: key));
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
"No matching pushKey found. Skipping deregistration from homeserver.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UnifiedPush.unregister(clientState.deviceId!);
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider<UnifiedPushController, bool>(
|
||||||
|
UnifiedPushController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
33
lib/controllers/unified_push_allowed.dart
Normal file
33
lib/controllers/unified_push_allowed.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:unifiedpush/unifiedpush.dart";
|
||||||
|
|
||||||
|
class UnifiedPushAllowedController extends AsyncNotifier<String?> {
|
||||||
|
@override
|
||||||
|
Future<String?> build() async {
|
||||||
|
if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
|
||||||
|
return "No valid distributors found. Try installing ${Platform.isLinux
|
||||||
|
? "KUnifiedPush"
|
||||||
|
: Platform.isAndroid
|
||||||
|
? "Google Play Services or NTFY"
|
||||||
|
: "NTFY"}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
final capabilities = await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getCapabilities();
|
||||||
|
|
||||||
|
if (capabilities.webpush?.vapid == null) {
|
||||||
|
return "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.autoDispose<UnifiedPushAllowedController, String?>(
|
||||||
|
UnifiedPushAllowedController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,10 @@ import "package:media_kit/media_kit.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/multi_provider.dart";
|
import "package:nexus/controllers/multi_provider.dart";
|
||||||
|
import "package:nexus/controllers/notifications.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/shared_prefs.dart";
|
import "package:nexus/controllers/shared_prefs.dart";
|
||||||
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
||||||
import "package:nexus/helpers/font_licenses.dart";
|
import "package:nexus/helpers/font_licenses.dart";
|
||||||
|
|
@ -22,6 +24,7 @@ import "package:window_manager/window_manager.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
late final bool isInBackground;
|
||||||
|
|
||||||
final class Logger extends ProviderObserver {
|
final class Logger extends ProviderObserver {
|
||||||
@override
|
@override
|
||||||
|
|
@ -45,6 +48,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
|
||||||
error.toString().contains("Invalid source") ||
|
error.toString().contains("Invalid source") ||
|
||||||
error.toString().contains("UTF-16") ||
|
error.toString().contains("UTF-16") ||
|
||||||
error.toString().contains("HTTP request failed") ||
|
error.toString().contains("HTTP request failed") ||
|
||||||
|
error.toString().contains("'_nextFrame != null': is not true.") ||
|
||||||
error.toString().contains("Invalid image data")) {
|
error.toString().contains("Invalid image data")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +66,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main(List<String> args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
|
|
@ -77,11 +81,14 @@ void main() async {
|
||||||
await windowManager.setMinimumSize(Size.square(500));
|
await windowManager.setMinimumSize(Size.square(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInBackground = args.contains("--unifiedpush-bg");
|
||||||
|
|
||||||
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
|
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||||
showError(details.exception.toString(), details.stack);
|
showError(details.exception.toString(), details.stack);
|
||||||
|
|
||||||
|
if (!isInBackground) {
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
retry: (_, _) => null,
|
retry: (_, _) => null,
|
||||||
|
|
@ -90,12 +97,14 @@ void main() async {
|
||||||
// ignore: dead_code
|
// ignore: dead_code
|
||||||
if (false && kDebugMode) Logger(),
|
if (false && kDebugMode) Logger(),
|
||||||
],
|
],
|
||||||
child: const App(),
|
child: App(isInBackground),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class const App({super.key}) extends StatelessWidget {
|
class const App(final bool isInBackground, {super.key})
|
||||||
|
extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => DynamicColorBuilder(
|
Widget build(BuildContext context) => DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) => Consumer(
|
builder: (lightDynamic, darkDynamic) => Consumer(
|
||||||
|
|
@ -138,6 +147,8 @@ class const App({super.key}) extends StatelessWidget {
|
||||||
IListConst([
|
IListConst([
|
||||||
SharedPrefsController.provider,
|
SharedPrefsController.provider,
|
||||||
ClientController.provider,
|
ClientController.provider,
|
||||||
|
NotificationsController.provider,
|
||||||
|
UnifiedPushController.provider,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
25
lib/models/capabilities.dart
Normal file
25
lib/models/capabilities.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "capabilities.freezed.dart";
|
||||||
|
part "capabilities.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const Capabilities({
|
||||||
|
@JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush,
|
||||||
|
}) with _$Capabilities {
|
||||||
|
Map<String, Object?> toJson() => _$CapabilitiesToJson(this);
|
||||||
|
|
||||||
|
factory Capabilities.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$CapabilitiesFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const WebPush({required final bool enabled, final String? vapid})
|
||||||
|
with _$WebPush {
|
||||||
|
Map<String, Object?> toJson() => _$WebPushToJson(this);
|
||||||
|
|
||||||
|
factory WebPush.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$WebPushFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ class const ClientState({
|
||||||
required final bool isLoggedIn,
|
required final bool isLoggedIn,
|
||||||
required final bool isVerified,
|
required final bool isVerified,
|
||||||
required final String? userId,
|
required final String? userId,
|
||||||
|
required final String? deviceId,
|
||||||
required final String? homeserverUrl,
|
required final String? homeserverUrl,
|
||||||
}) with _$ClientState {
|
}) with _$ClientState {
|
||||||
Map<String, Object?> toJson() => _$ClientStateToJson(this);
|
Map<String, Object?> toJson() => _$ClientStateToJson(this);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
@JsonEnum(fieldRename: FieldRename.snake)
|
@JsonEnum(fieldRename: .snake)
|
||||||
enum JoinRule { public, knock, invite, private, restricted, knockRestricted }
|
enum JoinRule { public, knock, invite, private, restricted, knockRestricted }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
|
||||||
|
|
||||||
@JsonEnum()
|
|
||||||
enum MembershipAction { ban, kick, unban, invite }
|
enum MembershipAction { ban, kick, unban, invite }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
|
||||||
|
|
||||||
@JsonEnum()
|
|
||||||
enum MembershipStatus { leave, invite, ban, join, knock }
|
enum MembershipStatus { leave, invite, ban, join, knock }
|
||||||
|
|
|
||||||
13
lib/models/requests/deregister_pusher.dart
Normal file
13
lib/models/requests/deregister_pusher.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "deregister_pusher.freezed.dart";
|
||||||
|
part "deregister_pusher.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const DeregisterPusherRequest({
|
||||||
|
required final String appId,
|
||||||
|
@JsonKey(name: "pushkey") required final String pushKey,
|
||||||
|
}) with _$DeregisterPusherRequest {
|
||||||
|
Map<String, Object?> toJson() => _$DeregisterPusherRequestToJson(this);
|
||||||
|
}
|
||||||
56
lib/models/requests/register_pusher.dart
Normal file
56
lib/models/requests/register_pusher.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
|
part "register_pusher.freezed.dart";
|
||||||
|
part "register_pusher.g.dart";
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const RegisterPusherRequest({
|
||||||
|
required final String appDisplayName,
|
||||||
|
required final String appId,
|
||||||
|
final bool append = false,
|
||||||
|
required final PusherData data,
|
||||||
|
required final String deviceDisplayName,
|
||||||
|
required final PusherKind kind,
|
||||||
|
required final String lang,
|
||||||
|
|
||||||
|
/// TODO: What does this do?
|
||||||
|
final String? profileTag,
|
||||||
|
|
||||||
|
@JsonKey(name: "pushkey") required final String pushKey,
|
||||||
|
}) with _$RegisterPusherRequest {
|
||||||
|
Map<String, Object?> toJson() => _$RegisterPusherRequestToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class PusherData with _$PusherData {
|
||||||
|
const factory PusherData.http({
|
||||||
|
required Uri url,
|
||||||
|
@Default(PushFormat.eventIdOnly) PushFormat format,
|
||||||
|
}) = HttpPusherData;
|
||||||
|
|
||||||
|
const factory PusherData.webPush({
|
||||||
|
required Uri url,
|
||||||
|
@Default(PushFormat.eventIdOnly) PushFormat format,
|
||||||
|
|
||||||
|
/// `data.auth`: RFC8291 authentication secret.
|
||||||
|
required String auth,
|
||||||
|
}) = WebPushPusherData;
|
||||||
|
|
||||||
|
factory PusherData.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$PusherDataFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonEnum(fieldRename: .snake)
|
||||||
|
enum PushFormat {
|
||||||
|
@JsonValue(null)
|
||||||
|
all,
|
||||||
|
eventIdOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PusherKind {
|
||||||
|
http,
|
||||||
|
email,
|
||||||
|
@JsonValue("org.matrix.msc4174.webpush")
|
||||||
|
webPush,
|
||||||
|
}
|
||||||
14
lib/pages/notifications.dart
Normal file
14
lib/pages/notifications.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/widgets/appbar.dart";
|
||||||
|
|
||||||
|
class const NotificationsPage({final String? eventId, super.key})
|
||||||
|
extends HookConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: Appbar(title: Text("Notifications")),
|
||||||
|
body: Placeholder(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ final class const Appbar({
|
||||||
: (_) => windowManager.startDragging(),
|
: (_) => windowManager.startDragging(),
|
||||||
),
|
),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
leading: InkWell(onTap: onTap, child: leading),
|
leading: leading == null ? null : InkWell(onTap: onTap, child: leading),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
scrolledUnderElevation: scrolledUnderElevation,
|
scrolledUnderElevation: scrolledUnderElevation,
|
||||||
actionsPadding: const .symmetric(horizontal: 8),
|
actionsPadding: const .symmetric(horizontal: 8),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
#include <webcrypto/webcrypto_plugin.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
|
@ -37,6 +38,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
media_kit_video
|
media_kit_video
|
||||||
screen_retriever_linux
|
screen_retriever_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
webcrypto
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ pkgs.mkShell {
|
||||||
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2";
|
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs pkgs.stdenv.isLinux {
|
// lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||||
CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ];
|
|
||||||
LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}";
|
LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "my_application.h"
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
#include <flutter_linux/flutter_linux.h>
|
#include <flutter_linux/flutter_linux.h>
|
||||||
#ifdef GDK_WINDOWING_X11
|
#ifdef GDK_WINDOWING_X11
|
||||||
#include <gdk/gdkx.h>
|
#include <gdk/gdkx.h>
|
||||||
|
|
@ -72,6 +73,7 @@ static void my_application_activate(GApplication* application) {
|
||||||
gdk_rgba_parse(&background_color, "#000000");
|
gdk_rgba_parse(&background_color, "#000000");
|
||||||
fl_view_set_background_color(view, &background_color);
|
fl_view_set_background_color(view, &background_color);
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
if (std::getenv("FLUTTER_HEADLESS")) gtk_widget_hide(GTK_WIDGET(window));
|
||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
// Show the window when Flutter renders.
|
// Show the window when Flutter renders.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Foundation
|
||||||
import app_links
|
import app_links
|
||||||
import dynamic_color
|
import dynamic_color
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import flutter_local_notifications
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
import media_kit_video
|
import media_kit_video
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
|
|
@ -15,12 +16,14 @@ import screen_retriever_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
import webcrypto
|
||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
|
@ -28,5 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
|
WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
pubspec.lock
112
pubspec.lock
|
|
@ -495,6 +495,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
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: "direct main"
|
||||||
|
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:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -1429,6 +1469,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.7.1"
|
version: "3.7.1"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1437,6 +1485,54 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
unifiedpush:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: unifiedpush
|
||||||
|
sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.0"
|
||||||
|
unifiedpush_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unifiedpush_android
|
||||||
|
sha256: a902e505e91c8e28b54b379bb27f255f66df409274a97af24d6719f12f0d13dc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.5.0"
|
||||||
|
unifiedpush_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unifiedpush_linux
|
||||||
|
sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
unifiedpush_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unifiedpush_platform_interface
|
||||||
|
sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
unifiedpush_storage_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unifiedpush_storage_interface
|
||||||
|
sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
unifiedpush_storage_shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: unifiedpush_storage_shared_preferences
|
||||||
|
sha256: eda9c52bac0058f81e0d9c65e33eedc12c5901d2e68ad47fdf68175ddf00a3b4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
universal_platform:
|
universal_platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1613,6 +1709,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
webcrypto:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: webcrypto
|
||||||
|
sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
|
webpush_encryption:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webpush_encryption
|
||||||
|
sha256: "5b83272b91acda6ae515fcd980c94f06bf413702282497c5a68f5dfc64fed27f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ dependencies:
|
||||||
app_links: 7.2.1
|
app_links: 7.2.1
|
||||||
file_selector: 1.1.0
|
file_selector: 1.1.0
|
||||||
material_ui: 1.2.0
|
material_ui: 1.2.0
|
||||||
|
unifiedpush: 6.2.0
|
||||||
|
unifiedpush_storage_shared_preferences: 1.0.0
|
||||||
|
flutter_local_notifications: 22.3.0
|
||||||
|
flutter_local_notifications_linux: 8.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: 2.16.1
|
build_runner: 2.16.1
|
||||||
|
|
@ -87,6 +91,8 @@ dev_dependencies:
|
||||||
riverpod_lint: 3.1.9
|
riverpod_lint: 3.1.9
|
||||||
flutter_launcher_icons: 0.14.4
|
flutter_launcher_icons: 0.14.4
|
||||||
json_serializable: 6.14.1
|
json_serializable: 6.14.1
|
||||||
|
# Transitive dep but needed for commands
|
||||||
|
webcrypto: any
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
image_path: assets/icon.png
|
image_path: assets/icon.png
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
#include <webcrypto/webcrypto_plugin.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
|
@ -30,6 +31,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
|
WebcryptoPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("WebcryptoPlugin"));
|
||||||
WindowManagerPluginRegisterWithRegistrar(
|
WindowManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
media_kit_video
|
media_kit_video
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
webcrypto
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
flutter_local_notifications_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue