Skip to content

Commit e041362

Browse files
tddang-linagorahoangdat
authored andcommitted
TF-3034 Prevent duplicate draft warning
1 parent 416baa1 commit e041362

File tree

8 files changed

+1785
-61
lines changed

8 files changed

+1785
-61
lines changed

core/lib/utils/platform_info.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ abstract class PlatformInfo {
77
static bool isTestingForWeb = false;
88

99
static bool get isWeb => kIsWeb || isTestingForWeb;
10-
static bool get isLinux => !kIsWeb && defaultTargetPlatform == TargetPlatform.linux;
11-
static bool get isWindows => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
12-
static bool get isMacOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
13-
static bool get isFuchsia => !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia;
14-
static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
15-
static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
10+
static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux;
11+
static bool get isWindows => !isWeb && defaultTargetPlatform == TargetPlatform.windows;
12+
static bool get isMacOS => !isWeb && defaultTargetPlatform == TargetPlatform.macOS;
13+
static bool get isFuchsia => !isWeb && defaultTargetPlatform == TargetPlatform.fuchsia;
14+
static bool get isIOS => !isWeb && defaultTargetPlatform == TargetPlatform.iOS;
15+
static bool get isAndroid => !isWeb && defaultTargetPlatform == TargetPlatform.android;
1616
static bool get isMobile => isAndroid || isIOS;
1717
static bool get isDesktop => isLinux || isWindows || isMacOS;
1818
static bool get isCanvasKit => isRendererCanvasKit;

lib/features/composer/presentation/composer_controller.dart

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/list_sha
6262
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
6363
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
6464
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
65+
import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart';
6566
import 'package:tmail_ui_user/features/composer/presentation/model/signature_status.dart';
6667
import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart';
6768
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
@@ -109,7 +110,6 @@ class ComposerController extends BaseController with DragDropFileMixin implement
109110

110111
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
111112
final networkConnectionController = Get.find<NetworkConnectionController>();
112-
final _dynamicUrlInterceptors = Get.find<DynamicUrlInterceptors>();
113113
final _beforeReconnectManager = Get.find<BeforeReconnectManager>();
114114

115115
final composerArguments = Rxn<ComposerArguments>();
@@ -199,6 +199,14 @@ class ComposerController extends BaseController with DragDropFileMixin implement
199199
ButtonState _saveToDraftButtonState = ButtonState.enabled;
200200
ButtonState _sendButtonState = ButtonState.enabled;
201201
SignatureStatus _identityContentOnOpenPolicy = SignatureStatus.editedAvailable;
202+
int? _savedEmailDraftHash;
203+
bool _restoringSignatureButton = false;
204+
205+
@visibleForTesting
206+
bool get restoringSignatureButton => _restoringSignatureButton;
207+
208+
@visibleForTesting
209+
int? get savedEmailDraftHash => _savedEmailDraftHash;
202210

203211
late Worker uploadInlineImageWorker;
204212
late Worker dashboardViewStateWorker;
@@ -330,6 +338,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
330338
maxWithEditor = null;
331339
} else if (success is GetAlwaysReadReceiptSettingSuccess) {
332340
hasRequestReadReceipt.value = success.alwaysReadReceiptEnabled;
341+
_initEmailDraftHash();
333342
} else if (success is RestoreEmailInlineImagesSuccess) {
334343
_updateEditorContent(success);
335344
}
@@ -358,6 +367,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
358367
}
359368
} else if (failure is GetAlwaysReadReceiptSettingFailure) {
360369
hasRequestReadReceipt.value = false;
370+
_initEmailDraftHash();
361371
}
362372
}
363373

@@ -732,16 +742,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement
732742
(identity) => identity.id == identityId);
733743
}
734744

735-
void _initIdentities(ComposerArguments composerArguments) {
745+
Future<void> _initIdentities(ComposerArguments composerArguments) async {
736746
listFromIdentities.value = composerArguments.identities ?? [];
737747
final selectedIdentityFromId = _selectIdentityFromId(
738748
composerArguments.selectedIdentityId);
739749
if (listFromIdentities.isEmpty) {
740750
_getAllIdentities();
741751
} else if (selectedIdentityFromId != null) {
742-
_selectIdentity(selectedIdentityFromId);
752+
await _selectIdentity(selectedIdentityFromId);
753+
_initEmailDraftHash();
743754
} else if (composerArguments.identities?.isNotEmpty == true) {
744-
_selectIdentity(composerArguments.identities!.first);
755+
await _selectIdentity(composerArguments.identities!.first);
756+
_initEmailDraftHash();
745757
}
746758
}
747759

@@ -764,8 +776,10 @@ class ComposerController extends BaseController with DragDropFileMixin implement
764776
composerArguments.value?.selectedIdentityId);
765777
if (selectedIdentityFromId != null) {
766778
await _selectIdentity(selectedIdentityFromId);
779+
_initEmailDraftHash();
767780
} else {
768781
await _selectIdentity(listIdentitiesMayDeleted.firstOrNull);
782+
_initEmailDraftHash();
769783
}
770784
}
771785
}
@@ -1216,7 +1230,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
12161230
final session = mailboxDashBoardController.sessionCurrent;
12171231
final accountId = mailboxDashBoardController.accountId.value;
12181232
if (session != null && accountId != null) {
1219-
final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl);
1233+
final uploadUri = session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl);
12201234
uploadController.justUploadAttachmentsAction(
12211235
uploadFiles: pickedFiles,
12221236
uploadUri: uploadUri,
@@ -1230,50 +1244,41 @@ class ComposerController extends BaseController with DragDropFileMixin implement
12301244
uploadController.deleteFileUploaded(uploadId);
12311245
}
12321246

1233-
Future<bool> _validateEmailChange({
1234-
required BuildContext context,
1235-
required EmailActionType emailActionType,
1236-
PresentationEmail? presentationEmail,
1237-
Role? mailboxRole,
1238-
}) async {
1239-
final newEmailBody = await _getContentInEditor();
1240-
final oldEmailBody = _initTextEditor ?? '';
1241-
log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody');
1242-
final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim());
1243-
1244-
final newEmailSubject = subjectEmail.value ?? '';
1245-
final oldEmailSubject = emailActionType == EmailActionType.editDraft
1246-
? presentationEmail?.getEmailTitle().trim() ?? ''
1247-
: '';
1248-
final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim());
1249-
1250-
final recipients = presentationEmail
1251-
?.generateRecipientsEmailAddressForComposer(
1252-
emailActionType: emailActionType,
1253-
mailboxRole: mailboxRole
1254-
) ?? const Tuple3(<EmailAddress>[], <EmailAddress>[], <EmailAddress>[]);
1255-
1256-
final newToEmailAddress = listToEmailAddress;
1257-
final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : [];
1258-
final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress);
1259-
1260-
final newCcEmailAddress = listCcEmailAddress;
1261-
final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : [];
1262-
final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress);
1263-
1264-
final newBccEmailAddress = listBccEmailAddress;
1265-
final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : [];
1266-
final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress);
1267-
1268-
final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList());
1269-
log('ComposerController::_validateChangeEmail: isEmailBodyChanged = $isEmailBodyChanged | isEmailSubjectChanged = $isEmailSubjectChanged | isToEmailAddressChanged = $isToEmailAddressChanged | isCcEmailAddressChanged = $isCcEmailAddressChanged | isBccEmailAddressChanged = $isBccEmailAddressChanged | isAttachmentsChanged = $isAttachmentsChanged');
1270-
if (isEmailBodyChanged || isEmailSubjectChanged
1271-
|| isToEmailAddressChanged || isCcEmailAddressChanged
1272-
|| isBccEmailAddressChanged || isAttachmentsChanged) {
1273-
return true;
1247+
Future<bool> _validateEmailChange() async {
1248+
final newDraftHash = await _hashDraftEmail();
1249+
1250+
return _savedEmailDraftHash != newDraftHash;
1251+
}
1252+
1253+
Future<int> _hashDraftEmail() async {
1254+
final emailContent = await _getContentInEditor();
1255+
1256+
final savedEmailDraft = SavedEmailDraft(
1257+
subject: subjectEmail.value ?? '',
1258+
content: emailContent,
1259+
toRecipients: listToEmailAddress.toSet(),
1260+
ccRecipients: listCcEmailAddress.toSet(),
1261+
bccRecipients: listBccEmailAddress.toSet(),
1262+
identity: identitySelected.value,
1263+
attachments: uploadController.attachmentsUploaded,
1264+
hasReadReceipt: hasRequestReadReceipt.value,
1265+
);
1266+
1267+
return savedEmailDraft.hashCode;
1268+
}
1269+
1270+
Future<void> _updateSavedEmailDraftHash() async {
1271+
_savedEmailDraftHash = await _hashDraftEmail();
1272+
}
1273+
1274+
Future<void> _initEmailDraftHash() async {
1275+
if (composerArguments.value?.emailActionType != EmailActionType.compose
1276+
&& composerArguments.value?.emailActionType != EmailActionType.editDraft
1277+
) {
1278+
return;
12741279
}
12751280

1276-
return false;
1281+
_savedEmailDraftHash = await _hashDraftEmail();
12771282
}
12781283

12791284
void handleClickSaveAsDraftsButton(BuildContext context) async {
@@ -1306,10 +1311,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement
13061311
_saveToDraftButtonState = ButtonState.enabled;
13071312
_emailIdEditing = resultState.emailId;
13081313
mailboxDashBoardController.consumeState(Stream.value(Right<Failure, Success>(resultState)));
1314+
_updateSavedEmailDraftHash();
13091315
} else if (resultState is UpdateEmailDraftsSuccess) {
13101316
_saveToDraftButtonState = ButtonState.enabled;
13111317
_emailIdEditing = resultState.emailId;
13121318
mailboxDashBoardController.consumeState(Stream.value(Right<Failure, Success>(resultState)));
1319+
_updateSavedEmailDraftHash();
13131320
} else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) ||
13141321
(resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) {
13151322
_saveToDraftButtonState = ButtonState.enabled;
@@ -1437,6 +1444,8 @@ class ComposerController extends BaseController with DragDropFileMixin implement
14371444
final selectedIdentityFromHeader = _selectIdentityFromId(identityIdFromHeader);
14381445
if (selectedIdentityFromHeader == null) return;
14391446
identitySelected.value = selectedIdentityFromHeader;
1447+
1448+
_initEmailDraftHash();
14401449
}
14411450

14421451
Future<void> restoreCollapsibleButton(String? emailContent) async {
@@ -1445,6 +1454,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
14451454
final emailDocument = parse(emailContent);
14461455
final signature = emailDocument.querySelector('div.tmail-signature');
14471456
if (signature == null) return;
1457+
_restoringSignatureButton = true;
14481458
await _applySignature(signature.innerHtml);
14491459
} catch (e) {
14501460
logError('ComposerController::_restoreCollapsibleButton: $e');
@@ -1793,7 +1803,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
17931803
void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) {
17941804
uploadController.clearUploadInlineViewState();
17951805

1796-
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl);
1806+
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
17971807
final accountId = mailboxDashBoardController.accountId.value;
17981808

17991809
if (baseDownloadUrl != null && accountId != null) {
@@ -1988,6 +1998,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement
19881998
initTextEditor(text);
19891999
}
19902000
_textEditorWeb = text;
2001+
2002+
_initEmailDraftHashAfterSignatureButtonRestored(text);
2003+
}
2004+
2005+
void _initEmailDraftHashAfterSignatureButtonRestored(String? emailContent) {
2006+
if (!_restoringSignatureButton) return;
2007+
final emailDocument = parse(emailContent);
2008+
final signatureButton = emailDocument.querySelector('button.tmail-signature-button');
2009+
if (signatureButton == null) return;
2010+
2011+
_restoringSignatureButton = false;
2012+
_initEmailDraftHash();
19912013
}
19922014

19932015
void initTextEditor(String? text) {
@@ -2087,12 +2109,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
20872109
return;
20882110
}
20892111

2090-
final isChanged = await _validateEmailChange(
2091-
context: context,
2092-
emailActionType: composerArguments.value!.emailActionType,
2093-
presentationEmail: composerArguments.value!.presentationEmail,
2094-
mailboxRole: composerArguments.value!.mailboxRole
2095-
);
2112+
final isChanged = await _validateEmailChange();
20962113

20972114
if (isChanged && context.mounted) {
20982115
clearFocus(context);
@@ -2368,6 +2385,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
23682385
void _setUpRequestReadReceiptForDraftEmail(Email? email) {
23692386
if (email?.hasRequestReadReceipt == true) {
23702387
hasRequestReadReceipt.value = true;
2388+
_initEmailDraftHash();
23712389
} else {
23722390
_getAlwaysReadReceiptSetting();
23732391
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:jmap_dart_client/jmap/identities/identity.dart';
3+
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
4+
import 'package:model/email/attachment.dart';
5+
6+
class SavedEmailDraft with EquatableMixin {
7+
final String content;
8+
final String subject;
9+
final Set<EmailAddress> toRecipients;
10+
final Set<EmailAddress> ccRecipients;
11+
final Set<EmailAddress> bccRecipients;
12+
final List<Attachment> attachments;
13+
final Identity? identity;
14+
final bool hasReadReceipt;
15+
16+
SavedEmailDraft({
17+
required this.content,
18+
required this.subject,
19+
required this.toRecipients,
20+
required this.ccRecipients,
21+
required this.bccRecipients,
22+
required this.attachments,
23+
required this.identity,
24+
required this.hasReadReceipt,
25+
});
26+
27+
@override
28+
List<Object?> get props => [
29+
content,
30+
subject,
31+
// Prevent identical Set<EmailAddress>
32+
{0: toRecipients},
33+
{1: ccRecipients},
34+
{2: bccRecipients},
35+
attachments,
36+
identity,
37+
hasReadReceipt
38+
];
39+
}

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1578,7 +1578,7 @@ packages:
15781578
source: hosted
15791579
version: "3.1.3"
15801580
plugin_platform_interface:
1581-
dependency: transitive
1581+
dependency: "direct dev"
15821582
description:
15831583
name: plugin_platform_interface
15841584
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"

pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ dev_dependencies:
270270

271271
http_mock_adapter: 0.4.2
272272

273+
plugin_platform_interface: 2.1.8
274+
273275
dependency_overrides:
274276
firebase_core_platform_interface: 4.6.0
275277

0 commit comments

Comments
 (0)