diff --git a/assets/images/ic_delete_dialog_failed.svg b/assets/images/ic_delete_dialog_failed.svg new file mode 100644 index 000000000..9ce053a48 --- /dev/null +++ b/assets/images/ic_delete_dialog_failed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_delete_dialog_identity.svg b/assets/images/ic_delete_dialog_identity.svg new file mode 100644 index 000000000..f98ff95b7 --- /dev/null +++ b/assets/images/ic_delete_dialog_identity.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index c2e1bc36c..aad3162b7 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -112,6 +112,7 @@ extension AppColor on Color { static const colorLoading = Color(0x2999A2AD); static const colorActionButtonHover = Color(0xFFA2AAB3); static const colorBgMenuItemDropDownSelected = Color(0x80DEE2E7); + static const colorButtonCancelDialog = Color(0x0D000000); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 364c684f1..040e18258 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -101,6 +101,8 @@ class ImagePaths { String get icEmail => _getImagePath('ic_email.svg'); String get icReplyTo => _getImagePath('ic_reply_to.svg'); String get icEditIdentity => _getImagePath('ic_edit_identity.svg'); + String get icDeleteDialogIdentity => _getImagePath('ic_delete_dialog_identity.svg'); + String get icDeleteDialogFailed => _getImagePath('ic_delete_dialog_failed.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/base/mixin/popup_menu_widget_mixin.dart b/lib/features/base/mixin/popup_menu_widget_mixin.dart new file mode 100644 index 000000000..4afa1c7cf --- /dev/null +++ b/lib/features/base/mixin/popup_menu_widget_mixin.dart @@ -0,0 +1,30 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +mixin PopupMenuWidgetMixin { + + Widget popupItem(String iconAction, String nameAction, { + Color? colorIcon, + TextStyle? styleName, + Function? onCallbackAction + }) { + return InkWell( + onTap: () => onCallbackAction?.call(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: SizedBox( + child: Row(children: [ + SvgPicture.asset(iconAction, width: 20, height: 20, fit: BoxFit.fill, color: colorIcon), + const SizedBox(width: 12), + Expanded(child: Text(nameAction, + style: styleName ?? const TextStyle( + fontSize: 17, + fontWeight: FontWeight.normal, + color: Colors.black))), + ]) + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_bindings.dart b/lib/features/manage_account/presentation/profiles/identities/identities_bindings.dart index 76e1291b6..30c476e74 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_bindings.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_bindings.dart @@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/manage_account/data/datasource_impl/manag import 'package:tmail_ui_user/features/manage_account/data/network/manage_account_api.dart'; import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/delete_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart'; @@ -19,7 +20,10 @@ class IdentitiesBindings extends BaseBindings { @override void bindingsController() { - Get.lazyPut(() => IdentitiesController(Get.find())); + Get.lazyPut(() => IdentitiesController( + Get.find(), + Get.find(), + )); } @override @@ -35,6 +39,7 @@ class IdentitiesBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => GetAllIdentitiesInteractor(Get.find())); + Get.lazyPut(() => DeleteIdentityInteractor(Get.find())); } @override diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart index 3730a8b8e..c2b55c459 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart @@ -1,23 +1,36 @@ import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/delete_identity_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/delete_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class IdentitiesController extends BaseController { final _accountDashBoardController = Get.find(); + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + final _appToast = Get.find(); final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; + final DeleteIdentityInteractor _deleteIdentityInteractor; final identitySelected = Rxn(); final listIdentities = [].obs; - IdentitiesController(this._getAllIdentitiesInteractor); + IdentitiesController( + this._getAllIdentitiesInteractor, + this._deleteIdentityInteractor, + ); @override void onInit() { @@ -28,13 +41,19 @@ class IdentitiesController extends BaseController { @override void onDone() { viewState.value.fold( - (failure) {}, + (failure) { + if (failure is DeleteIdentityFailure) { + _deleteIdentityFailure(failure); + } + }, (success) { if (success is GetAllIdentitiesSuccess) { if (success.identities?.isNotEmpty == true) { listIdentities.value = success.identities ?? []; setIdentityDefault(); } + } else if (success is DeleteIdentitySuccess) { + _deleteIdentitySuccess(success); } } ); @@ -69,4 +88,99 @@ class IdentitiesController extends BaseController { void selectIdentity(Identity? newIdentity) { identitySelected.value = newIdentity; } + + void openConfirmationDialogDeleteIdentityAction(BuildContext context, Identity identity) { + popBack(); + + if (_responsiveUtils.isScreenWithShortestSide(context)) { + (ConfirmationDialogActionSheetBuilder(context) + ..messageText(AppLocalizations.of(context).message_confirmation_dialog_delete_identity) + ..onCancelAction(AppLocalizations.of(context).cancel, () => + popBack()) + ..onConfirmAction(AppLocalizations.of(context).delete, () => + _deleteIdentityAction(identity))) + .show(); + } else { + showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (BuildContext context) => (ConfirmDialogBuilder(_imagePaths) + ..key(const Key('confirm_dialog_delete_identity')) + ..title(AppLocalizations.of(context).delete_identity.capitalizeFirstEach) + ..content(AppLocalizations.of(context).message_confirmation_dialog_delete_identity) + ..addIcon(SvgPicture.asset(_imagePaths.icDeleteDialogIdentity, fit: BoxFit.fill)) + ..marginIcon(EdgeInsets.zero) + ..styleTitle(const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColor.colorActionDeleteConfirmDialog)) + ..styleContent(const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail)) + ..colorCancelButton(AppColor.colorButtonCancelDialog) + ..colorConfirmButton(AppColor.colorActionDeleteConfirmDialog) + ..styleTextConfirmButton(const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.white)) + ..styleTextCancelButton(const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorTextButton)) + ..onCloseButtonAction(() => popBack()) + ..onConfirmButtonAction(AppLocalizations.of(context).delete, () => + _deleteIdentityAction(identity)) + ..onCancelButtonAction(AppLocalizations.of(context).cancel, () => + popBack())) + .build()); + } + } + + void _deleteIdentityAction(Identity identity) { + popBack(); + + final accountId = _accountDashBoardController.accountId.value; + if (accountId != null && identity.id != null) { + consumeState(_deleteIdentityInteractor.execute(accountId, identity.id!)); + } + } + + void _deleteIdentitySuccess(DeleteIdentitySuccess success) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastWithIcon( + currentOverlayContext!, + message: AppLocalizations.of(currentContext!).identity_has_been_deleted, + icon: _imagePaths.icDeleteToast); + } + + final accountId = _accountDashBoardController.accountId.value; + if (accountId != null) { + _getAllIdentities(accountId); + } + } + + void _deleteIdentityFailure(DeleteIdentityFailure failure) { + if (currentContext != null) { + showDialog( + context: currentContext!, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (BuildContext context) => (ConfirmDialogBuilder(_imagePaths) + ..key(const Key('dialog_message_delete_identity_failed')) + ..title(AppLocalizations.of(context).delete_failed) + ..addIcon(SvgPicture.asset(_imagePaths.icDeleteDialogFailed, fit: BoxFit.fill)) + ..marginIcon(EdgeInsets.zero) + ..styleTitle(const TextStyle(fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black)) + ..colorConfirmButton(AppColor.colorTextButton) + ..paddingTitle(const EdgeInsets.symmetric(vertical: 12)) + ..styleTextConfirmButton(const TextStyle(fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.white)) + ..onCloseButtonAction(() => popBack()) + ..onConfirmButtonAction('${AppLocalizations.of(context).got_it}!', () => popBack())) + .build()); + } + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_view.dart b/lib/features/manage_account/presentation/profiles/identities/identities_view.dart index c2cfd8c8b..cae7efe4c 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_view.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_view.dart @@ -5,11 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identity_bottom_sheet_action_tile_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class IdentitiesView extends GetWidget { +class IdentitiesView extends GetWidget with PopupMenuWidgetMixin { final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); @@ -87,9 +89,10 @@ class IdentitiesView extends GetWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ buttonSelectIdentity, - Obx(() => IdentityInfoTileBuilder(context, - _imagePaths, - controller.identitySelected.value)), + Obx(() => IdentityInfoTileBuilder(_imagePaths, _responsiveUtils, + controller.identitySelected.value, + onMenuItemIdentityAction: (identity, position) => + _openIdentityMenuAction(context, identity, position))), ] )) ), @@ -108,13 +111,63 @@ class IdentitiesView extends GetWidget { SizedBox( width: constraints.maxWidth / 2, child: buttonSelectIdentity), - Obx(() => IdentityInfoTileBuilder(context, - _imagePaths, + Obx(() => IdentityInfoTileBuilder(_imagePaths, _responsiveUtils, controller.identitySelected.value, - maxWidth: constraints.maxWidth / 2)), + maxWidth: constraints.maxWidth / 2, + onMenuItemIdentityAction: (identity, position) => + _openIdentityMenuAction(context, identity, position))), ] )) ) )); } + + void _openIdentityMenuAction(BuildContext context, Identity identity, + RelativeRect? position) { + if (_responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction(context, _bottomSheetIdentityActionTiles(context, identity)); + } else { + controller.openPopupMenuAction(context, position, _popupMenuIdentityActionTiles(context, identity)); + } + } + + List _popupMenuIdentityActionTiles(BuildContext context, Identity identity) { + return [ + if (identity.mayDelete == true) + PopupMenuItem( + padding: EdgeInsets.zero, + child: popupItem(_imagePaths.icDeleteComposer, + AppLocalizations.of(context).delete_identity, + colorIcon: AppColor.colorActionDeleteConfirmDialog, + styleName: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 17, + color: AppColor.colorActionDeleteConfirmDialog), + onCallbackAction: () => controller.openConfirmationDialogDeleteIdentityAction(context, identity))), + ]; + } + + List _bottomSheetIdentityActionTiles(BuildContext context, Identity identity) { + return [ + if (identity.mayDelete == true) + _deleteIdentityActionTile(context, identity), + ]; + } + + Widget _deleteIdentityActionTile(BuildContext context, Identity identity) { + return (IdentityBottomSheetActionTileBuilder( + const Key('delete_identity_action'), + SvgPicture.asset(_imagePaths.icDeleteComposer, + color: AppColor.colorActionDeleteConfirmDialog), + AppLocalizations.of(context).delete_identity, + identity, + iconLeftPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(left: 12, right: 16) + : const EdgeInsets.only(right: 12), + iconRightPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(right: 12) + : EdgeInsets.zero) + ..onActionClick((identity) => controller.openConfirmationDialogDeleteIdentityAction(context, identity))) + .build(); + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/identity_bottom_sheet_action_tile_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_bottom_sheet_action_tile_builder.dart new file mode 100644 index 000000000..7b41110f9 --- /dev/null +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_bottom_sheet_action_tile_builder.dart @@ -0,0 +1,49 @@ + +import 'package:core/core.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; + +class IdentityBottomSheetActionTileBuilder extends CupertinoActionSheetActionBuilder { + + final Identity identity; + final SvgPicture? actionSelected; + final Color? bgColor; + final EdgeInsets? iconLeftPadding; + final EdgeInsets? iconRightPadding; + + IdentityBottomSheetActionTileBuilder( + Key key, + SvgPicture actionIcon, + String actionName, + this.identity, + { + this.actionSelected, + this.bgColor, + this.iconLeftPadding, + this.iconRightPadding, + } + ) : super(key, actionIcon, actionName); + + @override + Widget build() { + return Container( + color: bgColor ?? Colors.white, + child: CupertinoActionSheetAction( + key: key, + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Padding( + padding: iconLeftPadding ?? const EdgeInsets.only(left: 12, right: 16), + child: actionIcon), + Expanded(child: Text(actionName, textAlign: TextAlign.left, style: actionTextStyle())), + ]), + onPressed: () { + if (onCupertinoActionSheetActionClick != null) { + onCupertinoActionSheetActionClick!(identity); + } + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart index cabe6e3bd..dfa50b5b2 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_info_tile_builder.dart @@ -5,18 +5,25 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +typedef OnMenuItemIdentityAction = Function(Identity identity, RelativeRect? positon); + class IdentityInfoTileBuilder extends StatelessWidget { - final BuildContext _context; final ImagePaths _imagePaths; + final ResponsiveUtils _responsiveUtils; final Identity? _identity; final double? maxWidth; + final OnMenuItemIdentityAction? onMenuItemIdentityAction; const IdentityInfoTileBuilder( - this._context, this._imagePaths, + this._responsiveUtils, this._identity, - {Key? key, this.maxWidth} + { + Key? key, + this.maxWidth, + this.onMenuItemIdentityAction, + } ) : super(key: key); @override @@ -40,9 +47,19 @@ class IdentityInfoTileBuilder extends StatelessWidget { child: Row(children: [ Expanded(child: Text(_identity?.name ?? '', style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17, color: Colors.black))), - buildIconWeb( + buildIconWebHasPosition( + context, icon: SvgPicture.asset(_imagePaths.icMoreVertical, fit: BoxFit.fill), - onTap: () => {}) + onTap: () { + if (_responsiveUtils.isScreenWithShortestSide(context)) { + onMenuItemIdentityAction?.call(_identity!, null); + } + }, + onTapDown: (position) { + if (!_responsiveUtils.isScreenWithShortestSide(context)) { + onMenuItemIdentityAction?.call(_identity!, position); + } + }) ])), const SizedBox(height: 10), const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), @@ -75,24 +92,6 @@ class IdentityInfoTileBuilder extends StatelessWidget { Expanded(child: Text(_identity?.replyTo?.listEmailAddressToString(isFullEmailAddress: true) ?? '', style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: AppColor.colorHintSearchBar))) ])), - if (_identity?.cc != null && _identity?.cc?.isNotEmpty == true) const SizedBox(height: 6), - if (_identity?.cc != null && _identity?.cc?.isNotEmpty == true) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row(children: [ - Container( - width: 40, - padding: const EdgeInsets.only(left: 10), - child: Text(AppLocalizations.of(_context).cc_email_address_prefix, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - decoration: TextDecoration.underline, - color: AppColor.colorTextButton))), - const SizedBox(width: 4), - Expanded(child: Text(_identity?.cc?.listEmailAddressToString(isFullEmailAddress: true) ?? '', - style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: AppColor.colorHintSearchBar))) - ])), if (_identity?.bcc != null && _identity?.bcc?.isNotEmpty == true) const SizedBox(height: 6), if (_identity?.bcc != null && _identity?.bcc?.isNotEmpty == true) Padding( @@ -101,7 +100,7 @@ class IdentityInfoTileBuilder extends StatelessWidget { Container( padding: const EdgeInsets.only(left: 6), width: 40, - child: Text(AppLocalizations.of(_context).bcc_email_address_prefix, + child: Text(AppLocalizations.of(context).bcc_email_address_prefix, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.normal, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 77ddd7388..4ceb2ac70 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-04-26T09:31:06.288489", + "@@last_modified": "2022-05-05T15:05:07.334710", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -1125,5 +1125,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "delete_identity": "Delete identity", + "@delete_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_confirmation_dialog_delete_identity": "Are you sure you want to delete this identity?", + "@message_confirmation_dialog_delete_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identity_has_been_deleted": "Identity has been deleted", + "@identity_has_been_deleted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_failed": "Delete Failed", + "@delete_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 2d44dd108..b97787b61 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1178,4 +1178,24 @@ class AppLocalizations { return Intl.message('New Identity', name: 'new_identity'); } + + String get delete_identity { + return Intl.message('Delete identity', + name: 'delete_identity'); + } + + String get message_confirmation_dialog_delete_identity { + return Intl.message('Are you sure you want to delete this identity?', + name: 'message_confirmation_dialog_delete_identity'); + } + + String get identity_has_been_deleted { + return Intl.message('Identity has been deleted', + name: 'identity_has_been_deleted'); + } + + String get delete_failed { + return Intl.message('Delete Failed', + name: 'delete_failed'); + } } \ No newline at end of file