From 68b9291e682c7d7502fea1e56a4b9cff24e4c7c4 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 24 Dec 2024 14:14:52 +0700 Subject: [PATCH] TF-3344 Handle separator when pasting list of mail addresses --- core/lib/utils/string_convert.dart | 35 +++- core/test/utils/string_convert_test.dart | 162 ++++++++++++++++++ .../presentation/composer_controller.dart | 140 +++++++++------ 3 files changed, 283 insertions(+), 54 deletions(-) create mode 100644 core/test/utils/string_convert_test.dart diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart index 3042a1c9b..c3fefb45a 100644 --- a/core/lib/utils/string_convert.dart +++ b/core/lib/utils/string_convert.dart @@ -1,10 +1,41 @@ +import 'dart:convert'; + +import 'app_logger.dart'; + class StringConvert { - static String? writeEmptyToNull(String text) { + static const String separatorPattern = r'[ ,;]+'; + + static String? writeEmptyToNull(String text) { if (text.isEmpty) return null; return text; } - static String writeNullToEmpty(String? text) { + static String writeNullToEmpty(String? text) { return text ?? ''; } + + static List extractStrings(String input) { + try { + // Check if the input is URL encoded + if (input.contains('%')) { + input = Uri.decodeComponent(input); // Decode URL encoding + } + + // Check if the input is Base64 encoded + if (input.length % 4 == 0 && RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(input)) { + input = utf8.decode(base64.decode(input)); // Decode Base64 encoding + } + + final RegExp separator = RegExp(separatorPattern); + final listStrings = input + .replaceAll('\n', ' ') + .split(separator) + .where((value) => value.trim().isNotEmpty) + .toList(); + log('StringConvert::extractStrings:listStrings = $listStrings'); + return listStrings; + } catch (e) { + return []; + } + } } diff --git a/core/test/utils/string_convert_test.dart b/core/test/utils/string_convert_test.dart new file mode 100644 index 000000000..cecd1a271 --- /dev/null +++ b/core/test/utils/string_convert_test.dart @@ -0,0 +1,162 @@ +import 'dart:convert'; + +import 'package:core/utils/string_convert.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('StringConvert::extractStrings::', () { + group('Basic Functionality', () { + test('should extract strings separated by spaces', () { + const input = 'user1@example.com user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by commas', () { + const input = 'user1@example.com,user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by semicolons', () { + const input = 'user1@example.com;user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by mixed separators', () { + const input = 'user1@example.com, user2@example.com; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle multiple consecutive separators', () { + const input = + 'user1@example.com,,; user2@example.com;; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Edge Cases', () { + test('should handle empty input', () { + const input = ''; + final expected = []; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle input with only separators', () { + const input = ',, ;; '; + final expected = []; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle trailing and leading separators', () { + const input = ', user1@example.com; user2@example.com ,'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle extra spaces between separators', () { + const input = 'user1@example.com , user2@example.com ; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Stress Tests', () { + test('should handle a large number of strings', () { + final input = List.generate( + 1000, + (index) => + 'user$index@example.com${index % 3 == 0 ? ',' : index % 3 == 1 ? ';' : ' '}', + ).join(); + final expected = + List.generate(1000, (index) => 'user$index@example.com'); + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Encoded Input Tests', () { + test('should handle URL encoded input', () { + String input = 'user1%40example.com%20user2%40example.com%20user3%40example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle Base64 encoded input', () { + String input = 'dXNlcjFAZXhhbXBsZS5jb20gdXNlcjJAZXhhbXBsZS5jb20gdXNlcjNAZXhhbXBsZS5jb20='; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle input with both URL encoding and Base64 encoding', () { + String input = Uri.encodeComponent(base64.encode(utf8.encode('user1@example.com user2@example.com user3@example.com'))); + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Failing Cases', () { + test('should return empty list for empty input', () { + String input = ''; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should return empty list if input is only separators', () { + String input = ' , ; '; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should return correct result for input with invalid separators', () { + String input = 'user1@example.com,,user2@example.com;;;user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should return empty list for input with only whitespace and separators', () { + String input = ' , ; \n'; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should handle input with newline characters', () { + // Arrange + String input = 'user1@example.com\nuser2@example.com;user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + }); +} diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e89a0a86f..928190195 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1545,15 +1545,30 @@ class ComposerController extends BaseController final inputToEmail = toEmailAddressController.text; final inputCcEmail = ccEmailAddressController.text; final inputBccEmail = bccEmailAddressController.text; - - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(inputToEmail); + log('ComposerController::_autoCreateEmailTag:inputToEmail = $inputToEmail | inputCcEmail = $inputCcEmail | inputBccEmail = $inputBccEmail'); + if (inputToEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.to, + inputText: inputToEmail, + listEmailAddress: listToEmailAddress, + keyEmailTagEditor: keyToEmailTagEditor, + ); } - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(inputCcEmail); + if (inputCcEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.cc, + inputText: inputCcEmail, + listEmailAddress: listCcEmailAddress, + keyEmailTagEditor: keyCcEmailTagEditor, + ); } - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(inputBccEmail); + if (inputBccEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.bcc, + inputText: inputBccEmail, + listEmailAddress: listBccEmailAddress, + keyEmailTagEditor: keyBccEmailTagEditor, + ); } } @@ -1564,47 +1579,53 @@ class ComposerController extends BaseController .contains(inputEmail); } - void _autoCreateToEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listToEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listToEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - log('ComposerController::_autoCreateToEmailTag(): STATE: ${keyToEmailTagEditor.currentState}'); - keyToEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyToEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } + void _autoCreateEmailTagForRecipientField({ + required PrefixEmailAddress prefixEmail, + required String inputText, + required List listEmailAddress, + required GlobalKey keyEmailTagEditor, + }) { + log('ComposerController::_autoCreateEmailTagForRecipientField:prefixEmail = $prefixEmail | inputText = $inputText | listEmailAddress = $listEmailAddress'); + switch(prefixEmail) { + case PrefixEmailAddress.to: + case PrefixEmailAddress.cc: + case PrefixEmailAddress.bcc: + final listString = StringConvert.extractStrings(inputText).toSet(); + if (listString.isEmpty && !_isDuplicatedRecipient(inputText, listEmailAddress)) { + final emailAddress = EmailAddress(null, inputText); + listEmailAddress.add(emailAddress); + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + _updateStatusEmailSendButton(); + keyEmailTagEditor.currentState?.resetTextField(); + Future.delayed( + const Duration(milliseconds: 300), + keyEmailTagEditor.currentState?.closeSuggestionBox, + ); + } else if (listString.isNotEmpty) { + final listStringNotExist = listString + .where((text) => !_isDuplicatedRecipient(text, listEmailAddress)) + .toList(); - void _autoCreateCcEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listCcEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listCcEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); + if (listStringNotExist.isNotEmpty) { + final listAddress = listStringNotExist + .map((value) => EmailAddress(null, value)) + .toList(); + listEmailAddress.addAll(listAddress); + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + _updateStatusEmailSendButton(); + keyEmailTagEditor.currentState?.resetTextField(); + Future.delayed( + const Duration(milliseconds: 300), + keyEmailTagEditor.currentState?.closeSuggestionBox, + ); + } + } + break; + default: + break; } - keyCcEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyCcEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateBccEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listBccEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listBccEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - keyBccEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyBccEmailTagEditor.currentState?.closeSuggestionBox(); - }); } void _closeSuggestionBox() { @@ -1664,22 +1685,37 @@ class ComposerController extends BaseController case PrefixEmailAddress.to: toAddressExpandMode.value = ExpandMode.COLLAPSE; final inputToEmail = toEmailAddressController.text; - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(inputToEmail); + if (inputToEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.to, + inputText: inputToEmail, + listEmailAddress: listToEmailAddress, + keyEmailTagEditor: keyToEmailTagEditor, + ); } break; case PrefixEmailAddress.cc: ccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputCcEmail = ccEmailAddressController.text; - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(inputCcEmail); + if (inputCcEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.cc, + inputText: inputCcEmail, + listEmailAddress: listCcEmailAddress, + keyEmailTagEditor: keyCcEmailTagEditor, + ); } break; case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputBccEmail = bccEmailAddressController.text; - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(inputBccEmail); + if (inputBccEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.bcc, + inputText: inputBccEmail, + listEmailAddress: listBccEmailAddress, + keyEmailTagEditor: keyBccEmailTagEditor, + ); } break; default: