TF-4171 Display tag in mailbox list, search list on mobile

This commit is contained in:
dab246
2025-11-28 03:18:40 +07:00
committed by Dat H. Pham
parent 0ab57402a0
commit 5fb0aa84e8
11 changed files with 843 additions and 184 deletions
@@ -6,6 +6,7 @@ import 'package:model/email/presentation_email.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_widget.dart';
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
@@ -22,6 +23,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
final bool isShowingEmailContent;
final bool isSenderImportantFlagEnabled;
final bool isAINeedsActionEnabled;
final bool autoWrapTagsByMaxWidth;
final List<Label>? labels;
final OnPressEmailActionClick? emailActionClick;
final OnMoreActionClick? onMoreActionClick;
@@ -36,6 +38,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
this.isSearchEmailRunning = false,
this.isSenderImportantFlagEnabled = true,
this.isAINeedsActionEnabled = true,
this.autoWrapTagsByMaxWidth = false,
this.mailboxContain,
this.padding,
this.isDrag = false,
@@ -128,17 +131,7 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
)),
Padding(
padding: const EdgeInsetsDirectional.only(top: 2),
child: Row(children: [
Expanded(child: buildEmailPartialContent(
context,
presentationEmail,
isSearchEmailRunning,
searchQuery)),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
])
child: _buildPartialContent(context),
),
],
),
@@ -146,6 +139,94 @@ class EmailTileBuilder extends StatelessWidget with BaseEmailItemTile {
);
}
Widget _buildPartialContent(BuildContext context) {
final hasLabels = labels?.isNotEmpty == true;
final isDesktop = responsiveUtils.isDesktop(context);
final isAutoWrap = autoWrapTagsByMaxWidth;
final hasContent =
presentationEmail.getPartialContent().isNotEmpty;
final partialContent = buildEmailPartialContent(
context,
presentationEmail,
isSearchEmailRunning,
searchQuery,
);
if (!hasLabels) {
return Row(
children: [
if (hasContent)
Flexible(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
],
);
}
if (!hasContent) {
return Row(
children: [
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
Flexible(
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
}
if (!isAutoWrap) {
return Row(
children: [
Flexible(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
],
);
}
return LayoutBuilder(
builder: (_, constraints) {
return Row(
children: [
Expanded(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
},
);
}
bool get _shouldShowAIAction =>
isAINeedsActionEnabled && presentationEmail.hasNeedAction;
}
@@ -30,6 +30,7 @@ class EmailTileBuilder extends StatefulWidget {
final bool isShowingEmailContent;
final bool isSenderImportantFlagEnabled;
final bool isAINeedsActionEnabled;
final bool autoWrapTagsByMaxWidth;
final List<Label>? labels;
final OnPressEmailActionClick? emailActionClick;
final OnMoreActionClick? onMoreActionClick;
@@ -43,6 +44,7 @@ class EmailTileBuilder extends StatefulWidget {
this.searchQuery,
this.isSearchEmailRunning = false,
this.isSenderImportantFlagEnabled = true,
this.autoWrapTagsByMaxWidth = false,
this.mailboxContain,
this.padding,
this.isDrag = false,
@@ -100,88 +102,78 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
),
),
Expanded(
child: Column(children: [
Row(children: [
if (!widget.presentationEmail.hasRead)
Padding(
padding: const EdgeInsetsDirectional.only(end: 5),
child: buildIconUnreadStatus(),
),
Expanded(
child: buildInformationSender(
context,
widget.presentationEmail,
widget.mailboxContain,
widget.isSearchEmailRunning,
widget.searchQuery
)
),
buildIconAnsweredOrForwarded(
width: 16,
height: 16,
presentationEmail: widget.presentationEmail
),
if (widget.presentationEmail.hasAttachment == true)
Padding(
padding: const EdgeInsetsDirectional.only(start: 8),
child: buildIconAttachment(),
),
Padding(
padding: const EdgeInsetsDirectional.only(
end: 4,
start: 8
),
child: buildDateTime(context, widget.presentationEmail)
),
buildIconChevron()
]),
const SizedBox(height: 2),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.presentationEmail.hasCalendarEvent)
buildCalendarEventIcon(
context: context,
presentationEmail: widget.presentationEmail
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
if (!widget.presentationEmail.hasRead)
Padding(
padding: const EdgeInsetsDirectional.only(end: 5),
child: buildIconUnreadStatus(),
),
if (widget.presentationEmail.isMarkAsImportant && widget.isSenderImportantFlagEnabled)
buildMarkAsImportantIcon(context),
Expanded(
child: buildEmailTitle(
child: buildInformationSender(
context,
widget.presentationEmail,
widget.mailboxContain,
widget.isSearchEmailRunning,
widget.searchQuery
)
),
buildMailboxContain(
context,
widget.isSearchEmailRunning,
widget.presentationEmail
buildIconAnsweredOrForwarded(
width: 16,
height: 16,
presentationEmail: widget.presentationEmail
),
if (widget.presentationEmail.hasStarred)
if (widget.presentationEmail.hasAttachment == true)
Padding(
padding: const EdgeInsetsDirectional.only(start: 8),
child: buildIconStar(),
child: buildIconAttachment(),
),
],
),
const SizedBox(height: 2),
Row(children: [
Expanded(
child: buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery
)
),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
Padding(
padding: const EdgeInsetsDirectional.only(
end: 4,
start: 8
),
child: buildDateTime(context, widget.presentationEmail)
),
]),
]),
buildIconChevron()
]),
const SizedBox(height: 2),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.presentationEmail.hasCalendarEvent)
buildCalendarEventIcon(
context: context,
presentationEmail: widget.presentationEmail
),
if (widget.presentationEmail.isMarkAsImportant && widget.isSenderImportantFlagEnabled)
buildMarkAsImportantIcon(context),
Expanded(
child: buildEmailTitle(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery
)
),
buildMailboxContain(
context,
widget.isSearchEmailRunning,
widget.presentationEmail
),
if (widget.presentationEmail.hasStarred)
Padding(
padding: const EdgeInsetsDirectional.only(start: 8),
child: buildIconStar(),
),
],
),
const SizedBox(height: 2),
_buildPartialContentMobile(context),
],
),
)
]
),
@@ -197,6 +189,8 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
isDrag: widget.isDrag,
isSenderImportantFlagEnabled: widget.isSenderImportantFlagEnabled,
shouldShowAIAction: _shouldShowAIAction,
autoWrapTagsByMaxWidth: widget.autoWrapTagsByMaxWidth,
labels: widget.labels,
padding: widget.padding,
searchQuery: widget.searchQuery,
mailboxContain: widget.mailboxContain,
@@ -320,7 +314,7 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
)
),
const SizedBox(width: 24),
Expanded(child: _buildSubjectAndContent()),
Expanded(child: _buildSubjectAndContent(context)),
const SizedBox(width: 16),
ValueListenableBuilder(
valueListenable: _hoverNotifier,
@@ -373,8 +367,9 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
return widget.mailboxContain?.isTrash == true || widget.mailboxContain?.isDrafts == true || widget.mailboxContain?.isSpam == true;
}
Widget _buildSubjectAndContent() {
Widget _buildSubjectAndContent(BuildContext context) {
final emailTitle = widget.presentationEmail.getEmailTitle();
final emailPartialContent = widget.presentationEmail.getPartialContent();
return Row(
children: [
@@ -391,78 +386,40 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
Expanded(
child: LayoutBuilder(
builder: (_, constraints) {
const horizontalPadding = 12.0;
final maxWidth = constraints.maxWidth;
double horizontalPadding = 12;
final maxWidthNoPadding = maxWidth - horizontalPadding * 2;
final labelTagsMaxWidth = emailTitle.isNotEmpty
? maxWidthNoPadding * 0.4
: maxWidthNoPadding * 0.5;
final hasLabels = widget.labels?.isNotEmpty == true;
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
final hasTitle = emailTitle.isNotEmpty;
final hasContent = emailPartialContent.isNotEmpty;
final emailTitleMaxWidth = widget.labels?.isNotEmpty == true
? maxWidthNoPadding * 0.4
: maxWidthNoPadding * 0.5;
final labelTagsMaxWidth =
hasTitle ? maxWidthNoPadding * 0.4 : maxWidthNoPadding * 0.5;
final emailTitleMaxWidth =
hasLabels ? maxWidthNoPadding * 0.4 : maxWidthNoPadding * 0.5;
return Row(
children: [
if (widget.labels?.isNotEmpty == true) ...[
Container(
constraints: BoxConstraints(
maxWidth: labelTagsMaxWidth,
),
padding: EdgeInsetsDirectional.only(
end: horizontalPadding,
),
child: LabelTagListWidget(tags: widget.labels!),
if (hasLabels)
_buildLabelsDesktop(
context: context,
maxWidth: labelTagsMaxWidth,
horizontalPadding: horizontalPadding,
isAutoWrap: isAutoWrap,
),
if (emailTitle.isNotEmpty)
Container(
constraints: BoxConstraints(
maxWidth: emailTitleMaxWidth,
),
padding: EdgeInsetsDirectional.only(
end: horizontalPadding,
),
child: buildEmailTitle(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
),
Expanded(
child: buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
if (hasTitle)
_buildTitleDesktop(
context: context,
maxWidth: emailTitleMaxWidth,
horizontalPadding: horizontalPadding,
hasContent: hasContent,
),
] else ...[
if (emailTitle.isNotEmpty)
Container(
constraints: BoxConstraints(
maxWidth: emailTitleMaxWidth,
),
padding: EdgeInsetsDirectional.only(
end: horizontalPadding,
),
child: buildEmailTitle(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
),
Expanded(
child: buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
),
],
if (hasContent)
_buildPartialContentDesktop(context),
],
);
},
@@ -472,6 +429,165 @@ class _EmailTileBuilderState extends State<EmailTileBuilder> with BaseEmailItem
);
}
Widget _buildLabelsDesktop({
required BuildContext context,
required double maxWidth,
required double horizontalPadding,
required bool isAutoWrap,
}) {
final labels = widget.labels;
if (labels == null || labels.isEmpty) return const SizedBox.shrink();
final child = LabelTagListWidget(
tags: labels,
autoWrapTagsByMaxWidth: widget.autoWrapTagsByMaxWidth,
isDesktop: responsiveUtils.isDesktop(context),
);
if (isAutoWrap) {
return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
child: child,
);
}
return Padding(
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
child: child,
);
}
Widget _buildTitleDesktop({
required BuildContext context,
required double maxWidth,
required double horizontalPadding,
bool hasContent = false,
}) {
if (hasContent) {
return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
child: buildEmailTitle(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
);
} else {
return Expanded(
child: Padding(
padding: EdgeInsetsDirectional.only(end: horizontalPadding),
child: buildEmailTitle(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
),
);
}
}
Widget _buildPartialContentDesktop(BuildContext context) {
return Expanded(
child: buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
);
}
Widget _buildPartialContentMobile(BuildContext context) {
final labels = widget.labels;
final hasLabels = labels?.isNotEmpty == true;
final hasContent = widget.presentationEmail.getPartialContent().isNotEmpty;
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
final isDesktop = responsiveUtils.isDesktop(context);
final partialContent = buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
);
if (!hasLabels) {
return Row(
children: [
if (hasContent)
Flexible(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
],
);
}
if (!hasContent) {
return Row(
children: [
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
Flexible(
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
}
if (!isAutoWrap) {
return Row(
children: [
Flexible(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
],
);
}
return LayoutBuilder(
builder: (_, constraints) {
return Row(
children: [
Expanded(child: partialContent),
if (_shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
},
);
}
Widget _buildAvatarIcon({
required BuildContext context,
bool isHovered = false,
@@ -3,12 +3,14 @@ import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:labels/model/label.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/base/widget/labels/ai_action_tag_widget.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_widget.dart';
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
import 'package:tmail_ui_user/features/thread/presentation/mixin/base_email_item_tile.dart';
import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart';
@@ -24,6 +26,8 @@ class WebTabletBodyEmailItemWidget extends StatefulWidget {
final bool isDrag;
final bool isSenderImportantFlagEnabled;
final bool shouldShowAIAction;
final bool autoWrapTagsByMaxWidth;
final List<Label>? labels;
final EdgeInsetsGeometry? padding;
final SearchQuery? searchQuery;
final PresentationMailbox? mailboxContain;
@@ -39,6 +43,8 @@ class WebTabletBodyEmailItemWidget extends StatefulWidget {
required this.isShowingEmailContent,
required this.isDrag,
required this.isSenderImportantFlagEnabled,
required this.autoWrapTagsByMaxWidth,
required this.labels,
required this.padding,
required this.searchQuery,
required this.mailboxContain,
@@ -102,6 +108,7 @@ class _WebTabletBodyEmailItemWidgetState
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
if (!widget.presentationEmail.hasRead)
@@ -158,22 +165,7 @@ class _WebTabletBodyEmailItemWidgetState
],
),
const SizedBox(height: 2),
Row(
children: [
Expanded(
child: buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
),
),
if (widget.shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
],
),
_buildPartialContent(context),
],
),
)
@@ -384,4 +376,92 @@ class _WebTabletBodyEmailItemWidgetState
});
}
}
}
Widget _buildPartialContent(BuildContext context) {
final labels = widget.labels;
final hasLabels = labels?.isNotEmpty == true;
final isAutoWrap = widget.autoWrapTagsByMaxWidth;
final isDesktop = responsiveUtils.isDesktop(context);
final hasContent = widget.presentationEmail.getPartialContent().isNotEmpty;
final partialContent = buildEmailPartialContent(
context,
widget.presentationEmail,
widget.isSearchEmailRunning,
widget.searchQuery,
);
if (!hasLabels) {
return Row(
children: [
if (hasContent)
Flexible(child: partialContent),
if (widget.shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
],
);
}
if (!hasContent) {
return Row(
children: [
if (widget.shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
Flexible(
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
}
if (!isAutoWrap) {
return Row(
children: [
Flexible(child: partialContent),
if (widget.shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
],
);
}
return LayoutBuilder(
builder: (_, constraints) {
return Row(
children: [
Expanded(child: partialContent),
if (widget.shouldShowAIAction)
const AiActionTagWidget(
margin: EdgeInsetsDirectional.only(start: 8),
),
const SizedBox(width: 12),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2),
child: LabelTagListWidget(
tags: labels!,
autoWrapTagsByMaxWidth: isAutoWrap,
isDesktop: isDesktop,
),
),
],
);
},
);
}
}