From eb017b54b15ad2b1e2075978381935085acda1d9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 25 Mar 2026 18:36:10 +0700 Subject: [PATCH] TF-4383 Handle read only labels --- labels/lib/extensions/label_extension.dart | 4 +++ labels/lib/labels.dart | 1 + labels/lib/model/label.dart | 3 +++ labels/lib/model/labels_capability.dart | 23 +++++++++++++++++ .../capability_properties_converter.dart | 3 +++ .../domain/extensions/session_extensions.dart | 21 ++++++++++++++++ .../labels/presentation/label_controller.dart | 19 ++++++++++---- .../presentation/base_mailbox_view.dart | 2 ++ .../widgets/labels/label_list_item.dart | 25 +++++++++++++++++-- .../widgets/labels/label_list_view.dart | 3 +++ 10 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 labels/lib/model/labels_capability.dart diff --git a/labels/lib/extensions/label_extension.dart b/labels/lib/extensions/label_extension.dart index ba4c99c6d..c2472a8b9 100644 --- a/labels/lib/extensions/label_extension.dart +++ b/labels/lib/extensions/label_extension.dart @@ -29,12 +29,15 @@ extension LabelExtension on Label { } } + bool get isReadOnly => readOnly == true; + Label copyWith({ Id? id, KeyWordIdentifier? keyword, String? displayName, HexColor? color, String? description, + bool? readOnly, }) { return Label( id: id ?? this.id, @@ -42,6 +45,7 @@ extension LabelExtension on Label { keyword: keyword ?? this.keyword, color: color ?? this.color, description: description ?? this.description, + readOnly: readOnly ?? this.readOnly, ); } diff --git a/labels/lib/labels.dart b/labels/lib/labels.dart index 05170bb3e..c0cfb1221 100644 --- a/labels/lib/labels.dart +++ b/labels/lib/labels.dart @@ -1,5 +1,6 @@ export 'package:labels/model/label.dart'; export 'package:labels/model/hex_color.dart'; +export 'package:labels/model/labels_capability.dart'; export 'package:labels/utils/labels_constants.dart'; diff --git a/labels/lib/model/label.dart b/labels/lib/model/label.dart index f64cf3e2c..9504ec824 100644 --- a/labels/lib/model/label.dart +++ b/labels/lib/model/label.dart @@ -24,6 +24,7 @@ class Label with EquatableMixin { final String? displayName; final HexColor? color; final String? description; + final bool? readOnly; Label({ this.id, @@ -31,6 +32,7 @@ class Label with EquatableMixin { this.displayName, this.color, this.description, + this.readOnly, }); factory Label.fromJson(Map json) => _$LabelFromJson(json); @@ -44,5 +46,6 @@ class Label with EquatableMixin { displayName, color, description, + readOnly, ]; } diff --git a/labels/lib/model/labels_capability.dart b/labels/lib/model/labels_capability.dart new file mode 100644 index 000000000..9be306a93 --- /dev/null +++ b/labels/lib/model/labels_capability.dart @@ -0,0 +1,23 @@ +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'labels_capability.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class LabelsCapability extends CapabilityProperties { + final int? version; + + LabelsCapability({this.version}); + + factory LabelsCapability.fromJson(Map json) => + _$LabelsCapabilityFromJson(json); + + Map toJson() => _$LabelsCapabilityToJson(this); + + static LabelsCapability deserialize(Map json) { + return LabelsCapability.fromJson(json); + } + + @override + List get props => [version]; +} diff --git a/lib/features/home/domain/converter/capability_properties_converter.dart b/lib/features/home/domain/converter/capability_properties_converter.dart index e9534cde6..2b1e761a8 100644 --- a/lib/features/home/domain/converter/capability_properties_converter.dart +++ b/lib/features/home/domain/converter/capability_properties_converter.dart @@ -10,6 +10,7 @@ import 'package:jmap_dart_client/jmap/core/capability/submission_capability.dart import 'package:jmap_dart_client/jmap/core/capability/vacation_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/web_socket_ticket_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; +import 'package:labels/model/labels_capability.dart'; import 'package:model/saas/saas_account_capability.dart'; import 'package:model/support/contact_support_capability.dart'; import 'package:scribe/scribe/ai/presentation/model/ai_capability.dart'; @@ -41,6 +42,8 @@ class CapabilityPropertiesConverter { return properties.toJson(); } else if (properties is AICapability) { return properties.toJson(); + } else if (properties is LabelsCapability) { + return properties.toJson(); } else if (properties is DefaultCapability) { return properties.properties; } else if (properties is EmptyCapability) { diff --git a/lib/features/home/domain/extensions/session_extensions.dart b/lib/features/home/domain/extensions/session_extensions.dart index 64e7f539a..cb12ef434 100644 --- a/lib/features/home/domain/extensions/session_extensions.dart +++ b/lib/features/home/domain/extensions/session_extensions.dart @@ -12,6 +12,8 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:labels/model/labels_capability.dart'; +import 'package:labels/utils/labels_constants.dart'; import 'package:model/download_all/download_all_capability.dart'; import 'package:model/mailbox/mailbox_constants.dart'; import 'package:model/model.dart'; @@ -36,6 +38,7 @@ extension SessionExtensions on Session { capabilityServerSettings: SettingsCapability.deserialize, linagoraSaaSCapability: SaaSAccountCapability.deserialize, AiScribeConstants.aiCapability: AICapability.fromJson, + LabelsConstants.labelsCapability: LabelsCapability.fromJson, }; Map toJson() { @@ -199,4 +202,22 @@ extension SessionExtensions on Session { return null; } } + + LabelsCapability? getLabelsCapability(AccountId accountId) { + try { + if (!LabelsConstants.labelsCapability.isSupported(this, accountId)) { + return null; + } + + final labelsCapability = getCapabilityProperties( + accountId, + LabelsConstants.labelsCapability, + ); + log('SessionExtensions::getLabelsCapability:labelsCapability = $labelsCapability'); + return labelsCapability; + } catch (e) { + logWarning('SessionExtensions::getLabelsCapability():[Exception] $e'); + return null; + } + } } \ No newline at end of file diff --git a/lib/features/labels/presentation/label_controller.dart b/lib/features/labels/presentation/label_controller.dart index 629b390c4..889552c64 100644 --- a/lib/features/labels/presentation/label_controller.dart +++ b/lib/features/labels/presentation/label_controller.dart @@ -7,19 +7,18 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; -import 'package:labels/extensions/list_label_extension.dart'; -import 'package:labels/model/label.dart'; -import 'package:labels/utils/labels_constants.dart'; +import 'package:labels/labels.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart'; -import 'package:tmail_ui_user/features/labels/domain/state/edit_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/delete_a_label_state.dart'; +import 'package:tmail_ui_user/features/labels/domain/state/edit_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/get_all_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart'; -import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/delete_a_label_interactor.dart'; +import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_label_changes_interactor.dart'; import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart'; @@ -53,6 +52,7 @@ class LabelController extends BaseController with LabelContextMenuMixin { State? _currentLabelState; AccountId? _accountId; Session? _session; + LabelsCapability? _labelsCapability; @override void onInit() { @@ -64,9 +64,17 @@ class LabelController extends BaseController with LabelContextMenuMixin { return LabelsConstants.labelsCapability.isSupported(session, accountId); } + LabelsCapability? get labelsCapability => _labelsCapability; + + bool get shouldAskReadOnly { + final labelVersion = labelsCapability?.version; + return labelVersion != null && labelVersion >= 1; + } + void checkLabelSettingState(Session session, AccountId accountId) { _session = session; _accountId = accountId; + _labelsCapability = _session?.getLabelsCapability(accountId); _getLabelSettingStateInteractor = getBinding(); if (_getLabelSettingStateInteractor != null) { @@ -245,6 +253,7 @@ class LabelController extends BaseController with LabelContextMenuMixin { _currentLabelState = null; _accountId = null; _session = null; + _labelsCapability = null; super.onClose(); } } diff --git a/lib/features/mailbox/presentation/base_mailbox_view.dart b/lib/features/mailbox/presentation/base_mailbox_view.dart index 2d9eb5493..ac93efb92 100644 --- a/lib/features/mailbox/presentation/base_mailbox_view.dart +++ b/lib/features/mailbox/presentation/base_mailbox_view.dart @@ -413,6 +413,7 @@ abstract class BaseMailboxView extends GetWidget if (isLabelAvailable) { final labelListExpandMode = labelController.labelListExpandMode.value; final labels = labelController.labels; + final shouldAskReadOnly = labelController.shouldAskReadOnly; return AnimatedContainer( duration: const Duration(milliseconds: 400), @@ -421,6 +422,7 @@ abstract class BaseMailboxView extends GetWidget labels: labels, imagePaths: controller.imagePaths, isDesktop: isDesktop, + shouldAskReadOnly: shouldAskReadOnly, labelIdSelected: labelIdSelected, isMobileResponsive: isMobileResponsive, onOpenLabelCallback: (label) => controller.openMailbox( diff --git a/lib/features/mailbox/presentation/widgets/labels/label_list_item.dart b/lib/features/mailbox/presentation/widgets/labels/label_list_item.dart index 8feaf3fbb..86685b161 100644 --- a/lib/features/mailbox/presentation/widgets/labels/label_list_item.dart +++ b/lib/features/mailbox/presentation/widgets/labels/label_list_item.dart @@ -16,6 +16,7 @@ class LabelListItem extends StatefulWidget { final ImagePaths imagePaths; final bool isDesktop; final bool isSelected; + final bool shouldAskReadOnly; final bool isMobileResponsive; final OnOpenLabelCallback onOpenLabelCallback; final OnOpenLabelContextMenuAction? onOpenContextMenu; @@ -28,6 +29,7 @@ class LabelListItem extends StatefulWidget { required this.onOpenLabelCallback, this.isDesktop = false, this.isSelected = false, + this.shouldAskReadOnly = false, this.isMobileResponsive = false, this.onOpenContextMenu, this.onLongPressLabelItemAction, @@ -75,7 +77,26 @@ class _LabelListItemState extends State { : MailboxItemWidgetStyles.mobileLabelIconSpace; } - bool get _isMenuButtonVisible => _isContextMenuVisible || _isItemHovered; + bool get _isMenuButtonVisible { + final isActive = _isContextMenuVisible || _isItemHovered; + + if (!widget.shouldAskReadOnly) { + return isActive; + } + + return isActive && !widget.label.isReadOnly; + } + + bool get _isOnLongPressActive { + final isTouchPlatform = + PlatformInfo.isWebTouchDevice || PlatformInfo.isMobile; + + if (!widget.shouldAskReadOnly) { + return isTouchPlatform; + } + + return isTouchPlatform && !widget.label.isReadOnly; + } @override Widget build(BuildContext context) { @@ -85,7 +106,7 @@ class _LabelListItemState extends State { borderRadius: _borderRadius, onHover: _handleHoverChanged, onTap: () => widget.onOpenLabelCallback(widget.label), - onLongPress: PlatformInfo.isWebTouchDevice || PlatformInfo.isMobile + onLongPress: _isOnLongPressActive ? () => widget.onLongPressLabelItemAction?.call(widget.label) : null, child: Container( diff --git a/lib/features/mailbox/presentation/widgets/labels/label_list_view.dart b/lib/features/mailbox/presentation/widgets/labels/label_list_view.dart index 5dd17c673..fb048ea17 100644 --- a/lib/features/mailbox/presentation/widgets/labels/label_list_view.dart +++ b/lib/features/mailbox/presentation/widgets/labels/label_list_view.dart @@ -9,6 +9,7 @@ class LabelListView extends StatelessWidget { final List