Browse Source

Merge pull request #403 from AppFlowy-IO/refactor_document_data_layer

Refactor document data layer
Nathan.fooo 3 years ago
parent
commit
e7033aa6e8
100 changed files with 2475 additions and 1207 deletions
  1. 77 0
      frontend/app_flowy/lib/plugin/plugin.dart
  2. 1 0
      frontend/app_flowy/lib/plugin/src/runner.dart
  3. 46 0
      frontend/app_flowy/lib/plugin/src/sandbox.dart
  4. 2 2
      frontend/app_flowy/lib/startup/deps_resolver.dart
  5. 3 0
      frontend/app_flowy/lib/startup/startup.dart
  6. 36 0
      frontend/app_flowy/lib/startup/tasks/load_plugin.dart
  7. 2 1
      frontend/app_flowy/lib/startup/tasks/prelude.dart
  8. 0 0
      frontend/app_flowy/lib/startup/tasks/rust_sdk.dart
  9. 15 5
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  10. 5 5
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  11. 1 1
      frontend/app_flowy/lib/workspace/application/appearance.dart
  12. 5 3
      frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart
  13. 4 4
      frontend/app_flowy/lib/workspace/application/doc/doc_service.dart
  14. 1 1
      frontend/app_flowy/lib/workspace/application/doc/share_service.dart
  15. 0 0
      frontend/app_flowy/lib/workspace/application/edit_pannel/edit_context.dart
  16. 1 1
      frontend/app_flowy/lib/workspace/application/edit_pannel/edit_pannel_bloc.dart
  17. 1 1
      frontend/app_flowy/lib/workspace/application/home/home_bloc.dart
  18. 6 6
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart
  19. 1038 0
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart
  20. 50 0
      frontend/app_flowy/lib/workspace/application/view/view_ext.dart
  21. 0 29
      frontend/app_flowy/lib/workspace/domain/edit_action/app_edit.dart
  22. 0 34
      frontend/app_flowy/lib/workspace/domain/edit_action/view_edit.dart
  23. 0 25
      frontend/app_flowy/lib/workspace/domain/image.dart
  24. 0 123
      frontend/app_flowy/lib/workspace/domain/page_stack/page_stack.dart
  25. 0 30
      frontend/app_flowy/lib/workspace/domain/view_ext.dart
  26. 8 5
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  27. 213 0
      frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart
  28. 0 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/create_button.dart
  29. 18 15
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart
  30. 34 5
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  31. 2 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart
  32. 1 2
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart
  33. 2 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart
  34. 34 4
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  35. 4 5
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart
  36. 0 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/favorite.dart
  37. 0 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/header.dart
  38. 0 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/section.dart
  39. 42 28
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart
  40. 0 0
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart
  41. 4 7
      frontend/app_flowy/lib/workspace/presentation/home/navigation.dart
  42. 34 13
      frontend/app_flowy/lib/workspace/presentation/plugins/blank/blank.dart
  43. 61 25
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart
  44. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart
  45. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/styles.dart
  46. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/banner.dart
  47. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/style_widgets.dart
  48. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/check_button.dart
  49. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/color_picker.dart
  50. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/header_button.dart
  51. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/history_button.dart
  52. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/image_button.dart
  53. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/link_button.dart
  54. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/toggle_button.dart
  55. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/tool_bar.dart
  56. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/toolbar_icon_button.dart
  57. 5 4
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/menu.dart
  58. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/sizes.dart
  59. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart
  60. 0 0
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_header.dart
  61. 42 16
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart
  62. 0 97
      frontend/app_flowy/lib/workspace/presentation/stack_page/home_stack.dart
  63. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/edit_pannel/edit_pannel.dart
  64. 1 2
      frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart
  65. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  66. 0 67
      frontend/app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart
  67. 0 1
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/prelude.dart
  68. 0 36
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/top_bar.dart
  69. 0 5
      frontend/app_flowy/lib/workspace/presentation/widgets/prelude.dart
  70. 5 5
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-folder/dart_event.dart
  71. 60 60
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-collaboration/document_info.pb.dart
  72. 21 21
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-collaboration/document_info.pbjson.dart
  73. 8 8
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/share.pb.dart
  74. 2 2
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/share.pbjson.dart
  75. 130 32
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart
  76. 9 9
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart
  77. 21 14
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart
  78. 6 6
      frontend/app_flowy/pubspec.lock
  79. 2 0
      frontend/rust-lib/Cargo.lock
  80. 33 27
      frontend/rust-lib/flowy-document/src/block_editor.rs
  81. 6 6
      frontend/rust-lib/flowy-document/src/lib.rs
  82. 79 94
      frontend/rust-lib/flowy-document/src/manager.rs
  83. 18 18
      frontend/rust-lib/flowy-document/src/queue.rs
  84. 37 57
      frontend/rust-lib/flowy-document/src/web_socket.rs
  85. 4 4
      frontend/rust-lib/flowy-document/tests/document/edit_script.rs
  86. 1 1
      frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs
  87. 13 13
      frontend/rust-lib/flowy-document/tests/editor/mod.rs
  88. 1 1
      frontend/rust-lib/flowy-document/tests/editor/serde_test.rs
  89. 13 11
      frontend/rust-lib/flowy-folder/src/controller.rs
  90. 4 4
      frontend/rust-lib/flowy-folder/src/event_map.rs
  91. 7 7
      frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs
  92. 12 12
      frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
  93. 35 2
      frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs
  94. 13 7
      frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs
  95. 27 63
      frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs
  96. 2 2
      frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs
  97. 6 6
      frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs
  98. 41 49
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  99. 21 21
      frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs
  100. 34 30
      frontend/rust-lib/flowy-folder/src/services/web_socket.rs

+ 77 - 0
frontend/app_flowy/lib/plugin/plugin.dart

@@ -0,0 +1,77 @@
+library flowy_plugin;
+
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flutter/widgets.dart';
+
+export "./src/sandbox.dart";
+
+typedef PluginType = int;
+
+typedef PluginDataType = ViewDataType;
+
+typedef PluginId = String;
+
+abstract class Plugin {
+  PluginId get pluginId;
+
+  PluginDisplay get pluginDisplay;
+
+  PluginType get pluginType;
+
+  ChangeNotifier? get displayNotifier => null;
+
+  void dispose();
+}
+
+abstract class PluginBuilder {
+  Plugin build(dynamic data);
+
+  String get menuName;
+
+  PluginType get pluginType;
+
+  ViewDataType get dataType => ViewDataType.PlainText;
+}
+
+abstract class PluginConfig {
+  bool get creatable => true;
+}
+
+abstract class PluginDisplay with NavigationItem {
+  @override
+  Widget get leftBarItem;
+
+  @override
+  Widget? get rightBarItem;
+
+  List<NavigationItem> get navigationItems;
+
+  Widget buildWidget();
+}
+
+void registerPlugin({required PluginBuilder builder, PluginConfig? config}) {
+  getIt<PluginSandbox>().registerPlugin(builder.pluginType, builder, config: config);
+}
+
+Plugin makePlugin({required PluginType pluginType, dynamic data}) {
+  final plugin = getIt<PluginSandbox>().buildPlugin(pluginType, data);
+  return plugin;
+}
+
+List<PluginBuilder> pluginBuilders() {
+  final pluginBuilders = getIt<PluginSandbox>().builders;
+  final pluginConfigs = getIt<PluginSandbox>().pluginConfigs;
+  return pluginBuilders.where(
+    (builder) {
+      final config = pluginConfigs[builder.pluginType]?.creatable;
+      return config ?? true;
+    },
+  ).toList();
+}
+
+enum FlowyPluginException {
+  invalidData,
+}

+ 1 - 0
frontend/app_flowy/lib/plugin/src/runner.dart

@@ -0,0 +1 @@
+class PluginRunner {}

+ 46 - 0
frontend/app_flowy/lib/plugin/src/sandbox.dart

@@ -0,0 +1,46 @@
+import 'dart:collection';
+
+import 'package:flutter/services.dart';
+
+import '../plugin.dart';
+import 'runner.dart';
+
+class PluginSandbox {
+  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders = LinkedHashMap();
+  final Map<PluginType, PluginConfig> _pluginConfigs = <PluginType, PluginConfig>{};
+  late PluginRunner pluginRunner;
+
+  PluginSandbox() {
+    pluginRunner = PluginRunner();
+  }
+
+  int indexOf(PluginType pluginType) {
+    final index = _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);
+    if (index == -1) {
+      throw PlatformException(code: '-1', message: "Can't find the flowy plugin type: $pluginType");
+    }
+    return index;
+  }
+
+  Plugin buildPlugin(PluginType pluginType, dynamic data) {
+    final plugin = _pluginBuilders[pluginType]!.build(data);
+    return plugin;
+  }
+
+  void registerPlugin(PluginType pluginType, PluginBuilder builder, {PluginConfig? config}) {
+    if (_pluginBuilders.containsKey(pluginType)) {
+      throw PlatformException(code: '-1', message: "$pluginType was registered before");
+    }
+    _pluginBuilders[pluginType] = builder;
+
+    if (config != null) {
+      _pluginConfigs[pluginType] = config;
+    }
+  }
+
+  List<int> get supportPluginTypes => _pluginBuilders.keys.toList();
+
+  List<PluginBuilder> get builders => _pluginBuilders.values.toList();
+
+  Map<PluginType, PluginConfig> get pluginConfigs => _pluginConfigs;
+}

+ 2 - 2
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -19,7 +19,7 @@ import 'package:app_flowy/workspace/application/view/view_service.dart';
 import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart';
 import 'package:app_flowy/workspace/application/workspace/workspace_listener.dart';
 import 'package:app_flowy/workspace/application/workspace/workspace_service.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
@@ -55,7 +55,7 @@ class HomeDepsResolver {
 
     getIt.registerFactoryParam<ViewBloc, View, void>(
       (view, _) => ViewBloc(
-        view: view, 
+        view: view,
         service: ViewService(),
         listener: getIt<ViewListener>(param1: view),
       ),

+ 3 - 0
frontend/app_flowy/lib/startup/startup.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/tasks/prelude.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
@@ -41,6 +42,7 @@ class FlowyRunner {
     getIt<AppLauncher>().addTask(InitRustSDKTask());
 
     if (!env.isTest()) {
+      getIt<AppLauncher>().addTask(PluginLoadTask());
       getIt<AppLauncher>().addTask(InitAppWidgetTask());
       getIt<AppLauncher>().addTask(InitPlatformServiceTask());
     }
@@ -58,6 +60,7 @@ Future<void> initGetIt(
   getIt.registerFactory<EntryPoint>(() => f);
   getIt.registerLazySingleton<FlowySDK>(() => const FlowySDK());
   getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt));
+  getIt.registerSingleton<PluginSandbox>(PluginSandbox());
 
   await UserDepsResolver.resolve(getIt);
   await HomeDepsResolver.resolve(getIt);

+ 36 - 0
frontend/app_flowy/lib/startup/tasks/load_plugin.dart

@@ -0,0 +1,36 @@
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
+import 'package:app_flowy/workspace/presentation/plugins/doc/document.dart';
+import 'package:app_flowy/workspace/presentation/plugins/trash/trash.dart';
+
+enum DefaultPlugin {
+  quillEditor,
+  blank,
+  trash,
+}
+
+extension FlowyDefaultPluginExt on DefaultPlugin {
+  int type() {
+    switch (this) {
+      case DefaultPlugin.quillEditor:
+        return 0;
+      case DefaultPlugin.blank:
+        return 1;
+      case DefaultPlugin.trash:
+        return 2;
+    }
+  }
+}
+
+class PluginLoadTask extends LaunchTask {
+  @override
+  LaunchTaskType get type => LaunchTaskType.dataProcessing;
+
+  @override
+  Future<void> initialize(LaunchContext context) async {
+    registerPlugin(builder: BlankPluginBuilder(), config: BlankPluginConfig());
+    registerPlugin(builder: TrashPluginBuilder(), config: TrashPluginConfig());
+    registerPlugin(builder: DocumentPluginBuilder());
+  }
+}

+ 2 - 1
frontend/app_flowy/lib/startup/tasks/prelude.dart

@@ -1,3 +1,4 @@
 export 'app_widget.dart';
-export 'init_sdk.dart';
+export 'rust_sdk.dart';
 export 'platform_service.dart';
+export 'load_plugin.dart';

+ 0 - 0
frontend/app_flowy/lib/startup/tasks/init_sdk.dart → frontend/app_flowy/lib/startup/tasks/rust_sdk.dart


+ 15 - 5
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/app/app_listener.dart';
 import 'package:app_flowy/workspace/application/app/app_service.dart';
 import 'package:flowy_sdk/log.dart';
@@ -15,8 +16,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
   final AppService service;
   final AppListener listener;
 
-  AppBloc({required this.app, required this.service, required this.listener})
-      : super(AppState.initial(app)) {
+  AppBloc({required this.app, required this.service, required this.listener}) : super(AppState.initial(app)) {
     on<AppEvent>((event, emit) async {
       await event.map(initial: (e) async {
         listener.startListening(
@@ -25,8 +25,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
         );
         await _fetchViews(emit);
       }, createView: (CreateView value) async {
-        final viewOrFailed =
-            await service.createView(appId: app.id, name: value.name, desc: value.desc, viewType: value.viewType);
+        final viewOrFailed = await service.createView(
+          appId: app.id,
+          name: value.name,
+          desc: value.desc,
+          dataType: value.dataType,
+          pluginType: value.pluginType,
+        );
         viewOrFailed.fold(
           (view) => emit(state.copyWith(
             latestCreatedView: view,
@@ -100,7 +105,12 @@ class AppBloc extends Bloc<AppEvent, AppState> {
 @freezed
 class AppEvent with _$AppEvent {
   const factory AppEvent.initial() = Initial;
-  const factory AppEvent.createView(String name, String desc, ViewType viewType) = CreateView;
+  const factory AppEvent.createView(
+    String name,
+    String desc,
+    PluginDataType dataType,
+    PluginType pluginType,
+  ) = CreateView;
   const factory AppEvent.delete() = Delete;
   const factory AppEvent.rename(String newName) = Rename;
   const factory AppEvent.didReceiveViews(List<View> views) = ReceiveViews;

+ 5 - 5
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -4,9 +4,9 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:app_flowy/plugin/plugin.dart';
 
 class AppService {
-
   Future<Either<App, FlowyError>> getAppDesc({required String appId}) {
     final request = AppId.create()..value = appId;
 
@@ -17,13 +17,15 @@ class AppService {
     required String appId,
     required String name,
     required String desc,
-    required ViewType viewType,
+    required PluginDataType dataType,
+    required PluginType pluginType,
   }) {
     final request = CreateViewPayload.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
-      ..viewType = viewType;
+      ..dataType = dataType
+      ..pluginType = pluginType;
 
     return FolderEventCreateView(request).send();
   }
@@ -53,5 +55,3 @@ class AppService {
     return FolderEventUpdateApp(request).send();
   }
 }
-
-

+ 1 - 1
frontend/app_flowy/lib/workspace/application/appearance.dart

@@ -48,7 +48,7 @@ class AppearanceSettingModel extends ChangeNotifier with EquatableMixin {
   void setLocale(BuildContext context, Locale newLocale) {
     if (_locale != newLocale) {
       if (!context.supportedLocales.contains(newLocale)) {
-        Log.error("Unsupported locale: $newLocale");
+        Log.warn("Unsupported locale: $newLocale");
         newLocale = const Locale('en');
         Log.debug("Fallback to locale: $newLocale");
       }

+ 5 - 3
frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart

@@ -3,8 +3,8 @@ import 'package:app_flowy/workspace/application/doc/doc_service.dart';
 import 'package:app_flowy/workspace/application/trash/trash_service.dart';
 import 'package:app_flowy/workspace/application/view/view_listener.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/trash.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter_quill/flutter_quill.dart' show Document, Delta;
 import 'package:flowy_sdk/log.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -19,6 +19,7 @@ typedef FlutterQuillDocument = Document;
 class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
   final View view;
   final DocumentService service;
+
   final ViewListener listener;
   final TrashService trashService;
   late FlutterQuillDocument document;
@@ -43,6 +44,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
         },
         deletePermanently: (DeletePermanently value) async {
           final result = await trashService.deleteViews([Tuple2(view.id, TrashType.TrashView)]);
+
           final newState = result.fold((l) => state.copyWith(forceClose: true), (r) => state);
           emit(newState);
         },
@@ -85,8 +87,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
     listener.start();
     final result = await service.openDocument(docId: view.id);
     result.fold(
-      (doc) {
-        document = _decodeJsonToDocument(doc.deltaJson);
+      (block) {
+        document = _decodeJsonToDocument(block.deltaJson);
         _subscription = document.changes.listen((event) {
           final delta = event.item2;
           final documentDelta = document.toDelta();

+ 4 - 4
frontend/app_flowy/lib/workspace/application/doc/doc_service.dart

@@ -5,14 +5,14 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 
 class DocumentService {
-  Future<Either<DocumentDelta, FlowyError>> openDocument({required String docId}) {
+  Future<Either<BlockDelta, FlowyError>> openDocument({required String docId}) {
     final request = ViewId(value: docId);
     return FolderEventOpenView(request).send();
   }
 
-  Future<Either<DocumentDelta, FlowyError>> composeDelta({required String docId, required String data}) {
-    final request = DocumentDelta.create()
-      ..docId = docId
+  Future<Either<BlockDelta, FlowyError>> composeDelta({required String docId, required String data}) {
+    final request = BlockDelta.create()
+      ..blockId = docId
       ..deltaJson = data;
     return FolderEventApplyDocDelta(request).send();
   }

+ 1 - 1
frontend/app_flowy/lib/workspace/application/doc/share_service.dart

@@ -7,7 +7,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 class ShareService {
   Future<Either<ExportData, FlowyError>> export(String docId, ExportType type) {
     final request = ExportPayload.create()
-      ..docId = docId
+      ..viewId = docId
       ..exportType = type;
 
     return FolderEventExportDocument(request).send();

+ 0 - 0
frontend/app_flowy/lib/workspace/domain/edit_context.dart → frontend/app_flowy/lib/workspace/application/edit_pannel/edit_context.dart


+ 1 - 1
frontend/app_flowy/lib/workspace/application/edit_pannel/edit_pannel_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/domain/edit_context.dart';
+import 'package:app_flowy/workspace/application/edit_pannel/edit_context.dart';
 import 'package:dartz/dartz.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 // ignore: import_of_legacy_library_into_null_safe

+ 1 - 1
frontend/app_flowy/lib/workspace/application/home/home_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/domain/edit_context.dart';
+import 'package:app_flowy/workspace/application/edit_pannel/edit_context.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:dartz/dartz.dart';

+ 6 - 6
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -1,8 +1,8 @@
 import 'dart:async';
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/workspace/workspace_listener.dart';
 import 'package:app_flowy/workspace/application/workspace/workspace_service.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/blank/blank_page.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
@@ -29,7 +29,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
           emit(state.copyWith(isCollapse: !isCollapse));
         },
         openPage: (e) async {
-          emit(state.copyWith(stackContext: e.context));
+          emit(state.copyWith(plugin: e.plugin));
         },
         createApp: (CreateApp event) async {
           await _performActionOnCreateApp(event, emit);
@@ -85,7 +85,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
 class MenuEvent with _$MenuEvent {
   const factory MenuEvent.initial() = _Initial;
   const factory MenuEvent.collapse() = Collapse;
-  const factory MenuEvent.openPage(HomeStackContext context) = OpenPage;
+  const factory MenuEvent.openPage(Plugin plugin) = OpenPage;
   const factory MenuEvent.createApp(String name, {String? desc}) = CreateApp;
   const factory MenuEvent.didReceiveApps(Either<List<App>, FlowyError> appsOrFail) = ReceiveApps;
 }
@@ -96,13 +96,13 @@ class MenuState with _$MenuState {
     required bool isCollapse,
     required Option<List<App>> apps,
     required Either<Unit, FlowyError> successOrFailure,
-    required HomeStackContext stackContext,
+    required Plugin plugin,
   }) = _MenuState;
 
   factory MenuState.initial() => MenuState(
         isCollapse: false,
         apps: none(),
         successOrFailure: left(unit),
-        stackContext: BlankStackContext(),
+        plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
       );
 }

+ 1038 - 0
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart

@@ -0,0 +1,1038 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
+
+part of 'menu_bloc.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
+
+/// @nodoc
+class _$MenuEventTearOff {
+  const _$MenuEventTearOff();
+
+  _Initial initial() {
+    return const _Initial();
+  }
+
+  Collapse collapse() {
+    return const Collapse();
+  }
+
+  OpenPage openPage(Plugin plugin) {
+    return OpenPage(
+      plugin,
+    );
+  }
+
+  CreateApp createApp(String name, {String? desc}) {
+    return CreateApp(
+      name,
+      desc: desc,
+    );
+  }
+
+  ReceiveApps didReceiveApps(Either<List<App>, FlowyError> appsOrFail) {
+    return ReceiveApps(
+      appsOrFail,
+    );
+  }
+}
+
+/// @nodoc
+const $MenuEvent = _$MenuEventTearOff();
+
+/// @nodoc
+mixin _$MenuEvent {
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) =>
+      throw _privateConstructorUsedError;
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $MenuEventCopyWith<$Res> {
+  factory $MenuEventCopyWith(MenuEvent value, $Res Function(MenuEvent) then) =
+      _$MenuEventCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$MenuEventCopyWithImpl<$Res> implements $MenuEventCopyWith<$Res> {
+  _$MenuEventCopyWithImpl(this._value, this._then);
+
+  final MenuEvent _value;
+  // ignore: unused_field
+  final $Res Function(MenuEvent) _then;
+}
+
+/// @nodoc
+abstract class _$InitialCopyWith<$Res> {
+  factory _$InitialCopyWith(_Initial value, $Res Function(_Initial) then) =
+      __$InitialCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class __$InitialCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
+    implements _$InitialCopyWith<$Res> {
+  __$InitialCopyWithImpl(_Initial _value, $Res Function(_Initial) _then)
+      : super(_value, (v) => _then(v as _Initial));
+
+  @override
+  _Initial get _value => super._value as _Initial;
+}
+
+/// @nodoc
+
+class _$_Initial implements _Initial {
+  const _$_Initial();
+
+  @override
+  String toString() {
+    return 'MenuEvent.initial()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is _Initial);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) {
+    return initial();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) {
+    return initial?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (initial != null) {
+      return initial();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) {
+    return initial(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return initial?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (initial != null) {
+      return initial(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class _Initial implements MenuEvent {
+  const factory _Initial() = _$_Initial;
+}
+
+/// @nodoc
+abstract class $CollapseCopyWith<$Res> {
+  factory $CollapseCopyWith(Collapse value, $Res Function(Collapse) then) =
+      _$CollapseCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$CollapseCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
+    implements $CollapseCopyWith<$Res> {
+  _$CollapseCopyWithImpl(Collapse _value, $Res Function(Collapse) _then)
+      : super(_value, (v) => _then(v as Collapse));
+
+  @override
+  Collapse get _value => super._value as Collapse;
+}
+
+/// @nodoc
+
+class _$Collapse implements Collapse {
+  const _$Collapse();
+
+  @override
+  String toString() {
+    return 'MenuEvent.collapse()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is Collapse);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) {
+    return collapse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) {
+    return collapse?.call();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (collapse != null) {
+      return collapse();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) {
+    return collapse(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return collapse?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (collapse != null) {
+      return collapse(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class Collapse implements MenuEvent {
+  const factory Collapse() = _$Collapse;
+}
+
+/// @nodoc
+abstract class $OpenPageCopyWith<$Res> {
+  factory $OpenPageCopyWith(OpenPage value, $Res Function(OpenPage) then) =
+      _$OpenPageCopyWithImpl<$Res>;
+  $Res call({Plugin plugin});
+}
+
+/// @nodoc
+class _$OpenPageCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
+    implements $OpenPageCopyWith<$Res> {
+  _$OpenPageCopyWithImpl(OpenPage _value, $Res Function(OpenPage) _then)
+      : super(_value, (v) => _then(v as OpenPage));
+
+  @override
+  OpenPage get _value => super._value as OpenPage;
+
+  @override
+  $Res call({
+    Object? plugin = freezed,
+  }) {
+    return _then(OpenPage(
+      plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$OpenPage implements OpenPage {
+  const _$OpenPage(this.plugin);
+
+  @override
+  final Plugin plugin;
+
+  @override
+  String toString() {
+    return 'MenuEvent.openPage(plugin: $plugin)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is OpenPage &&
+            (identical(other.plugin, plugin) ||
+                const DeepCollectionEquality().equals(other.plugin, plugin)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(plugin);
+
+  @JsonKey(ignore: true)
+  @override
+  $OpenPageCopyWith<OpenPage> get copyWith =>
+      _$OpenPageCopyWithImpl<OpenPage>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) {
+    return openPage(plugin);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) {
+    return openPage?.call(plugin);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (openPage != null) {
+      return openPage(plugin);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) {
+    return openPage(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return openPage?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (openPage != null) {
+      return openPage(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class OpenPage implements MenuEvent {
+  const factory OpenPage(Plugin plugin) = _$OpenPage;
+
+  Plugin get plugin => throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $OpenPageCopyWith<OpenPage> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $CreateAppCopyWith<$Res> {
+  factory $CreateAppCopyWith(CreateApp value, $Res Function(CreateApp) then) =
+      _$CreateAppCopyWithImpl<$Res>;
+  $Res call({String name, String? desc});
+}
+
+/// @nodoc
+class _$CreateAppCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
+    implements $CreateAppCopyWith<$Res> {
+  _$CreateAppCopyWithImpl(CreateApp _value, $Res Function(CreateApp) _then)
+      : super(_value, (v) => _then(v as CreateApp));
+
+  @override
+  CreateApp get _value => super._value as CreateApp;
+
+  @override
+  $Res call({
+    Object? name = freezed,
+    Object? desc = freezed,
+  }) {
+    return _then(CreateApp(
+      name == freezed
+          ? _value.name
+          : name // ignore: cast_nullable_to_non_nullable
+              as String,
+      desc: desc == freezed
+          ? _value.desc
+          : desc // ignore: cast_nullable_to_non_nullable
+              as String?,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$CreateApp implements CreateApp {
+  const _$CreateApp(this.name, {this.desc});
+
+  @override
+  final String name;
+  @override
+  final String? desc;
+
+  @override
+  String toString() {
+    return 'MenuEvent.createApp(name: $name, desc: $desc)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is CreateApp &&
+            (identical(other.name, name) ||
+                const DeepCollectionEquality().equals(other.name, name)) &&
+            (identical(other.desc, desc) ||
+                const DeepCollectionEquality().equals(other.desc, desc)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^
+      const DeepCollectionEquality().hash(name) ^
+      const DeepCollectionEquality().hash(desc);
+
+  @JsonKey(ignore: true)
+  @override
+  $CreateAppCopyWith<CreateApp> get copyWith =>
+      _$CreateAppCopyWithImpl<CreateApp>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) {
+    return createApp(name, desc);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) {
+    return createApp?.call(name, desc);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (createApp != null) {
+      return createApp(name, desc);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) {
+    return createApp(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return createApp?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (createApp != null) {
+      return createApp(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class CreateApp implements MenuEvent {
+  const factory CreateApp(String name, {String? desc}) = _$CreateApp;
+
+  String get name => throw _privateConstructorUsedError;
+  String? get desc => throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $CreateAppCopyWith<CreateApp> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ReceiveAppsCopyWith<$Res> {
+  factory $ReceiveAppsCopyWith(
+          ReceiveApps value, $Res Function(ReceiveApps) then) =
+      _$ReceiveAppsCopyWithImpl<$Res>;
+  $Res call({Either<List<App>, FlowyError> appsOrFail});
+}
+
+/// @nodoc
+class _$ReceiveAppsCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
+    implements $ReceiveAppsCopyWith<$Res> {
+  _$ReceiveAppsCopyWithImpl(
+      ReceiveApps _value, $Res Function(ReceiveApps) _then)
+      : super(_value, (v) => _then(v as ReceiveApps));
+
+  @override
+  ReceiveApps get _value => super._value as ReceiveApps;
+
+  @override
+  $Res call({
+    Object? appsOrFail = freezed,
+  }) {
+    return _then(ReceiveApps(
+      appsOrFail == freezed
+          ? _value.appsOrFail
+          : appsOrFail // ignore: cast_nullable_to_non_nullable
+              as Either<List<App>, FlowyError>,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$ReceiveApps implements ReceiveApps {
+  const _$ReceiveApps(this.appsOrFail);
+
+  @override
+  final Either<List<App>, FlowyError> appsOrFail;
+
+  @override
+  String toString() {
+    return 'MenuEvent.didReceiveApps(appsOrFail: $appsOrFail)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is ReceiveApps &&
+            (identical(other.appsOrFail, appsOrFail) ||
+                const DeepCollectionEquality()
+                    .equals(other.appsOrFail, appsOrFail)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(appsOrFail);
+
+  @JsonKey(ignore: true)
+  @override
+  $ReceiveAppsCopyWith<ReceiveApps> get copyWith =>
+      _$ReceiveAppsCopyWithImpl<ReceiveApps>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function() collapse,
+    required TResult Function(Plugin plugin) openPage,
+    required TResult Function(String name, String? desc) createApp,
+    required TResult Function(Either<List<App>, FlowyError> appsOrFail)
+        didReceiveApps,
+  }) {
+    return didReceiveApps(appsOrFail);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? whenOrNull<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+  }) {
+    return didReceiveApps?.call(appsOrFail);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function()? collapse,
+    TResult Function(Plugin plugin)? openPage,
+    TResult Function(String name, String? desc)? createApp,
+    TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (didReceiveApps != null) {
+      return didReceiveApps(appsOrFail);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(_Initial value) initial,
+    required TResult Function(Collapse value) collapse,
+    required TResult Function(OpenPage value) openPage,
+    required TResult Function(CreateApp value) createApp,
+    required TResult Function(ReceiveApps value) didReceiveApps,
+  }) {
+    return didReceiveApps(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult? mapOrNull<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+  }) {
+    return didReceiveApps?.call(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(_Initial value)? initial,
+    TResult Function(Collapse value)? collapse,
+    TResult Function(OpenPage value)? openPage,
+    TResult Function(CreateApp value)? createApp,
+    TResult Function(ReceiveApps value)? didReceiveApps,
+    required TResult orElse(),
+  }) {
+    if (didReceiveApps != null) {
+      return didReceiveApps(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class ReceiveApps implements MenuEvent {
+  const factory ReceiveApps(Either<List<App>, FlowyError> appsOrFail) =
+      _$ReceiveApps;
+
+  Either<List<App>, FlowyError> get appsOrFail =>
+      throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $ReceiveAppsCopyWith<ReceiveApps> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+class _$MenuStateTearOff {
+  const _$MenuStateTearOff();
+
+  _MenuState call(
+      {required bool isCollapse,
+      required Option<List<App>> apps,
+      required Either<Unit, FlowyError> successOrFailure,
+      required Plugin plugin}) {
+    return _MenuState(
+      isCollapse: isCollapse,
+      apps: apps,
+      successOrFailure: successOrFailure,
+      plugin: plugin,
+    );
+  }
+}
+
+/// @nodoc
+const $MenuState = _$MenuStateTearOff();
+
+/// @nodoc
+mixin _$MenuState {
+  bool get isCollapse => throw _privateConstructorUsedError;
+  Option<List<App>> get apps => throw _privateConstructorUsedError;
+  Either<Unit, FlowyError> get successOrFailure =>
+      throw _privateConstructorUsedError;
+  Plugin get plugin => throw _privateConstructorUsedError;
+
+  @JsonKey(ignore: true)
+  $MenuStateCopyWith<MenuState> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $MenuStateCopyWith<$Res> {
+  factory $MenuStateCopyWith(MenuState value, $Res Function(MenuState) then) =
+      _$MenuStateCopyWithImpl<$Res>;
+  $Res call(
+      {bool isCollapse,
+      Option<List<App>> apps,
+      Either<Unit, FlowyError> successOrFailure,
+      Plugin plugin});
+}
+
+/// @nodoc
+class _$MenuStateCopyWithImpl<$Res> implements $MenuStateCopyWith<$Res> {
+  _$MenuStateCopyWithImpl(this._value, this._then);
+
+  final MenuState _value;
+  // ignore: unused_field
+  final $Res Function(MenuState) _then;
+
+  @override
+  $Res call({
+    Object? isCollapse = freezed,
+    Object? apps = freezed,
+    Object? successOrFailure = freezed,
+    Object? plugin = freezed,
+  }) {
+    return _then(_value.copyWith(
+      isCollapse: isCollapse == freezed
+          ? _value.isCollapse
+          : isCollapse // ignore: cast_nullable_to_non_nullable
+              as bool,
+      apps: apps == freezed
+          ? _value.apps
+          : apps // ignore: cast_nullable_to_non_nullable
+              as Option<List<App>>,
+      successOrFailure: successOrFailure == freezed
+          ? _value.successOrFailure
+          : successOrFailure // ignore: cast_nullable_to_non_nullable
+              as Either<Unit, FlowyError>,
+      plugin: plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
+    ));
+  }
+}
+
+/// @nodoc
+abstract class _$MenuStateCopyWith<$Res> implements $MenuStateCopyWith<$Res> {
+  factory _$MenuStateCopyWith(
+          _MenuState value, $Res Function(_MenuState) then) =
+      __$MenuStateCopyWithImpl<$Res>;
+  @override
+  $Res call(
+      {bool isCollapse,
+      Option<List<App>> apps,
+      Either<Unit, FlowyError> successOrFailure,
+      Plugin plugin});
+}
+
+/// @nodoc
+class __$MenuStateCopyWithImpl<$Res> extends _$MenuStateCopyWithImpl<$Res>
+    implements _$MenuStateCopyWith<$Res> {
+  __$MenuStateCopyWithImpl(_MenuState _value, $Res Function(_MenuState) _then)
+      : super(_value, (v) => _then(v as _MenuState));
+
+  @override
+  _MenuState get _value => super._value as _MenuState;
+
+  @override
+  $Res call({
+    Object? isCollapse = freezed,
+    Object? apps = freezed,
+    Object? successOrFailure = freezed,
+    Object? plugin = freezed,
+  }) {
+    return _then(_MenuState(
+      isCollapse: isCollapse == freezed
+          ? _value.isCollapse
+          : isCollapse // ignore: cast_nullable_to_non_nullable
+              as bool,
+      apps: apps == freezed
+          ? _value.apps
+          : apps // ignore: cast_nullable_to_non_nullable
+              as Option<List<App>>,
+      successOrFailure: successOrFailure == freezed
+          ? _value.successOrFailure
+          : successOrFailure // ignore: cast_nullable_to_non_nullable
+              as Either<Unit, FlowyError>,
+      plugin: plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$_MenuState implements _MenuState {
+  const _$_MenuState(
+      {required this.isCollapse,
+      required this.apps,
+      required this.successOrFailure,
+      required this.plugin});
+
+  @override
+  final bool isCollapse;
+  @override
+  final Option<List<App>> apps;
+  @override
+  final Either<Unit, FlowyError> successOrFailure;
+  @override
+  final Plugin plugin;
+
+  @override
+  String toString() {
+    return 'MenuState(isCollapse: $isCollapse, apps: $apps, successOrFailure: $successOrFailure, plugin: $plugin)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is _MenuState &&
+            (identical(other.isCollapse, isCollapse) ||
+                const DeepCollectionEquality()
+                    .equals(other.isCollapse, isCollapse)) &&
+            (identical(other.apps, apps) ||
+                const DeepCollectionEquality().equals(other.apps, apps)) &&
+            (identical(other.successOrFailure, successOrFailure) ||
+                const DeepCollectionEquality()
+                    .equals(other.successOrFailure, successOrFailure)) &&
+            (identical(other.plugin, plugin) ||
+                const DeepCollectionEquality().equals(other.plugin, plugin)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^
+      const DeepCollectionEquality().hash(isCollapse) ^
+      const DeepCollectionEquality().hash(apps) ^
+      const DeepCollectionEquality().hash(successOrFailure) ^
+      const DeepCollectionEquality().hash(plugin);
+
+  @JsonKey(ignore: true)
+  @override
+  _$MenuStateCopyWith<_MenuState> get copyWith =>
+      __$MenuStateCopyWithImpl<_MenuState>(this, _$identity);
+}
+
+abstract class _MenuState implements MenuState {
+  const factory _MenuState(
+      {required bool isCollapse,
+      required Option<List<App>> apps,
+      required Either<Unit, FlowyError> successOrFailure,
+      required Plugin plugin}) = _$_MenuState;
+
+  @override
+  bool get isCollapse => throw _privateConstructorUsedError;
+  @override
+  Option<List<App>> get apps => throw _privateConstructorUsedError;
+  @override
+  Either<Unit, FlowyError> get successOrFailure =>
+      throw _privateConstructorUsedError;
+  @override
+  Plugin get plugin => throw _privateConstructorUsedError;
+  @override
+  @JsonKey(ignore: true)
+  _$MenuStateCopyWith<_MenuState> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 50 - 0
frontend/app_flowy/lib/workspace/application/view/view_ext.dart

@@ -0,0 +1,50 @@
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flutter/material.dart';
+
+enum FlowyPlugin {
+  editor,
+  kanban,
+}
+
+extension FlowyPluginExtension on FlowyPlugin {
+  String displayName() {
+    switch (this) {
+      case FlowyPlugin.editor:
+        return "Doc";
+      case FlowyPlugin.kanban:
+        return "Kanban";
+      default:
+        return "";
+    }
+  }
+
+  bool enable() {
+    switch (this) {
+      case FlowyPlugin.editor:
+        return true;
+      case FlowyPlugin.kanban:
+        return false;
+      default:
+        return false;
+    }
+  }
+}
+
+extension ViewExtension on View {
+  Widget renderThumbnail({Color? iconColor}) {
+    String thumbnail = this.thumbnail;
+    if (thumbnail.isEmpty) {
+      thumbnail = "file_icon";
+    }
+
+    final Widget widget = svg(thumbnail, color: iconColor);
+    return widget;
+  }
+
+  Plugin plugin() {
+    final plugin = makePlugin(pluginType: pluginType, data: this);
+    return plugin;
+  }
+}

+ 0 - 29
frontend/app_flowy/lib/workspace/domain/edit_action/app_edit.dart

@@ -1,29 +0,0 @@
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flutter/material.dart';
-import 'package:app_flowy/generated/locale_keys.g.dart';
-
-enum AppDisclosureAction {
-  rename,
-  delete,
-}
-
-extension AppDisclosureExtension on AppDisclosureAction {
-  String get name {
-    switch (this) {
-      case AppDisclosureAction.rename:
-        return LocaleKeys.disclosureAction_rename.tr();
-      case AppDisclosureAction.delete:
-        return LocaleKeys.disclosureAction_delete.tr();
-    }
-  }
-
-  Widget get icon {
-    switch (this) {
-      case AppDisclosureAction.rename:
-        return svg('editor/edit', color: const Color(0xffe5e5e5));
-      case AppDisclosureAction.delete:
-        return svg('editor/delete', color: const Color(0xffe5e5e5));
-    }
-  }
-}

+ 0 - 34
frontend/app_flowy/lib/workspace/domain/edit_action/view_edit.dart

@@ -1,34 +0,0 @@
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flutter/material.dart';
-import 'package:app_flowy/generated/locale_keys.g.dart';
-
-enum ViewDisclosureAction {
-  rename,
-  delete,
-  duplicate,
-}
-
-extension ViewDisclosureExtension on ViewDisclosureAction {
-  String get name {
-    switch (this) {
-      case ViewDisclosureAction.rename:
-        return LocaleKeys.disclosureAction_rename.tr();
-      case ViewDisclosureAction.delete:
-        return LocaleKeys.disclosureAction_delete.tr();
-      case ViewDisclosureAction.duplicate:
-        return LocaleKeys.disclosureAction_duplicate.tr();
-    }
-  }
-
-  Widget get icon {
-    switch (this) {
-      case ViewDisclosureAction.rename:
-        return svg('editor/edit', color: const Color(0xff999999));
-      case ViewDisclosureAction.delete:
-        return svg('editor/delete', color: const Color(0xff999999));
-      case ViewDisclosureAction.duplicate:
-        return svg('editor/copy', color: const Color(0xff999999));
-    }
-  }
-}

+ 0 - 25
frontend/app_flowy/lib/workspace/domain/image.dart

@@ -1,25 +0,0 @@
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flutter/material.dart';
-import 'package:flowy_infra/image.dart';
-
-AssetImage assetImageForViewType(ViewType type) {
-  final imageName = _imageNameForViewType(type);
-  return AssetImage('assets/images/$imageName');
-}
-
-extension SvgViewType on View {
-  Widget thumbnail({Color? iconColor}) {
-    final imageName = _imageNameForViewType(viewType);
-    final Widget widget = svg(imageName, color: iconColor);
-    return widget;
-  }
-}
-
-String _imageNameForViewType(ViewType type) {
-  switch (type) {
-    case ViewType.Doc:
-      return "file_icon";
-    default:
-      return "file_icon";
-  }
-}

+ 0 - 123
frontend/app_flowy/lib/workspace/domain/page_stack/page_stack.dart

@@ -1,123 +0,0 @@
-import 'package:flowy_infra/notifier.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/home_stack.dart';
-import 'package:app_flowy/workspace/presentation/widgets/prelude.dart';
-
-typedef NavigationCallback = void Function(String id);
-
-abstract class NavigationItem {
-  Widget get leftBarItem;
-  Widget? get rightBarItem => null;
-  String get identifier;
-
-  NavigationCallback get action => (id) {
-        getIt<HomeStackManager>().setStackWithId(id);
-      };
-}
-
-enum HomeStackType {
-  blank,
-  doc,
-  trash,
-}
-
-List<HomeStackType> pages = HomeStackType.values.toList();
-
-abstract class HomeStackContext<T, S> with NavigationItem {
-  List<NavigationItem> get navigationItems;
-
-  @override
-  Widget get leftBarItem;
-
-  @override
-  Widget? get rightBarItem;
-
-  @override
-  String get identifier;
-
-  ValueNotifier<T> get isUpdated;
-
-  HomeStackType get type;
-
-  Widget buildWidget();
-
-  void dispose();
-}
-
-class HomeStackNotifier extends ChangeNotifier {
-  HomeStackContext stackContext;
-  PublishNotifier<bool> collapsedNotifier = PublishNotifier();
-
-  Widget get titleWidget => stackContext.leftBarItem;
-
-  HomeStackNotifier({HomeStackContext? context}) : stackContext = context ?? BlankStackContext();
-
-  set context(HomeStackContext context) {
-    if (stackContext.identifier == context.identifier) {
-      return;
-    }
-
-    stackContext.isUpdated.removeListener(notifyListeners);
-    stackContext.dispose();
-
-    stackContext = context;
-    stackContext.isUpdated.addListener(notifyListeners);
-    notifyListeners();
-  }
-
-  HomeStackContext get context => stackContext;
-}
-
-// HomeStack is initialized as singleton to control the page stack.
-class HomeStackManager {
-  final HomeStackNotifier _notifier = HomeStackNotifier();
-  HomeStackManager();
-
-  Widget title() {
-    return _notifier.context.leftBarItem;
-  }
-
-  PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;
-
-  void setStack(HomeStackContext context) {
-    _notifier.context = context;
-  }
-
-  void setStackWithId(String id) {}
-
-  Widget stackTopBar() {
-    return MultiProvider(
-      providers: [
-        ChangeNotifierProvider.value(value: _notifier),
-      ],
-      child: Selector<HomeStackNotifier, Widget>(
-        selector: (context, notifier) => notifier.titleWidget,
-        builder: (context, widget, child) {
-          return const HomeTopBar();
-        },
-      ),
-    );
-  }
-
-  Widget stackWidget() {
-    return MultiProvider(
-      providers: [
-        ChangeNotifierProvider.value(value: _notifier),
-      ],
-      child: Consumer(builder: (ctx, HomeStackNotifier notifier, child) {
-        return FadingIndexedStack(
-          index: pages.indexOf(notifier.context.type),
-          children: HomeStackType.values.map((viewType) {
-            if (viewType == notifier.context.type) {
-              return notifier.context.buildWidget();
-            } else {
-              return const BlankStackPage();
-            }
-          }).toList(),
-        );
-      }),
-    );
-  }
-}

+ 0 - 30
frontend/app_flowy/lib/workspace/domain/view_ext.dart

@@ -1,30 +0,0 @@
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/blank/blank_page.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/doc/doc_stack_page.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-
-extension ToHomeStackContext on View {
-  HomeStackContext stackContext() {
-    switch (viewType) {
-      case ViewType.Blank:
-        return BlankStackContext();
-      case ViewType.Doc:
-        return DocStackContext(view: this);
-      default:
-        return BlankStackContext();
-    }
-  }
-}
-
-extension ToHomeStackType on View {
-  HomeStackType stackType() {
-    switch (viewType) {
-      case ViewType.Blank:
-        return HomeStackType.blank;
-      case ViewType.Doc:
-        return HomeStackType.doc;
-      default:
-        return HomeStackType.blank;
-    }
-  }
-}

+ 8 - 5
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -1,9 +1,8 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
 import 'package:app_flowy/workspace/application/home/home_listen_bloc.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/widgets/edit_pannel/pannel_animation.dart';
 import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
-import 'package:app_flowy/workspace/presentation/widgets/prelude.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_infra_ui/style_widget/container.dart';
@@ -12,9 +11,12 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/protobuf.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
-import 'package:app_flowy/workspace/domain/view_ext.dart';
+
+import '../widgets/edit_pannel/edit_pannel.dart';
 
 import 'home_layout.dart';
+import 'home_stack.dart';
+import 'menu/menu.dart';
 
 class HomeScreen extends StatefulWidget {
   static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
@@ -109,7 +111,8 @@ class _HomeScreenState extends State<HomeScreen> {
   Widget _buildHomeMenu({required HomeLayout layout, required BuildContext context}) {
     if (initialView == null && widget.workspaceSetting.hasLatestView()) {
       initialView = widget.workspaceSetting.latestView;
-      getIt<HomeStackManager>().setStack(initialView!.stackContext());
+      final plugin = makePlugin(pluginType: initialView!.pluginType, data: initialView);
+      getIt<HomeStackManager>().setPlugin(plugin);
     }
 
     HomeMenu homeMenu = HomeMenu(

+ 213 - 0
frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart

@@ -0,0 +1,213 @@
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:time/time.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
+import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
+import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
+import 'package:app_flowy/workspace/presentation/home/navigation.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_infra_ui/style_widget/extension.dart';
+import 'package:flowy_infra/notifier.dart';
+
+typedef NavigationCallback = void Function(String id);
+
+late FToast fToast;
+
+class HomeStack extends StatelessWidget {
+  static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
+  // final Size size;
+  const HomeStack({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    Log.info('HomePage build');
+    final theme = context.watch<AppTheme>();
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      children: [
+        getIt<HomeStackManager>().stackTopBar(),
+        Expanded(
+          child: Container(
+            color: theme.surface,
+            child: FocusTraversalGroup(
+              child: getIt<HomeStackManager>().stackWidget(),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class FadingIndexedStack extends StatefulWidget {
+  final int index;
+  final List<Widget> children;
+  final Duration duration;
+
+  const FadingIndexedStack({
+    Key? key,
+    required this.index,
+    required this.children,
+    this.duration = const Duration(
+      milliseconds: 250,
+    ),
+  }) : super(key: key);
+
+  @override
+  _FadingIndexedStackState createState() => _FadingIndexedStackState();
+}
+
+class _FadingIndexedStackState extends State<FadingIndexedStack> {
+  double _targetOpacity = 1;
+
+  @override
+  void initState() {
+    super.initState();
+    fToast = FToast();
+    fToast.init(HomeScreen.scaffoldKey.currentState!.context);
+  }
+
+  @override
+  void didUpdateWidget(FadingIndexedStack oldWidget) {
+    if (oldWidget.index == widget.index) return;
+    setState(() => _targetOpacity = 0);
+    Future.delayed(1.milliseconds, () => setState(() => _targetOpacity = 1));
+    super.didUpdateWidget(oldWidget);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return TweenAnimationBuilder<double>(
+      duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds,
+      tween: Tween(begin: 0, end: _targetOpacity),
+      builder: (_, value, child) {
+        return Opacity(opacity: value, child: child);
+      },
+      child: IndexedStack(index: widget.index, children: widget.children),
+    );
+  }
+}
+
+abstract class NavigationItem {
+  Widget get leftBarItem;
+  Widget? get rightBarItem => null;
+
+  NavigationCallback get action => (id) {
+        getIt<HomeStackManager>().setStackWithId(id);
+      };
+}
+
+class HomeStackNotifier extends ChangeNotifier {
+  Plugin _plugin;
+  PublishNotifier<bool> collapsedNotifier = PublishNotifier();
+
+  Widget get titleWidget => _plugin.pluginDisplay.leftBarItem;
+
+  HomeStackNotifier({Plugin? plugin}) : _plugin = plugin ?? makePlugin(pluginType: DefaultPlugin.blank.type());
+
+  set plugin(Plugin newPlugin) {
+    if (newPlugin.pluginId == _plugin.pluginId) {
+      return;
+    }
+
+    _plugin.displayNotifier?.removeListener(notifyListeners);
+    _plugin.dispose();
+
+    _plugin = newPlugin;
+    _plugin.displayNotifier?.addListener(notifyListeners);
+    notifyListeners();
+  }
+
+  Plugin get plugin => _plugin;
+}
+
+// HomeStack is initialized as singleton to controll the page stack.
+class HomeStackManager {
+  final HomeStackNotifier _notifier = HomeStackNotifier();
+  HomeStackManager();
+
+  Widget title() {
+    return _notifier.plugin.pluginDisplay.leftBarItem;
+  }
+
+  PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;
+
+  void setPlugin(Plugin newPlugin) {
+    _notifier.plugin = newPlugin;
+  }
+
+  void setStackWithId(String id) {}
+
+  Widget stackTopBar() {
+    return MultiProvider(
+      providers: [
+        ChangeNotifierProvider.value(value: _notifier),
+      ],
+      child: Selector<HomeStackNotifier, Widget>(
+        selector: (context, notifier) => notifier.titleWidget,
+        builder: (context, widget, child) {
+          return const HomeTopBar();
+        },
+      ),
+    );
+  }
+
+  Widget stackWidget() {
+    return MultiProvider(
+      providers: [
+        ChangeNotifierProvider.value(value: _notifier),
+      ],
+      child: Consumer(builder: (ctx, HomeStackNotifier notifier, child) {
+        return FadingIndexedStack(
+          index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
+          children: getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
+            if (pluginType == notifier.plugin.pluginType) {
+              return notifier.plugin.pluginDisplay.buildWidget();
+            } else {
+              return const BlankStackPage();
+            }
+          }).toList(),
+        );
+      }),
+    );
+  }
+}
+
+class HomeTopBar extends StatelessWidget {
+  const HomeTopBar({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return Container(
+      color: theme.surface,
+      height: HomeSizes.topBarHeight,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          const FlowyNavigation(),
+          const HSpace(16),
+          ChangeNotifierProvider.value(
+            value: Provider.of<HomeStackNotifier>(context, listen: false),
+            child: Consumer(
+              builder: (BuildContext context, HomeStackNotifier notifier, Widget? child) {
+                return notifier.plugin.pluginDisplay.rightBarItem ?? const SizedBox();
+              },
+            ),
+          ) // _renderMoreButton(),
+        ],
+      )
+          .padding(
+            horizontal: HomeInsets.topBarTitlePadding,
+          )
+          .bottomBorder(color: Colors.grey.shade300),
+    );
+  }
+}

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/create_button.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/create_button.dart


+ 18 - 15
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/add_button.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart

@@ -1,16 +1,16 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 
 class AddButton extends StatelessWidget {
-  final Function(ViewType) onSelected;
+  final Function(PluginBuilder) onSelected;
   const AddButton({
     Key? key,
     required this.onSelected,
@@ -34,21 +34,24 @@ class AddButton extends StatelessWidget {
 }
 
 class ActionList {
-  final Function(ViewType) onSelected;
+  final Function(PluginBuilder) onSelected;
   final BuildContext anchorContext;
   final String _identifier = 'DisclosureButtonActionList';
 
   const ActionList({required this.anchorContext, required this.onSelected});
 
   void show(BuildContext buildContext) {
-    final items = ViewType.values.where((element) => element != ViewType.Blank).map((ty) {
-      return CreateItem(
-          viewType: ty,
-          onSelected: (viewType) {
+    final items = pluginBuilders().map(
+      (pluginBuilder) {
+        return CreateItem(
+          pluginBuilder: pluginBuilder,
+          onSelected: (builder) {
             FlowyOverlay.of(buildContext).remove(_identifier);
-            onSelected(viewType);
-          });
-    }).toList();
+            onSelected(builder);
+          },
+        );
+      },
+    ).toList();
 
     ListOverlay.showWithAnchor(
       buildContext,
@@ -64,11 +67,11 @@ class ActionList {
 }
 
 class CreateItem extends StatelessWidget {
-  final ViewType viewType;
-  final Function(ViewType) onSelected;
+  final PluginBuilder pluginBuilder;
+  final Function(PluginBuilder) onSelected;
   const CreateItem({
     Key? key,
-    required this.viewType,
+    required this.pluginBuilder,
     required this.onSelected,
   }) : super(key: key);
 
@@ -81,9 +84,9 @@ class CreateItem extends StatelessWidget {
       config: config,
       builder: (context, onHover) {
         return GestureDetector(
-          onTap: () => onSelected(viewType),
+          onTap: () => onSelected(pluginBuilder),
           child: FlowyText.medium(
-            viewType.name,
+            pluginBuilder.menuName,
             color: theme.textColor,
             fontSize: 12,
           ).padding(horizontal: 10, vertical: 6),

+ 34 - 5
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -1,4 +1,3 @@
-import 'package:app_flowy/workspace/domain/edit_action/app_edit.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:expandable/expandable.dart';
@@ -13,6 +12,8 @@ import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'package:dartz/dartz.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flowy_infra/image.dart';
+
 import '../menu_app.dart';
 import 'add_button.dart';
 import 'right_click_action.dart';
@@ -102,10 +103,13 @@ class MenuAppHeader extends StatelessWidget {
     return Tooltip(
       message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
       child: AddButton(
-        onSelected: (viewType) {
-          context
-              .read<AppBloc>()
-              .add(AppEvent.createView(LocaleKeys.menuAppHeader_defaultNewPageName.tr(), "", viewType));
+        onSelected: (pluginBuilder) {
+          context.read<AppBloc>().add(AppEvent.createView(
+                LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                "",
+                pluginBuilder.dataType,
+                pluginBuilder.pluginType,
+              ));
         },
       ).padding(right: MenuAppSizes.headerPadding),
     );
@@ -131,3 +135,28 @@ class MenuAppHeader extends StatelessWidget {
     });
   }
 }
+
+enum AppDisclosureAction {
+  rename,
+  delete,
+}
+
+extension AppDisclosureExtension on AppDisclosureAction {
+  String get name {
+    switch (this) {
+      case AppDisclosureAction.rename:
+        return LocaleKeys.disclosureAction_rename.tr();
+      case AppDisclosureAction.delete:
+        return LocaleKeys.disclosureAction_delete.tr();
+    }
+  }
+
+  Widget get icon {
+    switch (this) {
+      case AppDisclosureAction.rename:
+        return svg('editor/edit', color: const Color(0xffe5e5e5));
+      case AppDisclosureAction.delete:
+        return svg('editor/delete', color: const Color(0xffe5e5e5));
+    }
+  }
+}

+ 2 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/right_click_action.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/right_click_action.dart

@@ -1,9 +1,10 @@
-import 'package:app_flowy/workspace/domain/edit_action/app_edit.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 
+import 'header.dart';
+
 class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper> implements FlowyOverlayDelegate {
   final Function(dartz.Option<AppDisclosureAction>) onSelected;
   final _items = AppDisclosureAction.values.map((action) => DisclosureActionWrapper(action)).toList();

+ 1 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart

@@ -1,6 +1,5 @@
 import 'package:app_flowy/workspace/application/appearance.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header/header.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';

+ 2 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/disclosure_action.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart

@@ -1,4 +1,3 @@
-import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:flowy_infra/image.dart';
@@ -8,6 +7,8 @@ import 'package:flutter/material.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:provider/provider.dart';
 
+import 'item.dart';
+
 // [[Widget: LifeCycle]]
 // https://flutterbyexample.com/lesson/stateful-widget-lifecycle
 class ViewDisclosureButton extends StatelessWidget

+ 34 - 4
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/view/view_bloc.dart';
-import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
+import 'package:app_flowy/workspace/application/view/view_ext.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
@@ -12,9 +13,8 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
-import 'package:app_flowy/workspace/domain/image.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flowy_infra/image.dart';
 
 import 'disclosure_action.dart';
 
@@ -55,7 +55,7 @@ class ViewSectionItem extends StatelessWidget {
 
   Widget _render(BuildContext context, bool onHover, ViewState state, Color iconColor) {
     List<Widget> children = [
-      SizedBox(width: 16, height: 16, child: state.view.thumbnail(iconColor: iconColor)),
+      SizedBox(width: 16, height: 16, child: state.view.renderThumbnail(iconColor: iconColor)),
       const HSpace(2),
       Expanded(child: FlowyText.regular(state.view.name, fontSize: 12, overflow: TextOverflow.clip)),
     ];
@@ -104,3 +104,33 @@ class ViewSectionItem extends StatelessWidget {
     });
   }
 }
+
+enum ViewDisclosureAction {
+  rename,
+  delete,
+  duplicate,
+}
+
+extension ViewDisclosureExtension on ViewDisclosureAction {
+  String get name {
+    switch (this) {
+      case ViewDisclosureAction.rename:
+        return LocaleKeys.disclosureAction_rename.tr();
+      case ViewDisclosureAction.delete:
+        return LocaleKeys.disclosureAction_delete.tr();
+      case ViewDisclosureAction.duplicate:
+        return LocaleKeys.disclosureAction_duplicate.tr();
+    }
+  }
+
+  Widget get icon {
+    switch (this) {
+      case ViewDisclosureAction.rename:
+        return svg('editor/edit', color: const Color(0xff999999));
+      case ViewDisclosureAction.delete:
+        return svg('editor/delete', color: const Color(0xff999999));
+      case ViewDisclosureAction.duplicate:
+        return svg('editor/copy', color: const Color(0xff999999));
+    }
+  }
+}

+ 4 - 5
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/section.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart

@@ -1,8 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/domain/view_ext.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
+import 'package:app_flowy/workspace/application/view/view_ext.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -106,7 +105,7 @@ class ViewSectionNotifier with ChangeNotifier {
 
     if (view != null) {
       WidgetsBinding.instance?.addPostFrameCallback((_) {
-        getIt<HomeStackManager>().setStack(view.stackContext());
+        getIt<HomeStackManager>().setPlugin(view.plugin());
       });
     } else {
       // do nothing

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/favorite/favorite.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/favorite.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/favorite/header.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/header.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/favorite/section.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/favorite/section.dart


+ 42 - 28
frontend/app_flowy/lib/workspace/presentation/widgets/menu/menu.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart

@@ -1,4 +1,8 @@
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/top_bar.dart';
+export './app/header/header.dart';
+export './app/menu_app.dart';
+
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme.dart';
@@ -15,32 +19,13 @@ import 'package:expandable/expandable.dart';
 import 'package:flowy_infra/time/duration.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/widget/menu_user.dart';
-
-import 'widget/app/menu_app.dart';
-import 'widget/app/create_button.dart';
-import 'widget/menu_trash.dart';
+import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 
-// [[diagram: HomeMenu's widget structure]]
-//                                                                                    get user profile or modify user
-//                                                                                   ┌──────┐
-//                 ┌──────────┐                                                  ┌──▶│IUser │
-//              ┌─▶│MenuTopBar│                     ┌────────┐  ┌─────────────┐  │   └──────┘
-//              │  └──────────┘                 ┌───│MenuUser│─▶│MenuUserBloc │──┤
-// ┌──────────┐ │                               │   └────────┘  └─────────────┘  │   ┌─────────────┐
-// │ HomeMenu │─┤                               │                                └──▶│IUserListener│
-// └──────────┘ │                               │                                    └─────────────┘
-//              │                               │                                    listen workspace changes or user
-//              │                         impl  │                                    profile changes
-//              │  ┌──────────┐    ┌─────────┐  │
-//              └─▶│ MenuList │───▶│MenuItem │◀─┤
-//                 └──────────┘    └─────────┘  │                  ┌────────┐
-//                                              │               ┌─▶│AppBloc │  fetch app's views or modify view
-//                                              │               │  └────────┘
-//                                              │   ┌────────┐  │
-//                                              └───│MenuApp │──┤
-//                                                  └────────┘
+import 'app/menu_app.dart';
+import 'app/create_button.dart';
+import 'menu_user.dart';
 
 class HomeMenu extends StatelessWidget {
   final PublishNotifier<bool> _collapsedNotifier;
@@ -70,9 +55,9 @@ class HomeMenu extends StatelessWidget {
       child: MultiBlocListener(
         listeners: [
           BlocListener<MenuBloc, MenuState>(
-            listenWhen: (p, c) => p.stackContext != c.stackContext,
+            listenWhen: (p, c) => p.plugin.pluginId != c.plugin.pluginId,
             listener: (context, state) {
-              getIt<HomeStackManager>().setStack(state.stackContext);
+              getIt<HomeStackManager>().setPlugin(state.plugin);
             },
           ),
           BlocListener<MenuBloc, MenuState>(
@@ -181,3 +166,32 @@ class MenuSharedState extends ChangeNotifier {
     });
   }
 }
+
+class MenuTopBar extends StatelessWidget {
+  const MenuTopBar({Key? key}) : super(key: key);
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return BlocBuilder<MenuBloc, MenuState>(
+      builder: (context, state) {
+        return SizedBox(
+          height: HomeSizes.topBarHeight,
+          child: Row(
+            children: [
+              (theme.isDark
+                  ? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
+                  : svgWithSize("flowy_logo_with_text", const Size(92, 17))),
+              const Spacer(),
+              FlowyIconButton(
+                width: 28,
+                onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
+                iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
+                icon: svg("home/hide_menu", color: theme.iconColor),
+              )
+            ],
+          ),
+        );
+      },
+    );
+  }
+}

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_user.dart → frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart


+ 4 - 7
frontend/app_flowy/lib/workspace/presentation/home/navigation.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_infra/theme.dart';
@@ -17,8 +17,8 @@ class NavigationNotifier with ChangeNotifier {
 
   void update(HomeStackNotifier notifier) {
     bool shouldNotify = false;
-    if (navigationItems != notifier.context.navigationItems) {
-      navigationItems = notifier.context.navigationItems;
+    if (navigationItems != notifier.plugin.pluginDisplay.navigationItems) {
+      navigationItems = notifier.plugin.pluginDisplay.navigationItems;
       shouldNotify = true;
     }
 
@@ -59,7 +59,7 @@ class FlowyNavigation extends StatelessWidget {
       create: (_) {
         final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
         return NavigationNotifier(
-          navigationItems: notifier.context.navigationItems,
+          navigationItems: notifier.plugin.pluginDisplay.navigationItems,
           collapasedNotifier: notifier.collapsedNotifier,
         );
       },
@@ -179,7 +179,4 @@ class EllipsisNaviItem extends NavigationItem {
 
   @override
   NavigationCallback get action => (id) {};
-
-  @override
-  String get identifier => "Ellipsis";
 }

+ 34 - 13
frontend/app_flowy/lib/workspace/presentation/stack_page/blank/blank_page.dart → frontend/app_flowy/lib/workspace/presentation/plugins/blank/blank.dart

@@ -1,37 +1,58 @@
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
+
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugin/plugin.dart';
 
-class BlankStackContext extends HomeStackContext {
-  final ValueNotifier<bool> _isUpdated = ValueNotifier<bool>(false);
+class BlankPluginBuilder extends PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    return BlankPagePlugin(pluginType: pluginType);
+  }
 
   @override
-  String get identifier => "1";
+  String get menuName => "Blank";
 
   @override
-  Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr(), fontSize: 12);
+  PluginType get pluginType => DefaultPlugin.blank.type();
+}
 
+class BlankPluginConfig implements PluginConfig {
   @override
-  Widget? get rightBarItem => null;
+  bool get creatable => false;
+}
+
+class BlankPagePlugin extends Plugin {
+  final PluginType _pluginType;
+  BlankPagePlugin({
+    required PluginType pluginType,
+  }) : _pluginType = pluginType;
 
   @override
-  HomeStackType get type => HomeStackType.blank;
+  void dispose() {}
 
   @override
-  Widget buildWidget() {
-    return const BlankStackPage();
-  }
+  PluginDisplay get pluginDisplay => BlankPagePluginDisplay();
 
   @override
-  List<NavigationItem> get navigationItems => [this];
+  PluginId get pluginId => "BlankStack";
 
   @override
-  ValueNotifier<bool> get isUpdated => _isUpdated;
+  PluginType get pluginType => _pluginType;
+}
 
+class BlankPagePluginDisplay extends PluginDisplay {
   @override
-  void dispose() {}
+  Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr(), fontSize: 12);
+
+  @override
+  Widget buildWidget() => const BlankStackPage();
+
+  @override
+  List<NavigationItem> get navigationItems => [this];
 }
 
 class BlankStackPage extends StatefulWidget {

+ 61 - 25
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart

@@ -1,10 +1,18 @@
+library docuemnt_plugin;
+
+export './src/document_page.dart';
+export './src/widget/toolbar/history_button.dart';
+export './src/widget/toolbar/toolbar_icon_button.dart';
+export './src/widget/toolbar/tool_bar.dart';
+
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/application/doc/share_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_listener.dart';
 import 'package:app_flowy/workspace/application/view/view_service.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/domain/view_ext.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -23,62 +31,90 @@ import 'package:clipboard/clipboard.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:provider/provider.dart';
 
-import 'document_page.dart';
+import 'src/document_page.dart';
+
+class DocumentPluginBuilder extends PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    if (data is View) {
+      return DocumentPlugin(pluginType: pluginType, view: data);
+    } else {
+      throw FlowyPluginException.invalidData;
+    }
+  }
+
+  @override
+  String get menuName => "Doc";
+
+  @override
+  PluginType get pluginType => DefaultPlugin.quillEditor.type();
+
+  @override
+  ViewDataType get dataType => ViewDataType.RichText;
+}
 
-class DocStackContext extends HomeStackContext<int, ShareActionWrapper> {
-  View _view;
-  late ViewListener _listener;
-  final ValueNotifier<int> _isUpdated = ValueNotifier<int>(0);
+class DocumentPlugin implements Plugin {
+  late View _view;
+  ViewListener? _listener;
+  final ValueNotifier<int> _displayNotifier = ValueNotifier<int>(0);
+  late PluginType _pluginType;
 
-  DocStackContext({required View view, Key? key}) : _view = view {
+  DocumentPlugin({required PluginType pluginType, required View view, Key? key}) : _view = view {
+    _pluginType = pluginType;
     _listener = getIt<ViewListener>(param1: view);
-    _listener.updatedNotifier.addPublishListener((result) {
+    _listener?.updatedNotifier.addPublishListener((result) {
       result.fold(
         (newView) {
           _view = newView;
-          _isUpdated.value = _view.hashCode;
+          _displayNotifier.value = _view.hashCode;
         },
         (error) {},
       );
     });
-    _listener.start();
+    _listener?.start();
   }
 
   @override
-  Widget get leftBarItem => DocumentLeftBarItem(view: _view);
+  void dispose() {
+    _listener?.close();
+    _listener = null;
+  }
 
   @override
-  Widget? get rightBarItem => DocumentShareButton(view: _view);
+  PluginDisplay get pluginDisplay => DocumentPluginDisplay(view: _view);
+
+  @override
+  PluginType get pluginType => _pluginType;
 
   @override
-  String get identifier => _view.id;
+  PluginId get pluginId => _view.id;
 
   @override
-  HomeStackType get type => _view.stackType();
+  ChangeNotifier? get displayNotifier => _displayNotifier;
+}
+
+class DocumentPluginDisplay extends PluginDisplay {
+  final View _view;
+
+  DocumentPluginDisplay({required View view, Key? key}) : _view = view;
 
   @override
   Widget buildWidget() => DocumentPage(view: _view, key: ValueKey(_view.id));
 
   @override
-  List<NavigationItem> get navigationItems => _makeNavigationItems();
+  Widget get leftBarItem => DocumentLeftBarItem(view: _view);
 
   @override
-  ValueNotifier<int> get isUpdated => _isUpdated;
+  Widget? get rightBarItem => DocumentShareButton(view: _view);
 
-  // List<NavigationItem> get navigationItems => naviStacks.map((stack) {
-  //       return NavigationItemImpl(context: stack);
-  //     }).toList();
+  @override
+  List<NavigationItem> get navigationItems => _makeNavigationItems();
 
   List<NavigationItem> _makeNavigationItems() {
     return [
       this,
     ];
   }
-
-  @override
-  void dispose() {
-    _listener.close();
-  }
 }
 
 class DocumentLeftBarItem extends StatefulWidget {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/document_page.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/application/doc/doc_bloc.dart';
+import 'package:app_flowy/workspace/presentation/plugins/doc/document.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter_quill/flutter_quill.dart' as quill;
@@ -12,7 +13,6 @@ import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'styles.dart';
 import 'widget/banner.dart';
-import 'widget/toolbar/tool_bar.dart';
 
 class DocumentPage extends StatefulWidget {
   final View view;

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/styles.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/styles.dart

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
 import 'package:tuple/tuple.dart';
 import 'package:flowy_infra/theme.dart';
 
-import 'widget/style_widgets/style_widgets.dart';
+import 'widget/style_widgets.dart';
 
 DefaultStyles customStyles(BuildContext context) {
   const baseSpacing = Tuple2<double, double>(6, 0);

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/banner.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/banner.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/style_widgets/style_widgets.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/style_widgets.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/check_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/check_button.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/color_picker.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/color_picker.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/header_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/header_button.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/history_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/history_button.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/image_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/image_button.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/link_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/link_button.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/toggle_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/toggle_button.dart


+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/tool_bar.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/tool_bar.dart

@@ -1,7 +1,6 @@
 import 'dart:async';
 import 'dart:math';
 
-import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/history_button.dart';
 import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter_quill/flutter_quill.dart';
@@ -10,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
 import 'check_button.dart';
 import 'color_picker.dart';
 import 'header_button.dart';
+import 'history_button.dart';
 import 'link_button.dart';
 import 'toggle_button.dart';
 import 'toolbar_icon_button.dart';

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/toolbar_icon_button.dart → frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/widget/toolbar/toolbar_icon_button.dart


+ 5 - 4
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_trash.dart → frontend/app_flowy/lib/workspace/presentation/plugins/trash/menu.dart

@@ -1,8 +1,9 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/trash/trash_page.dart';
-import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -22,7 +23,7 @@ class MenuTrash extends StatelessWidget {
       child: InkWell(
         onTap: () {
           Provider.of<MenuSharedState>(context, listen: false).selectedView.value = null;
-          getIt<HomeStackManager>().setStack(TrashStackContext());
+          getIt<HomeStackManager>().setPlugin(makePlugin(pluginType: DefaultPlugin.trash.type()));
         },
         child: _render(context),
       ),

+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/trash/widget/sizes.dart → frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/sizes.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/trash/widget/trash_cell.dart → frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart


+ 0 - 0
frontend/app_flowy/lib/workspace/presentation/stack_page/trash/widget/trash_header.dart → frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_header.dart


+ 42 - 16
frontend/app_flowy/lib/workspace/presentation/stack_page/trash/trash_page.dart → frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart

@@ -1,8 +1,12 @@
+export "./src/sizes.dart";
+export "./src/trash_cell.dart";
+export "./src/trash_header.dart";
+
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/trash/trash_bloc.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/trash/widget/sizes.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/trash/widget/trash_cell.dart';
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -17,36 +21,58 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 
-import 'widget/trash_header.dart';
+import 'src/sizes.dart';
+import 'src/trash_cell.dart';
+import 'src/trash_header.dart';
 
-class TrashStackContext extends HomeStackContext {
-  final ValueNotifier<bool> _isUpdated = ValueNotifier<bool>(false);
+class TrashPluginBuilder extends PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    return TrashPlugin(pluginType: pluginType);
+  }
 
   @override
-  String get identifier => "TrashStackContext";
+  String get menuName => "Trash";
 
   @override
-  Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12);
+  PluginType get pluginType => DefaultPlugin.trash.type();
+}
 
+class TrashPluginConfig implements PluginConfig {
   @override
-  Widget? get rightBarItem => null;
+  bool get creatable => false;
+}
+
+class TrashPlugin extends Plugin {
+  final PluginType _pluginType;
+
+  TrashPlugin({required PluginType pluginType}) : _pluginType = pluginType;
 
   @override
-  HomeStackType get type => HomeStackType.trash;
+  void dispose() {}
 
   @override
-  Widget buildWidget() {
-    return const TrashStackPage(key: ValueKey('TrashStackPage'));
-  }
+  PluginDisplay get pluginDisplay => TrashPluginDisplay();
 
   @override
-  List<NavigationItem> get navigationItems => [this];
+  PluginId get pluginId => "TrashStack";
 
   @override
-  ValueNotifier<bool> get isUpdated => _isUpdated;
+  PluginType get pluginType => _pluginType;
+}
 
+class TrashPluginDisplay extends PluginDisplay {
   @override
-  void dispose() {}
+  Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12);
+
+  @override
+  Widget? get rightBarItem => null;
+
+  @override
+  Widget buildWidget() => const TrashStackPage(key: ValueKey('TrashStackPage'));
+
+  @override
+  List<NavigationItem> get navigationItems => [this];
 }
 
 class TrashStackPage extends StatefulWidget {

+ 0 - 97
frontend/app_flowy/lib/workspace/presentation/stack_page/home_stack.dart

@@ -1,97 +0,0 @@
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:flowy_sdk/log.dart';
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-import 'package:time/time.dart';
-import 'package:fluttertoast/fluttertoast.dart';
-
-late FToast fToast;
-
-// [[diagram: HomeStack's widget structure]]
-//
-//                                                               ┌──────────────────┐   ┌───────────────┐
-//                                                            ┌──│BlankStackContext │──▶│BlankStackPage │
-// ┌──────────┐  ┌───────────────────┐   ┌─────────────────┐  │  └──────────────────┘   └───────────────┘
-// │HomeStack │─▶│ HomeStackManager  │──▶│HomeStackContext │◀─┤
-// └──────────┘  └───────────────────┘   └─────────────────┘  │  ┌─────────────────┐    ┌────────────┐
-//                                                            └──│ DocStackContext │───▶│DocStackPage│
-//                                                               └─────────────────┘    └────────────┘
-//
-//
-class HomeStack extends StatelessWidget {
-  static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
-  // final Size size;
-  const HomeStack({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    Log.info('HomePage build');
-    final theme = context.watch<AppTheme>();
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.start,
-      children: [
-        getIt<HomeStackManager>().stackTopBar(),
-        Expanded(
-          child: Container(
-            color: theme.surface,
-            child: FocusTraversalGroup(
-              child: getIt<HomeStackManager>().stackWidget(),
-            ),
-          ),
-        ),
-      ],
-    );
-  }
-}
-
-class FadingIndexedStack extends StatefulWidget {
-  final int index;
-  final List<Widget> children;
-  final Duration duration;
-
-  const FadingIndexedStack({
-    Key? key,
-    required this.index,
-    required this.children,
-    this.duration = const Duration(
-      milliseconds: 250,
-    ),
-  }) : super(key: key);
-
-  @override
-  _FadingIndexedStackState createState() => _FadingIndexedStackState();
-}
-
-class _FadingIndexedStackState extends State<FadingIndexedStack> {
-  double _targetOpacity = 1;
-
-  @override
-  void initState() {
-    super.initState();
-    fToast = FToast();
-    fToast.init(HomeScreen.scaffoldKey.currentState!.context);
-  }
-
-  @override
-  void didUpdateWidget(FadingIndexedStack oldWidget) {
-    if (oldWidget.index == widget.index) return;
-    setState(() => _targetOpacity = 0);
-    Future.delayed(1.milliseconds, () => setState(() => _targetOpacity = 1));
-    super.didUpdateWidget(oldWidget);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return TweenAnimationBuilder<double>(
-      duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds,
-      tween: Tween(begin: 0, end: _targetOpacity),
-      builder: (_, value, child) {
-        return Opacity(opacity: value, child: child);
-      },
-      child: IndexedStack(index: widget.index, children: widget.children),
-    );
-  }
-}

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/edit_pannel/edit_pannel.dart

@@ -1,5 +1,5 @@
 import 'package:app_flowy/workspace/application/edit_pannel/edit_pannel_bloc.dart';
-import 'package:app_flowy/workspace/domain/edit_context.dart';
+import 'package:app_flowy/workspace/application/edit_pannel/edit_context.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
 import 'package:dartz/dartz.dart';

+ 1 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart

@@ -1,7 +1,6 @@
+import 'package:app_flowy/workspace/presentation/plugins/doc/document.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_quill/flutter_quill.dart';
-
-import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/toolbar_icon_button.dart';
 import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
 
 class FlowyEmojiStyleButton extends StatefulWidget {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme.dart';
@@ -16,7 +17,6 @@ import 'package:url_launcher/url_launcher.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:fluttertoast/fluttertoast.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/home_stack.dart';
 
 class QuestionBubble extends StatelessWidget {
   const QuestionBubble({Key? key}) : super(key: key);

+ 0 - 67
frontend/app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart

@@ -1,67 +0,0 @@
-import 'package:app_flowy/workspace/domain/image.dart';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
-import 'package:app_flowy/workspace/presentation/home/navigation.dart';
-import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flutter/material.dart';
-import 'package:flowy_infra_ui/style_widget/extension.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:provider/provider.dart';
-
-class HomeTopBar extends StatelessWidget {
-  const HomeTopBar({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return Container(
-      color: theme.surface,
-      height: HomeSizes.topBarHeight,
-      child: Row(
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: [
-          const FlowyNavigation(),
-          const HSpace(16),
-          ChangeNotifierProvider.value(
-            value: Provider.of<HomeStackNotifier>(context, listen: false),
-            child: Consumer(
-              builder: (BuildContext context, HomeStackNotifier notifier, Widget? child) {
-                return notifier.stackContext.rightBarItem ?? const SizedBox();
-              },
-            ),
-          ) // _renderMoreButton(),
-        ],
-      )
-          .padding(
-            horizontal: HomeInsets.topBarTitlePadding,
-          )
-          .bottomBorder(color: Colors.grey.shade300),
-    );
-  }
-}
-
-class HomeTitle extends StatelessWidget {
-  final String title;
-  final ViewType type;
-
-  const HomeTitle({
-    Key? key,
-    required this.title,
-    required this.type,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Flexible(
-      child: Row(
-        children: [
-          Image(fit: BoxFit.scaleDown, width: 15, height: 15, image: assetImageForViewType(type)),
-          const HSpace(6),
-          FlowyText(title, fontSize: 16),
-        ],
-      ),
-    );
-  }
-}

+ 0 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/prelude.dart

@@ -1 +0,0 @@
-export 'menu.dart';

+ 0 - 36
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/top_bar.dart

@@ -1,36 +0,0 @@
-import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
-import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flowy_infra/theme.dart';
-
-class MenuTopBar extends StatelessWidget {
-  const MenuTopBar({Key? key}) : super(key: key);
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    return BlocBuilder<MenuBloc, MenuState>(
-      builder: (context, state) {
-        return SizedBox(
-          height: HomeSizes.topBarHeight,
-          child: Row(
-            children: [
-              (theme.isDark
-                  ? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
-                  : svgWithSize("flowy_logo_with_text", const Size(92, 17))),
-              const Spacer(),
-              FlowyIconButton(
-                width: 28,
-                onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
-                iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
-                icon: svg("home/hide_menu", color: theme.iconColor),
-              )
-            ],
-          ),
-        );
-      },
-    );
-  }
-}

+ 0 - 5
frontend/app_flowy/lib/workspace/presentation/widgets/prelude.dart

@@ -1,5 +0,0 @@
-export '../stack_page/blank/blank_page.dart';
-export './edit_pannel/edit_pannel.dart';
-export './edit_pannel/pannel_animation.dart';
-export './home_top_bar.dart';
-export 'menu/menu.dart';

+ 5 - 5
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-folder/dart_event.dart

@@ -271,14 +271,14 @@ class FolderEventOpenView {
      ViewId request;
      FolderEventOpenView(this.request);
 
-    Future<Either<DocumentDelta, FlowyError>> send() {
+    Future<Either<BlockDelta, FlowyError>> send() {
     final request = FFIRequest.create()
           ..event = FolderEvent.OpenView.toString()
           ..payload = requestToBytes(this.request);
 
     return Dispatch.asyncRequest(request)
         .then((bytesResult) => bytesResult.fold(
-           (okBytes) => left(DocumentDelta.fromBuffer(okBytes)),
+           (okBytes) => left(BlockDelta.fromBuffer(okBytes)),
            (errBytes) => right(FlowyError.fromBuffer(errBytes)),
         ));
     }
@@ -378,17 +378,17 @@ class FolderEventDeleteAllTrash {
 }
 
 class FolderEventApplyDocDelta {
-     DocumentDelta request;
+     BlockDelta request;
      FolderEventApplyDocDelta(this.request);
 
-    Future<Either<DocumentDelta, FlowyError>> send() {
+    Future<Either<BlockDelta, FlowyError>> send() {
     final request = FFIRequest.create()
           ..event = FolderEvent.ApplyDocDelta.toString()
           ..payload = requestToBytes(this.request);
 
     return Dispatch.asyncRequest(request)
         .then((bytesResult) => bytesResult.fold(
-           (okBytes) => left(DocumentDelta.fromBuffer(okBytes)),
+           (okBytes) => left(BlockDelta.fromBuffer(okBytes)),
            (errBytes) => right(FlowyError.fromBuffer(errBytes)),
         ));
     }

+ 60 - 60
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-collaboration/document_info.pb.dart

@@ -12,15 +12,15 @@ import 'package:protobuf/protobuf.dart' as $pb;
 
 import 'revision.pb.dart' as $0;
 
-class CreateDocParams extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateDocParams', createEmptyInstance: create)
+class CreateBlockParams extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateBlockParams', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
     ..aOM<$0.RepeatedRevision>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revisions', subBuilder: $0.RepeatedRevision.create)
     ..hasRequiredFields = false
   ;
 
-  CreateDocParams._() : super();
-  factory CreateDocParams({
+  CreateBlockParams._() : super();
+  factory CreateBlockParams({
     $core.String? id,
     $0.RepeatedRevision? revisions,
   }) {
@@ -33,26 +33,26 @@ class CreateDocParams extends $pb.GeneratedMessage {
     }
     return _result;
   }
-  factory CreateDocParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
-  factory CreateDocParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  factory CreateBlockParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory CreateBlockParams.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
   'Will be removed in next major version')
-  CreateDocParams clone() => CreateDocParams()..mergeFromMessage(this);
+  CreateBlockParams clone() => CreateBlockParams()..mergeFromMessage(this);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
   'Will be removed in next major version')
-  CreateDocParams copyWith(void Function(CreateDocParams) updates) => super.copyWith((message) => updates(message as CreateDocParams)) as CreateDocParams; // ignore: deprecated_member_use
+  CreateBlockParams copyWith(void Function(CreateBlockParams) updates) => super.copyWith((message) => updates(message as CreateBlockParams)) as CreateBlockParams; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
-  static CreateDocParams create() => CreateDocParams._();
-  CreateDocParams createEmptyInstance() => create();
-  static $pb.PbList<CreateDocParams> createRepeated() => $pb.PbList<CreateDocParams>();
+  static CreateBlockParams create() => CreateBlockParams._();
+  CreateBlockParams createEmptyInstance() => create();
+  static $pb.PbList<CreateBlockParams> createRepeated() => $pb.PbList<CreateBlockParams>();
   @$core.pragma('dart2js:noInline')
-  static CreateDocParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateDocParams>(create);
-  static CreateDocParams? _defaultInstance;
+  static CreateBlockParams getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreateBlockParams>(create);
+  static CreateBlockParams? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get id => $_getSZ(0);
@@ -75,8 +75,8 @@ class CreateDocParams extends $pb.GeneratedMessage {
   $0.RepeatedRevision ensureRevisions() => $_ensure(1);
 }
 
-class DocumentInfo extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DocumentInfo', createEmptyInstance: create)
+class BlockInfo extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'BlockInfo', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'docId')
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'text')
     ..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revId')
@@ -84,8 +84,8 @@ class DocumentInfo extends $pb.GeneratedMessage {
     ..hasRequiredFields = false
   ;
 
-  DocumentInfo._() : super();
-  factory DocumentInfo({
+  BlockInfo._() : super();
+  factory BlockInfo({
     $core.String? docId,
     $core.String? text,
     $fixnum.Int64? revId,
@@ -106,26 +106,26 @@ class DocumentInfo extends $pb.GeneratedMessage {
     }
     return _result;
   }
-  factory DocumentInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
-  factory DocumentInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  factory BlockInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory BlockInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
   'Will be removed in next major version')
-  DocumentInfo clone() => DocumentInfo()..mergeFromMessage(this);
+  BlockInfo clone() => BlockInfo()..mergeFromMessage(this);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
   'Will be removed in next major version')
-  DocumentInfo copyWith(void Function(DocumentInfo) updates) => super.copyWith((message) => updates(message as DocumentInfo)) as DocumentInfo; // ignore: deprecated_member_use
+  BlockInfo copyWith(void Function(BlockInfo) updates) => super.copyWith((message) => updates(message as BlockInfo)) as BlockInfo; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
-  static DocumentInfo create() => DocumentInfo._();
-  DocumentInfo createEmptyInstance() => create();
-  static $pb.PbList<DocumentInfo> createRepeated() => $pb.PbList<DocumentInfo>();
+  static BlockInfo create() => BlockInfo._();
+  BlockInfo createEmptyInstance() => create();
+  static $pb.PbList<BlockInfo> createRepeated() => $pb.PbList<BlockInfo>();
   @$core.pragma('dart2js:noInline')
-  static DocumentInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DocumentInfo>(create);
-  static DocumentInfo? _defaultInstance;
+  static BlockInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockInfo>(create);
+  static BlockInfo? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get docId => $_getSZ(0);
@@ -227,56 +227,56 @@ class ResetDocumentParams extends $pb.GeneratedMessage {
   $0.RepeatedRevision ensureRevisions() => $_ensure(1);
 }
 
-class DocumentDelta extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DocumentDelta', createEmptyInstance: create)
-    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'docId')
+class BlockDelta extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'BlockDelta', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockId')
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'deltaJson')
     ..hasRequiredFields = false
   ;
 
-  DocumentDelta._() : super();
-  factory DocumentDelta({
-    $core.String? docId,
+  BlockDelta._() : super();
+  factory BlockDelta({
+    $core.String? blockId,
     $core.String? deltaJson,
   }) {
     final _result = create();
-    if (docId != null) {
-      _result.docId = docId;
+    if (blockId != null) {
+      _result.blockId = blockId;
     }
     if (deltaJson != null) {
       _result.deltaJson = deltaJson;
     }
     return _result;
   }
-  factory DocumentDelta.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
-  factory DocumentDelta.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  factory BlockDelta.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory BlockDelta.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
   'Will be removed in next major version')
-  DocumentDelta clone() => DocumentDelta()..mergeFromMessage(this);
+  BlockDelta clone() => BlockDelta()..mergeFromMessage(this);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
   'Will be removed in next major version')
-  DocumentDelta copyWith(void Function(DocumentDelta) updates) => super.copyWith((message) => updates(message as DocumentDelta)) as DocumentDelta; // ignore: deprecated_member_use
+  BlockDelta copyWith(void Function(BlockDelta) updates) => super.copyWith((message) => updates(message as BlockDelta)) as BlockDelta; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
-  static DocumentDelta create() => DocumentDelta._();
-  DocumentDelta createEmptyInstance() => create();
-  static $pb.PbList<DocumentDelta> createRepeated() => $pb.PbList<DocumentDelta>();
+  static BlockDelta create() => BlockDelta._();
+  BlockDelta createEmptyInstance() => create();
+  static $pb.PbList<BlockDelta> createRepeated() => $pb.PbList<BlockDelta>();
   @$core.pragma('dart2js:noInline')
-  static DocumentDelta getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DocumentDelta>(create);
-  static DocumentDelta? _defaultInstance;
+  static BlockDelta getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockDelta>(create);
+  static BlockDelta? _defaultInstance;
 
   @$pb.TagNumber(1)
-  $core.String get docId => $_getSZ(0);
+  $core.String get blockId => $_getSZ(0);
   @$pb.TagNumber(1)
-  set docId($core.String v) { $_setString(0, v); }
+  set blockId($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
-  $core.bool hasDocId() => $_has(0);
+  $core.bool hasBlockId() => $_has(0);
   @$pb.TagNumber(1)
-  void clearDocId() => clearField(1);
+  void clearBlockId() => clearField(1);
 
   @$pb.TagNumber(2)
   $core.String get deltaJson => $_getSZ(1);
@@ -363,14 +363,14 @@ class NewDocUser extends $pb.GeneratedMessage {
   void clearDocId() => clearField(3);
 }
 
-class DocumentId extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DocumentId', createEmptyInstance: create)
+class BlockId extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'BlockId', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'value')
     ..hasRequiredFields = false
   ;
 
-  DocumentId._() : super();
-  factory DocumentId({
+  BlockId._() : super();
+  factory BlockId({
     $core.String? value,
   }) {
     final _result = create();
@@ -379,26 +379,26 @@ class DocumentId extends $pb.GeneratedMessage {
     }
     return _result;
   }
-  factory DocumentId.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
-  factory DocumentId.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  factory BlockId.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory BlockId.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
   'Will be removed in next major version')
-  DocumentId clone() => DocumentId()..mergeFromMessage(this);
+  BlockId clone() => BlockId()..mergeFromMessage(this);
   @$core.Deprecated(
   'Using this can add significant overhead to your binary. '
   'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
   'Will be removed in next major version')
-  DocumentId copyWith(void Function(DocumentId) updates) => super.copyWith((message) => updates(message as DocumentId)) as DocumentId; // ignore: deprecated_member_use
+  BlockId copyWith(void Function(BlockId) updates) => super.copyWith((message) => updates(message as BlockId)) as BlockId; // ignore: deprecated_member_use
   $pb.BuilderInfo get info_ => _i;
   @$core.pragma('dart2js:noInline')
-  static DocumentId create() => DocumentId._();
-  DocumentId createEmptyInstance() => create();
-  static $pb.PbList<DocumentId> createRepeated() => $pb.PbList<DocumentId>();
+  static BlockId create() => BlockId._();
+  BlockId createEmptyInstance() => create();
+  static $pb.PbList<BlockId> createRepeated() => $pb.PbList<BlockId>();
   @$core.pragma('dart2js:noInline')
-  static DocumentId getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DocumentId>(create);
-  static DocumentId? _defaultInstance;
+  static BlockId getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<BlockId>(create);
+  static BlockId? _defaultInstance;
 
   @$pb.TagNumber(1)
   $core.String get value => $_getSZ(0);

+ 21 - 21
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-collaboration/document_info.pbjson.dart

@@ -8,20 +8,20 @@
 import 'dart:core' as $core;
 import 'dart:convert' as $convert;
 import 'dart:typed_data' as $typed_data;
-@$core.Deprecated('Use createDocParamsDescriptor instead')
-const CreateDocParams$json = const {
-  '1': 'CreateDocParams',
+@$core.Deprecated('Use createBlockParamsDescriptor instead')
+const CreateBlockParams$json = const {
+  '1': 'CreateBlockParams',
   '2': const [
     const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
     const {'1': 'revisions', '3': 2, '4': 1, '5': 11, '6': '.RepeatedRevision', '10': 'revisions'},
   ],
 };
 
-/// Descriptor for `CreateDocParams`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createDocParamsDescriptor = $convert.base64Decode('Cg9DcmVhdGVEb2NQYXJhbXMSDgoCaWQYASABKAlSAmlkEi8KCXJldmlzaW9ucxgCIAEoCzIRLlJlcGVhdGVkUmV2aXNpb25SCXJldmlzaW9ucw==');
-@$core.Deprecated('Use documentInfoDescriptor instead')
-const DocumentInfo$json = const {
-  '1': 'DocumentInfo',
+/// Descriptor for `CreateBlockParams`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List createBlockParamsDescriptor = $convert.base64Decode('ChFDcmVhdGVCbG9ja1BhcmFtcxIOCgJpZBgBIAEoCVICaWQSLwoJcmV2aXNpb25zGAIgASgLMhEuUmVwZWF0ZWRSZXZpc2lvblIJcmV2aXNpb25z');
+@$core.Deprecated('Use blockInfoDescriptor instead')
+const BlockInfo$json = const {
+  '1': 'BlockInfo',
   '2': const [
     const {'1': 'doc_id', '3': 1, '4': 1, '5': 9, '10': 'docId'},
     const {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'},
@@ -30,8 +30,8 @@ const DocumentInfo$json = const {
   ],
 };
 
-/// Descriptor for `DocumentInfo`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List documentInfoDescriptor = $convert.base64Decode('CgxEb2N1bWVudEluZm8SFQoGZG9jX2lkGAEgASgJUgVkb2NJZBISCgR0ZXh0GAIgASgJUgR0ZXh0EhUKBnJldl9pZBgDIAEoA1IFcmV2SWQSHgoLYmFzZV9yZXZfaWQYBCABKANSCWJhc2VSZXZJZA==');
+/// Descriptor for `BlockInfo`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List blockInfoDescriptor = $convert.base64Decode('CglCbG9ja0luZm8SFQoGZG9jX2lkGAEgASgJUgVkb2NJZBISCgR0ZXh0GAIgASgJUgR0ZXh0EhUKBnJldl9pZBgDIAEoA1IFcmV2SWQSHgoLYmFzZV9yZXZfaWQYBCABKANSCWJhc2VSZXZJZA==');
 @$core.Deprecated('Use resetDocumentParamsDescriptor instead')
 const ResetDocumentParams$json = const {
   '1': 'ResetDocumentParams',
@@ -43,17 +43,17 @@ const ResetDocumentParams$json = const {
 
 /// Descriptor for `ResetDocumentParams`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List resetDocumentParamsDescriptor = $convert.base64Decode('ChNSZXNldERvY3VtZW50UGFyYW1zEhUKBmRvY19pZBgBIAEoCVIFZG9jSWQSLwoJcmV2aXNpb25zGAIgASgLMhEuUmVwZWF0ZWRSZXZpc2lvblIJcmV2aXNpb25z');
-@$core.Deprecated('Use documentDeltaDescriptor instead')
-const DocumentDelta$json = const {
-  '1': 'DocumentDelta',
+@$core.Deprecated('Use blockDeltaDescriptor instead')
+const BlockDelta$json = const {
+  '1': 'BlockDelta',
   '2': const [
-    const {'1': 'doc_id', '3': 1, '4': 1, '5': 9, '10': 'docId'},
+    const {'1': 'block_id', '3': 1, '4': 1, '5': 9, '10': 'blockId'},
     const {'1': 'delta_json', '3': 2, '4': 1, '5': 9, '10': 'deltaJson'},
   ],
 };
 
-/// Descriptor for `DocumentDelta`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List documentDeltaDescriptor = $convert.base64Decode('Cg1Eb2N1bWVudERlbHRhEhUKBmRvY19pZBgBIAEoCVIFZG9jSWQSHQoKZGVsdGFfanNvbhgCIAEoCVIJZGVsdGFKc29u');
+/// Descriptor for `BlockDelta`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List blockDeltaDescriptor = $convert.base64Decode('CgpCbG9ja0RlbHRhEhkKCGJsb2NrX2lkGAEgASgJUgdibG9ja0lkEh0KCmRlbHRhX2pzb24YAiABKAlSCWRlbHRhSnNvbg==');
 @$core.Deprecated('Use newDocUserDescriptor instead')
 const NewDocUser$json = const {
   '1': 'NewDocUser',
@@ -66,13 +66,13 @@ const NewDocUser$json = const {
 
 /// Descriptor for `NewDocUser`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List newDocUserDescriptor = $convert.base64Decode('CgpOZXdEb2NVc2VyEhcKB3VzZXJfaWQYASABKAlSBnVzZXJJZBIVCgZyZXZfaWQYAiABKANSBXJldklkEhUKBmRvY19pZBgDIAEoCVIFZG9jSWQ=');
-@$core.Deprecated('Use documentIdDescriptor instead')
-const DocumentId$json = const {
-  '1': 'DocumentId',
+@$core.Deprecated('Use blockIdDescriptor instead')
+const BlockId$json = const {
+  '1': 'BlockId',
   '2': const [
     const {'1': 'value', '3': 1, '4': 1, '5': 9, '10': 'value'},
   ],
 };
 
-/// Descriptor for `DocumentId`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List documentIdDescriptor = $convert.base64Decode('CgpEb2N1bWVudElkEhQKBXZhbHVlGAEgASgJUgV2YWx1ZQ==');
+/// Descriptor for `BlockId`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List blockIdDescriptor = $convert.base64Decode('CgdCbG9ja0lkEhQKBXZhbHVlGAEgASgJUgV2YWx1ZQ==');

+ 8 - 8
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/share.pb.dart

@@ -15,19 +15,19 @@ export 'share.pbenum.dart';
 
 class ExportPayload extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ExportPayload', createEmptyInstance: create)
-    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'docId')
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewId')
     ..e<ExportType>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'exportType', $pb.PbFieldType.OE, defaultOrMaker: ExportType.Text, valueOf: ExportType.valueOf, enumValues: ExportType.values)
     ..hasRequiredFields = false
   ;
 
   ExportPayload._() : super();
   factory ExportPayload({
-    $core.String? docId,
+    $core.String? viewId,
     ExportType? exportType,
   }) {
     final _result = create();
-    if (docId != null) {
-      _result.docId = docId;
+    if (viewId != null) {
+      _result.viewId = viewId;
     }
     if (exportType != null) {
       _result.exportType = exportType;
@@ -56,13 +56,13 @@ class ExportPayload extends $pb.GeneratedMessage {
   static ExportPayload? _defaultInstance;
 
   @$pb.TagNumber(1)
-  $core.String get docId => $_getSZ(0);
+  $core.String get viewId => $_getSZ(0);
   @$pb.TagNumber(1)
-  set docId($core.String v) { $_setString(0, v); }
+  set viewId($core.String v) { $_setString(0, v); }
   @$pb.TagNumber(1)
-  $core.bool hasDocId() => $_has(0);
+  $core.bool hasViewId() => $_has(0);
   @$pb.TagNumber(1)
-  void clearDocId() => clearField(1);
+  void clearViewId() => clearField(1);
 
   @$pb.TagNumber(2)
   ExportType get exportType => $_getN(1);

+ 2 - 2
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/share.pbjson.dart

@@ -24,13 +24,13 @@ final $typed_data.Uint8List exportTypeDescriptor = $convert.base64Decode('CgpFeH
 const ExportPayload$json = const {
   '1': 'ExportPayload',
   '2': const [
-    const {'1': 'doc_id', '3': 1, '4': 1, '5': 9, '10': 'docId'},
+    const {'1': 'view_id', '3': 1, '4': 1, '5': 9, '10': 'viewId'},
     const {'1': 'export_type', '3': 2, '4': 1, '5': 14, '6': '.ExportType', '10': 'exportType'},
   ],
 };
 
 /// Descriptor for `ExportPayload`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List exportPayloadDescriptor = $convert.base64Decode('Cg1FeHBvcnRQYXlsb2FkEhUKBmRvY19pZBgBIAEoCVIFZG9jSWQSLAoLZXhwb3J0X3R5cGUYAiABKA4yCy5FeHBvcnRUeXBlUgpleHBvcnRUeXBl');
+final $typed_data.Uint8List exportPayloadDescriptor = $convert.base64Decode('Cg1FeHBvcnRQYXlsb2FkEhcKB3ZpZXdfaWQYASABKAlSBnZpZXdJZBIsCgtleHBvcnRfdHlwZRgCIAEoDjILLkV4cG9ydFR5cGVSCmV4cG9ydFR5cGU=');
 @$core.Deprecated('Use exportDataDescriptor instead')
 const ExportData$json = const {
   '1': 'ExportData',

+ 130 - 32
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart

@@ -20,11 +20,14 @@ class View extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'belongToId')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.Blank, valueOf: ViewType.valueOf, enumValues: ViewType.values)
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
     ..aInt64(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'version')
     ..aOM<RepeatedView>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'belongings', subBuilder: RepeatedView.create)
     ..aInt64(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'modifiedTime')
     ..aInt64(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'createTime')
+    ..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
+    ..aOS(11, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
+    ..a<$core.int>(12, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pluginType', $pb.PbFieldType.O3)
     ..hasRequiredFields = false
   ;
 
@@ -34,11 +37,14 @@ class View extends $pb.GeneratedMessage {
     $core.String? belongToId,
     $core.String? name,
     $core.String? desc,
-    ViewType? viewType,
+    ViewDataType? dataType,
     $fixnum.Int64? version,
     RepeatedView? belongings,
     $fixnum.Int64? modifiedTime,
     $fixnum.Int64? createTime,
+    $core.String? extData,
+    $core.String? thumbnail,
+    $core.int? pluginType,
   }) {
     final _result = create();
     if (id != null) {
@@ -53,8 +59,8 @@ class View extends $pb.GeneratedMessage {
     if (desc != null) {
       _result.desc = desc;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
     }
     if (version != null) {
       _result.version = version;
@@ -68,6 +74,15 @@ class View extends $pb.GeneratedMessage {
     if (createTime != null) {
       _result.createTime = createTime;
     }
+    if (extData != null) {
+      _result.extData = extData;
+    }
+    if (thumbnail != null) {
+      _result.thumbnail = thumbnail;
+    }
+    if (pluginType != null) {
+      _result.pluginType = pluginType;
+    }
     return _result;
   }
   factory View.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -128,13 +143,13 @@ class View extends $pb.GeneratedMessage {
   void clearDesc() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
 
   @$pb.TagNumber(6)
   $fixnum.Int64 get version => $_getI64(5);
@@ -173,6 +188,33 @@ class View extends $pb.GeneratedMessage {
   $core.bool hasCreateTime() => $_has(8);
   @$pb.TagNumber(9)
   void clearCreateTime() => clearField(9);
+
+  @$pb.TagNumber(10)
+  $core.String get extData => $_getSZ(9);
+  @$pb.TagNumber(10)
+  set extData($core.String v) { $_setString(9, v); }
+  @$pb.TagNumber(10)
+  $core.bool hasExtData() => $_has(9);
+  @$pb.TagNumber(10)
+  void clearExtData() => clearField(10);
+
+  @$pb.TagNumber(11)
+  $core.String get thumbnail => $_getSZ(10);
+  @$pb.TagNumber(11)
+  set thumbnail($core.String v) { $_setString(10, v); }
+  @$pb.TagNumber(11)
+  $core.bool hasThumbnail() => $_has(10);
+  @$pb.TagNumber(11)
+  void clearThumbnail() => clearField(11);
+
+  @$pb.TagNumber(12)
+  $core.int get pluginType => $_getIZ(11);
+  @$pb.TagNumber(12)
+  set pluginType($core.int v) { $_setSignedInt32(11, v); }
+  @$pb.TagNumber(12)
+  $core.bool hasPluginType() => $_has(11);
+  @$pb.TagNumber(12)
+  void clearPluginType() => clearField(12);
 }
 
 class RepeatedView extends $pb.GeneratedMessage {
@@ -232,7 +274,9 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.Blank, valueOf: ViewType.valueOf, enumValues: ViewType.values)
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
+    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
+    ..a<$core.int>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pluginType', $pb.PbFieldType.O3)
     ..hasRequiredFields = false
   ;
 
@@ -242,7 +286,9 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     $core.String? name,
     $core.String? desc,
     $core.String? thumbnail,
-    ViewType? viewType,
+    ViewDataType? dataType,
+    $core.String? extData,
+    $core.int? pluginType,
   }) {
     final _result = create();
     if (belongToId != null) {
@@ -257,8 +303,14 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     if (thumbnail != null) {
       _result.thumbnail = thumbnail;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
+    }
+    if (extData != null) {
+      _result.extData = extData;
+    }
+    if (pluginType != null) {
+      _result.pluginType = pluginType;
     }
     return _result;
   }
@@ -323,13 +375,31 @@ class CreateViewPayload extends $pb.GeneratedMessage {
   void clearThumbnail() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
+
+  @$pb.TagNumber(6)
+  $core.String get extData => $_getSZ(5);
+  @$pb.TagNumber(6)
+  set extData($core.String v) { $_setString(5, v); }
+  @$pb.TagNumber(6)
+  $core.bool hasExtData() => $_has(5);
+  @$pb.TagNumber(6)
+  void clearExtData() => clearField(6);
+
+  @$pb.TagNumber(7)
+  $core.int get pluginType => $_getIZ(6);
+  @$pb.TagNumber(7)
+  set pluginType($core.int v) { $_setSignedInt32(6, v); }
+  @$pb.TagNumber(7)
+  $core.bool hasPluginType() => $_has(6);
+  @$pb.TagNumber(7)
+  void clearPluginType() => clearField(7);
 }
 
 class CreateViewParams extends $pb.GeneratedMessage {
@@ -338,9 +408,11 @@ class CreateViewParams extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.Blank, valueOf: ViewType.valueOf, enumValues: ViewType.values)
-    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewData')
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
+    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
     ..aOS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewId')
+    ..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data')
+    ..a<$core.int>(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pluginType', $pb.PbFieldType.O3)
     ..hasRequiredFields = false
   ;
 
@@ -350,9 +422,11 @@ class CreateViewParams extends $pb.GeneratedMessage {
     $core.String? name,
     $core.String? desc,
     $core.String? thumbnail,
-    ViewType? viewType,
-    $core.String? viewData,
+    ViewDataType? dataType,
+    $core.String? extData,
     $core.String? viewId,
+    $core.String? data,
+    $core.int? pluginType,
   }) {
     final _result = create();
     if (belongToId != null) {
@@ -367,15 +441,21 @@ class CreateViewParams extends $pb.GeneratedMessage {
     if (thumbnail != null) {
       _result.thumbnail = thumbnail;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
     }
-    if (viewData != null) {
-      _result.viewData = viewData;
+    if (extData != null) {
+      _result.extData = extData;
     }
     if (viewId != null) {
       _result.viewId = viewId;
     }
+    if (data != null) {
+      _result.data = data;
+    }
+    if (pluginType != null) {
+      _result.pluginType = pluginType;
+    }
     return _result;
   }
   factory CreateViewParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -436,22 +516,22 @@ class CreateViewParams extends $pb.GeneratedMessage {
   void clearThumbnail() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
 
   @$pb.TagNumber(6)
-  $core.String get viewData => $_getSZ(5);
+  $core.String get extData => $_getSZ(5);
   @$pb.TagNumber(6)
-  set viewData($core.String v) { $_setString(5, v); }
+  set extData($core.String v) { $_setString(5, v); }
   @$pb.TagNumber(6)
-  $core.bool hasViewData() => $_has(5);
+  $core.bool hasExtData() => $_has(5);
   @$pb.TagNumber(6)
-  void clearViewData() => clearField(6);
+  void clearExtData() => clearField(6);
 
   @$pb.TagNumber(7)
   $core.String get viewId => $_getSZ(6);
@@ -461,6 +541,24 @@ class CreateViewParams extends $pb.GeneratedMessage {
   $core.bool hasViewId() => $_has(6);
   @$pb.TagNumber(7)
   void clearViewId() => clearField(7);
+
+  @$pb.TagNumber(8)
+  $core.String get data => $_getSZ(7);
+  @$pb.TagNumber(8)
+  set data($core.String v) { $_setString(7, v); }
+  @$pb.TagNumber(8)
+  $core.bool hasData() => $_has(7);
+  @$pb.TagNumber(8)
+  void clearData() => clearField(8);
+
+  @$pb.TagNumber(9)
+  $core.int get pluginType => $_getIZ(8);
+  @$pb.TagNumber(9)
+  set pluginType($core.int v) { $_setSignedInt32(8, v); }
+  @$pb.TagNumber(9)
+  $core.bool hasPluginType() => $_has(8);
+  @$pb.TagNumber(9)
+  void clearPluginType() => clearField(9);
 }
 
 class ViewId extends $pb.GeneratedMessage {

+ 9 - 9
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart

@@ -9,18 +9,18 @@
 import 'dart:core' as $core;
 import 'package:protobuf/protobuf.dart' as $pb;
 
-class ViewType extends $pb.ProtobufEnum {
-  static const ViewType Blank = ViewType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Blank');
-  static const ViewType Doc = ViewType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Doc');
+class ViewDataType extends $pb.ProtobufEnum {
+  static const ViewDataType RichText = ViewDataType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RichText');
+  static const ViewDataType PlainText = ViewDataType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PlainText');
 
-  static const $core.List<ViewType> values = <ViewType> [
-    Blank,
-    Doc,
+  static const $core.List<ViewDataType> values = <ViewDataType> [
+    RichText,
+    PlainText,
   ];
 
-  static final $core.Map<$core.int, ViewType> _byValue = $pb.ProtobufEnum.initByValue(values);
-  static ViewType? valueOf($core.int value) => _byValue[value];
+  static final $core.Map<$core.int, ViewDataType> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static ViewDataType? valueOf($core.int value) => _byValue[value];
 
-  const ViewType._($core.int v, $core.String n) : super(v, n);
+  const ViewDataType._($core.int v, $core.String n) : super(v, n);
 }
 

+ 21 - 14
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart

@@ -8,17 +8,17 @@
 import 'dart:core' as $core;
 import 'dart:convert' as $convert;
 import 'dart:typed_data' as $typed_data;
-@$core.Deprecated('Use viewTypeDescriptor instead')
-const ViewType$json = const {
-  '1': 'ViewType',
+@$core.Deprecated('Use viewDataTypeDescriptor instead')
+const ViewDataType$json = const {
+  '1': 'ViewDataType',
   '2': const [
-    const {'1': 'Blank', '2': 0},
-    const {'1': 'Doc', '2': 1},
+    const {'1': 'RichText', '2': 0},
+    const {'1': 'PlainText', '2': 1},
   ],
 };
 
-/// Descriptor for `ViewType`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List viewTypeDescriptor = $convert.base64Decode('CghWaWV3VHlwZRIJCgVCbGFuaxAAEgcKA0RvYxAB');
+/// Descriptor for `ViewDataType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List viewDataTypeDescriptor = $convert.base64Decode('CgxWaWV3RGF0YVR5cGUSDAoIUmljaFRleHQQABINCglQbGFpblRleHQQAQ==');
 @$core.Deprecated('Use viewDescriptor instead')
 const View$json = const {
   '1': 'View',
@@ -27,16 +27,19 @@ const View$json = const {
     const {'1': 'belong_to_id', '3': 2, '4': 1, '5': 9, '10': 'belongToId'},
     const {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 4, '4': 1, '5': 9, '10': 'desc'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
     const {'1': 'version', '3': 6, '4': 1, '5': 3, '10': 'version'},
     const {'1': 'belongings', '3': 7, '4': 1, '5': 11, '6': '.RepeatedView', '10': 'belongings'},
     const {'1': 'modified_time', '3': 8, '4': 1, '5': 3, '10': 'modifiedTime'},
     const {'1': 'create_time', '3': 9, '4': 1, '5': 3, '10': 'createTime'},
+    const {'1': 'ext_data', '3': 10, '4': 1, '5': 9, '10': 'extData'},
+    const {'1': 'thumbnail', '3': 11, '4': 1, '5': 9, '10': 'thumbnail'},
+    const {'1': 'plugin_type', '3': 12, '4': 1, '5': 5, '10': 'pluginType'},
   ],
 };
 
 /// Descriptor for `View`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List viewDescriptor = $convert.base64Decode('CgRWaWV3Eg4KAmlkGAEgASgJUgJpZBIgCgxiZWxvbmdfdG9faWQYAiABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgDIAEoCVIEbmFtZRISCgRkZXNjGAQgASgJUgRkZXNjEiYKCXZpZXdfdHlwZRgFIAEoDjIJLlZpZXdUeXBlUgh2aWV3VHlwZRIYCgd2ZXJzaW9uGAYgASgDUgd2ZXJzaW9uEi0KCmJlbG9uZ2luZ3MYByABKAsyDS5SZXBlYXRlZFZpZXdSCmJlbG9uZ2luZ3MSIwoNbW9kaWZpZWRfdGltZRgIIAEoA1IMbW9kaWZpZWRUaW1lEh8KC2NyZWF0ZV90aW1lGAkgASgDUgpjcmVhdGVUaW1l');
+final $typed_data.Uint8List viewDescriptor = $convert.base64Decode('CgRWaWV3Eg4KAmlkGAEgASgJUgJpZBIgCgxiZWxvbmdfdG9faWQYAiABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgDIAEoCVIEbmFtZRISCgRkZXNjGAQgASgJUgRkZXNjEioKCWRhdGFfdHlwZRgFIAEoDjINLlZpZXdEYXRhVHlwZVIIZGF0YVR5cGUSGAoHdmVyc2lvbhgGIAEoA1IHdmVyc2lvbhItCgpiZWxvbmdpbmdzGAcgASgLMg0uUmVwZWF0ZWRWaWV3UgpiZWxvbmdpbmdzEiMKDW1vZGlmaWVkX3RpbWUYCCABKANSDG1vZGlmaWVkVGltZRIfCgtjcmVhdGVfdGltZRgJIAEoA1IKY3JlYXRlVGltZRIZCghleHRfZGF0YRgKIAEoCVIHZXh0RGF0YRIcCgl0aHVtYm5haWwYCyABKAlSCXRodW1ibmFpbBIfCgtwbHVnaW5fdHlwZRgMIAEoBVIKcGx1Z2luVHlwZQ==');
 @$core.Deprecated('Use repeatedViewDescriptor instead')
 const RepeatedView$json = const {
   '1': 'RepeatedView',
@@ -55,7 +58,9 @@ const CreateViewPayload$json = const {
     const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 3, '4': 1, '5': 9, '10': 'desc'},
     const {'1': 'thumbnail', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'thumbnail'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
+    const {'1': 'ext_data', '3': 6, '4': 1, '5': 9, '10': 'extData'},
+    const {'1': 'plugin_type', '3': 7, '4': 1, '5': 5, '10': 'pluginType'},
   ],
   '8': const [
     const {'1': 'one_of_thumbnail'},
@@ -63,7 +68,7 @@ const CreateViewPayload$json = const {
 };
 
 /// Descriptor for `CreateViewPayload`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createViewPayloadDescriptor = $convert.base64Decode('ChFDcmVhdGVWaWV3UGF5bG9hZBIgCgxiZWxvbmdfdG9faWQYASABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgAUgl0aHVtYm5haWwSJgoJdmlld190eXBlGAUgASgOMgkuVmlld1R5cGVSCHZpZXdUeXBlQhIKEG9uZV9vZl90aHVtYm5haWw=');
+final $typed_data.Uint8List createViewPayloadDescriptor = $convert.base64Decode('ChFDcmVhdGVWaWV3UGF5bG9hZBIgCgxiZWxvbmdfdG9faWQYASABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgAUgl0aHVtYm5haWwSKgoJZGF0YV90eXBlGAUgASgOMg0uVmlld0RhdGFUeXBlUghkYXRhVHlwZRIZCghleHRfZGF0YRgGIAEoCVIHZXh0RGF0YRIfCgtwbHVnaW5fdHlwZRgHIAEoBVIKcGx1Z2luVHlwZUISChBvbmVfb2ZfdGh1bWJuYWls');
 @$core.Deprecated('Use createViewParamsDescriptor instead')
 const CreateViewParams$json = const {
   '1': 'CreateViewParams',
@@ -72,14 +77,16 @@ const CreateViewParams$json = const {
     const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 3, '4': 1, '5': 9, '10': 'desc'},
     const {'1': 'thumbnail', '3': 4, '4': 1, '5': 9, '10': 'thumbnail'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
-    const {'1': 'view_data', '3': 6, '4': 1, '5': 9, '10': 'viewData'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
+    const {'1': 'ext_data', '3': 6, '4': 1, '5': 9, '10': 'extData'},
     const {'1': 'view_id', '3': 7, '4': 1, '5': 9, '10': 'viewId'},
+    const {'1': 'data', '3': 8, '4': 1, '5': 9, '10': 'data'},
+    const {'1': 'plugin_type', '3': 9, '4': 1, '5': 5, '10': 'pluginType'},
   ],
 };
 
 /// Descriptor for `CreateViewParams`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createViewParamsDescriptor = $convert.base64Decode('ChBDcmVhdGVWaWV3UGFyYW1zEiAKDGJlbG9uZ190b19pZBgBIAEoCVIKYmVsb25nVG9JZBISCgRuYW1lGAIgASgJUgRuYW1lEhIKBGRlc2MYAyABKAlSBGRlc2MSHAoJdGh1bWJuYWlsGAQgASgJUgl0aHVtYm5haWwSJgoJdmlld190eXBlGAUgASgOMgkuVmlld1R5cGVSCHZpZXdUeXBlEhsKCXZpZXdfZGF0YRgGIAEoCVIIdmlld0RhdGESFwoHdmlld19pZBgHIAEoCVIGdmlld0lk');
+final $typed_data.Uint8List createViewParamsDescriptor = $convert.base64Decode('ChBDcmVhdGVWaWV3UGFyYW1zEiAKDGJlbG9uZ190b19pZBgBIAEoCVIKYmVsb25nVG9JZBISCgRuYW1lGAIgASgJUgRuYW1lEhIKBGRlc2MYAyABKAlSBGRlc2MSHAoJdGh1bWJuYWlsGAQgASgJUgl0aHVtYm5haWwSKgoJZGF0YV90eXBlGAUgASgOMg0uVmlld0RhdGFUeXBlUghkYXRhVHlwZRIZCghleHRfZGF0YRgGIAEoCVIHZXh0RGF0YRIXCgd2aWV3X2lkGAcgASgJUgZ2aWV3SWQSEgoEZGF0YRgIIAEoCVIEZGF0YRIfCgtwbHVnaW5fdHlwZRgJIAEoBVIKcGx1Z2luVHlwZQ==');
 @$core.Deprecated('Use viewIdDescriptor instead')
 const ViewId$json = const {
   '1': 'ViewId',

+ 6 - 6
frontend/app_flowy/pubspec.lock

@@ -645,7 +645,7 @@ packages:
       name: js
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.6.3"
+    version: "0.6.4"
   json_annotation:
     dependency: transitive
     description:
@@ -799,7 +799,7 @@ packages:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.0"
+    version: "1.8.1"
   path_drawing:
     dependency: transitive
     description:
@@ -1133,21 +1133,21 @@ packages:
       name: test
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.19.5"
+    version: "1.20.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.8"
+    version: "0.4.9"
   test_core:
     dependency: transitive
     description:
       name: test_core
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.11"
   textstyle_extensions:
     dependency: transitive
     description:
@@ -1354,5 +1354,5 @@ packages:
     source: hosted
     version: "8.0.0"
 sdks:
-  dart: ">=2.15.0-116.0.dev <3.0.0"
+  dart: ">=2.16.0-100.0.dev <3.0.0"
   flutter: ">=2.5.0"

+ 2 - 0
frontend/rust-lib/Cargo.lock

@@ -1031,6 +1031,7 @@ dependencies = [
  "protobuf",
  "serde",
  "serde_json",
+ "serde_repr",
  "strum",
  "strum_macros",
  "unicode-segmentation",
@@ -1108,6 +1109,7 @@ name = "flowy-sync"
 version = "0.1.0"
 dependencies = [
  "async-stream",
+ "async-trait",
  "bytes",
  "dashmap",
  "diesel",

+ 33 - 27
frontend/rust-lib/flowy-document/src/editor.rs → frontend/rust-lib/flowy-document/src/block_editor.rs

@@ -1,13 +1,14 @@
-use crate::queue::DocumentRevisionCompact;
-use crate::web_socket::{make_document_ws_manager, EditorCommandSender};
+use crate::queue::BlockRevisionCompact;
+use crate::web_socket::{make_block_ws_manager, EditorCommandSender};
 use crate::{
     errors::FlowyError,
-    queue::{EditorCommand, EditorCommandQueue},
-    DocumentUser, DocumentWSReceiver,
+    queue::{EditBlockQueue, EditorCommand},
+    BlockUser,
 };
 use bytes::Bytes;
+use flowy_collaboration::entities::ws_data::ServerRevisionWSData;
 use flowy_collaboration::{
-    entities::{document_info::DocumentInfo, revision::Revision},
+    entities::{document_info::BlockInfo, revision::Revision},
     errors::CollaborateResult,
     util::make_delta_from_revisions,
 };
@@ -19,10 +20,11 @@ use lib_ot::{
     core::{Interval, Operation},
     rich_text::{RichTextAttribute, RichTextDelta},
 };
+use lib_ws::WSConnectState;
 use std::sync::Arc;
 use tokio::sync::{mpsc, oneshot};
 
-pub struct ClientDocumentEditor {
+pub struct ClientBlockEditor {
     pub doc_id: String,
     #[allow(dead_code)]
     rev_manager: Arc<RevisionManager>,
@@ -30,16 +32,16 @@ pub struct ClientDocumentEditor {
     edit_cmd_tx: EditorCommandSender,
 }
 
-impl ClientDocumentEditor {
+impl ClientBlockEditor {
     pub(crate) async fn new(
         doc_id: &str,
-        user: Arc<dyn DocumentUser>,
+        user: Arc<dyn BlockUser>,
         mut rev_manager: RevisionManager,
         rev_web_socket: Arc<dyn RevisionWebSocket>,
         cloud_service: Arc<dyn RevisionCloudService>,
     ) -> FlowyResult<Arc<Self>> {
         let document_info = rev_manager
-            .load::<DocumentInfoBuilder, DocumentRevisionCompact>(cloud_service)
+            .load::<BlockInfoBuilder, BlockRevisionCompact>(cloud_service)
             .await?;
         let delta = document_info.delta()?;
         let rev_manager = Arc::new(rev_manager);
@@ -47,7 +49,7 @@ impl ClientDocumentEditor {
         let user_id = user.user_id()?;
 
         let edit_cmd_tx = spawn_edit_queue(user, rev_manager.clone(), delta);
-        let ws_manager = make_document_ws_manager(
+        let ws_manager = make_block_ws_manager(
             doc_id.clone(),
             user_id.clone(),
             edit_cmd_tx.clone(),
@@ -138,9 +140,9 @@ impl ClientDocumentEditor {
         Ok(())
     }
 
-    pub async fn document_json(&self) -> FlowyResult<String> {
+    pub async fn block_json(&self) -> FlowyResult<String> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
-        let msg = EditorCommand::ReadDocumentAsJson { ret };
+        let msg = EditorCommand::ReadBlockJson { ret };
         let _ = self.edit_cmd_tx.send(msg).await;
         let json = rx.await.map_err(internal_error)??;
         Ok(json)
@@ -163,34 +165,38 @@ impl ClientDocumentEditor {
         self.ws_manager.stop();
     }
 
-    pub(crate) fn ws_handler(&self) -> Arc<dyn DocumentWSReceiver> {
-        self.ws_manager.clone()
+    pub(crate) async fn receive_ws_data(&self, data: ServerRevisionWSData) -> Result<(), FlowyError> {
+        self.ws_manager.receive_ws_data(data).await
+    }
+
+    pub(crate) fn receive_ws_state(&self, state: &WSConnectState) {
+        self.ws_manager.connect_state_changed(state.clone());
     }
 }
 
-impl std::ops::Drop for ClientDocumentEditor {
+impl std::ops::Drop for ClientBlockEditor {
     fn drop(&mut self) {
-        tracing::trace!("{} ClientDocumentEditor was dropped", self.doc_id)
+        tracing::trace!("{} ClientBlockEditor was dropped", self.doc_id)
     }
 }
 
 // The edit queue will exit after the EditorCommandSender was dropped.
 fn spawn_edit_queue(
-    user: Arc<dyn DocumentUser>,
+    user: Arc<dyn BlockUser>,
     rev_manager: Arc<RevisionManager>,
     delta: RichTextDelta,
 ) -> EditorCommandSender {
     let (sender, receiver) = mpsc::channel(1000);
-    let actor = EditorCommandQueue::new(user, rev_manager, delta, receiver);
-    tokio::spawn(actor.run());
+    let edit_queue = EditBlockQueue::new(user, rev_manager, delta, receiver);
+    tokio::spawn(edit_queue.run());
     sender
 }
 
 #[cfg(feature = "flowy_unit_test")]
-impl ClientDocumentEditor {
+impl ClientBlockEditor {
     pub async fn doc_json(&self) -> FlowyResult<String> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
-        let msg = EditorCommand::ReadDocumentAsJson { ret };
+        let msg = EditorCommand::ReadBlockJson { ret };
         let _ = self.edit_cmd_tx.send(msg).await;
         let s = rx.await.map_err(internal_error)??;
         Ok(s)
@@ -198,7 +204,7 @@ impl ClientDocumentEditor {
 
     pub async fn doc_delta(&self) -> FlowyResult<RichTextDelta> {
         let (ret, rx) = oneshot::channel::<CollaborateResult<RichTextDelta>>();
-        let msg = EditorCommand::ReadDocumentAsDelta { ret };
+        let msg = EditorCommand::ReadBlockDelta { ret };
         let _ = self.edit_cmd_tx.send(msg).await;
         let delta = rx.await.map_err(internal_error)??;
         Ok(delta)
@@ -209,18 +215,18 @@ impl ClientDocumentEditor {
     }
 }
 
-struct DocumentInfoBuilder();
-impl RevisionObjectBuilder for DocumentInfoBuilder {
-    type Output = DocumentInfo;
+struct BlockInfoBuilder();
+impl RevisionObjectBuilder for BlockInfoBuilder {
+    type Output = BlockInfo;
 
     fn build_object(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
         let (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id();
         let mut delta = make_delta_from_revisions(revisions)?;
         correct_delta(&mut delta);
 
-        Result::<DocumentInfo, FlowyError>::Ok(DocumentInfo {
+        Result::<BlockInfo, FlowyError>::Ok(BlockInfo {
             doc_id: object_id.to_owned(),
-            text: delta.to_json(),
+            text: delta.to_delta_json(),
             rev_id,
             base_rev_id,
         })

+ 6 - 6
frontend/rust-lib/flowy-document/src/lib.rs

@@ -1,4 +1,4 @@
-pub mod editor;
+pub mod block_editor;
 pub mod manager;
 mod queue;
 mod web_socket;
@@ -11,13 +11,13 @@ pub mod errors {
 pub const DOCUMENT_SYNC_INTERVAL_IN_MILLIS: u64 = 1000;
 
 use crate::errors::FlowyError;
-use flowy_collaboration::entities::document_info::{CreateDocParams, DocumentId, DocumentInfo, ResetDocumentParams};
+use flowy_collaboration::entities::document_info::{BlockId, BlockInfo, CreateBlockParams, ResetDocumentParams};
 use lib_infra::future::FutureResult;
 
-pub trait DocumentCloudService: Send + Sync {
-    fn create_document(&self, token: &str, params: CreateDocParams) -> FutureResult<(), FlowyError>;
+pub trait BlockCloudService: Send + Sync {
+    fn create_block(&self, token: &str, params: CreateBlockParams) -> FutureResult<(), FlowyError>;
 
-    fn read_document(&self, token: &str, params: DocumentId) -> FutureResult<Option<DocumentInfo>, FlowyError>;
+    fn read_block(&self, token: &str, params: BlockId) -> FutureResult<Option<BlockInfo>, FlowyError>;
 
-    fn update_document(&self, token: &str, params: ResetDocumentParams) -> FutureResult<(), FlowyError>;
+    fn update_block(&self, token: &str, params: ResetDocumentParams) -> FutureResult<(), FlowyError>;
 }

+ 79 - 94
frontend/rust-lib/flowy-document/src/manager.rs

@@ -1,76 +1,64 @@
-use crate::{editor::ClientDocumentEditor, errors::FlowyError, DocumentCloudService};
-use async_trait::async_trait;
+use crate::{block_editor::ClientBlockEditor, errors::FlowyError, BlockCloudService};
 use bytes::Bytes;
 use dashmap::DashMap;
 use flowy_collaboration::entities::{
-    document_info::{DocumentDelta, DocumentId},
+    document_info::{BlockDelta, BlockId},
     revision::{md5, RepeatedRevision, Revision},
     ws_data::ServerRevisionWSData,
 };
 use flowy_database::ConnectionPool;
 use flowy_error::FlowyResult;
-use flowy_sync::{RevisionCache, RevisionCloudService, RevisionManager, RevisionWebSocket};
+use flowy_sync::{RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket};
 use lib_infra::future::FutureResult;
-use lib_ws::WSConnectState;
 use std::{convert::TryInto, sync::Arc};
 
-pub trait DocumentUser: Send + Sync {
+pub trait BlockUser: Send + Sync {
     fn user_dir(&self) -> Result<String, FlowyError>;
     fn user_id(&self) -> Result<String, FlowyError>;
     fn token(&self) -> Result<String, FlowyError>;
     fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
 }
 
-#[async_trait]
-pub(crate) trait DocumentWSReceiver: Send + Sync {
-    async fn receive_ws_data(&self, data: ServerRevisionWSData) -> Result<(), FlowyError>;
-    fn connect_state_changed(&self, state: WSConnectState);
-}
-type WebSocketDataReceivers = Arc<DashMap<String, Arc<dyn DocumentWSReceiver>>>;
-pub struct FlowyDocumentManager {
-    cloud_service: Arc<dyn DocumentCloudService>,
-    ws_data_receivers: WebSocketDataReceivers,
+pub struct BlockManager {
+    cloud_service: Arc<dyn BlockCloudService>,
     rev_web_socket: Arc<dyn RevisionWebSocket>,
-    document_handlers: Arc<DocumentEditorHandlers>,
-    document_user: Arc<dyn DocumentUser>,
+    block_editors: Arc<BlockEditors>,
+    block_user: Arc<dyn BlockUser>,
 }
 
-impl FlowyDocumentManager {
+impl BlockManager {
     pub fn new(
-        cloud_service: Arc<dyn DocumentCloudService>,
-        document_user: Arc<dyn DocumentUser>,
+        cloud_service: Arc<dyn BlockCloudService>,
+        block_user: Arc<dyn BlockUser>,
         rev_web_socket: Arc<dyn RevisionWebSocket>,
     ) -> Self {
-        let ws_data_receivers = Arc::new(DashMap::new());
-        let document_handlers = Arc::new(DocumentEditorHandlers::new());
+        let block_handlers = Arc::new(BlockEditors::new());
         Self {
             cloud_service,
-            ws_data_receivers,
             rev_web_socket,
-            document_handlers,
-            document_user,
+            block_editors: block_handlers,
+            block_user,
         }
     }
 
     pub fn init(&self) -> FlowyResult<()> {
-        listen_ws_state_changed(self.rev_web_socket.clone(), self.ws_data_receivers.clone());
+        listen_ws_state_changed(self.rev_web_socket.clone(), self.block_editors.clone());
 
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self, doc_id), fields(doc_id), err)]
-    pub async fn open_document<T: AsRef<str>>(&self, doc_id: T) -> Result<Arc<ClientDocumentEditor>, FlowyError> {
-        let doc_id = doc_id.as_ref();
-        tracing::Span::current().record("doc_id", &doc_id);
-        self.get_editor(doc_id).await
+    #[tracing::instrument(level = "debug", skip(self, block_id), fields(block_id), err)]
+    pub async fn open_block<T: AsRef<str>>(&self, block_id: T) -> Result<Arc<ClientBlockEditor>, FlowyError> {
+        let block_id = block_id.as_ref();
+        tracing::Span::current().record("block_id", &block_id);
+        self.get_block_editor(block_id).await
     }
 
-    #[tracing::instrument(level = "trace", skip(self, doc_id), fields(doc_id), err)]
-    pub fn close_document<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> {
-        let doc_id = doc_id.as_ref();
-        tracing::Span::current().record("doc_id", &doc_id);
-        self.document_handlers.remove(doc_id);
-        self.ws_data_receivers.remove(doc_id);
+    #[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)]
+    pub fn close_block<T: AsRef<str>>(&self, block_id: T) -> Result<(), FlowyError> {
+        let block_id = block_id.as_ref();
+        tracing::Span::current().record("block_id", &block_id);
+        self.block_editors.remove(block_id);
         Ok(())
     }
 
@@ -78,25 +66,25 @@ impl FlowyDocumentManager {
     pub fn delete<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> {
         let doc_id = doc_id.as_ref();
         tracing::Span::current().record("doc_id", &doc_id);
-        self.document_handlers.remove(doc_id);
-        self.ws_data_receivers.remove(doc_id);
+        self.block_editors.remove(doc_id);
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.doc_id), err)]
-    pub async fn receive_local_delta(&self, delta: DocumentDelta) -> Result<DocumentDelta, FlowyError> {
-        let editor = self.get_editor(&delta.doc_id).await?;
+    #[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)]
+    pub async fn receive_local_delta(&self, delta: BlockDelta) -> Result<BlockDelta, FlowyError> {
+        let editor = self.get_block_editor(&delta.block_id).await?;
         let _ = editor.compose_local_delta(Bytes::from(delta.delta_json)).await?;
-        let document_json = editor.document_json().await?;
-        Ok(DocumentDelta {
-            doc_id: delta.doc_id.clone(),
+        let document_json = editor.block_json().await?;
+        Ok(BlockDelta {
+            block_id: delta.block_id.clone(),
             delta_json: document_json,
         })
     }
 
-    pub async fn reset_with_revisions<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
+    pub async fn create_block<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
         let doc_id = doc_id.as_ref().to_owned();
-        let db_pool = self.document_user.db_pool()?;
+        let db_pool = self.block_user.db_pool()?;
+        // Maybe we could save the block to disk without creating the RevisionManager
         let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
         let _ = rev_manager.reset_object(revisions).await?;
         Ok(())
@@ -105,9 +93,9 @@ impl FlowyDocumentManager {
     pub async fn receive_ws_data(&self, data: Bytes) {
         let result: Result<ServerRevisionWSData, protobuf::ProtobufError> = data.try_into();
         match result {
-            Ok(data) => match self.ws_data_receivers.get(&data.object_id) {
+            Ok(data) => match self.block_editors.get(&data.object_id) {
                 None => tracing::error!("Can't find any source handler for {:?}-{:?}", data.object_id, data.ty),
-                Some(handler) => match handler.receive_ws_data(data).await {
+                Some(block_editor) => match block_editor.receive_ws_data(data).await {
                     Ok(_) => {}
                     Err(e) => tracing::error!("{}", e),
                 },
@@ -119,59 +107,57 @@ impl FlowyDocumentManager {
     }
 }
 
-impl FlowyDocumentManager {
-    async fn get_editor(&self, doc_id: &str) -> FlowyResult<Arc<ClientDocumentEditor>> {
-        match self.document_handlers.get(doc_id) {
+impl BlockManager {
+    async fn get_block_editor(&self, block_id: &str) -> FlowyResult<Arc<ClientBlockEditor>> {
+        match self.block_editors.get(block_id) {
             None => {
-                let db_pool = self.document_user.db_pool()?;
-                self.make_editor(doc_id, db_pool).await
+                let db_pool = self.block_user.db_pool()?;
+                self.make_block_editor(block_id, db_pool).await
             }
             Some(editor) => Ok(editor),
         }
     }
 
-    async fn make_editor(
+    async fn make_block_editor(
         &self,
-        doc_id: &str,
+        block_id: &str,
         pool: Arc<ConnectionPool>,
-    ) -> Result<Arc<ClientDocumentEditor>, FlowyError> {
-        let user = self.document_user.clone();
-        let token = self.document_user.token()?;
-        let rev_manager = self.make_rev_manager(doc_id, pool.clone())?;
-        let cloud_service = Arc::new(DocumentRevisionCloudServiceImpl {
+    ) -> Result<Arc<ClientBlockEditor>, FlowyError> {
+        let user = self.block_user.clone();
+        let token = self.block_user.token()?;
+        let rev_manager = self.make_rev_manager(block_id, pool.clone())?;
+        let cloud_service = Arc::new(BlockRevisionCloudService {
             token,
             server: self.cloud_service.clone(),
         });
         let doc_editor =
-            ClientDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
-        self.ws_data_receivers
-            .insert(doc_id.to_string(), doc_editor.ws_handler());
-        self.document_handlers.insert(doc_id, &doc_editor);
+            ClientBlockEditor::new(block_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
+        self.block_editors.insert(block_id, &doc_editor);
         Ok(doc_editor)
     }
 
     fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
-        let user_id = self.document_user.user_id()?;
-        let cache = Arc::new(RevisionCache::new(&user_id, doc_id, pool));
-        Ok(RevisionManager::new(&user_id, doc_id, cache))
+        let user_id = self.block_user.user_id()?;
+        let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, doc_id, pool));
+        Ok(RevisionManager::new(&user_id, doc_id, rev_persistence))
     }
 }
 
-struct DocumentRevisionCloudServiceImpl {
+struct BlockRevisionCloudService {
     token: String,
-    server: Arc<dyn DocumentCloudService>,
+    server: Arc<dyn BlockCloudService>,
 }
 
-impl RevisionCloudService for DocumentRevisionCloudServiceImpl {
+impl RevisionCloudService for BlockRevisionCloudService {
     #[tracing::instrument(level = "trace", skip(self))]
     fn fetch_object(&self, user_id: &str, object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
-        let params: DocumentId = object_id.to_string().into();
+        let params: BlockId = object_id.to_string().into();
         let server = self.server.clone();
         let token = self.token.clone();
         let user_id = user_id.to_string();
 
         FutureResult::new(async move {
-            match server.read_document(&token, params).await? {
+            match server.read_block(&token, params).await? {
                 None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")),
                 Some(doc) => {
                     let delta_data = Bytes::from(doc.text.clone());
@@ -185,51 +171,50 @@ impl RevisionCloudService for DocumentRevisionCloudServiceImpl {
     }
 }
 
-pub struct DocumentEditorHandlers {
-    inner: DashMap<String, Arc<ClientDocumentEditor>>,
+pub struct BlockEditors {
+    inner: DashMap<String, Arc<ClientBlockEditor>>,
 }
 
-impl DocumentEditorHandlers {
+impl BlockEditors {
     fn new() -> Self {
         Self { inner: DashMap::new() }
     }
 
-    pub(crate) fn insert(&self, doc_id: &str, doc: &Arc<ClientDocumentEditor>) {
-        if self.inner.contains_key(doc_id) {
-            log::warn!("Doc:{} already exists in cache", doc_id);
+    pub(crate) fn insert(&self, block_id: &str, doc: &Arc<ClientBlockEditor>) {
+        if self.inner.contains_key(block_id) {
+            log::warn!("Doc:{} already exists in cache", block_id);
         }
-        self.inner.insert(doc_id.to_string(), doc.clone());
+        self.inner.insert(block_id.to_string(), doc.clone());
     }
 
-    pub(crate) fn contains(&self, doc_id: &str) -> bool {
-        self.inner.get(doc_id).is_some()
+    pub(crate) fn contains(&self, block_id: &str) -> bool {
+        self.inner.get(block_id).is_some()
     }
 
-    pub(crate) fn get(&self, doc_id: &str) -> Option<Arc<ClientDocumentEditor>> {
-        if !self.contains(doc_id) {
+    pub(crate) fn get(&self, block_id: &str) -> Option<Arc<ClientBlockEditor>> {
+        if !self.contains(block_id) {
             return None;
         }
-        let opened_doc = self.inner.get(doc_id).unwrap();
+        let opened_doc = self.inner.get(block_id).unwrap();
         Some(opened_doc.clone())
     }
 
-    pub(crate) fn remove(&self, id: &str) {
-        let doc_id = id.to_string();
-        if let Some(editor) = self.get(id) {
+    pub(crate) fn remove(&self, block_id: &str) {
+        if let Some(editor) = self.get(block_id) {
             editor.stop()
         }
-        self.inner.remove(&doc_id);
+        self.inner.remove(block_id);
     }
 }
 
-#[tracing::instrument(level = "trace", skip(web_socket, receivers))]
-fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, receivers: WebSocketDataReceivers) {
+#[tracing::instrument(level = "trace", skip(web_socket, handlers))]
+fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<BlockEditors>) {
     tokio::spawn(async move {
         let mut notify = web_socket.subscribe_state_changed().await;
         while let Ok(state) = notify.recv().await {
-            for receiver in receivers.iter() {
-                receiver.value().connect_state_changed(state.clone());
-            }
+            handlers.inner.iter().for_each(|handler| {
+                handler.receive_ws_state(&state);
+            })
         }
     });
 }

+ 18 - 18
frontend/rust-lib/flowy-document/src/queue.rs

@@ -1,5 +1,5 @@
 use crate::web_socket::EditorCommandReceiver;
-use crate::DocumentUser;
+use crate::BlockUser;
 use async_stream::stream;
 use flowy_collaboration::util::make_delta_from_revisions;
 use flowy_collaboration::{
@@ -8,7 +8,7 @@ use flowy_collaboration::{
     errors::CollaborateError,
 };
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_sync::{DeltaMD5, RevisionCompact, RevisionManager, TransformDeltas};
+use flowy_sync::{DeltaMD5, RevisionCompact, RevisionManager, RichTextTransformDeltas, TransformDeltas};
 use futures::stream::StreamExt;
 use lib_ot::{
     core::{Interval, OperationTransformable},
@@ -19,16 +19,16 @@ use tokio::sync::{oneshot, RwLock};
 
 // The EditorCommandQueue executes each command that will alter the document in
 // serial.
-pub(crate) struct EditorCommandQueue {
+pub(crate) struct EditBlockQueue {
     document: Arc<RwLock<ClientDocument>>,
-    user: Arc<dyn DocumentUser>,
+    user: Arc<dyn BlockUser>,
     rev_manager: Arc<RevisionManager>,
     receiver: Option<EditorCommandReceiver>,
 }
 
-impl EditorCommandQueue {
+impl EditBlockQueue {
     pub(crate) fn new(
-        user: Arc<dyn DocumentUser>,
+        user: Arc<dyn BlockUser>,
         rev_manager: Arc<RevisionManager>,
         delta: RichTextDelta,
         receiver: EditorCommandReceiver,
@@ -102,7 +102,7 @@ impl EditorCommandQueue {
                         server_prime = Some(s_prime);
                     }
                     drop(read_guard);
-                    Ok::<TransformDeltas<RichTextAttributes>, CollaborateError>(TransformDeltas {
+                    Ok::<RichTextTransformDeltas, CollaborateError>(TransformDeltas {
                         client_prime,
                         server_prime,
                     })
@@ -161,11 +161,11 @@ impl EditorCommandQueue {
                 let _ = self.save_local_delta(delta, md5).await?;
                 let _ = ret.send(Ok(()));
             }
-            EditorCommand::ReadDocumentAsJson { ret } => {
+            EditorCommand::ReadBlockJson { ret } => {
                 let data = self.document.read().await.to_json();
                 let _ = ret.send(Ok(data));
             }
-            EditorCommand::ReadDocumentAsDelta { ret } => {
+            EditorCommand::ReadBlockDelta { ret } => {
                 let delta = self.document.read().await.delta().clone();
                 let _ = ret.send(Ok(delta));
             }
@@ -187,17 +187,17 @@ impl EditorCommandQueue {
         );
         let _ = self
             .rev_manager
-            .add_local_revision::<DocumentRevisionCompact>(&revision)
+            .add_local_revision::<BlockRevisionCompact>(&revision)
             .await?;
         Ok(rev_id.into())
     }
 }
 
-pub(crate) struct DocumentRevisionCompact();
-impl RevisionCompact for DocumentRevisionCompact {
+pub(crate) struct BlockRevisionCompact();
+impl RevisionCompact for BlockRevisionCompact {
     fn compact_revisions(user_id: &str, object_id: &str, mut revisions: Vec<Revision>) -> FlowyResult<Revision> {
         if revisions.is_empty() {
-            return Err(FlowyError::internal().context("Can't compact the empty document's revisions"));
+            return Err(FlowyError::internal().context("Can't compact the empty block's revisions"));
         }
 
         if revisions.len() == 1 {
@@ -232,7 +232,7 @@ pub(crate) enum EditorCommand {
     },
     TransformDelta {
         delta: RichTextDelta,
-        ret: Ret<TransformDeltas<RichTextAttributes>>,
+        ret: Ret<RichTextTransformDeltas>,
     },
     Insert {
         index: usize,
@@ -265,11 +265,11 @@ pub(crate) enum EditorCommand {
     Redo {
         ret: Ret<()>,
     },
-    ReadDocumentAsJson {
+    ReadBlockJson {
         ret: Ret<String>,
     },
     #[allow(dead_code)]
-    ReadDocumentAsDelta {
+    ReadBlockDelta {
         ret: Ret<RichTextDelta>,
     },
 }
@@ -289,8 +289,8 @@ impl std::fmt::Debug for EditorCommand {
             EditorCommand::CanRedo { .. } => "CanRedo",
             EditorCommand::Undo { .. } => "Undo",
             EditorCommand::Redo { .. } => "Redo",
-            EditorCommand::ReadDocumentAsJson { .. } => "ReadDocumentAsJson",
-            EditorCommand::ReadDocumentAsDelta { .. } => "ReadDocumentAsDelta",
+            EditorCommand::ReadBlockJson { .. } => "ReadDocumentAsJson",
+            EditorCommand::ReadBlockDelta { .. } => "ReadDocumentAsDelta",
         };
         f.write_str(s)
     }

+ 37 - 57
frontend/rust-lib/flowy-document/src/web_socket.rs

@@ -1,17 +1,17 @@
-use crate::{queue::EditorCommand, DocumentWSReceiver, DOCUMENT_SYNC_INTERVAL_IN_MILLIS};
-use async_trait::async_trait;
+use crate::{queue::EditorCommand, DOCUMENT_SYNC_INTERVAL_IN_MILLIS};
 use bytes::Bytes;
 use flowy_collaboration::{
     entities::{
         revision::RevisionRange,
-        ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSData, ServerRevisionWSDataType},
+        ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSDataType},
     },
     errors::CollaborateResult,
 };
 use flowy_error::{internal_error, FlowyError};
 use flowy_sync::*;
 use lib_infra::future::{BoxResultFuture, FutureResult};
-use lib_ot::{core::Delta, rich_text::RichTextAttributes};
+use lib_ot::rich_text::RichTextAttributes;
+use lib_ot::rich_text::RichTextDelta;
 use lib_ws::WSConnectState;
 use std::{sync::Arc, time::Duration};
 use tokio::sync::{
@@ -23,33 +23,26 @@ use tokio::sync::{
 pub(crate) type EditorCommandSender = Sender<EditorCommand>;
 pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
 
-pub(crate) async fn make_document_ws_manager(
+pub(crate) async fn make_block_ws_manager(
     doc_id: String,
     user_id: String,
     edit_cmd_tx: EditorCommandSender,
     rev_manager: Arc<RevisionManager>,
     rev_web_socket: Arc<dyn RevisionWebSocket>,
 ) -> Arc<RevisionWebSocketManager> {
-    let composite_sink_provider = Arc::new(CompositeWSSinkDataProvider::new(&doc_id, rev_manager.clone()));
-    let resolve_target = Arc::new(DocumentRevisionResolveTarget { edit_cmd_tx });
-    let resolver = RevisionConflictResolver::<RichTextAttributes>::new(
-        &user_id,
-        resolve_target,
-        Arc::new(composite_sink_provider.clone()),
-        rev_manager,
-    );
-    let ws_stream_consumer = Arc::new(DocumentWSSteamConsumerAdapter {
-        resolver: Arc::new(resolver),
-    });
-
-    let sink_provider = Arc::new(DocumentWSSinkDataProviderAdapter(composite_sink_provider));
+    let ws_data_provider = Arc::new(WSDataProvider::new(&doc_id, Arc::new(rev_manager.clone())));
+    let resolver = Arc::new(BlockConflictResolver { edit_cmd_tx });
+    let conflict_controller =
+        RichTextConflictController::new(&user_id, resolver, Arc::new(ws_data_provider.clone()), rev_manager);
+    let ws_data_stream = Arc::new(BlockRevisionWSDataStream::new(conflict_controller));
+    let ws_data_sink = Arc::new(BlockWSDataSink(ws_data_provider));
     let ping_duration = Duration::from_millis(DOCUMENT_SYNC_INTERVAL_IN_MILLIS);
     let ws_manager = Arc::new(RevisionWebSocketManager::new(
-        "Document",
+        "Block",
         &doc_id,
         rev_web_socket,
-        sink_provider,
-        ws_stream_consumer,
+        ws_data_sink,
+        ws_data_stream,
         ping_duration,
     ));
     listen_document_ws_state(&user_id, &doc_id, ws_manager.scribe_state());
@@ -69,18 +62,26 @@ fn listen_document_ws_state(_user_id: &str, _doc_id: &str, mut subscriber: broad
     });
 }
 
-pub(crate) struct DocumentWSSteamConsumerAdapter {
-    resolver: Arc<RevisionConflictResolver<RichTextAttributes>>,
+pub(crate) struct BlockRevisionWSDataStream {
+    conflict_controller: Arc<RichTextConflictController>,
+}
+
+impl BlockRevisionWSDataStream {
+    pub fn new(conflict_controller: RichTextConflictController) -> Self {
+        Self {
+            conflict_controller: Arc::new(conflict_controller),
+        }
+    }
 }
 
-impl RevisionWSSteamConsumer for DocumentWSSteamConsumerAdapter {
+impl RevisionWSDataStream for BlockRevisionWSDataStream {
     fn receive_push_revision(&self, bytes: Bytes) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.receive_bytes(bytes).await })
     }
 
     fn receive_ack(&self, id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.ack_revision(id, ty).await })
     }
 
@@ -90,25 +91,25 @@ impl RevisionWSSteamConsumer for DocumentWSSteamConsumerAdapter {
     }
 
     fn pull_revisions_in_range(&self, range: RevisionRange) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.send_revisions(range).await })
     }
 }
 
-pub(crate) struct DocumentWSSinkDataProviderAdapter(pub(crate) Arc<CompositeWSSinkDataProvider>);
-impl RevisionWSSinkDataProvider for DocumentWSSinkDataProviderAdapter {
+pub(crate) struct BlockWSDataSink(pub(crate) Arc<WSDataProvider>);
+impl RevisionWSDataIterator for BlockWSDataSink {
     fn next(&self) -> FutureResult<Option<ClientRevisionWSData>, FlowyError> {
         let sink_provider = self.0.clone();
         FutureResult::new(async move { sink_provider.next().await })
     }
 }
 
-struct DocumentRevisionResolveTarget {
+struct BlockConflictResolver {
     edit_cmd_tx: EditorCommandSender,
 }
 
-impl ResolverTarget<RichTextAttributes> for DocumentRevisionResolveTarget {
-    fn compose_delta(&self, delta: Delta<RichTextAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+impl ConflictResolver<RichTextAttributes> for BlockConflictResolver {
+    fn compose_delta(&self, delta: RichTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
             let (ret, rx) = oneshot::channel();
@@ -127,11 +128,11 @@ impl ResolverTarget<RichTextAttributes> for DocumentRevisionResolveTarget {
 
     fn transform_delta(
         &self,
-        delta: Delta<RichTextAttributes>,
-    ) -> BoxResultFuture<flowy_sync::TransformDeltas<RichTextAttributes>, FlowyError> {
+        delta: RichTextDelta,
+    ) -> BoxResultFuture<flowy_sync::RichTextTransformDeltas, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
-            let (ret, rx) = oneshot::channel::<CollaborateResult<TransformDeltas<RichTextAttributes>>>();
+            let (ret, rx) = oneshot::channel::<CollaborateResult<RichTextTransformDeltas>>();
             tx.send(EditorCommand::TransformDelta { delta, ret })
                 .await
                 .map_err(internal_error)?;
@@ -142,7 +143,7 @@ impl ResolverTarget<RichTextAttributes> for DocumentRevisionResolveTarget {
         })
     }
 
-    fn reset_delta(&self, delta: Delta<RichTextAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+    fn reset_delta(&self, delta: RichTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
             let (ret, rx) = oneshot::channel();
@@ -157,24 +158,3 @@ impl ResolverTarget<RichTextAttributes> for DocumentRevisionResolveTarget {
         })
     }
 }
-
-//  RevisionWebSocketManager registers itself as a DocumentWSReceiver for each
-//  opened document.
-#[async_trait]
-impl DocumentWSReceiver for RevisionWebSocketManager {
-    #[tracing::instrument(level = "debug", skip(self, data), err)]
-    async fn receive_ws_data(&self, data: ServerRevisionWSData) -> Result<(), FlowyError> {
-        let _ = self.ws_passthrough_tx.send(data).await.map_err(|e| {
-            let err_msg = format!("{} passthrough error: {}", self.object_id, e);
-            FlowyError::internal().context(err_msg)
-        })?;
-        Ok(())
-    }
-
-    fn connect_state_changed(&self, state: WSConnectState) {
-        match self.state_passthrough_tx.send(state) {
-            Ok(_) => {}
-            Err(e) => tracing::error!("{}", e),
-        }
-    }
-}

+ 4 - 4
frontend/rust-lib/flowy-document/tests/document/edit_script.rs

@@ -1,5 +1,5 @@
 use flowy_collaboration::entities::revision::RevisionState;
-use flowy_document::editor::ClientDocumentEditor;
+use flowy_document::block_editor::ClientBlockEditor;
 use flowy_document::DOCUMENT_SYNC_INTERVAL_IN_MILLIS;
 use flowy_test::{helper::ViewTest, FlowySDKTest};
 use lib_ot::{core::Interval, rich_text::RichTextDelta};
@@ -19,7 +19,7 @@ pub enum EditorScript {
 
 pub struct EditorTest {
     pub sdk: FlowySDKTest,
-    pub editor: Arc<ClientDocumentEditor>,
+    pub editor: Arc<ClientBlockEditor>,
 }
 
 impl EditorTest {
@@ -27,7 +27,7 @@ impl EditorTest {
         let sdk = FlowySDKTest::default();
         let _ = sdk.init_user().await;
         let test = ViewTest::new(&sdk).await;
-        let editor = sdk.document_manager.open_document(&test.view.id).await.unwrap();
+        let editor = sdk.document_manager.open_block(&test.view.id).await.unwrap();
         Self { sdk, editor }
     }
 
@@ -77,7 +77,7 @@ impl EditorTest {
                 let delta = self.editor.doc_delta().await.unwrap();
                 if expected_delta != delta {
                     eprintln!("✅ expect: {}", expected,);
-                    eprintln!("❌ receive: {}", delta.to_json());
+                    eprintln!("❌ receive: {}", delta.to_delta_json());
                 }
                 assert_eq!(expected_delta, delta);
             }

+ 1 - 1
frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs

@@ -774,7 +774,7 @@ fn delta_compose() {
         delta = delta.compose(&d).unwrap();
     }
     assert_eq!(
-        delta.to_json(),
+        delta.to_delta_json(),
         r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\n"}]"#
     );
 

+ 13 - 13
frontend/rust-lib/flowy-document/tests/editor/mod.rs

@@ -108,20 +108,20 @@ impl TestBuilder {
             TestOp::Insert(delta_i, s, index) => {
                 let document = &mut self.documents[*delta_i];
                 let delta = document.insert(*index, s).unwrap();
-                tracing::debug!("Insert delta: {}", delta.to_json());
+                tracing::debug!("Insert delta: {}", delta.to_delta_json());
 
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Delete(delta_i, iv) => {
                 let document = &mut self.documents[*delta_i];
                 let delta = document.replace(*iv, "").unwrap();
-                tracing::trace!("Delete delta: {}", delta.to_json());
+                tracing::trace!("Delete delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Replace(delta_i, iv, s) => {
                 let document = &mut self.documents[*delta_i];
                 let delta = document.replace(*iv, s).unwrap();
-                tracing::trace!("Replace delta: {}", delta.to_json());
+                tracing::trace!("Replace delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::InsertBold(delta_i, s, iv) => {
@@ -133,7 +133,7 @@ impl TestBuilder {
                 let document = &mut self.documents[*delta_i];
                 let attribute = RichTextAttribute::Bold(*enable);
                 let delta = document.format(*iv, attribute).unwrap();
-                tracing::trace!("Bold delta: {}", delta.to_json());
+                tracing::trace!("Bold delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Italic(delta_i, iv, enable) => {
@@ -143,28 +143,28 @@ impl TestBuilder {
                     false => RichTextAttribute::Italic(false),
                 };
                 let delta = document.format(*iv, attribute).unwrap();
-                tracing::trace!("Italic delta: {}", delta.to_json());
+                tracing::trace!("Italic delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Header(delta_i, iv, level) => {
                 let document = &mut self.documents[*delta_i];
                 let attribute = RichTextAttribute::Header(*level);
                 let delta = document.format(*iv, attribute).unwrap();
-                tracing::trace!("Header delta: {}", delta.to_json());
+                tracing::trace!("Header delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Link(delta_i, iv, link) => {
                 let document = &mut self.documents[*delta_i];
                 let attribute = RichTextAttribute::Link(link.to_owned());
                 let delta = document.format(*iv, attribute).unwrap();
-                tracing::trace!("Link delta: {}", delta.to_json());
+                tracing::trace!("Link delta: {}", delta.to_delta_json());
                 self.deltas.insert(*delta_i, Some(delta));
             }
             TestOp::Bullet(delta_i, iv, enable) => {
                 let document = &mut self.documents[*delta_i];
                 let attribute = RichTextAttribute::Bullet(*enable);
                 let delta = document.format(*iv, attribute).unwrap();
-                tracing::debug!("Bullet delta: {}", delta.to_json());
+                tracing::debug!("Bullet delta: {}", delta.to_delta_json());
 
                 self.deltas.insert(*delta_i, Some(delta));
             }
@@ -194,15 +194,15 @@ impl TestBuilder {
                 let delta_a = &self.documents[*delta_a_i].delta();
                 let delta_b = &self.documents[*delta_b_i].delta();
                 tracing::debug!("Invert: ");
-                tracing::debug!("a: {}", delta_a.to_json());
-                tracing::debug!("b: {}", delta_b.to_json());
+                tracing::debug!("a: {}", delta_a.to_delta_json());
+                tracing::debug!("b: {}", delta_b.to_delta_json());
 
                 let (_, b_prime) = delta_a.transform(delta_b).unwrap();
                 let undo = b_prime.invert(delta_a);
 
                 let new_delta = delta_a.compose(&b_prime).unwrap();
-                tracing::debug!("new delta: {}", new_delta.to_json());
-                tracing::debug!("undo delta: {}", undo.to_json());
+                tracing::debug!("new delta: {}", new_delta.to_delta_json());
+                tracing::debug!("undo delta: {}", undo.to_delta_json());
 
                 let new_delta_after_undo = new_delta.compose(&undo).unwrap();
 
@@ -238,7 +238,7 @@ impl TestBuilder {
             }
 
             TestOp::AssertPrimeJson(doc_i, expected) => {
-                let prime_json = self.primes[*doc_i].as_ref().unwrap().to_json();
+                let prime_json = self.primes[*doc_i].as_ref().unwrap().to_delta_json();
                 let expected_prime: RichTextDelta = serde_json::from_str(expected).unwrap();
                 let target_prime: RichTextDelta = serde_json::from_str(&prime_json).unwrap();
 

+ 1 - 1
frontend/rust-lib/flowy-document/tests/editor/serde_test.rs

@@ -92,7 +92,7 @@ fn delta_deserialize_null_test() {
     attribute.value = RichTextAttributeValue(None);
     let delta2 = DeltaBuilder::new().retain_with_attributes(7, attribute.into()).build();
 
-    assert_eq!(delta2.to_json(), r#"[{"retain":7,"attributes":{"bold":""}}]"#);
+    assert_eq!(delta2.to_delta_json(), r#"[{"retain":7,"attributes":{"bold":""}}]"#);
     assert_eq!(delta1, delta2);
 }
 

+ 13 - 11
frontend/rust-lib/flowy-folder/src/controller.rs

@@ -6,8 +6,9 @@ use flowy_sync::RevisionWebSocket;
 use lazy_static::lazy_static;
 
 use flowy_collaboration::{client_folder::FolderPad, entities::ws_data::ServerRevisionWSData};
-use flowy_document::FlowyDocumentManager;
+use flowy_document::BlockManager;
 
+use flowy_collaboration::entities::revision::{RepeatedRevision, Revision};
 use std::{collections::HashMap, convert::TryInto, fmt::Formatter, sync::Arc};
 use tokio::sync::RwLock as TokioRwLock;
 
@@ -17,7 +18,7 @@ use crate::{
     errors::FlowyResult,
     event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
     services::{
-        folder_editor::FolderEditor, persistence::FolderPersistence, set_current_workspace, AppController,
+        folder_editor::ClientFolderEditor, persistence::FolderPersistence, set_current_workspace, AppController,
         TrashController, ViewController, WorkspaceController,
     },
 };
@@ -63,7 +64,7 @@ pub struct FolderManager {
     pub(crate) view_controller: Arc<ViewController>,
     pub(crate) trash_controller: Arc<TrashController>,
     web_socket: Arc<dyn RevisionWebSocket>,
-    folder_editor: Arc<TokioRwLock<Option<Arc<FolderEditor>>>>,
+    folder_editor: Arc<TokioRwLock<Option<Arc<ClientFolderEditor>>>>,
 }
 
 impl FolderManager {
@@ -71,7 +72,7 @@ impl FolderManager {
         user: Arc<dyn WorkspaceUser>,
         cloud_service: Arc<dyn FolderCouldServiceV1>,
         database: Arc<dyn WorkspaceDatabase>,
-        document_manager: Arc<FlowyDocumentManager>,
+        document_manager: Arc<BlockManager>,
         web_socket: Arc<dyn RevisionWebSocket>,
     ) -> Self {
         if let Ok(user_id) = user.user_id() {
@@ -162,7 +163,7 @@ impl FolderManager {
         let _ = self.persistence.initialize(user_id, &folder_id).await?;
 
         let pool = self.persistence.db_pool()?;
-        let folder_editor = FolderEditor::new(user_id, &folder_id, token, pool, self.web_socket.clone()).await?;
+        let folder_editor = ClientFolderEditor::new(user_id, &folder_id, token, pool, self.web_socket.clone()).await?;
         *self.folder_editor.write().await = Some(Arc::new(folder_editor));
 
         let _ = self.app_controller.initialize()?;
@@ -196,14 +197,15 @@ impl DefaultFolderBuilder {
         for app in workspace.apps.iter() {
             for (index, view) in app.belongings.iter().enumerate() {
                 let view_data = if index == 0 {
-                    initial_read_me().to_json()
+                    initial_read_me().to_delta_json()
                 } else {
-                    initial_delta().to_json()
+                    initial_delta().to_delta_json()
                 };
                 view_controller.set_latest_view(view);
-                let _ = view_controller
-                    .create_view_document_content(&view.id, view_data)
-                    .await?;
+                let delta_data = Bytes::from(view_data);
+                let repeated_revision: RepeatedRevision =
+                    Revision::initial_revision(user_id, &view.id, delta_data).into();
+                let _ = view_controller.create_view(&view.id, repeated_revision).await?;
             }
         }
         let folder = FolderPad::new(vec![workspace.clone()], vec![])?;
@@ -219,7 +221,7 @@ impl DefaultFolderBuilder {
 
 #[cfg(feature = "flowy_unit_test")]
 impl FolderManager {
-    pub async fn folder_editor(&self) -> Arc<FolderEditor> {
+    pub async fn folder_editor(&self) -> Arc<ClientFolderEditor> {
         self.folder_editor.read().await.clone().unwrap()
     }
 }

+ 4 - 4
frontend/rust-lib/flowy-folder/src/event_map.rs

@@ -63,9 +63,9 @@ pub fn create(folder: Arc<FolderManager>) -> Module {
         .event(FolderEvent::UpdateView, update_view_handler)
         .event(FolderEvent::DeleteView, delete_view_handler)
         .event(FolderEvent::DuplicateView, duplicate_view_handler)
-        .event(FolderEvent::OpenView, open_document_handler)
+        .event(FolderEvent::OpenView, open_view_handler)
         .event(FolderEvent::CloseView, close_view_handler)
-        .event(FolderEvent::ApplyDocDelta, document_delta_handler);
+        .event(FolderEvent::ApplyDocDelta, block_delta_handler);
 
     module = module
         .event(FolderEvent::ReadTrash, read_trash_handler)
@@ -130,7 +130,7 @@ pub enum FolderEvent {
     #[event()]
     CopyLink = 206,
 
-    #[event(input = "ViewId", output = "DocumentDelta")]
+    #[event(input = "ViewId", output = "BlockDelta")]
     OpenView = 207,
 
     #[event(input = "ViewId")]
@@ -151,7 +151,7 @@ pub enum FolderEvent {
     #[event()]
     DeleteAllTrash = 304,
 
-    #[event(input = "DocumentDelta", output = "DocumentDelta")]
+    #[event(input = "BlockDelta", output = "BlockDelta")]
     ApplyDocDelta = 400,
 
     #[event(input = "ExportPayload", output = "ExportData")]

+ 7 - 7
frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs

@@ -6,12 +6,12 @@ use crate::{
     errors::FlowyError,
     services::{AppController, TrashController, ViewController},
 };
-use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
+use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::{convert::TryInto, sync::Arc};
 
 pub(crate) async fn create_app_handler(
     data: Data<CreateAppPayload>,
-    controller: Unit<Arc<AppController>>,
+    controller: AppData<Arc<AppController>>,
 ) -> DataResult<App, FlowyError> {
     let params: CreateAppParams = data.into_inner().try_into()?;
     let detail = controller.create_app_from_params(params).await?;
@@ -21,8 +21,8 @@ pub(crate) async fn create_app_handler(
 
 pub(crate) async fn delete_app_handler(
     data: Data<AppId>,
-    app_controller: Unit<Arc<AppController>>,
-    trash_controller: Unit<Arc<TrashController>>,
+    app_controller: AppData<Arc<AppController>>,
+    trash_controller: AppData<Arc<TrashController>>,
 ) -> Result<(), FlowyError> {
     let params: AppId = data.into_inner();
     let trash = app_controller
@@ -39,7 +39,7 @@ pub(crate) async fn delete_app_handler(
 #[tracing::instrument(skip(data, controller))]
 pub(crate) async fn update_app_handler(
     data: Data<UpdateAppPayload>,
-    controller: Unit<Arc<AppController>>,
+    controller: AppData<Arc<AppController>>,
 ) -> Result<(), FlowyError> {
     let params: UpdateAppParams = data.into_inner().try_into()?;
     let _ = controller.update_app(params).await?;
@@ -49,8 +49,8 @@ pub(crate) async fn update_app_handler(
 #[tracing::instrument(skip(data, app_controller, view_controller))]
 pub(crate) async fn read_app_handler(
     data: Data<AppId>,
-    app_controller: Unit<Arc<AppController>>,
-    view_controller: Unit<Arc<ViewController>>,
+    app_controller: AppData<Arc<AppController>>,
+    view_controller: AppData<Arc<ViewController>>,
 ) -> DataResult<App, FlowyError> {
     let params: AppId = data.into_inner();
     let mut app = app_controller.read_app(params.clone()).await?;

+ 12 - 12
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs

@@ -8,16 +8,16 @@ use crate::controller::FolderId;
 use flowy_collaboration::util::make_delta_from_revisions;
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_sync::{
-    RevisionCache, RevisionCloudService, RevisionCompact, RevisionManager, RevisionObjectBuilder, RevisionWebSocket,
-    RevisionWebSocketManager,
+    RevisionCloudService, RevisionCompact, RevisionManager, RevisionObjectBuilder, RevisionPersistence,
+    RevisionWebSocket, RevisionWebSocketManager,
 };
 use lib_infra::future::FutureResult;
-use lib_ot::core::PlainAttributes;
+use lib_ot::core::PlainTextAttributes;
 use lib_sqlite::ConnectionPool;
 use parking_lot::RwLock;
 use std::sync::Arc;
 
-pub struct FolderEditor {
+pub struct ClientFolderEditor {
     user_id: String,
     pub(crate) folder_id: FolderId,
     pub(crate) folder: Arc<RwLock<FolderPad>>,
@@ -25,7 +25,7 @@ pub struct FolderEditor {
     ws_manager: Arc<RevisionWebSocketManager>,
 }
 
-impl FolderEditor {
+impl ClientFolderEditor {
     pub async fn new(
         user_id: &str,
         folder_id: &FolderId,
@@ -33,9 +33,9 @@ impl FolderEditor {
         pool: Arc<ConnectionPool>,
         web_socket: Arc<dyn RevisionWebSocket>,
     ) -> FlowyResult<Self> {
-        let cache = Arc::new(RevisionCache::new(user_id, folder_id.as_ref(), pool));
-        let mut rev_manager = RevisionManager::new(user_id, folder_id.as_ref(), cache);
-        let cloud = Arc::new(FolderRevisionCloudServiceImpl {
+        let rev_persistence = Arc::new(RevisionPersistence::new(user_id, folder_id.as_ref(), pool));
+        let mut rev_manager = RevisionManager::new(user_id, folder_id.as_ref(), rev_persistence);
+        let cloud = Arc::new(FolderRevisionCloudService {
             token: token.to_string(),
         });
         let folder = Arc::new(RwLock::new(
@@ -109,12 +109,12 @@ impl RevisionObjectBuilder for FolderPadBuilder {
     }
 }
 
-struct FolderRevisionCloudServiceImpl {
+struct FolderRevisionCloudService {
     #[allow(dead_code)]
     token: String,
 }
 
-impl RevisionCloudService for FolderRevisionCloudServiceImpl {
+impl RevisionCloudService for FolderRevisionCloudService {
     #[tracing::instrument(level = "trace", skip(self))]
     fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
         FutureResult::new(async move { Ok(vec![]) })
@@ -122,7 +122,7 @@ impl RevisionCloudService for FolderRevisionCloudServiceImpl {
 }
 
 #[cfg(feature = "flowy_unit_test")]
-impl FolderEditor {
+impl ClientFolderEditor {
     pub fn rev_manager(&self) -> Arc<RevisionManager> {
         self.rev_manager.clone()
     }
@@ -144,7 +144,7 @@ impl RevisionCompact for FolderRevisionCompact {
 
         let (base_rev_id, rev_id) = first_revision.pair_rev_id();
         let md5 = last_revision.md5.clone();
-        let delta = make_delta_from_revisions::<PlainAttributes>(revisions)?;
+        let delta = make_delta_from_revisions::<PlainTextAttributes>(revisions)?;
         let delta_data = delta.to_bytes();
         Ok(Revision::new(object_id, base_rev_id, rev_id, delta_data, user_id, md5))
     }

+ 35 - 2
frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs

@@ -1,3 +1,4 @@
+use crate::controller::FolderId;
 use crate::{
     event_map::WorkspaceDatabase,
     services::persistence::{AppTableSql, TrashTableSql, ViewTableSql, WorkspaceTableSql},
@@ -10,9 +11,11 @@ use flowy_folder_data_model::entities::{
     view::{RepeatedView, View},
     workspace::Workspace,
 };
+use flowy_sync::{RevisionLoader, RevisionPersistence};
 use std::sync::Arc;
 
-pub(crate) const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
+const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
+const V2_MIGRATION: &str = "FOLDER_V2_MIGRATION";
 
 pub(crate) struct FolderMigration {
     user_id: String,
@@ -32,7 +35,7 @@ impl FolderMigration {
         if KV::get_bool(&key) {
             return Ok(None);
         }
-        tracing::trace!("Run folder version 1 migrations");
+
         let pool = self.database.db_pool()?;
         let conn = &*pool.get()?;
         let workspaces = conn.immediate_transaction::<_, FlowyError, _>(|| {
@@ -62,6 +65,7 @@ impl FolderMigration {
         })?;
 
         if workspaces.is_empty() {
+            tracing::trace!("Run folder v1 migration, but workspace is empty");
             KV::set_bool(&key, true);
             return Ok(None);
         }
@@ -73,6 +77,35 @@ impl FolderMigration {
 
         let folder = FolderPad::new(workspaces, trash)?;
         KV::set_bool(&key, true);
+        tracing::trace!("Run folder v1 migration");
         Ok(Some(folder))
     }
+
+    pub async fn run_v2_migration(&self, user_id: &str, folder_id: &FolderId) -> FlowyResult<Option<FolderPad>> {
+        let key = md5(format!("{}{}", self.user_id, V2_MIGRATION));
+        if KV::get_bool(&key) {
+            return Ok(None);
+        }
+        let pool = self.database.db_pool()?;
+        let rev_persistence = Arc::new(RevisionPersistence::new(user_id, folder_id.as_ref(), pool.clone()));
+        let (revisions, _) = RevisionLoader {
+            object_id: folder_id.as_ref().to_owned(),
+            user_id: self.user_id.clone(),
+            cloud: None,
+            rev_persistence,
+        }
+        .load()
+        .await?;
+
+        if revisions.is_empty() {
+            tracing::trace!("Run folder v2 migration, but revision is empty");
+            KV::set_bool(&key, true);
+            return Ok(None);
+        }
+
+        let pad = FolderPad::from_revisions(revisions)?;
+        KV::set_bool(&key, true);
+        tracing::trace!("Run folder v2 migration");
+        Ok(Some(pad))
+    }
 }

+ 13 - 7
frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs

@@ -2,6 +2,7 @@ mod migration;
 pub mod version_1;
 mod version_2;
 
+use flowy_collaboration::client_folder::initial_folder_delta;
 use flowy_collaboration::{
     client_folder::FolderPad,
     entities::revision::{Revision, RevisionState},
@@ -13,7 +14,7 @@ pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::
 use crate::{
     controller::FolderId,
     event_map::WorkspaceDatabase,
-    services::{folder_editor::FolderEditor, persistence::migration::FolderMigration},
+    services::{folder_editor::ClientFolderEditor, persistence::migration::FolderMigration},
 };
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_folder_data_model::entities::{
@@ -50,11 +51,14 @@ pub trait FolderPersistenceTransaction {
 
 pub struct FolderPersistence {
     database: Arc<dyn WorkspaceDatabase>,
-    folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>,
+    folder_editor: Arc<RwLock<Option<Arc<ClientFolderEditor>>>>,
 }
 
 impl FolderPersistence {
-    pub fn new(database: Arc<dyn WorkspaceDatabase>, folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>) -> Self {
+    pub fn new(
+        database: Arc<dyn WorkspaceDatabase>,
+        folder_editor: Arc<RwLock<Option<Arc<ClientFolderEditor>>>>,
+    ) -> Self {
         Self {
             database,
             folder_editor,
@@ -102,7 +106,10 @@ impl FolderPersistence {
     pub async fn initialize(&self, user_id: &str, folder_id: &FolderId) -> FlowyResult<()> {
         let migrations = FolderMigration::new(user_id, self.database.clone());
         if let Some(migrated_folder) = migrations.run_v1_migration()? {
-            tracing::trace!("Save migration folder");
+            self.save_folder(user_id, folder_id, migrated_folder).await?;
+        }
+
+        if let Some(migrated_folder) = migrations.run_v2_migration(user_id, folder_id).await? {
             self.save_folder(user_id, folder_id, migrated_folder).await?;
         }
 
@@ -111,7 +118,7 @@ impl FolderPersistence {
 
     pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> {
         let pool = self.database.db_pool()?;
-        let delta_data = folder.delta().to_bytes();
+        let delta_data = initial_folder_delta(&folder)?.to_bytes();
         let md5 = folder.md5();
         let revision = Revision::new(folder_id.as_ref(), 0, 0, delta_data, user_id, md5);
         let record = RevisionRecord {
@@ -120,8 +127,7 @@ impl FolderPersistence {
             write_to_disk: true,
         };
 
-        let conn = pool.get()?;
         let disk_cache = mk_revision_disk_cache(user_id, pool);
-        disk_cache.create_revision_records(vec![record], &conn)
+        disk_cache.delete_and_insert_records(folder_id.as_ref(), None, vec![record])
     }
 }

+ 27 - 63
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs

@@ -1,7 +1,7 @@
 use crate::{
     entities::{
         trash::{Trash, TrashType},
-        view::{RepeatedView, UpdateViewParams, View, ViewType},
+        view::{RepeatedView, UpdateViewParams, View, ViewDataType},
     },
     errors::FlowyError,
     services::persistence::version_1::app_sql::AppTable,
@@ -65,49 +65,6 @@ impl ViewTableSql {
     }
 }
 
-// pub(crate) fn read_views(
-//     belong_to_id: &str,
-//     is_trash: Option<bool>,
-//     conn: &SqliteConnection,
-// ) -> Result<RepeatedView, FlowyError> {
-//     let views = dsl::view_table
-//         .inner_join(trash_table::dsl::trash_table.on(trash_id.ne(view_table::
-// id)))         .filter(view_table::belong_to_id.eq(belong_to_id))
-//         .select((
-//             view_table::id,
-//             view_table::belong_to_id,
-//             view_table::name,
-//             view_table::desc,
-//             view_table::modified_time,
-//             view_table::create_time,
-//             view_table::thumbnail,
-//             view_table::view_type,
-//             view_table::version,
-//         ))
-//         .load(conn)?
-//         .into_iter()
-//         .map(
-//             |(id, belong_to_id, name, desc, create_time, modified_time,
-// thumbnail, view_type, version)| {                 ViewTable {
-//                     id,
-//                     belong_to_id,
-//                     name,
-//                     desc,
-//                     modified_time,
-//                     create_time,
-//                     thumbnail,
-//                     view_type,
-//                     version,
-//                     is_trash: false,
-//                 }
-//                 .into()
-//             },
-//         )
-//         .collect::<Vec<View>>();
-//
-//     Ok(RepeatedView { items: views })
-// }
-
 #[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
 #[belongs_to(AppTable, foreign_key = "belong_to_id")]
 #[table_name = "view_table"]
@@ -119,16 +76,16 @@ pub(crate) struct ViewTable {
     pub modified_time: i64,
     pub create_time: i64,
     pub thumbnail: String,
-    pub view_type: ViewTableType,
+    pub view_type: SqlViewDataType,
     pub version: i64,
     pub is_trash: bool,
 }
 
 impl ViewTable {
     pub fn new(view: View) -> Self {
-        let view_type = match view.view_type {
-            ViewType::Blank => ViewTableType::Docs,
-            ViewType::Doc => ViewTableType::Docs,
+        let data_type = match view.data_type {
+            ViewDataType::RichText => SqlViewDataType::RichText,
+            ViewDataType::PlainText => SqlViewDataType::PlainText,
         };
 
         ViewTable {
@@ -138,9 +95,8 @@ impl ViewTable {
             desc: view.desc,
             modified_time: view.modified_time,
             create_time: view.create_time,
-            // TODO: thumbnail
-            thumbnail: "".to_owned(),
-            view_type,
+            thumbnail: view.thumbnail,
+            view_type: data_type,
             version: 0,
             is_trash: false,
         }
@@ -149,8 +105,9 @@ impl ViewTable {
 
 impl std::convert::From<ViewTable> for View {
     fn from(table: ViewTable) -> Self {
-        let view_type = match table.view_type {
-            ViewTableType::Docs => ViewType::Doc,
+        let data_type = match table.view_type {
+            SqlViewDataType::RichText => ViewDataType::RichText,
+            SqlViewDataType::PlainText => ViewDataType::PlainText,
         };
 
         View {
@@ -158,11 +115,16 @@ impl std::convert::From<ViewTable> for View {
             belong_to_id: table.belong_to_id,
             name: table.name,
             desc: table.desc,
-            view_type,
+            data_type,
             belongings: RepeatedView::default(),
             modified_time: table.modified_time,
             version: table.version,
             create_time: table.create_time,
+            ext_data: "".to_string(),
+            thumbnail: table.thumbnail,
+            // Store the view in ViewTable was deprecated since v0.0.2.
+            // No need worry about plugin_type.
+            plugin_type: 0,
         }
     }
 }
@@ -214,32 +176,34 @@ impl ViewChangeset {
 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
 #[repr(i32)]
 #[sql_type = "Integer"]
-pub enum ViewTableType {
-    Docs = 0,
+pub enum SqlViewDataType {
+    RichText = 0,
+    PlainText = 1,
 }
 
-impl std::default::Default for ViewTableType {
+impl std::default::Default for SqlViewDataType {
     fn default() -> Self {
-        ViewTableType::Docs
+        SqlViewDataType::RichText
     }
 }
 
-impl std::convert::From<i32> for ViewTableType {
+impl std::convert::From<i32> for SqlViewDataType {
     fn from(value: i32) -> Self {
         match value {
-            0 => ViewTableType::Docs,
+            0 => SqlViewDataType::RichText,
+            1 => SqlViewDataType::PlainText,
             o => {
                 log::error!("Unsupported view type {}, fallback to ViewType::Docs", o);
-                ViewTableType::Docs
+                SqlViewDataType::PlainText
             }
         }
     }
 }
 
-impl ViewTableType {
+impl SqlViewDataType {
     pub fn value(&self) -> i32 {
         *self as i32
     }
 }
 
-impl_sql_integer_expression!(ViewTableType);
+impl_sql_integer_expression!(SqlViewDataType);

+ 2 - 2
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs

@@ -1,5 +1,5 @@
 use crate::services::{
-    folder_editor::FolderEditor,
+    folder_editor::ClientFolderEditor,
     persistence::{AppChangeset, FolderPersistenceTransaction, ViewChangeset, WorkspaceChangeset},
 };
 use flowy_error::{FlowyError, FlowyResult};
@@ -11,7 +11,7 @@ use flowy_folder_data_model::entities::{
 };
 use std::sync::Arc;
 
-impl FolderPersistenceTransaction for FolderEditor {
+impl FolderPersistenceTransaction for ClientFolderEditor {
     fn create_workspace(&self, _user_id: &str, workspace: Workspace) -> FlowyResult<()> {
         if let Some(change) = self.folder.write().create_workspace(workspace)? {
             let _ = self.apply_change(change)?;

+ 6 - 6
frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs

@@ -3,12 +3,12 @@ use crate::{
     errors::FlowyError,
     services::TrashController,
 };
-use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
+use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::sync::Arc;
 
 #[tracing::instrument(skip(controller), err)]
 pub(crate) async fn read_trash_handler(
-    controller: Unit<Arc<TrashController>>,
+    controller: AppData<Arc<TrashController>>,
 ) -> DataResult<RepeatedTrash, FlowyError> {
     let repeated_trash = controller.read_trash().await?;
     data_result(repeated_trash)
@@ -17,7 +17,7 @@ pub(crate) async fn read_trash_handler(
 #[tracing::instrument(skip(identifier, controller), err)]
 pub(crate) async fn putback_trash_handler(
     identifier: Data<TrashId>,
-    controller: Unit<Arc<TrashController>>,
+    controller: AppData<Arc<TrashController>>,
 ) -> Result<(), FlowyError> {
     let _ = controller.putback(&identifier.id).await?;
     Ok(())
@@ -26,20 +26,20 @@ pub(crate) async fn putback_trash_handler(
 #[tracing::instrument(skip(identifiers, controller), err)]
 pub(crate) async fn delete_trash_handler(
     identifiers: Data<RepeatedTrashId>,
-    controller: Unit<Arc<TrashController>>,
+    controller: AppData<Arc<TrashController>>,
 ) -> Result<(), FlowyError> {
     let _ = controller.delete(identifiers.into_inner()).await?;
     Ok(())
 }
 
 #[tracing::instrument(skip(controller), err)]
-pub(crate) async fn restore_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
+pub(crate) async fn restore_all_trash_handler(controller: AppData<Arc<TrashController>>) -> Result<(), FlowyError> {
     let _ = controller.restore_all_trash().await?;
     Ok(())
 }
 
 #[tracing::instrument(skip(controller), err)]
-pub(crate) async fn delete_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
+pub(crate) async fn delete_all_trash_handler(controller: AppData<Arc<TrashController>>) -> Result<(), FlowyError> {
     let _ = controller.delete_all_trash().await?;
     Ok(())
 }

+ 41 - 49
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -1,6 +1,6 @@
 use bytes::Bytes;
 use flowy_collaboration::entities::{
-    document_info::{DocumentDelta, DocumentId},
+    document_info::{BlockDelta, BlockId},
     revision::{RepeatedRevision, Revision},
 };
 
@@ -22,8 +22,9 @@ use crate::{
     },
 };
 use flowy_database::kv::KV;
-use flowy_document::FlowyDocumentManager;
+use flowy_document::BlockManager;
 use flowy_folder_data_model::entities::share::{ExportData, ExportParams};
+
 use lib_infra::uuid_string;
 
 const LATEST_VIEW_ID: &str = "latest_view_id";
@@ -33,7 +34,7 @@ pub(crate) struct ViewController {
     cloud_service: Arc<dyn FolderCouldServiceV1>,
     persistence: Arc<FolderPersistence>,
     trash_controller: Arc<TrashController>,
-    document_manager: Arc<FlowyDocumentManager>,
+    block_manager: Arc<BlockManager>,
 }
 
 impl ViewController {
@@ -42,62 +43,51 @@ impl ViewController {
         persistence: Arc<FolderPersistence>,
         cloud_service: Arc<dyn FolderCouldServiceV1>,
         trash_can: Arc<TrashController>,
-        document_manager: Arc<FlowyDocumentManager>,
+        document_manager: Arc<BlockManager>,
     ) -> Self {
         Self {
             user,
             cloud_service,
             persistence,
             trash_controller: trash_can,
-            document_manager,
+            block_manager: document_manager,
         }
     }
 
     pub(crate) fn initialize(&self) -> Result<(), FlowyError> {
-        let _ = self.document_manager.init()?;
+        let _ = self.block_manager.init()?;
         self.listen_trash_can_event();
         Ok(())
     }
 
     #[tracing::instrument(level = "trace", skip(self, params), fields(name = %params.name), err)]
     pub(crate) async fn create_view_from_params(&self, params: CreateViewParams) -> Result<View, FlowyError> {
-        let view_data = if params.view_data.is_empty() {
+        let view_data = if params.data.is_empty() {
             initial_delta_string()
         } else {
-            params.view_data.clone()
+            params.data.clone()
         };
 
         let delta_data = Bytes::from(view_data);
         let user_id = self.user.user_id()?;
         let repeated_revision: RepeatedRevision =
             Revision::initial_revision(&user_id, &params.view_id, delta_data).into();
-        let _ = self
-            .document_manager
-            .reset_with_revisions(&params.view_id, repeated_revision)
-            .await?;
+        let _ = self.create_view(&params.view_id, repeated_revision).await?;
         let view = self.create_view_on_server(params).await?;
         let _ = self.create_view_on_local(view.clone()).await?;
-
         Ok(view)
     }
 
-    #[tracing::instrument(level = "debug", skip(self, view_id, view_data), err)]
-    pub(crate) async fn create_view_document_content(
+    #[tracing::instrument(level = "debug", skip(self, view_id, repeated_revision), err)]
+    pub(crate) async fn create_view(
         &self,
         view_id: &str,
-        view_data: String,
+        repeated_revision: RepeatedRevision,
     ) -> Result<(), FlowyError> {
-        if view_data.is_empty() {
+        if repeated_revision.is_empty() {
             return Err(FlowyError::internal().context("The content of the view should not be empty"));
         }
-
-        let delta_data = Bytes::from(view_data);
-        let user_id = self.user.user_id()?;
-        let repeated_revision: RepeatedRevision = Revision::initial_revision(&user_id, view_id, delta_data).into();
-        let _ = self
-            .document_manager
-            .reset_with_revisions(view_id, repeated_revision)
-            .await?;
+        let _ = self.block_manager.create_block(view_id, repeated_revision).await?;
         Ok(())
     }
 
@@ -143,50 +133,52 @@ impl ViewController {
     }
 
     #[tracing::instrument(level = "debug", skip(self), err)]
-    pub(crate) async fn open_document(&self, doc_id: &str) -> Result<DocumentDelta, FlowyError> {
-        let editor = self.document_manager.open_document(doc_id).await?;
-        KV::set_str(LATEST_VIEW_ID, doc_id.to_owned());
-        let document_json = editor.document_json().await?;
-        Ok(DocumentDelta {
-            doc_id: doc_id.to_string(),
+    pub(crate) async fn open_view(&self, view_id: &str) -> Result<BlockDelta, FlowyError> {
+        let editor = self.block_manager.open_block(view_id).await?;
+        KV::set_str(LATEST_VIEW_ID, view_id.to_owned());
+        let document_json = editor.block_json().await?;
+        Ok(BlockDelta {
+            block_id: view_id.to_string(),
             delta_json: document_json,
         })
     }
 
     #[tracing::instrument(level = "debug", skip(self), err)]
     pub(crate) async fn close_view(&self, doc_id: &str) -> Result<(), FlowyError> {
-        let _ = self.document_manager.close_document(doc_id)?;
+        let _ = self.block_manager.close_block(doc_id)?;
         Ok(())
     }
 
     #[tracing::instrument(level = "debug", skip(self,params), fields(doc_id = %params.value), err)]
-    pub(crate) async fn delete_view(&self, params: DocumentId) -> Result<(), FlowyError> {
+    pub(crate) async fn delete_view(&self, params: BlockId) -> Result<(), FlowyError> {
         if let Some(view_id) = KV::get_str(LATEST_VIEW_ID) {
             if view_id == params.value {
                 let _ = KV::remove(LATEST_VIEW_ID);
             }
         }
-        let _ = self.document_manager.close_document(&params.value)?;
+        let _ = self.block_manager.close_block(&params.value)?;
         Ok(())
     }
 
     #[tracing::instrument(level = "debug", skip(self), err)]
-    pub(crate) async fn duplicate_view(&self, doc_id: &str) -> Result<(), FlowyError> {
+    pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> {
         let view = self
             .persistence
-            .begin_transaction(|transaction| transaction.read_view(doc_id))
+            .begin_transaction(|transaction| transaction.read_view(view_id))
             .await?;
 
-        let editor = self.document_manager.open_document(doc_id).await?;
-        let document_json = editor.document_json().await?;
+        let editor = self.block_manager.open_block(view_id).await?;
+        let document_json = editor.block_json().await?;
         let duplicate_params = CreateViewParams {
             belong_to_id: view.belong_to_id.clone(),
             name: format!("{} (copy)", &view.name),
-            desc: view.desc.clone(),
-            thumbnail: "".to_owned(),
-            view_type: view.view_type.clone(),
-            view_data: document_json,
+            desc: view.desc,
+            thumbnail: view.thumbnail,
+            data_type: view.data_type,
+            data: document_json,
             view_id: uuid_string(),
+            ext_data: view.ext_data,
+            plugin_type: view.plugin_type,
         };
 
         let _ = self.create_view_from_params(duplicate_params).await?;
@@ -194,9 +186,9 @@ impl ViewController {
     }
 
     #[tracing::instrument(level = "debug", skip(self, params), err)]
-    pub(crate) async fn export_doc(&self, params: ExportParams) -> Result<ExportData, FlowyError> {
-        let editor = self.document_manager.open_document(&params.doc_id).await?;
-        let delta_json = editor.document_json().await?;
+    pub(crate) async fn export_view(&self, params: ExportParams) -> Result<ExportData, FlowyError> {
+        let editor = self.block_manager.open_block(&params.view_id).await?;
+        let delta_json = editor.block_json().await?;
         Ok(ExportData {
             data: delta_json,
             export_type: params.export_type,
@@ -234,8 +226,8 @@ impl ViewController {
         Ok(view)
     }
 
-    pub(crate) async fn receive_document_delta(&self, params: DocumentDelta) -> Result<DocumentDelta, FlowyError> {
-        let doc = self.document_manager.receive_local_delta(params).await?;
+    pub(crate) async fn receive_delta(&self, params: BlockDelta) -> Result<BlockDelta, FlowyError> {
+        let doc = self.block_manager.receive_local_delta(params).await?;
         Ok(doc)
     }
 
@@ -312,7 +304,7 @@ impl ViewController {
     fn listen_trash_can_event(&self) {
         let mut rx = self.trash_controller.subscribe();
         let persistence = self.persistence.clone();
-        let document_manager = self.document_manager.clone();
+        let document_manager = self.block_manager.clone();
         let trash_controller = self.trash_controller.clone();
         let _ = tokio::spawn(async move {
             loop {
@@ -340,7 +332,7 @@ impl ViewController {
 #[tracing::instrument(level = "trace", skip(persistence, document_manager, trash_can))]
 async fn handle_trash_event(
     persistence: Arc<FolderPersistence>,
-    document_manager: Arc<FlowyDocumentManager>,
+    document_manager: Arc<BlockManager>,
     trash_can: Arc<TrashController>,
     event: TrashEvent,
 ) {

+ 21 - 21
frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs

@@ -8,14 +8,14 @@ use crate::{
     errors::FlowyError,
     services::{TrashController, ViewController},
 };
-use flowy_collaboration::entities::document_info::DocumentDelta;
+use flowy_collaboration::entities::document_info::BlockDelta;
 use flowy_folder_data_model::entities::share::{ExportData, ExportParams, ExportPayload};
-use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
+use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::{convert::TryInto, sync::Arc};
 
 pub(crate) async fn create_view_handler(
     data: Data<CreateViewPayload>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> DataResult<View, FlowyError> {
     let params: CreateViewParams = data.into_inner().try_into()?;
     let view = controller.create_view_from_params(params).await?;
@@ -24,7 +24,7 @@ pub(crate) async fn create_view_handler(
 
 pub(crate) async fn read_view_handler(
     data: Data<ViewId>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> DataResult<View, FlowyError> {
     let view_id: ViewId = data.into_inner();
     let mut view = controller.read_view(view_id.clone()).await?;
@@ -38,7 +38,7 @@ pub(crate) async fn read_view_handler(
 #[tracing::instrument(skip(data, controller), err)]
 pub(crate) async fn update_view_handler(
     data: Data<UpdateViewPayload>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> Result<(), FlowyError> {
     let params: UpdateViewParams = data.into_inner().try_into()?;
     let _ = controller.update_view(params).await?;
@@ -46,18 +46,18 @@ pub(crate) async fn update_view_handler(
     Ok(())
 }
 
-pub(crate) async fn document_delta_handler(
-    data: Data<DocumentDelta>,
-    controller: Unit<Arc<ViewController>>,
-) -> DataResult<DocumentDelta, FlowyError> {
-    let doc = controller.receive_document_delta(data.into_inner()).await?;
-    data_result(doc)
+pub(crate) async fn block_delta_handler(
+    data: Data<BlockDelta>,
+    controller: AppData<Arc<ViewController>>,
+) -> DataResult<BlockDelta, FlowyError> {
+    let block_delta = controller.receive_delta(data.into_inner()).await?;
+    data_result(block_delta)
 }
 
 pub(crate) async fn delete_view_handler(
     data: Data<RepeatedViewId>,
-    view_controller: Unit<Arc<ViewController>>,
-    trash_controller: Unit<Arc<TrashController>>,
+    view_controller: AppData<Arc<ViewController>>,
+    trash_controller: AppData<Arc<TrashController>>,
 ) -> Result<(), FlowyError> {
     let params: RepeatedViewId = data.into_inner();
     for view_id in &params.items {
@@ -75,18 +75,18 @@ pub(crate) async fn delete_view_handler(
     Ok(())
 }
 
-pub(crate) async fn open_document_handler(
+pub(crate) async fn open_view_handler(
     data: Data<ViewId>,
-    controller: Unit<Arc<ViewController>>,
-) -> DataResult<DocumentDelta, FlowyError> {
+    controller: AppData<Arc<ViewController>>,
+) -> DataResult<BlockDelta, FlowyError> {
     let view_id: ViewId = data.into_inner();
-    let doc = controller.open_document(&view_id.value).await?;
+    let doc = controller.open_view(&view_id.value).await?;
     data_result(doc)
 }
 
 pub(crate) async fn close_view_handler(
     data: Data<ViewId>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> Result<(), FlowyError> {
     let view_id: ViewId = data.into_inner();
     let _ = controller.close_view(&view_id.value).await?;
@@ -96,7 +96,7 @@ pub(crate) async fn close_view_handler(
 #[tracing::instrument(skip(data, controller), err)]
 pub(crate) async fn duplicate_view_handler(
     data: Data<ViewId>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> Result<(), FlowyError> {
     let view_id: ViewId = data.into_inner();
     let _ = controller.duplicate_view(&view_id.value).await?;
@@ -106,9 +106,9 @@ pub(crate) async fn duplicate_view_handler(
 #[tracing::instrument(skip(data, controller), err)]
 pub(crate) async fn export_handler(
     data: Data<ExportPayload>,
-    controller: Unit<Arc<ViewController>>,
+    controller: AppData<Arc<ViewController>>,
 ) -> DataResult<ExportData, FlowyError> {
     let params: ExportParams = data.into_inner().try_into()?;
-    let data = controller.export_doc(params).await?;
+    let data = controller.export_view(params).await?;
     data_result(data)
 }

+ 34 - 30
frontend/rust-lib/flowy-folder/src/services/web_socket.rs

@@ -10,7 +10,7 @@ use flowy_collaboration::{
 use flowy_error::FlowyError;
 use flowy_sync::*;
 use lib_infra::future::{BoxResultFuture, FutureResult};
-use lib_ot::core::{Delta, OperationTransformable, PlainAttributes, PlainDelta};
+use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta};
 use parking_lot::RwLock;
 use std::{sync::Arc, time::Duration};
 
@@ -21,45 +21,41 @@ pub(crate) async fn make_folder_ws_manager(
     web_socket: Arc<dyn RevisionWebSocket>,
     folder_pad: Arc<RwLock<FolderPad>>,
 ) -> Arc<RevisionWebSocketManager> {
-    let composite_sink_provider = Arc::new(CompositeWSSinkDataProvider::new(folder_id, rev_manager.clone()));
-    let resolve_target = Arc::new(FolderRevisionResolveTarget { folder_pad });
-    let resolver = RevisionConflictResolver::<PlainAttributes>::new(
+    let ws_data_provider = Arc::new(WSDataProvider::new(folder_id, Arc::new(rev_manager.clone())));
+    let resolver = Arc::new(FolderConflictResolver { folder_pad });
+    let conflict_controller = ConflictController::<PlainTextAttributes>::new(
         user_id,
-        resolve_target,
-        Arc::new(composite_sink_provider.clone()),
+        resolver,
+        Arc::new(ws_data_provider.clone()),
         rev_manager,
     );
-
-    let ws_stream_consumer = Arc::new(FolderWSStreamConsumerAdapter {
-        resolver: Arc::new(resolver),
-    });
-
-    let sink_provider = Arc::new(FolderWSSinkDataProviderAdapter(composite_sink_provider));
+    let ws_data_stream = Arc::new(FolderRevisionWSDataStream::new(conflict_controller));
+    let ws_data_sink = Arc::new(FolderWSDataSink(ws_data_provider));
     let ping_duration = Duration::from_millis(FOLDER_SYNC_INTERVAL_IN_MILLIS);
     Arc::new(RevisionWebSocketManager::new(
         "Folder",
         folder_id,
         web_socket,
-        sink_provider,
-        ws_stream_consumer,
+        ws_data_sink,
+        ws_data_stream,
         ping_duration,
     ))
 }
 
-pub(crate) struct FolderWSSinkDataProviderAdapter(Arc<CompositeWSSinkDataProvider>);
-impl RevisionWSSinkDataProvider for FolderWSSinkDataProviderAdapter {
+pub(crate) struct FolderWSDataSink(Arc<WSDataProvider>);
+impl RevisionWSDataIterator for FolderWSDataSink {
     fn next(&self) -> FutureResult<Option<ClientRevisionWSData>, FlowyError> {
         let sink_provider = self.0.clone();
         FutureResult::new(async move { sink_provider.next().await })
     }
 }
 
-struct FolderRevisionResolveTarget {
+struct FolderConflictResolver {
     folder_pad: Arc<RwLock<FolderPad>>,
 }
 
-impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
-    fn compose_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+impl ConflictResolver<PlainTextAttributes> for FolderConflictResolver {
+    fn compose_delta(&self, delta: PlainTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let md5 = folder_pad.write().compose_remote_delta(delta)?;
@@ -69,13 +65,13 @@ impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
 
     fn transform_delta(
         &self,
-        delta: Delta<PlainAttributes>,
-    ) -> BoxResultFuture<TransformDeltas<PlainAttributes>, FlowyError> {
+        delta: PlainTextDelta,
+    ) -> BoxResultFuture<TransformDeltas<PlainTextAttributes>, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let read_guard = folder_pad.read();
-            let mut server_prime: Option<PlainDelta> = None;
-            let client_prime: PlainDelta;
+            let mut server_prime: Option<PlainTextDelta> = None;
+            let client_prime: PlainTextDelta;
             if read_guard.is_empty() {
                 // Do nothing
                 client_prime = delta;
@@ -92,7 +88,7 @@ impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
         })
     }
 
-    fn reset_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+    fn reset_delta(&self, delta: PlainTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let md5 = folder_pad.write().reset_folder(delta)?;
@@ -101,18 +97,26 @@ impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
     }
 }
 
-struct FolderWSStreamConsumerAdapter {
-    resolver: Arc<RevisionConflictResolver<PlainAttributes>>,
+struct FolderRevisionWSDataStream {
+    conflict_controller: Arc<PlainTextConflictController>,
+}
+
+impl FolderRevisionWSDataStream {
+    pub fn new(conflict_controller: PlainTextConflictController) -> Self {
+        Self {
+            conflict_controller: Arc::new(conflict_controller),
+        }
+    }
 }
 
-impl RevisionWSSteamConsumer for FolderWSStreamConsumerAdapter {
+impl RevisionWSDataStream for FolderRevisionWSDataStream {
     fn receive_push_revision(&self, bytes: Bytes) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.receive_bytes(bytes).await })
     }
 
     fn receive_ack(&self, id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.ack_revision(id, ty).await })
     }
 
@@ -122,7 +126,7 @@ impl RevisionWSSteamConsumer for FolderWSStreamConsumerAdapter {
     }
 
     fn pull_revisions_in_range(&self, range: RevisionRange) -> BoxResultFuture<(), FlowyError> {
-        let resolver = self.resolver.clone();
+        let resolver = self.conflict_controller.clone();
         Box::pin(async move { resolver.send_revisions(range).await })
     }
 }

Some files were not shown because too many files changed in this diff