Watch
1
0
Fork
You've already forked nexus
0
forked from Nexus/nexus

send notifications natively

This commit is contained in:
Henry Hiles 2026-08-26 00:25:47 -04:00
commit 2beb6dcb9b
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
14 changed files with 225 additions and 30 deletions

View file

@ -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<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"),
iOS: darwin,
macOS: darwin,
linux: .new(defaultActionName: "Open"),
),
onDidReceiveNotificationResponse: (details) {
print(details.data);
},
);
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),
),
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);
}

View file

@ -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,

View file

@ -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<bool> {
);
},
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(),

View file

@ -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,
]),
),