From 55e27c78edcc995bf35ea1f2c74b705fe16682b4 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 30 Jan 2026 14:38:33 +0700 Subject: [PATCH] TF-4265 Additional config for include and exclude keywords --- configurations/attachment_keywords.json | 4 + .../attachment_keywords_configuration.md | 77 +++++++++++++++++++ .../presentation/composer_controller.dart | 6 ++ .../attachment_detection_extension.dart | 9 ++- ...achment_keywords_configuration_parser.dart | 26 +++++++ .../manager/keyword_config_manager.dart | 43 +++++++++++ .../presentation/model/keyword_config.dart | 23 ++++++ lib/main/utils/app_config.dart | 1 + 8 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 configurations/attachment_keywords.json create mode 100644 docs/configuration/attachment_keywords_configuration.md create mode 100644 lib/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart create mode 100644 lib/features/composer/presentation/manager/keyword_config_manager.dart create mode 100644 lib/features/composer/presentation/model/keyword_config.dart diff --git a/configurations/attachment_keywords.json b/configurations/attachment_keywords.json new file mode 100644 index 000000000..0bd0ed28d --- /dev/null +++ b/configurations/attachment_keywords.json @@ -0,0 +1,4 @@ +{ + "includeList": [], + "excludeList": [] +} \ No newline at end of file diff --git a/docs/configuration/attachment_keywords_configuration.md b/docs/configuration/attachment_keywords_configuration.md new file mode 100644 index 000000000..45e89be0c --- /dev/null +++ b/docs/configuration/attachment_keywords_configuration.md @@ -0,0 +1,77 @@ +## Configuration for Attachment Keyword Detection + +### Context + +* 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 + +1. Configuration File Location + + * 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 + +The file consists of two main arrays: + +```json +{ + "includeList": [ + "invoice", + "estimate", + "báo giá" + ], + "excludeList": [ + "signature-logo", + "no-reply", + "icon-app" + ] +} + +``` + +* `includeList`: List of keywords to **add** to the detection dictionary. + * Used when the default dictionary (hardcoded in app) is missing specific business terms. + * Matches are case-insensitive. + * Example: Adding `"cv"` will trigger the alert for "Attached is my CV". + + +* `excludeList`: List of tokens to **ignore/block** from detection. + * Used to prevent false positives where a keyword exists but is part of a system string or signature. + * The filter checks the full surrounding token (e.g., if you exclude `"signature-logo"`, the text `"check signature-logo"` will be ignored even if it contains "signature"). + + + +3. Application Logic + +* **Loading:** The file is loaded lazily and cached in memory upon the first request (Singleton pattern). +* **Execution Flow:** + 1. User clicks "Send". + 2. App loads `includeList` and merges it with the default multi-language dictionary. + 3. Regex scans the text. + 4. Matches are filtered against the `excludeList`. + 5. If valid keywords remain and no file is attached, a warning dialog is shown. + + + +4. Example Customization + +If you want to support a new document type called "Blueprints" and ignore a specific CSS class in the email body: + +**Modify `configurations\attachment_keywords.json`:** + +```json +{ + "includeList": [ + "blueprints", + "bản vẽ" + ], + "excludeList": [ + "css-class-attachment", + "div-id-file" + ] +} + +``` \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 4ab4291f3..3e9be0aeb 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -890,6 +890,12 @@ class ComposerController extends BaseController emailSubject: subjectEmail.value ?? '', emailContent: emailContent, ); + + if (!context.mounted) { + logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED'); + return; + } + if (attachmentKeywords.isNotEmpty && uploadController.attachmentsUploaded.isEmpty) { showAttachmentReminderModal( diff --git a/lib/features/composer/presentation/extensions/attachment_detection_extension.dart b/lib/features/composer/presentation/extensions/attachment_detection_extension.dart index 9ac335e86..929afa052 100644 --- a/lib/features/composer/presentation/extensions/attachment_detection_extension.dart +++ b/lib/features/composer/presentation/extensions/attachment_detection_extension.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_text_detector.dart'; +import 'package:tmail_ui_user/features/composer/presentation/manager/keyword_config_manager.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -16,8 +17,12 @@ extension AttachmentDetectionExtension on ComposerController { try { final fullContent = '$emailSubject $emailContent'; final plainText = HtmlUtils.extractPlainText(fullContent); - final keywords = - await AttachmentTextDetector.matchedKeywordsUnique(plainText); + final keywordConfig = await KeywordConfigManager().getConfig(); + final keywords = await AttachmentTextDetector.matchedKeywordsUnique( + plainText, + includeList: keywordConfig.includeList, + excludeList: keywordConfig.excludeList, + ); if (keywords.isEmpty) { return []; } else { diff --git a/lib/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart b/lib/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart new file mode 100644 index 000000000..2d8f43bc7 --- /dev/null +++ b/lib/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/config/app_config_parser.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/keyword_config.dart'; + +class AttachmentKeywordsConfigurationParser + extends AppConfigParser { + @override + Future parse(String value) async { + try { + final jsonObject = jsonDecode(value); + return KeywordConfig.fromJson(jsonObject); + } catch (e) { + logWarning('AttachmentKeywordsConfigurationParser::parse(): $e'); + rethrow; + } + } + + @override + Future parseData(ByteData data) { + throw UnimplementedError(); + } +} diff --git a/lib/features/composer/presentation/manager/keyword_config_manager.dart b/lib/features/composer/presentation/manager/keyword_config_manager.dart new file mode 100644 index 000000000..99e66694d --- /dev/null +++ b/lib/features/composer/presentation/manager/keyword_config_manager.dart @@ -0,0 +1,43 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/config/app_config_loader.dart'; +import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_keywords_configuration_parser.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/keyword_config.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; + +class KeywordConfigManager { + static final KeywordConfigManager _instance = KeywordConfigManager._(); + + factory KeywordConfigManager() => _instance; + + KeywordConfigManager._(); + + KeywordConfig? _cachedConfig; + AppConfigLoader? _appConfigLoader; + + static const String _configPath = + AppConfig.attachmentKeywordsConfigurationPath; + + Future getConfig() async { + if (_cachedConfig != null) { + return _cachedConfig!; + } + + try { + _appConfigLoader ??= AppConfigLoader(); + _cachedConfig = await _appConfigLoader?.load( + _configPath, + AttachmentKeywordsConfigurationParser(), + ); + } catch (e) { + logWarning( + "KeywordConfigManager::getConfig:Error loading keyword config: $e"); + _cachedConfig = const KeywordConfig(); + } + + return _cachedConfig!; + } + + void clearCache() { + _cachedConfig = null; + } +} diff --git a/lib/features/composer/presentation/model/keyword_config.dart b/lib/features/composer/presentation/model/keyword_config.dart new file mode 100644 index 000000000..56f878af6 --- /dev/null +++ b/lib/features/composer/presentation/model/keyword_config.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'keyword_config.g.dart'; + +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class KeywordConfig with EquatableMixin { + final List includeList; + final List excludeList; + + const KeywordConfig({ + this.includeList = const [], + this.excludeList = const [], + }); + + factory KeywordConfig.fromJson(Map json) => + _$KeywordConfigFromJson(json); + + Map toJson() => _$KeywordConfigToJson(this); + + @override + List get props => [includeList, excludeList]; +} diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index dbc79dd1a..b7c955a7b 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -12,6 +12,7 @@ class AppConfig { static const int defaultLimitAutocomplete = 8; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; + static const String attachmentKeywordsConfigurationPath = "configurations/attachment_keywords.json"; static const String iOSKeychainSharingGroupId = 'KUT463DS29.com.linagora.ios.teammail.shared'; static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions'; static const String saasPlatform = 'saas';