diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
index 8da4aeccce..aa4f8252d5 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
@@ -19,7 +19,7 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- group('copy and paste in document', () {
+ group('copy and paste in document:', () {
testWidgets('paste multiple lines at the first line', (tester) async {
// mock the clipboard
const lines = 3;
@@ -172,305 +172,314 @@ void main() {
},
);
});
- });
- testWidgets('paste text on part of bullet list', (tester) async {
- const plainText = 'test';
+ testWidgets('paste text on part of bullet list', (tester) async {
+ const plainText = 'test';
- await tester.pasteContent(
- plainText: plainText,
- beforeTest: (editorState) async {
- final transaction = editorState.transaction;
- transaction.insertNodes(
- [0],
- [
- Node(
- type: BulletedListBlockKeys.type,
- attributes: {
- 'delta': [
- {"insert": "bullet list"},
- ],
- },
- ),
- ],
- );
-
- // Set the selection to the second numbered list node (which has empty delta)
- transaction.afterSelection = Selection(
- start: Position(path: [0], offset: 7),
- end: Position(path: [0], offset: 11),
- );
-
- await editorState.apply(transaction);
- await tester.pumpAndSettle();
- },
- (editorState) {
- final node = editorState.getNodeAtPath([0]);
- expect(node?.delta?.toPlainText(), 'bullet test');
- expect(node?.type, BulletedListBlockKeys.type);
- },
- );
- });
-
- testWidgets('paste image(png) from memory', (tester) async {
- final image = await rootBundle.load('assets/test/images/sample.png');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(image: ('png', bytes), (editorState) {
- expect(editorState.document.root.children.length, 1);
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- expect(node.attributes[ImageBlockKeys.url], isNotNull);
- });
- });
-
- testWidgets('paste image(jpeg) from memory', (tester) async {
- final image = await rootBundle.load('assets/test/images/sample.jpeg');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
- expect(editorState.document.root.children.length, 1);
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- expect(node.attributes[ImageBlockKeys.url], isNotNull);
- });
- });
-
- testWidgets('paste image(gif) from memory', (tester) async {
- final image = await rootBundle.load('assets/test/images/sample.gif');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(image: ('gif', bytes), (editorState) {
- expect(editorState.document.root.children.length, 1);
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- expect(node.attributes[ImageBlockKeys.url], isNotNull);
- });
- });
-
- testWidgets(
- 'format the selected text to href when pasting url if available',
- (tester) async {
- const text = 'appflowy';
- const url = 'https://appflowy.io';
await tester.pasteContent(
- plainText: url,
+ plainText: plainText,
beforeTest: (editorState) async {
- await tester.ime.insertText(text);
- await tester.editor.updateSelection(
- Selection.single(
- path: [0],
- startOffset: 0,
- endOffset: text.length,
- ),
+ final transaction = editorState.transaction;
+ transaction.insertNodes(
+ [0],
+ [
+ Node(
+ type: BulletedListBlockKeys.type,
+ attributes: {
+ 'delta': [
+ {"insert": "bullet list"},
+ ],
+ },
+ ),
+ ],
);
- },
- (editorState) {
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ParagraphBlockKeys.type);
- expect(node.delta!.toJson(), [
- {
- 'insert': text,
- 'attributes': {'href': url},
- }
- ]);
- },
- );
- },
- );
- // https://github.com/AppFlowy-IO/AppFlowy/issues/3263
- testWidgets(
- 'paste the image from clipboard when html and image are both available',
- (tester) async {
- const html =
- '''
''';
- final image = await rootBundle.load('assets/test/images/sample.png');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(
- html: html,
- image: ('png', bytes),
- (editorState) {
- expect(editorState.document.root.children.length, 1);
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- },
- );
- },
- );
+ // Set the selection to the second numbered list node (which has empty delta)
+ transaction.afterSelection = Selection(
+ start: Position(path: [0], offset: 7),
+ end: Position(path: [0], offset: 11),
+ );
- testWidgets('paste the html content contains section', (tester) async {
- const html =
- '''''';
- await tester.pasteContent(html: html, (editorState) {
- expect(editorState.document.root.children.length, 2);
- final node1 = editorState.getNodeAtPath([0])!;
- final node2 = editorState.getNodeAtPath([1])!;
- expect(node1.type, ParagraphBlockKeys.type);
- expect(node2.type, ParagraphBlockKeys.type);
- });
- });
-
- testWidgets('paste the html from google translation', (tester) async {
- const html =
- '''Assessment focus: potential motivations, empathy➢Personality characteristics and potential motivations:-Reflection of self-worth-Have a unique definition of success-Be true to your own lifestyle''';
- await tester.pasteContent(html: html, (editorState) {
- expect(editorState.document.root.children.length, 8);
- });
- });
-
- testWidgets(
- 'auto convert url to link preview block',
- (tester) async {
- const url = 'https://appflowy.io';
- await tester.pasteContent(plainText: url, (editorState) async {
- // the second one is the paragraph node
- expect(editorState.document.root.children.length, 2);
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, LinkPreviewBlockKeys.type);
- expect(node.attributes[LinkPreviewBlockKeys.url], url);
- });
-
- // hover on the link preview block
- // click the more button
- // and select convert to link
- await tester.hoverOnWidget(
- find.byType(CustomLinkPreviewWidget),
- onHover: () async {
- final convertToLinkButton = find.byWidgetPredicate((widget) {
- return widget is MenuBlockButton &&
- widget.tooltip ==
- LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
- });
- expect(convertToLinkButton, findsOneWidget);
- await tester.tap(convertToLinkButton);
+ await editorState.apply(transaction);
await tester.pumpAndSettle();
},
+ (editorState) {
+ final node = editorState.getNodeAtPath([0]);
+ expect(node?.delta?.toPlainText(), 'bullet test');
+ expect(node?.type, BulletedListBlockKeys.type);
+ },
);
+ });
- await tester.pumpAndSettle();
-
- final editorState = tester.editor.getCurrentEditorState();
- final textNode = editorState.getNodeAtPath([0])!;
- expect(textNode.type, ParagraphBlockKeys.type);
- expect(textNode.delta!.toJson(), [
- {
- 'insert': url,
- 'attributes': {'href': url},
- }
- ]);
- },
- );
-
- testWidgets(
- 'ctrl/cmd+z to undo the auto convert url to link preview block',
- (tester) async {
- const url = 'https://appflowy.io';
- await tester.pasteContent(plainText: url, (editorState) async {
- // the second one is the paragraph node
- expect(editorState.document.root.children.length, 2);
+ testWidgets('paste image(png) from memory', (tester) async {
+ final image = await rootBundle.load('assets/test/images/sample.png');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(image: ('png', bytes), (editorState) {
+ expect(editorState.document.root.children.length, 1);
final node = editorState.getNodeAtPath([0])!;
- expect(node.type, LinkPreviewBlockKeys.type);
- expect(node.attributes[LinkPreviewBlockKeys.url], url);
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotNull);
});
-
- await tester.editor.tapLineOfEditorAt(0);
- await tester.simulateKeyEvent(
- LogicalKeyboardKey.keyZ,
- isControlPressed:
- UniversalPlatform.isLinux || UniversalPlatform.isWindows,
- isMetaPressed: UniversalPlatform.isMacOS,
- );
- await tester.pumpAndSettle();
-
- final editorState = tester.editor.getCurrentEditorState();
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ParagraphBlockKeys.type);
- expect(node.delta!.toJson(), [
- {
- 'insert': url,
- 'attributes': {'href': url},
- }
- ]);
- },
- );
-
- testWidgets(
- 'paste the nodes start with non-delta node',
- (tester) async {
- await tester.pasteContent((_) {});
- const text = 'Hello World';
- final editorState = tester.editor.getCurrentEditorState();
- final transaction = editorState.transaction;
- // [image_block]
- // [paragraph_block]
- transaction.insertNodes([
- 0,
- ], [
- customImageNode(url: ''),
- paragraphNode(text: text),
- ]);
- await editorState.apply(transaction);
- await tester.pumpAndSettle();
-
- await tester.editor.tapLineOfEditorAt(0);
- // select all and copy
- await tester.simulateKeyEvent(
- LogicalKeyboardKey.keyA,
- isControlPressed:
- UniversalPlatform.isLinux || UniversalPlatform.isWindows,
- isMetaPressed: UniversalPlatform.isMacOS,
- );
- await tester.simulateKeyEvent(
- LogicalKeyboardKey.keyC,
- isControlPressed:
- UniversalPlatform.isLinux || UniversalPlatform.isWindows,
- isMetaPressed: UniversalPlatform.isMacOS,
- );
-
- // put the cursor to the end of the paragraph block
- await tester.editor.tapLineOfEditorAt(0);
-
- // paste the content
- await tester.simulateKeyEvent(
- LogicalKeyboardKey.keyV,
- isControlPressed:
- UniversalPlatform.isLinux || UniversalPlatform.isWindows,
- isMetaPressed: UniversalPlatform.isMacOS,
- );
- await tester.pumpAndSettle();
-
- // expect the image and the paragraph block are inserted below the cursor
- expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
- expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
- expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
- expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
- },
- );
-
- testWidgets('paste the url without protocol', (tester) async {
- // paste the image that from local file
- const plainText = '1.jpg';
- final image = await rootBundle.load('assets/test/images/sample.jpeg');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
- (editorState) {
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
- });
- testWidgets('paste the image url', (tester) async {
- const plainText = 'https://appflowy.io/1.jpg';
- final image = await rootBundle.load('assets/test/images/sample.jpeg');
- final bytes = image.buffer.asUint8List();
- await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
- (editorState) {
- final node = editorState.getNodeAtPath([0])!;
- expect(node.type, ImageBlockKeys.type);
- expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
+ testWidgets('paste image(jpeg) from memory', (tester) async {
+ final image = await rootBundle.load('assets/test/images/sample.jpeg');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
+ expect(editorState.document.root.children.length, 1);
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotNull);
+ });
});
- });
- const testMarkdownText = '''
+ testWidgets('paste image(gif) from memory', (tester) async {
+ final image = await rootBundle.load('assets/test/images/sample.gif');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(image: ('gif', bytes), (editorState) {
+ expect(editorState.document.root.children.length, 1);
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotNull);
+ });
+ });
+
+ testWidgets(
+ 'format the selected text to href when pasting url if available',
+ (tester) async {
+ const text = 'appflowy';
+ const url = 'https://appflowy.io';
+ await tester.pasteContent(
+ plainText: url,
+ beforeTest: (editorState) async {
+ await tester.ime.insertText(text);
+ await tester.editor.updateSelection(
+ Selection.single(
+ path: [0],
+ startOffset: 0,
+ endOffset: text.length,
+ ),
+ );
+ },
+ (editorState) {
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ParagraphBlockKeys.type);
+ expect(node.delta!.toJson(), [
+ {
+ 'insert': text,
+ 'attributes': {'href': url},
+ }
+ ]);
+ },
+ );
+ },
+ );
+
+ // https://github.com/AppFlowy-IO/AppFlowy/issues/3263
+ testWidgets(
+ 'paste the image from clipboard when html and image are both available',
+ (tester) async {
+ const html =
+ '''
''';
+ final image = await rootBundle.load('assets/test/images/sample.png');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(
+ html: html,
+ image: ('png', bytes),
+ (editorState) {
+ expect(editorState.document.root.children.length, 1);
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ },
+ );
+ },
+ );
+
+ testWidgets('paste the html content contains section', (tester) async {
+ const html =
+ '''''';
+ await tester.pasteContent(html: html, (editorState) {
+ expect(editorState.document.root.children.length, 2);
+ final node1 = editorState.getNodeAtPath([0])!;
+ final node2 = editorState.getNodeAtPath([1])!;
+ expect(node1.type, ParagraphBlockKeys.type);
+ expect(node2.type, ParagraphBlockKeys.type);
+ });
+ });
+
+ testWidgets('paste the html from google translation', (tester) async {
+ const html =
+ '''Assessment focus: potential motivations, empathy➢Personality characteristics and potential motivations:-Reflection of self-worth-Have a unique definition of success-Be true to your own lifestyle''';
+ await tester.pasteContent(html: html, (editorState) {
+ expect(editorState.document.root.children.length, 8);
+ });
+ });
+
+ testWidgets(
+ 'auto convert url to link preview block',
+ (tester) async {
+ const url = 'https://appflowy.io';
+ await tester.pasteContent(plainText: url, (editorState) async {
+ // the second one is the paragraph node
+ expect(editorState.document.root.children.length, 2);
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, LinkPreviewBlockKeys.type);
+ expect(node.attributes[LinkPreviewBlockKeys.url], url);
+ });
+
+ // hover on the link preview block
+ // click the more button
+ // and select convert to link
+ await tester.hoverOnWidget(
+ find.byType(CustomLinkPreviewWidget),
+ onHover: () async {
+ final convertToLinkButton = find.byWidgetPredicate((widget) {
+ return widget is MenuBlockButton &&
+ widget.tooltip ==
+ LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
+ });
+ expect(convertToLinkButton, findsOneWidget);
+ await tester.tap(convertToLinkButton);
+ await tester.pumpAndSettle();
+ },
+ );
+
+ await tester.pumpAndSettle();
+
+ final editorState = tester.editor.getCurrentEditorState();
+ final textNode = editorState.getNodeAtPath([0])!;
+ expect(textNode.type, ParagraphBlockKeys.type);
+ expect(textNode.delta!.toJson(), [
+ {
+ 'insert': url,
+ 'attributes': {'href': url},
+ }
+ ]);
+ },
+ );
+
+ testWidgets(
+ 'ctrl/cmd+z to undo the auto convert url to link preview block',
+ (tester) async {
+ const url = 'https://appflowy.io';
+ await tester.pasteContent(plainText: url, (editorState) async {
+ // the second one is the paragraph node
+ expect(editorState.document.root.children.length, 2);
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, LinkPreviewBlockKeys.type);
+ expect(node.attributes[LinkPreviewBlockKeys.url], url);
+ });
+
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.simulateKeyEvent(
+ LogicalKeyboardKey.keyZ,
+ isControlPressed:
+ UniversalPlatform.isLinux || UniversalPlatform.isWindows,
+ isMetaPressed: UniversalPlatform.isMacOS,
+ );
+ await tester.pumpAndSettle();
+
+ final editorState = tester.editor.getCurrentEditorState();
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ParagraphBlockKeys.type);
+ expect(node.delta!.toJson(), [
+ {
+ 'insert': url,
+ 'attributes': {'href': url},
+ }
+ ]);
+ },
+ );
+
+ testWidgets(
+ 'paste the nodes start with non-delta node',
+ (tester) async {
+ await tester.pasteContent((_) {});
+ const text = 'Hello World';
+ final editorState = tester.editor.getCurrentEditorState();
+ final transaction = editorState.transaction;
+ // [image_block]
+ // [paragraph_block]
+ transaction.insertNodes([
+ 0,
+ ], [
+ customImageNode(url: ''),
+ paragraphNode(text: text),
+ ]);
+ await editorState.apply(transaction);
+ await tester.pumpAndSettle();
+
+ await tester.editor.tapLineOfEditorAt(0);
+ // select all and copy
+ await tester.simulateKeyEvent(
+ LogicalKeyboardKey.keyA,
+ isControlPressed:
+ UniversalPlatform.isLinux || UniversalPlatform.isWindows,
+ isMetaPressed: UniversalPlatform.isMacOS,
+ );
+ await tester.simulateKeyEvent(
+ LogicalKeyboardKey.keyC,
+ isControlPressed:
+ UniversalPlatform.isLinux || UniversalPlatform.isWindows,
+ isMetaPressed: UniversalPlatform.isMacOS,
+ );
+
+ // put the cursor to the end of the paragraph block
+ await tester.editor.tapLineOfEditorAt(0);
+
+ // paste the content
+ await tester.simulateKeyEvent(
+ LogicalKeyboardKey.keyV,
+ isControlPressed:
+ UniversalPlatform.isLinux || UniversalPlatform.isWindows,
+ isMetaPressed: UniversalPlatform.isMacOS,
+ );
+ await tester.pumpAndSettle();
+
+ // expect the image and the paragraph block are inserted below the cursor
+ expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
+ expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
+ expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
+ expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
+ },
+ );
+
+ testWidgets('paste the url without protocol', (tester) async {
+ // paste the image that from local file
+ const plainText = '1.jpg';
+ final image = await rootBundle.load('assets/test/images/sample.jpeg');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
+ (editorState) {
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
+ });
+ });
+
+ testWidgets('paste the image url', (tester) async {
+ const plainText = 'https://appflowy.io/1.jpg';
+ final image = await rootBundle.load('assets/test/images/sample.jpeg');
+ final bytes = image.buffer.asUint8List();
+ await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
+ (editorState) {
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
+ });
+ });
+
+ testWidgets('paste image url without extension', (tester) async {
+ const plainText =
+ 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
+ await tester.pasteContent(plainText: plainText, (editorState) {
+ final node = editorState.getNodeAtPath([0])!;
+ expect(node.type, ImageBlockKeys.type);
+ expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
+ });
+ });
+
+ const testMarkdownText = '''
# I'm h1
## I'm h2
### I'm h3
@@ -478,34 +487,35 @@ void main() {
##### I'm h5
###### I'm h6''';
- testWidgets('paste markdowns', (tester) async {
- await tester.pasteContent(
- plainText: testMarkdownText,
- (editorState) {
- final children = editorState.document.root.children;
- expect(children.length, 6);
- for (int i = 1; i <= children.length; i++) {
- final text = children[i - 1].delta!.toPlainText();
- expect(text, 'I\'m h$i');
- }
- },
- );
- });
+ testWidgets('paste markdowns', (tester) async {
+ await tester.pasteContent(
+ plainText: testMarkdownText,
+ (editorState) {
+ final children = editorState.document.root.children;
+ expect(children.length, 6);
+ for (int i = 1; i <= children.length; i++) {
+ final text = children[i - 1].delta!.toPlainText();
+ expect(text, 'I\'m h$i');
+ }
+ },
+ );
+ });
- testWidgets('paste markdowns as plain', (tester) async {
- await tester.pasteContent(
- plainText: testMarkdownText,
- pasteAsPlain: true,
- (editorState) {
- final children = editorState.document.root.children;
- expect(children.length, 6);
- for (int i = 1; i <= children.length; i++) {
- final text = children[i - 1].delta!.toPlainText();
- final expectText = '${'#' * i} I\'m h$i';
- expect(text, expectText);
- }
- },
- );
+ testWidgets('paste markdowns as plain', (tester) async {
+ await tester.pasteContent(
+ plainText: testMarkdownText,
+ pasteAsPlain: true,
+ (editorState) {
+ final children = editorState.document.root.children;
+ expect(children.length, 6);
+ for (int i = 1; i <= children.length; i++) {
+ final text = children[i - 1].delta!.toPlainText();
+ final expectText = '${'#' * i} I\'m h$i';
+ expect(text, expectText);
+ }
+ },
+ );
+ });
});
}
@@ -523,7 +533,7 @@ extension on WidgetTester {
await tapAnonymousSignInButton();
// create a new document
- await createNewPageWithNameUnderParent(name: 'Test Document');
+ await createNewPageWithNameUnderParent();
// tap the editor
await tapButton(find.byType(AppFlowyEditor));
@@ -546,7 +556,7 @@ extension on WidgetTester {
isShiftPressed: pasteAsPlain,
isMetaPressed: Platform.isMacOS,
);
- await pumpAndSettle();
+ await pumpAndSettle(const Duration(milliseconds: 1000));
test(editor.getCurrentEditorState());
}
diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart
index 79a4868a9f..cf20fb2742 100644
--- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart
+++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart
@@ -13,6 +13,7 @@ import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.d
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
+import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
@@ -170,7 +171,7 @@ class CopyButton extends StatelessWidget {
);
await getIt().setData(
ClipboardServiceData(
- plainText: textMessage.text,
+ plainText: _getTrimmedPlainText(textMessage.text),
inAppJson: jsonEncode(document.toJson()),
),
);
@@ -184,6 +185,16 @@ class CopyButton extends StatelessWidget {
),
);
}
+
+ String _getTrimmedPlainText(String plainText) {
+ // match and capture inner url as group
+ final matches = singleLineMarkdownImageRegex.allMatches(plainText);
+ if (matches.length != 1) {
+ return plainText;
+ }
+
+ return matches.first[1] ?? plainText;
+ }
}
class RegenerateButton extends StatelessWidget {
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart
index ed353103fb..06ccf9aae5 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart
@@ -8,12 +8,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';
import 'package:appflowy/shared/clipboard_state.dart';
import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy/util/default_extensions.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:http/http.dart' as http;
import 'package:string_validator/string_validator.dart';
/// - support
@@ -154,17 +154,14 @@ Future _pasteAsLinkPreview(
EditorState editorState,
String? text,
) async {
- // 1. the url should contains a protocol
- // 2. the url should not be an image url
- if (text == null ||
- text.isImageUrl() ||
- !isURL(text, {'require_protocol': true})) {
+ // the url should contain a protocol
+ if (text == null || !isURL(text, {'require_protocol': true})) {
return false;
}
final selection = editorState.selection;
// Apply the update only when the selection is collapsed
- // and at the start of the current line
+ // and at the start of the current line
if (selection == null ||
!selection.isCollapsed ||
selection.startIndex != 0) {
@@ -173,44 +170,52 @@ Future _pasteAsLinkPreview(
final node = editorState.getNodeAtPath(selection.start.path);
// Apply the update only when the current node is a paragraph
- // and the paragraph is empty
+ // and the paragraph is empty
if (node == null ||
node.type != ParagraphBlockKeys.type ||
node.delta?.toPlainText().isNotEmpty == true) {
return false;
}
- // 1. insert the text with link format
- // 2. convert it the link preview node
- final textTransaction = editorState.transaction;
- textTransaction.insertText(
- node,
- 0,
- text,
- attributes: {AppFlowyRichTextKeys.href: text},
- );
+ final bool isImageUrl;
+ try {
+ isImageUrl = await _isImageUrl(text);
+ } catch (e) {
+ Log.info('unable to get content header');
+ return false;
+ }
+
+ // insert the text with link format
+ final textTransaction = editorState.transaction
+ ..insertText(
+ node,
+ 0,
+ text,
+ attributes: {AppFlowyRichTextKeys.href: text},
+ );
await editorState.apply(
textTransaction,
skipHistoryDebounce: true,
);
- final linkPreviewTransaction = editorState.transaction;
- final insertedNodes = [
- linkPreviewNode(url: text),
+ // convert it to image or link preview node
+ final replacementInsertedNodes = [
+ isImageUrl ? imageNode(url: text) : linkPreviewNode(url: text),
// if the next node is null, insert a empty paragraph node
if (node.next == null) paragraphNode(),
];
- linkPreviewTransaction.insertNodes(
- selection.start.path,
- insertedNodes,
- );
- linkPreviewTransaction.deleteNode(node);
- linkPreviewTransaction.afterSelection = Selection.collapsed(
- Position(
- path: node.path.next,
- ),
- );
- await editorState.apply(linkPreviewTransaction);
+
+ final replacementTransaction = editorState.transaction
+ ..insertNodes(
+ selection.start.path,
+ replacementInsertedNodes,
+ )
+ ..deleteNode(node)
+ ..afterSelection = Selection.collapsed(
+ Position(path: node.path.next),
+ );
+
+ await editorState.apply(replacementTransaction);
return true;
}
@@ -235,3 +240,16 @@ Future doPlainPaste(EditorState editorState) async {
Log.info('unable to parse the clipboard content');
return;
}
+
+Future _isImageUrl(String text) async {
+ final response = await http.head(Uri.parse(text));
+
+ if (response.statusCode == 200) {
+ final contentType = response.headers['content-type'];
+ if (contentType != null) {
+ return contentType.startsWith('image/');
+ }
+ }
+
+ throw 'bad status code';
+}
diff --git a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart
index 4b07db8c78..15c2cdb3fe 100644
--- a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart
+++ b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart
@@ -13,6 +13,9 @@ const _imgUrlPattern =
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\s[",><]*)?';
final imgUrlRegex = RegExp(_imgUrlPattern);
+const _singleLineMarkdownImagePattern = "^!\\[.*\\]\\(($_hrefPattern)\\)\$";
+final singleLineMarkdownImageRegex = RegExp(_singleLineMarkdownImagePattern);
+
/// This pattern allows for both HTTP and HTTPS Scheme
/// It allows for query parameters
/// It only allows the following video extensions: