From 62d416c4a4a49a01cf5cc2bfd9c6bfe3abfe5198 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 26 Oct 2021 13:17:12 +0700 Subject: [PATCH] TF-138 Add domain layer for search mail with text pattern --- core/lib/core.dart | 1 + .../extensions/color_extension.dart | 3 + .../views/text/rich_text_builder.dart | 69 +++++++++++++++++++ .../views/text/text_field_builder.dart | 29 +++++++- .../thread/domain/model/search_query.dart | 11 +++ .../domain/repository/thread_repository.dart | 11 +++ .../domain/state/search_email_state.dart | 39 +++++++++++ .../usecases/search_email_interactor.dart | 47 +++++++++++++ model/lib/email/presentation_email.dart | 10 ++- model/lib/extensions/email_extension.dart | 1 + .../presentation_email_extension.dart | 37 ++++++++++ 11 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 core/lib/presentation/views/text/rich_text_builder.dart create mode 100644 lib/features/thread/domain/model/search_query.dart create mode 100644 lib/features/thread/domain/state/search_email_state.dart create mode 100644 lib/features/thread/domain/usecases/search_email_interactor.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 0ffdc7ecb..970a3d7d4 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -25,6 +25,7 @@ export 'presentation/views/text/slogan_builder.dart'; export 'presentation/views/text/text_field_builder.dart'; export 'presentation/views/text/input_decoration_builder.dart'; export 'presentation/views/text/text_builder.dart'; +export 'presentation/views/text/rich_text_builder.dart'; export 'presentation/views/responsive/responsive_widget.dart'; export 'presentation/views/list/tree_view.dart'; export 'presentation/views/button/button_builder.dart'; diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 0258d71e4..6c1e4f997 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -59,4 +59,7 @@ extension AppColor on Color { static const toastWithActionBackgroundColor = Color(0xFF3F3F3F); static const buttonActionToastWithActionColor = Color(0xFF7ADCF8); static const backgroundCountAttachment = Color(0x681C1C1C); + static const bgStatusResultSearch = Color(0xFFF5F5F7); + static const bgWordSearch = Color(0xFFD7D6FC); } + diff --git a/core/lib/presentation/views/text/rich_text_builder.dart b/core/lib/presentation/views/text/rich_text_builder.dart new file mode 100644 index 000000000..f4b49180c --- /dev/null +++ b/core/lib/presentation/views/text/rich_text_builder.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +class RichTextBuilder { + + final String _textOrigin; + final String _wordToStyle; + final TextStyle _styleOrigin; + final TextStyle _styleWord; + + Key? _key; + int? _maxLines; + TextOverflow? _overflow; + + RichTextBuilder( + this._textOrigin, + this._wordToStyle, + this._styleOrigin, + this._styleWord, + ); + + void key(Key key) { + _key = key; + } + + void maxLines(int maxLines) { + _maxLines = maxLines; + } + + void setOverflow(TextOverflow textOverflow) { + _overflow = textOverflow; + } + + RichText build() { + return RichText( + key: _key, + maxLines: _maxLines ?? 1, + overflow: _overflow ?? TextOverflow.ellipsis, + text: TextSpan( + style: _styleOrigin, + children: _getSpans(_textOrigin, _wordToStyle, _styleWord))); + } + + List _getSpans(String text, String matchWord, TextStyle style) { + List spans = []; + int spanBoundary = 0; + do { + // look for the next match + final startIndex = text.toLowerCase().indexOf(matchWord.toLowerCase(), spanBoundary); + // if no more matches then add the rest of the string without style + if (startIndex == -1) { + spans.add(TextSpan(text: text.substring(spanBoundary))); + return spans; + } + // add any unStyled text before the next match + if (startIndex > spanBoundary) { + spans.add(TextSpan(text: text.substring(spanBoundary, startIndex))); + } + // style the matched text + final endIndex = startIndex + matchWord.length; + final spanText = text.substring(startIndex, endIndex); + spans.add(TextSpan(text: spanText, style: style)); + // mark the boundary to start the next search from + spanBoundary = endIndex; + // continue until there are no more matches + } while (spanBoundary < text.length); + + return spans; + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/text/text_field_builder.dart b/core/lib/presentation/views/text/text_field_builder.dart index a9009cf05..d80225d62 100644 --- a/core/lib/presentation/views/text/text_field_builder.dart +++ b/core/lib/presentation/views/text/text_field_builder.dart @@ -1,9 +1,12 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; +typedef OnOpenSearchMailActionClick = void Function(); + class TextFieldBuilder { Key? _key; ValueChanged? _onTextChange; + ValueChanged? _onTextSubmitted; TextStyle? _textStyle; TextInputAction? _textInputAction; InputDecoration? _inputDecoration; @@ -11,6 +14,9 @@ class TextFieldBuilder { int? _maxLines = 1; TextEditingController? _textController; TextInputType? _keyboardType; + Color? _cursorColor; + bool? _autoFocus; + FocusNode? _focusNode; void key(Key key) { _key = key; @@ -20,6 +26,10 @@ class TextFieldBuilder { _onTextChange = onChange; } + void onSubmitted(ValueChanged onSubmitted) { + _onTextSubmitted = onSubmitted; + } + void textStyle(TextStyle style) { _textStyle = style; } @@ -40,7 +50,7 @@ class TextFieldBuilder { _textController = TextEditingController.fromValue(TextEditingValue(text: value)); } - void addController(TextEditingController textEditingController) { + void addController(TextEditingController? textEditingController) { _textController = textEditingController; } @@ -52,11 +62,23 @@ class TextFieldBuilder { _keyboardType = value; } + void cursorColor(Color? color) { + _cursorColor = color; + } + + void autoFocus(bool autoFocus) { + _autoFocus = autoFocus; + } + + void addFocusNode(FocusNode? focusNode) { + _focusNode = focusNode; + } + TextField build() { return TextField( key: _key ?? Key('TextFieldBuilder'), onChanged: _onTextChange, - cursorColor: AppColor.primaryColor, + cursorColor: _cursorColor ?? AppColor.primaryColor, controller: _textController, autocorrect: false, textInputAction: _textInputAction, @@ -66,6 +88,9 @@ class TextFieldBuilder { style: _textStyle ?? TextStyle(color: AppColor.textFieldTextColor), obscureText: _obscureText ?? false, keyboardType: _keyboardType, + onSubmitted: _onTextSubmitted, + autofocus: _autoFocus ?? false, + focusNode: _focusNode, ); } } \ No newline at end of file diff --git a/lib/features/thread/domain/model/search_query.dart b/lib/features/thread/domain/model/search_query.dart new file mode 100644 index 000000000..711e88a34 --- /dev/null +++ b/lib/features/thread/domain/model/search_query.dart @@ -0,0 +1,11 @@ + +import 'package:equatable/equatable.dart'; + +class SearchQuery with EquatableMixin { + final String value; + + SearchQuery(this.value); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 384a2f5bd..77091c7e5 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; @@ -40,4 +41,14 @@ abstract class ThreadRepository { Properties? properties, } ); + + Future> searchEmails( + AccountId accountId, + { + UnsignedInt? limit, + Set? sort, + Filter? filter, + Properties? properties, + } + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/search_email_state.dart b/lib/features/thread/domain/state/search_email_state.dart new file mode 100644 index 000000000..0efede1d4 --- /dev/null +++ b/lib/features/thread/domain/state/search_email_state.dart @@ -0,0 +1,39 @@ + +import 'package:core/core.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; + +class SearchingState extends UIState { + + SearchingState(); + + @override + List get props => []; +} + +class SearchEmailNewQuery extends UIState { + final SearchQuery searchQuery; + + SearchEmailNewQuery(this.searchQuery); + + @override + List get props => [searchQuery]; +} + +class SearchEmailSuccess extends UIState { + final List emailList; + + SearchEmailSuccess(this.emailList); + + @override + List get props => [emailList]; +} + +class SearchEmailFailure extends FeatureFailure { + final exception; + + SearchEmailFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/search_email_interactor.dart b/lib/features/thread/domain/usecases/search_email_interactor.dart new file mode 100644 index 000000000..ab9515bfb --- /dev/null +++ b/lib/features/thread/domain/usecases/search_email_interactor.dart @@ -0,0 +1,47 @@ + +import 'package:core/core.dart'; +import 'package:model/model.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; + +class SearchEmailInteractor { + + final ThreadRepository threadRepository; + + SearchEmailInteractor(this.threadRepository); + + Stream> execute( + AccountId accountId, + { + UnsignedInt? limit, + Set? sort, + Filter? filter, + Properties? properties, + } + ) async* { + try { + yield Right(SearchingState()); + + final emailList = await threadRepository.searchEmails( + accountId, + limit: limit, + sort: sort, + filter: filter, + properties: properties); + + final presentationEmailList = emailList + .map((email) => email.toPresentationEmail()) + .toList(); + + yield Right(SearchEmailSuccess(presentationEmailList)); + } catch (e) { + yield Left(SearchEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 760fb072e..3961602b0 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -5,8 +5,8 @@ import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; -import 'package:model/mailbox/select_mode.dart'; -import 'package:model/extensions/email_address_extension.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/model.dart'; class PresentationEmail with EquatableMixin { @@ -23,6 +23,8 @@ class PresentationEmail with EquatableMixin { final Set? cc; final Set? bcc; final Set? replyTo; + final Map? mailboxIds; + final List? mailboxNames; final SelectMode selectMode; PresentationEmail( @@ -40,6 +42,8 @@ class PresentationEmail with EquatableMixin { this.cc, this.bcc, this.replyTo, + this.mailboxIds, + this.mailboxNames, this.selectMode = SelectMode.INACTIVE } ); @@ -65,6 +69,8 @@ class PresentationEmail with EquatableMixin { bool isFlaggedEmail() => keywords?.containsKey(KeyWordIdentifier.emailFlagged) == true; + String get mailboxName => mailboxNames?.first?.name ?? ''; + @override List get props => [ id, diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index c7db294c2..3319fd5cf 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -57,6 +57,7 @@ extension EmailExtension on Email { cc: cc, bcc: bcc, replyTo: replyTo, + mailboxIds: mailboxIds, selectMode: selectMode ); } diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 4e1130ecb..64c68e9f5 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -34,6 +34,8 @@ extension PresentationEmailExtension on PresentationEmail { cc: cc, bcc: bcc, replyTo: replyTo, + mailboxIds: mailboxIds, + mailboxNames: mailboxNames, selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE ); } @@ -53,6 +55,8 @@ extension PresentationEmailExtension on PresentationEmail { cc: cc, bcc: bcc, replyTo: replyTo, + mailboxIds: mailboxIds, + mailboxNames: mailboxNames, selectMode: selectMode ); } @@ -102,4 +106,37 @@ extension PresentationEmailExtension on PresentationEmail { return Tuple3([], [], []); } } + + PresentationEmail toSearchPresentationEmail(Map mapMailboxes) { + mailboxIds?.removeWhere((key, value) => !value); + + final listMailboxId = mailboxIds?.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + + final listMailboxName = listMailboxId + ?.map((mailboxId) => mapMailboxes.containsKey(mailboxId) ? mapMailboxes[mailboxId]?.name : null) + .where((mailboxName) => mailboxName != null) + .toList(); + + return PresentationEmail( + this.id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + mailboxIds: mailboxIds, + mailboxNames: listMailboxName, + selectMode: selectMode + ); + } } \ No newline at end of file