From 74c0c75023abad2ce58f75a77ced7d13f8953252 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 30 Jan 2026 15:36:04 +0700 Subject: [PATCH] TF-4265 Refactor: Move Regex pattern generation to Main Thread to leverage static caching and reduce Isolate initialization overhead. --- .../attachment_keywords_configuration.md | 4 +- .../presentation/composer_controller.dart | 6 +-- .../manager/attachment_text_detector.dart | 43 ++++++++++--------- .../manager/detection_params.dart | 4 +- .../manager/exclude_list_filter.dart | 4 ++ 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/configuration/attachment_keywords_configuration.md b/docs/configuration/attachment_keywords_configuration.md index 45e89be0c..7fad1e729 100644 --- a/docs/configuration/attachment_keywords_configuration.md +++ b/docs/configuration/attachment_keywords_configuration.md @@ -5,11 +5,11 @@ * When a user composes an email, the system detects if the content implies an attachment (e.g., "Please find attached...") but no file is attached. * We need a flexible way to add specific keywords (Include) or ignore specific tokens (Exclude) without modifying the source code. -### How to config +### How to configure 1. Configuration File Location - * The configuration is managed in the JSON file: `configurations\attachment_keywords.json` + * The configuration is managed in the JSON file: `configurations/attachment_keywords.json` * This file must be declared in `pubspec.yaml` under `assets`. 2. JSON Structure diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3e9be0aeb..b7c0e4ac5 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -882,10 +882,7 @@ class ComposerController extends BaseController } final emailContent = await getContentInEditor(); - if (!context.mounted) { - _sendButtonState = ButtonState.enabled; - return; - } + final attachmentKeywords = await validateAttachmentReminder( emailSubject: subjectEmail.value ?? '', emailContent: emailContent, @@ -893,6 +890,7 @@ class ComposerController extends BaseController if (!context.mounted) { logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED'); + _sendButtonState = ButtonState.enabled; return; } diff --git a/lib/features/composer/presentation/manager/attachment_text_detector.dart b/lib/features/composer/presentation/manager/attachment_text_detector.dart index eb3d2647a..beed66b1a 100644 --- a/lib/features/composer/presentation/manager/attachment_text_detector.dart +++ b/lib/features/composer/presentation/manager/attachment_text_detector.dart @@ -46,13 +46,12 @@ class AttachmentTextDetector { "مستند", "ملف", "تقرير", - "ملف", "إرفاق", ], }; - /// Cached RegExp for the default case (when no include list is provided). - static RegExp? _cachedDefaultRegExp; + /// Cached RegExp Pattern for the default case (when no include list is provided). + static String? _cachedDefaultPattern; /// Threshold to switch from Sync to Async execution. /// /// Value: 20,000 characters (approx. 4-5 pages of text). @@ -118,7 +117,7 @@ class AttachmentTextDetector { /// Builds the Regex pattern. /// Merges [defaultKeywords] with [includeList] to create the search scope. - static RegExp _buildRegExp(List additionalKeywords) { + static String _generatePatternString(List additionalKeywords) { // 1. Get all default keywords final defaultKeywords = _keywordsByLang.values.expand((l) => l).toList(); @@ -133,11 +132,16 @@ class AttachmentTextDetector { // (?![\p{L}]) ensures we don't match substrings inside other words (e.g., "filetage"). final pattern = allKeywords.map(RegExp.escape).join('|'); - return RegExp( - '($pattern)(?![\\p{L}])', - unicode: true, - caseSensitive: false, - ); + return'($pattern)(?![\\p{L}])'; + } + + static String _getPattern(List includeList) { + if (includeList.isNotEmpty) { + return _generatePatternString(includeList); + } + + _cachedDefaultPattern ??= _generatePatternString([]); + return _cachedDefaultPattern!; } /// The core logic function. @@ -148,16 +152,11 @@ class AttachmentTextDetector { final text = params.text; if (text.isEmpty) return []; - RegExp regex; - - // Use cached Regex if there are no custom keywords. - if (params.includeList.isEmpty) { - _cachedDefaultRegExp ??= _buildRegExp([]); - regex = _cachedDefaultRegExp!; - } else { - // Rebuild Regex if Include List is present. - regex = _buildRegExp(params.includeList); - } + final regex = RegExp( + params.regexPattern, + unicode: true, + caseSensitive: false, + ); final matches = regex.allMatches(text); final result = {}; @@ -199,10 +198,12 @@ class AttachmentTextDetector { filters.add(ExcludeListFilter(excludeList)); } + final patternString = _getPattern(includeList); + final params = DetectionParams( text: text, - includeList: includeList, // Passed to build the Regex - filters: filters, // Passed to filter the matches + regexPattern: patternString, + filters: filters, ); // Decision: Run Sync (fast) or Async (safe) diff --git a/lib/features/composer/presentation/manager/detection_params.dart b/lib/features/composer/presentation/manager/detection_params.dart index 384949048..f0b721e01 100644 --- a/lib/features/composer/presentation/manager/detection_params.dart +++ b/lib/features/composer/presentation/manager/detection_params.dart @@ -4,12 +4,12 @@ import 'package:tmail_ui_user/features/composer/presentation/manager/keyword_fil /// Isolates cannot share memory, so we package everything here. class DetectionParams { final String text; - final List includeList; // Keywords to ADD to the search. + final String regexPattern; final List filters; // Filters to BLOCK specific matches. DetectionParams({ required this.text, - required this.includeList, + required this.regexPattern, required this.filters, }); } \ No newline at end of file diff --git a/lib/features/composer/presentation/manager/exclude_list_filter.dart b/lib/features/composer/presentation/manager/exclude_list_filter.dart index a83069a82..f468bd1e5 100644 --- a/lib/features/composer/presentation/manager/exclude_list_filter.dart +++ b/lib/features/composer/presentation/manager/exclude_list_filter.dart @@ -23,6 +23,10 @@ class ExcludeListFilter with TokenExtractionMixin implements KeywordFilter { final cleanToken = lowerToken.replaceAll(RegExp(r'[^\w\s]+$'), ''); if (_excludes.contains(cleanToken)) return false; + // Check 3: Block even if it has leading punctuation (e.g., "(file") + final fullyCleanToken = cleanToken.replaceAll(RegExp(r'^[^\w\s]+'), ''); + if (_excludes.contains(fullyCleanToken)) return false; + return true; } }