From 36a9117e00db450dc7ead7428577a41d4c964787 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 10 Mar 2025 16:24:47 +0700 Subject: [PATCH] Fix cannot auto create tag when paste list email and switch focus Signed-off-by: dab246 --- core/lib/utils/string_convert.dart | 20 +++++-- core/test/utils/string_convert_test.dart | 59 ++++++++----------- ...o_create_tag_for_recipients_extension.dart | 31 +++++++--- .../widgets/recipient_composer_widget.dart | 2 +- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart index 1a229d583..10d6f4576 100644 --- a/core/lib/utils/string_convert.dart +++ b/core/lib/utils/string_convert.dart @@ -6,7 +6,7 @@ import 'package:core/domain/exceptions/string_exception.dart'; import 'package:http_parser/http_parser.dart'; class StringConvert { - static const String separatorPattern = r'[ ,;]+'; + static const String emailSeparatorPattern = r'[,;]+'; static String? writeEmptyToNull(String text) { if (text.isEmpty) return null; @@ -26,23 +26,28 @@ class StringConvert { } } - static List extractStrings(String input) { + static List extractStrings(String input, String separatorPattern) { 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 + // Efficient Base64 validation: Check length and minimal regex match + if (input.length % 4 == 0 && input.contains(RegExp(r'^[A-Za-z0-9+/=]+$'))) { + try { + input = utf8.decode(base64.decode(input)); // Decode Base64 encoding + } catch (_) { + // Ignore if decoding fails + } } final RegExp separator = RegExp(separatorPattern); final listStrings = input .replaceAll('\n', ' ') .split(separator) - .where((value) => value.trim().isNotEmpty) + .map((value) => value.trim()) + .where((value) => value.isNotEmpty) .toList(); log('StringConvert::extractStrings:listStrings = $listStrings'); return listStrings; @@ -51,6 +56,9 @@ class StringConvert { } } + static List extractEmailAddress(String input) => + extractStrings(input, emailSeparatorPattern); + static String decodeFromBytes( Uint8List bytes, { required String? charset, diff --git a/core/test/utils/string_convert_test.dart b/core/test/utils/string_convert_test.dart index d51ed847a..af88be2d3 100644 --- a/core/test/utils/string_convert_test.dart +++ b/core/test/utils/string_convert_test.dart @@ -41,24 +41,24 @@ void main() { }); }); - group('StringConvert::extractStrings::', () { + group('StringConvert::extractEmailAddress::', () { group('Basic Functionality', () { - test('should extract strings separated by spaces', () { + test('should not 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)); + final expected = ['user1@example.com user2@example.com']; + expect(StringConvert.extractEmailAddress(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)); + expect(StringConvert.extractEmailAddress(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)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should extract strings separated by mixed separators', () { @@ -68,7 +68,7 @@ void main() { 'user2@example.com', 'user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should handle multiple consecutive separators', () { @@ -79,7 +79,7 @@ void main() { 'user2@example.com', 'user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); }); @@ -87,19 +87,19 @@ void main() { test('should handle empty input', () { const input = ''; final expected = []; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should handle input with only separators', () { const input = ',, ;; '; final expected = []; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(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)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should handle extra spaces between separators', () { @@ -109,7 +109,7 @@ void main() { 'user2@example.com', 'user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); }); @@ -118,11 +118,11 @@ void main() { final input = List.generate( 1000, (index) => - 'user$index@example.com${index % 3 == 0 ? ',' : index % 3 == 1 ? ';' : ' '}', + 'user$index@example.com${index % 3 == 0 ? ',' : ';'}', ).join(); final expected = List.generate(1000, (index) => 'user$index@example.com'); - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); }); @@ -130,43 +130,37 @@ void main() { 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' + 'user1@example.com user2@example.com user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should handle Base64 encoded input', () { String input = 'dXNlcjFAZXhhbXBsZS5jb20gdXNlcjJAZXhhbXBsZS5jb20gdXNlcjNAZXhhbXBsZS5jb20='; final expected = [ - 'user1@example.com', - 'user2@example.com', - 'user3@example.com' + 'user1@example.com user2@example.com user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(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' + 'user1@example.com user2@example.com user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); }); group('Failing Cases', () { test('should return empty list for empty input', () { String input = ''; - expect(StringConvert.extractStrings(input), equals([])); + expect(StringConvert.extractEmailAddress(input), equals([])); }); test('should return empty list if input is only separators', () { String input = ' , ; '; - expect(StringConvert.extractStrings(input), equals([])); + expect(StringConvert.extractEmailAddress(input), equals([])); }); test('should return correct result for input with invalid separators', () { @@ -176,23 +170,22 @@ void main() { 'user2@example.com', 'user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); test('should return empty list for input with only whitespace and separators', () { String input = ' , ; \n'; - expect(StringConvert.extractStrings(input), equals([])); + expect(StringConvert.extractEmailAddress(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', + 'user1@example.com user2@example.com', 'user3@example.com' ]; - expect(StringConvert.extractStrings(input), equals(expected)); + expect(StringConvert.extractEmailAddress(input), equals(expected)); }); }); }); diff --git a/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart b/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart index db8690d97..5799f2b98 100644 --- a/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart +++ b/lib/features/composer/presentation/extensions/auto_create_tag_for_recipients_extension.dart @@ -1,5 +1,6 @@ import 'package:core/utils/mail/mail_address.dart'; +import 'package:core/utils/string_convert.dart'; import 'package:flutter/cupertino.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; @@ -39,21 +40,33 @@ extension AutoCreateTagForRecipientsExtension on ComposerController { }; void autoCreateEmailTagForType(PrefixEmailAddress type, String input) { - final emailAddressRecord = _generateEmailAddressFromString(input); + final addressSet = StringConvert.extractEmailAddress(input).toSet(); + if (addressSet.isEmpty) return; final emailList = _emailLists[type]!; final keyEditor = _emailEditors[type]!; + final listEmailAddressRecord = addressSet + .map((address) => _generateEmailAddressFromString(address)) + .toList(); - final emailSet = emailList.toSet(); - if (emailSet.any((email) => email.email == emailAddressRecord.$1)) return; + final emailSet = emailList.map((email) => email.email).toSet(); - emailSet.add(emailAddressRecord.$2); - emailList - ..clear() - ..addAll(emailSet); + final newEmails = addressSet.difference(emailSet); + + if (newEmails.isEmpty) return; + + final listEmailAddress = listEmailAddressRecord + .where((emailRecord) => newEmails.contains(emailRecord.$1)) + .map((emailRecord) => emailRecord.$2) + .toList(); + + emailList.addAll(listEmailAddress); + + if (!isInitialRecipient.value) { + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + } - isInitialRecipient.value = true; - isInitialRecipient.refresh(); updateStatusEmailSendButton(); keyEditor.currentState?.resetTextField(); diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 5abe4f6e5..b958cd22e 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -531,7 +531,7 @@ class _RecipientComposerWidgetState extends State { ) => _createMailTag(value, stateSetter); void _createMailTag(String value, StateSetter stateSetter) { - final listString = StringConvert.extractStrings(value.trim()).toSet(); + final listString = StringConvert.extractEmailAddress(value.trim()).toSet(); if (listString.isEmpty && !_isDuplicatedRecipient(value)) { _onEmailAddressReceived(value, stateSetter);