TF-4265 Refactor: Move Regex pattern generation to Main Thread to leverage static caching and reduce Isolate initialization overhead.
This commit is contained in:
@@ -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.
|
* 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.
|
* 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
|
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`.
|
* This file must be declared in `pubspec.yaml` under `assets`.
|
||||||
|
|
||||||
2. JSON Structure
|
2. JSON Structure
|
||||||
|
|||||||
@@ -882,10 +882,7 @@ class ComposerController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
final emailContent = await getContentInEditor();
|
final emailContent = await getContentInEditor();
|
||||||
if (!context.mounted) {
|
|
||||||
_sendButtonState = ButtonState.enabled;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final attachmentKeywords = await validateAttachmentReminder(
|
final attachmentKeywords = await validateAttachmentReminder(
|
||||||
emailSubject: subjectEmail.value ?? '',
|
emailSubject: subjectEmail.value ?? '',
|
||||||
emailContent: emailContent,
|
emailContent: emailContent,
|
||||||
@@ -893,6 +890,7 @@ class ComposerController extends BaseController
|
|||||||
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED');
|
logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED');
|
||||||
|
_sendButtonState = ButtonState.enabled;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,12 @@ class AttachmentTextDetector {
|
|||||||
"مستند",
|
"مستند",
|
||||||
"ملف",
|
"ملف",
|
||||||
"تقرير",
|
"تقرير",
|
||||||
"ملف",
|
|
||||||
"إرفاق",
|
"إرفاق",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Cached RegExp for the default case (when no include list is provided).
|
/// Cached RegExp Pattern for the default case (when no include list is provided).
|
||||||
static RegExp? _cachedDefaultRegExp;
|
static String? _cachedDefaultPattern;
|
||||||
/// Threshold to switch from Sync to Async execution.
|
/// Threshold to switch from Sync to Async execution.
|
||||||
///
|
///
|
||||||
/// Value: 20,000 characters (approx. 4-5 pages of text).
|
/// Value: 20,000 characters (approx. 4-5 pages of text).
|
||||||
@@ -118,7 +117,7 @@ class AttachmentTextDetector {
|
|||||||
|
|
||||||
/// Builds the Regex pattern.
|
/// Builds the Regex pattern.
|
||||||
/// Merges [defaultKeywords] with [includeList] to create the search scope.
|
/// Merges [defaultKeywords] with [includeList] to create the search scope.
|
||||||
static RegExp _buildRegExp(List<String> additionalKeywords) {
|
static String _generatePatternString(List<String> additionalKeywords) {
|
||||||
// 1. Get all default keywords
|
// 1. Get all default keywords
|
||||||
final defaultKeywords = _keywordsByLang.values.expand((l) => l).toList();
|
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").
|
// (?![\p{L}]) ensures we don't match substrings inside other words (e.g., "filetage").
|
||||||
final pattern = allKeywords.map(RegExp.escape).join('|');
|
final pattern = allKeywords.map(RegExp.escape).join('|');
|
||||||
|
|
||||||
return RegExp(
|
return'($pattern)(?![\\p{L}])';
|
||||||
'($pattern)(?![\\p{L}])',
|
}
|
||||||
unicode: true,
|
|
||||||
caseSensitive: false,
|
static String _getPattern(List<String> includeList) {
|
||||||
);
|
if (includeList.isNotEmpty) {
|
||||||
|
return _generatePatternString(includeList);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedDefaultPattern ??= _generatePatternString([]);
|
||||||
|
return _cachedDefaultPattern!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The core logic function.
|
/// The core logic function.
|
||||||
@@ -148,16 +152,11 @@ class AttachmentTextDetector {
|
|||||||
final text = params.text;
|
final text = params.text;
|
||||||
if (text.isEmpty) return [];
|
if (text.isEmpty) return [];
|
||||||
|
|
||||||
RegExp regex;
|
final regex = RegExp(
|
||||||
|
params.regexPattern,
|
||||||
// Use cached Regex if there are no custom keywords.
|
unicode: true,
|
||||||
if (params.includeList.isEmpty) {
|
caseSensitive: false,
|
||||||
_cachedDefaultRegExp ??= _buildRegExp([]);
|
);
|
||||||
regex = _cachedDefaultRegExp!;
|
|
||||||
} else {
|
|
||||||
// Rebuild Regex if Include List is present.
|
|
||||||
regex = _buildRegExp(params.includeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
final matches = regex.allMatches(text);
|
final matches = regex.allMatches(text);
|
||||||
final result = <String>{};
|
final result = <String>{};
|
||||||
@@ -199,10 +198,12 @@ class AttachmentTextDetector {
|
|||||||
filters.add(ExcludeListFilter(excludeList));
|
filters.add(ExcludeListFilter(excludeList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final patternString = _getPattern(includeList);
|
||||||
|
|
||||||
final params = DetectionParams(
|
final params = DetectionParams(
|
||||||
text: text,
|
text: text,
|
||||||
includeList: includeList, // Passed to build the Regex
|
regexPattern: patternString,
|
||||||
filters: filters, // Passed to filter the matches
|
filters: filters,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decision: Run Sync (fast) or Async (safe)
|
// Decision: Run Sync (fast) or Async (safe)
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import 'package:tmail_ui_user/features/composer/presentation/manager/keyword_fil
|
|||||||
/// Isolates cannot share memory, so we package everything here.
|
/// Isolates cannot share memory, so we package everything here.
|
||||||
class DetectionParams {
|
class DetectionParams {
|
||||||
final String text;
|
final String text;
|
||||||
final List<String> includeList; // Keywords to ADD to the search.
|
final String regexPattern;
|
||||||
final List<KeywordFilter> filters; // Filters to BLOCK specific matches.
|
final List<KeywordFilter> filters; // Filters to BLOCK specific matches.
|
||||||
|
|
||||||
DetectionParams({
|
DetectionParams({
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.includeList,
|
required this.regexPattern,
|
||||||
required this.filters,
|
required this.filters,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,10 @@ class ExcludeListFilter with TokenExtractionMixin implements KeywordFilter {
|
|||||||
final cleanToken = lowerToken.replaceAll(RegExp(r'[^\w\s]+$'), '');
|
final cleanToken = lowerToken.replaceAll(RegExp(r'[^\w\s]+$'), '');
|
||||||
if (_excludes.contains(cleanToken)) return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user