From 6a245f228a6bff9d84abdc66b5c88a85ef1112df Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 25 Mar 2026 10:30:50 +0700 Subject: [PATCH] TF-4404 Fix missing subject when perform `Mail to attendees` action in event email --- .../email_action_type_extension.dart | 35 +--- .../controller/single_email_controller.dart | 11 +- .../extensions/calendar_event_extension.dart | 9 + .../email/presentation/utils/email_utils.dart | 25 +++ .../calendar_event_detail_widget.dart | 6 +- .../calendar_event_information_widget.dart | 3 + .../email/presentation/apply_prefix_test.dart | 189 ++++++++++++++++++ 7 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 test/features/email/presentation/apply_prefix_test.dart diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index ac1498579..10acb8f79 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -7,12 +7,10 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/utc_date_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; extension EmailActionTypeExtension on EmailActionType { - static const String _defaultReplyPrefix = 'Re:'; - static const String _defaultForwardPrefix = 'Fwd:'; - String getSubjectComposer(BuildContext? context, String subject) { final l10n = context != null ? AppLocalizations.of(context) : null; @@ -20,15 +18,15 @@ extension EmailActionTypeExtension on EmailActionType { case EmailActionType.reply: case EmailActionType.replyToList: case EmailActionType.replyAll: - return _applyPrefix( + return EmailUtils.applyPrefix( subject: subject, - defaultPrefix: _defaultReplyPrefix, + defaultPrefix: EmailUtils.defaultReplyPrefix, localizedPrefix: l10n?.prefix_reply_email, ); case EmailActionType.forward: - return _applyPrefix( + return EmailUtils.applyPrefix( subject: subject, - defaultPrefix: _defaultForwardPrefix, + defaultPrefix: EmailUtils.defaultForwardPrefix, localizedPrefix: l10n?.prefix_forward_email, ); case EmailActionType.editDraft: @@ -43,29 +41,6 @@ extension EmailActionTypeExtension on EmailActionType { } } - String _applyPrefix({ - required String subject, - required String defaultPrefix, - String? localizedPrefix, - }) { - final trimmed = subject.trim(); - final lowerSubject = trimmed.toLowerCase(); - - final prefixes = [ - defaultPrefix.toLowerCase(), - if (localizedPrefix != null) localizedPrefix.toLowerCase(), - ]; - - final hasPrefix = prefixes.any(lowerSubject.startsWith); - - if (hasPrefix) { - return subject; - } - - final prefix = localizedPrefix ?? defaultPrefix; - return '$prefix $subject'; - } - String getToastMessageMoveToMailboxSuccess(BuildContext context, {String? destinationPath}) { switch(this) { case EmailActionType.archiveMessage: diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 89c6336e1..d37642d75 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1499,7 +1499,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ); } - void handleMailToAttendees(CalendarOrganizer? organizer, List? attendees) { + void handleMailToAttendees( + CalendarOrganizer? organizer, + List? attendees, + String? eventTitle, + ) { List listEmailAddressAttendees = []; if (organizer != null) { @@ -1516,7 +1520,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { listEmailAddressAttendees.removeInvalidEmails(ownEmailAddress); log('SingleEmailController::handleMailToAttendees: listEmailAddressMailTo = $listEmailAddressMailTo'); mailboxDashBoardController.openComposer( - ComposerArguments.fromMailtoUri(listEmailAddress: listEmailAddressMailTo) + ComposerArguments.fromMailtoUri( + listEmailAddress: listEmailAddressMailTo, + subject: eventTitle, + ) ); } diff --git a/lib/features/email/presentation/extensions/calendar_event_extension.dart b/lib/features/email/presentation/extensions/calendar_event_extension.dart index d712c2410..89525b69d 100644 --- a/lib/features/email/presentation/extensions/calendar_event_extension.dart +++ b/lib/features/email/presentation/extensions/calendar_event_extension.dart @@ -21,6 +21,7 @@ import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_id.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_method.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/recurrence_rule/recurrence_rule.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -495,4 +496,12 @@ extension CalendarEventExtension on CalendarEvent { excludedCalendarEvents: excludedCalendarEvents ?? this.excludedCalendarEvents, ); } + + String getMailToAttendeesEventTitle(AppLocalizations appLocalizations) { + return EmailUtils.applyPrefix( + subject: title ?? '', + defaultPrefix: EmailUtils.defaultReplyPrefix, + localizedPrefix: appLocalizations.prefix_reply_email, + ); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 63413debc..18c214d00 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -29,9 +29,34 @@ class EmailUtils { static const double attachmentItemHeight = 36; static const double attachmentIcon = 20; static const int maxMobileVisibleAttachments = 3; + static const String defaultReplyPrefix = 'Re:'; + static const String defaultForwardPrefix = 'Fwd:'; EmailUtils._(); + static String applyPrefix({ + required String subject, + required String defaultPrefix, + String? localizedPrefix, + }) { + final trimmed = subject.trim(); + final lowerSubject = trimmed.toLowerCase(); + + final prefixes = [ + defaultPrefix.toLowerCase(), + if (localizedPrefix != null) localizedPrefix.toLowerCase(), + ]; + + final hasPrefix = prefixes.any(lowerSubject.startsWith); + + if (hasPrefix) { + return subject; + } + + final prefix = localizedPrefix ?? defaultPrefix; + return '$prefix $subject'; + } + static Properties getPropertiesForEmailGetMethod(Session session, AccountId accountId) { if (CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId)) { return ThreadConstants.propertiesCalendarEvent; diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart index fbac15187..86cb0f3cb 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart @@ -10,7 +10,11 @@ import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_ import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_body_content_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; -typedef OnMailtoAttendeesAction = Function(CalendarOrganizer? organizer, List? participants); +typedef OnMailtoAttendeesAction = Function( + CalendarOrganizer? organizer, + List? participants, + String? eventTitle, +); class CalendarEventDetailWidget extends StatelessWidget { diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart index aba5a5de7..7856af9ed 100644 --- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -185,6 +185,9 @@ class CalendarEventInformationWidget extends StatelessWidget { onMailToAttendeesAction: () => onMailtoAttendeesAction?.call( calendarEvent.organizer, calendarEvent.participants, + calendarEvent.getMailToAttendeesEventTitle( + AppLocalizations.of(context), + ), ), ), ], diff --git a/test/features/email/presentation/apply_prefix_test.dart b/test/features/email/presentation/apply_prefix_test.dart new file mode 100644 index 000000000..4cc179946 --- /dev/null +++ b/test/features/email/presentation/apply_prefix_test.dart @@ -0,0 +1,189 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +void main() { + group('EmailUtils::applyPrefix', () { + group('Reply prefix', () { + test('should add Re: prefix when subject has no prefix', () { + expect( + EmailUtils.applyPrefix( + subject: 'Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: Hello'), + ); + }); + + test('should not add Re: prefix when subject already has Re:', () { + expect( + EmailUtils.applyPrefix( + subject: 'Re: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: Hello'), + ); + }); + + test('should not add prefix when subject has Re: case-insensitively', () { + expect( + EmailUtils.applyPrefix( + subject: 'RE: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('RE: Hello'), + ); + }); + + test('should not add prefix when subject has re: lowercase', () { + expect( + EmailUtils.applyPrefix( + subject: 're: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('re: Hello'), + ); + }); + }); + + group('Forward prefix', () { + test('should add Fwd: prefix when subject has no prefix', () { + expect( + EmailUtils.applyPrefix( + subject: 'Hello', + defaultPrefix: EmailUtils.defaultForwardPrefix, + ), + equals('Fwd: Hello'), + ); + }); + + test('should not add Fwd: prefix when subject already has Fwd:', () { + expect( + EmailUtils.applyPrefix( + subject: 'Fwd: Hello', + defaultPrefix: EmailUtils.defaultForwardPrefix, + ), + equals('Fwd: Hello'), + ); + }); + + test('should not add prefix when subject has FWD: uppercase', () { + expect( + EmailUtils.applyPrefix( + subject: 'FWD: Hello', + defaultPrefix: EmailUtils.defaultForwardPrefix, + ), + equals('FWD: Hello'), + ); + }); + }); + + group('Localized prefix', () { + test('should use localized prefix when provided', () { + expect( + EmailUtils.applyPrefix( + subject: 'Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + localizedPrefix: 'Rép:', + ), + equals('Rép: Hello'), + ); + }); + + test( + 'should not add prefix when subject already has localized prefix', + () { + expect( + EmailUtils.applyPrefix( + subject: 'Rép: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + localizedPrefix: 'Rép:', + ), + equals('Rép: Hello'), + ); + }, + ); + + test( + 'should not add prefix when subject has localized prefix ' + 'case-insensitively', + () { + expect( + EmailUtils.applyPrefix( + subject: 'rép: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + localizedPrefix: 'Rép:', + ), + equals('rép: Hello'), + ); + }, + ); + + test( + 'should not add prefix when subject has default prefix ' + 'even with localized prefix provided', + () { + expect( + EmailUtils.applyPrefix( + subject: 'Re: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + localizedPrefix: 'Rép:', + ), + equals('Re: Hello'), + ); + }, + ); + }); + + group('Edge cases', () { + test('should handle empty subject', () { + expect( + EmailUtils.applyPrefix( + subject: '', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: '), + ); + }); + + test('should handle subject with only spaces', () { + expect( + EmailUtils.applyPrefix( + subject: ' ', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: '), + ); + }); + + test('should preserve original subject when prefix already exists', () { + expect( + EmailUtils.applyPrefix( + subject: 'Re: Hello ', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: Hello '), + ); + }); + + test('should handle subject that contains prefix in middle', () { + expect( + EmailUtils.applyPrefix( + subject: 'Hello Re: World', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals('Re: Hello Re: World'), + ); + }); + + test('should handle subject with leading spaces before prefix', () { + expect( + EmailUtils.applyPrefix( + subject: ' Re: Hello', + defaultPrefix: EmailUtils.defaultReplyPrefix, + ), + equals(' Re: Hello'), + ); + }); + }); + }); +}