Skip to content

[Scribe mobile] Add and enable Scribe mobile#4245

Merged
hoangdat merged 16 commits intoscribe-mobilefrom
feat/scribe-mobile
Feb 2, 2026
Merged

[Scribe mobile] Add and enable Scribe mobile#4245
hoangdat merged 16 commits intoscribe-mobilefrom
feat/scribe-mobile

Conversation

@zatteo
Copy link
Member

@zatteo zatteo commented Jan 12, 2026

Before merging the Scribe desktop PR, we left the scribe as is (unfinished) after disabling it.

Here I:

  • Enable Scribe in mobile
  • Add Scribe mobile UI (a full screen modal)
  • Fix various bugs related to mobile

See PR #4247 for the keyboard and selection visible in the modal fix.

Screen.Recording.2026-01-08.at.17.23.01.mov

Summary by CodeRabbit

  • New Features

    • AI Assistant available on mobile with AI button in composer app bars and mobile bottom sheets for actions and suggestions
    • Selection save/restore helpers to improve editor workflows on mobile
  • Bug Fixes

    • Improved text selection detection, positioning and collapse behavior across platforms
  • Refactor

    • Platform-aware modal handling and mobile editor focus/flow adjustments
  • Style

    • New visual tokens for mobile AI components (icons, sizes, padding)

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

Extends HtmlUtils selection APIs with an isWebPlatform flag, adds save/restore/clear selection scripts, and makes selection coordinate logic platform-aware (web vs mobile). Removes mobile-only guards so AIScribe config and bindings can run on mobile. Adds mobile-specific editor focus/save helpers, makes several composer and overlay methods async, wires an onOpenAiAssistantModal callback into multiple app-bar and bottom-bar widgets, and introduces mobile AIScribe UI widgets and modal bottom-sheet flows. Several new exports, image path, and style constants were also added.

Possibly related PRs

  • linagora/tmail-flutter PR 4247 — Adds save/restore/clear selection scripts and mobile selection-save/unfocus/restore flows that align with this change.
  • linagora/tmail-flutter PR 4217 — Modifies HtmlUtils.registerSelectionChangeListener API and selection-listener logic touching the same selection integration points.
  • linagora/tmail-flutter PR 4219 — Changes editor-relative coordinate computation and selection-clamping logic used by the selection listener scripts extended here.

Suggested labels

Label

Suggested reviewers

  • hoangdat
  • tddang-linagora
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[Scribe mobile] Add and enable Scribe mobile' directly and clearly summarizes the primary objective of the PR: enabling and adding Scribe mobile functionality, which aligns with the main changes across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/scribe-mobile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

This PR has been deployed to https://linagora.github.io/tmail-flutter/4245.

@zatteo zatteo marked this pull request as ready for review January 12, 2026 10:29
@zatteo
Copy link
Member Author

zatteo commented Jan 12, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart (1)

184-186: Consider EdgeInsetsDirectional for RTL consistency.

The new backIconPadding uses EdgeInsets.only(right: ...) while other paddings in this file use EdgeInsetsDirectional. If the back button should have its padding adapt for RTL layouts, consider:

♻️ Suggested change for RTL support
-  static const EdgeInsetsGeometry backIconPadding =
-      EdgeInsets.only(right: 8.0, top: 8.0, bottom: 8.0);
+  static const EdgeInsetsGeometry backIconPadding =
+      EdgeInsetsDirectional.only(end: 8.0, top: 8.0, bottom: 8.0);

If the back icon intentionally does not adapt for RTL, this can be ignored.

lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart (1)

55-65: Minor styling inconsistency with LandscapeAppBarComposerWidget.

The AI assistant button here doesn't explicitly set iconColor: MobileAppBarComposerWidgetStyle.iconColor, unlike the same button in LandscapeAppBarComposerWidget (line 62). For visual consistency across orientations, consider adding the iconColor property.

Suggested fix
              TMailButtonWidget.fromIcon(
                icon: imagePaths.icGradientSparkle,
                backgroundColor: Colors.transparent,
+               iconColor: MobileAppBarComposerWidgetStyle.iconColor,
                iconSize: MobileAppBarComposerWidgetStyle.iconSize,
                tooltipMessage: ScribeLocalizations.of(context).aiAssistant,
                onTapActionCallback: () => onOpenAiAssistantModal!(null, null),
              ),
scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart (1)

94-108: Remove unused content parameter from showMobileAIScribeMenuModal.

The content parameter is accepted but not passed to AiScribeMobileActionsBottomSheet, which doesn't accept it. Unlike showMobileAIScribeSuggestionModal, the menu modal only needs image paths and available categories. Remove the parameter to avoid confusion.

scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_bottom_sheet.dart (2)

57-59: Consider using consistent icon sizing.

The back chevron uses AIScribeSizes.aiAssistantIcon (line 59) while the close button uses a hardcoded 24 (line 73). For consistency and maintainability, consider extracting the close icon size to a constant or reusing the existing size constant if appropriate.

♻️ Suggested improvement
           IconButton(
             icon: const Icon(
               Icons.close,
-              size: 24,
+              size: AIScribeSizes.aiAssistantIcon,
             ),

Also applies to: 70-74


99-111: Optional: Extract to local variable for null-safety clarity.

The force-unwrap on line 104 is safe because itemBuilder only runs when itemCount > 0, but the reasoning is implicit. A local variable would make the null-safety more explicit.

♻️ Suggested improvement
 Widget _buildSubmenuListView() {
+  final submenuActions = _selectedCategory!.submenuActions ?? [];
   return ListView.builder(
     shrinkWrap: true,
-    itemCount: _selectedCategory!.submenuActions?.length ?? 0,
+    itemCount: submenuActions.length,
     itemBuilder: (context, index) {
-      final submenuAction = _selectedCategory!.submenuActions![index];
+      final submenuAction = submenuActions[index];
       return AiScribeSubmenuItem(
         menuAction: submenuAction,
         onSelectAction: _onActionSelected,
       );
     },
   );
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 209c090 and a9a21c0.

📒 Files selected for processing (15)
  • core/lib/utils/html/html_utils.dart
  • lib/features/base/mixin/ai_scribe_mixin.dart
  • lib/features/composer/presentation/composer_view.dart
  • lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart
  • lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
  • lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
  • lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart
  • scribe/lib/scribe.dart
  • scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_bottom_sheet.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_suggestion_bottom_sheet.dart
💤 Files with no reviewable changes (2)
  • lib/features/base/mixin/ai_scribe_mixin.dart
  • lib/features/mailbox_dashboard/presentation/extensions/ai_scribe/setup_cached_ai_scribe_extension.dart
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-09T09:36:45.349Z
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4194
File: lib/features/manage_account/presentation/manage_account_dashboard_controller.dart:174-176
Timestamp: 2025-12-09T09:36:45.349Z
Learning: In Dart/Flutter projects using GetX, do not wrap getBinding<T>() calls in try/catch since they return null when not found. Only wrap Get.find<T>() calls in try/catch because they throw if a dependency is unavailable. When a binding is optional, consider checking Get.isRegistered<T>() or handling the null/exception path gracefully instead of blindly catching, and document the expectation for failure modes where a dependency may not be registered.

Applied to files:

  • scribe/lib/scribe.dart
  • lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
  • lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart
  • core/lib/utils/html/html_utils.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_item.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart
  • lib/features/composer/presentation/composer_view.dart
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
  • scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart
  • lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_bottom_sheet.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_suggestion_bottom_sheet.dart
📚 Learning: 2025-12-12T09:03:05.962Z
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4204
File: lib/features/email/presentation/model/popup_menu_item_email_action.dart:34-35
Timestamp: 2025-12-12T09:03:05.962Z
Learning: In the tmail-flutter codebase, popup menu items intentionally use a consistent hoverIcon (imagePaths.icThumbsUp) across all popup item types (email actions, mailbox actions, profile settings, etc.), while actionIcon varies per action type. This is a deliberate UI/UX design choice.

Applied to files:

  • lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart
  • scribe/lib/scribe/ai/presentation/styles/ai_scribe_styles.dart
  • scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_item.dart
  • scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart
  • lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart
📚 Learning: 2025-12-12T04:54:11.121Z
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4191
File: lib/features/email/presentation/extensions/handle_email_action_extension.dart:37-80
Timestamp: 2025-12-12T04:54:11.121Z
Learning: In lib/features/email/presentation/extensions/handle_email_action_extension.dart, the mailboxDashBoardController.selectedEmail should only be synchronized when isMobileThreadDisabled is true. This is intentional behavior and should not be changed to update selectedEmail in non-mobile or thread-enabled contexts.

Applied to files:

  • core/lib/utils/html/html_utils.dart
  • lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart
🔇 Additional comments (20)
lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart (1)

11-22: LGTM!

The removal of the mobile platform restriction correctly enables AIScribe on mobile when both the config is enabled and the endpoint is available. The rest of the extension already handles platform differences appropriately via PlatformInfo.isWeb checks.

scribe/lib/scribe/ai/presentation/model/ai_scribe_menu_action.dart (1)

77-98: Good fix for the missing grammar icon.

Adding the correctGrammar case ensures the grammar action displays its icon consistently with the category. Previously, this would fall through to the default case and return null.

lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart (1)

94-101: LGTM!

Good fix for the iOS AI Scribe overlay button display issue. Delaying the selection listener setup to onCompleted ensures the WebView is fully loaded before registering the JavaScript handler, preventing race conditions on iOS.

core/lib/utils/html/html_utils.dart (3)

82-83: LGTM on platform detection.

The isDesktopEditor check using !window.flutter_inappwebview correctly distinguishes between web iframe (desktop) and InAppWebView (mobile) contexts.


103-117: LGTM on platform-specific selector logic.

The selector branching correctly targets:

  • Desktop: .note-editor .note-editable (Summernote editor)
  • Mobile: #editor (InAppWebView HTML editor)

154-160: Verify the mobile offset values on various devices.

The arbitrary offset { x: 22, y: -20 } to avoid native selection marks may need adjustment depending on device densities or OS versions. Consider documenting or making these values configurable if issues arise during testing.

scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_suggestion_bottom_sheet.dart (2)

5-68: Well-structured mobile suggestion bottom sheet implementation.

The widget correctly implements AiScribeSuggestionStateMixin with proper delegation to widget properties. The use of Flexible for the content area allows the state content to expand within the full-height container while respecting the header's space.


70-102: LGTM!

The state overrides follow a clean pattern: wrapping the base mixin implementations with consistent padding. The error state's vertical centering provides better UX for mobile screens.

scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart (2)

20-44: Clean platform-aware branching for menu modal.

The branching logic correctly separates mobile and desktop paths. The desktop path properly manages the PopupSubmenuController lifecycle with whenComplete(submenuController.dispose).


110-128: LGTM!

The mobile suggestion modal implementation correctly passes all required parameters including content and onSelectAiScribeSuggestionAction. The isScrollControlled: true and useSafeArea: true settings are appropriate for a full-screen modal experience.

scribe/lib/scribe.dart (1)

46-48: LGTM!

The new mobile widget exports follow the established pattern and correctly expose the new mobile AI Scribe UI components for external consumption.

lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart (2)

57-68: LGTM!

The AI assistant button integration follows the existing widget patterns with proper conditional rendering, consistent styling, and localized tooltip. The placement after Spacer() ensures it appears in the action buttons area.


4-5: The ai_assistant_button.dart import is not unused. The OnOpenAiAssistantModal typedef from this import is actively used in line 21 as the type for the onOpenAiAssistantModal field.

Likely an incorrect or invalid review comment.

lib/features/composer/presentation/composer_view.dart (2)

69-71: LGTM!

The conditional wiring controller.isAIScribeAvailable ? controller.openAIAssistantModal : null correctly enables the AI assistant button only when the feature is available.


93-95: Consistent AI assistant modal wiring across layouts.

The onOpenAiAssistantModal callback is wired identically for both portrait (AppBarComposerWidget) and tablet (TabletBottomBarComposerWidget) layouts, ensuring consistent behavior across all mobile form factors.

Also applies to: 479-481

scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_item.dart (2)

19-35: LGTM!

The category handling correctly routes to onCategorySelected for category items (allowing submenu navigation) and onActionSelected for direct actions. The type check ensures proper callback routing.


37-53: Handle or clarify the submenuActions.length > 1 case.

When submenuActions.length > 1, the code falls through to render a plain AiScribeMenuItem without explicitly showing all submenu actions. While this case may be data-impossible given the model constraints, either add explicit handling (e.g., a comment explaining why this is safe) or handle it like the bottom sheet does by iterating all actions.

scribe/lib/scribe/ai/presentation/widgets/mobile/ai_scribe_mobile_actions_bottom_sheet.dart (3)

122-134: Good keyboard-aware padding implementation.

The use of MediaQuery.of(context).viewInsets.bottom properly handles keyboard appearance, ensuring the input bar remains accessible when the keyboard is visible.


139-179: LGTM - Well-structured full-screen modal layout.

The build method properly:

  • Uses SafeArea to handle device notches and home indicators
  • Organizes content with appropriate flex behavior
  • Maintains a sticky bottom bar for custom prompts

The height: double.infinity aligns with the PR objective of implementing a full-screen modal.


25-29: The silent no-op for non-AiScribeActionContextMenuAction types is intentional design.

The code uses type-based routing: AiScribeMobileActionsItem filters AiScribeCategoryContextMenuAction instances and routes them to onCategorySelected, while all other action types flow to onActionSelected. This pattern is consistent throughout the file—different action types are handled by separate callbacks (onCategorySelected, onActionSelected, onCustomPromptSubmit). The _onActionSelected method correctly handles AiScribeActionContextMenuAction as the primary type it receives.

@dab246
Copy link
Member

dab246 commented Jan 13, 2026

  • Please fix nitpick comments

@zatteo
Copy link
Member Author

zatteo commented Feb 2, 2026

  • when we click on scribe button in topbar to transtlate whole email content (no text selected), we can not replace

Screen_Recording_20260202_052743_Twake.Mail.mp4

This is because we miss linagora/enough_html_editor#40. Can we merge it?

@dab246
Copy link
Member

dab246 commented Feb 2, 2026

This is because we miss linagora/enough_html_editor#40. Can we merge it?

We've merged it, please update it in the pubspec so we can retest it. @zatteo

@zatteo
Copy link
Member Author

zatteo commented Feb 2, 2026

This is because we miss linagora/enough_html_editor#40. Can we merge it?

We've merged it, please update it in the pubspec so we can retest it. @zatteo

Done 👍

Copy link
Member

@dab246 dab246 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Squash all fix-up commits

@zatteo
Copy link
Member Author

zatteo commented Feb 2, 2026

  • Squash all fix-up commits

Done 👍

@dab246
Copy link
Member

dab246 commented Feb 2, 2026

  • Fix build CI is failed

zatteo added 16 commits February 2, 2026 10:42
Because the HTML editor is different, we need a different selector to get the editable element.

We also need to add a small offset to the coordinates to avoid positionning the overlay icon above native selection UI in mobile. For example, Android adds a marker at the beginning and the end of the selection and we do not want to display the Scribe overlay icon above this marker.
The button was never displayed because the selectionchange event listener was loaded when the WebView was not entirely loaded so selectionchange event were never fired.

Now we setup selection listener when the WebView is loaded instead of created.
- Same icon than for desktop
- Displayed only if AI Scribe is enabled
Same features than Scribe desktop but as a full screen modal instead of  a context menu.

Reuse menu and submenu components.
Reuse suggestion components.
Reuse 99% of Scribe desktop style.
When opening Scribe mobile modal, we now unfocus the composer to hide the keyboard and the selection start and end icons from the OS.

Add new methods to save, restore, and clear text selection in HTML editor. These methods allow preserving and restoring user selection state when opening the Scribe mobile modal so that the "replace" action still works.
enough_html_editor uses execCommand to insert HTML. However
execCommand only works if the contenteditable element is focused. So
inserting or replacing Scribe result do not work if the user did not
click before in the composer.

Here we manually focus the contenteditable element to fix this before
inserting content.
- Fix Scribe replace on mobile when opened from menu bar (to be complete
this fix needs this PR Enough-Software/enough_html_editor#37)
- Ensure all mobile editor call are awaited
- Do not clear text if a selection has been restored (which mean we replace a selection)
- Avoid collapseToEnd crash
- Add getSavedSelection to check saved selection without consuming it
- Distinguish between setText (replace all) and insertText (at cursor) operations
- Fix replace callback to use setText when no selection exists
- Clean up saved selection state after restoration in HTML utils
- Move mobile editor focus and selection restore to appropriate callbacks
In recent commits I separated how we could add text in the editor. So
we need to escape HTML in both method that add text:
insertTextInEditor and setTextInEditor.

I had to extract the HTML escape part of
convertTextContentToHtmlContent because when using setTextInEditor we
do not need to convert \n to <br>.
To enjoy the fix from the following PR linagora/enough_html_editor#40
@zatteo zatteo force-pushed the feat/scribe-mobile branch from d025cae to 41f6580 Compare February 2, 2026 09:42
@hoangdat hoangdat merged commit 25b64cc into scribe-mobile Feb 2, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants