diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index 5f2985149..45d9d4619 100644
--- a/core/lib/utils/html/html_utils.dart
+++ b/core/lib/utils/html/html_utils.dart
@@ -214,6 +214,40 @@ class HtmlUtils {
})();''',
name: 'deleteSelectionContent');
+ static const saveSelection = (
+ script: '''
+ (() => {
+ const selection = window.getSelection();
+ if (selection && selection.rangeCount > 0) {
+ window._savedRange = selection.getRangeAt(0).cloneRange();
+ return true;
+ }
+ return false;
+ })();''',
+ name: 'saveSelection');
+
+ static const restoreSelection = (
+ script: '''
+ (() => {
+ if (window._savedRange) {
+ const selection = window.getSelection();
+ if (selection) {
+ selection.removeAllRanges();
+ selection.addRange(window._savedRange);
+ return true;
+ }
+ }
+ return false;
+ })();''',
+ name: 'restoreSelection');
+
+ static const clearSavedSelection = (
+ script: '''
+ (() => {
+ delete window._savedRange;
+ })();''',
+ name: 'clearSavedSelection');
+
static recalculateEditorHeight({double? maxHeight}) => (
script: '''
const editable = document.querySelector('.note-editable');
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 0304f1ccb..7b3e5c094 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
@@ -71,6 +71,83 @@ extension HandleAiScribeInComposerExtension on ComposerController {
}
}
+ Future saveSelection() async {
+ try {
+ if (PlatformInfo.isWeb) {
+ final result = await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.saveSelection.name,
+ hasReturnValue: true,
+ ) ?? false;
+ return result;
+ } else {
+ final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.saveSelection.script,
+ ) ?? false;
+ return result;
+ }
+ } catch (e) {
+ logError('$runtimeType::saveSelection:Exception = $e');
+ return false;
+ }
+ }
+
+ Future restoreSelection() async {
+ try {
+ if (PlatformInfo.isWeb) {
+ final result = await richTextWebController?.editorController.evaluateJavascriptWeb(
+ HtmlUtils.restoreSelection.name,
+ hasReturnValue: true,
+ ) ?? false;
+ return result;
+ } else {
+ final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
+ .evaluateJavascript(
+ source: HtmlUtils.restoreSelection.script,
+ ) ?? false;
+ return result;
+ }
+ } catch (e) {
+ logError('$runtimeType::restoreSelection:Exception = $e');
+ return false;
+ }
+ }
+
+ 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) {
+ logError('$runtimeType::clearSavedSelection:Exception = $e');
+ }
+ }
+
+ Future unfocusEditor() async {
+ final editorApi = richTextMobileTabletController?.htmlEditorApi;
+ if (PlatformInfo.isIOS) {
+ await editorApi?.unfocus();
+ } else if (PlatformInfo.isAndroid) {
+ await editorApi?.hideKeyboard();
+ await editorApi?.unfocus();
+ }
+ }
+
+ Future saveAndUnfocusForModal() async {
+ final saved = await saveSelection();
+ if (saved) {
+ await unfocusEditor();
+ }
+ }
+
void clearTextInEditor() {
try {
if (PlatformInfo.isWeb) {
@@ -92,6 +169,11 @@ extension HandleAiScribeInComposerExtension on ComposerController {
// 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 (PlatformInfo.isMobile) {
+ await restoreSelection();
+ }
+
if (selection == null || selection.isEmpty) {
clearTextInEditor();
}
@@ -103,6 +185,10 @@ extension HandleAiScribeInComposerExtension on ComposerController {
clearFocusRecipients();
clearFocusSubject();
+ if (PlatformInfo.isMobile) {
+ await saveAndUnfocusForModal();
+ }
+
final fullText = await _getTextOnlyContentInEditor();
await AiScribeModalManager.showAIScribeMenuModal(
@@ -129,6 +215,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 eb859653b..46b072066 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
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:core/utils/platform_info.dart';
import 'package:scribe/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart';
@@ -29,8 +30,12 @@ class ComposerAiScribeSelectionOverlay extends StatelessWidget {
});
}
- void _clearComposerInputFocus() {
+ Future _clearComposerInputFocus() async {
controller.clearFocusRecipients();
controller.clearFocusSubject();
+
+ if (PlatformInfo.isMobile) {
+ await controller.saveAndUnfocusForModal();
+ }
}
}
diff --git a/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart b/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart
index 06d3524ad..2ec3fa582 100644
--- a/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart
+++ b/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart
@@ -1,5 +1,6 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scribe/scribe.dart';
@@ -7,7 +8,7 @@ class InlineAiAssistButton extends StatelessWidget {
final ImagePaths imagePaths;
final String? selectedText;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
- final VoidCallback? onTapFallback;
+ final AsyncCallback? onTapFallback;
const InlineAiAssistButton({
super.key,
@@ -42,7 +43,7 @@ class InlineAiAssistButton extends StatelessWidget {
size = renderBox.size;
}
- onTapFallback?.call();
+ await onTapFallback?.call();
await AiScribeModalManager.showAIScribeMenuModal(
imagePaths: imagePaths,
diff --git a/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart b/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart
index ea0621c4d..ba148bee5 100644
--- a/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart
+++ b/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart
@@ -1,4 +1,5 @@
import 'package:core/presentation/resources/image_paths.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:scribe/scribe.dart';
@@ -15,7 +16,7 @@ class AiSelectionOverlay extends StatelessWidget {
final TextSelectionModel? selection;
final ImagePaths imagePaths;
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
- final VoidCallback? onTapFallback;
+ final AsyncCallback? onTapFallback;
@override
Widget build(BuildContext context) {