diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index c693b1f01..ab8d9d84f 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart'; import 'package:enough_html_editor/enough_html_editor.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_chips_input/flutter_chips_input.dart'; import 'package:get/get.dart'; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; @@ -61,6 +62,9 @@ class ComposerController extends BaseController { String? _subjectEmail; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.localContact; HtmlEditorApi? htmlEditorApi; + final keyToEmailAddress = GlobalKey(); + final keyCcEmailAddress = GlobalKey(); + final keyBccEmailAddress = GlobalKey(); void setSubjectEmail(String subject) => _subjectEmail = subject; @@ -177,6 +181,18 @@ class ComposerController extends BaseController { } else { expandMode.value = ExpandMode.COLLAPSE; } + + if (listToEmailAddress.isNotEmpty) { + keyToEmailAddress.currentState?.addMultipleValue(listToEmailAddress); + } + + if (listCcEmailAddress.isNotEmpty) { + keyCcEmailAddress.currentState?.addMultipleValue(listCcEmailAddress); + } + + if (listBccEmailAddress.isNotEmpty) { + keyBccEmailAddress.currentState?.addMultipleValue(listBccEmailAddress); + } } _updateStatusEmailSendButton(); } diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index b572609b2..f7c335062 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -8,7 +8,7 @@ import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_composer_widget_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/top_bar_composer_widget_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -40,11 +40,7 @@ class ComposerView extends GetWidget { mainAxisSize: MainAxisSize.max, children: [ _buildTopBar(context), - Expanded(child: Container( - color: AppColor.bgComposer, - margin: EdgeInsets.zero, - alignment: Alignment.topCenter, - child: _buildBodyComposer(context))) + Expanded(child: _buildBodyComposer(context)) ]) ) ), @@ -108,18 +104,76 @@ class ComposerView extends GetWidget { } Widget _buildEmailAddress(BuildContext context) { - return Obx(() => (EmailAddressComposerWidgetBuilder( - context, - imagePaths, - controller.expandMode.value, - controller.listToEmailAddress, - controller.listCcEmailAddress, - controller.listBccEmailAddress, - controller.composerArguments.value?.userProfile) - ..addExpandAddressActionClick(() => controller.expandEmailAddressAction()) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) - .build() + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: 8), + child: Text( + '${AppLocalizations.of(context).from_email_address_prefix}:', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(fontSize: 14, color: AppColor.nameUserColor, fontWeight: FontWeight.w500))), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() => controller.composerArguments.value != null + ? Text( + '<${controller.composerArguments.value?.userProfile.email ?? ''}>', + style: TextStyle(fontSize: 14, color: AppColor.nameUserColor)) + : SizedBox.shrink() + ) + ], + ) + ) + ] + ) + ), + Obx(() => (EmailAddressInputBuilder( + context, + imagePaths, + PrefixEmailAddress.to, + controller.listToEmailAddress, + keyInput: controller.keyToEmailAddress, + expandMode: controller.expandMode.value) + ..addExpandAddressActionClick(() => controller.expandEmailAddressAction()) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + ), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? (EmailAddressInputBuilder( + context, + imagePaths, + PrefixEmailAddress.cc, + controller.listCcEmailAddress, + keyInput: controller.keyCcEmailAddress) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + : SizedBox.shrink() + ), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? (EmailAddressInputBuilder( + context, + imagePaths, + PrefixEmailAddress.bcc, + controller.listBccEmailAddress, + keyInput: controller.keyBccEmailAddress) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + : SizedBox.shrink() + ), + ], ); } @@ -158,7 +212,7 @@ class ComposerView extends GetWidget { child: Column( children: [ Padding( - padding: EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 30), + padding: EdgeInsets.only(top: 8, left: 16, right: 16, bottom: 24), child: _buildComposerEditor(context)), _buildAttachmentsLoadingView(), _buildAttachments(context), @@ -175,7 +229,7 @@ class ComposerView extends GetWidget { if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) { return HtmlEditor( key: Key('composer_editor'), - minHeight: 400, + minHeight: 100, supportZoom: true, disableHorizontalScroll: false, disableVerticalScroll: false, @@ -186,7 +240,7 @@ class ComposerView extends GetWidget { return message.isNotEmpty ? HtmlEditor( key: Key('composer_editor'), - minHeight: 400, + minHeight: 100, supportZoom: true, disableHorizontalScroll: false, disableVerticalScroll: false, @@ -203,7 +257,7 @@ class ComposerView extends GetWidget { (failure) => SizedBox.shrink(), (success) => success is UploadingAttachmentState ? Center(child: Padding( - padding: EdgeInsets.only(top: 8), + padding: EdgeInsets.only(bottom: controller.attachments.isNotEmpty ? 0 : 24), child: SizedBox( width: 20, height: 20, diff --git a/lib/features/composer/presentation/widgets/email_address_composer_widget_builder.dart b/lib/features/composer/presentation/widgets/email_address_composer_widget_builder.dart deleted file mode 100644 index d3774c4d5..000000000 --- a/lib/features/composer/presentation/widgets/email_address_composer_widget_builder.dart +++ /dev/null @@ -1,238 +0,0 @@ - -import 'dart:async'; - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_chips_input/flutter_chips_input.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/model.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnOpenExpandAddressActionClick = void Function(); -typedef OnSuggestionEmailAddress = Future> Function(String word); -typedef OnUpdateListEmailAddressAction = void Function( - PrefixEmailAddress prefixEmailAddress, - List listEmailAddress -); - -class EmailAddressComposerWidgetBuilder { - - final ImagePaths _imagePaths; - final BuildContext _context; - final ExpandMode _expandMode; - final List _listToEmailAddress; - final List _listCcEmailAddress; - final List _listBccEmailAddress; - final UserProfile? _userProfile; - - final keyToEmailAddress = new GlobalKey(); - - OnOpenExpandAddressActionClick? _onOpenExpandAddressActionClick; - OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction; - OnSuggestionEmailAddress? _onSuggestionEmailAddress; - - EmailAddressComposerWidgetBuilder( - this._context, - this._imagePaths, - this._expandMode, - this._listToEmailAddress, - this._listCcEmailAddress, - this._listBccEmailAddress, - this._userProfile, - ); - - void addExpandAddressActionClick(OnOpenExpandAddressActionClick onOpenExpandAddressActionClick) { - _onOpenExpandAddressActionClick = onOpenExpandAddressActionClick; - } - - void addOnUpdateListEmailAddressAction(OnUpdateListEmailAddressAction onUpdateListEmailAddressAction) { - _onUpdateListEmailAddressAction = onUpdateListEmailAddressAction; - } - - void addOnSuggestionEmailAddress(OnSuggestionEmailAddress onSuggestionEmailAddress) { - _onSuggestionEmailAddress = onSuggestionEmailAddress; - } - - Widget build() { - keyToEmailAddress.currentState?.selectSuggestion(_listToEmailAddress); - - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent), - child: MediaQuery( - data: MediaQueryData(padding: EdgeInsets.zero), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: 8), - child: Text( - '${AppLocalizations.of(_context).from_email_address_prefix}:', - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle(fontSize: 14, color: AppColor.nameUserColor, fontWeight: FontWeight.w500))), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '<${_userProfile?.email}>', - style: TextStyle(fontSize: 14, color: AppColor.nameUserColor) - ) - ], - ) - ) - ])), - _buildEmailAddressWidget(PrefixEmailAddress.to, hasExpandButton: true), - if (_expandMode == ExpandMode.EXPAND) _buildEmailAddressWidget(PrefixEmailAddress.cc), - if (_expandMode == ExpandMode.EXPAND) _buildEmailAddressWidget(PrefixEmailAddress.bcc), - ], - ) - ) - ); - } - - Widget _buildEmailAddressWidget(PrefixEmailAddress prefixEmailAddress, {bool hasExpandButton = false}) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: 8, top: _paddingTo(prefixEmailAddress)), - child: Text( - '${prefixEmailAddress.asName(_context)}:', - style: TextStyle(fontSize: 14, color: AppColor.nameUserColor, fontWeight: FontWeight.w500))), - Expanded(child: ChipsInput( - key: prefixEmailAddress == PrefixEmailAddress.to - ? keyToEmailAddress - : Key('${prefixEmailAddress.toString()}_email_address_input'), - initialValue: _getListEmailAddressCurrent(prefixEmailAddress), - autofocus: prefixEmailAddress == PrefixEmailAddress.to ? true : false, - textStyle: TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500), - cursorColor: AppColor.primaryColor, - inputType: TextInputType.emailAddress, - decoration: InputDecoration( - border: InputBorder.none, - hintText: AppLocalizations.of(_context).hint_text_email_address, - hintMaxLines: 1, - hintStyle: TextStyle(color: AppColor.baseTextColor, fontSize: 14, fontWeight: FontWeight.w500)), - findSuggestions: (query) { - if (query.isNotEmpty && _onSuggestionEmailAddress != null) { - return _onSuggestionEmailAddress!(query); - } - return []; - }, - onChanged: (data) { - if (_onUpdateListEmailAddressAction != null) { - _onUpdateListEmailAddressAction!(prefixEmailAddress, data); - } - }, - onChipInputActionDone: (state, value) { - if (GetUtils.isEmail(value)) { - state.selectSuggestion(EmailAddress(value, value)); - } - }, - onChipInputChangeFocusAction: (state, value) { - if (GetUtils.isEmail(value)) { - state.selectSuggestion(EmailAddress(value, value)); - } - }, - chipBuilder: (context, state, emailAddress) { - return InputChip( - key: ObjectKey(emailAddress), - label: Text(emailAddress.asString()), - labelStyle: TextStyle(color: AppColor.nameUserColor, fontSize: 12, fontWeight: FontWeight.w500), - backgroundColor: AppColor.emailAddressChipColor, - padding: EdgeInsets.all(3), - deleteIconColor: AppColor.baseTextColor, - avatar: CircleAvatar( - backgroundColor: AppColor.avatarColor, - child: Text( - emailAddress.asString().isNotEmpty ? emailAddress.asString()[0].toUpperCase() : '', - style: TextStyle(color: AppColor.appColor, fontSize: 12, fontWeight: FontWeight.w500))), - onDeleted: () => state.deleteChip(emailAddress), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); - }, - suggestionBuilder: (context, state, emailAddress) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.white), - color: Colors.white), - child: ListTile( - key: ObjectKey(emailAddress), - leading: CircleAvatar( - backgroundColor: AppColor.avatarColor, - child: Text( - emailAddress.asString().isNotEmpty ? emailAddress.asString()[0].toUpperCase() : '', - style: TextStyle(color: AppColor.appColor, fontSize: 16, fontWeight: FontWeight.w500))), - title: Text( - emailAddress.asString(), - style: TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500)), - subtitle: emailAddress.displayName.isNotEmpty && emailAddress.emailAddress.isNotEmpty - ? Text( - emailAddress.emailAddress, - style: TextStyle(color: AppColor.baseTextColor, fontSize: 12, fontWeight: FontWeight.w500)) - : null, - onTap: () => state.selectSuggestion(emailAddress), - ), - ); - })), - if (hasExpandButton) _buildButtonExpandAddress() - ] - ); - } - - Widget _buildButtonExpandAddress() { - return GestureDetector( - onTap: () { - if (_onOpenExpandAddressActionClick != null) { - _onOpenExpandAddressActionClick!(); - }}, - child: Padding( - padding: EdgeInsets.only(top: 20), - child: SvgPicture.asset( - _expandMode == ExpandMode.EXPAND ? _imagePaths.icExpandAddress : _imagePaths.icMoreReceiver, - width: 20, - height: 20, - fit: BoxFit.fill), - ), - ); - } - - List _getListEmailAddressCurrent(PrefixEmailAddress prefixEmailAddress) { - switch(prefixEmailAddress) { - case PrefixEmailAddress.to: - return _listToEmailAddress; - case PrefixEmailAddress.cc: - return _listCcEmailAddress; - case PrefixEmailAddress.bcc: - return _listBccEmailAddress; - } - } - - double _paddingTo(PrefixEmailAddress prefixEmailAddress) { - switch(prefixEmailAddress) { - case PrefixEmailAddress.to: - return _listToEmailAddress.isNotEmpty ? 20 : 18; - case PrefixEmailAddress.cc: - return _listCcEmailAddress.isNotEmpty ? 20 : 18; - case PrefixEmailAddress.bcc: - return _listBccEmailAddress.isNotEmpty ? 20 : 18; - } - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/email_address_input_builder.dart b/lib/features/composer/presentation/widgets/email_address_input_builder.dart new file mode 100644 index 000000000..abd789686 --- /dev/null +++ b/lib/features/composer/presentation/widgets/email_address_input_builder.dart @@ -0,0 +1,163 @@ + +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_chips_input/flutter_chips_input.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/model.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnOpenExpandAddressActionClick = void Function(); +typedef OnSuggestionEmailAddress = Future> Function(String word); +typedef OnUpdateListEmailAddressAction = void Function( + PrefixEmailAddress prefixEmailAddress, + List listEmailAddress +); + +class EmailAddressInputBuilder { + + final Key? keyInput; + final BuildContext _context; + final ImagePaths _imagePaths; + final ExpandMode expandMode; + final PrefixEmailAddress _prefixEmailAddress; + final List _listEmailAddress; + + OnOpenExpandAddressActionClick? _onOpenExpandAddressActionClick; + OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction; + OnSuggestionEmailAddress? _onSuggestionEmailAddress; + + void addExpandAddressActionClick(OnOpenExpandAddressActionClick onOpenExpandAddressActionClick) { + _onOpenExpandAddressActionClick = onOpenExpandAddressActionClick; + } + + void addOnUpdateListEmailAddressAction(OnUpdateListEmailAddressAction onUpdateListEmailAddressAction) { + _onUpdateListEmailAddressAction = onUpdateListEmailAddressAction; + } + + void addOnSuggestionEmailAddress(OnSuggestionEmailAddress onSuggestionEmailAddress) { + _onSuggestionEmailAddress = onSuggestionEmailAddress; + } + + EmailAddressInputBuilder( + this._context, + this._imagePaths, + this._prefixEmailAddress, + this._listEmailAddress, + { + this.keyInput, + this.expandMode = ExpandMode.COLLAPSE, + } + ); + + Widget build() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: 8, top: _listEmailAddress.isNotEmpty ? 20 : 18), + child: Text( + '${_prefixEmailAddress.asName(_context)}:', + style: TextStyle(fontSize: 14, color: AppColor.nameUserColor, fontWeight: FontWeight.w500))), + Expanded(child: ChipsInput( + key: keyInput, + initialValue: _listEmailAddress, + textStyle: TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500), + cursorColor: AppColor.primaryColor, + inputType: TextInputType.emailAddress, + decoration: InputDecoration( + border: InputBorder.none, + hintText: AppLocalizations.of(_context).hint_text_email_address, + hintMaxLines: 1, + hintStyle: TextStyle(color: AppColor.baseTextColor, fontSize: 14, fontWeight: FontWeight.w500)), + findSuggestions: (query) { + if (query.isNotEmpty && _onSuggestionEmailAddress != null) { + return _onSuggestionEmailAddress!(query); + } + return []; + }, + onChanged: (data) { + if (_onUpdateListEmailAddressAction != null) { + _onUpdateListEmailAddressAction!(_prefixEmailAddress, data); + } + }, + onChipInputActionDone: (state, value) { + if (GetUtils.isEmail(value)) { + state.selectSuggestion(EmailAddress(value, value)); + } + }, + onChipInputChangeFocusAction: (state, value) { + if (GetUtils.isEmail(value)) { + state.selectSuggestion(EmailAddress(value, value)); + } + }, + chipBuilder: (context, state, emailAddress) { + return InputChip( + key: ObjectKey(emailAddress), + label: Text(emailAddress.asString()), + labelStyle: TextStyle(color: AppColor.nameUserColor, fontSize: 12, fontWeight: FontWeight.w500), + backgroundColor: AppColor.emailAddressChipColor, + padding: EdgeInsets.all(3), + deleteIconColor: AppColor.baseTextColor, + avatar: CircleAvatar( + backgroundColor: AppColor.avatarColor, + child: Text( + emailAddress.asString().isNotEmpty ? emailAddress.asString()[0].toUpperCase() : '', + style: TextStyle(color: AppColor.appColor, fontSize: 12, fontWeight: FontWeight.w500))), + onDeleted: () => state.deleteChip(emailAddress), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }, + suggestionBuilder: (context, state, emailAddress) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white), + color: Colors.white), + child: ListTile( + key: ObjectKey(emailAddress), + leading: CircleAvatar( + backgroundColor: AppColor.avatarColor, + child: Text( + emailAddress.asString().isNotEmpty ? emailAddress.asString()[0].toUpperCase() : '', + style: TextStyle(color: AppColor.appColor, fontSize: 16, fontWeight: FontWeight.w500))), + title: Text( + emailAddress.asString(), + style: TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500)), + subtitle: emailAddress.displayName.isNotEmpty && emailAddress.emailAddress.isNotEmpty + ? Text( + emailAddress.emailAddress, + style: TextStyle(color: AppColor.baseTextColor, fontSize: 12, fontWeight: FontWeight.w500)) + : null, + onTap: () => state.selectSuggestion(emailAddress), + ), + ); + } + )), + if (_prefixEmailAddress == PrefixEmailAddress.to) + Padding( + padding: EdgeInsets.only(top: _listEmailAddress.isNotEmpty ? 6 : 3), + child: _buildButtonExpandAddress()) + ] + ); + } + + Widget _buildButtonExpandAddress() { + return IconButton( + color: AppColor.baseTextColor, + icon: SvgPicture.asset( + expandMode == ExpandMode.EXPAND ? _imagePaths.icExpandAddress : _imagePaths.icMoreReceiver, + width: 20, + height: 20, + fit: BoxFit.fill), + onPressed: () { + if (_onOpenExpandAddressActionClick != null) { + _onOpenExpandAddressActionClick!(); + } + } + ); + } +} \ No newline at end of file