{"results":{"result":{"added-files":{"code-health":9.975395482630322,"old-code-health":0.0,"files":[{"file":"drive_attachment/lib/drive_attachment/data/datasource_impl/drive_attachment_datasource_impl.dart","loc":52,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/data/model/drive_intent_response.dart","loc":40,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/data/repository_impl/drive_attachment_repository_impl.dart","loc":13,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/entity/drive_attachment.dart","loc":10,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/entity/drive_document.dart","loc":24,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/entity/drive_intent.dart","loc":8,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/message/drive_intent_message.dart","loc":47,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/state/drive_intent_state.dart","loc":27,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/usecase/create_drive_intent_interactor.dart","loc":18,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/domain/usecase/exchange_drive_token_interactor.dart","loc":18,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/presentation/mixin/drive_intent_message_handler_mixin.dart","loc":61,"code-health":9.6882083290695},{"file":"drive_attachment/lib/drive_attachment/presentation/mixin/drive_intent_shims.dart","loc":14,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/presentation/notifier/drive_attachment_notifier.dart","loc":66,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/presentation/provider/drive_access_token_notifier.dart","loc":23,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/presentation/provider/drive_attachment_providers.dart","loc":35,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/presentation/provider/workplace_fqdn_notifier.dart","loc":17,"code-health":10.0},{"file":"drive_attachment/lib/drive_attachment/data/model/drive_intent_request.dart","loc":31,"code-health":10.0},{"file":"drive_attachment/test/drive_document_extension_test.dart","loc":70,"code-health":10.0},{"file":"drive_attachment/test/drive_intent_message_test.dart","loc":98,"code-health":10.0},{"file":"drive_attachment/test/workplace_fqdn_notifier_test.dart","loc":63,"code-health":10.0},{"file":"lib/features/composer/data/service/drive_external_attachment_adapter.dart","loc":19,"code-health":10.0},{"file":"lib/features/composer/domain/service/external_attachment_service.dart","loc":19,"code-health":10.0}]},"external-review-url":"https://github.com/linagora/tmail-flutter/pull/4581","old-code-health":8.696011559044711,"modified-files":{"code-health":8.694549487624776,"old-code-health":8.696011559044711,"files":[{"file":"lib/features/base/reloadable/reloadable_controller.dart","loc":198,"old-loc":192,"code-health":9.387218218812514,"old-code-health":9.387218218812514},{"file":"lib/features/composer/presentation/composer_controller.dart","loc":2199,"old-loc":2190,"code-health":8.545379580978913,"old-code-health":8.545379580978913},{"file":"lib/features/composer/presentation/composer_view.dart","loc":565,"old-loc":557,"code-health":7.52567002238316,"old-code-health":7.554724718168824},{"file":"lib/features/composer/presentation/extensions/create_email_request_extension.dart","loc":255,"old-loc":246,"code-health":8.413528317237752,"old-code-health":8.413528317237752},{"file":"lib/features/composer/presentation/model/create_email_request.dart","loc":130,"old-loc":127,"code-health":10.0,"old-code-health":10.0},{"file":"lib/features/login/data/network/interceptors/authorization_interceptors.dart","loc":427,"old-loc":426,"code-health":10.0,"old-code-health":10.0},{"file":"lib/main/bindings/network/network_bindings.dart","loc":150,"old-loc":139,"code-health":10.0,"old-code-health":10.0}]},"removed-files":{"code-health":0.0,"old-code-health":0.0,"files":[]},"external-review-id":"4581","analysis-time":"2026-06-05T08:08:49Z","negative-impact-count":1,"suppressions":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"affected-hotspots":1,"commits":["a2ae3468121b73299dad5b64c9025bcf3df8f032","53eb3ce3f5c4ba6d96d6820c634dd53037f8d0cc","d217234153620e59c4998c8d0ed7a51c98d74926"],"is-negative-review":true,"negative-findings":{"number-of-types":1,"number-of-files-touched":1,"findings":[{"method":"onMessage","why-it-occurs":"A Complex Method has a high cyclomatic complexity. The recommended threshold for the Dart language is a cyclomatic complexity lower than 9.","name":"Complex Method","file":"drive_attachment/lib/drive_attachment/presentation/mixin/drive_intent_message_handler_mixin.dart","refactoring-examples":[{"architectural-component-id":null,"author-name":"dab246","training-data":{"loc-added":"21","loc-deleted":"12","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"","commit-date":"2026-06-10T07:41:46Z","current-rev":"d3246903b","filename":"tmail-flutter/lib/main/exceptions/thrower/dio_no_response_error_handler.dart","previous-rev":"378a5b508","commit-title":"refactor: extract _asIoConnectionError in DioNoResponseErrorHandler to reduce complexity","language":"Dart","id":"f915c9cf409f2baf699ac6c94a22dfd694df8131","model-score":0.54,"author-id":null,"project-id":75877,"delta-file-score":0.31179166,"diff":"diff --git a/lib/main/exceptions/thrower/dio_no_response_error_handler.dart b/lib/main/exceptions/thrower/dio_no_response_error_handler.dart\nindex b88865db8..176ad7fa4 100644\n--- a/lib/main/exceptions/thrower/dio_no_response_error_handler.dart\n+++ b/lib/main/exceptions/thrower/dio_no_response_error_handler.dart\n@@ -29,22 +29,31 @@ class DioNoResponseErrorHandler {\n     }\n-    if (underlyingError is HandshakeException) {\n+    if (underlyingError is OAuthAuthorizationError) {\n+      throw underlyingError;\n+    }\n+    final ioConnectionError = _asIoConnectionError(underlyingError, stackTrace);\n+    if (ioConnectionError != null) throw ioConnectionError;\n+    _throwUnknownRemoteException(underlyingError, stackTrace);\n+  }\n+\n+  /// Maps dart:io transport errors that are connectivity failures (not server\n+  /// rejections) to [ConnectionError], so the session is kept and a toast is\n+  /// shown instead of logging the user out.\n+  ConnectionError? _asIoConnectionError(dynamic error, StackTrace? stackTrace) {\n+    if (error is HandshakeException) {\n       logError(\n-        'RemoteExceptionThrower: TLS handshake error — will_logout=false | ${underlyingError.message}',\n-        exception: underlyingError,\n+        'RemoteExceptionThrower: TLS handshake error — will_logout=false | ${error.message}',\n+        exception: error,\n         stackTrace: stackTrace,\n       );\n-      throw ConnectionError(message: underlyingError.message);\n+      return ConnectionError(message: error.message);\n     }\n-    if (underlyingError is HttpException) {\n+    if (error is HttpException) {\n       logError(\n-        'RemoteExceptionThrower: HTTP connection abort — will_logout=false | ${underlyingError.message}',\n-        exception: underlyingError,\n+        'RemoteExceptionThrower: HTTP connection abort — will_logout=false | ${error.message}',\n+        exception: error,\n         stackTrace: stackTrace,\n       );\n-      throw ConnectionError(message: underlyingError.message);\n-    }\n-    if (underlyingError is OAuthAuthorizationError) {\n-      throw underlyingError;\n+      return ConnectionError(message: error.message);\n     }\n-    _throwUnknownRemoteException(underlyingError, stackTrace);\n+    return null;\n   }\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dang Dat","training-data":{"loc-added":"6","loc-deleted":"20","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"9.387218218812514"},"author-email":"tddang@linagora.com","commit-full-message":"","commit-date":"2026-01-07T17:55:12Z","current-rev":"b1b0e6538","filename":"twake-on-matrix/lib/utils/client_manager.dart","previous-rev":"b3f092e7e","commit-title":"[Migrate Matrix] Optimize database","language":"Dart","id":"965d46b658492336cde0c65813bfc31a068b9e29","model-score":0.52,"author-id":null,"project-id":75877,"delta-file-score":0.29056275,"diff":"diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart\nindex fe2a3edbb..8acfb9a64 100644\n--- a/lib/utils/client_manager.dart\n+++ b/lib/utils/client_manager.dart\n@@ -33,17 +33,3 @@ abstract class ClientManager {\n     }\n-    final clients = await Future.wait(\n-      clientNames.map((name) async {\n-        late DatabaseApi database;\n-        try {\n-          database = await MatrixSdkDatabase.init(\n-            name,\n-            database: await openSqfliteDb(name: name),\n-          );\n-        } catch (e) {\n-          Logs().e('Unable to open database for client $name', e);\n-          database = FlutterHiveCollectionsDatabase(name, '');\n-        }\n-        return createClient(name, database: database);\n-      }),\n-    );\n+    final clients = await Future.wait(clientNames.map(createClient));\n     if (initialize) {\n@@ -105,6 +91,3 @@ abstract class ClientManager {\n \n-  static Client createClient(\n-    String clientName, {\n-    required DatabaseApi database,\n-  }) {\n+  static Future<Client> createClient(String clientName) async {\n     return Client(\n@@ -125,3 +108,6 @@ abstract class ClientManager {\n       logLevel: kReleaseMode ? Level.warning : Level.verbose,\n-      database: database,\n+      database: await MatrixSdkDatabase.init(\n+        clientName,\n+        database: await openSqfliteDb(name: clientName),\n+      ),\n       legacyDatabaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder,\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dat Vu","training-data":{"loc-added":"30","loc-deleted":"57","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"2.79","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"","commit-date":"2026-05-05T04:01:45Z","current-rev":"ba1ac1831","filename":"tmail-flutter/lib/main/exceptions/thrower/remote_exception_thrower.dart","previous-rev":"5e4ffcba4","commit-title":"fix(logger): Reduce Sentry noise with targeted error logging (ADR-0076) (#4467)","language":"Dart","id":"b46679f58da3f1ba4d7a39883cfe8aab033b6d24","model-score":0.46,"author-id":null,"project-id":75877,"delta-file-score":0.9857092,"diff":"diff --git a/lib/main/exceptions/thrower/remote_exception_thrower.dart b/lib/main/exceptions/thrower/remote_exception_thrower.dart\nindex 133056dce..334cf3663 100644\n--- a/lib/main/exceptions/thrower/remote_exception_thrower.dart\n+++ b/lib/main/exceptions/thrower/remote_exception_thrower.dart\n@@ -1,10 +1,5 @@\n-import 'dart:io';\n-\n import 'package:core/utils/app_logger.dart';\n import 'package:dio/dio.dart';\n-import 'package:get/get_connect/http/src/status/http_status.dart';\n import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart';\n import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart';\n-import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';\n-import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart';\n import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'\n@@ -14,4 +9,4 @@ import 'package:tmail_ui_user/main/exceptions/remote/method_level_exception.dart\n import 'package:tmail_ui_user/main/exceptions/remote/network_exception.dart';\n-import 'package:tmail_ui_user/main/exceptions/remote/server_exception.dart';\n-import 'package:tmail_ui_user/main/exceptions/remote/unknown_remote_exception.dart';\n+import 'package:tmail_ui_user/main/exceptions/thrower/dio_no_response_error_handler.dart';\n+import 'package:tmail_ui_user/main/exceptions/thrower/dio_response_error_handler.dart';\n import 'package:tmail_ui_user/main/exceptions/thrower/exception_thrower.dart';\n@@ -20,2 +15,4 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart';\n class RemoteExceptionThrower extends ExceptionThrower {\n+  final _responseHandler = DioResponseErrorHandler();\n+  final _noResponseHandler = DioNoResponseErrorHandler();\n \n@@ -23,7 +20,2 @@ class RemoteExceptionThrower extends ExceptionThrower {\n   throwException(dynamic error, dynamic stackTrace) {\n-    logError(\n-      'RemoteExceptionThrower::throwException():error: $error | stackTrace: $stackTrace',\n-      exception: error,\n-      stackTrace: stackTrace,\n-    );\n     final networkConnectionController = getBinding<NetworkConnectionController>();\n@@ -33,3 +25,3 @@ class RemoteExceptionThrower extends ExceptionThrower {\n     } else {\n-      handleDioError(error);\n+      handleDioError(error, stackTrace);\n     }\n@@ -37,70 +29,51 @@ class RemoteExceptionThrower extends ExceptionThrower {\n \n-  void handleDioError(dynamic error) {\n+  void handleDioError(dynamic error, [StackTrace? stackTrace]) {\n     if (error is DioException) {\n-      logWarning(\n-        'RemoteExceptionThrower::throwException():type: ${error.type} | response: ${error.response} | error: ${error.error}',\n-      );\n+      _handleDioException(error, stackTrace);\n+      return;\n+    }\n \n-      if (error.error is RefreshTokenFailedException) {\n-        throw RefreshTokenFailedException();\n-      }\n+    if (error is ErrorMethodResponseException) {\n+      _handleMethodResponseException(error);\n+      return;\n+    }\n \n-      final response = error.response;\n-      final statusCode = response?.statusCode;\n+    logError(\n+      'RemoteExceptionThrower::handleDioError(): unrecognised error',\n+      exception: error,\n+      stackTrace: stackTrace,\n+    );\n+    throw error;\n+  }\n \n-      if (response != null) {\n-        switch (statusCode) {\n-          case HttpStatus.internalServerError:\n-            throw const InternalServerError();\n-          case HttpStatus.badGateway:\n-            throw BadGateway();\n-          case HttpStatus.unauthorized:\n-            throw const BadCredentialsException();\n-          default:\n-            throw UnknownRemoteException(\n-              code: statusCode,\n-              message: response.statusMessage,\n-            );\n-        }\n-      }\n+  void _handleDioException(DioException error, [StackTrace? stackTrace]) {\n+    logWarning(\n+      'RemoteExceptionThrower::_handleDioException(): type=${error.type}'\n+      ' status=${error.response?.statusCode}'\n+      ' underlying=${error.error?.runtimeType}',\n+    );\n \n-      return _handleDioErrorWithoutResponse(error);\n+    if (error.error is RefreshTokenFailedException) {\n+      throw RefreshTokenFailedException();\n     }\n \n-    if (error is ErrorMethodResponseException) {\n-      final errorResponse = error.errorResponse as ErrorMethodResponse;\n-      if (errorResponse is CannotCalculateChangesMethodResponse) {\n-        throw CannotCalculateChangesMethodResponseException();\n-      } else {\n-        throw MethodLevelErrors(\n-          errorResponse.type,\n-          message: errorResponse.description,\n-        );\n-      }\n+    final response = error.response;\n+    if (response != null) {\n+      return _responseHandler.handle(response, stackTrace);\n     }\n \n-    throw error;\n+    return _noResponseHandler.handle(error, stackTrace);\n   }\n \n-  void _handleDioErrorWithoutResponse(DioException error) {\n-    switch (error.type) {\n-      case DioExceptionType.connectionTimeout:\n-        throw ConnectionTimeout(message: error.message);\n-      case DioExceptionType.connectionError:\n-        throw ConnectionError(message: error.message);\n-      case DioExceptionType.badResponse:\n-        throw const BadCredentialsException();\n-      default:\n-        final underlyingError = error.error;\n-        if (underlyingError is SocketException) {\n-          throw const SocketError();\n-        } else if (underlyingError is OAuthAuthorizationError) {\n-          throw underlyingError;\n-        } else if (underlyingError != null) {\n-          throw UnknownRemoteException(error: underlyingError);\n-        } else {\n-          throw const UnknownRemoteException();\n-        }\n+  void _handleMethodResponseException(ErrorMethodResponseException error) {\n+    final errorResponse = error.errorResponse as ErrorMethodResponse;\n+    if (errorResponse is CannotCalculateChangesMethodResponse) {\n+      throw CannotCalculateChangesMethodResponseException();\n+    } else {\n+      throw MethodLevelErrors(\n+        errorResponse.type,\n+        message: errorResponse.description,\n+      );\n     }\n   }\n-}\n\\ No newline at end of file\n+}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Théo Poizat","training-data":{"loc-added":"11","loc-deleted":"74","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.17","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"hello@zatteo.com","commit-full-message":"Here we map our AI actions to the prompt from the JSON file\nto be able to use them.","commit-date":"2026-01-29T15:27:37Z","current-rev":"04696eecd","filename":"tmail-flutter/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart","previous-rev":"11fd80808","commit-title":"Get and build prompt from local JSON file","language":"Dart","id":"47987d0723033cd2d4affa01cfd5cc0ac59992f7","model-score":0.45,"author-id":null,"project-id":75877,"delta-file-score":0.95172936,"diff":"diff --git a/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart b/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart\nindex 73b934695..b44293e91 100644\n--- a/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart\n+++ b/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart\n@@ -3,16 +3,11 @@ import 'package:scribe/scribe/ai/presentation/model/ai_action.dart';\n import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart';\n+import 'package:scribe/scribe/ai/domain/service/prompt_service.dart';\n \n class AIPrompts {\n-  static const _performTask = \"Perform only the following task:\";\n-  static const _preserveLanguagePrompt =\n-      \"Do not translate. Strictly keep the original language of the input text. For example, if it's French, keep French. If it's English, keep English.\";\n-  static const _doNotAddInfoPrompt =\n-      \"Do not add any extra information or interpret anything beyond the explicit task.\";\n+  static final PromptService _promptService = PromptService();\n \n-  static List<AIMessage> buildPrompt(AIAction action, String? text) {\n-    final prompt =  switch (action) {\n+  static Future<List<AIMessage>> buildPrompt(AIAction action, String? text) async {\n+    return switch (action) {\n       PredefinedAction(action: final menuAction) =>\n-      text?.trim().isNotEmpty == true\n-          ? buildPredefinedPrompt(menuAction, text!)\n-          :  throw ArgumentError('Text cannot be empty for predefined actions'),\n+        buildActionPrompt(menuAction, text),\n       CustomPromptAction(prompt: final customPrompt) =>\n@@ -20,71 +15,13 @@ class AIPrompts {\n     };\n-\n-    final message = [AIMessage.ofUser(prompt)];\n-\n-    return message;\n   }\n \n-  static String buildPredefinedPrompt(AIScribeMenuAction action, String text) {\n-    switch (action) {\n-      case AIScribeMenuAction.correctGrammar:\n-        return correctGrammar(text);\n-      case AIScribeMenuAction.improveMakeShorter:\n-        return improveMakeShorter(text);\n-      case AIScribeMenuAction.improveExpandContext:\n-        return improveExpandContext(text);\n-      case AIScribeMenuAction.improveEmojify:\n-        return improveEmojify(text);\n-      case AIScribeMenuAction.improveTransformToBullets:\n-        return improveTransformToBullets(text);\n-      case AIScribeMenuAction.changeToneProfessional:\n-        return changeToneTo(text, 'professional');\n-      case AIScribeMenuAction.changeToneCasual:\n-        return changeToneTo(text, 'casual');\n-      case AIScribeMenuAction.changeTonePolite:\n-        return changeToneTo(text, 'polite');\n-      case AIScribeMenuAction.translateFrench:\n-        return translateTo(text, 'French');\n-      case AIScribeMenuAction.translateEnglish:\n-        return translateTo(text, 'English');\n-      case AIScribeMenuAction.translateRussian:\n-        return translateTo(text, 'Russian');\n-      case AIScribeMenuAction.translateVietnamese:\n-        return translateTo(text, 'Vietnamese');\n+  static Future<List<AIMessage>> buildActionPrompt(AIScribeMenuAction menuAction, String? text) async {\n+    if (text == null || text.trim().isEmpty) {\n+      throw ArgumentError('Text cannot be empty for predefined actions');\n     }\n+    return await _promptService.buildPromptByName(menuAction.promptId, text);\n   }\n \n-  static String improveMakeShorter(String text) {\n-    return '$_performTask make the text shorter but preserve the meaning. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String improveExpandContext(String text) {\n-    return '$_performTask expand the context of the text to make it more detailed and comprehensive. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String improveEmojify(String text) {\n-    return '$_performTask add emojis to the important parts of the text. Do not try to rephrase or replace text. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String improveTransformToBullets(String text) {\n-    return '$_performTask transform the text into a bullet list. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String correctGrammar(String text) {\n-    return '$_performTask correct grammar and spelling. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String changeToneTo(String text, String tone) {\n-    return '$_performTask change the tone to be $tone. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String translateTo(String text, String language) {\n-    return '$_performTask translate. Translate the text to the specified language: $language. $_doNotAddInfoPrompt Text:\\n\\n$text';\n-  }\n-\n-  static String buildCustomPrompt(String customPrompt, String? text) {\n-    if (text == null) {\n-      return customPrompt;\n-    }\n-\n-    return 'You help the user write an email following his instruction: $customPrompt\\n\\nDo not output a subject or a signature, only the content of the email. Text:\\n\\n$text';\n+  static Future<List<AIMessage>> buildCustomPrompt(String customPrompt, String? text) async {\n+    return await _promptService.buildPromptByName(CustomPromptAction.promptId, text ?? '', task: customPrompt);\n   }\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"dab246","training-data":{"loc-added":"15","loc-deleted":"28","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.08","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"Signed-off-by: dab246 <tdvu@linagora.com>","commit-date":"2026-05-05T08:45:50Z","current-rev":"6f9be87a3","filename":"tmail-flutter/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart","previous-rev":"1474cd84e","commit-title":"TF-4473 Refactor reduce cyclomatic complexity in email setup extensions","language":"Dart","id":"8bece3d06b724c73a87d05804c937af9579ad543","model-score":0.42,"author-id":null,"project-id":75877,"delta-file-score":0.33626333,"diff":"diff --git a/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart b/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart\nindex 6c963a9b4..f45b9f360 100644\n--- a/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart\n+++ b/lib/features/composer/presentation/extensions/setup_email_subject_extension.dart\n@@ -10,30 +10,6 @@ extension SetupEmailSubjectExtension on ComposerController {\n   void setupEmailSubject(ComposerArguments arguments) {\n-    String subject = '';\n-\n-    switch(currentEmailActionType!) {\n-      case EmailActionType.editAsNewEmail:\n-      case EmailActionType.editDraft:\n-      case EmailActionType.reply:\n-      case EmailActionType.replyToList:\n-      case EmailActionType.replyAll:\n-      case EmailActionType.forward:\n-      case EmailActionType.reopenComposerBrowser:\n-        subject = arguments.presentationEmail!.getEmailTitle().trim();\n-        break;\n-      case EmailActionType.editSendingEmail:\n-        subject = arguments.sendingEmail!.presentationEmail.getEmailTitle().trim();\n-        break;\n-      case EmailActionType.composeFromMailtoUri:\n-      case EmailActionType.composeFromUnsubscribeMailtoLink:\n-        subject = arguments.subject ?? '';\n-        break;\n-      default:\n-        break;\n-    }\n-\n-    final newSubject = currentEmailActionType!.getSubjectComposer(\n-      currentContext,\n-      subject,\n-    );\n-\n+    final actionType = currentEmailActionType;\n+    if (actionType == null) return;\n+    final subject = _resolveRawSubject(arguments, actionType);\n+    final newSubject = actionType.getSubjectComposer(currentContext, subject);\n     if (newSubject.isNotEmpty) {\n@@ -43,2 +19,13 @@ extension SetupEmailSubjectExtension on ComposerController {\n   }\n-}\n\\ No newline at end of file\n+\n+  String _resolveRawSubject(ComposerArguments arguments, EmailActionType actionType) {\n+    if (actionType == EmailActionType.editSendingEmail) {\n+      return arguments.sendingEmail?.presentationEmail.getEmailTitle().trim() ?? '';\n+    }\n+    if (actionType == EmailActionType.composeFromMailtoUri ||\n+        actionType == EmailActionType.composeFromUnsubscribeMailtoLink) {\n+      return arguments.subject ?? '';\n+    }\n+    return arguments.presentationEmail?.getEmailTitle().trim() ?? '';\n+  }\n+}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dat Dang","training-data":{"loc-added":"32","loc-deleted":"18","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"2.034","delta-n-functions":"0","current-file-score":"9.387218218812514"},"author-email":"tddang@linagora.com","commit-full-message":"","commit-date":"2026-01-26T10:26:27Z","current-rev":"01dec9f04","filename":"twake-on-matrix/lib/presentation/mixins/contacts_view_controller_mixin.dart","previous-rev":"9054a4e34","commit-title":"TW-2852 Handle search external matrix id when contact is not ready (#2853)","language":"Dart","id":"2be1f7967796458eebe87adca488d1d74afe0ccb","model-score":0.41,"author-id":null,"project-id":75877,"delta-file-score":0.58042353,"diff":"diff --git a/lib/presentation/mixins/contacts_view_controller_mixin.dart b/lib/presentation/mixins/contacts_view_controller_mixin.dart\nindex 7c7536cf4..1b52b3e60 100644\n--- a/lib/presentation/mixins/contacts_view_controller_mixin.dart\n+++ b/lib/presentation/mixins/contacts_view_controller_mixin.dart\n@@ -75,2 +75,8 @@ mixin class ContactsViewControllerMixin {\n \n+  bool get phoneBookFilterSuccess =>\n+      presentationPhonebookContactNotifier.value.fold(\n+        (_) => false,\n+        (success) => success is GetPhonebookContactsSuccess,\n+      );\n+\n   Future displayContactPermissionDialog(BuildContext context) async {\n@@ -304,2 +310,4 @@ mixin class ContactsViewControllerMixin {\n \n+    final externalContactState = _checkExternalContact(keyword);\n+\n     presentationContactNotifier.value =\n@@ -307,2 +315,6 @@ mixin class ContactsViewControllerMixin {\n       (failure) {\n+        if (externalContactState != null) {\n+          return externalContactState;\n+        }\n+\n         if (failure is GetContactsFailure) {\n@@ -340,19 +352,4 @@ mixin class ContactsViewControllerMixin {\n           if (combinedContacts.isEmpty) {\n-            if (keyword.isValidMatrixId && keyword.startsWith(\"@\")) {\n-              return Right(\n-                PresentationExternalContactSuccess(\n-                  contact: PresentationContact(\n-                    matrixId: keyword,\n-                    displayName: keyword.substring(1),\n-                    type: ContactType.external,\n-                  ),\n-                ),\n-              );\n-            } else {\n-              return Left(\n-                GetPresentationContactsEmpty(\n-                  keyword: keyword,\n-                ),\n-              );\n-            }\n+            return externalContactState ??\n+                Left(GetPresentationContactsEmpty(keyword: keyword));\n           } else {\n@@ -366,3 +363,3 @@ mixin class ContactsViewControllerMixin {\n         }\n-        return Right(success);\n+        return externalContactState ?? Right(success);\n       },\n@@ -371,2 +368,19 @@ mixin class ContactsViewControllerMixin {\n \n+  Either<Failure, Success>? _checkExternalContact(\n+    String keyword,\n+  ) {\n+    if (keyword.isValidMatrixId && keyword.startsWith(\"@\")) {\n+      return Right(\n+        PresentationExternalContactSuccess(\n+          contact: PresentationContact(\n+            matrixId: keyword,\n+            displayName: keyword.substring(1),\n+            type: ContactType.external,\n+          ),\n+        ),\n+      );\n+    }\n+    return null;\n+  }\n+\n   Future<void> _refreshPhoneBookContacts(String keyword) async {\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dat Vu","training-data":{"loc-added":"1","loc-deleted":"53","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"","commit-date":"2026-05-05T04:04:48Z","current-rev":"3b47b799f","filename":"tmail-flutter/lib/features/search/email/presentation/extension/update_search_filter_extension.dart","previous-rev":"ba1ac1831","commit-title":"Implement `not include events` filter for search (#4451)","language":"Dart","id":"ee16d58c97ed3dc6be1e836b61ba1ac08cb1f5b7","model-score":0.38,"author-id":null,"project-id":75877,"delta-file-score":0.31179166,"diff":"diff --git a/lib/features/search/email/presentation/extension/update_search_filter_extension.dart b/lib/features/search/email/presentation/extension/update_search_filter_extension.dart\nindex 6f81760f8..c7ca9c502 100644\n--- a/lib/features/search/email/presentation/extension/update_search_filter_extension.dart\n+++ b/lib/features/search/email/presentation/extension/update_search_filter_extension.dart\n@@ -1,8 +1,3 @@\n import 'package:dartz/dartz.dart';\n-import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';\n import 'package:labels/model/label.dart';\n-import 'package:model/extensions/keyword_identifier_extension.dart';\n-import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';\n-import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart';\n-import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart';\n import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';\n@@ -10,51 +5,4 @@ import 'package:tmail_ui_user/features/search/email/presentation/search_email_co\n extension UpdateSearchFilterExtension on SearchEmailController {\n-  void deleteQuickSearchFilter({required QuickSearchFilter filter}) {\n-    switch (filter) {\n-      case QuickSearchFilter.hasAttachment:\n-        updateSimpleSearchFilter(hasAttachmentOption: const None());\n-        break;\n-      case QuickSearchFilter.starred:\n-        final keywords = Set<String>.from(listHasKeywordFiltered)\n-          ..remove(KeyWordIdentifier.emailFlagged.value);\n-        updateSimpleSearchFilter(\n-          hasKeywordOption: keywords.isEmpty ? const None() : Some(keywords),\n-        );\n-        break;\n-      case QuickSearchFilter.events:\n-        final keywords = Set<String>.from(listHasKeywordFiltered)\n-          ..remove(KeyWordIdentifierExtension.eventsMail.value);\n-        updateSimpleSearchFilter(\n-          hasKeywordOption: keywords.isEmpty ? const None() : Some(keywords),\n-        );\n-        break;\n-      case QuickSearchFilter.unread:\n-        updateSimpleSearchFilter(unreadOption: const None());\n-        break;\n-      case QuickSearchFilter.sortBy:\n-        updateSimpleSearchFilter(\n-          sortOrderTypeOption: const Some(SearchEmailFilter.defaultSortOrder),\n-        );\n-        break;\n-      case QuickSearchFilter.dateTime:\n-        updateSimpleSearchFilter(\n-          emailReceiveTimeTypeOption: const Some(EmailReceiveTimeType.allTime),\n-          startDateOption: const None(),\n-          endDateOption: const None(),\n-        );\n-        break;\n-      case QuickSearchFilter.from:\n-        updateSimpleSearchFilter(fromOption: const None());\n-        break;\n-      case QuickSearchFilter.to:\n-        updateSimpleSearchFilter(toOption: const None());\n-        break;\n-      case QuickSearchFilter.folder:\n-        updateSimpleSearchFilter(mailboxOption: const None());\n-        break;\n-      case QuickSearchFilter.labels:\n-        updateSimpleSearchFilter(labelOption: const None());\n-        break;\n-      default:\n-        break;\n-    }\n+  void deleteQuickSearchLabelFilter() {\n+    updateSimpleSearchFilter(labelOption: const None());\n     searchEmailAction();\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"dab246","training-data":{"loc-added":"1","loc-deleted":"19","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"9.387218218812514"},"author-email":"tdvu@linagora.com","commit-full-message":"","commit-date":"2025-11-26T09:32:53Z","current-rev":"38c47ca37","filename":"tmail-flutter/lib/features/mailbox/data/network/mailbox_api.dart","previous-rev":"8f1da20e5","commit-title":"TF-4178 Add datasource/repository/interactor for create new label","language":"Dart","id":"a59947e60596b61e66bcd28eb66a873eee517138","model-score":0.38,"author-id":null,"project-id":75877,"delta-file-score":0.29056275,"diff":"diff --git a/lib/features/mailbox/data/network/mailbox_api.dart b/lib/features/mailbox/data/network/mailbox_api.dart\nindex ca50b2865..997a809a5 100644\n--- a/lib/features/mailbox/data/network/mailbox_api.dart\n+++ b/lib/features/mailbox/data/network/mailbox_api.dart\n@@ -12,3 +12,2 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart\n import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart';\n-import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart';\n import 'package:jmap_dart_client/jmap/core/error/set_error.dart';\n@@ -199,20 +198,3 @@ class MailboxAPI with HandleSetErrorMixin, MailAPIMixin {\n     } else {\n-      throw _parseErrorForSetMailboxResponse(setMailboxResponse, generateCreateId);\n-    }\n-  }\n-\n-  _parseErrorForSetMailboxResponse(SetMailboxResponse? response, Id requestId) {\n-    final mapError = response?.notCreated ?? response?.notUpdated ?? response?.notDestroyed;\n-    if (mapError != null && mapError.containsKey(requestId)) {\n-      final setError = mapError[requestId];\n-      log('MailboxAPI::_parseErrorForSetMailboxResponse():setError: $setError');\n-      if (setError?.type == ErrorMethodResponse.invalidArguments) {\n-        throw InvalidArgumentsMethodResponse(description: setError?.description);\n-      } else if (setError?.type == ErrorMethodResponse.invalidResultReference) {\n-        throw InvalidResultReferenceMethodResponse(description: setError?.description);\n-      } else {\n-        throw UnknownMethodResponse(description: setError?.description);\n-      }\n-    }  else {\n-      throw UnknownMethodResponse();\n+      throw parseErrorForSetResponse(setMailboxResponse, generateCreateId);\n     }\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"dab246","training-data":{"loc-added":"46","loc-deleted":"32","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.35","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"Signed-off-by: dab246 <tdvu@linagora.com>","commit-date":"2026-05-05T08:45:50Z","current-rev":"6f9be87a3","filename":"tmail-flutter/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart","previous-rev":"1474cd84e","commit-title":"TF-4473 Refactor reduce cyclomatic complexity in email setup extensions","language":"Dart","id":"2b83c84b0f4a4d42918a1f29da4e68c17ca3b9e9","model-score":0.32,"author-id":null,"project-id":75877,"delta-file-score":0.41834745,"diff":"diff --git a/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart b/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart\nindex 4240f41a2..f243e4e3f 100644\n--- a/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart\n+++ b/lib/features/composer/presentation/extensions/setup_email_recipients_extension.dart\n@@ -9,60 +9,74 @@ extension SetupEmailRecipientsExtension on ComposerController {\n   void setupEmailRecipients(ComposerArguments arguments) {\n-    switch(currentEmailActionType) {\n-      case EmailActionType.editAsNewEmail:\n-      case EmailActionType.editDraft:\n-      case EmailActionType.reopenComposerBrowser:\n-        initEmailAddress(\n-          presentationEmail: arguments.presentationEmail!,\n-          actionType: currentEmailActionType!,\n-        );\n-        break;\n-      case EmailActionType.editSendingEmail:\n-        initEmailAddress(\n-          presentationEmail: arguments.sendingEmail!.presentationEmail,\n-          actionType: currentEmailActionType!,\n-        );\n-        break;\n-      case EmailActionType.composeFromEmailAddress:\n-      case EmailActionType.composeFromUnsubscribeMailtoLink:\n-        final emailAddressOfTo = arguments.listEmailAddress ?? [];\n-        if (emailAddressOfTo.isNotEmpty) {\n-          listToEmailAddress.addAll(emailAddressOfTo);\n-          isInitialRecipient.value = true;\n-        }\n-        break;\n-      case EmailActionType.composeFromMailtoUri:\n-        final emailAddressOfTo = arguments.listEmailAddress ?? [];\n-        final emailAddressOfCc = arguments.cc ?? [];\n-        final emailAddressOfBc = arguments.bcc ?? [];\n+    if (_isDraftLikeRecipientsAction) {\n+      initEmailAddress(\n+        presentationEmail: arguments.presentationEmail!,\n+        actionType: currentEmailActionType!,\n+      );\n+    } else if (currentEmailActionType == EmailActionType.editSendingEmail) {\n+      initEmailAddress(\n+        presentationEmail: arguments.sendingEmail!.presentationEmail,\n+        actionType: currentEmailActionType!,\n+      );\n+    } else if (_isDirectToAddressAction) {\n+      _setupToAddressOnly(arguments);\n+    } else if (_isFullAddressAction) {\n+      _setupFullAddressRecipients(arguments);\n+    } else if (_isReplyLikeRecipientsAction) {\n+      initEmailAddress(\n+        presentationEmail: arguments.presentationEmail!,\n+        actionType: currentEmailActionType!,\n+        listPost: arguments.listPost,\n+      );\n+    }\n+\n+    updateStatusEmailSendButton();\n+  }\n+\n+  bool get _isDraftLikeRecipientsAction => const {\n+    EmailActionType.editAsNewEmail,\n+    EmailActionType.editDraft,\n+    EmailActionType.reopenComposerBrowser,\n+  }.contains(currentEmailActionType);\n+\n+  bool get _isDirectToAddressAction => const {\n+    EmailActionType.composeFromEmailAddress,\n+    EmailActionType.composeFromUnsubscribeMailtoLink,\n+  }.contains(currentEmailActionType);\n \n-        if (emailAddressOfTo.isNotEmpty) {\n-          listToEmailAddress.addAll(emailAddressOfTo);\n-          isInitialRecipient.value = true;\n-        }\n+  bool get _isFullAddressAction => const {\n+    EmailActionType.composeFromMailtoUri,\n+  }.contains(currentEmailActionType);\n \n-        if (emailAddressOfCc.isNotEmpty) {\n-          listCcEmailAddress = emailAddressOfCc;\n-          ccRecipientState.value = PrefixRecipientState.enabled;\n-        }\n+  bool get _isReplyLikeRecipientsAction => const {\n+    EmailActionType.reply,\n+    EmailActionType.replyToList,\n+    EmailActionType.replyAll,\n+  }.contains(currentEmailActionType);\n \n-        if (emailAddressOfBc.isNotEmpty) {\n-          listBccEmailAddress = emailAddressOfBc;\n-          bccRecipientState.value = PrefixRecipientState.enabled;\n-        }\n-        break;\n-      case EmailActionType.reply:\n-      case EmailActionType.replyToList:\n-      case EmailActionType.replyAll:\n-        initEmailAddress(\n-          presentationEmail: arguments.presentationEmail!,\n-          actionType: currentEmailActionType!,\n-          listPost: arguments.listPost,\n-        );\n-        break;\n-      default:\n-        break;\n+  void _setupToAddressOnly(ComposerArguments arguments) {\n+    final emailAddressOfTo = arguments.listEmailAddress ?? [];\n+    if (emailAddressOfTo.isNotEmpty) {\n+      listToEmailAddress.addAll(emailAddressOfTo);\n+      isInitialRecipient.value = true;\n     }\n+  }\n \n-    updateStatusEmailSendButton();\n+  void _setupFullAddressRecipients(ComposerArguments arguments) {\n+    final emailAddressOfTo = arguments.listEmailAddress ?? [];\n+    final emailAddressOfCc = arguments.cc ?? [];\n+    final emailAddressOfBcc = arguments.bcc ?? [];\n+\n+    if (emailAddressOfTo.isNotEmpty) {\n+      listToEmailAddress.addAll(emailAddressOfTo);\n+      isInitialRecipient.value = true;\n+    }\n+    if (emailAddressOfCc.isNotEmpty) {\n+      listCcEmailAddress = List.from(emailAddressOfCc);\n+      ccRecipientState.value = PrefixRecipientState.enabled;\n+    }\n+    if (emailAddressOfBcc.isNotEmpty) {\n+      listBccEmailAddress = List.from(emailAddressOfBcc);\n+      bccRecipientState.value = PrefixRecipientState.enabled;\n+    }\n   }\n-}\n\\ No newline at end of file\n+}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"HuyNguyen","training-data":{"loc-added":"50","loc-deleted":"31","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.584","delta-n-functions":"0","current-file-score":"9.6882083290695"},"author-email":"quanghnguyen@linagora.com","commit-full-message":"","commit-date":"2026-04-17T09:26:40Z","current-rev":"de821e427","filename":"twake-on-matrix/lib/utils/matrix_sdk_extensions/event_list_extension.dart","previous-rev":"9bfb56874","commit-title":"fixup! SENTRY Track wrong member count","language":"Dart","id":"1c1ec043780d5b2543f095af556473d18fd9ff02","model-score":0.24,"author-id":null,"project-id":75877,"delta-file-score":2.3810542,"diff":"diff --git a/lib/utils/matrix_sdk_extensions/event_list_extension.dart b/lib/utils/matrix_sdk_extensions/event_list_extension.dart\nindex c1e621cb9..1ad7eb437 100644\n--- a/lib/utils/matrix_sdk_extensions/event_list_extension.dart\n+++ b/lib/utils/matrix_sdk_extensions/event_list_extension.dart\n@@ -110,41 +110,9 @@ extension EventListExtension on List<Event> {\n     final startDiff = _findStartDiff(newEvents, firstOldEventInLists);\n-    bool shouldScrollToBottom = false;\n-\n-    if (startDiff > 0) {\n-      final newStartItems = newEvents.sublist(0, startDiff);\n-\n-      // Filter out duplicates using map lookup (O(1) per item)\n-      final itemsToAdd = <Event>[];\n-      for (final newItem in newStartItems) {\n-        if (newItem.findEventInMap(existingEventsMap) == null) {\n-          itemsToAdd.add(newItem);\n-          // Update the map to prevent duplicates in end processing\n-          existingEventsMap[newItem.eventId] = newItem;\n-          final transactionId = newItem.unsigned?['transaction_id'] as String?;\n-          if (transactionId != null) {\n-            existingEventsMap[transactionId] = newItem;\n-          }\n-        }\n-      }\n-\n-      // Debug: log if any new-start items were silently dropped as duplicates\n-      if (itemsToAdd.length < newStartItems.length) {\n-        final droppedCount = newStartItems.length - itemsToAdd.length;\n-        Logs().d(\n-          '${SentryTrackedEvents.missingLastMessage.message}: '\n-          'syncEventLists() $droppedCount new-start event(s) dropped as '\n-          'duplicates (startDiff=$startDiff, added=${itemsToAdd.length})',\n-        );\n-      }\n-\n-      updatedTop.addAll(itemsToAdd.reversed);\n-\n-      // Scroll to bottom only for own sent messages, not incoming from others\n-      if (!wasRequestingFuture &&\n-          itemsToAdd.any(\n-            (event) => event.isVisibleInGui && event.isOwnMessage,\n-          )) {\n-        shouldScrollToBottom = true;\n-      }\n-    }\n+    final shouldScrollToBottom = _applyStartDiff(\n+      newEvents: newEvents,\n+      startDiff: startDiff,\n+      existingEventsMap: existingEventsMap,\n+      updatedTop: updatedTop,\n+      wasRequestingFuture: wasRequestingFuture,\n+    );\n \n@@ -152,15 +120,8 @@ extension EventListExtension on List<Event> {\n     final endDiff = _findEndDiff(newEvents, lastOldEventInLists);\n-    if (endDiff > 0) {\n-      final newEndItems = newEvents.sublist(newEvents.length - endDiff);\n-\n-      // Filter out duplicates using map lookup (O(1) per item)\n-      final itemsToAdd = <Event>[];\n-      for (final newItem in newEndItems) {\n-        if (newItem.findEventInMap(existingEventsMap) == null) {\n-          itemsToAdd.add(newItem);\n-        }\n-      }\n-\n-      updatedBottom.addAll(itemsToAdd);\n-    }\n+    _applyEndDiff(\n+      newEvents: newEvents,\n+      endDiff: endDiff,\n+      existingEventsMap: existingEventsMap,\n+      updatedBottom: updatedBottom,\n+    );\n \n@@ -173,2 +134,60 @@ extension EventListExtension on List<Event> {\n \n+  /// Applies new events found at the start of [newEvents] to [updatedTop].\n+  /// Returns whether the view should scroll to bottom.\n+  static bool _applyStartDiff({\n+    required List<Event> newEvents,\n+    required int startDiff,\n+    required Map<String, Event> existingEventsMap,\n+    required List<Event> updatedTop,\n+    required bool wasRequestingFuture,\n+  }) {\n+    if (startDiff == 0) return false;\n+\n+    final newStartItems = newEvents.sublist(0, startDiff);\n+    final itemsToAdd = <Event>[];\n+\n+    for (final newItem in newStartItems) {\n+      if (newItem.findEventInMap(existingEventsMap) == null) {\n+        itemsToAdd.add(newItem);\n+        existingEventsMap[newItem.eventId] = newItem;\n+        final transactionId = newItem.unsigned?['transaction_id'] as String?;\n+        if (transactionId != null) {\n+          existingEventsMap[transactionId] = newItem;\n+        }\n+      }\n+    }\n+\n+    if (itemsToAdd.length < newStartItems.length) {\n+      final droppedCount = newStartItems.length - itemsToAdd.length;\n+      Logs().d(\n+        '${SentryTrackedEvents.missingLastMessage.message}: '\n+        'syncEventLists() $droppedCount new-start event(s) dropped as '\n+        'duplicates (startDiff=$startDiff, added=${itemsToAdd.length})',\n+      );\n+    }\n+\n+    updatedTop.addAll(itemsToAdd.reversed);\n+\n+    return !wasRequestingFuture &&\n+        itemsToAdd.any((event) => event.isVisibleInGui && event.isOwnMessage);\n+  }\n+\n+  /// Applies new events found at the end of [newEvents] to [updatedBottom].\n+  static void _applyEndDiff({\n+    required List<Event> newEvents,\n+    required int endDiff,\n+    required Map<String, Event> existingEventsMap,\n+    required List<Event> updatedBottom,\n+  }) {\n+    if (endDiff == 0) return;\n+\n+    final newEndItems = newEvents.sublist(newEvents.length - endDiff);\n+    final itemsToAdd = [\n+      for (final newItem in newEndItems)\n+        if (newItem.findEventInMap(existingEventsMap) == null) newItem,\n+    ];\n+\n+    updatedBottom.addAll(itemsToAdd);\n+  }\n+\n   /// Removes deleted events and updates changed events in a list.\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"dab246","training-data":{"loc-added":"61","loc-deleted":"33","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.8","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tdvu@linagora.com","commit-full-message":"Signed-off-by: dab246 <tdvu@linagora.com>","commit-date":"2026-04-09T09:22:37Z","current-rev":"eeaab49e9","filename":"tmail-flutter/lib/features/push_notification/presentation/listener/email_change_listener.dart","previous-rev":"cb5d43630","commit-title":"TF-4268 Cache Sentry config and user for FCM background re-initialization","language":"Dart","id":"8db8c6f1443acdf8e3fc2ddee6418e9ce86a4208","model-score":0.23,"author-id":null,"project-id":75877,"delta-file-score":1.7926581,"diff":"diff --git a/lib/features/push_notification/presentation/listener/email_change_listener.dart b/lib/features/push_notification/presentation/listener/email_change_listener.dart\nindex 6bb676dee..7188f9e36 100644\n--- a/lib/features/push_notification/presentation/listener/email_change_listener.dart\n+++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart\n@@ -93,15 +93,5 @@ class EmailChangeListener extends ChangeListener {\n       if (action is SynchronizeEmailOnForegroundAction) {\n-        if (PlatformInfo.isAndroid) {\n-          _handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session);\n-        }\n-        _synchronizeEmailOnForegroundAction(action.newState);\n-        if (PlatformInfo.isMobile) {\n-          _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);\n-        }\n+        _onSynchronizeEmailOnForeground(action);\n       } else if (action is PushNotificationAction) {\n-        _pushNotificationAction(action.newState, action.accountId, action.userName, action.session);\n-\n-        if (PlatformInfo.isAndroid) {\n-          _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);\n-        }\n+        _onPushNotification(action);\n       } else if (action is StoreEmailStateToRefreshAction) {\n@@ -114,2 +104,19 @@ class EmailChangeListener extends ChangeListener {\n \n+  void _onSynchronizeEmailOnForeground(SynchronizeEmailOnForegroundAction action) {\n+    if (PlatformInfo.isAndroid) {\n+      _handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session);\n+    }\n+    _synchronizeEmailOnForegroundAction(action.newState);\n+    if (PlatformInfo.isMobile) {\n+      _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);\n+    }\n+  }\n+\n+  void _onPushNotification(PushNotificationAction action) {\n+    _pushNotificationAction(action.newState, action.accountId, action.userName, action.session);\n+    if (PlatformInfo.isAndroid) {\n+      _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState);\n+    }\n+  }\n+\n   void _synchronizeEmailOnForegroundAction(jmap.State newState) {\n@@ -144,5 +151,7 @@ class EmailChangeListener extends ChangeListener {\n   void _getStoredEmailState() {\n-    if (_getStoredEmailStateInteractor != null && _session != null && _accountId != null) {\n-      consumeState(_getStoredEmailStateInteractor!.execute(_session!, _accountId!));\n-    }\n+    final canExecute = _getStoredEmailStateInteractor != null\n+        && _session != null\n+        && _accountId != null;\n+    if (!canExecute) return;\n+    consumeState(_getStoredEmailStateInteractor!.execute(_session!, _accountId!));\n   }\n@@ -150,17 +159,17 @@ class EmailChangeListener extends ChangeListener {\n   void _getEmailChangesAction(jmap.State state) {\n-    if (_getEmailChangesToPushNotificationInteractor != null &&\n-        _accountId != null &&\n-        _session != null &&\n-        _userName != null) {\n-      consumeState(_getEmailChangesToPushNotificationInteractor!.execute(\n+    final canExecute = _getEmailChangesToPushNotificationInteractor != null\n+        && _accountId != null\n+        && _session != null\n+        && _userName != null;\n+    if (!canExecute) return;\n+    consumeState(_getEmailChangesToPushNotificationInteractor!.execute(\n+      _session!,\n+      _accountId!,\n+      _userName!,\n+      state,\n+      propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(\n         _session!,\n         _accountId!,\n-        _userName!,\n-        state,\n-        propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(\n-          _session!,\n-          _accountId!,\n-        ),\n-      ));\n-    }\n+      ),\n+    ));\n   }\n@@ -207,15 +216,8 @@ class EmailChangeListener extends ChangeListener {\n     log('EmailChangeListener::_handleSuccessViewState(): $success');\n-    if (success is GetStoredEmailDeliveryStateSuccess && _newStateEmailDelivery != success.state) {\n-      _getEmailChangesAction(success.state);\n+    if (success is GetStoredEmailDeliveryStateSuccess) {\n+      _handleGetStoredEmailDeliveryStateSuccess(success);\n     } else if (success is GetStoredEmailStateSuccess) {\n       _getEmailChangesAction(success.state);\n-    } else if (success is GetEmailChangesToPushNotificationSuccess && _newStateEmailDelivery != null) {\n-      _storeEmailDeliveryStateAction(success.accountId, success.userName, _newStateEmailDelivery!);\n-\n-      if (PlatformInfo.isAndroid) {\n-        _handleListEmailToPushNotification(\n-          userName: success.userName,\n-          emailList: success.emailList\n-        );\n-      }\n+    } else if (success is GetEmailChangesToPushNotificationSuccess) {\n+      _handleGetEmailChangesToPushNotificationSuccess(success);\n     } else if (success is GetMailboxesNotPutNotificationsSuccess) {\n@@ -242,2 +244,25 @@ class EmailChangeListener extends ChangeListener {\n \n+  void _handleGetStoredEmailDeliveryStateSuccess(GetStoredEmailDeliveryStateSuccess success) {\n+    if (_newStateEmailDelivery != success.state) {\n+      _getEmailChangesAction(success.state);\n+    }\n+  }\n+\n+  void _handleGetEmailChangesToPushNotificationSuccess(GetEmailChangesToPushNotificationSuccess success) {\n+    if (_newStateEmailDelivery == null) return;\n+\n+    _storeEmailDeliveryStateAction(\n+      success.accountId,\n+      success.userName,\n+      _newStateEmailDelivery!,\n+    );\n+\n+    if (PlatformInfo.isAndroid) {\n+      _handleListEmailToPushNotification(\n+        userName: success.userName,\n+        emailList: success.emailList,\n+      );\n+    }\n+  }\n+\n   void _handleListEmailToPushNotification({\n@@ -247,3 +272,6 @@ class EmailChangeListener extends ChangeListener {\n     _emailsAvailablePushNotification = emailList;\n-    if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) {\n+    final canFilterByMailbox = _getMailboxesNotPutNotificationsInteractor != null\n+        && _accountId != null\n+        && _session != null;\n+    if (canFilterByMailbox) {\n       consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!));\n@@ -253,3 +281,3 @@ class EmailChangeListener extends ChangeListener {\n         userName: userName,\n-        emailList: listEmails\n+        emailList: listEmails,\n       );\n@@ -321,17 +349,17 @@ class EmailChangeListener extends ChangeListener {\n     log('EmailChangeListener::_getListDetailedEmailByIdAction():emailIds: $emailIds');\n-    if (_getListDetailedEmailByIdInteractor != null &&\n-        _dynamicUrlInterceptors != null &&\n-        session != null) {\n-      try {\n-        final baseDownloadUrl = session.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors!.jmapUrl);\n-        consumeState(_getListDetailedEmailByIdInteractor!.execute(\n-            session,\n-            accountId,\n-            emailIds,\n-            baseDownloadUrl\n-        ));\n-      } catch (e) {\n-        logWarning('EmailChangeListener::_getListDetailedEmailByIdAction(): $e');\n-        consumeState(Stream.value(Left(GetDetailedEmailByIdFailure(e))));\n-      }\n+    final canExecute = _getListDetailedEmailByIdInteractor != null\n+        && _dynamicUrlInterceptors != null\n+        && session != null;\n+    if (!canExecute) return;\n+    try {\n+      final baseDownloadUrl = session.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors!.jmapUrl);\n+      consumeState(_getListDetailedEmailByIdInteractor!.execute(\n+        session,\n+        accountId,\n+        emailIds,\n+        baseDownloadUrl,\n+      ));\n+    } catch (e) {\n+      logWarning('EmailChangeListener::_getListDetailedEmailByIdAction(): $e');\n+      consumeState(Stream.value(Left(GetDetailedEmailByIdFailure(e))));\n     }\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dat H. Pham","training-data":{"loc-added":"71","loc-deleted":"74","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"2.43","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"dphamhoang@linagora.com","commit-full-message":"When a received email contains an inline attachment with a CID but no\nmatching img[src=\"cid:...\"] in the HTML body, tmail previously hid it\ncompletely — not shown in the body (no img tag) and not in the attachment\npanel (isOutsideAttachment returned false due to hasCid+isDispositionInlined).\n\nIn GetEmailContentInteractor, extract referenced CIDs from the raw HTML\nbefore transformation, then split allInlineImages into referenced (rendered\nin body) and orphaned (no body reference). Orphaned attachments are promoted\nto the attachment panel so users can see and download them.","commit-date":"2026-05-10T22:56:08Z","current-rev":"86c4b4ca2","filename":"tmail-flutter/lib/features/email/data/local/html_analyzer.dart","previous-rev":"e7d9c421b","commit-title":"fix: show orphaned inline attachments in email attachment panel","language":"Dart","id":"986401aa661d8135fb6258258047a4f53a6e28e1","model-score":0.23,"author-id":null,"project-id":75877,"delta-file-score":1.5681522,"diff":"diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart\nindex b90bce023..ec83d99c2 100644\n--- a/lib/features/email/data/local/html_analyzer.dart\n+++ b/lib/features/email/data/local/html_analyzer.dart\n@@ -11,2 +11,3 @@ import 'package:core/utils/string_convert.dart';\n import 'package:dartz/dartz.dart';\n+import 'package:html/dom.dart';\n import 'package:html/parser.dart';\n@@ -64,44 +65,18 @@ class HtmlAnalyzer {\n       final document = parse(emailContents);\n-\n-      final openPaasLinkElements = document.querySelectorAll('a.part-button');\n-      if (openPaasLinkElements.isNotEmpty) {\n-        final listEventAction = openPaasLinkElements\n-          .mapIndexed((index, element) {\n-            final hrefLink = element.attributes['href'] ?? '';\n-            if (hrefLink.isNotEmpty) {\n-              if (index == 0) {\n-                return EventAction(EventActionType.yes, hrefLink);\n-              } else if (index == 1) {\n-                return EventAction(EventActionType.maybe, hrefLink);\n-              } else if (index == 2) {\n-                return EventAction(EventActionType.no, hrefLink);\n-              }\n-            }\n-            return null;\n-          })\n-          .nonNulls\n-          .toList();\n-        log('HtmlAnalyzer::getListEventAction:OPEN_PAAS::listEventAction: $listEventAction');\n-        return listEventAction;\n-      } else {\n-        final googleLinkElements = document.querySelectorAll('a.grey-button-text');\n-        final listEventAction = googleLinkElements\n-          .mapIndexed((index, element) {\n-            final hrefLink = element.attributes['href'] ?? '';\n-            if (hrefLink.isNotEmpty) {\n-              if (index == 0) {\n-                return EventAction(EventActionType.yes, hrefLink);\n-              } else if (index == 1) {\n-                return EventAction(EventActionType.no, hrefLink);\n-              } else if (index == 2) {\n-                return EventAction(EventActionType.maybe, hrefLink);\n-              }\n-            }\n-            return null;\n-          })\n-          .nonNulls\n-          .toList();\n-        log('HtmlAnalyzer::getListEventAction:GOOGLE::listEventAction: $listEventAction');\n-        return listEventAction;\n+      final openPaasElements = document.querySelectorAll('a.part-button');\n+      if (openPaasElements.isNotEmpty) {\n+        final result = _extractEventActions(\n+          openPaasElements,\n+          [EventActionType.yes, EventActionType.maybe, EventActionType.no],\n+        );\n+        log('HtmlAnalyzer::getListEventAction:OPEN_PAAS::listEventAction: $result');\n+        return result;\n       }\n+      final googleElements = document.querySelectorAll('a.grey-button-text');\n+      final result = _extractEventActions(\n+        googleElements,\n+        [EventActionType.yes, EventActionType.no, EventActionType.maybe],\n+      );\n+      log('HtmlAnalyzer::getListEventAction:GOOGLE::listEventAction: $result');\n+      return result;\n     } catch(e) {\n@@ -112,2 +87,16 @@ class HtmlAnalyzer {\n \n+  List<EventAction> _extractEventActions(\n+    List<Element> elements,\n+    List<EventActionType> typeOrder,\n+  ) {\n+    return elements\n+      .mapIndexed((index, element) {\n+        final hrefLink = element.attributes['href'] ?? '';\n+        if (hrefLink.isEmpty || index >= typeOrder.length) return null;\n+        return EventAction(typeOrder[index], hrefLink);\n+      })\n+      .nonNulls\n+      .toList();\n+  }\n+\n   Future<String> transformHtmlEmailContent(\n@@ -130,2 +119,3 @@ class HtmlAnalyzer {\n     final listImgTag = document.querySelectorAll('img[src^=\"data:image/\"]');\n+    final cidImgTags = document.querySelectorAll('img[src^=\"$cidPrefixKey\"]');\n \n@@ -133,21 +123,4 @@ class HtmlAnalyzer {\n \n-    if (listImgTag.isEmpty) {\n-      if (inlineAttachments.isEmpty) return Tuple2(emailContent, {});\n-\n-      // Only include attachments that are actually referenced by a cid: img tag in the body.\n-      // Orphaned inline attachments (CID present in metadata but no body reference) must not\n-      // be re-forwarded, as this produces sent emails with unreferenced attachments.\n-      final cidImgTags = document.querySelectorAll('img[src^=\"$cidPrefixKey\"]');\n-      if (cidImgTags.isEmpty) return Tuple2(emailContent, {});\n-\n-      final referencedCids = cidImgTags\n-        .map((img) => img.attributes['src']!.substring(cidPrefixKey.length).trim())\n-        .toSet();\n-\n-      final referencedAttachments = inlineAttachments.entries\n-        .where((entry) => referencedCids.contains(entry.key))\n-        .map((entry) => entry.value.toEmailBodyPart(charset: Constant.base64Charset))\n-        .toSet();\n-\n-      return Tuple2(emailContent, referencedAttachments);\n+    if (listImgTag.isEmpty && (inlineAttachments.isEmpty || cidImgTags.isEmpty)) {\n+      return Tuple2(emailContent, {});\n     }\n@@ -157,52 +130,77 @@ class HtmlAnalyzer {\n     for (final imgTag in listImgTag) {\n-      final attributes = imgTag.attributes;\n-      final imageSrc = attributes['src'];\n+      final imageSrc = imgTag.attributes['src'];\n       if (imageSrc?.isEmpty ?? true) continue;\n+      final bodyPart = await _processBase64ImageTag(\n+        attributes: imgTag.attributes,\n+        inlineAttachments: inlineAttachments,\n+        uploadUri: uploadUri,\n+        imageSrc: imageSrc!,\n+      );\n+      if (bodyPart != null) inlineAttachmentsSet.add(bodyPart);\n+    }\n+\n+    // Include attachments for cid: images whose download failed during view (never converted to base64).\n+    _includeCidImageAttachments(\n+      cidImgTags: cidImgTags,\n+      inlineAttachments: inlineAttachments,\n+      inlineAttachmentsSet: inlineAttachmentsSet,\n+    );\n \n-      final idImg = attributes['id'];\n-      if (idImg?.startsWith(cidPrefixKey) == true) {\n-        final cid = idImg!.substring(cidPrefixKey.length).trim();\n-        final attachment = inlineAttachments[cid];\n+    return Tuple2(document.body?.innerHtml ?? emailContent, inlineAttachmentsSet);\n+  }\n \n-        if (attachment != null) {\n-          attributes['src'] = '$cidPrefixKey$cid';\n-          attributes.remove('id');\n-          inlineAttachmentsSet.add(attachment.toEmailBodyPart(charset: Constant.base64Charset));\n-          continue;\n-        }\n+  Future<EmailBodyPart?> _processBase64ImageTag({\n+    required Map<Object, String> attributes,\n+    required Map<String, Attachment> inlineAttachments,\n+    required Uri? uploadUri,\n+    required String imageSrc,\n+  }) async {\n+    final idImg = attributes['id'];\n+\n+    if (idImg?.startsWith(cidPrefixKey) == true) {\n+      final cid = idImg!.substring(cidPrefixKey.length).trim();\n+      final attachment = inlineAttachments[cid];\n+      if (attachment != null) {\n+        attributes['src'] = '$cidPrefixKey$cid';\n+        attributes.remove('id');\n+        return attachment.toEmailBodyPart(charset: Constant.base64Charset);\n       }\n+    }\n \n-      if (uploadUri == null) continue;\n+    if (uploadUri == null) return null;\n \n-      final taskId = idImg?.startsWith(cidPrefixKey) == true\n-        ? idImg!.substring(cidPrefixKey.length)\n-        : _uuid.v1();\n+    final taskId = idImg?.startsWith(cidPrefixKey) == true\n+      ? idImg!.substring(cidPrefixKey.length)\n+      : _uuid.v1();\n \n-      final attachmentRecord = await _retrieveAttachmentFromUpload(\n-        taskId: taskId,\n-        uploadUri: uploadUri,\n-        base64ImageTag: imageSrc!,\n-      );\n+    final attachmentRecord = await _retrieveAttachmentFromUpload(\n+      taskId: taskId,\n+      uploadUri: uploadUri,\n+      base64ImageTag: imageSrc,\n+    );\n \n-      if (attachmentRecord == null) continue;\n+    if (attachmentRecord == null) return null;\n \n-      final newInlineAttachment = attachmentRecord.$1.toAttachmentWithDisposition(\n-        disposition: ContentDisposition.inline,\n-        cid: attachmentRecord.$2,\n-      );\n+    final newInlineAttachment = attachmentRecord.$1.toAttachmentWithDisposition(\n+      disposition: ContentDisposition.inline,\n+      cid: attachmentRecord.$2,\n+    );\n \n-      final newCid = newInlineAttachment.cid!;\n-      inlineAttachments[newCid] = newInlineAttachment;\n-      attributes['src'] = '$cidPrefixKey$newCid';\n-      attributes.remove('id');\n+    final newCid = newInlineAttachment.cid!;\n+    inlineAttachments[newCid] = newInlineAttachment;\n+    attributes['src'] = '$cidPrefixKey$newCid';\n+    attributes.remove('id');\n \n-      inlineAttachmentsSet.add(newInlineAttachment.toEmailBodyPart(charset: Constant.base64Charset));\n-    }\n+    return newInlineAttachment.toEmailBodyPart(charset: Constant.base64Charset);\n+  }\n \n-    // Include attachments for cid: images that were never converted to base64\n-    // (e.g. download failed during view). These are not in listImgTag but still\n-    // need their attachment included so the recipient can load them.\n-    final remainingCidImgs = document.querySelectorAll('img[src^=\"$cidPrefixKey\"]');\n-    for (final img in remainingCidImgs) {\n-      final cid = img.attributes['src']!.substring(cidPrefixKey.length).trim();\n+  void _includeCidImageAttachments({\n+    required List<Element> cidImgTags,\n+    required Map<String, Attachment> inlineAttachments,\n+    required Set<EmailBodyPart> inlineAttachmentsSet,\n+  }) {\n+    for (final img in cidImgTags) {\n+      final src = img.attributes['src'];\n+      if (src == null) continue;\n+      final cid = src.substring(cidPrefixKey.length).trim();\n       final attachment = inlineAttachments[cid];\n@@ -212,4 +210,2 @@ class HtmlAnalyzer {\n     }\n-\n-    return Tuple2(document.body?.innerHtml ?? emailContent, inlineAttachmentsSet);\n   }\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Dat Dang","training-data":{"loc-added":"91","loc-deleted":"65","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"2.07","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"tddang@linagora.com","commit-full-message":"","commit-date":"2026-05-22T07:12:33Z","current-rev":"bdf59ce9e","filename":"tmail-flutter/lib/features/login/data/network/interceptors/authorization_interceptors.dart","previous-rev":"b52671da7","commit-title":"TF-4081 Prevent network error log user out on mobile (#4531)","language":"Dart","id":"1458704167ed90a40e5cfff89b411b24d3866929","model-score":0.21,"author-id":null,"project-id":75877,"delta-file-score":0.6334563,"diff":"diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart\nindex a304b7fcd..722dbe428 100644\n--- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart\n+++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart\n@@ -13,2 +13,3 @@ import 'package:model/oidc/oidc_configuration.dart';\n import 'package:model/oidc/token_oidc.dart';\n+import 'package:tmail_ui_user/features/base/extensions/object_extensions.dart';\n import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart';\n@@ -16,3 +17,2 @@ import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager\n import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';\n-import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart';\n import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';\n@@ -90,6 +90,3 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {\n       final requestOptions = err.requestOptions;\n-      final extraInRequest = requestOptions.extra;\n-      bool isRetryRequest = false;\n-\n-      final hasAttemptedRefresh = extraInRequest[_refreshAttemptedKey] == true;\n+      final hasAttemptedRefresh = requestOptions.extra[_refreshAttemptedKey] == true;\n \n@@ -101,3 +98,3 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {\n         log('AuthorizationInterceptors::onError: Request using old token, retry with updated token');\n-        isRetryRequest = true;\n+        return await _performRetry(requestOptions, err, handler);\n       } else if (!hasAttemptedRefresh && validateToRefreshToken(\n@@ -106,93 +103,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {\n       )) {\n-        try {\n-          log('AuthorizationInterceptors::onError: Perform get New Token');\n-          final newTokenOidc = PlatformInfo.isIOS\n-            ? await _getNewTokenForIOSPlatform()\n-            : await _getNewTokenForOtherPlatform();\n-\n-          if (newTokenOidc.token == _token?.token) {\n-            log('AuthorizationInterceptors::onError: Token duplicated');\n-            return super.onError(err, handler);\n-          }\n-          _updateNewToken(newTokenOidc);\n-\n-          final personalAccount = await _updateCurrentAccount(tokenOIDC: newTokenOidc);\n-\n-          if (PlatformInfo.isIOS) {\n-            await _iosSharingManager.saveKeyChainSharingSession(personalAccount);\n-          }\n-\n-          requestOptions.extra[_refreshAttemptedKey] = true;\n-          isRetryRequest = true;\n-        } on DioException catch (refreshError, st) {\n-          if (refreshError.response?.statusCode == 400) {\n-            logError(\n-              'AuthorizationInterceptors: Refresh Token Failed 400',\n-              exception: refreshError,\n-              stackTrace: st,\n-            );\n-\n-            clear();\n-\n-            final sessionExpiredError = DioException(\n-              requestOptions: err.requestOptions,\n-              error: RefreshTokenFailedException(),\n-              type: DioExceptionType.badResponse,\n-              response: refreshError.response,\n-            );\n-\n-            return handler.reject(sessionExpiredError);\n-          }\n-\n-          logError(\n-            'AuthorizationInterceptors: Refresh token failed with '\n-            'statusCode=${refreshError.response?.statusCode}',\n-            exception: refreshError,\n-            stackTrace: st,\n-          );\n-\n-          if (refreshError is ServerError ||\n-              refreshError is TemporarilyUnavailable) {\n-            return super.onError(\n-              DioException(\n-                requestOptions: err.requestOptions,\n-                error: refreshError,\n-              ),\n-              handler,\n-            );\n-          } else {\n-            return super.onError(err.copyWith(error: refreshError), handler);\n-          }\n-        }\n-      } else {\n-        logTrace(\n-          'AuthorizationInterceptors::onError: '\n-          'No retry or refresh applicable. '\n-          'statusCode = ${err.response?.statusCode} | '\n-          'authType = $_authenticationType | '\n-          'hasConfig = ${_configOIDC != null} | '\n-          'hasAttemptedRefresh = $hasAttemptedRefresh | '\n-          'url = ${err.requestOptions.uri}',\n-          webConsoleEnabled: true,\n-        );\n-        return super.onError(err, handler);\n+        return await _refreshTokenThenRetry(err, requestOptions, handler);\n       }\n \n-      if (isRetryRequest) {\n-        try {\n-          final response = await _retryRequest(requestOptions, extraInRequest);\n-          return handler.resolve(response);\n-        } catch (retryError) {\n-          logError(\n-            'AuthorizationInterceptors::onError: '\n-            'Retry failed with error=$retryError',\n-          );\n-          return super.onError(\n-            err.copyWith(error: retryError),\n-            handler,\n-          );\n-        }\n-      } else {\n-        return super.onError(err, handler);\n-      }\n+      logTrace(\n+        'AuthorizationInterceptors::onError: '\n+        'No retry or refresh applicable. '\n+        'statusCode = ${err.response?.statusCode} | '\n+        'authType = $_authenticationType | '\n+        'hasConfig = ${_configOIDC != null} | '\n+        'hasAttemptedRefresh = $hasAttemptedRefresh | '\n+        'url = ${err.requestOptions.uri}',\n+        webConsoleEnabled: true,\n+      );\n+      return super.onError(err, handler);\n     } catch (e, stackTrace) {\n@@ -203,11 +123,117 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {\n       );\n-      if (e is ServerError || e is TemporarilyUnavailable) {\n-        return super.onError(\n-          DioException(requestOptions: err.requestOptions, error: e),\n-          handler,\n+      return _handleThrownException(\n+        e,\n+        handler: handler,\n+        requestOptions: err.requestOptions,\n+      );\n+    }\n+  }\n+\n+  void _handleThrownException(\n+    Object exception, {\n+    required ErrorInterceptorHandler handler,\n+    required RequestOptions requestOptions,\n+  }) {\n+    if (exception is DioException) {\n+      return super.onError(exception, handler);\n+    }\n+    return super.onError(\n+      exception.toDioException(requestOptions: requestOptions),\n+      handler,\n+    );\n+  }\n+\n+  Future<void> _refreshTokenThenRetry(\n+    DioException err,\n+    RequestOptions requestOptions,\n+    ErrorInterceptorHandler handler,\n+  ) async {\n+    try {\n+      log('AuthorizationInterceptors::onError: Perform get New Token');\n+      final newTokenOidc = PlatformInfo.isIOS\n+        ? await _getNewTokenForIOSPlatform()\n+        : await _getNewTokenForOtherPlatform();\n+\n+      if (newTokenOidc.token == _token?.token) {\n+        logError('AuthorizationInterceptors::onError: Token duplicated', exception: err);\n+        return super.onError(err, handler);\n+      }\n+      _updateNewToken(newTokenOidc);\n+\n+      final personalAccount = await _updateCurrentAccount(tokenOIDC: newTokenOidc);\n+\n+      if (PlatformInfo.isIOS) {\n+        await _iosSharingManager.saveKeyChainSharingSession(personalAccount);\n+      }\n+\n+      requestOptions.extra[_refreshAttemptedKey] = true;\n+      return await _performRetry(requestOptions, err, handler);\n+    } on DioException catch (refreshError, st) {\n+      if (refreshError.response?.statusCode == 400) {\n+        logWarning(\n+          'AuthorizationInterceptors: Refresh Token Failed 400 - error=$refreshError | stackTrace=$st',\n         );\n-      } else {\n-        return super.onError(err.copyWith(error: e), handler);\n+        clear();\n+        return handler.reject(DioException(\n+          requestOptions: err.requestOptions,\n+          error: RefreshTokenFailedException(),\n+          type: DioExceptionType.badResponse,\n+          response: refreshError.response,\n+        ));\n       }\n+      logWarning(\n+        'AuthorizationInterceptors: Refresh token failed with '\n+        'statusCode=${refreshError.response?.statusCode} \\n'\n+        'error=$refreshError | stackTrace=$st',\n+      );\n+      return _handleDioRefreshError(\n+        refreshError: refreshError,\n+        originalError: err,\n+        handler: handler,\n+      );\n+    }\n+  }\n+\n+  Future<void> _performRetry(\n+    RequestOptions requestOptions,\n+    DioException originalErr,\n+    ErrorInterceptorHandler handler,\n+  ) async {\n+    try {\n+      final response = await _retryRequest(requestOptions, requestOptions.extra);\n+      return handler.resolve(response);\n+    } catch (retryError) {\n+      logError(\n+        'AuthorizationInterceptors::onError: '\n+        'Retry failed with error=$retryError',\n+      );\n+      // Pass retry errors directly so the stale 401 response is not preserved\n+      // via err.copyWith.\n+      return _handleThrownException(\n+        retryError,\n+        handler: handler,\n+        requestOptions: originalErr.requestOptions,\n+      );\n+    }\n+  }\n+\n+  void _handleDioRefreshError({\n+    required DioException refreshError,\n+    required DioException originalError,\n+    required ErrorInterceptorHandler handler,\n+  }) {\n+    // Network failure during refresh — don't carry the original 401 response\n+    // forward, as that would make RemoteExceptionThrower classify this as\n+    // BadCredentialsException and log the user out.\n+    if (refreshError.response == null) {\n+      return super.onError(\n+        refreshError.error.toDioException(\n+          requestOptions: originalError.requestOptions,\n+          type: refreshError.type,\n+          message: refreshError.message,\n+        ),\n+        handler,\n+      );\n     }\n+    return super.onError(refreshError, handler);\n   }\n","improvement-type":"Complex Method"}],"change-level":"warning","is-hotspot?":false,"line":29,"what-changed":"onMessage has a cyclomatic complexity of 10, threshold = 9","how-to-fix":"There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using [EXTRACT FUNCTION](https://refactoring.com/catalog/extractFunction.html). Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring.","change-type":"introduced"}]},"positive-impact-count":0,"repo":"tmail-flutter","code-health":8.90534232435871,"version":"3.0","authors":["Dang Dat"],"directives":{"added":[],"removed":[]},"positive-findings":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"notices":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"external-review-provider":"GitHub"},"analysistime":"2026-06-05T08:08:49.000Z","project-name":"james-project","repository":"https://github.com/linagora/tmail-flutter.git"}}