diff --git a/assets/knott.png b/assets/knott.png index 148b91a1..e50812f7 100644 Binary files a/assets/knott.png and b/assets/knott.png differ diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 63e28097..1bb5922c 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -165,10 +165,10 @@ class CwtchNotifier { var notification = data["notification"]; if (notification == "SimpleEvent") { - notificationManager.notify(notificationSimple ?? "New Message"); + notificationManager.notify(notificationSimple ?? "New Message", "", 0); } else if (notification == "ContactInfo") { var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier); - notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString()))); + notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier); } profileCN.getProfile(data["ProfileOnion"])?.newMessage( @@ -242,10 +242,10 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation); if (notification == "SimpleEvent") { - notificationManager.notify(notificationSimple ?? "New Message"); + notificationManager.notify(notificationSimple ?? "New Message", "", 0); } else if (notification == "ContactInfo") { var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier); - notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString()))); + notificationManager.notify((notificationConversationInfo ?? "New Message from %1").replaceFirst("%1", (contact?.nickname ?? senderHandle.toString())), data["ProfileOnion"], identifier); } appState.notifyProfileUnread(); } diff --git a/lib/main.dart b/lib/main.dart index e9f1b98d..d7c0021e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; import 'package:cwtch/notification_manager.dart'; import 'package:cwtch/themes/cwtch.dart'; +import 'package:cwtch/views/doublecolview.dart'; import 'package:cwtch/views/messageview.dart'; import 'package:flutter/foundation.dart'; import 'package:cwtch/cwtch/ffi.dart'; @@ -17,8 +18,11 @@ import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; import 'licenses.dart'; import 'models/appstate.dart'; +import 'models/contactlist.dart'; +import 'models/profile.dart'; import 'models/profilelist.dart'; import 'models/servers.dart'; +import 'views/contactsview.dart'; import 'views/profilemgrview.dart'; import 'views/splashView.dart'; import 'dart:io' show Platform, exit, sleep; @@ -78,10 +82,10 @@ class FlwtchState extends State with WindowListener { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList); cwtch = CwtchGomobile(cwtchNotifier); } else if (Platform.isLinux) { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList); + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList); cwtch = CwtchFfi(cwtchNotifier); } else { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList); + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList); cwtch = CwtchFfi(cwtchNotifier); } print("initState: invoking cwtch.Start()"); @@ -182,36 +186,43 @@ class FlwtchState extends State with WindowListener { // coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID Future _externalNotificationClicked(MethodCall call) async { var args = jsonDecode(call.arguments); - var profile = profs.getProfile(args["ProfileOnion"])!; - var convo = profile.contactList.getContact(args["Handle"])!; + _notificationSelectConvo(args["ProfileOnion"], args["Handle"]); + } + + Future _notificationSelectConvo(String profileOnion, int convoId) async { + var profile = profs.getProfile(profileOnion)!; + var convo = profile.contactList.getContact(convoId)!; + if (profileOnion.isEmpty) { + return; + } Provider.of(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages; convo.unreadMessages = 0; - // single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation - var isLandscape = Provider.of(navKey.currentContext!, listen: false).isLandscape(navKey.currentContext!); - if (Provider.of(navKey.currentContext!, listen: false).uiColumns(isLandscape).length == 1) { - while (navKey.currentState!.canPop()) { - print("messageview already open; popping before pushing replacement"); - navKey.currentState!.pop(); - } - navKey.currentState?.push( - MaterialPageRoute( - builder: (BuildContext builderContext) { - return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: profile), - ChangeNotifierProvider.value(value: convo), - ], - builder: (context, child) => MessageView(), - ); - }, - ), - ); - } else { - //dual pane - Provider.of(navKey.currentContext!, listen: false).selectedProfile = args["ProfileOnion"]; - Provider.of(navKey.currentContext!, listen: false).selectedConversation = args["Handle"]; + // Clear nav path back to root + while (navKey.currentState!.canPop()) { + navKey.currentState!.pop(); } + + Provider.of(navKey.currentContext!, listen: false).selectedConversation = null; + Provider.of(navKey.currentContext!, listen: false).selectedProfile = profileOnion; + Provider.of(navKey.currentContext!, listen: false).selectedConversation = convoId; + + Navigator.of(navKey.currentContext!).push( + MaterialPageRoute( + settings: RouteSettings(name: "conversations"), + builder: (BuildContext buildcontext) { + return OrientationBuilder(builder: (orientationBuilderContext, orientation) { + return MultiProvider( + providers: [ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile.contactList)], + builder: (innercontext, widget) { + var appState = Provider.of(navKey.currentContext!); + var settings = Provider.of(navKey.currentContext!); + return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : MessageView(); + }); + }); + }, + ), + ); } // using windowManager flutter plugin until proper lifecycle management lands in desktop diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index 91d6ada8..8461099e 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -1,38 +1,24 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:cwtch/main.dart'; import 'package:win_toast/win_toast.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path/path.dart' as path; import 'config.dart'; // NotificationsManager provides a wrapper around platform specific notifications logic. abstract class NotificationsManager { - Future notify(String message); + Future notify(String message, String profile, int conversationId); } // NullNotificationsManager ignores all notification requests class NullNotificationsManager implements NotificationsManager { @override - Future notify(String message) async {} -} - -// LinuxNotificationsManager uses the desktop_notifications package to implement -// the standard dbus-powered linux desktop notifications. -class LinuxNotificationsManager implements NotificationsManager { - int previous_id = 0; - late NotificationsClient client; - - LinuxNotificationsManager(NotificationsClient client) { - this.client = client; - } - - Future notify(String message) async { - var iconPath = Uri.file(path.join(path.current, "cwtch.png")); - client.notify(message, appName: "cwtch", appIcon: iconPath.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id); - } + Future notify(String message, String profile, int conversationId) async {} } // Windows Notification Manager uses https://pub.dev/packages/desktoasts to implement @@ -47,7 +33,7 @@ class WindowsNotificationManager implements NotificationsManager { }); } - Future notify(String message) async { + Future notify(String message, String profile, int conversationId) async { if (initialized && !globalAppState.focus) { if (!active) { active = true; @@ -64,16 +50,73 @@ class WindowsNotificationManager implements NotificationsManager { } } -NotificationsManager newDesktopNotificationsManager() { - if (Platform.isLinux) { +class NotificationPayload { + late String profileOnion; + late int convoId; + + NotificationPayload(String po, int cid) { + profileOnion = po; + convoId = cid; + } + + NotificationPayload.fromJson(Map json) + : profileOnion = json['profileOnion'], + convoId = json['convoId']; + + Map toJson() => { + 'profileOnion': profileOnion, + 'convoId': convoId, + }; +} + +// FlutterLocalNotificationsPlugin based NotificationManager that handles Linux and MacOS +// Todo: it can also handle Android, do we want to migrate away from our manual solution? +class NixNotificationManager implements NotificationsManager { + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + late Future Function(String, int) notificationSelectConvo; + + NixNotificationManager(Future Function(String, int) notificationSelectConvo) { + this.notificationSelectConvo = notificationSelectConvo; + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(); + final LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings( + defaultActionName: 'Open notification', + defaultIcon: AssetsLinuxIcon('assets/knott.png'), + ); + + final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux); + + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + + scheduleMicrotask(() async { + await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification); + }); + } + + void selectNotification(String? payloadJson) async { + if (payloadJson != null) { + Map payloadMap = jsonDecode(payloadJson); + var payload = NotificationPayload.fromJson(payloadMap); + notificationSelectConvo(payload.profileOnion, payload.convoId); + } +} + + Future notify(String message, String profile, int conversationId) async { + await flutterLocalNotificationsPlugin.show(0, 'Cwtch', message, null, payload: jsonEncode(NotificationPayload(profile, conversationId))); + } +} + +NotificationsManager newDesktopNotificationsManager(Future Function(String profileOnion, int convoId) notificationSelectConvo) { + if (Platform.isLinux || Platform.isMacOS) { try { - // Test that we can actually access DBUS. Otherwise return a null - // notifications manager... - NotificationsClient client = NotificationsClient(); - client.getCapabilities(); - return LinuxNotificationsManager(client); + return NixNotificationManager(notificationSelectConvo); } catch (e) { - EnvironmentConfig.debugLog("Attempted to access DBUS for notifications but failed. Switching off notifications."); + EnvironmentConfig.debugLog("Failed to create NixNotificationManager. Switching off notifications."); } } else if (Platform.isWindows) { try { @@ -82,5 +125,6 @@ NotificationsManager newDesktopNotificationsManager() { EnvironmentConfig.debugLog("Failed to create Windows desktoasts notification manager"); } } + return NullNotificationsManager(); } diff --git a/pubspec.lock b/pubspec.lock index c3459757..2a4d1f66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,14 +189,14 @@ packages: name: dbus url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.6.8" desktop_notifications: dependency: "direct main" description: name: desktop_notifications url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.6.0" fake_async: dependency: transitive description: @@ -256,6 +256,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0-rc.9" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "9.3.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -519,13 +540,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" petitparser: dependency: transitive description: @@ -671,6 +685,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.3" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 706c652d..f5d90737 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: cupertino_icons: ^1.0.0 ffi: ^1.0.0 path_provider: ^2.0.0 - desktop_notifications: 0.5.0 crypto: 3.0.1 glob: any @@ -42,8 +41,10 @@ dependencies: file_picker: ^4.3.2 file_picker_desktop: ^1.1.0 url_launcher: ^6.0.18 - win_toast: ^0.0.2 window_manager: ^0.1.4 + desktop_notifications: 0.6.0 + win_toast: ^0.0.2 + flutter_local_notifications: 9.3.2 dev_dependencies: msix: ^2.1.3