Skip to content
Open
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
601fc0b
Extract AI Scribe suggestion logic
zatteo Jan 7, 2026
776e13c
Extract AI Scribe menu item component
zatteo Jan 12, 2026
9ab6345
Allow boxShadow to be changed in AIScribeBar
zatteo Jan 7, 2026
be8f72f
Extract Scribe menu item text and icons in components
zatteo Jan 13, 2026
4a41a84
Guard Scribe suggestion loading if interactor is null
zatteo Jan 13, 2026
0a829ec
Remove unused localizations arguments in Scribe suggestion
zatteo Jan 13, 2026
2e978ce
Enable AI scribe in mobile
zatteo Jan 7, 2026
f55c502
Fix selection coordinates in mobile
zatteo Jan 7, 2026
7b8e909
Fix AI Scribe overlay button display in iOS
zatteo Jan 8, 2026
1a2ff0b
Fix correct grammar icon not present in submenu getIcon
zatteo Jan 12, 2026
e63a1cf
Display sparkle button in mobile app bar to launch AI Scribe
zatteo Jan 7, 2026
4cd09bd
Add Scribe mobile
zatteo Jan 12, 2026
667316a
Use proper icons and styles for bottomsheet icons in Scribe
zatteo Jan 14, 2026
0e7956b
Hide keyboard and selection icons when Scribe mobile modal opened
zatteo Jan 12, 2026
73587f4
Ensure editor is focused before calling insert HTML
zatteo Jan 14, 2026
9d25cfa
Do not display Scribe actions if no content in composer in mobile
zatteo Jan 15, 2026
876df81
Fix Scribe mobile issues
zatteo Jan 15, 2026
1f77776
Fix more Scribe mobile issues
zatteo Jan 21, 2026
6162335
Move Scribe icon in tablet at left of icons
zatteo Jan 21, 2026
f8647c5
Use warning logs instead of error logs for Scribe
zatteo Jan 26, 2026
3e0cffa
Properly escape HTML in every Scribe actions
zatteo Jan 27, 2026
912b21f
Upgrade enough_html_editor
zatteo Feb 2, 2026
163c392
Add current text in a card in Scribe mobile
zatteo Jan 13, 2026
3d87481
Display Scribe mobile (bottomsheet) only when responsive mobile
zatteo Jan 26, 2026
3f03a99
Adjust Scribe context menu to support tablet screens
zatteo Jan 26, 2026
81d41e8
Ensure replace function respect new lines in every editor
zatteo Feb 9, 2026
2009b10
Fix Scribe on responsive mobile
zatteo Feb 10, 2026
81e73a6
fixup! Display Scribe mobile (bottomsheet) only when responsive mobile
zatteo Feb 10, 2026
d9401e3
Display AI Scribe toggle in preferences in mobile
zatteo Feb 10, 2026
69fc725
Do not focus composer when clicking on tablet bottom bar button
zatteo Feb 10, 2026
2d8752e
Display Scribe modals correctly when a keyboard is opened
zatteo Feb 10, 2026
1955409
Include spacer in tablet bottom bar Scribe button
zatteo Feb 10, 2026
4dd58e2
fixup! Display Scribe modals correctly when a keyboard is opened
zatteo Feb 11, 2026
b16ca2d
Autofocus Scribe input bar
zatteo Feb 11, 2026
ec84c7d
feat(scribe-mobile): Display the result prompt modal in the correct p…
dab246 Feb 13, 2026
3332e2f
feat(scribe-mobile):Fix sometimes the top part is cut off
dab246 Feb 13, 2026
400e514
Rename bottomsheetIcon to secondaryIcon
zatteo Jan 20, 2026
ad12bb0
Add a toolbar to execute quick actions to Scribe suggestion
zatteo Feb 4, 2026
73fb4e4
[Scribe] Add Improve button (#4335)
zatteo Feb 27, 2026
1855320
Manage prompts as a "messages" object during prompt lifecycle
zatteo Jan 29, 2026
1bfa2ff
Add prompts.json in Scribe package
zatteo Jan 29, 2026
acdd657
Get and build prompt from local JSON file
zatteo Jan 29, 2026
e4b7e4e
Allow to get scribePromptUrl from LinagoraEcosystem
zatteo Feb 19, 2026
591f2d9
Fetch prompts from scribePromptUrl
zatteo Feb 19, 2026
139f0a9
Create and use an enum for AIRole in Scribe
zatteo Feb 23, 2026
2ef34c8
Cache LinagoraEcosystem to avoid multiple network calls
zatteo Feb 26, 2026
f3af0c8
Rework PromptService
zatteo Feb 26, 2026
219d2f1
Revert "Cache LinagoraEcosystem to avoid multiple network calls"
zatteo Mar 3, 2026
47dc204
Cache Linagora Ecosystem in controller for Scribe
zatteo Mar 3, 2026
5022083
Use own Dio instance for prompt service
zatteo Mar 3, 2026
6df32ff
Fix Scribe mobile inline never showing modal
zatteo Feb 24, 2026
004dc29
Increase Scribe mobile inline button size
zatteo Feb 24, 2026
323ac8e
Update Scribe mobile custom prompt bar UI
zatteo Feb 25, 2026
7eea968
Update Scribe success button actions
zatteo Feb 25, 2026
2ba6453
Add toast when copying Scribe suggestion
zatteo Mar 2, 2026
9a35cbe
fixup! Update Scribe mobile custom prompt bar UI
zatteo Mar 2, 2026
3222cf7
Fix Scribe mobile in landscape mode
zatteo Mar 2, 2026
f5c011d
TF-4378 Migrate `scribe-mobile` branch to `master` branch
dab246 Mar 16, 2026
a885dca
fixup! TF-4378 Migrate `scribe-mobile` branch to `master` branch
dab246 Mar 16, 2026
88115e8
fixup! fixup! TF-4378 Migrate `scribe-mobile` branch to `master` branch
dab246 Mar 16, 2026
925bf25
fixup! fixup! fixup! TF-4378 Migrate `scribe-mobile` branch to `maste…
dab246 Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/images/ic_arrow_back_ios.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/lib/presentation/resources/image_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
90 changes: 80 additions & 10 deletions core/lib/utils/html/html_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -183,7 +197,7 @@ class HtmlUtils {
script: '''
(() => {
const selection = window.getSelection();
if (selection) {
if (selection && selection.rangeCount > 0) {
selection.collapseToEnd()
}
})();''',
Expand All @@ -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');
Expand Down
16 changes: 7 additions & 9 deletions core/lib/utils/string_convert.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
static String escapeTextContent(String textContent) {
const HtmlEscape htmlEscape = HtmlEscape();

final htmlContent = escapedContent.replaceAll('\n', '<br>');
return htmlEscape.convert(textContent);
}

static String convertTextContentToHtmlContent(String textContent) {
final escapedText = escapeTextContent(textContent);
final htmlContent = escapedText.replaceAll('\n', '<br>');
return '<div>$htmlContent</div>';
}
}
3 changes: 0 additions & 3 deletions lib/features/base/mixin/ai_scribe_mixin.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,8 +28,6 @@ mixin AiScribeMixin {

void injectAIScribeBindings(Session? session, AccountId? accountId) {
try {
if (PlatformInfo.isMobile) return;

final aiCapability = getAICapability(
session: session,
accountId: accountId,
Expand Down
6 changes: 6 additions & 0 deletions lib/features/composer/presentation/composer_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class ComposerView extends GetWidget<ComposerController> {
controller.handleOpenContextMenu(context, position);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
onOpenAiAssistantModal: controller.isAIScribeAvailable
? controller.openAIAssistantModal
: null,
attachFileAction: () => controller.openPickAttachmentMenu(
context,
_pickAttachmentsActionTiles(context)
Expand All @@ -87,6 +90,9 @@ class ComposerView extends GetWidget<ComposerController> {
controller.handleOpenContextMenu(context, position);
},
isNetworkConnectionAvailable: controller.isNetworkConnectionAvailable,
onOpenAiAssistantModal: controller.isAIScribeAvailable
? controller.openAIAssistantModal
: null,
attachFileAction: () => controller.openPickAttachmentMenu(
context,
_pickAttachmentsActionTiles(context)
Expand Down
3 changes: 3 additions & 0 deletions lib/features/composer/presentation/composer_view_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class ComposerView extends GetWidget<ComposerController> {
saveToDraftsAction: () => controller.handleClickSaveAsDraftsButton(context),
saveToTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
deleteComposerAction: controller.handleClickDeleteComposer,
onOpenAiAssistantModal: controller.isAIScribeAvailable
? controller.openAIAssistantModal
: null,
)),
ConstrainedBox(
constraints: BoxConstraints(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ class RichTextMobileTabletController extends GetxController {

Future<bool> get isEditorFocused async => await htmlEditorApi?.hasFocus() ?? false;

Future<void> 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');
Expand Down
Loading
Loading