TF-138 Add domain layer for search mail with text pattern
This commit is contained in:
@@ -25,6 +25,7 @@ export 'presentation/views/text/slogan_builder.dart';
|
|||||||
export 'presentation/views/text/text_field_builder.dart';
|
export 'presentation/views/text/text_field_builder.dart';
|
||||||
export 'presentation/views/text/input_decoration_builder.dart';
|
export 'presentation/views/text/input_decoration_builder.dart';
|
||||||
export 'presentation/views/text/text_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/responsive/responsive_widget.dart';
|
||||||
export 'presentation/views/list/tree_view.dart';
|
export 'presentation/views/list/tree_view.dart';
|
||||||
export 'presentation/views/button/button_builder.dart';
|
export 'presentation/views/button/button_builder.dart';
|
||||||
|
|||||||
@@ -59,4 +59,7 @@ extension AppColor on Color {
|
|||||||
static const toastWithActionBackgroundColor = Color(0xFF3F3F3F);
|
static const toastWithActionBackgroundColor = Color(0xFF3F3F3F);
|
||||||
static const buttonActionToastWithActionColor = Color(0xFF7ADCF8);
|
static const buttonActionToastWithActionColor = Color(0xFF7ADCF8);
|
||||||
static const backgroundCountAttachment = Color(0x681C1C1C);
|
static const backgroundCountAttachment = Color(0x681C1C1C);
|
||||||
|
static const bgStatusResultSearch = Color(0xFFF5F5F7);
|
||||||
|
static const bgWordSearch = Color(0xFFD7D6FC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<TextSpan> _getSpans(String text, String matchWord, TextStyle style) {
|
||||||
|
List<TextSpan> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:core/presentation/extensions/color_extension.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef OnOpenSearchMailActionClick = void Function();
|
||||||
|
|
||||||
class TextFieldBuilder {
|
class TextFieldBuilder {
|
||||||
Key? _key;
|
Key? _key;
|
||||||
ValueChanged<String>? _onTextChange;
|
ValueChanged<String>? _onTextChange;
|
||||||
|
ValueChanged<String>? _onTextSubmitted;
|
||||||
TextStyle? _textStyle;
|
TextStyle? _textStyle;
|
||||||
TextInputAction? _textInputAction;
|
TextInputAction? _textInputAction;
|
||||||
InputDecoration? _inputDecoration;
|
InputDecoration? _inputDecoration;
|
||||||
@@ -11,6 +14,9 @@ class TextFieldBuilder {
|
|||||||
int? _maxLines = 1;
|
int? _maxLines = 1;
|
||||||
TextEditingController? _textController;
|
TextEditingController? _textController;
|
||||||
TextInputType? _keyboardType;
|
TextInputType? _keyboardType;
|
||||||
|
Color? _cursorColor;
|
||||||
|
bool? _autoFocus;
|
||||||
|
FocusNode? _focusNode;
|
||||||
|
|
||||||
void key(Key key) {
|
void key(Key key) {
|
||||||
_key = key;
|
_key = key;
|
||||||
@@ -20,6 +26,10 @@ class TextFieldBuilder {
|
|||||||
_onTextChange = onChange;
|
_onTextChange = onChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onSubmitted(ValueChanged<String> onSubmitted) {
|
||||||
|
_onTextSubmitted = onSubmitted;
|
||||||
|
}
|
||||||
|
|
||||||
void textStyle(TextStyle style) {
|
void textStyle(TextStyle style) {
|
||||||
_textStyle = style;
|
_textStyle = style;
|
||||||
}
|
}
|
||||||
@@ -40,7 +50,7 @@ class TextFieldBuilder {
|
|||||||
_textController = TextEditingController.fromValue(TextEditingValue(text: value));
|
_textController = TextEditingController.fromValue(TextEditingValue(text: value));
|
||||||
}
|
}
|
||||||
|
|
||||||
void addController(TextEditingController textEditingController) {
|
void addController(TextEditingController? textEditingController) {
|
||||||
_textController = textEditingController;
|
_textController = textEditingController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +62,23 @@ class TextFieldBuilder {
|
|||||||
_keyboardType = value;
|
_keyboardType = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cursorColor(Color? color) {
|
||||||
|
_cursorColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoFocus(bool autoFocus) {
|
||||||
|
_autoFocus = autoFocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFocusNode(FocusNode? focusNode) {
|
||||||
|
_focusNode = focusNode;
|
||||||
|
}
|
||||||
|
|
||||||
TextField build() {
|
TextField build() {
|
||||||
return TextField(
|
return TextField(
|
||||||
key: _key ?? Key('TextFieldBuilder'),
|
key: _key ?? Key('TextFieldBuilder'),
|
||||||
onChanged: _onTextChange,
|
onChanged: _onTextChange,
|
||||||
cursorColor: AppColor.primaryColor,
|
cursorColor: _cursorColor ?? AppColor.primaryColor,
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
textInputAction: _textInputAction,
|
textInputAction: _textInputAction,
|
||||||
@@ -66,6 +88,9 @@ class TextFieldBuilder {
|
|||||||
style: _textStyle ?? TextStyle(color: AppColor.textFieldTextColor),
|
style: _textStyle ?? TextStyle(color: AppColor.textFieldTextColor),
|
||||||
obscureText: _obscureText ?? false,
|
obscureText: _obscureText ?? false,
|
||||||
keyboardType: _keyboardType,
|
keyboardType: _keyboardType,
|
||||||
|
onSubmitted: _onTextSubmitted,
|
||||||
|
autofocus: _autoFocus ?? false,
|
||||||
|
focusNode: _focusNode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class SearchQuery with EquatableMixin {
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
SearchQuery(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [value];
|
||||||
|
}
|
||||||
@@ -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/sort/comparator.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/state.dart' as jmap;
|
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/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:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
||||||
import 'package:model/model.dart';
|
import 'package:model/model.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart';
|
import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart';
|
||||||
@@ -40,4 +41,14 @@ abstract class ThreadRepository {
|
|||||||
Properties? properties,
|
Properties? properties,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<List<Email>> searchEmails(
|
||||||
|
AccountId accountId,
|
||||||
|
{
|
||||||
|
UnsignedInt? limit,
|
||||||
|
Set<Comparator>? sort,
|
||||||
|
Filter? filter,
|
||||||
|
Properties? properties,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchEmailNewQuery extends UIState {
|
||||||
|
final SearchQuery searchQuery;
|
||||||
|
|
||||||
|
SearchEmailNewQuery(this.searchQuery);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [searchQuery];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchEmailSuccess extends UIState {
|
||||||
|
final List<PresentationEmail> emailList;
|
||||||
|
|
||||||
|
SearchEmailSuccess(this.emailList);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [emailList];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchEmailFailure extends FeatureFailure {
|
||||||
|
final exception;
|
||||||
|
|
||||||
|
SearchEmailFailure(this.exception);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [exception];
|
||||||
|
}
|
||||||
@@ -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<Either<Failure, Success>> execute(
|
||||||
|
AccountId accountId,
|
||||||
|
{
|
||||||
|
UnsignedInt? limit,
|
||||||
|
Set<Comparator>? 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.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:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
||||||
import 'package:model/mailbox/select_mode.dart';
|
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
||||||
import 'package:model/extensions/email_address_extension.dart';
|
import 'package:model/model.dart';
|
||||||
|
|
||||||
class PresentationEmail with EquatableMixin {
|
class PresentationEmail with EquatableMixin {
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ class PresentationEmail with EquatableMixin {
|
|||||||
final Set<EmailAddress>? cc;
|
final Set<EmailAddress>? cc;
|
||||||
final Set<EmailAddress>? bcc;
|
final Set<EmailAddress>? bcc;
|
||||||
final Set<EmailAddress>? replyTo;
|
final Set<EmailAddress>? replyTo;
|
||||||
|
final Map<MailboxId, bool>? mailboxIds;
|
||||||
|
final List<MailboxName?>? mailboxNames;
|
||||||
final SelectMode selectMode;
|
final SelectMode selectMode;
|
||||||
|
|
||||||
PresentationEmail(
|
PresentationEmail(
|
||||||
@@ -40,6 +42,8 @@ class PresentationEmail with EquatableMixin {
|
|||||||
this.cc,
|
this.cc,
|
||||||
this.bcc,
|
this.bcc,
|
||||||
this.replyTo,
|
this.replyTo,
|
||||||
|
this.mailboxIds,
|
||||||
|
this.mailboxNames,
|
||||||
this.selectMode = SelectMode.INACTIVE
|
this.selectMode = SelectMode.INACTIVE
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -65,6 +69,8 @@ class PresentationEmail with EquatableMixin {
|
|||||||
|
|
||||||
bool isFlaggedEmail() => keywords?.containsKey(KeyWordIdentifier.emailFlagged) == true;
|
bool isFlaggedEmail() => keywords?.containsKey(KeyWordIdentifier.emailFlagged) == true;
|
||||||
|
|
||||||
|
String get mailboxName => mailboxNames?.first?.name ?? '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ extension EmailExtension on Email {
|
|||||||
cc: cc,
|
cc: cc,
|
||||||
bcc: bcc,
|
bcc: bcc,
|
||||||
replyTo: replyTo,
|
replyTo: replyTo,
|
||||||
|
mailboxIds: mailboxIds,
|
||||||
selectMode: selectMode
|
selectMode: selectMode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ extension PresentationEmailExtension on PresentationEmail {
|
|||||||
cc: cc,
|
cc: cc,
|
||||||
bcc: bcc,
|
bcc: bcc,
|
||||||
replyTo: replyTo,
|
replyTo: replyTo,
|
||||||
|
mailboxIds: mailboxIds,
|
||||||
|
mailboxNames: mailboxNames,
|
||||||
selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE
|
selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,6 +55,8 @@ extension PresentationEmailExtension on PresentationEmail {
|
|||||||
cc: cc,
|
cc: cc,
|
||||||
bcc: bcc,
|
bcc: bcc,
|
||||||
replyTo: replyTo,
|
replyTo: replyTo,
|
||||||
|
mailboxIds: mailboxIds,
|
||||||
|
mailboxNames: mailboxNames,
|
||||||
selectMode: selectMode
|
selectMode: selectMode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,4 +106,37 @@ extension PresentationEmailExtension on PresentationEmail {
|
|||||||
return Tuple3([], [], []);
|
return Tuple3([], [], []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PresentationEmail toSearchPresentationEmail(Map<MailboxId, PresentationMailbox> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user