TF-3034 Prevent duplicate draft warning

This commit is contained in:
DatDang
2024-10-01 11:44:40 +07:00
committed by Dat H. Pham
parent 416baa18d3
commit e041362b11
8 changed files with 1779 additions and 55 deletions
+6 -6
View File
@@ -7,12 +7,12 @@ abstract class PlatformInfo {
static bool isTestingForWeb = false;
static bool get isWeb => kIsWeb || isTestingForWeb;
static bool get isLinux => !kIsWeb && defaultTargetPlatform == TargetPlatform.linux;
static bool get isWindows => !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
static bool get isMacOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
static bool get isFuchsia => !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia;
static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
static bool get isLinux => !isWeb && defaultTargetPlatform == TargetPlatform.linux;
static bool get isWindows => !isWeb && defaultTargetPlatform == TargetPlatform.windows;
static bool get isMacOS => !isWeb && defaultTargetPlatform == TargetPlatform.macOS;
static bool get isFuchsia => !isWeb && defaultTargetPlatform == TargetPlatform.fuchsia;
static bool get isIOS => !isWeb && defaultTargetPlatform == TargetPlatform.iOS;
static bool get isAndroid => !isWeb && defaultTargetPlatform == TargetPlatform.android;
static bool get isMobile => isAndroid || isIOS;
static bool get isDesktop => isLinux || isWindows || isMacOS;
static bool get isCanvasKit => isRendererCanvasKit;
@@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/list_sha
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/signature_status.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
@@ -109,7 +110,6 @@ class ComposerController extends BaseController with DragDropFileMixin implement
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final networkConnectionController = Get.find<NetworkConnectionController>();
final _dynamicUrlInterceptors = Get.find<DynamicUrlInterceptors>();
final _beforeReconnectManager = Get.find<BeforeReconnectManager>();
final composerArguments = Rxn<ComposerArguments>();
@@ -199,6 +199,14 @@ class ComposerController extends BaseController with DragDropFileMixin implement
ButtonState _saveToDraftButtonState = ButtonState.enabled;
ButtonState _sendButtonState = ButtonState.enabled;
SignatureStatus _identityContentOnOpenPolicy = SignatureStatus.editedAvailable;
int? _savedEmailDraftHash;
bool _restoringSignatureButton = false;
@visibleForTesting
bool get restoringSignatureButton => _restoringSignatureButton;
@visibleForTesting
int? get savedEmailDraftHash => _savedEmailDraftHash;
late Worker uploadInlineImageWorker;
late Worker dashboardViewStateWorker;
@@ -330,6 +338,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
maxWithEditor = null;
} else if (success is GetAlwaysReadReceiptSettingSuccess) {
hasRequestReadReceipt.value = success.alwaysReadReceiptEnabled;
_initEmailDraftHash();
} else if (success is RestoreEmailInlineImagesSuccess) {
_updateEditorContent(success);
}
@@ -358,6 +367,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
}
} else if (failure is GetAlwaysReadReceiptSettingFailure) {
hasRequestReadReceipt.value = false;
_initEmailDraftHash();
}
}
@@ -732,16 +742,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement
(identity) => identity.id == identityId);
}
void _initIdentities(ComposerArguments composerArguments) {
Future<void> _initIdentities(ComposerArguments composerArguments) async {
listFromIdentities.value = composerArguments.identities ?? [];
final selectedIdentityFromId = _selectIdentityFromId(
composerArguments.selectedIdentityId);
if (listFromIdentities.isEmpty) {
_getAllIdentities();
} else if (selectedIdentityFromId != null) {
_selectIdentity(selectedIdentityFromId);
await _selectIdentity(selectedIdentityFromId);
_initEmailDraftHash();
} else if (composerArguments.identities?.isNotEmpty == true) {
_selectIdentity(composerArguments.identities!.first);
await _selectIdentity(composerArguments.identities!.first);
_initEmailDraftHash();
}
}
@@ -764,8 +776,10 @@ class ComposerController extends BaseController with DragDropFileMixin implement
composerArguments.value?.selectedIdentityId);
if (selectedIdentityFromId != null) {
await _selectIdentity(selectedIdentityFromId);
_initEmailDraftHash();
} else {
await _selectIdentity(listIdentitiesMayDeleted.firstOrNull);
_initEmailDraftHash();
}
}
}
@@ -1216,7 +1230,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
if (session != null && accountId != null) {
final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl);
final uploadUri = session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl);
uploadController.justUploadAttachmentsAction(
uploadFiles: pickedFiles,
uploadUri: uploadUri,
@@ -1230,50 +1244,41 @@ class ComposerController extends BaseController with DragDropFileMixin implement
uploadController.deleteFileUploaded(uploadId);
}
Future<bool> _validateEmailChange({
required BuildContext context,
required EmailActionType emailActionType,
PresentationEmail? presentationEmail,
Role? mailboxRole,
}) async {
final newEmailBody = await _getContentInEditor();
final oldEmailBody = _initTextEditor ?? '';
log('ComposerController::_validateEmailChange: newEmailBody = $newEmailBody | oldEmailBody = $oldEmailBody');
final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim());
Future<bool> _validateEmailChange() async {
final newDraftHash = await _hashDraftEmail();
final newEmailSubject = subjectEmail.value ?? '';
final oldEmailSubject = emailActionType == EmailActionType.editDraft
? presentationEmail?.getEmailTitle().trim() ?? ''
: '';
final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim());
final recipients = presentationEmail
?.generateRecipientsEmailAddressForComposer(
emailActionType: emailActionType,
mailboxRole: mailboxRole
) ?? const Tuple3(<EmailAddress>[], <EmailAddress>[], <EmailAddress>[]);
final newToEmailAddress = listToEmailAddress;
final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : [];
final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress);
final newCcEmailAddress = listCcEmailAddress;
final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : [];
final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress);
final newBccEmailAddress = listBccEmailAddress;
final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : [];
final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress);
final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList());
log('ComposerController::_validateChangeEmail: isEmailBodyChanged = $isEmailBodyChanged | isEmailSubjectChanged = $isEmailSubjectChanged | isToEmailAddressChanged = $isToEmailAddressChanged | isCcEmailAddressChanged = $isCcEmailAddressChanged | isBccEmailAddressChanged = $isBccEmailAddressChanged | isAttachmentsChanged = $isAttachmentsChanged');
if (isEmailBodyChanged || isEmailSubjectChanged
|| isToEmailAddressChanged || isCcEmailAddressChanged
|| isBccEmailAddressChanged || isAttachmentsChanged) {
return true;
return _savedEmailDraftHash != newDraftHash;
}
return false;
Future<int> _hashDraftEmail() async {
final emailContent = await _getContentInEditor();
final savedEmailDraft = SavedEmailDraft(
subject: subjectEmail.value ?? '',
content: emailContent,
toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(),
identity: identitySelected.value,
attachments: uploadController.attachmentsUploaded,
hasReadReceipt: hasRequestReadReceipt.value,
);
return savedEmailDraft.hashCode;
}
Future<void> _updateSavedEmailDraftHash() async {
_savedEmailDraftHash = await _hashDraftEmail();
}
Future<void> _initEmailDraftHash() async {
if (composerArguments.value?.emailActionType != EmailActionType.compose
&& composerArguments.value?.emailActionType != EmailActionType.editDraft
) {
return;
}
_savedEmailDraftHash = await _hashDraftEmail();
}
void handleClickSaveAsDraftsButton(BuildContext context) async {
@@ -1306,10 +1311,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_saveToDraftButtonState = ButtonState.enabled;
_emailIdEditing = resultState.emailId;
mailboxDashBoardController.consumeState(Stream.value(Right<Failure, Success>(resultState)));
_updateSavedEmailDraftHash();
} else if (resultState is UpdateEmailDraftsSuccess) {
_saveToDraftButtonState = ButtonState.enabled;
_emailIdEditing = resultState.emailId;
mailboxDashBoardController.consumeState(Stream.value(Right<Failure, Success>(resultState)));
_updateSavedEmailDraftHash();
} else if ((resultState is SaveEmailAsDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException) ||
(resultState is UpdateEmailDraftsFailure && resultState.exception is SavingEmailToDraftsCanceledException)) {
_saveToDraftButtonState = ButtonState.enabled;
@@ -1437,6 +1444,8 @@ class ComposerController extends BaseController with DragDropFileMixin implement
final selectedIdentityFromHeader = _selectIdentityFromId(identityIdFromHeader);
if (selectedIdentityFromHeader == null) return;
identitySelected.value = selectedIdentityFromHeader;
_initEmailDraftHash();
}
Future<void> restoreCollapsibleButton(String? emailContent) async {
@@ -1445,6 +1454,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
final emailDocument = parse(emailContent);
final signature = emailDocument.querySelector('div.tmail-signature');
if (signature == null) return;
_restoringSignatureButton = true;
await _applySignature(signature.innerHtml);
} catch (e) {
logError('ComposerController::_restoreCollapsibleButton: $e');
@@ -1793,7 +1803,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) {
uploadController.clearUploadInlineViewState();
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl);
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
final accountId = mailboxDashBoardController.accountId.value;
if (baseDownloadUrl != null && accountId != null) {
@@ -1988,6 +1998,18 @@ class ComposerController extends BaseController with DragDropFileMixin implement
initTextEditor(text);
}
_textEditorWeb = text;
_initEmailDraftHashAfterSignatureButtonRestored(text);
}
void _initEmailDraftHashAfterSignatureButtonRestored(String? emailContent) {
if (!_restoringSignatureButton) return;
final emailDocument = parse(emailContent);
final signatureButton = emailDocument.querySelector('button.tmail-signature-button');
if (signatureButton == null) return;
_restoringSignatureButton = false;
_initEmailDraftHash();
}
void initTextEditor(String? text) {
@@ -2087,12 +2109,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
return;
}
final isChanged = await _validateEmailChange(
context: context,
emailActionType: composerArguments.value!.emailActionType,
presentationEmail: composerArguments.value!.presentationEmail,
mailboxRole: composerArguments.value!.mailboxRole
);
final isChanged = await _validateEmailChange();
if (isChanged && context.mounted) {
clearFocus(context);
@@ -2368,6 +2385,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
void _setUpRequestReadReceiptForDraftEmail(Email? email) {
if (email?.hasRequestReadReceipt == true) {
hasRequestReadReceipt.value = true;
_initEmailDraftHash();
} else {
_getAlwaysReadReceiptSetting();
}
@@ -0,0 +1,39 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/attachment.dart';
class SavedEmailDraft with EquatableMixin {
final String content;
final String subject;
final Set<EmailAddress> toRecipients;
final Set<EmailAddress> ccRecipients;
final Set<EmailAddress> bccRecipients;
final List<Attachment> attachments;
final Identity? identity;
final bool hasReadReceipt;
SavedEmailDraft({
required this.content,
required this.subject,
required this.toRecipients,
required this.ccRecipients,
required this.bccRecipients,
required this.attachments,
required this.identity,
required this.hasReadReceipt,
});
@override
List<Object?> get props => [
content,
subject,
// Prevent identical Set<EmailAddress>
{0: toRecipients},
{1: ccRecipients},
{2: bccRecipients},
attachments,
identity,
hasReadReceipt
];
}
+1 -1
View File
@@ -1578,7 +1578,7 @@ packages:
source: hosted
version: "3.1.3"
plugin_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+2
View File
@@ -270,6 +270,8 @@ dev_dependencies:
http_mock_adapter: 0.4.2
plugin_platform_interface: 2.1.8
dependency_overrides:
firebase_core_platform_interface: 4.6.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,148 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart';
void main() {
group('saved email draft test', () {
test(
'should tag toRecipients, ccRecipients and bccRecipients in props',
() {
// arrange
final savedEmailDraft = SavedEmailDraft(
subject: 'subject',
content: 'content',
toRecipients: {EmailAddress('to name', 'to email')},
ccRecipients: {EmailAddress('cc name', 'cc email')},
bccRecipients: {EmailAddress('bcc name', 'bcc email')},
identity: null,
attachments: [],
hasReadReceipt: false
);
// act
final props = savedEmailDraft.props;
// assert
expect(props[2], equals({0: savedEmailDraft.toRecipients}));
expect(props[3], equals({1: savedEmailDraft.ccRecipients}));
expect(props[4], equals({2: savedEmailDraft.bccRecipients}));
});
test(
'should generate different hashcode '
'when toRecipients, ccRecipients and bccRecipients are different',
() {
// arrange
const subject = 'subject';
const content = 'content';
final recipent = EmailAddress('recipent name', 'recipent email');
final identity = Identity();
final attachments = <Attachment>[];
const hasReadReceipt = false;
final toSavedEmailDraft = SavedEmailDraft(
subject: subject,
content: content,
toRecipients: {recipent},
ccRecipients: {},
bccRecipients: {},
identity: identity,
attachments: attachments,
hasReadReceipt: hasReadReceipt
);
final ccSavedEmailDraft = SavedEmailDraft(
subject: subject,
content: content,
toRecipients: {},
ccRecipients: {recipent},
bccRecipients: {},
identity: identity,
attachments: attachments,
hasReadReceipt: hasReadReceipt
);
final bccSavedEmailDraft = SavedEmailDraft(
subject: subject,
content: content,
toRecipients: {},
ccRecipients: {},
bccRecipients: {recipent},
identity: identity,
attachments: attachments,
hasReadReceipt: hasReadReceipt
);
// act
final toProps = toSavedEmailDraft.props;
final ccProps = ccSavedEmailDraft.props;
final bccProps = bccSavedEmailDraft.props;
// assert
expect(toProps.hashCode, isNot(ccProps.hashCode));
expect(ccProps.hashCode, isNot(bccProps.hashCode));
expect(bccProps.hashCode, isNot(toProps.hashCode));
});
test(
'should generate different hashcode '
'when toRecipients is updated',
() {
// arrange
final listToRecipients = {
EmailAddress('to name', 'to email')
};
final savedEmailDraft = SavedEmailDraft(
subject: 'subject',
content: 'content',
toRecipients: listToRecipients,
ccRecipients: {EmailAddress('cc name', 'cc email')},
bccRecipients: {EmailAddress('bcc name', 'bcc email')},
identity: null,
attachments: [],
hasReadReceipt: false
);
final hashCodeBeforeChange = savedEmailDraft.hashCode;
// act
listToRecipients.add(EmailAddress('to name 2', 'to email 2'));
final hashCodeAfterChange = savedEmailDraft.hashCode;
// assert
expect(hashCodeBeforeChange, isNot(hashCodeAfterChange));
});
test(
'should generate same hashcode '
'when all properties are the same',
() {
// arrange
final savedEmailDraft = SavedEmailDraft(
subject: 'subject',
content: 'content',
toRecipients: {EmailAddress('to name', 'to email')},
ccRecipients: {EmailAddress('cc name', 'cc email')},
bccRecipients: {EmailAddress('bcc name', 'bcc email')},
identity: null,
attachments: [],
hasReadReceipt: false
);
final savedEmailDraft2 = SavedEmailDraft(
subject: 'subject',
content: 'content',
toRecipients: {EmailAddress('to name', 'to email')},
ccRecipients: {EmailAddress('cc name', 'cc email')},
bccRecipients: {EmailAddress('bcc name', 'bcc email')},
identity: null,
attachments: [],
hasReadReceipt: false
);
// assert
expect(savedEmailDraft.hashCode, equals(savedEmailDraft2.hashCode));
});
});
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockWebViewPlatform extends InAppWebViewPlatform with MockPlatformInterfaceMixin {
@override
PlatformInAppWebViewWidget createPlatformInAppWebViewWidget(
PlatformInAppWebViewWidgetCreationParams params,
) => MockWebViewWidget.implementation(params);
@override
PlatformCookieManager createPlatformCookieManager(
PlatformCookieManagerCreationParams params,
) => MockPlatformCookieManager();
}
class MockPlatformCookieManager extends Fake implements PlatformCookieManager {
@override
Future<bool> deleteAllCookies() async {
return true;
}
@override
Future<bool> setCookie({
required WebUri url,
required String name,
required String value,
String path = '/',
String? domain,
int? expiresDate,
int? maxAge,
bool? isSecure,
bool? isHttpOnly,
HTTPCookieSameSitePolicy? sameSite,
PlatformInAppWebViewController? iosBelow11WebViewController,
PlatformInAppWebViewController? webViewController,
}) async {
return true;
}
}
class MockWebViewWidget extends PlatformInAppWebViewWidget {
MockWebViewWidget.implementation(super.params) : super.implementation();
@override
Widget build(BuildContext context) {
return const SizedBox.shrink();
}
@override
T controllerFromPlatform<T>(PlatformInAppWebViewController controller) {
throw UnimplementedError();
}
@override
void dispose() {}
}