diff --git a/assets/images/ic_arrow_back_ios.svg b/assets/images/ic_arrow_back_ios.svg
new file mode 100644
index 0000000000..621f359a71
--- /dev/null
+++ b/assets/images/ic_arrow_back_ios.svg
@@ -0,0 +1,3 @@
+
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 2faff10718..445a597f41 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -241,6 +241,7 @@ class ImagePaths {
String get icRefreshQuotas => _getImagePath('ic_refresh_quotas.svg');
String get icCreateFilter => _getImagePath('ic_create_filter.svg');
String get icArrowBack => _getImagePath('ic_arrow_back.svg');
+ String get icArrowBackIos => _getImagePath('ic_arrow_back_ios.svg');
String get icRadio => _getImagePath('ic_radio.svg');
String get icRadioSelected => _getImagePath('ic_radio_selected.svg');
String get icCheck => _getImagePath('ic_check.svg');
diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index 0fe3597e7d..ea1beb2f66 100644
--- a/core/lib/utils/html/html_utils.dart
+++ b/core/lib/utils/html/html_utils.dart
@@ -73,12 +73,15 @@ class HtmlUtils {
name: 'unregisterDropListener');
static ({String name, String script}) registerSelectionChangeListener(
- String viewId,
- ) =>
+ String viewId, {
+ bool isWebPlatform = false,
+ }) =>
(
script: '''
let lastSelectedText = '';
+ const isWebPlatform = $isWebPlatform;
+
const sendSelectionChangeMessage = (data) => {
// When iframe
if (window.parent) {
@@ -101,10 +104,17 @@ class HtmlUtils {
function getEditableFromSelection(selection) {
const node = selection?.focusNode || selection?.anchorNode;
const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
- return (
- el?.closest('.note-editor .note-editable') ||
- document.querySelector('.note-editor .note-editable')
- );
+
+ if (isWebPlatform) {
+ return (
+ el?.closest('.note-editor .note-editable') ||
+ document.querySelector('.note-editor .note-editable')
+ );
+ } else {
+ return (
+ el?.closest('#editor')
+ );
+ }
}
function clamp(v, min, max) {
@@ -142,9 +152,13 @@ class HtmlUtils {
}
const lastRect = rects[rects.length - 1];
-
- let x = lastRect.right - editableRect.left;
- let y = lastRect.bottom - editableRect.top;
+
+ // Avoid native selection marks in mobile
+ // Offset has been arbitrary determined to avoid selection marks on Android and iOS
+ const buttonOffset = isWebPlatform ? { x: 0, y: 0 } : { x: 24, y: -24 };
+
+ let x = lastRect.right - editableRect.left + buttonOffset.x;
+ let y = lastRect.bottom - editableRect.top + buttonOffset.y;
const isInside =
lastRect.bottom >= editableRect.top &&
@@ -183,7 +197,7 @@ class HtmlUtils {
script: '''
(() => {
const selection = window.getSelection();
- if (selection) {
+ if (selection && selection.rangeCount > 0) {
selection.collapseToEnd()
}
})();''',
@@ -200,6 +214,62 @@ class HtmlUtils {
})();''',
name: 'deleteSelectionContent');
+ static const saveSelection = (
+ script: '''
+ (() => {
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount > 0) {
+ window._savedRange = selection.getRangeAt(0).cloneRange();
+ const result = selection.toString()
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: saveSelection", result }), "*");
+ return result;
+ }
+ delete window._savedRange;
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: saveSelection", result: "" }), "*");
+ return "";
+ })();''',
+ name: 'saveSelection');
+
+ static const restoreSelection = (
+ script: '''
+ (() => {
+ if (window._savedRange) {
+ const selection = window.getSelection();
+ if (selection) {
+ selection.removeAllRanges();
+ selection.addRange(window._savedRange);
+ delete window._savedRange;
+ const result = selection.toString()
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: restoreSelection", result }), "*");
+ return result;
+ }
+ }
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: restoreSelection", result: "" }), "*");
+ return "";
+ })();''',
+ name: 'restoreSelection');
+
+ static const getSavedSelection = (
+ script: '''
+ (() => {
+ if(window._savedRange) {
+ const result = window._savedRange.toString();
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: getSavedSelection", result }), "*");
+ return result;
+ } else {
+ window.parent.postMessage(JSON.stringify({ "type": "toDart: getSavedSelection", result: "" }), "*");
+ return "";
+ }
+ })();''',
+ name: 'getSavedSelection');
+
+ static const clearSavedSelection = (
+ script: '''
+ (() => {
+ delete window._savedRange;
+ })();''',
+ name: 'clearSavedSelection');
+
static recalculateEditorHeight({double? maxHeight}) => (
script: '''
const editable = document.querySelector('.note-editable');
diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart
index b9ab697c30..158c7d0fbb 100644
--- a/core/lib/utils/string_convert.dart
+++ b/core/lib/utils/string_convert.dart
@@ -251,17 +251,15 @@ class StringConvert {
}
}
- static String convertTextContentToHtmlContent(String textContent) {
- // Escape HTML entities first to prevent interpretation as HTML
- final escapedContent = textContent
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll("'", ''');
+ static String escapeTextContent(String textContent) {
+ const HtmlEscape htmlEscape = HtmlEscape();
- final htmlContent = escapedContent.replaceAll('\n', '
');
+ return htmlEscape.convert(textContent);
+ }
+ static String convertTextContentToHtmlContent(String textContent) {
+ final escapedText = escapeTextContent(textContent);
+ final htmlContent = escapedText.replaceAll('\n', '
');
return '
$htmlContent
';
}
}
\ No newline at end of file
diff --git a/lib/features/base/mixin/ai_scribe_mixin.dart b/lib/features/base/mixin/ai_scribe_mixin.dart
index 6f422fd6ee..2e0c6b9028 100644
--- a/lib/features/base/mixin/ai_scribe_mixin.dart
+++ b/lib/features/base/mixin/ai_scribe_mixin.dart
@@ -1,5 +1,4 @@
import 'package:core/utils/app_logger.dart';
-import 'package:core/utils/platform_info.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:scribe/scribe.dart';
@@ -29,8 +28,6 @@ mixin AiScribeMixin {
void injectAIScribeBindings(Session? session, AccountId? accountId) {
try {
- if (PlatformInfo.isMobile) return;
-
final aiCapability = getAICapability(
session: session,
accountId: accountId,
diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart
index 1fe3c7fb6a..e51705a629 100644
--- a/lib/features/composer/presentation/composer_view.dart
+++ b/lib/features/composer/presentation/composer_view.dart
@@ -66,6 +66,9 @@ class ComposerView extends GetWidget {
controller.handleOpenContextMenu(context, position);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
+ onOpenAiAssistantModal: controller.isAIScribeAvailable
+ ? controller.openAIAssistantModal
+ : null,
attachFileAction: () => controller.openPickAttachmentMenu(
context,
_pickAttachmentsActionTiles(context)
@@ -87,6 +90,9 @@ class ComposerView extends GetWidget {
controller.handleOpenContextMenu(context, position);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
+ onOpenAiAssistantModal: controller.isAIScribeAvailable
+ ? controller.openAIAssistantModal
+ : null,
attachFileAction: () => controller.openPickAttachmentMenu(
context,
_pickAttachmentsActionTiles(context)
diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart
index 41c3682f62..ecfb57db9d 100644
--- a/lib/features/composer/presentation/composer_view_web.dart
+++ b/lib/features/composer/presentation/composer_view_web.dart
@@ -134,6 +134,9 @@ class ComposerView extends GetWidget {
saveToDraftsAction: () => controller.handleClickSaveAsDraftsButton(context),
saveToTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
deleteComposerAction: controller.handleClickDeleteComposer,
+ onOpenAiAssistantModal: controller.isAIScribeAvailable
+ ? controller.openAIAssistantModal
+ : null,
)),
ConstrainedBox(
constraints: BoxConstraints(
diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart
index 2b771be044..b70c718e0d 100644
--- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart
+++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart
@@ -19,6 +19,20 @@ class RichTextMobileTabletController extends GetxController {
Future get isEditorFocused async => await htmlEditorApi?.hasFocus() ?? false;
+ Future focus() async {
+ try {
+ await htmlEditorApi?.webViewController.evaluateJavascript(source: '''
+ (() => {
+ const editor = document.getElementById('editor');
+ if (editor && typeof editor.focus === 'function') {
+ editor.focus();
+ }
+ })();''');
+ } catch (e) {
+ logWarning('RichTextMobileTabletController::focus:Exception: $e');
+ }
+ }
+
void insertImage(InlineImage inlineImage) async {
final isFocused = await isEditorFocused;
log('RichTextMobileTabletController::insertImage: isEditorFocused = $isFocused');
diff --git a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart
index 3890ced0c8..c4c2377cc6 100644
--- a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart
+++ b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart
@@ -3,6 +3,7 @@ import 'package:core/utils/html/html_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/string_convert.dart';
import 'package:flutter/material.dart';
+import 'package:get/get.dart';
import 'package:scribe/scribe.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
@@ -18,9 +19,12 @@ extension HandleAiScribeInComposerExtension on ComposerController {
final isAIScribeConfigEnabled =
mailboxDashBoardController.cachedAIScribeConfig.value.isEnabled;
- return isAIScribeConfigEnabled &&
- isAIScribeEndpointAvailable &&
- !PlatformInfo.isMobile;
+ return isAIScribeConfigEnabled && isAIScribeEndpointAvailable;
+ }
+
+ bool get isScribeMobile {
+ final context = Get.context;
+ return AiScribeMobileUtils.isScribeInMobileMode(context);
}
Future _getTextOnlyContentInEditor() async {
@@ -36,9 +40,10 @@ extension HandleAiScribeInComposerExtension on ComposerController {
}
}
- Future insertTextInEditor(String text) async {
+ Future insertTextInEditor(String text) async {
try {
- final htmlContent = StringConvert.convertTextContentToHtmlContent(text);
+ final escapedText = StringConvert.escapeTextContent(text);
+ final htmlContent = StringConvert.convertTextContentToHtmlContent(escapedText);
if (PlatformInfo.isWeb) {
await richTextWebController?.editorController.evaluateJavascriptWeb(
@@ -48,13 +53,28 @@ extension HandleAiScribeInComposerExtension on ComposerController {
richTextWebController?.editorController.insertHtml(htmlContent);
} else {
- richTextMobileTabletController?.htmlEditorApi?.insertHtml(htmlContent);
+ await richTextMobileTabletController?.htmlEditorApi?.insertHtml(htmlContent);
}
} catch (e) {
logWarning('$runtimeType::insertTextInEditor:Exception = $e');
}
}
+ Future setTextInEditor(String text) async {
+ try {
+ final escapedText = StringConvert.escapeTextContent(text);
+ final htmlContent = StringConvert.convertTextContentToHtmlContent(escapedText);
+
+ if (PlatformInfo.isWeb) {
+ richTextWebController?.editorController.setText(htmlContent);
+ } else {
+ await richTextMobileTabletController?.htmlEditorApi?.setText(htmlContent);
+ }
+ } catch (e) {
+ logWarning('$runtimeType::setTextInEditor:Exception = $e');
+ }
+ }
+
Future collapseSelection() async {
try {
if (PlatformInfo.isWeb) {
@@ -73,41 +93,164 @@ extension HandleAiScribeInComposerExtension on ComposerController {
}
}
- void clearTextInEditor() {
+ Future saveSelection() async {
try {
if (PlatformInfo.isWeb) {
- richTextWebController?.editorController.setText('');
+ final result = await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.saveSelection.name,
+ hasReturnValue: true,
+ );
+ return result?.toString() ?? '';
} else {
- richTextMobileTabletController?.htmlEditorApi?.setText('');
+ final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.saveSelection.script,
+ );
+ return result?.toString() ?? '';
}
} catch (e) {
- logWarning('$runtimeType::clearTextInEditor:Exception = $e');
+ logWarning('$runtimeType::saveSelection:Exception = $e');
+ return '';
+ }
+ }
+
+ Future restoreSelection() async {
+ try {
+ if (PlatformInfo.isWeb) {
+ final result = await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.restoreSelection.name,
+ hasReturnValue: true,
+ );
+ return result?.toString() ?? '';
+ } else {
+ final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.restoreSelection.script,
+ );
+ return result?.toString() ?? '';
+ }
+ } catch (e) {
+ logWarning('$runtimeType::restoreSelection:Exception = $e');
+ return '';
+ }
+ }
+
+ Future getSavedSelection() async {
+ try {
+ if (PlatformInfo.isWeb) {
+ final result = await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.getSavedSelection.name,
+ hasReturnValue: true,
+ );
+ return result?.toString() ?? '';
+ } else {
+ final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.getSavedSelection.script,
+ );
+ return result?.toString() ?? '';
+ }
+ } catch (e) {
+ logWarning('$runtimeType::getSavedSelection:Exception = $e');
+ return '';
+ }
+ }
+
+
+ Future clearSavedSelection() async {
+ try {
+ if (PlatformInfo.isWeb) {
+ await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.clearSavedSelection.name,
+ hasReturnValue: false,
+ );
+ } else {
+ await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.clearSavedSelection.script,
+ );
+ }
+ } catch (e) {
+ logWarning('$runtimeType::clearSavedSelection:Exception = $e');
+ }
+ }
+
+ Future unfocusEditor() async {
+ try {
+ final editorApi = richTextMobileTabletController?.htmlEditorApi;
+ if (PlatformInfo.isIOS) {
+ await editorApi?.unfocus();
+ } else if (PlatformInfo.isAndroid) {
+ await editorApi?.hideKeyboard();
+ await editorApi?.unfocus();
+ }
+ } catch (e) {
+ logWarning('$runtimeType::unfocusEditor:Exception = $e');
+ }
+ }
+
+ Future saveAndUnfocusForModal() async {
+ await saveSelection();
+ await unfocusEditor();
+ }
+
+ Future ensureMobileEditorFocused() async {
+ try {
+ await richTextMobileTabletController?.focus();
+ } catch (e) {
+ logWarning('$runtimeType::ensureMobileEditorFocused:Exception = $e');
}
}
- // Ensure we only insert at cursor position by collapsing selection before inserting
Future onInsertTextCallback(String text) async {
+ if (PlatformInfo.isMobile) {
+ await ensureMobileEditorFocused();
+
+ await restoreSelection();
+ }
+
await collapseSelection();
+
await insertTextInEditor(text);
}
// If there is a selection, it will replace the selection, else it will replace everything
Future onReplaceTextCallback(String text) async {
final selection = editorTextSelection.value?.selectedText;
- if (selection == null || selection.isEmpty) {
- clearTextInEditor();
- }
- await insertTextInEditor(text);
+ final savedSelection = isScribeMobile ? await getSavedSelection() : "";
+
+ final shouldReplaceEverything = (selection == null || selection.isEmpty) && savedSelection.isEmpty;
+
+ if (shouldReplaceEverything) {
+ try {
+ await setTextInEditor(text);
+ } catch (e) {
+ logWarning('$runtimeType::onReplaceTextCallback:Exception = $e');
+ }
+ } else {
+ if (PlatformInfo.isMobile) {
+ await ensureMobileEditorFocused();
+
+ await restoreSelection();
+ }
+
+ await insertTextInEditor(text);
+ }
}
Future openAIAssistantModal(Offset? position, Size? size) async {
clearFocusRecipients();
clearFocusSubject();
+ final scribeMobile = isScribeMobile;
+ if (scribeMobile) {
+ await saveAndUnfocusForModal();
+ }
+
final fullText = await _getTextOnlyContentInEditor();
- await AiScribeModalManager.showAIScribeMenuModal(
+ await AiScribeModalManager.showAIScribeModal(
imagePaths: imagePaths,
availableCategories: AIScribeMenuCategory.values,
buttonPosition: position,
@@ -116,6 +259,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
preferredPlacement: ModalPlacement.top,
crossAxisAlignment: ModalCrossAxisAlignment.start,
onSelectAiScribeSuggestionAction: handleAiScribeSuggestionAction,
+ isScribeMobile: scribeMobile,
);
}
@@ -131,6 +275,10 @@ extension HandleAiScribeInComposerExtension on ComposerController {
await onInsertTextCallback(suggestionText);
break;
}
+
+ if (PlatformInfo.isMobile) {
+ await clearSavedSelection();
+ }
}
void handleTextSelection(TextSelectionData? textSelectionData) {
diff --git a/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart b/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart
index eb859653b9..93da1828b8 100644
--- a/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart
+++ b/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart
@@ -29,8 +29,12 @@ class ComposerAiScribeSelectionOverlay extends StatelessWidget {
});
}
- void _clearComposerInputFocus() {
+ Future _clearComposerInputFocus() async {
controller.clearFocusRecipients();
controller.clearFocusSubject();
+
+ if (controller.isScribeMobile) {
+ await controller.saveAndUnfocusForModal();
+ }
}
}
diff --git a/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
index f0159c87ec..4c8dfddd3a 100644
--- a/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
+++ b/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
@@ -1,6 +1,7 @@
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:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -17,6 +18,7 @@ class AppBarComposerWidget extends StatelessWidget {
final VoidCallback? insertImageAction;
final VoidCallback openRichToolbarAction;
final OnOpenContextMenuAction openContextMenuAction;
+ final OnOpenAiAssistantModal? onOpenAiAssistantModal;
const AppBarComposerWidget({
super.key,
@@ -29,6 +31,7 @@ class AppBarComposerWidget extends StatelessWidget {
this.isNetworkConnectionAvailable = false,
this.attachFileAction,
this.insertImageAction,
+ this.onOpenAiAssistantModal,
});
@override
@@ -48,6 +51,15 @@ class AppBarComposerWidget extends StatelessWidget {
onTapActionCallback: onCloseViewAction
),
const Spacer(),
+ if (onOpenAiAssistantModal != null)
+ AiAssistantButton(
+ imagePaths: imagePaths,
+ margin: const EdgeInsetsDirectional.only(
+ start: MobileAppBarComposerWidgetStyle.space,
+ end: MobileAppBarComposerWidgetStyle.space,
+ ),
+ onOpenAiAssistantModal: onOpenAiAssistantModal!,
+ ),
TMailButtonWidget.fromIcon(
icon: imagePaths.icRichToolbar,
iconColor: MobileAppBarComposerWidgetStyle.iconColor,
diff --git a/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
index 459dc9d87b..9018cb1a3f 100644
--- a/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
+++ b/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
@@ -1,6 +1,7 @@
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:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -16,6 +17,7 @@ class LandscapeAppBarComposerWidget extends StatelessWidget {
final VoidCallback? insertImageAction;
final VoidCallback openRichToolbarAction;
final OnOpenContextMenuAction openContextMenuAction;
+ final OnOpenAiAssistantModal? onOpenAiAssistantModal;
const LandscapeAppBarComposerWidget({
super.key,
@@ -28,6 +30,7 @@ class LandscapeAppBarComposerWidget extends StatelessWidget {
this.isNetworkConnectionAvailable = false,
this.attachFileAction,
this.insertImageAction,
+ this.onOpenAiAssistantModal,
});
@override
@@ -50,6 +53,15 @@ class LandscapeAppBarComposerWidget extends StatelessWidget {
onTapActionCallback: onCloseViewAction
),
const Spacer(),
+ if (onOpenAiAssistantModal != null)
+ AiAssistantButton(
+ imagePaths: imagePaths,
+ margin: const EdgeInsetsDirectional.only(
+ start: MobileAppBarComposerWidgetStyle.space,
+ end: MobileAppBarComposerWidgetStyle.space,
+ ),
+ onOpenAiAssistantModal: onOpenAiAssistantModal!,
+ ),
TMailButtonWidget.fromIcon(
icon: imagePaths.icRichToolbar,
iconColor: MobileAppBarComposerWidgetStyle.iconColor,
diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
index f5fb4170ce..640157555f 100644
--- a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
+++ b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
@@ -66,7 +66,7 @@ class _MobileEditorState extends State with TextSelectionMix
_editorController = editorApi.webViewController;
registerSelectionChange =
- HtmlUtils.registerSelectionChangeListener(_createdViewId);
+ HtmlUtils.registerSelectionChangeListener(_createdViewId, isWebPlatform: PlatformInfo.isWeb);
_editorController?.addJavaScriptHandler(
handlerName: registerSelectionChange!.name,
@@ -89,6 +89,10 @@ class _MobileEditorState extends State with TextSelectionMix
Future _onWebViewCreated(HtmlEditorApi editorApi) async {
widget.onCreatedEditorAction.call(context, editorApi, widget.content);
+ }
+
+ Future _onWebViewCompleted(HtmlEditorApi editorApi, WebUri? webUri) async {
+ widget.onLoadCompletedEditorAction(editorApi, webUri);
try {
await _setupSelectionListener(editorApi);
} catch (e) {
@@ -96,6 +100,7 @@ class _MobileEditorState extends State with TextSelectionMix
}
}
+
@override
Widget build(BuildContext context) {
return HtmlEditor(
@@ -109,7 +114,7 @@ class _MobileEditorState extends State with TextSelectionMix
useDefaultFontStyle: true,
),
onCreated: _onWebViewCreated,
- onCompleted: widget.onLoadCompletedEditorAction,
+ onCompleted: _onWebViewCompleted,
onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null,
);
}
diff --git a/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
index 46229fdeca..534d8d9875 100644
--- a/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
+++ b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart
@@ -1,6 +1,7 @@
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:pointer_interceptor/pointer_interceptor.dart';
import 'package:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -35,76 +36,80 @@ class TabletBottomBarComposerWidget extends StatelessWidget {
return Container(
padding: TabletBottomBarComposerWidgetStyle.padding,
color: TabletBottomBarComposerWidgetStyle.backgroundColor,
- child: Row(
- children: [
- const Spacer(),
- TMailButtonWidget.fromIcon(
- icon: imagePaths.icDeleteMailbox,
- borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
- padding: TabletBottomBarComposerWidgetStyle.iconPadding,
- iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
- tooltipMessage: AppLocalizations.of(context).delete,
- onTapActionCallback: deleteComposerAction,
- ),
- const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
- TMailButtonWidget.fromIcon(
- icon: imagePaths.icMarkAsImportant,
- borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
- padding: TabletBottomBarComposerWidgetStyle.iconPadding,
- iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
- iconColor: isMarkAsImportant
+ child: PointerInterceptor(
+ child: Row(
+ children: [
+ const Spacer(),
+ if (onOpenAiAssistantModal != null) ...[
+ AiAssistantButton(
+ imagePaths: imagePaths,
+ margin: const EdgeInsetsDirectional.only(
+ start: TabletBottomBarComposerWidgetStyle.space,
+ ),
+ onOpenAiAssistantModal: onOpenAiAssistantModal!,
+ ),
+ const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
+ ],
+ TMailButtonWidget.fromIcon(
+ icon: imagePaths.icDeleteMailbox,
+ borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
+ padding: TabletBottomBarComposerWidgetStyle.iconPadding,
+ iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
+ tooltipMessage: AppLocalizations.of(context).delete,
+ onTapActionCallback: deleteComposerAction,
+ ),
+ const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
+ TMailButtonWidget.fromIcon(
+ icon: imagePaths.icMarkAsImportant,
+ borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
+ padding: TabletBottomBarComposerWidgetStyle.iconPadding,
+ iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
+ iconColor: isMarkAsImportant
+ ? TabletBottomBarComposerWidgetStyle.selectedIconColor
+ : TabletBottomBarComposerWidgetStyle.iconColor,
+ tooltipMessage: isMarkAsImportant
+ ? AppLocalizations.of(context).turnOffMarkAsImportant
+ : AppLocalizations.of(context).turnOnMarkAsImportant,
+ onTapActionCallback: toggleMarkAsImportantAction,
+ ),
+ const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
+ TMailButtonWidget.fromIcon(
+ icon: imagePaths.icReadReceipt,
+ borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
+ padding: TabletBottomBarComposerWidgetStyle.iconPadding,
+ iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
+ iconColor: hasReadReceipt
? TabletBottomBarComposerWidgetStyle.selectedIconColor
: TabletBottomBarComposerWidgetStyle.iconColor,
- tooltipMessage: isMarkAsImportant
- ? AppLocalizations.of(context).turnOffMarkAsImportant
- : AppLocalizations.of(context).turnOnMarkAsImportant,
- onTapActionCallback: toggleMarkAsImportantAction,
- ),
- const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
- TMailButtonWidget.fromIcon(
- icon: imagePaths.icReadReceipt,
- borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
- padding: TabletBottomBarComposerWidgetStyle.iconPadding,
- iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
- iconColor: hasReadReceipt
- ? TabletBottomBarComposerWidgetStyle.selectedIconColor
- : TabletBottomBarComposerWidgetStyle.iconColor,
- tooltipMessage: hasReadReceipt
- ? AppLocalizations.of(context).turnOffRequestReadReceipt
- : AppLocalizations.of(context).turnOnRequestReadReceipt,
- onTapActionCallback: requestReadReceiptAction,
- ),
- if (onOpenAiAssistantModal != null)
- AiAssistantButton(
- imagePaths: imagePaths,
- margin: const EdgeInsetsDirectional.only(
- start: TabletBottomBarComposerWidgetStyle.space,
- ),
- onOpenAiAssistantModal: onOpenAiAssistantModal!,
+ tooltipMessage: hasReadReceipt
+ ? AppLocalizations.of(context).turnOffRequestReadReceipt
+ : AppLocalizations.of(context).turnOnRequestReadReceipt,
+ onTapActionCallback: requestReadReceiptAction,
+ ),
+ const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
+ TMailButtonWidget.fromIcon(
+ icon: imagePaths.icSaveToDraft,
+ borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
+ padding: TabletBottomBarComposerWidgetStyle.iconPadding,
+ iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
+ tooltipMessage: AppLocalizations.of(context).saveAsDraft,
+ onTapActionCallback: saveToDraftAction,
),
- const SizedBox(width: TabletBottomBarComposerWidgetStyle.space),
- TMailButtonWidget.fromIcon(
- icon: imagePaths.icSaveToDraft,
- borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius,
- padding: TabletBottomBarComposerWidgetStyle.iconPadding,
- iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
- tooltipMessage: AppLocalizations.of(context).saveAsDraft,
- onTapActionCallback: saveToDraftAction,
- ),
- const SizedBox(width: TabletBottomBarComposerWidgetStyle.sendButtonSpace),
- TMailButtonWidget(
- text: AppLocalizations.of(context).send,
- icon: imagePaths.icSend,
- iconAlignment: TextDirection.rtl,
- padding: TabletBottomBarComposerWidgetStyle.sendButtonPadding,
- iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
- iconSpace: TabletBottomBarComposerWidgetStyle.sendButtonIconSpace,
- textStyle: TabletBottomBarComposerWidgetStyle.sendButtonTextStyle,
- backgroundColor: TabletBottomBarComposerWidgetStyle.sendButtonBackgroundColor,
- borderRadius: TabletBottomBarComposerWidgetStyle.sendButtonRadius,
- onTapActionCallback: sendMessageAction,
- )
- ]
+ const SizedBox(width: TabletBottomBarComposerWidgetStyle.sendButtonSpace),
+ TMailButtonWidget(
+ text: AppLocalizations.of(context).send,
+ icon: imagePaths.icSend,
+ iconAlignment: TextDirection.rtl,
+ padding: TabletBottomBarComposerWidgetStyle.sendButtonPadding,
+ iconSize: TabletBottomBarComposerWidgetStyle.iconSize,
+ iconSpace: TabletBottomBarComposerWidgetStyle.sendButtonIconSpace,
+ textStyle: TabletBottomBarComposerWidgetStyle.sendButtonTextStyle,
+ backgroundColor: TabletBottomBarComposerWidgetStyle.sendButtonBackgroundColor,
+ borderRadius: TabletBottomBarComposerWidgetStyle.sendButtonRadius,
+ onTapActionCallback: sendMessageAction,
+ )
+ ]
+ ),
),
);
}
diff --git a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart
index 936fdef2f6..89b92b6968 100644
--- a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart
+++ b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart
@@ -4,6 +4,7 @@ import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/utils/platform_info.dart';
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
import 'package:flutter/material.dart';
+import 'package:scribe/scribe/ai/presentation/widgets/button/ai_assistant_button.dart';
import 'package:tmail_ui_user/features/base/widget/highlight_svg_icon_on_hover.dart';
import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu_overlay_widget.dart';
@@ -32,6 +33,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
final VoidCallback saveToTemplateAction;
final VoidCallback deleteComposerAction;
final VoidCallback toggleMarkAsImportantAction;
+ final OnOpenAiAssistantModal? onOpenAiAssistantModal;
const MobileResponsiveAppBarComposerWidget({
super.key,
@@ -55,6 +57,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
required this.saveToTemplateAction,
required this.deleteComposerAction,
required this.toggleMarkAsImportantAction,
+ this.onOpenAiAssistantModal,
});
@override
@@ -74,6 +77,14 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
onTapActionCallback: onCloseViewAction
),
const Spacer(),
+ if (onOpenAiAssistantModal != null)
+ AiAssistantButton(
+ imagePaths: imagePaths,
+ margin: const EdgeInsetsDirectional.only(
+ end: MobileAppBarComposerWidgetStyle.space,
+ ),
+ onOpenAiAssistantModal: onOpenAiAssistantModal!,
+ ),
TMailButtonWidget.fromIcon(
icon: imagePaths.icRichToolbar,
padding: MobileAppBarComposerWidgetStyle.richTextIconPadding,
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 c8ccd7a7e5..e675f0faa6 100644
--- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart
+++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/html/html_utils.dart';
+import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/text_selection_mixin.dart';
@@ -104,7 +105,7 @@ class _WebEditorState extends State with TextSelectionMixin {
_editorController = widget.editorController;
final registerSelectionChange =
- HtmlUtils.registerSelectionChangeListener(_createdViewId);
+ HtmlUtils.registerSelectionChangeListener(_createdViewId, isWebPlatform: PlatformInfo.isWeb);
_selectionChangeScript = WebScript(
name: registerSelectionChange.name,
script: registerSelectionChange.script,
@@ -204,6 +205,22 @@ class _WebEditorState extends State with TextSelectionMixin {
name: HtmlUtils.deleteSelectionContent.name,
script: HtmlUtils.deleteSelectionContent.script,
),
+ WebScript(
+ name: HtmlUtils.saveSelection.name,
+ script: HtmlUtils.saveSelection.script,
+ ),
+ WebScript(
+ name: HtmlUtils.restoreSelection.name,
+ script: HtmlUtils.restoreSelection.script,
+ ),
+ WebScript(
+ name: HtmlUtils.getSavedSelection.name,
+ script: HtmlUtils.getSavedSelection.script,
+ ),
+ WebScript(
+ name: HtmlUtils.clearSavedSelection.name,
+ script: HtmlUtils.clearSavedSelection.script,
+ ),
WebScript(
name: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).name,
script: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).script,
@@ -224,7 +241,7 @@ class _WebEditorState extends State with TextSelectionMixin {
_editorController.evaluateJavascriptWeb(
HtmlUtils.registerDropListener.name);
_editorController.evaluateJavascriptWeb(
- HtmlUtils.registerSelectionChangeListener(_createdViewId).name);
+ _selectionChangeScript.name);
_editorListenerRegistered = true;
}
},
diff --git a/lib/features/mailbox_dashboard/data/repository/linagora_ecosystem_repository_impl.dart b/lib/features/mailbox_dashboard/data/repository/linagora_ecosystem_repository_impl.dart
new file mode 100644
index 0000000000..9edcbc55cd
--- /dev/null
+++ b/lib/features/mailbox_dashboard/data/repository/linagora_ecosystem_repository_impl.dart
@@ -0,0 +1,15 @@
+import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/linagora_ecosystem_repository.dart';
+
+class LinagoraEcosystemRepositoryImpl extends LinagoraEcosystemRepository {
+ final LinagoraEcosystemApi _linagoraEcosystemApi;
+
+ LinagoraEcosystemRepositoryImpl(this._linagoraEcosystemApi);
+
+ @override
+ Future getLinagoraEcosystem(String baseUrl) {
+ return _linagoraEcosystemApi.getLinagoraEcosystem(baseUrl);
+ }
+
+}
\ No newline at end of file
diff --git a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart
index e031963aec..59edd6f025 100644
--- a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart
+++ b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart
@@ -28,6 +28,7 @@ class LinagoraEcosystemConverter {
LinagoraEcosystemIdentifier.linShare: AppLinagoraEcosystem.deserialize,
LinagoraEcosystemIdentifier.mobileApps: MobileAppsLinagoraEcosystemConverter.deserialize,
LinagoraEcosystemIdentifier.paywallURL: ApiUrlLinagoraEcosystem.deserialize,
+ LinagoraEcosystemIdentifier.scribePromptUrl: ApiUrlLinagoraEcosystem.deserialize,
});
}
diff --git a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart
index 6519a1d534..80cb52def7 100644
--- a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart
+++ b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart
@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/api_url_linagora_ecosystem.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/app_linagora_ecosystem.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart';
@@ -44,4 +45,7 @@ extension LinagoraEcosystemExtension on LinagoraEcosystem {
List get listAppLinagoraEcosystemOnAndroid {
return listAppLinagoraEcosystem.where((app) => app.isAppAndroidEnabled).toList();
}
+
+ String? get scribePromptUrl =>
+ (properties?[LinagoraEcosystemIdentifier.scribePromptUrl] as ApiUrlLinagoraEcosystem?)?.value;
}
\ No newline at end of file
diff --git a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart
index f4a5f38c4a..13306a28e8 100644
--- a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart
+++ b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart
@@ -12,6 +12,7 @@ class LinagoraEcosystemIdentifier with EquatableMixin {
static final twakeSync = LinagoraEcosystemIdentifier('Twake Sync');
static final linShare = LinagoraEcosystemIdentifier('LinShare');
static final paywallURL = LinagoraEcosystemIdentifier('paywallUrlTemplate');
+ static final scribePromptUrl = LinagoraEcosystemIdentifier('scribePromptUrl');
final String value;
diff --git a/lib/features/mailbox_dashboard/domain/repository/linagora_ecosystem_repository.dart b/lib/features/mailbox_dashboard/domain/repository/linagora_ecosystem_repository.dart
new file mode 100644
index 0000000000..19b550c4c8
--- /dev/null
+++ b/lib/features/mailbox_dashboard/domain/repository/linagora_ecosystem_repository.dart
@@ -0,0 +1,5 @@
+import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart';
+
+abstract class LinagoraEcosystemRepository {
+ Future getLinagoraEcosystem(String baseUrl);
+}
\ No newline at end of file
diff --git a/lib/features/mailbox_dashboard/domain/state/get_linagora_ecosystem_state.dart b/lib/features/mailbox_dashboard/domain/state/get_linagora_ecosystem_state.dart
new file mode 100644
index 0000000000..27736c1a0b
--- /dev/null
+++ b/lib/features/mailbox_dashboard/domain/state/get_linagora_ecosystem_state.dart
@@ -0,0 +1,21 @@
+import 'package:core/presentation/state/success.dart';
+import 'package:core/presentation/state/failure.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart';
+
+class GetLinagoraEcosystemSuccess extends Success {
+ final LinagoraEcosystem linagoraEcosystem;
+
+ GetLinagoraEcosystemSuccess(this.linagoraEcosystem);
+
+ @override
+ List