TF-4236 Implement remove a label from an email when click on the cross on a tag in the opened mail

This commit is contained in:
dab246
2026-01-09 16:37:09 +07:00
committed by Dat H. Pham
parent 3ef66bbbda
commit b37359af03
9 changed files with 145 additions and 11 deletions
@@ -11,6 +11,8 @@ class TagWidget extends StatelessWidget {
final bool isTruncateText;
final bool showTooltip;
final EdgeInsetsGeometry? margin;
final Widget? actionWidget;
final EdgeInsetsGeometry? padding;
const TagWidget({
super.key,
@@ -22,6 +24,8 @@ class TagWidget extends StatelessWidget {
this.textColor,
this.maxWidth,
this.margin,
this.actionWidget,
this.padding,
});
@override
@@ -42,16 +46,32 @@ class TagWidget extends StatelessWidget {
);
}
return Container(
if (actionWidget != null) {
labelText = Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: labelText),
actionWidget!,
],
);
}
labelText = Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
constraints:
maxWidth != null ? BoxConstraints(maxWidth: maxWidth!) : null,
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
padding: padding ?? EdgeInsets.symmetric(horizontal: horizontalPadding),
margin: margin,
child: labelText,
);
if (actionWidget != null) {
labelText = GestureDetector(onTap: () {}, child: labelText);
}
return labelText;
}
}
@@ -1,6 +1,7 @@
import 'package:jmap_dart_client/jmap/core/state.dart' as jmap;
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:labels/model/label.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
@@ -115,4 +116,14 @@ class TriggerMailViewKeyboardShortcutAction extends EmailUIAction {
@override
List<Object?> get props => [actionType, email];
}
}
class RemoveLabelFromEmailAction extends EmailUIAction {
RemoveLabelFromEmailAction(this.emailId, this.label);
final EmailId emailId;
final Label label;
@override
List<Object?> get props => [emailId, label];
}
@@ -357,6 +357,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
actionType: action.actionType,
email: action.email,
);
} else if (action is RemoveLabelFromEmailAction) {
mailboxDashBoardController.clearEmailUIAction();
if (_currentEmailId == null ||
action.emailId != _currentEmailId) {
return;
}
toggleLabelToEmail(action.emailId, action.label, false);
}
}));
@@ -275,8 +275,14 @@ class EmailView extends GetWidget<SingleEmailController> {
presentationEmail: presentationEmail.copyWith(
subject: threadSubject,
),
imagePaths: controller.imagePaths,
isMobileResponsive: isMobileResponsive,
labels: emailLabels,
onDeleteLabelAction: (label) => controller.toggleLabelToEmail(
presentationEmail.id!,
label,
false,
),
);
}),
Obx(() => InformationSenderAndReceiverBuilder(
@@ -1,28 +1,49 @@
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:labels/model/label.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_subject_styles.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_widget.dart';
class EmailSubjectWidget extends StatelessWidget {
typedef OnDeleteLabelAction = void Function(Label label);
class EmailSubjectWidget extends StatefulWidget {
final PresentationEmail presentationEmail;
final ImagePaths imagePaths;
final bool isMobileResponsive;
final List<Label>? labels;
final OnDeleteLabelAction? onDeleteLabelAction;
const EmailSubjectWidget({
super.key,
required this.presentationEmail,
required this.imagePaths,
this.isMobileResponsive = false,
this.labels,
this.onDeleteLabelAction,
});
String get _title => presentationEmail.getEmailTitle();
@override
State<EmailSubjectWidget> createState() => _EmailSubjectWidgetState();
}
bool get _hasLabels => labels?.isNotEmpty == true;
class _EmailSubjectWidgetState extends State<EmailSubjectWidget> {
String get _title => widget.presentationEmail.getEmailTitle();
bool get _hasLabels => _currentLabels?.isNotEmpty == true;
List<Label>? _currentLabels;
@override
void initState() {
super.initState();
_currentLabels = widget.labels;
}
@override
Widget build(BuildContext context) {
final padding = isMobileResponsive
final padding = widget.isMobileResponsive
? EmailSubjectStyles.mobilePadding
: EmailSubjectStyles.padding;
@@ -77,6 +98,35 @@ class EmailSubjectWidget extends StatelessWidget {
}
List<Widget> _buildLabelWidgets() {
return labels?.map(LabelWidget.create).toList() ?? const [];
return _currentLabels
?.map((label) => LabelWidget.create(
label: label,
removeLabelAction: _buildRemoveLabelWidget(label),
padding: const EdgeInsetsDirectional.only(start: 4, end: 2),
))
.toList() ??
const [];
}
Widget _buildRemoveLabelWidget(Label label) {
return TMailButtonWidget.fromIcon(
icon: widget.imagePaths.icDeleteSelection,
iconSize: 8,
iconColor: Colors.white,
padding: const EdgeInsets.all(6),
backgroundColor: Colors.transparent,
onTapActionCallback: () => _onDeleteLabelAction(label),
);
}
void _onDeleteLabelAction(Label labelRemoved) {
if (mounted) {
setState(() {
_currentLabels = _currentLabels
?.where((label) => label.id != labelRemoved.id)
.toList();
});
widget.onDeleteLabelAction?.call(labelRemoved);
}
}
}
@@ -9,6 +9,8 @@ class LabelWidget extends StatelessWidget {
final double horizontalPadding;
final double? maxWidth;
final bool isTruncateLabel;
final Widget? actionWidget;
final EdgeInsetsGeometry? padding;
const LabelWidget({
super.key,
@@ -16,9 +18,20 @@ class LabelWidget extends StatelessWidget {
this.horizontalPadding = 4,
this.maxWidth,
this.isTruncateLabel = false,
this.actionWidget,
this.padding,
});
factory LabelWidget.create(Label label) => LabelWidget(label: label);
factory LabelWidget.create({
required Label label,
required Widget removeLabelAction,
EdgeInsetsGeometry? padding,
}) =>
LabelWidget(
label: label,
actionWidget: removeLabelAction,
padding: padding,
);
@override
Widget build(BuildContext context) {
@@ -33,6 +46,8 @@ class LabelWidget extends StatelessWidget {
maxWidth: maxWidth,
isTruncateText: isTruncateLabel,
showTooltip: showTooltip,
actionWidget: actionWidget,
padding: padding,
);
}
}
@@ -2,10 +2,12 @@ import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:labels/model/label.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/labels/remove_label_from_email_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart';
@@ -46,7 +48,10 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
final isFirstEmailInThreadDetail = indexOfEmailId == 0;
if (presentationEmail.emailInThreadStatus == EmailInThreadStatus.collapsed) {
final lastEmail = emailIdsPresentation.values.last;
final emailLabels = _getLabelsForFirstEmail(
lastEmail: lastEmail,
isFirstEmailInThreadDetail: isFirstEmailInThreadDetail,
availableLabels: labels,
);
@@ -54,7 +59,7 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
return ThreadDetailCollapsedEmail(
presentationEmail: presentationEmail.copyWith(
subject: isFirstEmailInThreadDetail
? emailIdsPresentation.values.last?.subject
? lastEmail?.subject
: null
),
showSubject: isFirstEmailInThreadDetail,
@@ -72,6 +77,10 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail);
},
onDeleteLabelAction: (label) => removeLabelFromEmail(
lastEmail!.id!,
label,
),
);
}
@@ -112,6 +121,7 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
}
List<Label>? _getLabelsForFirstEmail({
required PresentationEmail? lastEmail,
required bool isFirstEmailInThreadDetail,
required List<Label>? availableLabels,
}) {
@@ -121,7 +131,6 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
return null;
}
final lastEmail = emailIdsPresentation.values.last;
return lastEmail?.getLabelList(availableLabels);
}
}
@@ -0,0 +1,12 @@
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension RemoveLabelFromEmailExtension on ThreadDetailController {
void removeLabelFromEmail(EmailId emailId, Label label) {
mailboxDashBoardController.dispatchEmailUIAction(
RemoveLabelFromEmailAction(emailId, label),
);
}
}
@@ -25,6 +25,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
this.labels,
this.onEmailActionClick,
this.onToggleThreadDetailCollapseExpand,
this.onDeleteLabelAction,
});
final PresentationEmail presentationEmail;
@@ -37,6 +38,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
final OnEmailActionClick? onEmailActionClick;
final List<Label>? labels;
final VoidCallback? onToggleThreadDetailCollapseExpand;
final OnDeleteLabelAction? onDeleteLabelAction;
String get preview => presentationEmail.getPartialContent();
@@ -63,8 +65,10 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
if (showSubject)
EmailSubjectWidget(
presentationEmail: presentationEmail,
imagePaths: imagePaths,
isMobileResponsive: isMobileResponsive,
labels: labels,
onDeleteLabelAction: onDeleteLabelAction,
),
InformationSenderAndReceiverBuilder(
emailSelected: presentationEmail,