TF-4013 Unified UX between mobile and web

(cherry picked from commit 298afd1871aedc3549191b16796241aec9b4552d)
This commit is contained in:
dab246
2025-10-07 13:52:03 +07:00
committed by Dat H. Pham
parent 69d2e59f35
commit bfdd6af3f7
11 changed files with 335 additions and 506 deletions
@@ -139,10 +139,6 @@ class ComposerController extends BaseController
final isInitialRecipient = false.obs;
final subjectEmail = Rxn<String>();
final screenDisplayMode = ScreenDisplayMode.normal.obs;
final toAddressExpandMode = ExpandMode.EXPAND.obs;
final ccAddressExpandMode = ExpandMode.EXPAND.obs;
final bccAddressExpandMode = ExpandMode.EXPAND.obs;
final replyToAddressExpandMode = ExpandMode.EXPAND.obs;
final emailContentsViewState = Rxn<Either<Failure, Success>>();
final hasRequestReadReceipt = false.obs;
final fromRecipientState = PrefixRecipientState.disabled.obs;
@@ -602,9 +598,8 @@ class ComposerController extends BaseController
&& !responsiveUtils.isScreenWithShortestSide(currentContext!)) {
richTextMobileTabletController?.richTextController.hideRichTextView();
}
_collapseAllRecipient();
autoCreateEmailTag();
if (PlatformInfo.isWeb && isRecipientsNotEmpty) {
if (isRecipientsNotEmpty) {
hideAllRecipients();
}
}
@@ -725,33 +720,11 @@ class ComposerController extends BaseController
listBccEmailAddress = List.from(recipients.bcc);
listReplyToEmailAddress = List.from(recipients.replyTo);
if (PlatformInfo.isWeb) {
if (isRecipientsNotEmpty) {
hideAllRecipients();
isInitialRecipient.value = true;
} else {
toRecipientState.value = PrefixRecipientState.enabled;
}
if (isRecipientsNotEmpty) {
hideAllRecipients();
isInitialRecipient.value = true;
} else {
if (isRecipientsNotEmpty) {
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listCcEmailAddress.isNotEmpty) {
ccRecipientState.value = PrefixRecipientState.enabled;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listBccEmailAddress.isNotEmpty) {
bccRecipientState.value = PrefixRecipientState.enabled;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listReplyToEmailAddress.isNotEmpty) {
replyToRecipientState.value = PrefixRecipientState.enabled;
replyToAddressExpandMode.value = ExpandMode.COLLAPSE;
}
toRecipientState.value = PrefixRecipientState.enabled;
}
updateStatusEmailSendButton();
}
@@ -802,7 +775,6 @@ class ComposerController extends BaseController
|| ccEmailAddressController.text.isNotEmpty
|| bccEmailAddressController.text.isNotEmpty
|| replyToEmailAddressController.text.isNotEmpty) {
_collapseAllRecipient();
autoCreateEmailTag();
}
@@ -824,12 +796,7 @@ class ComposerController extends BaseController
MessageDialogActionManager().showConfirmDialogAction(context,
appLocalizations.message_dialog_send_email_with_email_address_invalid,
appLocalizations.fix_email_addresses,
onConfirmAction: () {
toAddressExpandMode.value = ExpandMode.EXPAND;
ccAddressExpandMode.value = ExpandMode.EXPAND;
bccAddressExpandMode.value = ExpandMode.EXPAND;
replyToAddressExpandMode.value = ExpandMode.EXPAND;
},
onConfirmAction: showFullRecipients,
showAsBottomSheet: true,
title: appLocalizations.sending_failed,
hasCancelButton: false,
@@ -1473,8 +1440,7 @@ class ComposerController extends BaseController
void clickOutsideComposer(BuildContext context) {
clearFocus(context);
if (PlatformInfo.isWeb && isRecipientsNotEmpty) {
_collapseAllRecipient();
if (isRecipientsNotEmpty) {
hideAllRecipients();
}
}
@@ -1555,13 +1521,6 @@ class ComposerController extends BaseController
updatePrefixRootState();
}
void _collapseAllRecipient() {
toAddressExpandMode.value = ExpandMode.COLLAPSE;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
replyToAddressExpandMode.value = ExpandMode.COLLAPSE;
}
void clearFocusRecipients() {
toAddressFocusNode?.unfocus();
ccAddressFocusNode?.unfocus();
@@ -1599,47 +1558,8 @@ class ComposerController extends BaseController
}
}
void showFullEmailAddress(PrefixEmailAddress prefixEmailAddress) {
switch(prefixEmailAddress) {
case PrefixEmailAddress.to:
toAddressExpandMode.value = ExpandMode.EXPAND;
toAddressFocusNode?.requestFocus();
break;
case PrefixEmailAddress.cc:
ccAddressExpandMode.value = ExpandMode.EXPAND;
ccAddressFocusNode?.requestFocus();
break;
case PrefixEmailAddress.bcc:
bccAddressExpandMode.value = ExpandMode.EXPAND;
bccAddressFocusNode?.requestFocus();
break;
case PrefixEmailAddress.replyTo:
replyToAddressExpandMode.value = ExpandMode.EXPAND;
replyToAddressFocusNode?.requestFocus();
break;
default:
break;
}
}
void onEmailAddressFocusChange(PrefixEmailAddress prefixEmailAddress, bool isFocus) {
if (isFocus) {
switch(prefixEmailAddress) {
case PrefixEmailAddress.to:
toAddressExpandMode.value = ExpandMode.EXPAND;
break;
case PrefixEmailAddress.cc:
ccAddressExpandMode.value = ExpandMode.EXPAND;
break;
case PrefixEmailAddress.bcc:
bccAddressExpandMode.value = ExpandMode.EXPAND;
break;
case PrefixEmailAddress.replyTo:
replyToAddressExpandMode.value = ExpandMode.EXPAND;
break;
default:
break;
}
_closeSuggestionBox();
if (PlatformInfo.isMobile
&& currentContext != null
@@ -1649,28 +1569,24 @@ class ComposerController extends BaseController
} else {
switch(prefixEmailAddress) {
case PrefixEmailAddress.to:
toAddressExpandMode.value = ExpandMode.COLLAPSE;
final inputToEmail = toEmailAddressController.text;
if (inputToEmail.trim().isNotEmpty) {
autoCreateEmailTagForType(PrefixEmailAddress.to, inputToEmail);
}
break;
case PrefixEmailAddress.cc:
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
final inputCcEmail = ccEmailAddressController.text;
if (inputCcEmail.trim().isNotEmpty) {
autoCreateEmailTagForType(PrefixEmailAddress.cc, inputCcEmail);
}
break;
case PrefixEmailAddress.bcc:
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
final inputBccEmail = bccEmailAddressController.text;
if (inputBccEmail.trim().isNotEmpty) {
autoCreateEmailTagForType(PrefixEmailAddress.bcc, inputBccEmail);
}
break;
case PrefixEmailAddress.replyTo:
replyToAddressExpandMode.value = ExpandMode.COLLAPSE;
final inputReplyToEmail = replyToEmailAddressController.text;
if (inputReplyToEmail.trim().isNotEmpty) {
autoCreateEmailTagForType(PrefixEmailAddress.replyTo, inputReplyToEmail);
@@ -1722,10 +1638,6 @@ class ComposerController extends BaseController
} else {
listBccEmailAddress = listEmailAddress.toList();
}
toAddressExpandMode.value = ExpandMode.COLLAPSE;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
bccAddressExpandMode.refresh();
updateStatusEmailSendButton();
}
@@ -1736,9 +1648,6 @@ class ComposerController extends BaseController
if (listBccEmailAddress.isEmpty) {
bccRecipientState.value = PrefixRecipientState.disabled;
}
toAddressExpandMode.value = ExpandMode.COLLAPSE;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
updateStatusEmailSendButton();
}
@@ -1813,8 +1722,10 @@ class ComposerController extends BaseController
const Duration(milliseconds: 300),
richTextMobileTabletController?.richTextController.showDeviceKeyboard);
}
_collapseAllRecipient();
autoCreateEmailTag();
if (isRecipientsNotEmpty) {
hideAllRecipients();
}
}
void _onChangeCursorOnMobile(List<int>? coordinates, BuildContext context) {
@@ -1946,15 +1857,11 @@ class ComposerController extends BaseController
popBack();
}
autoCreateEmailTag();
if (PlatformInfo.isWeb && isRecipientsNotEmpty) {
if (isRecipientsNotEmpty) {
hideAllRecipients();
}
}
void handleOnMouseDownHtmlEditorWeb() {
_collapseAllRecipient();
}
FocusNode? getNextFocusOfToEmailAddress() {
if (ccRecipientState.value == PrefixRecipientState.enabled) {
return ccAddressFocusNode;
@@ -6,12 +6,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_open_context_menu_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/remove_draggable_email_address_between_recipient_fields_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart';
@@ -19,6 +22,7 @@ import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/tablet_container_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart';
@@ -121,58 +125,48 @@ class ComposerView extends GetWidget<ComposerController> {
return const SizedBox.shrink();
}
}),
Obx(() => RecipientComposerWidget(
prefix: PrefixEmailAddress.to,
listEmailAddress: controller.listToEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
fromState: controller.fromRecipientState.value,
ccState: controller.ccRecipientState.value,
bccState: controller.bccRecipientState.value,
replyToState: controller.replyToRecipientState.value,
expandMode: controller.toAddressExpandMode.value,
controller: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
keyTagEditor: controller.keyToEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
nextFocusNode: controller.getNextFocusOfToEmailAddress(),
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onAddEmailAddressTypeAction: controller.addEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
)),
Obx(() {
if (controller.recipientsCollapsedState.value == PrefixRecipientState.enabled) {
return RecipientsCollapsedComposerWidget(
listEmailAddress: controller.allListEmailAddress,
margin: ComposerStyle.mobileRecipientMargin,
onShowAllRecipientsAction: controller.showFullRecipients,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.toRecipientState.value == PrefixRecipientState.enabled) {
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.to,
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listToEmailAddress,
textController: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
focusNodeKeyboard: controller.toAddressFocusNodeKeyboard,
keyTagEditor: controller.keyToEmailTagEditor,
nextFocusNode: controller.getNextFocusOfToEmailAddress(),
isMobile: true,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.ccRecipientState.value == PrefixRecipientState.enabled) {
return RecipientComposerWidget(
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.cc,
listEmailAddress: controller.listCcEmailAddress,
imagePaths: controller.imagePaths,
controller: controller,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
expandMode: controller.ccAddressExpandMode.value,
controller: controller.ccEmailAddressController,
listEmailAddress: controller.listCcEmailAddress,
textController: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard,
keyTagEditor: controller.keyCcEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
nextFocusNode: controller.getNextFocusOfCcEmailAddress(),
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
isMobile: true,
);
} else {
return const SizedBox.shrink();
@@ -180,28 +174,17 @@ class ComposerView extends GetWidget<ComposerController> {
}),
Obx(() {
if (controller.bccRecipientState.value == PrefixRecipientState.enabled) {
return RecipientComposerWidget(
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.bcc,
listEmailAddress: controller.listBccEmailAddress,
imagePaths: controller.imagePaths,
controller: controller,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
expandMode: controller.bccAddressExpandMode.value,
controller: controller.bccEmailAddressController,
listEmailAddress: controller.listBccEmailAddress,
textController: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard,
keyTagEditor: controller.keyBccEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
nextFocusNode: controller.getNextFocusOfBccEmailAddress(),
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
isMobile: true,
);
} else {
return const SizedBox.shrink();
@@ -209,28 +192,17 @@ class ComposerView extends GetWidget<ComposerController> {
}),
Obx(() {
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) {
return RecipientComposerWidget(
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.replyTo,
listEmailAddress: controller.listReplyToEmailAddress,
imagePaths: controller.imagePaths,
controller: controller,
maxWidth: constraints.maxWidth,
expandMode: controller.replyToAddressExpandMode.value,
controller: controller.replyToEmailAddressController,
listEmailAddress: controller.listReplyToEmailAddress,
textController: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard,
keyTagEditor: controller.keyReplyToEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
nextFocusNode: controller.subjectEmailInputFocusNode,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onAddEmailAddressTypeAction: controller.addEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
isMobile: true,
);
} else {
return const SizedBox.shrink();
@@ -332,105 +304,89 @@ class ComposerView extends GetWidget<ComposerController> {
margin: ComposerStyle.mobileRecipientMargin,
onChangeIdentity: controller.onChangeIdentity,
),
RecipientComposerWidget(
prefix: PrefixEmailAddress.to,
listEmailAddress: controller.listToEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
fromState: controller.fromRecipientState.value,
ccState: controller.ccRecipientState.value,
bccState: controller.bccRecipientState.value,
replyToState: controller.replyToRecipientState.value,
expandMode: controller.toAddressExpandMode.value,
controller: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
keyTagEditor: controller.keyToEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
nextFocusNode: controller.getNextFocusOfToEmailAddress(),
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onAddEmailAddressTypeAction: controller.addEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
if (controller.ccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
prefix: PrefixEmailAddress.cc,
listEmailAddress: controller.listCcEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
expandMode: controller.ccAddressExpandMode.value,
controller: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
keyTagEditor: controller.keyCcEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
nextFocusNode: controller.getNextFocusOfCcEmailAddress(),
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
if (controller.bccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
prefix: PrefixEmailAddress.bcc,
listEmailAddress: controller.listBccEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: constraints.maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
expandMode: controller.bccAddressExpandMode.value,
controller: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
keyTagEditor: controller.keyBccEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
nextFocusNode: controller.getNextFocusOfBccEmailAddress(),
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
prefix: PrefixEmailAddress.replyTo,
listEmailAddress: controller.listReplyToEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: constraints.maxWidth,
expandMode: controller.replyToAddressExpandMode.value,
controller: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
keyTagEditor: controller.keyReplyToEmailTagEditor,
isInitial: controller.isInitialRecipient.value,
nextFocusNode: controller.subjectEmailInputFocusNode,
padding: ComposerStyle.mobileRecipientPadding,
margin: ComposerStyle.mobileRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
Obx(() {
if (controller.recipientsCollapsedState.value == PrefixRecipientState.enabled) {
return RecipientsCollapsedComposerWidget(
listEmailAddress: controller.allListEmailAddress,
margin: ComposerStyle.mobileRecipientMargin,
onShowAllRecipientsAction: controller.showFullRecipients,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.toRecipientState.value == PrefixRecipientState.enabled) {
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.to,
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listToEmailAddress,
textController: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
focusNodeKeyboard: controller.toAddressFocusNodeKeyboard,
keyTagEditor: controller.keyToEmailTagEditor,
nextFocusNode: controller.getNextFocusOfToEmailAddress(),
isMobile: true,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.ccRecipientState.value == PrefixRecipientState.enabled) {
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.cc,
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listCcEmailAddress,
textController: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard,
keyTagEditor: controller.keyCcEmailTagEditor,
nextFocusNode: controller.getNextFocusOfCcEmailAddress(),
isMobile: true,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.bccRecipientState.value == PrefixRecipientState.enabled) {
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.bcc,
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listBccEmailAddress,
textController: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard,
keyTagEditor: controller.keyBccEmailTagEditor,
nextFocusNode: controller.getNextFocusOfBccEmailAddress(),
isMobile: true,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) {
return _buildRecipientComposerWidget(
prefix: PrefixEmailAddress.replyTo,
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listReplyToEmailAddress,
textController: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard,
keyTagEditor: controller.keyReplyToEmailTagEditor,
nextFocusNode: controller.subjectEmailInputFocusNode,
isMobile: true,
);
} else {
return const SizedBox.shrink();
}
}),
],
)),
SubjectComposerWidget(
@@ -525,4 +481,53 @@ class ComposerView extends GetWidget<ComposerController> {
..onActionClick((_) => controller.openFilePickerByType(context, FileType.any)))
.build();
}
Widget _buildRecipientComposerWidget({
required PrefixEmailAddress prefix,
required ComposerController controller,
required double maxWidth,
required List<EmailAddress> listEmailAddress,
required TextEditingController textController,
required FocusNode? focusNode,
required FocusNode? focusNodeKeyboard,
required GlobalKey keyTagEditor,
required FocusNode? nextFocusNode,
bool isMobile = false,
}) {
return Obx(() => RecipientComposerWidget(
prefix: prefix,
prefixRootState: controller.prefixRootState.value,
fromState: controller.fromRecipientState.value,
toState: controller.toRecipientState.value,
ccState: controller.ccRecipientState.value,
bccState: controller.bccRecipientState.value,
replyToState: controller.replyToRecipientState.value,
listEmailAddress: listEmailAddress,
imagePaths: controller.imagePaths,
maxWidth: maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
controller: textController,
focusNode: focusNode,
focusNodeKeyboard: focusNodeKeyboard,
keyTagEditor: keyTagEditor,
isInitial: controller.isInitialRecipient.value,
nextFocusNode: nextFocusNode,
padding: isMobile
? ComposerStyle.mobileRecipientPadding
: ComposerStyle.desktopRecipientPadding,
margin: isMobile
? ComposerStyle.mobileRecipientMargin
: ComposerStyle.desktopRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
onAddEmailAddressTypeAction: controller.addEmailAddressType,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction,
));
}
}
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.dart';
@@ -134,7 +133,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listToEmailAddress,
expandMode: controller.toAddressExpandMode.value,
textController: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
focusNodeKeyboard: controller.toAddressFocusNodeKeyboard,
@@ -149,7 +147,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listCcEmailAddress,
expandMode: controller.ccAddressExpandMode.value,
textController: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard,
@@ -164,7 +161,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listBccEmailAddress,
expandMode: controller.bccAddressExpandMode.value,
textController: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard,
@@ -179,7 +175,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listReplyToEmailAddress,
expandMode: controller.replyToAddressExpandMode.value,
textController: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard,
@@ -215,7 +210,6 @@ class ComposerView extends GetWidget<ComposerController> {
onInitial: controller.handleInitHtmlEditorWeb,
onChangeContent: controller.onChangeTextEditorWeb,
onFocus: controller.handleOnFocusHtmlEditorWeb,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
onEditorSettings: controller.richTextWebController!.onEditorSettingsChange,
onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged,
height: constraintsEditor.maxHeight,
@@ -366,7 +360,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listToEmailAddress,
expandMode: controller.toAddressExpandMode.value,
textController: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
focusNodeKeyboard: controller.toAddressFocusNodeKeyboard,
@@ -380,7 +373,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listCcEmailAddress,
expandMode: controller.ccAddressExpandMode.value,
textController: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard,
@@ -394,7 +386,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listBccEmailAddress,
expandMode: controller.bccAddressExpandMode.value,
textController: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard,
@@ -408,7 +399,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listReplyToEmailAddress,
expandMode: controller.replyToAddressExpandMode.value,
textController: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard,
@@ -457,7 +447,6 @@ class ComposerView extends GetWidget<ComposerController> {
onInitial: controller.handleInitHtmlEditorWeb,
onChangeContent: controller.onChangeTextEditorWeb,
onFocus: controller.handleOnFocusHtmlEditorWeb,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
onEditorSettings: controller.richTextWebController?.onEditorSettingsChange,
onEditorTextSizeChanged: controller.richTextWebController?.onEditorTextSizeChanged,
height: constraintsEditor.maxHeight,
@@ -639,7 +628,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listToEmailAddress,
expandMode: controller.toAddressExpandMode.value,
textController: controller.toEmailAddressController,
focusNode: controller.toAddressFocusNode,
focusNodeKeyboard: controller.toAddressFocusNodeKeyboard,
@@ -653,7 +641,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listCcEmailAddress,
expandMode: controller.ccAddressExpandMode.value,
textController: controller.ccEmailAddressController,
focusNode: controller.ccAddressFocusNode,
focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard,
@@ -667,7 +654,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listBccEmailAddress,
expandMode: controller.bccAddressExpandMode.value,
textController: controller.bccEmailAddressController,
focusNode: controller.bccAddressFocusNode,
focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard,
@@ -681,7 +667,6 @@ class ComposerView extends GetWidget<ComposerController> {
controller: controller,
maxWidth: constraints.maxWidth,
listEmailAddress: controller.listReplyToEmailAddress,
expandMode: controller.replyToAddressExpandMode.value,
textController: controller.replyToEmailAddressController,
focusNode: controller.replyToAddressFocusNode,
focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard,
@@ -729,7 +714,6 @@ class ComposerView extends GetWidget<ComposerController> {
onInitial: controller.handleInitHtmlEditorWeb,
onChangeContent: controller.onChangeTextEditorWeb,
onFocus: controller.handleOnFocusHtmlEditorWeb,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
onEditorSettings: controller.richTextWebController!.onEditorSettingsChange,
onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged,
height: constraintsBody.maxHeight,
@@ -866,7 +850,6 @@ class ComposerView extends GetWidget<ComposerController> {
required ComposerController controller,
required double maxWidth,
required List<EmailAddress> listEmailAddress,
required ExpandMode expandMode,
required TextEditingController textController,
required FocusNode? focusNode,
required FocusNode? focusNodeKeyboard,
@@ -887,7 +870,6 @@ class ComposerView extends GetWidget<ComposerController> {
imagePaths: controller.imagePaths,
maxWidth: maxWidth,
minInputLengthAutocomplete: controller.minInputLengthAutocomplete,
expandMode: expandMode,
controller: textController,
focusNode: focusNode,
focusNodeKeyboard: focusNodeKeyboard,
@@ -901,16 +883,15 @@ class ComposerView extends GetWidget<ComposerController> {
? ComposerStyle.mobileRecipientMargin
: ComposerStyle.desktopRecipientMargin,
onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange,
onShowFullListEmailAddressAction: controller.showFullEmailAddress,
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction:
controller.removeDraggableEmailAddress,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
onAddEmailAddressTypeAction: controller.addEmailAddressType,
onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType,
onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction,
));
}
}
@@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/extensions/email_address_extension.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/email_address_action_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_create_new_rule_filter.dart';
@@ -60,8 +59,7 @@ extension HandleEditRecipientExtension on ComposerController {
switch(prefix) {
case PrefixEmailAddress.to:
listToEmailAddress.remove(emailAddress);
toAddressExpandMode.value = ExpandMode.EXPAND;
toAddressExpandMode.refresh();
toRecipientState.refresh();
_setTextAndFocus(
controller: toEmailAddressController,
focusNode: toAddressFocusNode,
@@ -70,8 +68,7 @@ extension HandleEditRecipientExtension on ComposerController {
break;
case PrefixEmailAddress.cc:
listCcEmailAddress.remove(emailAddress);
ccAddressExpandMode.value = ExpandMode.EXPAND;
ccAddressExpandMode.refresh();
ccRecipientState.refresh();
_setTextAndFocus(
controller: ccEmailAddressController,
focusNode: ccAddressFocusNode,
@@ -80,8 +77,7 @@ extension HandleEditRecipientExtension on ComposerController {
break;
case PrefixEmailAddress.bcc:
listBccEmailAddress.remove(emailAddress);
bccAddressExpandMode.value = ExpandMode.EXPAND;
bccAddressExpandMode.refresh();
bccRecipientState.refresh();
_setTextAndFocus(
controller: bccEmailAddressController,
focusNode: bccAddressFocusNode,
@@ -90,8 +86,7 @@ extension HandleEditRecipientExtension on ComposerController {
break;
case PrefixEmailAddress.replyTo:
listReplyToEmailAddress.remove(emailAddress);
replyToAddressExpandMode.value = ExpandMode.EXPAND;
replyToAddressExpandMode.refresh();
replyToRecipientState.refresh();
_setTextAndFocus(
controller: replyToEmailAddressController,
focusNode: replyToAddressFocusNode,
@@ -1,7 +1,6 @@
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/extensions/email_address_extension.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
@@ -31,22 +30,18 @@ extension HandleRecipientsCollapsedExtensions on ComposerController {
if (listToEmailAddress.isNotEmpty) {
toRecipientState.value = PrefixRecipientState.enabled;
toAddressExpandMode.value = ExpandMode.EXPAND;
}
if (listCcEmailAddress.isNotEmpty) {
ccRecipientState.value = PrefixRecipientState.enabled;
ccAddressExpandMode.value = ExpandMode.EXPAND;
}
if (listBccEmailAddress.isNotEmpty) {
bccRecipientState.value = PrefixRecipientState.enabled;
bccAddressExpandMode.value = ExpandMode.EXPAND;
}
if (listReplyToEmailAddress.isNotEmpty) {
replyToRecipientState.value = PrefixRecipientState.enabled;
replyToAddressExpandMode.value = ExpandMode.EXPAND;
}
updatePrefixRootState();
@@ -96,6 +91,7 @@ extension HandleRecipientsCollapsedExtensions on ComposerController {
}
void hideAllRecipients() {
fromRecipientState.value = PrefixRecipientState.disabled;
toRecipientState.value = PrefixRecipientState.disabled;
ccRecipientState.value = PrefixRecipientState.disabled;
bccRecipientState.value = PrefixRecipientState.disabled;
@@ -1,5 +1,4 @@
import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/base/model/filter_filter.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
@@ -33,19 +32,15 @@ extension RemoveDraggableEmailAddressBetweenRecipientFieldsExtension on Composer
switch(draggableEmailAddress.filterField) {
case FilterField.to:
controller.listToEmailAddress.remove(draggableEmailAddress.emailAddress);
controller.toAddressExpandMode.value = ExpandMode.EXPAND;
break;
case FilterField.cc:
controller.listCcEmailAddress.remove(draggableEmailAddress.emailAddress);
controller.ccAddressExpandMode.value = ExpandMode.EXPAND;
break;
case FilterField.bcc:
controller.listBccEmailAddress.remove(draggableEmailAddress.emailAddress);
controller.bccAddressExpandMode.value = ExpandMode.EXPAND;
break;
case FilterField.replyTo:
controller.listReplyToEmailAddress.remove(draggableEmailAddress.emailAddress);
controller.replyToAddressExpandMode.value = ExpandMode.EXPAND;
break;
default:
break;
@@ -1,6 +1,5 @@
import 'package:model/email/email_action_type.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
@@ -29,7 +28,6 @@ extension SetupEmailRecipientsExtension on ComposerController {
if (emailAddressOfTo.isNotEmpty) {
listToEmailAddress.addAll(emailAddressOfTo);
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
break;
case EmailActionType.composeFromMailtoUri:
@@ -40,19 +38,16 @@ extension SetupEmailRecipientsExtension on ComposerController {
if (emailAddressOfTo.isNotEmpty) {
listToEmailAddress.addAll(emailAddressOfTo);
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (emailAddressOfCc.isNotEmpty) {
listCcEmailAddress = emailAddressOfCc;
ccRecipientState.value = PrefixRecipientState.enabled;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (emailAddressOfBc.isNotEmpty) {
listBccEmailAddress = emailAddressOfBc;
bccRecipientState.value = PrefixRecipientState.enabled;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
break;
case EmailActionType.reply:
@@ -29,20 +29,31 @@ class RecipientsCollapsedComposerWidget extends StatelessWidget {
List<Widget> visibleChips = [];
int hiddenCount = 0;
for (var emailAddress in listEmailAddress) {
final textWidth =
_estimateTextWidth(context, emailAddress.asString()) + 80;
if (usedWidth + textWidth > maxWidth - 60) {
hiddenCount = listEmailAddress.length - visibleChips.length;
break;
}
usedWidth += textWidth + spacing;
if (listEmailAddress.length == 1) {
visibleChips.add(
Padding(
padding: const EdgeInsetsDirectional.only(end: spacing),
child: RecipientCollapsedItemWidget(emailAddress: emailAddress),
child: RecipientCollapsedItemWidget(
emailAddress: listEmailAddress.first,
),
),
);
} else {
for (var emailAddress in listEmailAddress) {
final textWidth =
_estimateTextWidth(context, emailAddress.asString()) + 80;
if (usedWidth + textWidth > maxWidth - 60) {
hiddenCount = listEmailAddress.length - visibleChips.length;
break;
}
usedWidth += textWidth + spacing;
visibleChips.add(
Padding(
padding: const EdgeInsetsDirectional.only(end: spacing),
child: RecipientCollapsedItemWidget(emailAddress: emailAddress),
),
);
}
}
if (hiddenCount > 0) {
@@ -16,12 +16,11 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/extensions/email_address_extension.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:super_tag_editor/tag_editor.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_address_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_named_address_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/mail_address_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/email_address_action_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
@@ -54,7 +53,6 @@ class RecipientComposerWidget extends StatefulWidget {
final List<EmailAddress> listEmailAddress;
final ImagePaths imagePaths;
final double maxWidth;
final ExpandMode expandMode;
final PrefixRecipientState fromState;
final PrefixRecipientState toState;
final PrefixRecipientState ccState;
@@ -71,7 +69,6 @@ class RecipientComposerWidget extends StatefulWidget {
final OnSuggestionEmailAddress? onSuggestionEmailAddress;
final OnAddEmailAddressTypeAction? onAddEmailAddressTypeAction;
final OnDeleteEmailAddressTypeAction? onDeleteEmailAddressTypeAction;
final OnShowFullListEmailAddressAction? onShowFullListEmailAddressAction;
final OnFocusEmailAddressChangeAction? onFocusEmailAddressChangeAction;
final OnRemoveDraggableEmailAddressAction? onRemoveDraggableEmailAddressAction;
final OnEditRecipientAction? onEditRecipientAction;
@@ -101,7 +98,6 @@ class RecipientComposerWidget extends StatefulWidget {
this.isInitial,
this.controller,
this.focusNode,
this.expandMode = ExpandMode.EXPAND,
this.keyTagEditor,
this.nextFocusNode,
this.padding,
@@ -111,7 +107,6 @@ class RecipientComposerWidget extends StatefulWidget {
this.onSuggestionEmailAddress,
this.onAddEmailAddressTypeAction,
this.onDeleteEmailAddressTypeAction,
this.onShowFullListEmailAddressAction,
this.onFocusEmailAddressChangeAction,
this.onFocusNextAddressAction,
this.onRemoveDraggableEmailAddressAction,
@@ -150,6 +145,8 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
@override
Widget build(BuildContext context) {
bool isMobileResponsive = _responsiveUtils.isMobile(context);
return Container(
decoration: const BoxDecoration(
border: Border(
@@ -226,9 +223,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
onSubmitted: (value) =>
_handleSubmitTagAction.call(value, stateSetter),
onTapOutside: (_) {},
onFocusTextInput: () {
widget.onShowFullListEmailAddressAction?.call(widget.prefix);
},
onFocusTextInput: () {},
inputDecoration: const InputDecoration(border: InputBorder.none),
tagBuilder: (context, index) {
final currentEmailAddress = _currentListEmailAddress[index];
@@ -240,13 +235,10 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
composerId: widget.composerId,
currentEmailAddress: currentEmailAddress,
currentListEmailAddress: _currentListEmailAddress,
collapsedListEmailAddress: _collapsedListEmailAddress,
isCollapsed: _isCollapse,
isTagFocused: _tagIndexFocused == index,
maxWidth: widget.maxWidth,
isMobile: _responsiveUtils.isMobile(context),
isMobile: isMobileResponsive,
onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter),
onShowFullAction: widget.onShowFullListEmailAddressAction,
onEditRecipientAction: widget.onEditRecipientAction,
onClearFocusAction: widget.onClearFocusAction,
);
@@ -311,75 +303,11 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
)
)
),
const SizedBox(width: RecipientComposerWidgetStyle.space),
if (widget.prefix == widget.prefixRootState)
if (PlatformInfo.isWeb || widget.isTestingForWeb)
...[
if (widget.fromState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_from_button'),
text: AppLocalizations.of(context).from_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from),
),
if (widget.toState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_to_button'),
text: AppLocalizations.of(context).to_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.to),
),
if (widget.ccState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_cc_button'),
text: AppLocalizations.of(context).cc_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc),
),
if (widget.bccState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'),
text: AppLocalizations.of(context).bcc_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc),
),
if (widget.replyToState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_reply_to_button'),
text: AppLocalizations.of(context).reply_to_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.replyTo),
),
]
else
TMailButtonWidget.fromIcon(
key: Key('prefix_${widget.prefix.name}_recipient_expand_button'),
icon: _isAllRecipientInputEnabled
? widget.imagePaths.icChevronUp
: widget.imagePaths.icChevronDownOutline,
backgroundColor: Colors.transparent,
iconSize: 24,
padding: const EdgeInsets.all(5),
iconColor: AppColor.colorLabelComposer,
margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin,
onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled),
)
else if (PlatformInfo.isWeb || widget.isTestingForWeb)
if (widget.prefix == widget.prefixRootState && _isWeb && !isMobileResponsive)
..._buildListPrefixWidgets(),
if (widget.prefix == widget.prefixRootState && (isMobileResponsive || widget.isTestingForWeb))
_buildExpandButton(),
if (widget.prefix != widget.prefixRootState && _isWeb && !isMobileResponsive)
TMailButtonWidget.fromIcon(
icon: widget.imagePaths.icClose,
backgroundColor: Colors.transparent,
@@ -394,6 +322,82 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
);
}
bool get _isWeb => PlatformInfo.isWeb || widget.isTestingForWeb;
List<Widget> _buildListPrefixWidgets() {
return [
const SizedBox(width: RecipientComposerWidgetStyle.space),
if (widget.fromState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_from_button'),
text: AppLocalizations.of(context).from_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from),
),
if (widget.toState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_to_button'),
text: AppLocalizations.of(context).to_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.to),
),
if (widget.ccState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_cc_button'),
text: AppLocalizations.of(context).cc_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc),
),
if (widget.bccState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'),
text: AppLocalizations.of(context).bcc_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc),
),
if (widget.replyToState == PrefixRecipientState.disabled)
TMailButtonWidget.fromText(
key: Key('prefix_${widget.prefix.name}_recipient_reply_to_button'),
text: AppLocalizations.of(context).reply_to_email_address_prefix,
textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle,
backgroundColor: Colors.transparent,
padding: RecipientComposerWidgetStyle.prefixButtonPadding,
margin: RecipientComposerWidgetStyle.recipientMargin,
onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.replyTo),
),
];
}
Widget _buildExpandButton() {
return TMailButtonWidget.fromIcon(
key: Key('prefix_${widget.prefix.name}_recipient_expand_button'),
icon: _isAllRecipientInputEnabled
? widget.imagePaths.icChevronUp
: widget.imagePaths.icChevronDownOutline,
backgroundColor: Colors.transparent,
iconSize: 24,
padding: const EdgeInsets.all(5),
iconColor: AppColor.colorLabelComposer,
margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin,
onTapActionCallback: () =>
widget.onEnableAllRecipientsInputAction?.call(
_isAllRecipientInputEnabled,
),
);
}
KeyEventResult _recipientInputOnKeyListener(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) {
widget.nextFocusNode?.requestFocus();
@@ -403,17 +407,11 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
return KeyEventResult.ignored;
}
bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE;
bool get _isAllRecipientInputEnabled => widget.fromState == PrefixRecipientState.enabled
&& widget.ccState == PrefixRecipientState.enabled
&& widget.bccState == PrefixRecipientState.enabled
&& widget.replyToState == PrefixRecipientState.enabled;
List<EmailAddress> get _collapsedListEmailAddress => _isCollapse
? _currentListEmailAddress.sublist(0, 1)
: _currentListEmailAddress;
FutureOr<List<SuggestionEmailAddress>> _findSuggestions(String query, {int? limit}) async {
if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) {
return [];
@@ -21,7 +21,6 @@ import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart
class RecipientTagItemWidget extends StatelessWidget {
final bool isCollapsed;
final bool isTagFocused;
final ImagePaths imagePaths;
final double? maxWidth;
@@ -29,8 +28,6 @@ class RecipientTagItemWidget extends StatelessWidget {
final PrefixEmailAddress prefix;
final EmailAddress currentEmailAddress;
final List<EmailAddress> currentListEmailAddress;
final List<EmailAddress> collapsedListEmailAddress;
final OnShowFullListEmailAddressAction? onShowFullAction;
final OnDeleteTagAction? onDeleteTagAction;
final OnEditRecipientAction? onEditRecipientAction;
final bool isTestingForWeb;
@@ -44,13 +41,10 @@ class RecipientTagItemWidget extends StatelessWidget {
required this.prefix,
required this.currentEmailAddress,
required this.currentListEmailAddress,
required this.collapsedListEmailAddress,
required this.imagePaths,
@visibleForTesting this.isTestingForWeb = false,
this.isCollapsed = false,
this.isTagFocused = false,
this.isMobile = false,
this.onShowFullAction,
this.onDeleteTagAction,
this.onEditRecipientAction,
this.maxWidth,
@@ -95,9 +89,7 @@ class RecipientTagItemWidget extends StatelessWidget {
),
onClearFocusAction: onClearFocusAction,
child: Container(
key: PlatformInfo.isWeb
? Key('recipient_tag_item_${prefix.name}_$index')
: null,
key: Key('recipient_tag_item_${prefix.name}_$index'),
constraints: const BoxConstraints(maxWidth: 267),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
@@ -108,6 +100,9 @@ class RecipientTagItemWidget extends StatelessWidget {
),
height: 32,
padding: const EdgeInsetsDirectional.only(start: 8, end: 4),
margin: PlatformInfo.isMobile
? const EdgeInsetsDirectional.only(top: 8)
: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -120,28 +115,20 @@ class RecipientTagItemWidget extends StatelessWidget {
),
const SizedBox(width: 4),
Flexible(
child: PlatformInfo.isWeb
? ExtendedText(
key: Key('label_recipient_tag_item_${prefix.name}_$index'),
currentEmailAddress.asString(),
maxLines: 1,
overflowWidget: TextOverflowWidget(
position: TextOverflowPosition.middle,
clearType: TextOverflowClearType.clipRect,
child: Text(
'...',
style: RecipientTagItemWidgetStyle.labelTextStyle,
),
),
style: RecipientTagItemWidgetStyle.labelTextStyle,
)
: Text(
key: Key('label_recipient_tag_item_${prefix.name}_$index'),
currentEmailAddress.asString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: ExtendedText(
key: Key('label_recipient_tag_item_${prefix.name}_$index'),
currentEmailAddress.asString(),
maxLines: 1,
overflowWidget: TextOverflowWidget(
position: TextOverflowPosition.middle,
clearType: TextOverflowClearType.clipRect,
child: Text(
'...',
style: RecipientTagItemWidgetStyle.labelTextStyle,
),
),
style: RecipientTagItemWidgetStyle.labelTextStyle,
),
),
const SizedBox(width: 4),
TMailButtonWidget.fromIcon(
@@ -186,47 +173,9 @@ class RecipientTagItemWidget extends StatelessWidget {
);
}
if (PlatformInfo.isWeb) {
return tagWidget;
} else {
return Container(
key: Key('recipient_tag_item_${prefix.name}_$index'),
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: tagWidget),
if (isCollapsed)
TMailButtonWidget.fromText(
key: Key('counter_recipient_tag_item_${prefix.name}_$index'),
margin: _counterMargin,
text: '+$countRecipients',
onTapActionCallback: () => onShowFullAction?.call(prefix),
borderRadius: RecipientTagItemWidgetStyle.radius,
textStyle: RecipientTagItemWidgetStyle.labelTextStyle,
padding: PlatformInfo.isWeb || isTestingForWeb
? RecipientTagItemWidgetStyle.counterPadding
: RecipientTagItemWidgetStyle.mobileCounterPadding,
backgroundColor: AppColor.colorEmailAddressTag,
),
],
),
);
}
return tagWidget;
}
EdgeInsetsGeometry? get _counterMargin {
if (PlatformInfo.isWeb || isTestingForWeb) {
return PlatformInfo.isCanvasKit
? RecipientTagItemWidgetStyle.webCounterMargin
: RecipientTagItemWidgetStyle.webMobileCounterMargin;
} else {
return RecipientTagItemWidgetStyle.counterMargin;
}
}
int get countRecipients => currentListEmailAddress.length - collapsedListEmailAddress.length;
Color _getTagBackgroundColor() {
if (isTagFocused) {
return AppColor.colorItemRecipientSelected;
@@ -8,8 +8,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:super_tag_editor/tag_editor.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart';
@@ -86,7 +86,7 @@ void main() {
expect(find.text('test2@example.com'), findsOneWidget);
});
testWidgets('RecipientTagItemWidget should have a `maxWidth` equal to the RecipientComposerWidget\'s `maxWidth`', (tester) async {
testWidgets('RecipientTagItemWidget should have a `maxWidth` equal to the default `maxWidth`', (tester) async {
final listEmailAddress = <EmailAddress>[
EmailAddress('test1', 'test1@example.com'),
];
@@ -110,7 +110,7 @@ void main() {
final Container recipientTagItemWidget = tester.widget(recipientTagItemWidgetFinder);
expect(recipientTagItemWidgetFinder, findsOneWidget);
expect(recipientTagItemWidget.constraints!.maxWidth, 360);
expect(recipientTagItemWidget.constraints!.maxWidth, 267);
});
testWidgets('WHEN EmailAddress has address is not empty AND display name is not empty\n'
@@ -273,7 +273,7 @@ void main() {
});
testWidgets('WHEN To has multiple recipients AND expandMode is COLLAPSE\n'
'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon, CounterTag)', (tester) async {
'RecipientTagItemWidget should have all the components (AvatarIcon, Label, DeleteIcon)', (tester) async {
final listEmailAddress = <EmailAddress>[
EmailAddress('test1', 'test1@example.com'),
EmailAddress('test2', 'test2@example.com'),
@@ -285,7 +285,6 @@ void main() {
listEmailAddress: listEmailAddress,
imagePaths: imagePaths,
maxWidth: 360,
expandMode: ExpandMode.COLLAPSE,
keyTagEditor: keyEmailTagEditor,
),
);
@@ -312,24 +311,20 @@ void main() {
final labelRecipientTagItemWidgetFinder = find.byKey(Key('label_recipient_tag_item_${prefix.name}_0'));
final deleteIconRecipientTagItemWidgetFinder = find.byKey(Key('delete_icon_recipient_tag_item_${prefix.name}_0'));
final avatarIconRecipientTagItemWidgetFinder = find.byKey(Key('avatar_icon_recipient_tag_item_${prefix.name}_0'));
final counterRecipientTagItemWidgetFinder = find.byKey(Key('counter_recipient_tag_item_${prefix.name}_0'));
final Size labelRecipientTagItemWidgetSize = tester.getSize(labelRecipientTagItemWidgetFinder);
final Size deleteIconRecipientTagItemWidgetSize = tester.getSize(deleteIconRecipientTagItemWidgetFinder);
final Size avatarIconRecipientTagItemWidgetSize = tester.getSize(avatarIconRecipientTagItemWidgetFinder);
final Size counterRecipientTagItemWidgetSize = tester.getSize(counterRecipientTagItemWidgetFinder);
log('recipient_composer_widget_test::main: LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize | CounterTagSize = $counterRecipientTagItemWidgetSize');
log('recipient_composer_widget_test::main: LabelTagSize = $labelRecipientTagItemWidgetSize | DeleteIconTagSize = $deleteIconRecipientTagItemWidgetSize | AvatarIconTagSize = $avatarIconRecipientTagItemWidgetSize');
expect(labelRecipientTagItemWidgetFinder, findsOneWidget);
expect(deleteIconRecipientTagItemWidgetFinder, findsOneWidget);
expect(avatarIconRecipientTagItemWidgetFinder, findsOneWidget);
expect(counterRecipientTagItemWidgetFinder, findsOneWidget);
final totalSizeOfAllComponents = labelRecipientTagItemWidgetSize.width +
deleteIconRecipientTagItemWidgetSize.width +
avatarIconRecipientTagItemWidgetSize.width;
counterRecipientTagItemWidgetSize.width;
expect(
totalSizeOfAllComponents,
@@ -350,7 +345,6 @@ void main() {
listEmailAddress: listEmailAddress,
imagePaths: imagePaths,
maxWidth: 360,
expandMode: ExpandMode.EXPAND,
keyTagEditor: keyEmailTagEditor,
),
);
@@ -391,8 +385,6 @@ void main() {
final labelRecipientTagItemWidget = tester.widget<Text>(labelRecipientTagItemWidgetFinder);
final labelTagWidth = tester.getSize(labelRecipientTagItemWidgetFinder).width;
expect(labelRecipientTagItemWidget.overflow, equals(TextOverflow.ellipsis));
final TextPainter textPainter = TextPainter(
maxLines: labelRecipientTagItemWidget.maxLines,
textDirection: labelRecipientTagItemWidget.textDirection ?? TextDirection.ltr,
@@ -477,10 +469,13 @@ void main() {
final widget = makeTestableWidget(
child: RecipientComposerWidget(
prefix: prefix,
prefixRootState: prefix,
listEmailAddress: listEmailAddress,
imagePaths: imagePaths,
maxWidth: 360,
keyTagEditor: keyEmailTagEditor,
isTestingForWeb: true,
toState: PrefixRecipientState.enabled,
),
);
@@ -518,7 +513,7 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, FromButton, CCButton, BccButton) on web platform', (tester) async {
testWidgets('ToRecipientComponentWidget should have all the components (PrefixLabel, RecipientTagItemWidget, FromButton, CCButton, BccButton, ReplyToButton) on web platform', (tester) async {
final listEmailAddress = <EmailAddress>[
EmailAddress('test1', 'test1@example.com'),
];
@@ -526,11 +521,13 @@ void main() {
final widget = makeTestableWidget(
child: RecipientComposerWidget(
prefix: prefix,
prefixRootState: prefix,
listEmailAddress: listEmailAddress,
imagePaths: imagePaths,
maxWidth: 360,
keyTagEditor: keyEmailTagEditor,
isTestingForWeb: true,
toState: PrefixRecipientState.enabled,
),
);