diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index e1dfc3b..d49f6ef 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cwtch/notification_manager.dart'; import 'package:provider/provider.dart'; import 'package:cwtch/torstatus.dart'; @@ -14,12 +15,14 @@ class CwtchNotifier { late Settings settings; late ErrorHandler error; late TorStatus torStatus; + late NotificationsManager notificationManager; - CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN) { + CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP) { profileCN = pcn; settings = settingsCN; error = errorCN; torStatus = torStatusCN; + notificationManager = notificationManagerP; } void handleMessage(String type, dynamic data) { @@ -60,6 +63,7 @@ class CwtchNotifier { } break; case "NewMessageFromPeer": + notificationManager.notify("New Message From Peer!"); profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).unreadMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now()); diff --git a/lib/main.dart b/lib/main.dart index d7d8ed4..c4cafc8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/notification_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:cwtch/cwtch/ffi.dart'; import 'package:cwtch/cwtch/gomobile.dart'; @@ -50,11 +51,15 @@ class FlwtchState extends State { cwtchInit = false; profs = ProfileListState(); - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus); if (Platform.isAndroid) { + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager()); cwtch = CwtchGomobile(cwtchNotifier); + } else if (Platform.isLinux) { + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager()); + cwtch = CwtchFfi(cwtchNotifier); } else { + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager()); cwtch = CwtchFfi(cwtchNotifier); } @@ -78,7 +83,13 @@ class FlwtchState extends State { //appStatus = AppModel(cwtch: cwtch); return MultiProvider( - providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider()], + providers: [ + getFlwtchStateProvider(), + getProfileListProvider(), + getSettingsProvider(), + getErrorHandlerProvider(), + getTorStatusProvider(), + ], builder: (context, widget) { Provider.of(context).initPackageInfo(); return Consumer( diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart new file mode 100644 index 0000000..9cddae0 --- /dev/null +++ b/lib/notification_manager.dart @@ -0,0 +1,26 @@ +import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:path/path.dart' as path; + +// NotificationsManager provides a wrapper around platform specific notifications logic. +abstract class NotificationsManager { + Future notify(String message); +} + +// 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; + LinuxNotificationsManager() {} + Future notify(String message) async { + var client = NotificationsClient(); + var icon_path = Uri.file(path.join(path.current, "cwtch.png")); + client.notify('New Message from Peer!', appName: "cwtch", appIcon: icon_path.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id); + client.close(); + } +} diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 65169c8..26e6cd8 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -31,48 +31,50 @@ class _ContactsViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Row(children: [ - ProfileImage( - imagePath: Provider.of(context).imagePath, - diameter: 42, - border: Provider.of(context).theme.portraitOnlineBorderColor(), - badgeTextColor: Colors.red, - badgeColor: Colors.red, - ), - SizedBox( - width: 10, - ), - Expanded( - child: Text( - "%1 » %2".replaceAll("%1", Provider.of(context).nickname).replaceAll("%2", "Contacts"), - overflow: TextOverflow.ellipsis, - )), //todo - ]), - actions: [ - IconButton(icon: TorIcon(), onPressed: _pushTorStatus), - IconButton( - icon: Icon(Icons.copy), - onPressed: _copyOnion, - ), - IconButton( - // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset - icon: Icon(showSearchBar || Provider.of(context).isFiltered ? Icons.search_off : Icons.search), - onPressed: () { - Provider.of(context, listen: false).filter = ""; - setState(() { - showSearchBar = !showSearchBar; - }); - }) - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: _pushAddContact, - tooltip: AppLocalizations.of(context)!.tooltipAddContact, - child: const Icon(Icons.person_add_sharp), - ), - body: showSearchBar || Provider.of(context).isFiltered ? _buildFilterable() : _buildContactList(), - ); + endDrawerEnableOpenDragGesture: false, + drawerEnableOpenDragGesture: false, + appBar: AppBar( + title: RepaintBoundary( + child: Row(children: [ + ProfileImage( + imagePath: Provider.of(context).imagePath, + diameter: 42, + border: Provider.of(context).theme.portraitOnlineBorderColor(), + badgeTextColor: Colors.red, + badgeColor: Colors.red, + ), + SizedBox( + width: 10, + ), + Expanded( + child: Text( + "%1 » %2".replaceAll("%1", Provider.of(context).nickname).replaceAll("%2", "Contacts"), + overflow: TextOverflow.ellipsis, + )), //todo + ])), + actions: [ + IconButton(icon: TorIcon(), onPressed: _pushTorStatus), + IconButton( + icon: Icon(Icons.copy), + onPressed: _copyOnion, + ), + IconButton( + // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset + icon: Icon(showSearchBar || Provider.of(context).isFiltered ? Icons.search_off : Icons.search), + onPressed: () { + Provider.of(context, listen: false).filter = ""; + setState(() { + showSearchBar = !showSearchBar; + }); + }) + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _pushAddContact, + tooltip: AppLocalizations.of(context)!.tooltipAddContact, + child: const Icon(Icons.person_add_sharp), + ), + body: showSearchBar || Provider.of(context).isFiltered ? _buildFilterable() : _buildContactList()); } Widget _buildFilterable() { @@ -89,13 +91,13 @@ class _ContactsViewState extends State { Widget _buildContactList() { final tiles = Provider.of(context).contacts.map((ContactInfoState contact) { - return ChangeNotifierProvider.value(key: ValueKey(contact.profileOnion + "" + contact.onion), value: contact, builder: (_, __) => ContactRow()); + return ChangeNotifierProvider.value(key: ValueKey(contact.profileOnion + "" + contact.onion), value: contact, builder: (_, __) => RepaintBoundary(child: ContactRow())); }); final divided = ListTile.divideTiles( context: context, tiles: tiles, ).toList(); - return ListView(children: divided); + return RepaintBoundary(child: ListView(children: divided)); } void _pushAddContact() { diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index 0abcc29..a45edfe 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -113,42 +113,43 @@ class _ProfileMgrViewState extends State { showModalBottomSheet( context: context, builder: (BuildContext context) { - return Container( - height: 200, // bespoke value courtesy of the [TextField] docs - child: Center( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context)!.enterProfilePassword), - SizedBox( - height: 20, - ), - CwtchPasswordField( - controller: ctrlrPassword, - validator: (value) {}, - ), - SizedBox( - height: 20, - ), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Spacer(), - Expanded( - child: ElevatedButton( - child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), - onPressed: () { - Provider.of(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text); - ctrlrPassword.text = ""; - Navigator.pop(context); - }, - )), - Spacer() - ]), - ], - )), - )); + return RepaintBoundary( + child: Container( + height: 200, // bespoke value courtesy of the [TextField] docs + child: Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)!.enterProfilePassword), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrPassword, + validator: (value) {}, + ), + SizedBox( + height: 20, + ), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + Spacer(), + Expanded( + child: ElevatedButton( + child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), + onPressed: () { + Provider.of(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text); + ctrlrPassword.text = ""; + Navigator.pop(context); + }, + )), + Spacer() + ]), + ], + )), + ))); }); } @@ -157,7 +158,7 @@ class _ProfileMgrViewState extends State { (ProfileInfoState profile) { return ChangeNotifierProvider.value( value: profile, - builder: (context, child) => ProfileRow(), + builder: (context, child) => RepaintBoundary(child: ProfileRow()), ); }, ); diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 96c8143..bc7e24c 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -71,26 +71,27 @@ class MessageBubbleState extends State { return LayoutBuilder(builder: (context, constraints) { //print(constraints.toString()+", "+constraints.maxWidth.toString()); - return Container( + return RepaintBoundary( child: Container( - decoration: BoxDecoration( - color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), - border: - Border.all(color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), width: 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(borderRadiousEh), - topRight: Radius.circular(borderRadiousEh), - bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, - bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), - ), - ), - child: Padding( - padding: EdgeInsets.all(9.0), - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))); + child: Container( + decoration: BoxDecoration( + color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), + border: Border.all( + color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor(), width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Padding( + padding: EdgeInsets.all(9.0), + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); }); } } diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index aae7741..fc5b728 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -15,41 +15,42 @@ class _MessageListState extends State { @override Widget build(BuildContext outerContext) { - return Container( - child: Scrollbar( - isAlwaysShown: true, - controller: ctrlr1, - child: Container( - // Only show broken heart is the contact is offline... - decoration: BoxDecoration( - image: Provider.of(outerContext).isOnline() - ? null - : DecorationImage( - fit: BoxFit.contain, - image: AssetImage("assets/core/negative_heart_512px.png"), - colorFilter: ColorFilter.mode(Provider.of(context).theme.mainTextColor(), BlendMode.srcIn))), - child: ListView.builder( + return RepaintBoundary( + child: Container( + child: Scrollbar( + isAlwaysShown: true, controller: ctrlr1, - itemCount: Provider.of(outerContext).totalMessages, - reverse: true, - itemBuilder: (itemBuilderContext, index) { - var trueIndex = Provider.of(outerContext).totalMessages - index - 1; - return ChangeNotifierProvider( - key: ValueKey(trueIndex), - create: (x) => MessageState( - context: itemBuilderContext, - profileOnion: Provider.of(outerContext, listen: false).onion, - contactHandle: Provider.of(x, listen: false).onion, - messageIndex: trueIndex, - ), - builder: (bcontext, child) { - String idx = Provider.of(outerContext).isGroup == true && Provider.of(bcontext).signature.isEmpty == false - ? Provider.of(bcontext).signature - : trueIndex.toString(); - return MessageRow(key: Provider.of(bcontext).getMessageKey(idx)); - }); - }, - ), - ))); + child: Container( + // Only show broken heart is the contact is offline... + decoration: BoxDecoration( + image: Provider.of(outerContext).isOnline() + ? null + : DecorationImage( + fit: BoxFit.contain, + image: AssetImage("assets/core/negative_heart_512px.png"), + colorFilter: ColorFilter.mode(Provider.of(context).theme.mainTextColor(), BlendMode.srcIn))), + child: ListView.builder( + controller: ctrlr1, + itemCount: Provider.of(outerContext).totalMessages, + reverse: true, + itemBuilder: (itemBuilderContext, index) { + var trueIndex = Provider.of(outerContext).totalMessages - index - 1; + return ChangeNotifierProvider( + key: ValueKey(trueIndex), + create: (x) => MessageState( + context: itemBuilderContext, + profileOnion: Provider.of(outerContext, listen: false).onion, + contactHandle: Provider.of(x, listen: false).onion, + messageIndex: trueIndex, + ), + builder: (bcontext, child) { + String idx = Provider.of(outerContext).isGroup == true && Provider.of(bcontext).signature.isEmpty == false + ? Provider.of(bcontext).signature + : trueIndex.toString(); + return RepaintBoundary(child: MessageRow(key: Provider.of(bcontext).getMessageKey(idx))); + }); + }, + ), + )))); } } diff --git a/lib/widgets/profileimage.dart b/lib/widgets/profileimage.dart index 3ed1a4b..b558e70 100644 --- a/lib/widgets/profileimage.dart +++ b/lib/widgets/profileimage.dart @@ -21,7 +21,8 @@ class ProfileImage extends StatefulWidget { class _ProfileImageState extends State { @override Widget build(BuildContext context) { - return Stack(children: [ + return RepaintBoundary( + child: Stack(children: [ ClipOval( clipBehavior: Clip.antiAlias, child: Container( @@ -57,6 +58,6 @@ class _ProfileImageState extends State { child: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)), ), )), - ]); + ])); } } diff --git a/lib/widgets/tor_icon.dart b/lib/widgets/tor_icon.dart index b86d6e1..eb0c2bf 100644 --- a/lib/widgets/tor_icon.dart +++ b/lib/widgets/tor_icon.dart @@ -15,7 +15,8 @@ class TorIcon extends StatefulWidget { class _TorIconState extends State { @override Widget build(BuildContext context) { - return Image( + return RepaintBoundary( + child: Image( image: AssetImage(Provider.of(context).progress == 0 ? "assets/core/Tor_OFF.png" : (Provider.of(context).progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_Booting_up.png")), @@ -25,6 +26,6 @@ class _TorIconState extends State { semanticLabel: Provider.of(context).progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : (Provider.of(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor), - ); + )); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d38195a..99da0ef 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -4,6 +4,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) window_size_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); + window_size_plugin_register_with_registrar(window_size_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 51436ae..9e12128 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + window_size ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 54950a0..7aeaf06 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.2" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" async: dependency: transitive description: @@ -64,6 +71,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + desktop_notifications: + dependency: "direct main" + description: + name: desktop_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" fake_async: dependency: transitive description: @@ -269,6 +290,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" platform: dependency: transitive description: @@ -386,6 +414,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + window_size: + dependency: "direct main" + description: + path: "plugins/window_size" + ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 + resolved-ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 + url: "git://github.com/google/flutter-desktop-embedding.git" + source: git + version: "0.1.0" xdg_directories: dependency: transitive description: @@ -393,6 +430,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" sdks: dart: ">=2.13.0 <3.0.0" flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7b71879..bfea36d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: cupertino_icons: ^1.0.0 ffi: ^1.0.0 path_provider: ^2.0.0 + desktop_notifications: 0.5.0 glob: any # todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829 @@ -44,6 +45,12 @@ dependencies: flutter_driver: sdk: flutter + window_size: + git: + url: git://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: e48abe7c3e9ebfe0b81622167c5201d4e783bb81 + #dev_dependencies: # flutter_lokalise: any