diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 7ab5f2c..ef78d83 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -203,11 +203,20 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val groupName = (a.get("groupname") as? String) ?: ""; Cwtch.createGroup(profile, server, groupName); } + "DeleteProfile" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("pass") as? String) ?: ""; + Cwtch.deleteProfile(profile, groupHandle); + } + "LeaveConversation" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val contactHandle = (a.get("contactHandle") as? String) ?: ""; + Cwtch.leaveConversation(profile, contactHandle); + } "LeaveGroup" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; val groupHandle = (a.get("groupHandle") as? String) ?: ""; - Log.i("FlwtchWorker.kt", "LeaveGroup: need to recompile aar and uncomment this line")//todo - //Cwtch.leaveGroup(profile, groupHandle); + Cwtch.leaveGroup(profile, groupHandle); } "RejectInvite" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 7c51a94..3e866da 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -1,3 +1,5 @@ +import 'package:flutter/src/services/text_input.dart'; + abstract class Cwtch { // ignore: non_constant_identifier_names Future Start(); @@ -10,6 +12,8 @@ abstract class Cwtch { void CreateProfile(String nick, String pass); // ignore: non_constant_identifier_names void LoadProfiles(String pass); + // ignore: non_constant_identifier_names + void DeleteProfile(String onion, String pass); // ignore: non_constant_identifier_names void ResetTor(); @@ -48,6 +52,9 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void SendInvitation(String profile, String handle, String target); + // ignore: non_constant_identifier_names + void LeaveConversation(String profile, String handle); + // ignore: non_constant_identifier_names void CreateGroup(String profile, String server, String groupName); // ignore: non_constant_identifier_names diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 950e21c..05dc60e 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -64,6 +64,14 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); } break; + case "PeerDeleted": + profileCN.delete(data["Identity"]); + // todo standarize + error.handleUpdate("deleteprofile.success"); + break; + case "DeleteContact": + profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]); + break; case "DeleteGroup": profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]); break; @@ -152,7 +160,10 @@ class CwtchNotifier { break; case "AppError": print("New App Error: $data"); - if (data["Data"] != null) { + // special case for delete error (todo: standardize cwtch errors) + if (data["Error"] == "Password did not match") { + error.handleUpdate("deleteprofile.error"); + } else if (data["Data"] != null) { error.handleUpdate(data["Data"]); } break; diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index f0c8786..c0f7225 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:io' show Platform; import 'package:cwtch/cwtch/cwtchNotifier.dart'; +import 'package:flutter/src/services/text_input.dart'; import 'package:path/path.dart' as path; import 'package:ffi/ffi.dart'; @@ -385,15 +386,26 @@ class CwtchFfi implements Cwtch { CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length); } + @override + // ignore: non_constant_identifier_names + void LeaveConversation(String profileOnion, String handle) { + var leaveConversation = library.lookup>("c_LeaveConversation"); + // ignore: non_constant_identifier_names + final LeaveConversation = leaveConversation.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = handle.toNativeUtf8(); + LeaveConversation(u1, u1.length, u2, u2.length); + } + @override // ignore: non_constant_identifier_names void LeaveGroup(String profileOnion, String groupHandle) { var leaveGroup = library.lookup>("c_LeaveGroup"); // ignore: non_constant_identifier_names - final RejectInvite = leaveGroup.asFunction(); + final LeaveGroup = leaveGroup.asFunction(); final u1 = profileOnion.toNativeUtf8(); final u2 = groupHandle.toNativeUtf8(); - RejectInvite(u1, u1.length, u2, u2.length); + LeaveGroup(u1, u1.length, u2, u2.length); } @override @@ -405,4 +417,15 @@ class CwtchFfi implements Cwtch { final utf8handle = handle.toNativeUtf8(); updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags); } + + @override + // ignore: non_constant_identifier_names + void DeleteProfile(String onion, String currentPassword) { + var deleteprofile = library.lookup>("c_DeleteProfile"); + // ignore: non_constant_identifier_names + final DeleteProfile = deleteprofile.asFunction(); + final u1 = onion.toNativeUtf8(); + final u2 = currentPassword.toNativeUtf8(); + DeleteProfile(u1, u1.length, u2, u2.length); + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index a193811..b37ade2 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -81,6 +81,11 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("LoadProfiles", {"pass": pass}); } + // ignore: non_constant_identifier_names + void DeleteProfile(String onion, String pass) { + cwtchPlatform.invokeMethod("DeleteProfile", {"onion": onion, "pass": pass}); + } + // ignore: non_constant_identifier_names Future ACNEvents() { return cwtchPlatform.invokeMethod("ACNEvents"); @@ -179,7 +184,7 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names void RejectInvite(String profileOnion, String groupHandle) { - cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "handle": groupHandle}); + cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle}); } @override @@ -190,7 +195,13 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names void LeaveGroup(String profileOnion, String groupHandle) { - cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "handle": groupHandle}); + cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "groupHandle": groupHandle}); + } + + @override + // ignore: non_constant_identifier_names + void LeaveConversation(String profileOnion, String contactHandle) { + cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": contactHandle}); } @override diff --git a/lib/errorHandler.dart b/lib/errorHandler.dart index cf26cde..4feada1 100644 --- a/lib/errorHandler.dart +++ b/lib/errorHandler.dart @@ -17,6 +17,10 @@ class ErrorHandler extends ChangeNotifier { bool importBundleError = false; bool importBundleSuccess = false; + static const String deleteProfileErrorPrefix = "deleteprofile"; + bool deleteProfileError = false; + bool deleteProfileSuccess = false; + /// Called by the event bus. handleUpdate(String error) { var parts = error.split("."); @@ -30,6 +34,9 @@ class ErrorHandler extends ChangeNotifier { case importBundleErrorPrefix: handleImportBundleError(errorType); break; + case deleteProfileErrorPrefix: + handleDeleteProfileError(errorType); + break; } notifyListeners(); @@ -69,4 +76,19 @@ class ErrorHandler extends ChangeNotifier { break; } } + + handleDeleteProfileError(String errorType) { + // Reset add contact errors + deleteProfileError = false; + deleteProfileSuccess = false; + + switch (errorType) { + case successErrorType: + deleteProfileSuccess = true; + break; + default: + deleteProfileError = true; + break; + } + } } diff --git a/lib/model.dart b/lib/model.dart index 7dec94b..db689da 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -54,6 +54,11 @@ class ProfileListState extends ChangeNotifier { int idx = _profiles.indexWhere((element) => element.onion == onion); return idx >= 0 ? _profiles[idx] : null; } + + void delete(String onion) { + _profiles.removeWhere((element) => element.onion == onion); + notifyListeners(); + } } class ContactListState extends ChangeNotifier { diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 7fcc3b4..03bafb8 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,6 +13,7 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../cwtch_icons_icons.dart'; +import '../errorHandler.dart'; import '../main.dart'; import '../opaque.dart'; import '../settings.dart'; @@ -166,6 +168,9 @@ class _AddEditProfileViewState extends State { if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { return AppLocalizations.of(context)!.passwordErrorEmpty; } + if (Provider.of(context).deleteProfileError == true) { + return AppLocalizations.of(context)!.enterCurrentPasswordForDelete; + } return null; }, ), @@ -237,11 +242,9 @@ class _AddEditProfileViewState extends State { Tooltip( message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete, child: ElevatedButton.icon( - onPressed: checkCurrentPassword() - ? null - : () { - showAlertDialog(context); - }, + onPressed: () { + showAlertDialog(context); + }, style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()), icon: Icon(Icons.delete_forever), label: Text(AppLocalizations.of(context)!.deleteBtn), @@ -307,52 +310,49 @@ class _AddEditProfileViewState extends State { } } - // TODO Stub - wire this into a libCwtch call. - bool checkCurrentPassword() { - return ctrlrOldPass.value.text.isEmpty; + showAlertDialog(BuildContext context) { + // set up the buttons + Widget cancelButton = TextButton( + child: Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); // dismiss dialog + }, + ); + Widget continueButton = TextButton( + child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), + onPressed: () { + var onion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.DeleteProfile(onion, ctrlrOldPass.value.text); + + Future.delayed( + const Duration(milliseconds: 500), + () { + if (globalErrorHandler.deleteProfileSuccess) { + final snackBar = SnackBar(content: Text("Successfully deleted profile:" + onion)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + Navigator.of(context).popUntil((route) => route.isFirst); // dismiss dialog + } else { + Navigator.of(context).pop(); + } + }, + ); + }); + + // set up the AlertDialog + AlertDialog alert = AlertDialog( + title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), + actions: [ + cancelButton, + continueButton, + ], + ); + + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); } } - -showAlertDialog(BuildContext context) { - // set up the buttons - Widget cancelButton = TextButton( - child: Text("Cancel"), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), - foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()), - overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()), - padding: MaterialStateProperty.all(EdgeInsets.all(20))), - onPressed: () { - Navigator.of(context).pop(); // dismiss dialog - }, - ); - Widget continueButton = TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), - foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()), - overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()), - padding: MaterialStateProperty.all(EdgeInsets.all(20))), - child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), - onPressed: () { - // TODO Actually Delete the Peer - Navigator.of(context).pop(); // dismiss dialog - }, - ); - - // set up the AlertDialog - AlertDialog alert = AlertDialog( - title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn), - actions: [ - cancelButton, - continueButton, - ], - ); - - // show the dialog - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - ); -} diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index b3d4488..4f68556 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -189,10 +189,12 @@ class _MessageViewState extends State { ), ElevatedButton( child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn), - onPressed: () { - this._sendInvitation(); - Navigator.pop(bcontext); - }, + onPressed: this.selectedContact == "" + ? null + : () { + this._sendInvitation(); + Navigator.pop(bcontext); + }, ), ], )), diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 7bd3cb3..608ab54 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -187,6 +187,22 @@ class _PeerSettingsViewState extends State { ); }).toList())), ]), + Column(mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ + SizedBox( + height: 20, + ), + Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ + Tooltip( + message: AppLocalizations.of(context)!.leaveGroup, + child: ElevatedButton.icon( + onPressed: () { + showAlertDialog(context); + }, + icon: Icon(CwtchIcons.leave_chat), + label: Text(AppLocalizations.of(context)!.leaveGroup), + )) + ]) + ]), ]))))); }); }); @@ -197,4 +213,44 @@ class _PeerSettingsViewState extends State { final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification)); ScaffoldMessenger.of(context).showSnackBar(snackBar); } + + showAlertDialog(BuildContext context) { + // set up the buttons + Widget cancelButton = TextButton( + child: Text("Cancel"), + style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))), + onPressed: () { + Navigator.of(context).pop(); // dismiss dialog + }, + ); + Widget continueButton = TextButton( + style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))), + child: Text(AppLocalizations.of(context)!.yesLeave), + onPressed: () { + var profileOnion = Provider.of(context, listen: false).profileOnion; + var handle = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.LeaveConversation(profileOnion, handle); + Future.delayed(Duration(milliseconds: 500), () { + Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog + }); + }, + ); + + // set up the AlertDialog + AlertDialog alert = AlertDialog( + title: Text(AppLocalizations.of(context)!.reallyLeaveThisGroupPrompt), + actions: [ + cancelButton, + continueButton, + ], + ); + + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); + } } diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 7913f38..6fdad16 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -23,10 +23,13 @@ class InvitationBubble extends StatefulWidget { class InvitationBubbleState extends State { bool rejected = false; bool isAccepted = false; - FocusNode _focus = FocusNode(); @override Widget build(BuildContext context) { + if (Provider.of(context).malformed) { + return MalformedBubble(); + } + var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; var isGroup = Provider.of(context).overlay == 101; isAccepted = Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget) != null; diff --git a/lib/widgets/passwordfield.dart b/lib/widgets/passwordfield.dart index 5b095a0..64b4404 100644 --- a/lib/widgets/passwordfield.dart +++ b/lib/widgets/passwordfield.dart @@ -33,6 +33,7 @@ class _CwtchTextFieldState extends State { controller: widget.controller, validator: widget.validator, obscureText: obscureText, + autovalidateMode: AutovalidateMode.always, onFieldSubmitted: widget.action, textInputAction: TextInputAction.unspecified, enableSuggestions: false,