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: