From 26f8bbf7c6ca9e37d3d9f4ede166055c71d8619f Mon Sep 17 00:00:00 2001
From: "Lucas.Xu" <lucas.xu@appflowy.io>
Date: Mon, 19 Feb 2024 16:24:47 +0700
Subject: [PATCH] feat: optimize image upload process and display an error
 message if upload fails (#4679)

* chore: optimize image upload

* feat: show upload image status

* chore: upload the ai image to cloud server
---
 .../document/application/doc_service.dart     |  2 +
 .../header/document_header_node_widget.dart   |  2 +-
 .../image/image_placeholder.dart              | 39 +++++++++++++------
 .../editor_plugins/image/image_util.dart      |  9 +++--
 .../image/resizeable_image.dart               |  1 +
 .../lib/shared/cloud_image_checker.dart       | 20 ++++++++++
 frontend/resources/translations/en.json       |  3 +-
 7 files changed, 60 insertions(+), 16 deletions(-)
 create mode 100644 frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart

diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
index 74684a19d5..9009ea52ff 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart
@@ -104,12 +104,14 @@ class DocumentService {
   /// Upload a file to the cloud storage.
   Future<Either<FlowyError, UploadedFilePB>> uploadFile({
     required String localFilePath,
+    bool isAsync = true,
   }) async {
     final workspace = await FolderEventReadCurrentWorkspace().send();
     return workspace.fold((l) async {
       final payload = UploadFileParamsPB(
         workspaceId: l.id,
         localFilePath: localFilePath,
+        isAsync: isAsync,
       );
       final result = await DocumentEventUploadFile(payload).send();
       return result.swap();
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart
index 49a85406db..784a7f246c 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart
@@ -615,7 +615,7 @@ class DocumentCoverState extends State<DocumentCover> {
         details = await saveImageToLocalStorage(details);
       } else {
         // else we should save the image to cloud storage
-        details = await saveImageToCloudStorage(details);
+        (details, _) = await saveImageToCloudStorage(details);
       }
     }
     widget.onChangeCover(type, details);
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart
index 3a680d1c6b..064580b752 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart
@@ -44,6 +44,8 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
   final documentService = DocumentService();
   late final editorState = context.read<EditorState>();
 
+  bool showLoading = false;
+
   @override
   Widget build(BuildContext context) {
     final Widget child = DecoratedBox(
@@ -65,9 +67,19 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
                 size: Size.square(24),
               ),
               const HSpace(10),
-              FlowyText(
-                LocaleKeys.document_plugins_image_addAnImage.tr(),
-              ),
+              ...showLoading
+                  ? [
+                      FlowyText(
+                        LocaleKeys.document_imageBlock_imageIsUploading.tr(),
+                      ),
+                      const HSpace(8),
+                      const CircularProgressIndicator.adaptive(),
+                    ]
+                  : [
+                      FlowyText(
+                        LocaleKeys.document_plugins_image_addAnImage.tr(),
+                      ),
+                    ],
             ],
           ),
         ),
@@ -188,6 +200,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
     final transaction = editorState.transaction;
 
     String? path;
+    String? errorMessage;
     CustomImageType imageType = CustomImageType.local;
 
     // if the user is using local authenticator, we need to save the image to local storage
@@ -195,14 +208,22 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
       path = await saveImageToLocalStorage(url);
     } else {
       // else we should save the image to cloud storage
-      path = await saveImageToCloudStorage(url);
+      setState(() {
+        showLoading = true;
+      });
+      (path, errorMessage) = await saveImageToCloudStorage(url);
+      setState(() {
+        showLoading = false;
+      });
       imageType = CustomImageType.internal;
     }
 
     if (mounted && path == null) {
       showSnackBarMessage(
         context,
-        LocaleKeys.document_imageBlock_error_invalidImage.tr(),
+        errorMessage == null
+            ? LocaleKeys.document_imageBlock_error_invalidImage.tr()
+            : ': $errorMessage',
       );
       return;
     }
@@ -244,12 +265,8 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
 
       final response = await get(uri);
       await File(copyToPath).writeAsBytes(response.bodyBytes);
-
-      final transaction = editorState.transaction;
-      transaction.updateNode(widget.node, {
-        ImageBlockKeys.url: copyToPath,
-      });
-      await editorState.apply(transaction);
+      await insertLocalImage(copyToPath);
+      await File(copyToPath).delete();
     } catch (e) {
       Log.error('cannot save image file', e);
     }
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart
index 12fbf93b07..d6a31bb7ea 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart
@@ -34,19 +34,22 @@ Future<String?> saveImageToLocalStorage(String localImagePath) async {
   }
 }
 
-Future<String?> saveImageToCloudStorage(String localImagePath) async {
+Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
+  String localImagePath,
+) async {
   final documentService = DocumentService();
   final result = await documentService.uploadFile(
     localFilePath: localImagePath,
+    isAsync: false,
   );
   return result.fold(
-    (l) => null,
+    (l) => (null, l.msg),
     (r) async {
       await CustomImageCacheManager().putFile(
         r.url,
         File(localImagePath).readAsBytesSync(),
       );
-      return r.url;
+      return (r.url, null);
     },
   );
 }
diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart
index 7b04ca3119..5038e781a0 100644
--- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart
+++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart
@@ -102,6 +102,7 @@ class _ResizableImageState extends State<ResizableImage> {
         progressIndicatorBuilder: (context, url, progress) =>
             _buildLoading(context),
       );
+
       child = _cacheImage!;
     } else {
       // load local file
diff --git a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart
new file mode 100644
index 0000000000..5a7bac2c75
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart
@@ -0,0 +1,20 @@
+import 'dart:convert';
+
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:http/http.dart' as http;
+
+Future<bool> isImageExistOnCloud({
+  required String url,
+  required UserProfilePB userProfilePB,
+}) async {
+  final header = <String, String>{};
+  final token = userProfilePB.token;
+  try {
+    final decodedToken = jsonDecode(token);
+    header['Authorization'] = 'Bearer ${decodedToken['access_token']}';
+    final response = await http.get(Uri.http(url), headers: header);
+    return response.statusCode == 200;
+  } catch (_) {
+    return false;
+  }
+}
diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json
index 70fb0fffc1..4b5c29da69 100644
--- a/frontend/resources/translations/en.json
+++ b/frontend/resources/translations/en.json
@@ -862,7 +862,8 @@
       "successToAddImageToGallery": "Image added to gallery successfully",
       "unableToLoadImage": "Unable to load image",
       "maximumImageSize": "Maximum supported upload image size is 10MB",
-      "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB"
+      "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB",
+      "imageIsUploading": "Image is uploading"
     },
     "codeBlock": {
       "language": {