Skip to content

[Scribe mobile] Refactoring to reuse components for Scribe mobile#4244

Merged
hoangdat merged 6 commits intoscribe-mobilefrom
feat/prepare-scribe-mobile
Jan 15, 2026
Merged

[Scribe mobile] Refactoring to reuse components for Scribe mobile#4244
hoangdat merged 6 commits intoscribe-mobilefrom
feat/prepare-scribe-mobile

Conversation

@zatteo
Copy link
Member

@zatteo zatteo commented Jan 12, 2026

Summary by CodeRabbit

  • New Features

    • New AI context menu components: menu item, icon, submenu indicator, and menu text widgets.
    • Reusable AI suggestion UI mixin providing unified loading/error/success states and selection handling.
  • Improvements

    • Optional shadow customization for the AI search bar.
    • Consolidated AI suggestion flow and menu rendering for more consistent hover and select behavior.

✏️ 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.

Walkthrough

This change updates the scribe package's public exports and UI components for AI Scribe: it adds AiScribeSuggestionStateMixin and new menu-item widgets (AiScribeMenuItem, AiScribeMenuIcon, AiScribeMenuSubmenuIcon, AiScribeMenuText), replaces an exported submenu file with the new item widgets in scribe/lib/scribe.dart, refactors AiScribeSuggestionWidget to use the new mixin, simplifies AiScribeContextMenuItem to delegate to AiScribeMenuItem, and adds an optional boxShadow field to AIScribeBar. Only exported/public surface and UI wiring are modified.

Possibly related PRs

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] Refactoring to reuse components for Scribe mobile' accurately reflects the main objective of extracting and reusing Scribe UI components (menu items, icons, text, state management) across desktop and mobile platforms.
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.


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/4244.

@zatteo zatteo marked this pull request as ready for review January 12, 2026 10:22
@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: 1

🤖 Fix all issues with AI agents
In
@scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart:
- Around line 40-54: The public method loadSuggestion uses a potentially null
_interactor with a forced unwrap (_interactor!), which can cause a crash if
registration failed; guard against null by checking _interactor before calling
execute and set _suggestionState to a failure (or appropriate state) if it's
null, e.g., early-return when _interactor == null after optionally logging or
assigning a Left failure; keep the existing mounted check and continue using
aiAction and content when calling _interactor.execute.
🧹 Nitpick comments (2)
scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart (1)

64-75: _hoverController?.exit() is always a no-op for non-submenu items.

Since _hoverController is only initialized when menuAction.hasSubmenu is true (lines 33-36), it will always be null in this non-submenu branch. While harmless due to null-safety, this call is misleading and should be removed for clarity.

Also, there's a minor formatting issue with extra whitespace on line 74.

Suggested cleanup
     return AiScribeMenuItem(
       itemKey: _itemKey,
       menuAction: widget.menuAction,
       onSelectAction: widget.onSelectAction,
       imagePaths: widget.imagePaths,
       onHover: (_) {
-          _hoverController?.exit();
           widget.onHoverOtherItem?.call();
         }
     );
-  
   }
scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart (1)

82-92: Unused localizations parameter in buildLoadingState and buildErrorState.

Both methods accept ScribeLocalizations localizations but don't use it. If these are placeholders for future localization, consider adding a comment. Otherwise, the parameter could be removed to avoid confusion.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (7)
  • scribe/lib/scribe.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
  • scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_submenu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
  • scribe/lib/scribe/ai/presentation/widgets/search/ai_scribe_bar.dart
🧰 Additional context used
🧠 Learnings (2)
📚 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/ai/presentation/widgets/search/ai_scribe_bar.dart
  • scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
  • scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
  • scribe/lib/scribe.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:

  • scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
🔇 Additional comments (8)
scribe/lib/scribe/ai/presentation/widgets/search/ai_scribe_bar.dart (1)

12-18: LGTM!

The optional boxShadow parameter is cleanly integrated with a sensible fallback to AIScribeShadows.modal. This approach maintains backward compatibility while enabling mobile-specific shadow customization.

Also applies to: 67-67

scribe/lib/scribe.dart (1)

28-43: LGTM!

The new exports are well-organized and correctly expose the refactored components (mixin, menu items, and suggestion UI widgets) for reuse across desktop and mobile implementations.

scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart (2)

27-28: Verify: Should submenu category items trigger onSelectAction on tap?

The onTap callback fires for all menu items, including those with hasSubmenu == true. Typically, category items with submenus only reveal their submenu on hover and don't perform an action on tap. If this is intentional (e.g., tapping navigates into the submenu on mobile), consider adding a comment. Otherwise, guard the tap:

Suggested guard if tap should be disabled for submenu items
       child: InkWell(
-        onTap: () => onSelectAction(menuAction),
+        onTap: menuAction.hasSubmenu ? null : () => onSelectAction(menuAction),
         hoverColor: AppColor.grayBackgroundColor,

36-69: LGTM on the menu item layout.

The Row structure with conditional icon, flexible text label, and optional submenu arrow is clean and follows the existing UI patterns. The use of Flexible ensures proper text truncation with ellipsis.

scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart (2)

35-48: LGTM!

The mixin integration is clean. The getter overrides correctly proxy widget properties to satisfy the mixin's abstract requirements, enabling state logic reuse while keeping widget-specific layout in this file.


157-165: LGTM on click-outside handling.

The dismiss logic correctly uses suggestionState from the mixin to determine if the modal should close (only when in failure or success state, not during loading).

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

23-38: LGTM on initialization flow.

The registration check with Get.isRegistered<T>() before Get.find<T>() follows the project's GetX patterns (per learnings). The early return on registration failure with an appropriate error state is a good defensive approach.


107-111: LGTM on disposal.

The _suggestionState ValueNotifier is properly disposed to prevent memory leaks.

@dab246
Copy link
Member

dab246 commented Jan 13, 2026

  • please also fix nitpick comments

@dab246
Copy link
Member

dab246 commented Jan 13, 2026

@coderabbitai Please review again

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

@dab246, I'll perform a fresh review of the PR now, including checking for any outstanding nitpick comments that need to be addressed.

✅ 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 (1)
scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart (1)

40-64: Consider removing the unnecessary mounted check on line 44.

The mounted check at line 44 is redundant for the null interactor case. If _interactor is null, setting the failure state should happen immediately without checking mounted. The guard at line 58 after the async call is correct.

Suggested simplification
  Future<void> loadSuggestion() async {
    _suggestionState.value = dartz.Right(GenerateAITextLoading());

    if (_interactor == null) {
-     if (!mounted) return;
      _suggestionState.value = dartz.Left(
        GenerateAITextFailure(
          GenerateAITextInteractorIsNotRegisteredException(),
        ),
      );
      return;
    }
📜 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 88388a6.

📒 Files selected for processing (8)
  • scribe/lib/scribe.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
  • scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_icon.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_submenu_icon.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_text.dart
  • scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • scribe/lib/scribe/ai/presentation/widgets/context_menu/ai_scribe_context_menu_item.dart
🧰 Additional context used
🧠 Learnings (2)
📚 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/ai/presentation/widgets/items/ai_scribe_menu_text.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_submenu_icon.dart
  • scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_icon.dart
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
  • scribe/lib/scribe.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:

  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_submenu_icon.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_icon.dart
  • scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart
🔇 Additional comments (13)
scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_text.dart (1)

4-19: LGTM!

Clean extraction of menu text rendering. The Flexible wrapper with single-line ellipsis overflow is appropriate for menu item labels in flex layouts.

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

23-38: Good interactor registration pattern.

The Get.isRegistered check before Get.find follows the correct pattern for optional dependencies. Based on learnings, this avoids catching exceptions from Get.find when the interactor might not be registered.


66-118: Well-structured state rendering and lifecycle management.

The ValueListenableBuilder pattern provides clean reactive UI updates. State widget factory methods (buildLoadingState, buildErrorState, buildSuccessState) are well-organized and properly dispose the ValueNotifier in dispose().

scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart (3)

35-48: Clean mixin integration with property delegation.

The getter overrides provide a clear contract for the mixin to access widget properties. This pattern enables clean reuse of the state management logic across different widget implementations.


122-124: Good delegation to mixin for state content rendering.

Replacing inline state handling with buildStateContent(context) reduces duplication and centralizes the state rendering logic in the mixin.


157-165: Correct usage of mixin's suggestionState property.

The _handleClickOutside method properly accesses suggestionState from the mixin to determine dismissibility.

scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_submenu_icon.dart (1)

6-23: LGTM!

Clean extraction of submenu icon rendering. The RTL-aware EdgeInsetsDirectional padding and consistent color filter application align with the codebase's icon styling patterns.

scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_icon.dart (1)

5-23: LGTM!

Clean icon widget accepting a path string for per-action icon customization. The 72% alpha gray filter and RTL-aware padding are consistent with the codebase's styling patterns. Based on learnings, the varying actionIcon per action type is intentional.

scribe/lib/scribe/ai/presentation/widgets/items/ai_scribe_menu_item.dart (3)

1-6: LGTM on imports and typedef.

The imports are appropriate and the OnHoverAction typedef provides a clear signature for hover callbacks.


8-22: Well-structured widget API.

The separation of super.key (widget identity) and itemKey (container reference for layout calculations) is appropriate for scenarios where the menu item's position needs to be queried externally.


37-44: No changes needed — text overflow is already handled.

AiScribeMenuText internally wraps its content with Flexible, sets maxLines: 1, and uses TextOverflow.ellipsis, so the Row layout does not require an additional Expanded wrapper. The text overflow concern is already addressed by the child widget.

Likely an incorrect or invalid review comment.

scribe/lib/scribe.dart (2)

28-28: LGTM on mixin export.

Adding the AiScribeSuggestionStateMixin export aligns with the PR objective to enable component reuse between desktop and mobile.


35-39: LGTM on new item widget exports.

The new exports properly expose the extracted menu item components. The relocation from context_menu/ to items/ directory improves organization by separating reusable item widgets from context-menu-specific logic. Verification confirms the old context_menu/ai_scribe_submenu_item.dart path has been removed and no external imports of the old path exist in the codebase.

@zatteo
Copy link
Member Author

zatteo commented Jan 13, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 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 (1)
scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart (1)

65-87: Consider computing hasContent inside the builder.

hasContent is computed outside the ValueListenableBuilder, so if content changes while only _suggestionState triggers a rebuild, the captured value may be stale. This is likely acceptable if content changes always trigger a full widget rebuild, but moving the computation inside the builder would be more robust.

♻️ Optional: Compute hasContent inside builder
   Widget buildStateContent(
     BuildContext context,
   ) {
-    final hasContent = content?.trim().isNotEmpty == true;
-
     return ValueListenableBuilder<dartz.Either<Failure, Success>>(
       valueListenable: _suggestionState,
       builder: (_, stateValue, __) {
         return stateValue.fold(
           (failure) => buildErrorState(),
           (value) {
             if (value is GenerateAITextSuccess) {
+              final hasContent = content?.trim().isNotEmpty == true;
               return buildSuccessState(
                 value.response.result,
                 hasContent,
               );
             }
             return buildLoadingState();
           },
         );
       },
     );
   }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88388a6 and 0e0f9d0.

📒 Files selected for processing (1)
  • scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
🧰 Additional context used
🧠 Learnings (1)
📚 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/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: analyze-test (model)
  • GitHub Check: analyze-test (email_recovery)
  • GitHub Check: analyze-test (default)
  • GitHub Check: analyze-test (contact)
  • GitHub Check: Build web version and deploy
  • GitHub Check: analyze-test (rule_filter)
  • GitHub Check: analyze-test (fcm)
  • GitHub Check: analyze-test (server_settings)
  • GitHub Check: analyze-test (core)
  • GitHub Check: analyze-test (forward)
🔇 Additional comments (3)
scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart (3)

23-38: LGTM - Correct GetX dependency check pattern.

The Get.isRegistered<T>() check before Get.find<T>() correctly avoids exceptions when the interactor isn't registered. This follows the recommended pattern for optional GetX dependencies. Based on learnings.


40-63: Good defensive coding with mounted check.

The mounted check on line 57 correctly prevents state updates after widget disposal. The redundant null check for _interactor is appropriate since loadSuggestion() could be called from subclasses for retry functionality.


113-117: Proper cleanup of ValueNotifier.

The dispose method correctly disposes _suggestionState before calling super.dispose(), preventing potential memory leaks.

@dab246
Copy link
Member

dab246 commented Jan 14, 2026

  • Consider computing hasContent inside the builder.
Screenshot 2026-01-14 at 10 12 52

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.

  • Should squash commit before merge

In Scribe desktop, the suggestion is displayed in a popup.
In Scribe mobile, it will be displayed in a full screen modal.

But it share the same logic about loading, success and error.

So here I extract this logic and use it in the AI Scribe desktop to be able to use it later in AI Scribe mobile.
Scribe is always composed of menu (aka category) and submenu (aka action) :
- submenu item UI was already in an UI only component. I moved it to items folder.
- menu item UI was mixed in a component with desktop related UX. So I extract the UI to create an UI only component. I moved it also to items folder.
We will need a custom boxShadow for mobile version.
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.

LGTM

@hoangdat hoangdat merged commit fdaa913 into scribe-mobile Jan 15, 2026
20 of 21 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.

3 participants