diff --git a/assets/images/ic_insert_link.svg b/assets/images/ic_insert_link.svg new file mode 100644 index 000000000..15ee78cd2 --- /dev/null +++ b/assets/images/ic_insert_link.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_remove_link.svg b/assets/images/ic_remove_link.svg new file mode 100644 index 000000000..41e1728b3 --- /dev/null +++ b/assets/images/ic_remove_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_text.svg b/assets/images/ic_text.svg new file mode 100644 index 000000000..5442b1b23 --- /dev/null +++ b/assets/images/ic_text.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 93557e81e..12a480dc1 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -261,6 +261,8 @@ extension AppColor on Color { static const gray9AA7B6 = Color(0xFF9AA7B6); static const redFF3347 = Color(0xFFFF3347); static const gray686E76 = Color(0xFF686E76); + static const gray900 = Color(0xFF222222); + static const gray400 = Color(0xFF939393); static const lightGrayEBEDF0 = Color(0xFFEBEDF0); static const gray99A2AD = Color(0xFF99A2AD); static const textSecondary = Color(0xFF1C1B1F); diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index f62ec5738..a0f761fe9 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -257,6 +257,9 @@ class ImagePaths { String get icNavigation => _getImagePath('ic_navigation.svg'); String get icReading => _getImagePath('ic_reading.svg'); String get icMoveFolderContent => _getImagePath('ic_move_folder_content.svg'); + String get icInsertLink => _getImagePath('ic_insert_link.svg'); + String get icRemoveLink => _getImagePath('ic_remove_link.svg'); + String get icText => _getImagePath('ic_text.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index c3c2f1035..628a4ef83 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -12,6 +12,7 @@ import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wra import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart'; @@ -248,6 +249,7 @@ class ComposerView extends GetWidget { onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, height: constraintsEditor.maxHeight, horizontalPadding: ComposerStyle.mobileEditorHorizontalPadding, + linkOverlayOptions: controller.createLinkOverlayOptions(context), onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, onDragOver: controller.handleOnDragOverHtmlEditorWeb, onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( @@ -489,6 +491,7 @@ class ComposerView extends GetWidget { onEditorTextSizeChanged: controller.richTextWebController?.onEditorTextSizeChanged, height: constraintsEditor.maxHeight, horizontalPadding: ComposerStyle.desktopEditorHorizontalPadding, + linkOverlayOptions: controller.createLinkOverlayOptions(context), onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, onDragOver: controller.handleOnDragOverHtmlEditorWeb, onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( @@ -551,6 +554,7 @@ class ComposerView extends GetWidget { toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), + onOpenInsertLink: controller.openInsertLink, )), ], ), @@ -760,6 +764,7 @@ class ComposerView extends GetWidget { onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged, height: constraintsBody.maxHeight, horizontalPadding: ComposerStyle.desktopEditorHorizontalPadding, + linkOverlayOptions: controller.createLinkOverlayOptions(context), onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, onDragOver: controller.handleOnDragOverHtmlEditorWeb, onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( @@ -821,6 +826,7 @@ class ComposerView extends GetWidget { toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context), saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context), + onOpenInsertLink: controller.openInsertLink, )), ], ), diff --git a/lib/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart b/lib/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart new file mode 100644 index 000000000..0b6ed3793 --- /dev/null +++ b/lib/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart @@ -0,0 +1,89 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +extension HandleInsertLinkComposerExtension on ComposerController { + void openInsertLink() { + richTextWebController?.editorController.openInsertLinkDialog(); + } + + LinkOverlayOptions createLinkOverlayOptions(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + return LinkOverlayOptions( + tooltipOptions: LinkTooltipOverlayOptions( + linkPrefixLabel: '${appLocalizations.goTo}:', + editLinkLabel: appLocalizations.change, + removeLinkTooltipMessage: appLocalizations.removeLink, + linkPrefixLabelStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.gray900, + ), + linkLabelStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.blue700, + ), + editLinkLabelStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.primaryMain, + ), + removeLinkButtonBuilder: (_, onTap) => TMailButtonWidget.fromIcon( + icon: imagePaths.icRemoveLink, + iconSize: 20, + iconColor: AppColor.primaryLinShare, + backgroundColor: Colors.transparent, + onTapActionCallback: onTap, + ), + ), + editDialogOptions: LinkEditDialogOverlayOptions( + hintText: appLocalizations.text, + hintUrl: appLocalizations.typeOrPasteLink, + applyButtonLabel: appLocalizations.apply, + hintTextStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.steelGray400, + ), + inputTextStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.gray900, + ), + applyButtonTextStyle: (isEnabled) => ThemeUtils.textStyleM3LabelLarge( + color: isEnabled ? AppColor.primaryMain : AppColor.gray400, + ), + textPrefixIcon: SvgPicture.asset( + imagePaths.icText, + width: 24, + height: 24, + fit: BoxFit.fill, + ), + urlPrefixIcon: SvgPicture.asset( + imagePaths.icInsertLink, + width: 24, + height: 24, + fit: BoxFit.fill, + ), + backgroundColor: Colors.white, + inputBackgroundColor: Colors.white, + dialogPadding: const EdgeInsetsDirectional.only( + start: 16, + end: 12, + top: 16, + bottom: 16, + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + width: 1, + color: AppColor.m3Neutral90, + ), + ), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + width: 1, + color: AppColor.primaryMain, + ), + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index d66cf1d5d..f23dcf371 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -29,6 +29,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? height; final double? horizontalPadding; + final LinkOverlayOptions? linkOverlayOptions; final OnDragEnterListener? onDragEnter; final OnDragOverListener? onDragOver; final OnPasteImageSuccessAction? onPasteImageSuccessAction; @@ -51,6 +52,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.onEditorTextSizeChanged, this.height, this.horizontalPadding, + this.linkOverlayOptions, this.onDragEnter, this.onDragOver, this.onPasteImageSuccessAction, @@ -82,6 +84,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, @@ -113,6 +116,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, @@ -146,6 +150,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, @@ -185,6 +190,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, @@ -219,6 +225,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, @@ -243,6 +250,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onEditorTextSizeChanged: onEditorTextSizeChanged, height: height, horizontalPadding: horizontalPadding, + linkOverlayOptions: linkOverlayOptions, onDragEnter: onDragEnter, onDragOver: onDragOver, onPasteImageSuccessAction: onPasteImageSuccessAction, diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index c1f23d552..31dcc635e 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -30,6 +30,7 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback printDraftAction; final VoidCallback toggleMarkAsImportantAction; final VoidCallback saveAsTemplateAction; + final VoidCallback onOpenInsertLink; final OnMenuChanged? onPopupMenuChanged; const BottomBarComposerWidget({ @@ -52,6 +53,7 @@ class BottomBarComposerWidget extends StatelessWidget { required this.printDraftAction, required this.toggleMarkAsImportantAction, required this.saveAsTemplateAction, + required this.onOpenInsertLink, this.onPopupMenuChanged, }); @@ -110,6 +112,22 @@ class BottomBarComposerWidget extends StatelessWidget { ), ), const SizedBox(width: BottomBarComposerWidgetStyle.space), + AbsorbPointer( + absorbing: isCodeViewEnabled, + child: TMailButtonWidget.fromIcon( + icon: imagePaths.icInsertLink, + iconColor: isCodeViewEnabled + ? BottomBarComposerWidgetStyle.disabledIconColor + : BottomBarComposerWidgetStyle.iconColor, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).insertLink, + onTapActionCallback: onOpenInsertLink, + ), + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), PopupMenuOverlayWidget( controller: menuMoreOptionController, iconButton: HighlightSVGIconOnHover( diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index 5bccf2d43..04041cf08 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -39,6 +39,7 @@ class WebEditorWidget extends StatefulWidget { final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? height; final double? horizontalPadding; + final LinkOverlayOptions? linkOverlayOptions; final OnDragEnterListener? onDragEnter; final OnDragOverListener? onDragOver; final OnPasteImageSuccessAction? onPasteImageSuccessAction; @@ -60,6 +61,7 @@ class WebEditorWidget extends StatefulWidget { this.onEditorTextSizeChanged, this.height, this.horizontalPadding, + this.linkOverlayOptions, this.onDragEnter, this.onDragOver, this.onPasteImageSuccessAction, @@ -141,6 +143,9 @@ class _WebEditorState extends State { useDefaultFontStyle: true, customScrollbar: true, ), + useLinkTooltipOverlay: true, + linkOverlayOptions: widget.linkOverlayOptions + ?? const LinkOverlayOptions(), spellCheck: true, disableDragAndDrop: true, normalizeHtmlTextWhenDropping: true, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 88e8d6bde..a0d8d52e1 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-11-05T13:34:06.612241", + "@@last_modified": "2025-11-10T12:07:59.064737", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -5069,5 +5069,47 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "insertLink": "Insert link", + "@insertLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "goTo": "Go to", + "@goTo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "change": "Change", + "@change": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "removeLink": "Remove link", + "@removeLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "text": "Text", + "@text": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "typeOrPasteLink": "Type or paste a link", + "@typeOrPasteLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "apply": "Apply", + "@apply": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index a70d7ccb5..718f60a6e 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -5366,4 +5366,53 @@ class AppLocalizations { name: 'favoriteMailboxDisplayName', ); } + + String get insertLink { + return Intl.message( + 'Insert link', + name: 'insertLink', + ); + } + + String get goTo { + return Intl.message( + 'Go to', + name: 'goTo', + ); + } + + String get change { + return Intl.message( + 'Change', + name: 'change', + ); + } + + String get removeLink { + return Intl.message( + 'Remove link', + name: 'removeLink', + ); + } + + String get text { + return Intl.message( + 'Text', + name: 'text', + ); + } + + String get typeOrPasteLink { + return Intl.message( + 'Type or paste a link', + name: 'typeOrPasteLink', + ); + } + + String get apply { + return Intl.message( + 'Apply', + name: 'apply', + ); + } } diff --git a/pubspec.lock b/pubspec.lock index b69baf0a9..086e38ce5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1256,8 +1256,8 @@ packages: dependency: "direct main" description: path: "." - ref: main - resolved-ref: "33c6f89b1a2cf47f8c148939268488b2bbc19144" + ref: "feature/summernote-link-tooltip-overlay" + resolved-ref: "6c7ac2828e67158734aa5d6c62d4cc94ed2475dc" url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "3.3.0" @@ -1480,6 +1480,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + middle_ellipsis_text: + dependency: transitive + description: + name: middle_ellipsis_text + sha256: "70a628b9a152e6a933e518ca835a86a8dbff7a1db73521aeb36cfd02b04f36c1" + url: "https://pub.dev" + source: hosted + version: "1.0.0" mime: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 03f647203..e6a485260 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: html_editor_enhanced: git: url: https://github.com/linagora/html-editor-enhanced.git - ref: main + ref: feature/summernote-link-tooltip-overlay jmap_dart_client: git: