TF-2116 Create RecipientComposer widget and style
(cherry picked from commit 24be032c8c04e9673953c9dc83d096e7b61fdb04)
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipientComposerWidgetStyle {
|
||||||
|
static const double deleteRecipientFieldIconSize = 20;
|
||||||
|
static const double space = 8;
|
||||||
|
|
||||||
|
static const Color borderColor = AppColor.colorLineComposer;
|
||||||
|
static const Color deleteRecipientFieldIconColor = AppColor.colorCollapseMailbox;
|
||||||
|
|
||||||
|
static const EdgeInsetsGeometry deleteRecipientFieldIconPadding = EdgeInsetsDirectional.all(3);
|
||||||
|
static const EdgeInsetsGeometry prefixButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 3, horizontal: 5);
|
||||||
|
static const EdgeInsetsGeometry labelMargin = EdgeInsetsDirectional.only(top: 16);
|
||||||
|
static const EdgeInsetsGeometry recipientMargin = EdgeInsetsDirectional.only(top: 12);
|
||||||
|
|
||||||
|
static const TextStyle prefixButtonTextStyle = TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
color: AppColor.colorPrefixButtonComposer
|
||||||
|
);
|
||||||
|
static const TextStyle labelTextStyle = TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColor.colorLabelComposer
|
||||||
|
);
|
||||||
|
static const TextStyle inputTextStyle = TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipientSuggestionItemWidgetStyle {
|
||||||
|
static const double radius = 20;
|
||||||
|
static const double selectedIconSize = 24;
|
||||||
|
|
||||||
|
static const EdgeInsetsGeometry suggestionDuplicatedMargin = EdgeInsets.all(8.0);
|
||||||
|
static const EdgeInsetsGeometry labelDuplicatedPadding = EdgeInsets.symmetric(horizontal: 8.0);
|
||||||
|
static const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric(horizontal: 16.0);
|
||||||
|
|
||||||
|
static const TextStyle labelTextStyle = TextStyle(
|
||||||
|
color: AppColor.colorHintSearchBar,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.normal
|
||||||
|
);
|
||||||
|
static const TextStyle labelHighlightTextStyle = TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:core/utils/platform_info.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RecipientTagItemWidgetStyle {
|
||||||
|
static const double radius = 10;
|
||||||
|
|
||||||
|
static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(start: 4);
|
||||||
|
static const EdgeInsetsGeometry counterPadding = EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8);
|
||||||
|
static const EdgeInsetsGeometry counterMargin = EdgeInsetsDirectional.only(top: PlatformInfo.isWeb ? 8 : 0);
|
||||||
|
|
||||||
|
static const TextStyle labelTextStyle = TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w400
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,526 +0,0 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:core/core.dart';
|
|
||||||
import 'package:core/utils/direction_utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.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:super_tag_editor/tag_editor.dart';
|
|
||||||
import 'package:super_tag_editor/widgets/rich_text_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
|
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
|
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart';
|
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart';
|
|
||||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
|
||||||
import 'package:tmail_ui_user/main/utils/app_constants.dart';
|
|
||||||
|
|
||||||
typedef OnSuggestionEmailAddress = Future<List<EmailAddress>> Function(String word);
|
|
||||||
typedef OnUpdateListEmailAddressAction = void Function(PrefixEmailAddress, List<EmailAddress>);
|
|
||||||
typedef OnAddEmailAddressTypeAction = void Function(PrefixEmailAddress);
|
|
||||||
typedef OnDeleteEmailAddressTypeAction = void Function(PrefixEmailAddress);
|
|
||||||
typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress);
|
|
||||||
typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress, bool);
|
|
||||||
typedef OnFocusNextAddressAction = void Function();
|
|
||||||
|
|
||||||
class EmailAddressInputBuilder {
|
|
||||||
|
|
||||||
static const _suggestionBoxRadius = 20.0;
|
|
||||||
final BuildContext _context;
|
|
||||||
final ImagePaths _imagePaths;
|
|
||||||
final ResponsiveUtils _responsiveUtils;
|
|
||||||
final ExpandMode expandMode;
|
|
||||||
final PrefixEmailAddress _prefixEmailAddress;
|
|
||||||
final List<PrefixEmailAddress> _listEmailAddressType;
|
|
||||||
final TextEditingController? controller;
|
|
||||||
final bool? isInitial;
|
|
||||||
final FocusNode? focusNode;
|
|
||||||
final bool autoDisposeFocusNode;
|
|
||||||
final GlobalKey? keyTagEditor;
|
|
||||||
final FocusNode? nextFocusNode;
|
|
||||||
|
|
||||||
List<EmailAddress> listEmailAddress = <EmailAddress>[];
|
|
||||||
|
|
||||||
OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction;
|
|
||||||
OnSuggestionEmailAddress? _onSuggestionEmailAddress;
|
|
||||||
OnAddEmailAddressTypeAction? _onAddEmailAddressTypeAction;
|
|
||||||
OnDeleteEmailAddressTypeAction? _onDeleteEmailAddressTypeAction;
|
|
||||||
OnShowFullListEmailAddressAction? _onShowFullListEmailAddressAction;
|
|
||||||
OnFocusEmailAddressChangeAction? _onFocusEmailAddressChangeAction;
|
|
||||||
OnFocusNextAddressAction? _onFocusNextAddressAction;
|
|
||||||
|
|
||||||
Timer? _gapBetweenTagChangedAndFindSuggestion;
|
|
||||||
bool lastTagFocused = false;
|
|
||||||
|
|
||||||
void addOnUpdateListEmailAddressAction(OnUpdateListEmailAddressAction onUpdateListEmailAddressAction) {
|
|
||||||
_onUpdateListEmailAddressAction = onUpdateListEmailAddressAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnSuggestionEmailAddress(OnSuggestionEmailAddress onSuggestionEmailAddress) {
|
|
||||||
_onSuggestionEmailAddress = onSuggestionEmailAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnAddEmailAddressTypeAction(OnAddEmailAddressTypeAction onAddEmailAddressTypeAction) {
|
|
||||||
_onAddEmailAddressTypeAction = onAddEmailAddressTypeAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnDeleteEmailAddressTypeAction(OnDeleteEmailAddressTypeAction onDeleteEmailAddressTypeAction) {
|
|
||||||
_onDeleteEmailAddressTypeAction = onDeleteEmailAddressTypeAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnShowFullListEmailAddressAction(OnShowFullListEmailAddressAction onShowFullListEmailAddressAction) {
|
|
||||||
_onShowFullListEmailAddressAction = onShowFullListEmailAddressAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnFocusEmailAddressChangeAction(OnFocusEmailAddressChangeAction onFocusEmailAddressChangeAction) {
|
|
||||||
_onFocusEmailAddressChangeAction = onFocusEmailAddressChangeAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOnFocusNextAddressAction(OnFocusNextAddressAction? onFocusNextAddressAction) {
|
|
||||||
_onFocusNextAddressAction = onFocusNextAddressAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
EmailAddressInputBuilder(
|
|
||||||
this._context,
|
|
||||||
this._imagePaths,
|
|
||||||
this._responsiveUtils,
|
|
||||||
this._prefixEmailAddress,
|
|
||||||
this.listEmailAddress,
|
|
||||||
this._listEmailAddressType,
|
|
||||||
{
|
|
||||||
this.isInitial,
|
|
||||||
this.controller,
|
|
||||||
this.focusNode,
|
|
||||||
this.autoDisposeFocusNode = true,
|
|
||||||
this.expandMode = ExpandMode.EXPAND,
|
|
||||||
this.keyTagEditor,
|
|
||||||
this.nextFocusNode,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget build() {
|
|
||||||
return Padding(
|
|
||||||
padding: _prefixEmailAddress == PrefixEmailAddress.to
|
|
||||||
? ComposerStyle.getToAddressPadding(_context, _responsiveUtils)
|
|
||||||
: ComposerStyle.getCcBccAddressPadding(_context, _responsiveUtils),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'${_prefixEmailAddress.asName(_context)}:',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
color: AppColor.colorHintEmailAddressInput
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SizedBox(width: ComposerStyle.getSpace(_context, _responsiveUtils)),
|
|
||||||
Expanded(child: _buildTagEditor()),
|
|
||||||
SizedBox(width: ComposerStyle.getSpace(_context, _responsiveUtils)),
|
|
||||||
if (_prefixEmailAddress == PrefixEmailAddress.to)
|
|
||||||
Row(children: [
|
|
||||||
if (!_listEmailAddressType.contains(PrefixEmailAddress.cc))
|
|
||||||
buildTextIcon(AppLocalizations.of(_context).cc_email_address_prefix,
|
|
||||||
padding: const EdgeInsets.all(5),
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
color: AppColor.lineItemListColor),
|
|
||||||
onTap: () => _onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc)),
|
|
||||||
if (!_listEmailAddressType.contains(PrefixEmailAddress.bcc))
|
|
||||||
buildTextIcon(AppLocalizations.of(_context).bcc_email_address_prefix,
|
|
||||||
padding: const EdgeInsets.all(5),
|
|
||||||
textStyle: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
color: AppColor.lineItemListColor),
|
|
||||||
onTap: () => _onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc)),
|
|
||||||
])
|
|
||||||
else
|
|
||||||
buildIconWeb(
|
|
||||||
icon: SvgPicture.asset(
|
|
||||||
_imagePaths.icCloseComposer,
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
fit: BoxFit.fill
|
|
||||||
),
|
|
||||||
iconPadding: EdgeInsets.zero,
|
|
||||||
onTap: () => _onDeleteEmailAddressTypeAction?.call(_prefixEmailAddress))
|
|
||||||
]
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTagEditor() {
|
|
||||||
return StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
|
||||||
final newListEmailAddress = _isCollapse ? listEmailAddress.sublist(0, 1) : listEmailAddress;
|
|
||||||
return FocusScope(child: Focus(
|
|
||||||
onFocusChange: (focus) => _onFocusEmailAddressChangeAction?.call(_prefixEmailAddress, focus),
|
|
||||||
onKey: (focusNode, event) {
|
|
||||||
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) {
|
|
||||||
nextFocusNode?.requestFocus();
|
|
||||||
_onFocusNextAddressAction?.call();
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
},
|
|
||||||
child: TagEditor<SuggestionEmailAddress>(
|
|
||||||
key: keyTagEditor,
|
|
||||||
length: newListEmailAddress.length,
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
autoDisposeFocusNode: autoDisposeFocusNode,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
debounceDuration: const Duration(milliseconds: 150),
|
|
||||||
hasAddButton: false,
|
|
||||||
tagSpacing: 8,
|
|
||||||
autofocus: _prefixEmailAddress != PrefixEmailAddress.to && listEmailAddress.isEmpty,
|
|
||||||
minTextFieldWidth: 20,
|
|
||||||
resetTextOnSubmitted: true,
|
|
||||||
suggestionsBoxElevation: _suggestionBoxRadius,
|
|
||||||
suggestionsBoxBackgroundColor: Colors.white,
|
|
||||||
suggestionsBoxRadius: 20,
|
|
||||||
suggestionsBoxMaxHeight: 350,
|
|
||||||
textStyle: const TextStyle(color: AppColor.colorEmailAddress, fontSize: 14, fontWeight: FontWeight.w500),
|
|
||||||
onFocusTagAction: (focused) {
|
|
||||||
setState(() {
|
|
||||||
lastTagFocused = focused;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDeleteTagAction: () => _handleDeleteTagAction(setState, context),
|
|
||||||
onSelectOptionAction: (item) {
|
|
||||||
if (!_isDuplicatedRecipient(item.emailAddress.emailAddress)) {
|
|
||||||
setState(() => listEmailAddress.add(item.emailAddress));
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSubmitted: (value) {
|
|
||||||
log('EmailAddressInputBuilder::_buildTagEditor():onSubmitted: value: $value');
|
|
||||||
final textTrim = value.trim();
|
|
||||||
log('EmailAddressInputBuilder::_buildTagEditor():onSubmitted: textTrim: $textTrim');
|
|
||||||
if (!_isDuplicatedRecipient(textTrim)) {
|
|
||||||
setState(() => listEmailAddress.add(EmailAddress(null, textTrim)));
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
inputDecoration: const InputDecoration(border: InputBorder.none),
|
|
||||||
tagBuilder: (context, index) {
|
|
||||||
final isLastEmail = index == listEmailAddress.length - 1;
|
|
||||||
return Stack(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsetsDirectional.only(
|
|
||||||
top: PlatformInfo.isWeb ? 8 : 0,
|
|
||||||
end: _isCollapse ? 50 : 0),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _isCollapse
|
|
||||||
? _onShowFullListEmailAddressAction?.call(_prefixEmailAddress)
|
|
||||||
: null,
|
|
||||||
child: Chip(
|
|
||||||
padding: DirectionUtils.isDirectionRTLByLanguage(context)
|
|
||||||
? EdgeInsets.zero
|
|
||||||
: null,
|
|
||||||
labelPadding: EdgeInsetsDirectional.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(newListEmailAddress[index].asString()) ? 0 : 2
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
newListEmailAddress[index].asString(),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: CommonTextStyle.defaultTextOverFlow,
|
|
||||||
softWrap: CommonTextStyle.defaultSoftWrap,
|
|
||||||
),
|
|
||||||
deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill),
|
|
||||||
labelStyle: const TextStyle(color: Colors.black, fontSize: 17, fontWeight: FontWeight.normal),
|
|
||||||
backgroundColor: _getTagBackgroundColor(listEmailAddress[index], isLastEmail),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
side: _getTagBorderSide(listEmailAddress[index], isLastEmail),
|
|
||||||
),
|
|
||||||
avatar: newListEmailAddress[index].displayName.isNotEmpty
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: AppColor.colorTextButton,
|
|
||||||
child: Text(
|
|
||||||
listEmailAddress[index].displayName[0].toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500
|
|
||||||
)
|
|
||||||
))
|
|
||||||
: null,
|
|
||||||
onDeleted: () {
|
|
||||||
setState(() => listEmailAddress.removeAt(index));
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
if (_isCollapse)
|
|
||||||
_buildCounter(
|
|
||||||
context,
|
|
||||||
listEmailAddress.length - newListEmailAddress.length
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onTagChanged: (value) {
|
|
||||||
log('EmailAddressInputBuilder::_buildTagEditor():onTagChanged: value: $value');
|
|
||||||
final textTrim = value.trim();
|
|
||||||
log('EmailAddressInputBuilder::_buildTagEditor():onTagChanged: textTrim: $textTrim');
|
|
||||||
if (!_isDuplicatedRecipient(textTrim)) {
|
|
||||||
setState(() => listEmailAddress.add(EmailAddress(null, textTrim)));
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
}
|
|
||||||
_gapBetweenTagChangedAndFindSuggestion = Timer(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
_handleGapBetweenTagChangedAndFindSuggestion);
|
|
||||||
},
|
|
||||||
findSuggestions: _findSuggestions,
|
|
||||||
useDefaultHighlight: false,
|
|
||||||
suggestionBuilder: (
|
|
||||||
context,
|
|
||||||
tagEditorState,
|
|
||||||
suggestionEmailAddress,
|
|
||||||
index,
|
|
||||||
length,
|
|
||||||
highlight,
|
|
||||||
suggestionValid
|
|
||||||
) {
|
|
||||||
switch (suggestionEmailAddress.state) {
|
|
||||||
case SuggestionEmailState.duplicated:
|
|
||||||
return _buildExistedSuggestionItem(
|
|
||||||
setState,
|
|
||||||
context,
|
|
||||||
suggestionEmailAddress.emailAddress,
|
|
||||||
suggestionValid
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return _buildSuggestionItem(
|
|
||||||
setState,
|
|
||||||
context,
|
|
||||||
tagEditorState,
|
|
||||||
suggestionEmailAddress.emailAddress,
|
|
||||||
index,
|
|
||||||
length,
|
|
||||||
highlight,
|
|
||||||
suggestionValid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCounter(BuildContext context, int count) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(start: 8, top: PlatformInfo.isWeb ? 8 : 0),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _onShowFullListEmailAddressAction?.call(_prefixEmailAddress),
|
|
||||||
child: Chip(
|
|
||||||
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
label: Text(
|
|
||||||
'+$count',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: CommonTextStyle.defaultTextOverFlow,
|
|
||||||
softWrap: CommonTextStyle.defaultSoftWrap),
|
|
||||||
labelStyle: const TextStyle(color: Colors.black, fontSize: 17, fontWeight: FontWeight.normal),
|
|
||||||
backgroundColor: AppColor.colorEmailAddressTag,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
side: const BorderSide(width: 0, color: AppColor.colorEmailAddressTag),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isCollapse {
|
|
||||||
return listEmailAddress.length > 1 && expandMode == ExpandMode.COLLAPSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildExistedSuggestionItem(
|
|
||||||
StateSetter setState,
|
|
||||||
BuildContext context,
|
|
||||||
EmailAddress emailAddress,
|
|
||||||
String? suggestionValid
|
|
||||||
) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(_suggestionBoxRadius))),
|
|
||||||
child: Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: AppColor.colorBgMenuItemDropDownSelected,
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(_suggestionBoxRadius))),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
leading: AvatarSuggestionItemWidget(emailAddress: emailAddress),
|
|
||||||
title: _buildTitleSuggestionItem(emailAddress, suggestionValid),
|
|
||||||
subtitle: _buildSubtitleSuggestionItem(emailAddress, suggestionValid),
|
|
||||||
trailing: SvgPicture.asset(_imagePaths.icFilterSelected,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
fit: BoxFit.fill),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSuggestionItem(
|
|
||||||
StateSetter setState,
|
|
||||||
BuildContext context,
|
|
||||||
TagsEditorState<SuggestionEmailAddress> tagEditorState,
|
|
||||||
EmailAddress emailAddress,
|
|
||||||
int index,
|
|
||||||
int length,
|
|
||||||
bool highlight,
|
|
||||||
String? suggestionValid
|
|
||||||
) {
|
|
||||||
return Container(
|
|
||||||
color: highlight ? AppColor.colorItemSelected : Colors.white,
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
leading: AvatarSuggestionItemWidget(emailAddress: emailAddress),
|
|
||||||
title: _buildTitleSuggestionItem(emailAddress, suggestionValid),
|
|
||||||
subtitle: _buildSubtitleSuggestionItem(emailAddress, suggestionValid),
|
|
||||||
onTap: () {
|
|
||||||
setState(() => listEmailAddress.add(emailAddress));
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
tagEditorState.resetTextField();
|
|
||||||
tagEditorState.closeSuggestionBox();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTitleSuggestionItem(EmailAddress emailAddress, String? suggestionValid) {
|
|
||||||
return RichTextWidget(
|
|
||||||
textOrigin: emailAddress.asString(),
|
|
||||||
wordSearched: suggestionValid ?? ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? _buildSubtitleSuggestionItem(EmailAddress emailAddress, String? suggestionValid) {
|
|
||||||
if (emailAddress.displayName.isNotEmpty && emailAddress.emailAddress.isNotEmpty) {
|
|
||||||
return RichTextWidget(
|
|
||||||
textOrigin: emailAddress.emailAddress,
|
|
||||||
wordSearched: suggestionValid ?? '',
|
|
||||||
styleTextOrigin: const TextStyle(
|
|
||||||
color: AppColor.colorHintSearchBar,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.normal
|
|
||||||
),
|
|
||||||
styleWordSearched: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FutureOr<List<SuggestionEmailAddress>> _findSuggestions(String query) async {
|
|
||||||
log('EmailAddressInputBuilder::_findSuggestions():query: $query');
|
|
||||||
if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) {
|
|
||||||
log('EmailAddressInputBuilder::_findSuggestions(): return empty');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final processedQuery = query.trim();
|
|
||||||
if (processedQuery.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final tmailSuggestion = List<SuggestionEmailAddress>.empty(growable: true);
|
|
||||||
if (processedQuery.length >= AppConstants.limitCharToStartSearch && _onSuggestionEmailAddress != null) {
|
|
||||||
tmailSuggestion.addAll(
|
|
||||||
(await _onSuggestionEmailAddress!(processedQuery))
|
|
||||||
.map((emailAddress) => _toSuggestionEmailAddress(emailAddress, listEmailAddress)));
|
|
||||||
}
|
|
||||||
|
|
||||||
tmailSuggestion.addAll(_matchedSuggestionEmailAddress(processedQuery, listEmailAddress));
|
|
||||||
|
|
||||||
final currentTextOnTextField = controller?.text ?? '';
|
|
||||||
if (currentTextOnTextField.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmailSuggestion.toSet().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isEmailAddressValid(String emailAddress) => GetUtils.isEmail(emailAddress);
|
|
||||||
|
|
||||||
bool _isDuplicatedRecipient(String inputEmail) {
|
|
||||||
if (inputEmail.isEmpty) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return listEmailAddress
|
|
||||||
.map((emailAddress) => emailAddress.email)
|
|
||||||
.whereNotNull()
|
|
||||||
.contains(inputEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List<EmailAddress> addedEmailAddresses) {
|
|
||||||
if (addedEmailAddresses.contains(item)) {
|
|
||||||
return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated);
|
|
||||||
} else {
|
|
||||||
return SuggestionEmailAddress(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<SuggestionEmailAddress> _matchedSuggestionEmailAddress(String query, List<EmailAddress> addedEmailAddress) {
|
|
||||||
return addedEmailAddress.where((addedMail) => addedMail.emailAddress.contains(query))
|
|
||||||
.map((emailAddress) => SuggestionEmailAddress(emailAddress, state: SuggestionEmailState.duplicated));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleGapBetweenTagChangedAndFindSuggestion() {
|
|
||||||
log('EmailAddressInputBuilder::_handleGapBetweenTagChangedAndFindSuggestion(): Timeout');
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getTagBackgroundColor(EmailAddress emailCurrent, bool isLastEmail) {
|
|
||||||
if (lastTagFocused && isLastEmail) {
|
|
||||||
return AppColor.colorItemRecipientSelected;
|
|
||||||
} else {
|
|
||||||
return _isEmailAddressValid(emailCurrent.emailAddress)
|
|
||||||
? AppColor.colorEmailAddressTag
|
|
||||||
: Colors.white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BorderSide _getTagBorderSide(EmailAddress emailCurrent, bool isLastEmail) {
|
|
||||||
if (lastTagFocused && isLastEmail) {
|
|
||||||
return const BorderSide(
|
|
||||||
width: 1,
|
|
||||||
color: AppColor.primaryColor);
|
|
||||||
} else {
|
|
||||||
return BorderSide(
|
|
||||||
width: _isEmailAddressValid(emailCurrent.emailAddress) ? 0 : 1,
|
|
||||||
color: _isEmailAddressValid(emailCurrent.emailAddress)
|
|
||||||
? AppColor.colorEmailAddressTag
|
|
||||||
: AppColor.colorBorderEmailAddressInvalid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleDeleteTagAction(StateSetter setState, BuildContext context) {
|
|
||||||
log('EmailAddressInputBuilder::_handleDeleteTagAction()');
|
|
||||||
if (listEmailAddress.isNotEmpty) {
|
|
||||||
setState(() {
|
|
||||||
listEmailAddress.removeLast();
|
|
||||||
});
|
|
||||||
_onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
|
import 'package:core/utils/app_logger.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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/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/prefix_email_address_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_composer_widget_style.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart';
|
||||||
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
|
import 'package:tmail_ui_user/main/utils/app_constants.dart';
|
||||||
|
|
||||||
|
typedef OnSuggestionEmailAddress = Future<List<EmailAddress>> Function(String word);
|
||||||
|
typedef OnUpdateListEmailAddressAction = void Function(PrefixEmailAddress prefix, List<EmailAddress> newData);
|
||||||
|
typedef OnAddEmailAddressTypeAction = void Function(PrefixEmailAddress prefix);
|
||||||
|
typedef OnDeleteEmailAddressTypeAction = void Function(PrefixEmailAddress prefix);
|
||||||
|
typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress prefix);
|
||||||
|
typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress prefix, bool isFocus);
|
||||||
|
|
||||||
|
class RecipientComposerWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
final PrefixEmailAddress prefix;
|
||||||
|
final List<EmailAddress> listEmailAddress;
|
||||||
|
final ExpandMode expandMode;
|
||||||
|
final PrefixRecipientState ccState;
|
||||||
|
final PrefixRecipientState bccState;
|
||||||
|
final bool? isInitial;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final bool autoDisposeFocusNode;
|
||||||
|
final GlobalKey? keyTagEditor;
|
||||||
|
final FocusNode? nextFocusNode;
|
||||||
|
final TextEditingController? controller;
|
||||||
|
final OnUpdateListEmailAddressAction? onUpdateListEmailAddressAction;
|
||||||
|
final OnSuggestionEmailAddress? onSuggestionEmailAddress;
|
||||||
|
final OnAddEmailAddressTypeAction? onAddEmailAddressTypeAction;
|
||||||
|
final OnDeleteEmailAddressTypeAction? onDeleteEmailAddressTypeAction;
|
||||||
|
final OnShowFullListEmailAddressAction? onShowFullListEmailAddressAction;
|
||||||
|
final OnFocusEmailAddressChangeAction? onFocusEmailAddressChangeAction;
|
||||||
|
final VoidCallback? onFocusNextAddressAction;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
final EdgeInsetsGeometry? margin;
|
||||||
|
|
||||||
|
const RecipientComposerWidget({
|
||||||
|
super.key,
|
||||||
|
required this.prefix,
|
||||||
|
required this.listEmailAddress,
|
||||||
|
this.ccState = PrefixRecipientState.disabled,
|
||||||
|
this.bccState = PrefixRecipientState.disabled,
|
||||||
|
this.isInitial,
|
||||||
|
this.controller,
|
||||||
|
this.focusNode,
|
||||||
|
this.autoDisposeFocusNode = true,
|
||||||
|
this.expandMode = ExpandMode.EXPAND,
|
||||||
|
this.keyTagEditor,
|
||||||
|
this.nextFocusNode,
|
||||||
|
this.padding,
|
||||||
|
this.margin,
|
||||||
|
this.onUpdateListEmailAddressAction,
|
||||||
|
this.onSuggestionEmailAddress,
|
||||||
|
this.onAddEmailAddressTypeAction,
|
||||||
|
this.onDeleteEmailAddressTypeAction,
|
||||||
|
this.onShowFullListEmailAddressAction,
|
||||||
|
this.onFocusEmailAddressChangeAction,
|
||||||
|
this.onFocusNextAddressAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipientComposerWidget> createState() => _RecipientComposerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
|
||||||
|
|
||||||
|
Timer? _gapBetweenTagChangedAndFindSuggestion;
|
||||||
|
bool _lastTagFocused = false;
|
||||||
|
late List<EmailAddress> _currentListEmailAddress;
|
||||||
|
|
||||||
|
final _imagePaths = Get.find<ImagePaths>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentListEmailAddress = widget.listEmailAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: RecipientComposerWidgetStyle.borderColor,
|
||||||
|
width: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
padding: widget.padding,
|
||||||
|
margin: widget.margin,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: RecipientComposerWidgetStyle.labelMargin,
|
||||||
|
child: Text(
|
||||||
|
'${widget.prefix.asName(context)}:',
|
||||||
|
style: RecipientComposerWidgetStyle.labelTextStyle
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: RecipientComposerWidgetStyle.space),
|
||||||
|
Expanded(
|
||||||
|
child: FocusScope(
|
||||||
|
child: Focus(
|
||||||
|
onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus),
|
||||||
|
onKey: (focusNode, event) {
|
||||||
|
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
|
widget.nextFocusNode?.requestFocus();
|
||||||
|
widget.onFocusNextAddressAction?.call();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: TagEditor<SuggestionEmailAddress>(
|
||||||
|
key: widget.keyTagEditor,
|
||||||
|
length: _collapsedListEmailAddress.length,
|
||||||
|
controller: widget.controller,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
autoDisposeFocusNode: widget.autoDisposeFocusNode,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
debounceDuration: const Duration(milliseconds: 150),
|
||||||
|
hasAddButton: false,
|
||||||
|
tagSpacing: 8,
|
||||||
|
autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty,
|
||||||
|
minTextFieldWidth: 20,
|
||||||
|
resetTextOnSubmitted: true,
|
||||||
|
suggestionsBoxElevation: 20.0,
|
||||||
|
suggestionsBoxBackgroundColor: Colors.white,
|
||||||
|
suggestionsBoxRadius: 20,
|
||||||
|
suggestionsBoxMaxHeight: 350,
|
||||||
|
textStyle: RecipientComposerWidgetStyle.inputTextStyle,
|
||||||
|
onFocusTagAction: (focused) {
|
||||||
|
setState(() {
|
||||||
|
_lastTagFocused = focused;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDeleteTagAction: () {
|
||||||
|
if (_currentListEmailAddress.isNotEmpty) {
|
||||||
|
setState(_currentListEmailAddress.removeLast);
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelectOptionAction: (item) {
|
||||||
|
if (!_isDuplicatedRecipient(item.emailAddress.emailAddress)) {
|
||||||
|
setState(() => _currentListEmailAddress.add(item.emailAddress));
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitted: (value) {
|
||||||
|
final textTrim = value.trim();
|
||||||
|
if (!_isDuplicatedRecipient(textTrim)) {
|
||||||
|
setState(() => _currentListEmailAddress.add(EmailAddress(null, textTrim)));
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputDecoration: const InputDecoration(border: InputBorder.none),
|
||||||
|
tagBuilder: (context, index) {
|
||||||
|
final currentEmailAddress = _currentListEmailAddress[index];
|
||||||
|
final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last;
|
||||||
|
|
||||||
|
return RecipientTagItemWidget(
|
||||||
|
prefix: widget.prefix,
|
||||||
|
currentEmailAddress: currentEmailAddress,
|
||||||
|
currentListEmailAddress: _currentListEmailAddress,
|
||||||
|
collapsedListEmailAddress: _collapsedListEmailAddress,
|
||||||
|
isLatestEmail: isLatestEmail,
|
||||||
|
isCollapsed: _isCollapse,
|
||||||
|
isLatestTagFocused: _lastTagFocused,
|
||||||
|
onDeleteTagAction: () {
|
||||||
|
setState(() => _currentListEmailAddress.removeAt(index));
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
},
|
||||||
|
onShowFullAction: widget.onShowFullListEmailAddressAction,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onTagChanged: (value) {
|
||||||
|
final textTrim = value.trim();
|
||||||
|
if (!_isDuplicatedRecipient(textTrim)) {
|
||||||
|
setState(() => _currentListEmailAddress.add(EmailAddress(null, textTrim)));
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
}
|
||||||
|
_gapBetweenTagChangedAndFindSuggestion = Timer(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
_handleGapBetweenTagChangedAndFindSuggestion);
|
||||||
|
},
|
||||||
|
findSuggestions: _findSuggestions,
|
||||||
|
useDefaultHighlight: false,
|
||||||
|
suggestionBuilder: (
|
||||||
|
context,
|
||||||
|
tagEditorState,
|
||||||
|
suggestionEmailAddress,
|
||||||
|
index,
|
||||||
|
length,
|
||||||
|
highlight,
|
||||||
|
suggestionValid
|
||||||
|
) {
|
||||||
|
return RecipientSuggestionItemWidget(
|
||||||
|
suggestionState: suggestionEmailAddress.state,
|
||||||
|
emailAddress: suggestionEmailAddress.emailAddress,
|
||||||
|
suggestionValid: suggestionValid,
|
||||||
|
highlight: highlight,
|
||||||
|
onSelectedAction: (emailAddress) {
|
||||||
|
setState(() => _currentListEmailAddress.add(emailAddress));
|
||||||
|
widget.onUpdateListEmailAddressAction?.call(widget.prefix, _currentListEmailAddress);
|
||||||
|
tagEditorState.resetTextField();
|
||||||
|
tagEditorState.closeSuggestionBox();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(width: RecipientComposerWidgetStyle.space),
|
||||||
|
if (widget.prefix == PrefixEmailAddress.to && widget.ccState == PrefixRecipientState.disabled)
|
||||||
|
TMailButtonWidget.fromText(
|
||||||
|
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.prefix == PrefixEmailAddress.to && widget.bccState == PrefixRecipientState.disabled)
|
||||||
|
TMailButtonWidget.fromText(
|
||||||
|
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.prefix != PrefixEmailAddress.to)
|
||||||
|
TMailButtonWidget.fromIcon(
|
||||||
|
icon: _imagePaths.icClose,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor,
|
||||||
|
iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize,
|
||||||
|
padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding,
|
||||||
|
margin: RecipientComposerWidgetStyle.recipientMargin,
|
||||||
|
onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE;
|
||||||
|
|
||||||
|
List<EmailAddress> get _collapsedListEmailAddress => _isCollapse
|
||||||
|
? _currentListEmailAddress.sublist(0, 1)
|
||||||
|
: _currentListEmailAddress;
|
||||||
|
|
||||||
|
FutureOr<List<SuggestionEmailAddress>> _findSuggestions(String query) async {
|
||||||
|
if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final processedQuery = query.trim();
|
||||||
|
if (processedQuery.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final tmailSuggestion = List<SuggestionEmailAddress>.empty(growable: true);
|
||||||
|
if (processedQuery.length >= AppConstants.limitCharToStartSearch &&
|
||||||
|
widget.onSuggestionEmailAddress != null) {
|
||||||
|
final listEmailAddress = await widget.onSuggestionEmailAddress!(processedQuery);
|
||||||
|
final listSuggestionEmailAddress = listEmailAddress.map((emailAddress) => _toSuggestionEmailAddress(emailAddress, _currentListEmailAddress));
|
||||||
|
tmailSuggestion.addAll(listSuggestionEmailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
tmailSuggestion.addAll(_matchedSuggestionEmailAddress(processedQuery, _currentListEmailAddress));
|
||||||
|
|
||||||
|
final currentTextOnTextField = widget.controller?.text ?? '';
|
||||||
|
if (currentTextOnTextField.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmailSuggestion.toSet().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isDuplicatedRecipient(String inputEmail) {
|
||||||
|
if (inputEmail.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _currentListEmailAddress
|
||||||
|
.map((emailAddress) => emailAddress.email)
|
||||||
|
.whereNotNull()
|
||||||
|
.contains(inputEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List<EmailAddress> addedEmailAddresses) {
|
||||||
|
if (addedEmailAddresses.contains(item)) {
|
||||||
|
return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated);
|
||||||
|
} else {
|
||||||
|
return SuggestionEmailAddress(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<SuggestionEmailAddress> _matchedSuggestionEmailAddress(String query, List<EmailAddress> addedEmailAddress) {
|
||||||
|
return addedEmailAddress
|
||||||
|
.where((addedMail) => addedMail.emailAddress.contains(query))
|
||||||
|
.map((emailAddress) => SuggestionEmailAddress(
|
||||||
|
emailAddress,
|
||||||
|
state: SuggestionEmailState.duplicated
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleGapBetweenTagChangedAndFindSuggestion() {
|
||||||
|
log('EmailAddressInputBuilder::_handleGapBetweenTagChangedAndFindSuggestion(): Timeout');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/resources/image_paths.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/extensions/email_address_extension.dart';
|
||||||
|
import 'package:super_tag_editor/widgets/rich_text_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_suggestion_item_widget_style.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart';
|
||||||
|
|
||||||
|
typedef OnSelectedRecipientSuggestionAction = Function(EmailAddress emailAddress);
|
||||||
|
|
||||||
|
class RecipientSuggestionItemWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final SuggestionEmailState suggestionState;
|
||||||
|
final EmailAddress emailAddress;
|
||||||
|
final String? suggestionValid;
|
||||||
|
final bool highlight;
|
||||||
|
final OnSelectedRecipientSuggestionAction? onSelectedAction;
|
||||||
|
|
||||||
|
final _imagePaths = Get.find<ImagePaths>();
|
||||||
|
|
||||||
|
RecipientSuggestionItemWidget({
|
||||||
|
super.key,
|
||||||
|
required this.suggestionState,
|
||||||
|
required this.emailAddress,
|
||||||
|
this.suggestionValid,
|
||||||
|
this.highlight = false,
|
||||||
|
this.onSelectedAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (suggestionState == SuggestionEmailState.duplicated) {
|
||||||
|
return Container(
|
||||||
|
margin: RecipientSuggestionItemWidgetStyle.suggestionDuplicatedMargin,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColor.colorBgMenuItemDropDownSelected,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(RecipientSuggestionItemWidgetStyle.radius))
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: RecipientSuggestionItemWidgetStyle.labelDuplicatedPadding,
|
||||||
|
leading: AvatarSuggestionItemWidget(emailAddress: emailAddress),
|
||||||
|
title: RichTextWidget(
|
||||||
|
textOrigin: emailAddress.asString(),
|
||||||
|
wordSearched: suggestionValid ?? ''
|
||||||
|
),
|
||||||
|
subtitle: emailAddress.emailAddress.isNotEmpty
|
||||||
|
? RichTextWidget(
|
||||||
|
textOrigin: emailAddress.emailAddress,
|
||||||
|
wordSearched: suggestionValid ?? '',
|
||||||
|
styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle,
|
||||||
|
styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: SvgPicture.asset(
|
||||||
|
_imagePaths.icFilterSelected,
|
||||||
|
width: RecipientSuggestionItemWidgetStyle.selectedIconSize,
|
||||||
|
height: RecipientSuggestionItemWidgetStyle.selectedIconSize,
|
||||||
|
fit: BoxFit.fill
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
color: highlight ? AppColor.colorItemSelected : Colors.white,
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: RecipientSuggestionItemWidgetStyle.labelPadding,
|
||||||
|
leading: AvatarSuggestionItemWidget(emailAddress: emailAddress),
|
||||||
|
title: RichTextWidget(
|
||||||
|
textOrigin: emailAddress.asString(),
|
||||||
|
wordSearched: suggestionValid ?? ''
|
||||||
|
),
|
||||||
|
subtitle: emailAddress.emailAddress.isNotEmpty
|
||||||
|
? RichTextWidget(
|
||||||
|
textOrigin: emailAddress.emailAddress,
|
||||||
|
wordSearched: suggestionValid ?? '',
|
||||||
|
styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle,
|
||||||
|
styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () => onSelectedAction?.call(emailAddress),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/extensions/string_extension.dart';
|
||||||
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:core/presentation/utils/style_utils.dart';
|
||||||
|
import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart';
|
||||||
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
|
import 'package:core/utils/direction_utils.dart';
|
||||||
|
import 'package:core/utils/platform_info.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:model/extensions/email_address_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart';
|
||||||
|
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
|
||||||
|
|
||||||
|
class RecipientTagItemWidget extends StatelessWidget {
|
||||||
|
|
||||||
|
final bool isCollapsed;
|
||||||
|
final bool isLatestTagFocused;
|
||||||
|
final bool isLatestEmail;
|
||||||
|
final PrefixEmailAddress prefix;
|
||||||
|
final EmailAddress currentEmailAddress;
|
||||||
|
final List<EmailAddress> currentListEmailAddress;
|
||||||
|
final List<EmailAddress> collapsedListEmailAddress;
|
||||||
|
final OnShowFullListEmailAddressAction? onShowFullAction;
|
||||||
|
final VoidCallback? onDeleteTagAction;
|
||||||
|
|
||||||
|
final _imagePaths = Get.find<ImagePaths>();
|
||||||
|
|
||||||
|
RecipientTagItemWidget({
|
||||||
|
super.key,
|
||||||
|
required this.prefix,
|
||||||
|
required this.currentEmailAddress,
|
||||||
|
required this.currentListEmailAddress,
|
||||||
|
required this.collapsedListEmailAddress,
|
||||||
|
this.isCollapsed = false,
|
||||||
|
this.isLatestTagFocused = false,
|
||||||
|
this.isLatestEmail = false,
|
||||||
|
this.onShowFullAction,
|
||||||
|
this.onDeleteTagAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(
|
||||||
|
top: PlatformInfo.isWeb ? 8 : 0,
|
||||||
|
end: isCollapsed ? 40 : 0),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => isCollapsed
|
||||||
|
? onShowFullAction?.call(prefix)
|
||||||
|
: null,
|
||||||
|
child: Chip(
|
||||||
|
labelPadding: EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
currentEmailAddress.asString(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: CommonTextStyle.defaultTextOverFlow,
|
||||||
|
softWrap: CommonTextStyle.defaultSoftWrap,
|
||||||
|
),
|
||||||
|
deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill),
|
||||||
|
labelStyle: RecipientTagItemWidgetStyle.labelTextStyle,
|
||||||
|
backgroundColor: _getTagBackgroundColor(),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)),
|
||||||
|
side: _getTagBorderSide(),
|
||||||
|
),
|
||||||
|
avatar: currentEmailAddress.displayName.isNotEmpty
|
||||||
|
? GradientCircleAvatarIcon(
|
||||||
|
colors: currentEmailAddress.avatarColors,
|
||||||
|
label: currentEmailAddress.displayName.firstLetterToUpperCase,
|
||||||
|
labelFontSize: 10,
|
||||||
|
iconSize: 24,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onDeleted: onDeleteTagAction,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isCollapsed)
|
||||||
|
TMailButtonWidget.fromText(
|
||||||
|
margin: RecipientTagItemWidgetStyle.counterMargin,
|
||||||
|
text: '+${currentListEmailAddress.length - collapsedListEmailAddress.length}',
|
||||||
|
onTapActionCallback: () => onShowFullAction?.call(prefix),
|
||||||
|
borderRadius: RecipientTagItemWidgetStyle.radius,
|
||||||
|
textStyle: RecipientTagItemWidgetStyle.labelTextStyle,
|
||||||
|
padding: RecipientTagItemWidgetStyle.counterPadding,
|
||||||
|
backgroundColor: AppColor.colorEmailAddressTag,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTagBackgroundColor() {
|
||||||
|
if (isLatestTagFocused && isLatestEmail) {
|
||||||
|
return AppColor.colorItemRecipientSelected;
|
||||||
|
} else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) {
|
||||||
|
return AppColor.colorEmailAddressTag;
|
||||||
|
} else {
|
||||||
|
return Colors.white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BorderSide _getTagBorderSide() {
|
||||||
|
if (isLatestTagFocused && isLatestEmail) {
|
||||||
|
return const BorderSide(width: 1, color: AppColor.primaryColor);
|
||||||
|
} else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) {
|
||||||
|
return BorderSide.none;
|
||||||
|
} else {
|
||||||
|
return const BorderSide(
|
||||||
|
width: 1,
|
||||||
|
color: AppColor.colorBorderEmailAddressInvalid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user