use xdg_desktop_portal over flutter_local_notifications on linux

This commit is contained in:
Henry Hiles 2026-09-21 15:43:01 -04:00
commit fea39e4191
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 132 additions and 76 deletions

View file

@ -1,10 +1,13 @@
import "dart:io"; import "dart:io";
import "package:flutter/foundation.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:nexus/controllers/portal.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:flutter_local_notifications/flutter_local_notifications.dart"; import "package:flutter_local_notifications/flutter_local_notifications.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/pages/notifications.dart"; import "package:nexus/pages/notifications.dart";
import "package:xdg_desktop_portal/xdg_desktop_portal.dart";
class NotificationController class NotificationController
extends AsyncNotifier<FlutterLocalNotificationsPlugin> { extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
@ -12,40 +15,77 @@ class NotificationController
Future<FlutterLocalNotificationsPlugin> build() async { Future<FlutterLocalNotificationsPlugin> build() async {
final notifications = FlutterLocalNotificationsPlugin(); final notifications = FlutterLocalNotificationsPlugin();
final darwin = DarwinInitializationSettings(); if (!Platform.isLinux) {
final darwin = DarwinInitializationSettings();
await notifications.initialize( await notifications.initialize(
settings: .new( settings: .new(
windows: .new( windows: .new(
appName: "Nexus", appName: "Nexus",
appUserModelId: "nexus.federated.nexus", appUserModelId: "nexus.federated.nexus",
guid: "dde78daf-130f-4e46-a80a-e31deeab45d7", guid: "dde78daf-130f-4e46-a80a-e31deeab45d7",
),
android: .new("ic_launcher_foreground"),
iOS: darwin,
macOS: darwin,
), ),
android: .new("ic_launcher_foreground"), onDidReceiveNotificationResponse: (details) {
iOS: darwin, if (details.payload case final eventId?) {
macOS: darwin, if (navigatorKey.currentContext case final context?) {
linux: .new(defaultActionName: "Open"), Navigator.of(context).push(
), MaterialPageRoute(
onDidReceiveNotificationResponse: (details) { builder: (_) => NotificationsPage(
if (details.payload case final eventId?) { highlightedEventId: eventId,
if (navigatorKey.currentContext case final context?) { defaultToAllNotifications: true,
),
),
);
}
}
},
);
}
if (Platform.isLinux) {
final portal = await ref.watch(PortalController.provider.future);
portal.notification.actionInvoked.listen((event) {
if (event.action != "app.event") {
return;
}
if (navigatorKey.currentContext case final context?) {
if (context.mounted) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => NotificationsPage( builder: (_) => NotificationsPage(
highlightedEventId: eventId, highlightedEventId: event.id,
defaultToAllNotifications: true, defaultToAllNotifications: true,
), ),
), ),
); );
} }
} }
}, });
); }
ref.onDispose(() {
// The portal client owns its D-Bus connection.
if (Platform.isLinux) {
ref.read(PortalController.provider.future).then((portal) {
portal.close();
});
}
});
return notifications; return notifications;
} }
Future<bool> requestPermissions() async { Future<bool> requestPermissions() async {
if (Platform.isLinux) {
return true;
}
final controller = await future; final controller = await future;
if (Platform.isIOS) { if (Platform.isIOS) {
@ -82,31 +122,41 @@ class NotificationController
String? payload, String? payload,
}) async { }) async {
debugPrint("Sending notification for $id"); debugPrint("Sending notification for $id");
final notificationDetails = NotificationDetails(
android: .new(
"messages",
"Messages",
largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path),
),
linux: .new(
category: LinuxNotificationCategory.imReceived,
defaultActionName: "app.event",
icon: icon == null ? null : FilePathLinuxIcon(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; if (Platform.isLinux) {
await controller.show( final portal = await ref.read(PortalController.provider.future);
id: id,
title: title, await portal.notification.addNotification(
body: body, id.toString(),
notificationDetails: notificationDetails, title: title,
payload: payload, body: body,
); icon: icon == null ? null : XdgNotificationIconFile(icon.path),
defaultAction: "app.event",
);
} else {
final notificationDetails = NotificationDetails(
android: .new(
"messages",
"Messages",
largeIcon: icon == null ? null : FilePathAndroidBitmap(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 = static final provider =

View file

@ -0,0 +1,17 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:xdg_desktop_portal/xdg_desktop_portal.dart";
class PortalController extends AsyncNotifier<XdgDesktopPortalClient> {
@override
Future<XdgDesktopPortalClient> build() async {
final portal = XdgDesktopPortalClient();
await portal.registerApplication("nexus.federated.nexus");
return portal;
}
static final provider =
AsyncNotifierProvider<PortalController, XdgDesktopPortalClient>(
PortalController.new,
);
}

View file

@ -8,6 +8,4 @@ Terminal=false
Type=Application Type=Application
Categories=Chat;Network;InstantMessaging; Categories=Chat;Network;InstantMessaging;
MimeType=x-scheme-handler/nexus.federated.nexus; MimeType=x-scheme-handler/nexus.federated.nexus;
StartupNotify=true DBusActivatable=true
DBusActivatable=true
X-GNOME-UsesNotifications=true

View file

@ -1,3 +1,3 @@
[D-BUS Service] [D-BUS Service]
Name=nexus.federated.nexus Name=nexus.federated.nexus
Exec=/usr/bin/env FLUTTER_HEADLESS=1 nexus Exec=/usr/bin/env nexus

View file

@ -495,23 +495,21 @@ packages:
source: hosted source: hosted
version: "22.3.0" version: "22.3.0"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: "direct main" dependency: transitive
description: description:
path: flutter_local_notifications_linux name: flutter_local_notifications_linux
ref: "quad/feat/xdg-portal-notifs" sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b"
resolved-ref: d853ff4997f6dd2b9ecdc53d37fb337cebe63863 url: "https://pub.dev"
url: "https://github.com/Henry-Hiles/flutter_local_notifications" source: hosted
source: git version: "8.0.1"
version: "9.0.0-dev.1"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: "direct overridden" dependency: transitive
description: description:
path: flutter_local_notifications_platform_interface name: flutter_local_notifications_platform_interface
ref: "quad/feat/xdg-portal-notifs" sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd"
resolved-ref: d853ff4997f6dd2b9ecdc53d37fb337cebe63863 url: "https://pub.dev"
url: "https://github.com/Henry-Hiles/flutter_local_notifications" source: hosted
source: git version: "12.2.0"
version: "13.0.0-dev.1"
flutter_local_notifications_web: flutter_local_notifications_web:
dependency: transitive dependency: transitive
description: description:
@ -1745,12 +1743,13 @@ packages:
source: hosted source: hosted
version: "0.5.2" version: "0.5.2"
xdg_desktop_portal: xdg_desktop_portal:
dependency: transitive dependency: "direct main"
description: description:
name: xdg_desktop_portal path: "."
sha256: "4fae74b6ed63a449d4c796a74265cebdd912c390336dedc06d705142f3b939d7" ref: HEAD
url: "https://pub.dev" resolved-ref: "6d070b677f908b01065b4dce79ddc9a208daadbf"
source: hosted url: "https://github.com/Henry-Hiles/xdg_desktop_portal.dart"
source: git
version: "0.1.14" version: "0.1.14"
xdg_directories: xdg_directories:
dependency: "direct main" dependency: "direct main"

View file

@ -35,16 +35,6 @@ dependency_overrides:
git: git:
url: https://github.com/appelladev/linkify url: https://github.com/appelladev/linkify
ref: fix/consecutive-periods-loose-url ref: fix/consecutive-periods-loose-url
flutter_local_notifications_linux:
git:
url: https://github.com/Henry-Hiles/flutter_local_notifications
ref: quad/feat/xdg-portal-notifs
path: flutter_local_notifications_linux
flutter_local_notifications_platform_interface:
git:
url: https://github.com/Henry-Hiles/flutter_local_notifications
ref: quad/feat/xdg-portal-notifs
path: flutter_local_notifications_platform_interface
dependencies: dependencies:
flutter: flutter:
@ -99,8 +89,10 @@ dependencies:
unifiedpush: 6.2.0 unifiedpush: 6.2.0
unifiedpush_storage_shared_preferences: 1.0.0 unifiedpush_storage_shared_preferences: 1.0.0
flutter_local_notifications: 22.3.0 flutter_local_notifications: 22.3.0
flutter_local_notifications_linux: 8.0.1
material_emoji_picker: 1.1.1 material_emoji_picker: 1.1.1
xdg_desktop_portal:
git:
url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart
dev_dependencies: dev_dependencies:
build_runner: 2.16.1 build_runner: 2.16.1