diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index fee09984..7b9668f6 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -8,7 +8,6 @@ import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/remoteserver.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/notification_manager.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:cwtch/torstatus.dart'; @@ -150,10 +149,9 @@ class CwtchNotifier { var senderImage = data['Picture']; var isAuto = data['Auto'] == "true"; String? contenthash = data['ContentHash']; - var selectedProfile = appState.selectedProfile == data["ProfileOnion"]; - var selectedConversation = selectedProfile && appState.selectedConversation == identifier; + var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; - profileCN.getProfile(data["ProfileOnion"])?.newMessage( + profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage( identifier, messageID, timestamp, @@ -162,10 +160,9 @@ class CwtchNotifier { isAuto, data["Data"], contenthash, - selectedProfile, selectedConversation, ); - appState.notifyProfileUnread(); + break; case "PeerAcknowledgement": // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... @@ -205,8 +202,7 @@ class CwtchNotifier { var currentTotal = contact!.totalMessages; var isAuto = data['Auto'] == "true"; String? contenthash = data['ContentHash']; - var selectedProfile = appState.selectedProfile == data["ProfileOnion"]; - var selectedConversation = selectedProfile && appState.selectedConversation == identifier; + var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { @@ -220,10 +216,9 @@ class CwtchNotifier { // For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts // and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time` // and `local now`. - profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation); + profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedConversation); notificationManager.notify("New Message From Group!"); - appState.notifyProfileUnread(); } RemoteServerInfoState? server = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(contact.server ?? ""); server?.updateSyncProgressFor(timestampSent); diff --git a/lib/main.dart b/lib/main.dart index c9e06c77..f6174247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -45,6 +45,10 @@ class Flwtch extends StatefulWidget { @override FlwtchState createState() => FlwtchState(); + + String yay() { + return "yay!"; + } } class FlwtchState extends State with WindowListener { diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 9d78dcbb..5a2293c3 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/widgets.dart'; enum ModalState { none, storageMigration } @@ -18,13 +16,6 @@ class AppState extends ChangeNotifier { bool _disableFilePicker = false; bool _focus = true; - StreamController _profilesUnreadNotifyControler = StreamController(); - late Stream profilesUnreadNotify; - - AppState() { - profilesUnreadNotify = _profilesUnreadNotifyControler.stream.asBroadcastStream(); - } - void SetCwtchInit() { cwtchInit = true; notifyListeners(); @@ -91,12 +82,4 @@ class AppState extends ChangeNotifier { } bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; - - void notifyProfileUnread() { - _profilesUnreadNotifyControler.add(true); - } - - Stream getUnreadProfileNotifyStream() { - return profilesUnreadNotify; - } } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index ee10ec47..ba618f8b 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -120,14 +120,10 @@ class ProfileInfoState extends ChangeNotifier { notifyListeners(); } - void recountUnread() { - this._unreadMessages = _contacts.contacts.fold(0, (i, c) => i + c.unreadMessages); - } - // Remove a contact from a list. Currently only used when rejecting a group invitation. // Eventually will also be used for other removals. - void removeContact(String handle) { - this.contactList.removeContactByHandle(handle); + void removeContact(int handle) { + this.contactList.removeContact(handle); notifyListeners(); } @@ -172,30 +168,11 @@ class ProfileInfoState extends ChangeNotifier { lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), )); } - unreadMessages += int.parse(contact["numUnread"]); }); } this._contacts.resort(); } - void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) { - if (!selectedProfile) { - unreadMessages++; - notifyListeners(); - } - - contactList.newMessage( - identifier, - messageID, - timestamp, - senderHandle, - senderImage, - isAuto, - data, - contenthash, - selectedConversation); - } - void downloadInit(String fileKey, int numChunks) { this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); notifyListeners(); diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart index 8cae18b5..1e0eb3e2 100644 --- a/lib/models/profilelist.dart +++ b/lib/models/profilelist.dart @@ -27,8 +27,4 @@ class ProfileListState extends ChangeNotifier { _profiles.removeWhere((element) => element.onion == onion); notifyListeners(); } - - int generateUnreadCount(String selectedProfile) => _profiles.where( (p) => p.onion != selectedProfile ).fold(0, (i, p) => i + p.unreadMessages); - - } diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index 91d6ada8..91f1cb52 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -1,8 +1,7 @@ -import 'dart:async'; import 'dart:io'; import 'package:cwtch/main.dart'; -import 'package:win_toast/win_toast.dart'; +import 'package:desktoasts/desktoasts.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:path/path.dart' as path; @@ -38,27 +37,44 @@ class LinuxNotificationsManager implements NotificationsManager { // Windows Notification Manager uses https://pub.dev/packages/desktoasts to implement // windows notifications class WindowsNotificationManager implements NotificationsManager { + late ToastService service; bool active = false; - bool initialized = false; WindowsNotificationManager() { - scheduleMicrotask(() async { - initialized = await WinToast.instance().initialize(appName: 'cwtch', productName: 'Cwtch', companyName: 'Open Privacy Research Society'); + service = new ToastService( + appName: 'cwtch', + companyName: 'Open Privacy Research Society', + productName: 'Cwtch', + ); + + service.stream.listen((event) { + // the user closed the notification of the OS timed it out + if (event is ToastDismissed) { + active = false; + } + // clicked + if (event is ToastActivated) { + active = false; + } + // if a supplied action was clicked + if (event is ToastInteracted) { + active = false; + } }); } Future notify(String message) async { - if (initialized && !globalAppState.focus) { + if (!globalAppState.focus) { if (!active) { + // One string of bold text on the first line (title), + // one string (subtitle) of regular text wrapped across the second and third lines. + Toast toast = new Toast( + type: ToastType.text02, + title: 'Cwtch', + subtitle: message, + ); + service.show(toast); active = true; - WinToast.instance().clear(); - final toast = await WinToast.instance().showToast(type: ToastType.text01, title: message); - toast?.eventStream.listen((event) { - if (event is ActivatedEvent) { - WinToast.instance().bringWindowToFront(); - } - active = false; - }); } } } diff --git a/lib/themes/cwtch.dart b/lib/themes/cwtch.dart index 5d0eb3e1..e4a660c5 100644 --- a/lib/themes/cwtch.dart +++ b/lib/themes/cwtch.dart @@ -68,8 +68,8 @@ class CwtchDark extends OpaqueThemeType { get portraitBlockedTextColor => lightGrey; get portraitContactBadgeColor => hotPink; get portraitContactBadgeTextColor => whiteishPurple; - get portraitProfileBadgeColor => hotPink; - get portraitProfileBadgeTextColor => whiteishPurple; + get portraitProfileBadgeColor => mauvePurple; + get portraitProfileBadgeTextColor => darkGreyPurple; get dropShadowColor => mauvePurple; get toolbarIconColor => settings; //whiteishPurple; get messageFromMeBackgroundColor => userBubble; // mauvePurple; @@ -112,7 +112,7 @@ class CwtchLight extends OpaqueThemeType { get portraitBlockedTextColor => softGrey; get portraitContactBadgeColor => accent; get portraitContactBadgeTextColor => whitePurple; - get portraitProfileBadgeColor => accent; + get portraitProfileBadgeColor => brightPurple; get portraitProfileBadgeTextColor => whitePurple; get dropShadowColor => purple; get toolbarIconColor => settings; //darkPurple; diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index 49edc309..3cee78c1 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -138,7 +138,6 @@ class _AddContactViewState extends State { height: 20, ), CwtchTextField( - testKey: Key("txtAddP2P"), controller: ctrlrContact, validator: (value) { if (value == "") { diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index d48ffada..9bc2f28d 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -76,31 +76,6 @@ class _ContactsViewState extends State { endDrawerEnableOpenDragGesture: false, drawerEnableOpenDragGesture: false, appBar: AppBar( - leading: Row(children: [ - IconButton( - icon: Icon(Icons.arrow_back), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - onPressed: () { - Provider.of(context, listen: false).recountUnread(); - Provider.of(context, listen: false).selectedProfile = ""; - Navigator.of(context).pop(); - }, - ), - - StreamBuilder( - stream: Provider.of(context).getUnreadProfileNotifyStream(), - builder: (BuildContext context, AsyncSnapshot unreadCountSnapshot) { - int unreadCount = Provider.of(context).generateUnreadCount(Provider.of(context).selectedProfile ?? "") ; - - return Visibility( - visible: unreadCount > 0, - child: CircleAvatar( - radius: 10.0, - backgroundColor: Provider.of(context).theme.portraitProfileBadgeColor, - child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)), - )); - }), - ]), title: RepaintBoundary( child: Row(children: [ ProfileImage( diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 71e8a5a3..5b733587 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -107,7 +107,7 @@ class _GlobalSettingsViewState extends State { items: themes.keys.map>((String themeId) { return DropdownMenuItem( value: themeId, - child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")), + child: Text("ddi_$themeId", key: Key("ddi_$themeId")), //getThemeName(context, themeId)), ); }).toList()), leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor), diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 18855e7e..d64c4907 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -151,7 +151,7 @@ class _GroupSettingsViewState extends State { Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog }); }, - icon: Icon(CwtchIcons.leave_chat), + icon: Icon(Icons.archive), label: Text(AppLocalizations.of(context)!.archiveConversation), )), SizedBox( @@ -198,8 +198,6 @@ class _GroupSettingsViewState extends State { onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; var identifier = Provider.of(context, listen: false).identifier; - // locally update cache... - Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).contactList.removeContact(identifier); Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, identifier); Future.delayed(Duration(milliseconds: 500), () { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 8db678ff..8d060cd3 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -167,7 +167,7 @@ class _MessageViewState extends State { ); } else { return MultiProvider( - providers: [ChangeNotifierProvider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider.value(value: Provider.of(context)), ChangeNotifierProvider.value(value: Provider.of(context))], child: PeerSettingsView(), ); } diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 0ab762ee..405a6e76 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/services.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; @@ -197,30 +198,46 @@ class _PeerSettingsViewState extends State { ); }).toList())), ]), - Column(mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ + SizedBox( + height: 20, + ), + Tooltip( + message: AppLocalizations.of(context)!.archiveConversation, + child: ElevatedButton.icon( + onPressed: () { + var profileOnion = Provider.of(context, listen: false).profileOnion; + var handle = Provider.of(context, listen: false).identifier; + // locally update cache... + Provider.of(context, listen: false).isArchived = true; + Provider.of(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); + Future.delayed(Duration(milliseconds: 500), () { + Provider.of(context, listen: false).selectedConversation = null; + Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog + }); + }, + icon: Icon(Icons.archive), + label: Text(AppLocalizations.of(context)!.archiveConversation), + )), SizedBox( height: 20, ), Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ Tooltip( - message: AppLocalizations.of(context)!.archiveConversation, - child: ElevatedButton.icon( + message: AppLocalizations.of(context)!.leaveGroup, + child: TextButton.icon( onPressed: () { - var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).identifier; - // locally update cache... - Provider.of(context, listen: false).isArchived = true; - Provider.of(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); - Future.delayed(Duration(milliseconds: 500), () { - Provider.of(context, listen: false).selectedConversation = null; - Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog - }); + showAlertDialog(context); }, - icon: Icon(CwtchIcons.leave_chat), - label: Text(AppLocalizations.of(context)!.archiveConversation), + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)), + icon: Icon(CwtchIcons.leave_group), + label: Text( + AppLocalizations.of(context)!.leaveGroup, + style: TextStyle(decoration: TextDecoration.underline), + ), )) ]) - ]), + ]) ]))))); }); }); @@ -247,8 +264,8 @@ class _PeerSettingsViewState extends State { onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; var handle = Provider.of(context, listen: false).identifier; + Provider.of(context).removeContact(handle); // locally update cache... - Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Future.delayed(Duration(milliseconds: 500), () { Provider.of(context, listen: false).selectedConversation = null; diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index cdad4f77..246917db 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -136,7 +136,7 @@ class _ContactRowState extends State { ContactInfoState contact = Provider.of(context, listen: false); if (contact.isGroup == true) { // FIXME This flow is incorrect. Groups never just show up on the contact list anymore - Provider.of(context, listen: false).removeContact(contact.onion); + Provider.of(context, listen: false).removeContact(contact.identifier); } else { Provider.of(context, listen: false).cwtch.BlockContact(Provider.of(context, listen: false).profileOnion, contact.identifier); } diff --git a/lib/widgets/folderpicker.dart b/lib/widgets/folderpicker.dart index 2b1f586e..90268052 100644 --- a/lib/widgets/folderpicker.dart +++ b/lib/widgets/folderpicker.dart @@ -64,7 +64,6 @@ class _CwtchFolderPickerState extends State { print(e); } }, - onChanged: widget.onSave, icon: Icon(Icons.folder), tooltip: widget.tooltip, ))); diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 770dbe54..1ab6e273 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -1,7 +1,6 @@ import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/contactlist.dart'; import 'package:cwtch/models/profile.dart'; -import 'package:cwtch/models/profilelist.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:cwtch/views/addeditprofileview.dart'; @@ -34,7 +33,7 @@ class _ProfileRowState extends State { Padding( padding: const EdgeInsets.all(6.0), //border size child: ProfileImage( - badgeCount: profile.unreadMessages, + badgeCount: 0, badgeColor: Provider.of(context).theme.portraitProfileBadgeColor, badgeTextColor: Provider.of(context).theme.portraitProfileBadgeTextColor, diameter: 64.0, diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index 793077ff..7545618b 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -8,7 +8,7 @@ doNothing(String x) {} // Provides a styled Text Field for use in Form Widgets. // Callers must provide a text controller, label helper text and a validator. class CwtchTextField extends StatefulWidget { - CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key, this.testKey}); + CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key}); final TextEditingController controller; final String hintText; final FormFieldValidator? validator; @@ -17,7 +17,6 @@ class CwtchTextField extends StatefulWidget { final bool multiLine; final bool number; final Key? key; - final Key? testKey; @override _CwtchTextFieldState createState() => _CwtchTextFieldState(); @@ -42,7 +41,6 @@ class _CwtchTextFieldState extends State { Widget build(BuildContext context) { return Consumer(builder: (context, theme, child) { return TextFormField( - key: widget.testKey, controller: widget.controller, validator: widget.validator, onChanged: widget.onChanged,