diff --git a/lib/features/composer/domain/exceptions/compose_email_exception.dart b/lib/features/composer/domain/exceptions/compose_email_exception.dart index b0d29ed9d2..f821d91b30 100644 --- a/lib/features/composer/domain/exceptions/compose_email_exception.dart +++ b/lib/features/composer/domain/exceptions/compose_email_exception.dart @@ -1,3 +1,7 @@ class SendingEmailCanceledException implements Exception {} -class SavingEmailToDraftsCanceledException implements Exception {} \ No newline at end of file +class SavingEmailToDraftsCanceledException implements Exception {} + +class SendingEmailTimeoutException implements Exception {} + +class SavingEmailToDraftsTimeoutException implements Exception {} \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index dd7649a7d2..ab2369ee5a 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; @@ -32,6 +34,7 @@ class CreateNewAndSaveEmailToDraftsInteractor { Stream> execute({ required CreateEmailRequest createEmailRequest, CancelToken? cancelToken, + Duration? timeout, }) async* { try { yield dartz.Right(GenerateEmailLoading()); @@ -43,45 +46,47 @@ class CreateNewAndSaveEmailToDraftsInteractor { final emailCreated = await _createEmailObject(createEmailRequest); - if (emailCreated != null) { - if (createEmailRequest.draftsEmailId == null) { - yield dartz.Right(SaveEmailAsDraftsLoading()); - - final emailDraftSaved = await _emailRepository.saveEmailAsDrafts( - createEmailRequest.session, - createEmailRequest.accountId, - emailCreated, - cancelToken: cancelToken - ); - - yield dartz.Right( - SaveEmailAsDraftsSuccess( - emailDraftSaved.id!, - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2 - ) - ); - } else { - yield dartz.Right(UpdatingEmailDrafts()); - - final emailDraftSaved = await _emailRepository.updateEmailDrafts( - createEmailRequest.session, - createEmailRequest.accountId, - emailCreated, - createEmailRequest.draftsEmailId!, - cancelToken: cancelToken - ); - - yield dartz.Right( - UpdateEmailDraftsSuccess( - emailDraftSaved.id!, - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2 - ) - ); - } - } else { + if (emailCreated == null) { yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + return; + } + + if (createEmailRequest.draftsEmailId == null) { + yield dartz.Right(SaveEmailAsDraftsLoading()); + + final emailDraftSaved = await _emailRepository.saveEmailAsDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + cancelToken: cancelToken, + timeout: timeout, + ); + + yield dartz.Right( + SaveEmailAsDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); + } else { + yield dartz.Right(UpdatingEmailDrafts()); + + final emailDraftSaved = await _emailRepository.updateEmailDrafts( + createEmailRequest.session, + createEmailRequest.accountId, + emailCreated, + createEmailRequest.draftsEmailId!, + cancelToken: cancelToken + ); + + yield dartz.Right( + UpdateEmailDraftsSuccess( + emailDraftSaved.id!, + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2 + ) + ); } } catch (e) { logError('CreateNewAndSaveEmailToDraftsInteractor::execute: Exception: $e'); @@ -91,6 +96,10 @@ class CreateNewAndSaveEmailToDraftsInteractor { } else { yield dartz.Left(UpdateEmailDraftsFailure(SavingEmailToDraftsCanceledException())); } + } else if (e is TimeoutException) { + yield dartz.Left(createEmailRequest.draftsEmailId == null + ? SaveEmailAsDraftsFailure(SavingEmailToDraftsTimeoutException()) + : UpdateEmailDraftsFailure(SavingEmailToDraftsTimeoutException())); } else { if (createEmailRequest.draftsEmailId == null) { yield dartz.Left(SaveEmailAsDraftsFailure(e)); diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart index cbff76b4db..015d5fef96 100644 --- a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; @@ -32,7 +34,8 @@ class CreateNewAndSendEmailInteractor { Stream> execute({ required CreateEmailRequest createEmailRequest, - CancelToken? cancelToken + CancelToken? cancelToken, + Duration? timeout, }) async* { SendingEmailArguments? sendingEmailArguments; try { @@ -45,36 +48,37 @@ class CreateNewAndSendEmailInteractor { sendingEmailArguments = await _createEmailObject(createEmailRequest); - if (sendingEmailArguments != null) { - yield dartz.Right(SendEmailLoading()); + if (sendingEmailArguments == null) { + yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); + return; + } - await _emailRepository.sendEmail( - sendingEmailArguments.session, - sendingEmailArguments.accountId, - sendingEmailArguments.emailRequest, - mailboxRequest: sendingEmailArguments.mailboxRequest, - cancelToken: cancelToken - ); + yield dartz.Right(SendEmailLoading()); - if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { - await _deleteOldDraftsEmail( - session: sendingEmailArguments.session, - accountId: sendingEmailArguments.accountId, - draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed!, - cancelToken: cancelToken - ); - } - - yield dartz.Right( - SendEmailSuccess( - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2, - emailRequest: sendingEmailArguments.emailRequest - ) + await _emailRepository.sendEmail( + sendingEmailArguments.session, + sendingEmailArguments.accountId, + sendingEmailArguments.emailRequest, + mailboxRequest: sendingEmailArguments.mailboxRequest, + cancelToken: cancelToken, + timeout: timeout, + ); + + if (sendingEmailArguments.emailRequest.emailIdDestroyed != null) { + await _deleteOldDraftsEmail( + session: sendingEmailArguments.session, + accountId: sendingEmailArguments.accountId, + draftEmailId: sendingEmailArguments.emailRequest.emailIdDestroyed!, ); - } else { - yield dartz.Left(GenerateEmailFailure(CannotCreateEmailObjectException())); } + + yield dartz.Right( + SendEmailSuccess( + currentMailboxState: listCurrentState?.value1, + currentEmailState: listCurrentState?.value2, + emailRequest: sendingEmailArguments.emailRequest + ) + ); } catch (e) { logError('CreateNewAndSendEmailInteractor::execute: Exception: $e'); if (e is UnknownError && e.message is List) { @@ -85,6 +89,14 @@ class CreateNewAndSendEmailInteractor { emailRequest: sendingEmailArguments?.emailRequest, mailboxRequest: sendingEmailArguments?.mailboxRequest, )); + } else if (e is TimeoutException) { + yield dartz.Left(SendEmailFailure( + exception: SendingEmailTimeoutException(), + session: sendingEmailArguments?.session, + accountId: sendingEmailArguments?.accountId, + emailRequest: sendingEmailArguments?.emailRequest, + mailboxRequest: sendingEmailArguments?.mailboxRequest, + )); } else { yield dartz.Left(SendEmailFailure( exception: e, @@ -132,14 +144,12 @@ class CreateNewAndSendEmailInteractor { required Session session, required AccountId accountId, required EmailId draftEmailId, - CancelToken? cancelToken }) async { try { await _emailRepository.deleteEmailPermanently( session, accountId, draftEmailId, - cancelToken: cancelToken ); } catch (e) { logError('CreateNewAndSendEmailInteractor::_deleteOldDraftsEmail: Exception: $e'); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index b3ec9a78df..e43cb623dc 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -6,7 +6,6 @@ import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; @@ -104,6 +103,7 @@ import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class ComposerController extends BaseController with DragDropFileMixin, AutoCompleteResultMixin @@ -915,7 +915,10 @@ class ComposerController extends BaseController showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_a_subject, AppLocalizations.of(context).send_anyway, - onConfirmAction: () => _handleSendMessages(context), + onConfirmAction: () => _handleSendMessages( + context: context, + timeout: AppConfig.sendingMessageTimeout + ), onCancelAction: popBack, autoPerformPopBack: false, title: AppLocalizations.of(context).empty_subject, @@ -951,7 +954,10 @@ class ComposerController extends BaseController return; } - _handleSendMessages(context); + _handleSendMessages( + context: context, + timeout: AppConfig.sendingMessageTimeout + ); } Future _getContentInEditor() async { @@ -965,7 +971,10 @@ class ComposerController extends BaseController } } - void _handleSendMessages(BuildContext context) async { + void _handleSendMessages({ + required BuildContext context, + Duration? timeout, + }) async { if (composerArguments.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null @@ -981,10 +990,10 @@ class ComposerController extends BaseController } final emailContent = await _getContentInEditor(); - final cancelToken = CancelToken(); + final resultState = await _showSendingMessageDialog( emailContent: emailContent, - cancelToken: cancelToken + timeout: timeout ); log('ComposerController::_handleSendMessages: resultState = $resultState'); if (resultState is SendEmailSuccess || mailboxDashBoardController.validateSendingEmailFailedWhenNetworkIsLostOnMobile(resultState)) { @@ -992,6 +1001,11 @@ class ComposerController extends BaseController _closeComposerAction(result: resultState); } else if (resultState is SendEmailFailure && resultState.exception is SendingEmailCanceledException) { _sendButtonState = ButtonState.enabled; + } else if (resultState is SendEmailFailure + && resultState.exception is SendingEmailTimeoutException + && context.mounted + ) { + await _showWarningDialogWhenSendEmailTimeout(context); } else if ((resultState is SendEmailFailure || resultState is GenerateEmailFailure) && context.mounted) { await _showConfirmDialogWhenSendMessageFailure( context: context, @@ -1004,7 +1018,7 @@ class ComposerController extends BaseController Future _showSendingMessageDialog({ required String emailContent, - CancelToken? cancelToken + Duration? timeout, }) { final childWidget = PointerInterceptor( child: SendingMessageDialogView( @@ -1033,8 +1047,7 @@ class ComposerController extends BaseController displayMode: screenDisplayMode.value ), createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, - onCancelSendingEmailAction: _handleCancelSendingMessage, - cancelToken: cancelToken, + timeout: timeout, ), ); @@ -1047,10 +1060,6 @@ class ComposerController extends BaseController ); } - void _handleCancelSendingMessage({CancelToken? cancelToken}) { - cancelToken?.cancel([SendingEmailCanceledException()]); - } - Future _showConfirmDialogWhenSendMessageFailure({ required BuildContext context, required FeatureFailure failure @@ -1094,6 +1103,46 @@ class ComposerController extends BaseController ); } + Future _showWarningDialogWhenSendEmailTimeout(context) async { + await showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSendEmailTimeout, + AppLocalizations.of(context).resend, + cancelTitle: AppLocalizations.of(context).closeAnyway, + alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, + onConfirmAction: () { + _sendButtonState = ButtonState.enabled; + popBack(); + _handleSendMessages(context: context); + }, + onCancelAction: () { + _sendButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); + } + void _checkContactPermission() async { final permissionStatus = await Permission.contacts.status; if (permissionStatus.isGranted) { @@ -1251,7 +1300,10 @@ class ComposerController extends BaseController _savedEmailDraftHash = await _hashDraftEmail(); } - void handleClickSaveAsDraftsButton(BuildContext context) async { + void handleClickSaveAsDraftsButton({ + required BuildContext context, + Duration? timeout, + }) async { if (_saveToDraftButtonState == ButtonState.disabled) { log('ComposerController::handleClickSaveAsDraftsButton: Saving to draft'); return; @@ -1270,11 +1322,10 @@ class ComposerController extends BaseController } final emailContent = await _getContentInEditor(); - final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( emailContent: emailContent, draftEmailId: _emailIdEditing, - cancelToken: cancelToken + timeout: timeout ); if (resultState is SaveEmailAsDraftsSuccess) { @@ -1290,6 +1341,22 @@ class ComposerController extends BaseController } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _saveToDraftButtonState = ButtonState.enabled; + } else if (_validateDisplayWarningDialogSaveAsDraftTimeout(resultState) + && context.mounted + ) { + await _showWarningDialogWhenSaveMessageToDraftsTimeout( + context: context, + onConfirmAction: () { + _saveToDraftButtonState = ButtonState.enabled; + popBack(); + handleClickSaveAsDraftsButton(context: context); + }, + onCancelAction: () { + _saveToDraftButtonState = ButtonState.enabled; + popBack(); + _autoFocusFieldWhenLauncher(); + } + ); } else if ((resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure) && @@ -1313,6 +1380,11 @@ class ComposerController extends BaseController } } + bool _validateDisplayWarningDialogSaveAsDraftTimeout(dynamic failure) { + return (failure is SaveEmailAsDraftsFailure && failure.exception is SavingEmailToDraftsTimeoutException) + || (failure is UpdateEmailDraftsFailure && failure.exception is SavingEmailToDraftsTimeoutException); + } + void _addAttachmentFromFileShare(List listSharedMediaFile) { final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); @@ -2113,7 +2185,10 @@ class ComposerController extends BaseController titleActionButtonMaxLines: 1, isArrangeActionButtonsVertical: true, usePopScope: true, - onConfirmAction: () => _handleSaveMessageToDraft(context), + onConfirmAction: () => _handleSaveMessageToDraft( + context: context, + timeout: AppConfig.savingMessageTimeout + ), onCancelAction: () { _closeComposerButtonState = ButtonState.enabled; _closeComposerAction(closeOverlays: true); @@ -2196,7 +2271,10 @@ class ComposerController extends BaseController ); } - void _handleSaveMessageToDraft(BuildContext context) async { + void _handleSaveMessageToDraft({ + required BuildContext context, + Duration? timeout, + }) async { if (composerArguments.value == null || mailboxDashBoardController.sessionCurrent == null || mailboxDashBoardController.accountId.value == null || @@ -2213,11 +2291,10 @@ class ComposerController extends BaseController final emailContent = await _getContentInEditor(); final draftEmailId = _getDraftEmailId(); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); - final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( emailContent: emailContent, draftEmailId: draftEmailId, - cancelToken: cancelToken + timeout: timeout ); if (resultState is SaveEmailAsDraftsSuccess || resultState is UpdateEmailDraftsSuccess) { @@ -2226,6 +2303,10 @@ class ComposerController extends BaseController } else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) || (resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) { _closeComposerButtonState = ButtonState.enabled; + } else if (_validateDisplayWarningDialogSaveAsDraftTimeout(resultState) + && context.mounted + ) { + await _showWarningDialogWhenSaveMessageToDraftsTimeout(context: context); } else if ((resultState is SaveEmailAsDraftsFailure || resultState is UpdateEmailDraftsFailure || resultState is GenerateEmailFailure) && @@ -2254,7 +2335,7 @@ class ComposerController extends BaseController Future _showSavingMessageToDraftsDialog({ required String emailContent, EmailId? draftEmailId, - CancelToken? cancelToken, + Duration? timeout, }) { final childWidget = PointerInterceptor( child: SavingMessageDialogView( @@ -2283,8 +2364,7 @@ class ComposerController extends BaseController displayMode: screenDisplayMode.value ), createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, - onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, - cancelToken: cancelToken, + timeout: timeout ), ); return Get.dialog( @@ -2296,10 +2376,6 @@ class ComposerController extends BaseController ); } - void _handleCancelSavingMessageToDrafts({CancelToken? cancelToken}) { - cancelToken?.cancel([SavingEmailToDraftsCanceledException()]); - } - Future _showConfirmDialogWhenSaveMessageToDraftsFailure({ required BuildContext context, required FeatureFailure failure, @@ -2345,6 +2421,51 @@ class ComposerController extends BaseController ); } + Future _showWarningDialogWhenSaveMessageToDraftsTimeout({ + required BuildContext context, + VoidCallback? onConfirmAction, + VoidCallback? onCancelAction, + }) async { + await showConfirmDialogAction( + context, + title: '', + AppLocalizations.of(context).warningMessageWhenSaveEmailToDraftsTimeout, + AppLocalizations.of(context).saveAgain, + cancelTitle: onConfirmAction != null + ? AppLocalizations.of(context).cancel + : AppLocalizations.of(context).closeAnyway, + alignCenter: true, + outsideDismissible: false, + autoPerformPopBack: false, + onConfirmAction: onConfirmAction ?? () { + _closeComposerButtonState = ButtonState.enabled; + _handleSaveMessageToDraft(context: context); + }, + onCancelAction: onCancelAction ?? () { + _closeComposerButtonState = ButtonState.enabled; + _closeComposerAction(closeOverlays: true); + }, + icon: SvgPicture.asset( + imagePaths.icQuotasWarning, + width: 40, + height: 40, + colorFilter: AppColor.colorBackgroundQuotasWarning.asFilter(), + ), + messageStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 14, + color: AppColor.colorTextBody + ), + actionStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.white + ), + cancelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( + fontSize: 17, + color: Colors.black + ) + ); + } + void handleEnableRecipientsInputAction(bool isEnabled) { fromRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 47c42d6123..541a0eae7f 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -26,6 +26,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_com import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class ComposerView extends GetWidget { @@ -384,7 +385,10 @@ class ComposerView extends GetWidget { Obx(() => TabletBottomBarComposerWidget( hasReadReceipt: controller.hasRequestReadReceipt.value, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton( + context: context, + timeout: AppConfig.savingMessageTimeout + ), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), )), @@ -449,7 +453,10 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.handleClickSaveAsDraftsButton(context); + controller.handleClickSaveAsDraftsButton( + context: context, + timeout: AppConfig.savingMessageTimeout + ); } ) ), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 6621e9f5f3..a1dcb3230c 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -26,6 +26,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_ import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class ComposerView extends GetWidget { @@ -499,7 +500,10 @@ class ComposerView extends GetWidget { insertImageAction: () => controller.insertImage(context, constraints.maxWidth), showCodeViewAction: controller.richTextWebController!.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton( + context: context, + timeout: AppConfig.savingMessageTimeout + ), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), )), @@ -769,7 +773,10 @@ class ComposerView extends GetWidget { insertImageAction: () => controller.insertImage(context, constraints.maxWidth), showCodeViewAction: controller.richTextWebController!.toggleCodeView, deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), + saveToDraftAction: () => controller.handleClickSaveAsDraftsButton( + context: context, + timeout: AppConfig.savingMessageTimeout + ), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), )), @@ -876,7 +883,10 @@ class ComposerView extends GetWidget { padding: ComposerStyle.popupItemPadding, onCallbackAction: () { popBack(); - controller.handleClickSaveAsDraftsButton(context); + controller.handleClickSaveAsDraftsButton( + context: context, + timeout: AppConfig.savingMessageTimeout + ); } ) ), diff --git a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart index 04a94b804c..1586b9bada 100644 --- a/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/saving_message_dialog_view.dart @@ -29,6 +29,7 @@ class SavingMessageDialogView extends StatefulWidget { final CreateNewAndSaveEmailToDraftsInteractor createNewAndSaveEmailToDraftsInteractor; final OnCancelSavingEmailToDraftsAction? onCancelSavingEmailToDraftsAction; final CancelToken? cancelToken; + final Duration? timeout; const SavingMessageDialogView({ super.key, @@ -36,6 +37,7 @@ class SavingMessageDialogView extends StatefulWidget { required this.createNewAndSaveEmailToDraftsInteractor, this.onCancelSavingEmailToDraftsAction, this.cancelToken, + this.timeout, }); @override @@ -53,7 +55,8 @@ class _SavingMessageDialogViewState extends State { _streamSubscription = widget.createNewAndSaveEmailToDraftsInteractor .execute( createEmailRequest: widget.createEmailRequest, - cancelToken: widget.cancelToken + cancelToken: widget.cancelToken, + timeout: widget.timeout ) .listen( _handleDataStream, @@ -84,6 +87,8 @@ class _SavingMessageDialogViewState extends State { logError('_SavingMessageDialogViewState::_handleErrorStream: Exception = $error'); if (error is UnknownError && error.message is List) { popBack(result: SaveEmailAsDraftsFailure(SavingEmailToDraftsCanceledException())); + } else if (error is TimeoutException) { + popBack(result: SaveEmailAsDraftsFailure(SavingEmailToDraftsTimeoutException())); } else { popBack(result: SaveEmailAsDraftsFailure(error)); } diff --git a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart index 774b7600c6..1ff680e726 100644 --- a/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart +++ b/lib/features/composer/presentation/widgets/sending_message_dialog_view.dart @@ -28,6 +28,7 @@ class SendingMessageDialogView extends StatefulWidget { final CreateNewAndSendEmailInteractor createNewAndSendEmailInteractor; final OnCancelSendingEmailAction? onCancelSendingEmailAction; final CancelToken? cancelToken; + final Duration? timeout; const SendingMessageDialogView({ super.key, @@ -35,6 +36,7 @@ class SendingMessageDialogView extends StatefulWidget { required this.createNewAndSendEmailInteractor, this.onCancelSendingEmailAction, this.cancelToken, + this.timeout, }); @override @@ -52,7 +54,8 @@ class _SendingMessageDialogViewState extends State { _streamSubscription = widget.createNewAndSendEmailInteractor .execute( createEmailRequest: widget.createEmailRequest, - cancelToken: widget.cancelToken + cancelToken: widget.cancelToken, + timeout: widget.timeout, ) .listen( _handleDataStream, @@ -81,6 +84,8 @@ class _SendingMessageDialogViewState extends State { logError('_SendingMessageDialogViewState::_handleErrorStream: Exception = $error'); if (error is UnknownError && error.message is List) { popBack(result: SendEmailFailure(exception: SendingEmailCanceledException())); + } else if (error is TimeoutException) { + popBack(result: SendEmailFailure(exception: SendingEmailTimeoutException())); } else { popBack(result: SendEmailFailure(exception: error)); } diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 2cfa7a9b44..201733567a 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -39,7 +39,8 @@ abstract class EmailDataSource { EmailRequest emailRequest, { CreateNewMailboxRequest? mailboxRequest, - CancelToken? cancelToken + CancelToken? cancelToken, + Duration? timeout, } ); @@ -83,14 +84,16 @@ abstract class EmailDataSource { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ); Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ); Future updateEmailDrafts( @@ -98,7 +101,10 @@ abstract class EmailDataSource { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); @@ -107,7 +113,6 @@ abstract class EmailDataSource { Session session, AccountId accountId, EmailId emailId, - {CancelToken? cancelToken} ); Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index c64e3ea668..bae9867118 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -59,6 +59,7 @@ class EmailDataSourceImpl extends EmailDataSource { { CreateNewMailboxRequest? mailboxRequest, CancelToken? cancelToken, + Duration? timeout, } ) async { try { @@ -67,7 +68,8 @@ class EmailDataSourceImpl extends EmailDataSource { accountId, emailRequest, mailboxRequest: mailboxRequest, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout, ); } catch (error, stackTrace) { return await _sendEmailExceptionThrower.throwException(error, stackTrace); @@ -130,14 +132,18 @@ class EmailDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { return Future.sync(() async { return await emailAPI.saveEmailAsDrafts( session, accountId, email, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout ); }).catchError(_exceptionThrower.throwException); } @@ -146,15 +152,13 @@ class EmailDataSourceImpl extends EmailDataSource { Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ) { return Future.sync(() async { return await emailAPI.removeEmailDrafts( session, accountId, - emailId, - cancelToken: cancelToken + emailId ); }).catchError(_exceptionThrower.throwException); } @@ -165,7 +169,10 @@ class EmailDataSourceImpl extends EmailDataSource { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { return Future.sync(() async { return await emailAPI.updateEmailDrafts( @@ -173,7 +180,8 @@ class EmailDataSourceImpl extends EmailDataSource { accountId, newEmail, oldEmailId, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout ); }).catchError(_exceptionThrower.throwException); } @@ -212,14 +220,12 @@ class EmailDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, EmailId emailId, - {CancelToken? cancelToken} ) { return Future.sync(() async { return await emailAPI.deleteEmailPermanently( session, accountId, emailId, - cancelToken: cancelToken ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index e866992c15..06582f8155 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -138,8 +138,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ) { throw UnimplementedError(); } @@ -149,7 +148,10 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { throw UnimplementedError(); } @@ -161,7 +163,8 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { EmailRequest emailRequest, { CreateNewMailboxRequest? mailboxRequest, - CancelToken? cancelToken + CancelToken? cancelToken, + Duration? timeout } ) { throw UnimplementedError(); @@ -199,7 +202,10 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 4ff1280058..6a95b201db 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -128,6 +128,7 @@ class EmailAPI with HandleSetErrorMixin { { CreateNewMailboxRequest? mailboxRequest, CancelToken? cancelToken, + Duration? timeout, } ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -207,11 +208,15 @@ class EmailAPI with HandleSetErrorMixin { final capabilities = setEmailSubmissionMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); - final response = await (requestBuilder + final futureResponse = (requestBuilder ..usings(capabilities)) .build() .execute(cancelToken: cancelToken); + final response = timeout != null + ? await futureResponse.timeout(timeout) + : await futureResponse; + final setEmailResponse = response.parse( setEmailInvocation.methodCallId, SetEmailResponse.deserialize); @@ -491,7 +496,10 @@ class EmailAPI with HandleSetErrorMixin { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout, + } ) async { final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) @@ -504,11 +512,15 @@ class EmailAPI with HandleSetErrorMixin { final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); - final response = await (requestBuilder + final futureResponse = (requestBuilder ..usings(capabilities)) .build() .execute(cancelToken: cancelToken); + final response = timeout != null + ? await futureResponse.timeout(timeout) + : await futureResponse; + final setEmailResponse = response.parse( setEmailInvocation.methodCallId, SetEmailResponse.deserialize @@ -527,8 +539,7 @@ class EmailAPI with HandleSetErrorMixin { Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ) async { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -543,7 +554,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(cancelToken: cancelToken); + .execute(); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, @@ -564,21 +575,24 @@ class EmailAPI with HandleSetErrorMixin { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout, + } ) async { final emailCreated = await saveEmailAsDrafts( session, accountId, newEmail, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout ); try { await removeEmailDrafts( session, accountId, - oldEmailId, - cancelToken: cancelToken + oldEmailId ); } catch (e) { logError('EmailAPI::updateEmailDrafts: Exception = $e'); @@ -623,7 +637,6 @@ class EmailAPI with HandleSetErrorMixin { Session session, AccountId accountId, EmailId emailId, - {CancelToken? cancelToken} ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailMethod = SetEmailMethod(accountId) @@ -637,7 +650,7 @@ class EmailAPI with HandleSetErrorMixin { final response = await (requestBuilder ..usings(capabilities)) .build() - .execute(cancelToken: cancelToken); + .execute(); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 57860f7ef3..80b062053f 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -70,7 +70,8 @@ class EmailRepositoryImpl extends EmailRepository { EmailRequest emailRequest, { CreateNewMailboxRequest? mailboxRequest, - CancelToken? cancelToken + CancelToken? cancelToken, + Duration? timeout } ) { return emailDataSource[DataSourceType.network]!.sendEmail( @@ -79,6 +80,7 @@ class EmailRepositoryImpl extends EmailRepository { emailRequest, mailboxRequest: mailboxRequest, cancelToken: cancelToken, + timeout: timeout, ); } @@ -155,13 +157,17 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( session, accountId, email, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout ); } @@ -169,14 +175,12 @@ class EmailRepositoryImpl extends EmailRepository { Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ) { return emailDataSource[DataSourceType.network]!.removeEmailDrafts( session, accountId, - emailId, - cancelToken: cancelToken + emailId ); } @@ -186,14 +190,18 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ) { return emailDataSource[DataSourceType.network]!.updateEmailDrafts( session, accountId, newEmail, oldEmailId, - cancelToken: cancelToken + cancelToken: cancelToken, + timeout: timeout ); } @@ -227,13 +235,11 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, EmailId emailId, - {CancelToken? cancelToken} ) { return emailDataSource[DataSourceType.network]!.deleteEmailPermanently( session, accountId, emailId, - cancelToken: cancelToken ); } diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 88ae516c27..481e98b057 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -41,7 +41,8 @@ abstract class EmailRepository { EmailRequest emailRequest, { CreateNewMailboxRequest? mailboxRequest, - CancelToken? cancelToken + CancelToken? cancelToken, + Duration? timeout, } ); @@ -91,14 +92,16 @@ abstract class EmailRepository { Session session, AccountId accountId, Email email, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ); Future removeEmailDrafts( Session session, AccountId accountId, - EmailId emailId, - {CancelToken? cancelToken} + EmailId emailId ); Future updateEmailDrafts( @@ -106,7 +109,10 @@ abstract class EmailRepository { AccountId accountId, Email newEmail, EmailId oldEmailId, - {CancelToken? cancelToken} + { + CancelToken? cancelToken, + Duration? timeout + } ); Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); @@ -115,7 +121,6 @@ abstract class EmailRepository { Session session, AccountId accountId, EmailId emailId, - {CancelToken? cancelToken} ); Future getEmailState(Session session, AccountId accountId); diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index cc68d12fd7..b6713490d9 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-09-18T01:15:29.034686", + "@@last_modified": "2024-10-01T18:18:47.787144", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4023,5 +4023,23 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "warningMessageWhenSendEmailTimeout": "The message sending time has exceeded the timeout of one minute. Do you want to resend?", + "@warningMessageWhenSendEmailTimeout": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "warningMessageWhenSaveEmailToDraftsTimeout": "The time it takes to save the message to the draft folder has exceeded the one minute timeout. Do you want to save it again?", + "@warningMessageWhenSaveEmailToDraftsTimeout": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveAgain": "Save again", + "@saveAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/exceptions/remote_exception.dart b/lib/main/exceptions/remote_exception.dart index abd77dd09e..946a94e6f7 100644 --- a/lib/main/exceptions/remote_exception.dart +++ b/lib/main/exceptions/remote_exception.dart @@ -10,6 +10,8 @@ abstract class RemoteException with EquatableMixin implements Exception { static const noNetworkError = 'No network error'; static const badCredentials = 'Bad credentials'; static const socketException = 'Socket exception'; + static const sendTimeout = 'Send data timeout'; + static const receiveTimeout = 'Receive data timeout'; final Object? message; final int? code; diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index b818e230e9..9a5af779d2 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4219,4 +4219,23 @@ class AppLocalizations { 'Find emails', name: 'findEmails'); } + + String get warningMessageWhenSendEmailTimeout { + return Intl.message( + 'The message sending time has exceeded the timeout of one minute. Do you want to resend?', + name: 'warningMessageWhenSendEmailTimeout'); + } + + String get warningMessageWhenSaveEmailToDraftsTimeout { + return Intl.message( + 'The time it takes to save the message to the draft folder has exceeded the one minute timeout. Do you want to save it again?', + name: 'warningMessageWhenSaveEmailToDraftsTimeout'); + } + + String get saveAgain { + return Intl.message( + 'Save again', + name: 'saveAgain', + ); + } } \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index c03faff1f0..3725547a87 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -16,6 +16,9 @@ class AppConfig { static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions'; static const String saasPlatform = 'saas'; + static const Duration sendingMessageTimeout = Duration(minutes: 1); + static const Duration savingMessageTimeout = Duration(minutes: 1); + static String get baseUrl => dotenv.get('SERVER_URL', fallback: ''); static String get domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: ''); static String get webOidcClientId => dotenv.get('WEB_OIDC_CLIENT_ID', fallback: ''); diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index 17a66d7fea..7b5d9c96fe 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -278,7 +278,7 @@ void main() { composerController = null; }); - group('ComposerController test:', () { + group('ComposerController::test:', () { group('hash draft email test:', () { const emailContent = 'some email content'; const emailSubject = 'some email subject'; @@ -581,8 +581,10 @@ void main() { when( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ).thenAnswer((_) => Stream.value( Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); final savedEmailDraft = SavedEmailDraft( @@ -611,7 +613,10 @@ void main() { await untilCalled( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ); // assert expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); @@ -652,8 +657,10 @@ void main() { when( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ).thenAnswer((_) => Stream.value( Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); final savedEmailDraft = SavedEmailDraft( @@ -682,7 +689,10 @@ void main() { await untilCalled( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ); // assert expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); @@ -1029,8 +1039,10 @@ void main() { when( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ).thenAnswer((_) => Stream.value( Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); final savedEmailDraft = SavedEmailDraft( @@ -1059,7 +1071,10 @@ void main() { await untilCalled( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ); // assert expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode); @@ -1101,8 +1116,10 @@ void main() { when( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))) - .thenAnswer((_) => Stream.value( + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ).thenAnswer((_) => Stream.value( Right(UpdateEmailDraftsSuccess(EmailId(Id('123')))))); final savedEmailDraft = SavedEmailDraft( @@ -1131,7 +1148,10 @@ void main() { await untilCalled( mockCreateNewAndSaveEmailToDraftsInteractor.execute( createEmailRequest: anyNamed('createEmailRequest'), - cancelToken: anyNamed('cancelToken'))); + cancelToken: anyNamed('cancelToken'), + timeout: anyNamed('timeout'), + ) + ); // assert expect(composerController?.savedEmailDraftHash, savedEmailDraft.hashCode);