Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-3034 Prevent duplicate draft warning #3184

Merged
merged 2 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions core/lib/utils/platform_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
132 changes: 78 additions & 54 deletions lib/features/composer/presentation/composer_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -358,6 +367,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
}
} else if (failure is GetAlwaysReadReceiptSettingFailure) {
hasRequestReadReceipt.value = false;
_initEmailDraftHash();
}
}

Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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();
}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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());

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;
Future<bool> _validateEmailChange() async {
final newDraftHash = await _hashDraftEmail();

return _savedEmailDraftHash != newDraftHash;
}

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
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
) {
return;
}

return false;
_savedEmailDraftHash = await _hashDraftEmail();
}

void handleClickSaveAsDraftsButton(BuildContext context) async {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1437,14 +1444,22 @@ 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 {
try {
if (emailContent == null) return;
final emailDocument = parse(emailContent);

final existedSignatureButton = emailDocument.querySelector(
'button.tmail-signature-button');
if (existedSignatureButton != null) return;

final signature = emailDocument.querySelector('div.tmail-signature');
if (signature == null) return;
_restoringSignatureButton = true;
await _applySignature(signature.innerHtml);
} catch (e) {
logError('ComposerController::_restoreCollapsibleButton: $e');
Expand Down Expand Up @@ -1793,7 +1808,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) {
Expand Down Expand Up @@ -1925,6 +1940,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement


void handleInitHtmlEditorWeb(String initContent) async {
if (_isEmailBodyLoaded) return;
log('ComposerController::handleInitHtmlEditorWeb:');
_isEmailBodyLoaded = true;
richTextWebController?.editorController.setFullScreen();
Expand Down Expand Up @@ -1988,6 +2004,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) {
Expand Down Expand Up @@ -2087,12 +2115,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);
Expand Down Expand Up @@ -2368,6 +2391,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
void _setUpRequestReadReceiptForDraftEmail(Email? email) {
if (email?.hasRequestReadReceipt == true) {
hasRequestReadReceipt.value = true;
_initEmailDraftHash();
} else {
_getAlwaysReadReceiptSetting();
}
Expand Down
39 changes: 39 additions & 0 deletions lib/features/composer/presentation/model/saved_email_draft.dart
Original file line number Diff line number Diff line change
@@ -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},
dab246 marked this conversation as resolved.
Show resolved Hide resolved
attachments,
identity,
hasReadReceipt
];
}
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading