diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index ab4d28b65..a50a87243 100644
--- a/core/lib/utils/html/html_utils.dart
+++ b/core/lib/utils/html/html_utils.dart
@@ -48,7 +48,7 @@ class HtmlUtils {
editor.parentNode.replaceChild(newEditor, editor);''',
name: 'unregisterDropListener');
- static ({String name, String script})registerSelectionChangeListener(String viewId) => (
+ static registerSelectionChangeListener(String viewId) => (
script: '''
let lastSelectedText = '';
diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart
index 8e1b7b75a..f8021f027 100644
--- a/core/lib/utils/string_convert.dart
+++ b/core/lib/utils/string_convert.dart
@@ -199,20 +199,25 @@ class StringConvert {
}
static String convertHtmlContentToTextContent(String htmlContent) {
- final document = parse(htmlContent);
+ try {
+ final document = parse(htmlContent);
- // Each paragraph is surrounded by block tags so we add a /n for each block tag
- // Even
are surrounded by block tags so we can ignore
and treat them
- // as paragraph
- const blockTags = 'p, div, li, section, blockquote, article, header, footer, h1, h2, h3, h4, h5, h6';
+ // Each paragraph is surrounded by block tags so we add a /n for each block tag
+ // Even
are surrounded by block tags so we can ignore
and treat them
+ // as paragraph
+ const blockTags = 'p, div, li, section, blockquote, article, header, footer, h1, h2, h3, h4, h5, h6';
- document.querySelectorAll(blockTags).forEach((element) {
- element.append(Text('\n'));
- });
+ document.querySelectorAll(blockTags).forEach((element) {
+ element.append(Text('\n'));
+ });
- final String textContent = document.body?.text ?? '';
+ final String textContent = document.body?.text ?? '';
- return textContent.trim();
+ return textContent.trim();
+ } catch (e) {
+ logError('StringConvert::convertHtmlContentToTextContent:Exception = $e');
+ return htmlContent.trim();
+ }
}
static String convertTextContentToHtmlContent(String textContent) {
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 a9a872215..549465e31 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
@@ -35,7 +35,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
}
}
- void insertTextInEditor(String text) async {
+ Future insertTextInEditor(String text) async {
try {
final htmlContent = StringConvert.convertTextContentToHtmlContent(text);
@@ -87,18 +87,17 @@ extension HandleAiScribeInComposerExtension on ComposerController {
// Ensure we only insert at cursor position by collapsing selection before inserting
Future onInsertTextCallback(String text) async {
await collapseSelection();
-
- insertTextInEditor(text);
+ await insertTextInEditor(text);
}
// If there is a selection, it will replace the selection, else it will replace everything
- void onReplaceTextCallback(String text) {
+ Future onReplaceTextCallback(String text) async {
final selection = editorTextSelection.value?.selectedText;
if (selection == null || selection.isEmpty) {
clearTextInEditor();
}
- insertTextInEditor(text);
+ await insertTextInEditor(text);
}
Future openAIAssistantModal(Offset? position, Size? size) async {
@@ -116,16 +115,16 @@ extension HandleAiScribeInComposerExtension on ComposerController {
);
}
- void handleAiScribeSuggestionAction(
+ Future handleAiScribeSuggestionAction(
AiScribeSuggestionActions action,
String suggestionText,
- ) {
+ ) async {
switch (action) {
case AiScribeSuggestionActions.replace:
- onReplaceTextCallback(suggestionText);
+ await onReplaceTextCallback(suggestionText);
break;
case AiScribeSuggestionActions.insert:
- onInsertTextCallback(suggestionText);
+ await onInsertTextCallback(suggestionText);
break;
}
}
diff --git a/lib/features/composer/presentation/mixin/text_selection_mixin.dart b/lib/features/composer/presentation/mixin/text_selection_mixin.dart
index 24fa2bb92..12c4f2d0b 100644
--- a/lib/features/composer/presentation/mixin/text_selection_mixin.dart
+++ b/lib/features/composer/presentation/mixin/text_selection_mixin.dart
@@ -58,12 +58,21 @@ class TextSelectionCoordinates {
});
factory TextSelectionCoordinates.fromMap(Map data) {
- return TextSelectionCoordinates(
- x: (data['x'] as num?)?.toDouble() ?? 0.0,
- y: (data['y'] as num?)?.toDouble() ?? 0.0,
- width: (data['width'] as num?)?.toDouble() ?? 0.0,
- height: (data['height'] as num?)?.toDouble() ?? 0.0,
- );
+ try {
+ return TextSelectionCoordinates(
+ x: (data['x'] as num?)?.toDouble() ?? 0.0,
+ y: (data['y'] as num?)?.toDouble() ?? 0.0,
+ width: (data['width'] as num?)?.toDouble() ?? 0.0,
+ height: (data['height'] as num?)?.toDouble() ?? 0.0,
+ );
+ } catch (e) {
+ return const TextSelectionCoordinates(
+ x: 0.0,
+ y: 0.0,
+ width: 0.0,
+ height: 0.0,
+ );
+ }
}
Offset get position => Offset(x, y);
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 79daaa71a..2ece1ec3c 100644
--- a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
+++ b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
@@ -1,5 +1,6 @@
import 'package:core/presentation/constants/constants_ui.dart';
+import 'package:core/utils/app_logger.dart';
import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/html/html_utils.dart';
@@ -36,7 +37,6 @@ class MobileEditorWidget extends StatefulWidget {
class _MobileEditorState extends State with TextSelectionMixin {
- HtmlEditorApi? _editorApi;
late String _createdViewId;
@override
@@ -48,9 +48,8 @@ class _MobileEditorState extends State with TextSelectionMix
@override
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
- void _setupSelectionListener() async {
- final webViewController = _editorApi?.webViewController;
- if (webViewController == null) return;
+ Future _setupSelectionListener(HtmlEditorApi editorApi) async {
+ final webViewController = editorApi.webViewController;
final registerSelectionChange =
HtmlUtils.registerSelectionChangeListener(_createdViewId);
@@ -61,8 +60,10 @@ class _MobileEditorState extends State with TextSelectionMix
if (!mounted) return;
if (args.isNotEmpty) {
- final data = args[0] as Map;
- handleSelectionChange(data);
+ final rawData = args[0];
+ if (rawData is Map) {
+ handleSelectionChange(rawData);
+ }
}
},
);
@@ -72,10 +73,13 @@ class _MobileEditorState extends State with TextSelectionMix
);
}
- @override
- void dispose() {
- _editorApi = null;
- super.dispose();
+ Future _onWebViewCreated(HtmlEditorApi editorApi) async {
+ widget.onCreatedEditorAction.call(context, editorApi, widget.content);
+ try {
+ await _setupSelectionListener(editorApi);
+ } catch (e) {
+ logError('Error onWebViewCreated: $e');
+ }
}
@override
@@ -90,11 +94,7 @@ class _MobileEditorState extends State with TextSelectionMix
direction: widget.direction,
useDefaultFontStyle: true,
),
- onCreated: (editorApi) {
- _editorApi = editorApi;
- widget.onCreatedEditorAction.call(context, editorApi, widget.content);
- _setupSelectionListener();
- },
+ onCreated: _onWebViewCreated,
onCompleted: widget.onLoadCompletedEditorAction,
onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null,
);
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 bd97f20f6..e78c2f806 100644
--- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart
+++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart
@@ -92,6 +92,7 @@ class _WebEditorState extends State with TextSelectionMixin {
bool _signatureTooltipReady = false;
late String _createdViewId;
+ late WebScript _selectionChangeScript;
@override
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
@@ -103,7 +104,11 @@ class _WebEditorState extends State with TextSelectionMixin {
_editorController = widget.editorController;
final registerSelectionChange =
- HtmlUtils.registerSelectionChangeListener(_createdViewId);
+ HtmlUtils.registerSelectionChangeListener(_createdViewId);
+ _selectionChangeScript = WebScript(
+ name: registerSelectionChange.name,
+ script: registerSelectionChange.script,
+ );
_editorListener = (event) {
try {
@@ -112,7 +117,7 @@ class _WebEditorState extends State with TextSelectionMixin {
if (data['name'] == HtmlUtils.registerDropListener.name) {
_editorController.evaluateJavascriptWeb(HtmlUtils.removeLineHeight1px.name);
- } else if (data['name'] == registerSelectionChange.name
+ } else if (data['name'] == _selectionChangeScript.name
&& data['viewId'] == _createdViewId) {
handleSelectionChange(data);
}
@@ -123,9 +128,8 @@ class _WebEditorState extends State with TextSelectionMixin {
);
}
};
- if (_editorListener != null) {
- window.addEventListener("message", _editorListener!);
- }
+
+ window.addEventListener("message", _editorListener!);
}
@override
@@ -189,8 +193,8 @@ class _WebEditorState extends State with TextSelectionMixin {
script: HtmlUtils.unregisterDropListener.script,
),
WebScript(
- name: HtmlUtils.registerSelectionChangeListener(_createdViewId).name,
- script: HtmlUtils.registerSelectionChangeListener(_createdViewId).script,
+ name: _selectionChangeScript.name,
+ script: _selectionChangeScript.script,
),
WebScript(
name: HtmlUtils.collapseSelectionToEnd.name,
diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
index d9b18172f..7e03ade3c 100644
--- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
+++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
@@ -309,7 +309,7 @@ class MailboxDashBoardController extends ReloadableController
final isPopupMenuOpened = RxBool(false);
final octetsQuota = Rxn();
final isTextFormattingMenuOpened = RxBool(false);
- final cachedAIScribeConfig = Rx(AIScribeConfig.initial());
+ final cachedAIScribeConfig = AIScribeConfig.initial().obs;
Map mapDefaultMailboxIdByRole = {};
Map mapMailboxById = {};
diff --git a/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart b/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart
index 8994a9cfd..09dd63546 100644
--- a/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart
+++ b/lib/features/manage_account/presentation/preferences/bindings/preferences_interactors_bindings.dart
@@ -6,7 +6,6 @@ import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_
import 'package:tmail_ui_user/features/manage_account/data/local/preferences_setting_manager.dart';
import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart';
import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart';
-import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_ai_scribe_config_interactor.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/save_language_interactor.dart';
import 'package:tmail_ui_user/features/server_settings/data/datasource/server_settings_data_source.dart';
import 'package:tmail_ui_user/features/server_settings/data/datasource_impl/remote_server_settings_data_source_impl.dart';
@@ -102,6 +101,5 @@ class PreferencesInteractorsBindings extends InteractorsBindings {
Get.delete(tag: composerId);
Get.delete(tag: composerId);
Get.delete(tag: composerId);
- Get.delete(tag: composerId);
}
}
\ No newline at end of file
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb
index 1a9197881..c1fcb50b6 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -3274,5 +3274,23 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
+ },
+ "aiScribe": "AI Scribe",
+ "@aiScribe": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "aiScribeSettingExplanation": "Use AI to help write and improve your emails",
+ "@aiScribeSettingExplanation": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
+ "aiScribeToggleDescription": "Enable AI Scribe",
+ "@aiScribeToggleDescription": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
}
}
diff --git a/scribe/lib/scribe/ai/data/model/ai_message.dart b/scribe/lib/scribe/ai/data/model/ai_message.dart
index 67d2482a8..eb8e6d571 100644
--- a/scribe/lib/scribe/ai/data/model/ai_message.dart
+++ b/scribe/lib/scribe/ai/data/model/ai_message.dart
@@ -4,6 +4,8 @@ part 'ai_message.g.dart';
@JsonSerializable()
class AIMessage {
+ static const String aiUserRole = 'user';
+
final String role;
final String content;
@@ -18,7 +20,7 @@ class AIMessage {
Map toJson() => _$AIMessageToJson(this);
factory AIMessage.ofUser(String content) => AIMessage(
- role: 'user',
+ role: aiUserRole,
content: content,
);
}
diff --git a/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart b/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart
index 9e6887c94..a0eafc2ed 100644
--- a/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart
+++ b/scribe/lib/scribe/ai/domain/constants/ai_prompts.dart
@@ -1,17 +1,19 @@
import 'package:scribe/scribe/ai/presentation/model/ai_action.dart';
import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart';
-const PERFORM_TASK = "Perform only the following task:";
-const PRESERVE_LANGUAGE_PROMPT = "Do not translate. Strictly keep the original language of the input text. For example, if it's French, keep French. If it's English, keep English.";
-const DO_NOT_ADD_INFO_PROMPT = "Do not add any extra information or interpret anything beyond the explicit task.";
-
-// TODO
-// In a near future, prompts will be loaded from a remote source
class AIPrompts {
+ static const _performTask = "Perform only the following task:";
+ static const _preserveLanguagePrompt =
+ "Do not translate. Strictly keep the original language of the input text. For example, if it's French, keep French. If it's English, keep English.";
+ static const _doNotAddInfoPrompt =
+ "Do not add any extra information or interpret anything beyond the explicit task.";
+
static String buildPrompt(AIAction action, String? text) {
return switch (action) {
PredefinedAction(action: final menuAction) =>
- buildPredefinedPrompt(menuAction, text ?? ''),
+ text?.trim().isNotEmpty == true
+ ? buildPredefinedPrompt(menuAction, text!)
+ : throw ArgumentError('Text cannot be empty for predefined actions'),
CustomPromptAction(prompt: final customPrompt) =>
buildCustomPrompt(customPrompt, text),
};
@@ -47,31 +49,31 @@ class AIPrompts {
}
static String improveMakeShorter(String text) {
- return '$PERFORM_TASK make the text shorter but preserve the meaning. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask make the text shorter but preserve the meaning. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String improveExpandContext(String text) {
- return '$PERFORM_TASK expand the context of the text to make it more detailed and comprehensive. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask expand the context of the text to make it more detailed and comprehensive. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String improveEmojify(String text) {
- return '$PERFORM_TASK add emojis to the important parts of the text. Do not try to rephrase or replace text. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask add emojis to the important parts of the text. Do not try to rephrase or replace text. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String improveTransformToBullets(String text) {
- return '$PERFORM_TASK transform the text in a bullet list. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask transform the text into a bullet list. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String correctGrammar(String text) {
- return '$PERFORM_TASK correct grammar and spelling. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask correct grammar and spelling. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String changeToneTo(String text, String tone) {
- return '$PERFORM_TASK change the tone to be $tone. $PRESERVE_LANGUAGE_PROMPT $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask change the tone to be $tone. $_preserveLanguagePrompt $_doNotAddInfoPrompt Text:\n\n$text';
}
static String translateTo(String text, String language) {
- return '$PERFORM_TASK translate. Translate the text to the specified language: $language. $DO_NOT_ADD_INFO_PROMPT Text:\n\n$text';
+ return '$_performTask translate. Translate the text to the specified language: $language. $_doNotAddInfoPrompt Text:\n\n$text';
}
static String buildCustomPrompt(String customPrompt, String? text) {
diff --git a/scribe/lib/scribe/ai/domain/model/ai_response.dart b/scribe/lib/scribe/ai/domain/model/ai_response.dart
index 2f73960f2..91379ad2b 100644
--- a/scribe/lib/scribe/ai/domain/model/ai_response.dart
+++ b/scribe/lib/scribe/ai/domain/model/ai_response.dart
@@ -2,16 +2,4 @@ class AIResponse {
final String result;
const AIResponse({required this.result});
-
- factory AIResponse.fromJson(Map json) {
- return AIResponse(
- result: json['result'] as String? ?? json['text'] as String? ?? '',
- );
- }
-
- Map toJson() {
- return {
- 'result': result,
- };
- }
}
diff --git a/scribe/lib/scribe/ai/l10n/intl_ru.arb b/scribe/lib/scribe/ai/l10n/intl_ru.arb
index 8e1e9fc16..c67135054 100644
--- a/scribe/lib/scribe/ai/l10n/intl_ru.arb
+++ b/scribe/lib/scribe/ai/l10n/intl_ru.arb
@@ -114,7 +114,7 @@
"placeholders_order": [],
"placeholders": {}
},
- "replaceButton": "заменить",
+ "replaceButton": "Заменить",
"@replaceButton": {
"type": "text",
"placeholders_order": [],
diff --git a/scribe/lib/scribe/ai/presentation/model/text_selection_model.dart b/scribe/lib/scribe/ai/presentation/model/text_selection_model.dart
index 995f91433..10a04378f 100644
--- a/scribe/lib/scribe/ai/presentation/model/text_selection_model.dart
+++ b/scribe/lib/scribe/ai/presentation/model/text_selection_model.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/cupertino.dart';
+import 'dart:ui' show Offset;
class TextSelectionModel {
final String? selectedText;
diff --git a/scribe/lib/scribe/ai/presentation/utils/context_menu/hover_submenu_controller.dart b/scribe/lib/scribe/ai/presentation/utils/context_menu/hover_submenu_controller.dart
index 43b04f7a3..d6eebd591 100644
--- a/scribe/lib/scribe/ai/presentation/utils/context_menu/hover_submenu_controller.dart
+++ b/scribe/lib/scribe/ai/presentation/utils/context_menu/hover_submenu_controller.dart
@@ -1,6 +1,6 @@
import 'dart:async';
-import 'package:flutter/cupertino.dart';
+import 'package:flutter/widgets.dart';
typedef OnHoverShowSubmenu = void Function(GlobalKey key);
diff --git a/scribe/lib/scribe/ai/presentation/utils/modal/anchored_modal_layout_calculator.dart b/scribe/lib/scribe/ai/presentation/utils/modal/anchored_modal_layout_calculator.dart
index a968977ec..f42e60c97 100644
--- a/scribe/lib/scribe/ai/presentation/utils/modal/anchored_modal_layout_calculator.dart
+++ b/scribe/lib/scribe/ai/presentation/utils/modal/anchored_modal_layout_calculator.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/animation.dart';
+import 'dart:ui';
import 'package:scribe/scribe.dart';
class AnchoredModalLayoutCalculator {
@@ -201,21 +201,22 @@ class AnchoredModalLayoutCalculator {
switch (placement) {
case ModalPlacement.right:
left = anchorPosition.dx + anchorSize.width + gap;
- bottom = anchorPosition.dy + verticalOffset;
+ bottom = screenSize.height - (anchorPosition.dy + verticalOffset);
break;
case ModalPlacement.bottom:
left = anchorPosition.dx;
- bottom = anchorPosition.dy + anchorSize.height + gap + verticalOffset;
+ bottom = screenSize.height - (anchorPosition.dy + anchorSize.height + gap + verticalOffset);
break;
case ModalPlacement.top:
left = anchorPosition.dx;
- bottom = anchorPosition.dy - menuSize.height - gap + verticalOffset;
+ bottom = screenSize.height - (anchorPosition.dy - menuSize.height - gap + verticalOffset);
break;
+
case ModalPlacement.left:
left = anchorPosition.dx - menuSize.width - gap;
- bottom = anchorPosition.dy + verticalOffset;
+ bottom = screenSize.height - (anchorPosition.dy + verticalOffset);
break;
}
diff --git a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
index bb359365c..84389d372 100644
--- a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
+++ b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
@@ -66,6 +66,8 @@ class _AiScribeSuggestionWidgetState extends State {
widget.content,
);
+ if (!mounted) return;
+
result.fold(
(failure) => _state.value = dartz.Left(failure),
(success) => _state.value = dartz.Right(success),
@@ -203,9 +205,18 @@ class _AiScribeSuggestionWidgetState extends State {
widget.buttonPosition != null && widget.buttonSize != null;
void _handleClickOutside() {
- final result = _state.value.getOrElse(() => UIState.idle);
- if (result is GenerateAITextSuccess || result is GenerateAITextFailure) {
+ final shouldDismiss = _state.value.fold(
+ (failure) => failure is GenerateAITextFailure,
+ (success) => success is GenerateAITextSuccess,
+ );
+ if (shouldDismiss) {
Navigator.of(context).pop();
}
}
+
+ @override
+ void dispose() {
+ _state.dispose();
+ super.dispose();
+ }
}
diff --git a/scribe/lib/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart b/scribe/lib/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart
index 5e1999a91..9a9ed8365 100644
--- a/scribe/lib/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart
+++ b/scribe/lib/scribe/ai/presentation/widgets/modal/suggestion/ai_scribe_suggestion_loading.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:flutter/material.dart';
@@ -110,21 +112,18 @@ class _AnimatedEllipsisText extends StatefulWidget {
class _AnimatedEllipsisTextState extends State<_AnimatedEllipsisText> {
static const _dotStates = ['', '.', '..', '...'];
int _index = 0;
+ Timer? _timer;
@override
void initState() {
super.initState();
- _tick();
- }
-
- void _tick() {
- Future.delayed(const Duration(milliseconds: 500), () {
+ _timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
if (!mounted) return;
setState(() => _index = (_index + 1) % _dotStates.length);
- _tick();
});
}
+
@override
Widget build(BuildContext context) {
return Text(
@@ -132,4 +131,10 @@ class _AnimatedEllipsisTextState extends State<_AnimatedEllipsisText> {
style: widget.style,
);
}
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
}
diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh
index 8293f779a..752055ef9 100755
--- a/scripts/prebuild.sh
+++ b/scripts/prebuild.sh
@@ -27,7 +27,8 @@ dart run build_runner build --delete-conflicting-outputs &&
dart run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main/localizations/app_localizations.dart lib/l10n/intl*.arb
# For scribe module localizations
-cd scribe
-dart run intl_generator:extract_to_arb --output-dir=./lib/scribe/ai/l10n lib/scribe/ai/localizations/scribe_localizations.dart &&
- dart run intl_generator:generate_from_arb --output-dir=lib/scribe/ai/l10n --no-use-deferred-loading lib/scribe/ai/localizations/scribe_localizations.dart lib/scribe/ai/l10n/intl*.arb
-cd ../
+(
+ cd scribe
+ dart run intl_generator:extract_to_arb --output-dir=./lib/scribe/ai/l10n lib/scribe/ai/localizations/scribe_localizations.dart &&
+ dart run intl_generator:generate_from_arb --output-dir=lib/scribe/ai/l10n --no-use-deferred-loading lib/scribe/ai/localizations/scribe_localizations.dart lib/scribe/ai/l10n/intl*.arb
+)
diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart
index d47551ca9..1bb31c6b8 100644
--- a/test/features/composer/presentation/composer_controller_test.dart
+++ b/test/features/composer/presentation/composer_controller_test.dart
@@ -25,7 +25,6 @@ import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
-import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart';
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
import 'package:tmail_ui_user/features/caching/caching_manager.dart';
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
@@ -147,7 +146,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon
RxBool get isPopupMenuOpened => false.obs;
@override
- Rx get cachedAIScribeConfig => Rx(AIScribeConfig.initial());
+ Rx get cachedAIScribeConfig => AIScribeConfig.initial().obs;
}
@GenerateNiceMocks([
@@ -183,7 +182,6 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon
MockSpec(),
MockSpec(),
MockSpec(),
- MockSpec(),
// Additional Getx dependencies mock specs
MockSpec(fallbackGenerators: fallbackGenerators),