diff --git a/lib/features/composer/presentation/manager/attachment_text_detector.dart b/lib/features/composer/presentation/manager/attachment_text_detector.dart new file mode 100644 index 000000000..da9fd0f37 --- /dev/null +++ b/lib/features/composer/presentation/manager/attachment_text_detector.dart @@ -0,0 +1,101 @@ +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; + +class AttachmentTextDetector { + const AttachmentTextDetector._(); + // List of keywords by language + static final Map> _keywordsByLang = { + LanguageCodeConstants.english: [ + 'attach', + 'attachment', + ], + LanguageCodeConstants.french: [ + 'pièce jointe', + 'fichier joint', + 'document joint', + 'pj', + 'jointe', + 'joint', + ], + LanguageCodeConstants.russian: [ + 'прикрепить', + 'приложение', + 'документ', + 'файл', + 'отчёт', + 'вложение', + ], + LanguageCodeConstants.vietnamese: [ + 'đính kèm', + 'tài liệu', + 'tệp', + 'báo cáo', + 'file', + 'attach', + 'attachment', + ], + }; + + /// Detect if the text contains keywords suggesting there is an attachment. + /// [lang] is the language code (`en`, `fr`, `ru`, `vi`, ...). + static bool containsAttachmentKeyword(String text, {required String lang}) { + final lowerText = text.toLowerCase(); + final keywords = _keywordsByLang[lang.toLowerCase()]; + if (keywords == null) return false; + + return keywords.any((k) => lowerText.contains(k.toLowerCase())); + } + + /// Returns a list of matched keywords (if any) for the specified language + static List matchedKeywords(String text, {required String lang}) { + final lowerText = text.toLowerCase(); + final keywords = _keywordsByLang[lang.toLowerCase()]; + if (keywords == null) return []; + + return keywords + .where((k) => lowerText.contains(k.toLowerCase())) + .toList(); + } + + /// Detect if text contains keyword in any language + static bool containsAnyAttachmentKeyword(String text) { + final lowerText = text.toLowerCase(); + for (final keywords in _keywordsByLang.values) { + if (keywords.any((k) => lowerText.contains(k.toLowerCase()))) { + return true; + } + } + return false; + } + + /// Returns a map of languages with a list of matched keywords + static Map> matchedKeywordsAll(String text) { + final result = >{}; + final lowerText = text.toLowerCase(); + + _keywordsByLang.forEach((lang, keywords) { + final matches = + keywords.where((k) => lowerText.contains(k.toLowerCase())).toList(); + if (matches.isNotEmpty) { + result[lang] = matches; + } + }); + + return result; + } + + /// Returns a list of matching keywords but removes duplicates + static List matchedKeywordsUnique(String text) { + final lowerText = text.toLowerCase(); + final result = {}; + + _keywordsByLang.forEach((_, keywords) { + for (final k in keywords) { + if (lowerText.contains(k.toLowerCase())) { + result.add(k); + } + } + }); + + return result.toList(); + } +} diff --git a/test/features/composer/presentation/manager/attachment_text_detector_test.dart b/test/features/composer/presentation/manager/attachment_text_detector_test.dart new file mode 100644 index 000000000..7edea0221 --- /dev/null +++ b/test/features/composer/presentation/manager/attachment_text_detector_test.dart @@ -0,0 +1,379 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/composer/presentation/manager/attachment_text_detector.dart'; + +/// Helper: generate email about [targetLength] characters long +String generateLongEmail(int targetLength, {bool includeKeywords = true}) { + final buffer = StringBuffer(); + const sample = "This is a random paragraph with no attachments. "; + while (buffer.length < targetLength) { + buffer.write(sample); + } + if (includeKeywords) { + buffer.write("Here is the pièce jointe and báo cáo attached."); + } + return buffer.toString(); +} + +void main() { + group('AttachmentTextDetector.containsAttachmentKeyword', () { + test('English - should detect "attach" and "attachment"', () { + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Please see the attached document for details.", + lang: 'en', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "No attachments here.", + lang: 'en', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Nothing relevant", + lang: 'en', + ), + isFalse, + ); + }); + + test('French - should detect common attachment words', () { + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Veuillez trouver la pièce jointe.", + lang: 'fr', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Ci-joint le fichier joint pour votre examen.", + lang: 'fr', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Aucun document ici", + lang: 'fr', + ), + isFalse, + ); + }); + + test('Russian - should detect common attachment words', () { + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Смотрите приложение с отчетом.", + lang: 'ru', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Документ прикрепить ниже.", + lang: 'ru', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Текст без вложений", + lang: 'ru', + ), + isFalse, + ); + }); + + test('Vietnamese - should detect common attachment words', () { + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Vui lòng xem tài liệu đính kèm.", + lang: 'vi', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Đây là một file rất quan trọng.", + lang: 'vi', + ), + isTrue, + ); + + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Không có gì cần gửi thêm.", + lang: 'vi', + ), + isFalse, + ); + }); + + test('Unsupported language should return false', () { + expect( + AttachmentTextDetector.containsAttachmentKeyword( + "Some random text", + lang: 'de', + ), + isFalse, + ); + }); + }); + + group('AttachmentTextDetector.matchedKeywords', () { + test('should return matched keywords for Vietnamese', () { + final matches = AttachmentTextDetector.matchedKeywords( + "Đây là báo cáo và tài liệu đính kèm.", + lang: 'vi', + ); + + expect(matches, containsAll(['báo cáo', 'tài liệu', 'đính kèm'])); + }); + + test('should return empty list when no keywords found', () { + final matches = AttachmentTextDetector.matchedKeywords( + "Nội dung không có gì liên quan", + lang: 'vi', + ); + + expect(matches, isEmpty); + }); + + test('should return matched keywords for English', () { + final matches = AttachmentTextDetector.matchedKeywords( + "Please check the attachment and attach your signature.", + lang: 'en', + ); + + expect(matches, containsAll(['attachment', 'attach'])); + }); + }); + + group('AttachmentTextDetector.containsAnyAttachmentKeyword', () { + test('should detect when email contains English keyword', () { + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword( + "Please see the ATTACHed document.", + ), + isTrue, + ); + }); + + test('should detect when email contains Vietnamese keyword', () { + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword( + "Đây là tài liệu đính kèm.", + ), + isTrue, + ); + }); + + test('should detect when email contains Russian keyword', () { + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword( + "Документ прикрепить ниже.", + ), + isTrue, + ); + }); + + test('should detect when email contains French keyword', () { + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword( + "Veuillez trouver la pièce jointe.", + ), + isTrue, + ); + }); + + test('should return false when no keywords found', () { + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword( + "This email has nothing special.", + ), + isFalse, + ); + }); + + test('should detect when email contains multiple languages', () { + const email = """ + Please see the attached document. + Vui lòng xem tài liệu đính kèm. + Смотрите приложение. + """; + + expect( + AttachmentTextDetector.containsAnyAttachmentKeyword(email), + isTrue, + ); + }); + }); + + group('AttachmentTextDetector.matchedKeywordsAll', () { + test('should return matches for multiple languages', () { + const email = """ + Please see the attached document. + Vui lòng xem tài liệu đính kèm. + Смотрите приложение с отчётом. + """; + + final matches = AttachmentTextDetector.matchedKeywordsAll(email); + + expect(matches.keys, containsAll(['en', 'vi', 'ru'])); + expect(matches['en'], contains('attach')); + expect(matches['vi'], containsAll(['tài liệu', 'đính kèm'])); + expect(matches['ru'], containsAll(['приложение', 'отчёт'])); + }); + + test('should return matches only for one language', () { + const email = "Veuillez trouver la pièce jointe."; + + final matches = AttachmentTextDetector.matchedKeywordsAll(email); + + expect(matches.keys, equals(['fr'])); + expect(matches['fr'], contains('pièce jointe')); + }); + + test('should return empty map when no keywords found', () { + const email = "Completely unrelated text."; + + final matches = AttachmentTextDetector.matchedKeywordsAll(email); + + expect(matches, isEmpty); + }); + + test('should be case-insensitive', () { + const email = "ATTACHMENT and PiÈce Jointe included."; + + final matches = AttachmentTextDetector.matchedKeywordsAll(email); + + expect(matches['en'], contains('attachment')); + expect(matches['fr'], contains('pièce jointe')); + }); + }); + + group('AttachmentTextDetector.matchedKeywordsUnique', () { + test('should return unique matches across multiple languages', () { + const email = """ + Please see the attached document. + Vui lòng xem tài liệu đính kèm. + Смотрите приложение с отчетом. + """; + + final matches = AttachmentTextDetector.matchedKeywordsUnique(email); + + expect(matches, containsAll(['attach', 'tài liệu', 'đính kèm', 'приложение'])); + expect(matches.toSet().length, matches.length, reason: 'No duplicates allowed'); + }); + + test('should return unique matches even if repeated multiple times', () { + const email = """ + file file file attach attach attach + đính kèm đính kèm + приложение приложение + """; + + final matches = AttachmentTextDetector.matchedKeywordsUnique(email); + + expect(matches, containsAll(['file', 'attach', 'đính kèm', 'приложение'])); + expect(matches.toSet().length, matches.length, reason: 'Duplicates must be removed'); + }); + + test('should return empty list if no keywords present', () { + const email = "This email does not mention anything related to document."; + final matches = AttachmentTextDetector.matchedKeywordsUnique(email); + expect(matches, isEmpty); + }); + + test('should match Russian word with both ё and е', () { + const email = "Вот наш отчёт и вот наш снова."; + final matches = AttachmentTextDetector.matchedKeywordsUnique(email); + + expect(matches, containsAll(['отчёт'])); + }); + }); + + /// Measure performance when detecting long emails, e.g. several thousand characters + group('AttachmentTextDetector Benchmark', () { + late String longEmail; + + setUp(() { + // Generate email ~50,000 characters long (many unrelated paragraphs) + final buffer = StringBuffer(); + const sample = "This is a random paragraph with no attachments. "; + for (int i = 0; i < 1000; i++) { + buffer.write(sample); + } + // Insert some keywords in the middle + buffer.write("Here is the pièce jointe and báo cáo attached."); + longEmail = buffer.toString(); + }); + + test('Benchmark containsAnyAttachmentKeyword', () { + final sw = Stopwatch()..start(); + final result = + AttachmentTextDetector.containsAnyAttachmentKeyword(longEmail); + sw.stop(); + + log( + 'containsAnyAttachmentKeyword -> result=$result, elapsed=${sw.elapsedMicroseconds}µs', + ); + expect(result, isTrue); + }); + + test('Benchmark matchedKeywordsAll', () { + final sw = Stopwatch()..start(); + final matches = AttachmentTextDetector.matchedKeywordsAll(longEmail); + sw.stop(); + + log( + 'matchedKeywordsAll -> found=${matches.keys}, elapsed=${sw.elapsedMicroseconds}µs', + ); + expect(matches.isNotEmpty, isTrue); + }); + }); + + /// Compare time when email is 100K, 500K, 1M characters long + group('AttachmentTextDetector Stress Benchmark', () { + final sizes = [100000, 500000, 1000000]; // 100K, 500K, 1M characters + + for (final size in sizes) { + test('Benchmark size=$size containsAnyAttachmentKeyword', () { + final email = generateLongEmail(size); + final sw = Stopwatch()..start(); + final result = + AttachmentTextDetector.containsAnyAttachmentKeyword(email); + sw.stop(); + + log( + 'containsAnyAttachmentKeyword(size=$size) -> result=$result, elapsed=${sw.elapsedMilliseconds}ms', + ); + expect(result, isTrue); + }); + + test('Benchmark size=$size matchedKeywordsAll', () { + final email = generateLongEmail(size); + final sw = Stopwatch()..start(); + final matches = AttachmentTextDetector.matchedKeywordsAll(email); + sw.stop(); + + log( + 'matchedKeywordsAll(size=$size) -> found=${matches.keys}, elapsed=${sw.elapsedMilliseconds}ms', + ); + expect(matches.isNotEmpty, isTrue); + }); + } + }); +}