TF-4265 Additional config for include and exclude keywords
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"includeList": [],
|
||||||
|
"excludeList": []
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
@@ -890,6 +890,12 @@ class ComposerController extends BaseController
|
|||||||
emailSubject: subjectEmail.value ?? '',
|
emailSubject: subjectEmail.value ?? '',
|
||||||
emailContent: emailContent,
|
emailContent: emailContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (attachmentKeywords.isNotEmpty &&
|
if (attachmentKeywords.isNotEmpty &&
|
||||||
uploadController.attachmentsUploaded.isEmpty) {
|
uploadController.attachmentsUploaded.isEmpty) {
|
||||||
showAttachmentReminderModal(
|
showAttachmentReminderModal(
|
||||||
|
|||||||
@@ -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/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/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/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/localizations/app_localizations.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||||
|
|
||||||
@@ -16,8 +17,12 @@ extension AttachmentDetectionExtension on ComposerController {
|
|||||||
try {
|
try {
|
||||||
final fullContent = '$emailSubject $emailContent';
|
final fullContent = '$emailSubject $emailContent';
|
||||||
final plainText = HtmlUtils.extractPlainText(fullContent);
|
final plainText = HtmlUtils.extractPlainText(fullContent);
|
||||||
final keywords =
|
final keywordConfig = await KeywordConfigManager().getConfig();
|
||||||
await AttachmentTextDetector.matchedKeywordsUnique(plainText);
|
final keywords = await AttachmentTextDetector.matchedKeywordsUnique(
|
||||||
|
plainText,
|
||||||
|
includeList: keywordConfig.includeList,
|
||||||
|
excludeList: keywordConfig.excludeList,
|
||||||
|
);
|
||||||
if (keywords.isEmpty) {
|
if (keywords.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+26
@@ -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<KeywordConfig> {
|
||||||
|
@override
|
||||||
|
Future<KeywordConfig> parse(String value) async {
|
||||||
|
try {
|
||||||
|
final jsonObject = jsonDecode(value);
|
||||||
|
return KeywordConfig.fromJson(jsonObject);
|
||||||
|
} catch (e) {
|
||||||
|
logWarning('AttachmentKeywordsConfigurationParser::parse(): $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<KeywordConfig> parseData(ByteData data) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<KeywordConfig> getConfig() async {
|
||||||
|
if (_cachedConfig != null) {
|
||||||
|
return _cachedConfig!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_appConfigLoader ??= AppConfigLoader();
|
||||||
|
_cachedConfig = await _appConfigLoader?.load<KeywordConfig>(
|
||||||
|
_configPath,
|
||||||
|
AttachmentKeywordsConfigurationParser(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logWarning(
|
||||||
|
"KeywordConfigManager::getConfig:Error loading keyword config: $e");
|
||||||
|
_cachedConfig = const KeywordConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _cachedConfig!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCache() {
|
||||||
|
_cachedConfig = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> includeList;
|
||||||
|
final List<String> excludeList;
|
||||||
|
|
||||||
|
const KeywordConfig({
|
||||||
|
this.includeList = const [],
|
||||||
|
this.excludeList = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory KeywordConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$KeywordConfigFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$KeywordConfigToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [includeList, excludeList];
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ class AppConfig {
|
|||||||
static const int defaultLimitAutocomplete = 8;
|
static const int defaultLimitAutocomplete = 8;
|
||||||
|
|
||||||
static const String appDashboardConfigurationPath = "configurations/app_dashboard.json";
|
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 iOSKeychainSharingGroupId = 'KUT463DS29.com.linagora.ios.teammail.shared';
|
||||||
static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions';
|
static const String iOSKeychainSharingService = 'com.linagora.ios.teammail.sessions';
|
||||||
static const String saasPlatform = 'saas';
|
static const String saasPlatform = 'saas';
|
||||||
|
|||||||
Reference in New Issue
Block a user