Jelajahi Sumber

[flutter]: config document delta data flow

appflowy 3 tahun lalu
induk
melakukan
538ab20c5f
24 mengubah file dengan 458 tambahan dan 235 penghapusan
  1. 42 25
      app_flowy/lib/workspace/application/doc/doc_bloc.dart
  2. 19 43
      app_flowy/lib/workspace/application/doc/doc_bloc.freezed.dart
  3. 34 20
      app_flowy/lib/workspace/application/view/view_bloc.dart
  4. 128 0
      app_flowy/lib/workspace/application/view/view_bloc.freezed.dart
  5. 0 49
      app_flowy/lib/workspace/domain/i_doc.dart
  6. 2 0
      app_flowy/lib/workspace/domain/i_view.dart
  7. 17 2
      app_flowy/lib/workspace/domain/view_edit.dart
  8. 5 0
      app_flowy/lib/workspace/infrastructure/i_view_impl.dart
  9. 5 0
      app_flowy/lib/workspace/infrastructure/repos/view_repo.dart
  10. 5 5
      app_flowy/lib/workspace/presentation/stack_page/doc/doc_page.dart
  11. 13 6
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/action.dart
  12. 3 0
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart
  13. 2 0
      app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart
  14. 1 1
      app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart
  15. 47 16
      app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart
  16. 8 4
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbenum.dart
  17. 6 4
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbjson.dart
  18. 10 0
      rust-lib/flowy-document/src/module.rs
  19. 11 5
      rust-lib/flowy-workspace/src/event.rs
  20. 10 0
      rust-lib/flowy-workspace/src/handlers/view_handler.rs
  21. 1 0
      rust-lib/flowy-workspace/src/module.rs
  22. 63 52
      rust-lib/flowy-workspace/src/protobuf/model/event.rs
  23. 5 3
      rust-lib/flowy-workspace/src/protobuf/proto/event.proto
  24. 21 0
      rust-lib/flowy-workspace/src/services/view_controller.rs

+ 42 - 25
app_flowy/lib/workspace/application/doc/doc_bloc.dart

@@ -1,30 +1,31 @@
+import 'dart:convert';
+
+import 'package:editor/flutter_quill.dart';
+import 'package:flowy_log/flowy_log.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/workspace/domain/i_doc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:dartz/dartz.dart';
-
+import 'dart:async';
 part 'doc_bloc.freezed.dart';
 
 class DocBloc extends Bloc<DocEvent, DocState> {
   final IDoc docManager;
+  late Document document;
+  late StreamSubscription _subscription;
 
   DocBloc({required this.docManager}) : super(DocState.initial());
 
   @override
   Stream<DocState> mapEventToState(DocEvent event) async* {
-    yield* event.map(
-      initial: _initial,
-    );
+    yield* event.map(initial: _initial);
   }
 
   @override
   Future<void> close() async {
+    await _subscription.cancel();
     docManager.closeDoc();
-
-    await state.doc.fold(() => null, (doc) async {
-      await doc.close();
-    });
     return super.close();
   }
 
@@ -32,17 +33,16 @@ class DocBloc extends Bloc<DocEvent, DocState> {
     final result = await docManager.readDoc();
     yield result.fold(
       (doc) {
-        final flowyDoc = FlowyDoc(doc: doc, iDocImpl: docManager);
-        return state.copyWith(
-          doc: some(flowyDoc),
-          loadState: DocLoadState.finish(left(flowyDoc)),
-        );
+        document = _decodeJsonToDocument(doc.data);
+        _subscription = document.changes.listen((event) {
+          final delta = event.item2;
+          final documentDelta = document.toDelta();
+          _composeDelta(delta, documentDelta);
+        });
+        return state.copyWith(loadState: DocLoadState.finish(left(unit)));
       },
       (err) {
-        return state.copyWith(
-          doc: none(),
-          loadState: DocLoadState.finish(right(err)),
-        );
+        return state.copyWith(loadState: DocLoadState.finish(right(err)));
       },
     );
   }
@@ -53,11 +53,26 @@ class DocBloc extends Bloc<DocEvent, DocState> {
   //   return document;
   // }
 
-  // Document _decodeJsonToDocument(String data) {
-  //   final json = jsonDecode(data);
-  //   final document = Document.fromJson(json);
-  //   return document;
-  // }
+  void _composeDelta(Delta composedDelta, Delta documentDelta) async {
+    final json = jsonEncode(composedDelta.toJson());
+    Log.debug("Send json: $json");
+    final result = await docManager.composeDelta(json: json);
+
+    result.fold((rustDoc) {
+      // final json = utf8.decode(doc.data);
+      final rustDelta = Delta.fromJson(jsonDecode(rustDoc.data));
+      if (documentDelta != rustDelta) {
+        Log.error("Receive : $rustDelta");
+        Log.error("Expected : $documentDelta");
+      }
+    }, (r) => null);
+  }
+
+  Document _decodeJsonToDocument(String data) {
+    final json = jsonDecode(data);
+    final document = Document.fromJson(json);
+    return document;
+  }
 }
 
 @freezed
@@ -67,13 +82,15 @@ class DocEvent with _$DocEvent {
 
 @freezed
 class DocState with _$DocState {
-  const factory DocState({required Option<FlowyDoc> doc, required DocLoadState loadState}) = _DocState;
+  const factory DocState({
+    required DocLoadState loadState,
+  }) = _DocState;
 
-  factory DocState.initial() => DocState(doc: none(), loadState: const _Loading());
+  factory DocState.initial() => const DocState(loadState: _Loading());
 }
 
 @freezed
 class DocLoadState with _$DocLoadState {
   const factory DocLoadState.loading() = _Loading;
-  const factory DocLoadState.finish(Either<FlowyDoc, WorkspaceError> successOrFail) = _Finish;
+  const factory DocLoadState.finish(Either<Unit, WorkspaceError> successOrFail) = _Finish;
 }

+ 19 - 43
app_flowy/lib/workspace/application/doc/doc_bloc.freezed.dart

@@ -148,10 +148,8 @@ abstract class Initial implements DocEvent {
 class _$DocStateTearOff {
   const _$DocStateTearOff();
 
-  _DocState call(
-      {required Option<FlowyDoc> doc, required DocLoadState loadState}) {
+  _DocState call({required DocLoadState loadState}) {
     return _DocState(
-      doc: doc,
       loadState: loadState,
     );
   }
@@ -162,7 +160,6 @@ const $DocState = _$DocStateTearOff();
 
 /// @nodoc
 mixin _$DocState {
-  Option<FlowyDoc> get doc => throw _privateConstructorUsedError;
   DocLoadState get loadState => throw _privateConstructorUsedError;
 
   @JsonKey(ignore: true)
@@ -174,7 +171,7 @@ mixin _$DocState {
 abstract class $DocStateCopyWith<$Res> {
   factory $DocStateCopyWith(DocState value, $Res Function(DocState) then) =
       _$DocStateCopyWithImpl<$Res>;
-  $Res call({Option<FlowyDoc> doc, DocLoadState loadState});
+  $Res call({DocLoadState loadState});
 
   $DocLoadStateCopyWith<$Res> get loadState;
 }
@@ -189,14 +186,9 @@ class _$DocStateCopyWithImpl<$Res> implements $DocStateCopyWith<$Res> {
 
   @override
   $Res call({
-    Object? doc = freezed,
     Object? loadState = freezed,
   }) {
     return _then(_value.copyWith(
-      doc: doc == freezed
-          ? _value.doc
-          : doc // ignore: cast_nullable_to_non_nullable
-              as Option<FlowyDoc>,
       loadState: loadState == freezed
           ? _value.loadState
           : loadState // ignore: cast_nullable_to_non_nullable
@@ -217,7 +209,7 @@ abstract class _$DocStateCopyWith<$Res> implements $DocStateCopyWith<$Res> {
   factory _$DocStateCopyWith(_DocState value, $Res Function(_DocState) then) =
       __$DocStateCopyWithImpl<$Res>;
   @override
-  $Res call({Option<FlowyDoc> doc, DocLoadState loadState});
+  $Res call({DocLoadState loadState});
 
   @override
   $DocLoadStateCopyWith<$Res> get loadState;
@@ -234,14 +226,9 @@ class __$DocStateCopyWithImpl<$Res> extends _$DocStateCopyWithImpl<$Res>
 
   @override
   $Res call({
-    Object? doc = freezed,
     Object? loadState = freezed,
   }) {
     return _then(_DocState(
-      doc: doc == freezed
-          ? _value.doc
-          : doc // ignore: cast_nullable_to_non_nullable
-              as Option<FlowyDoc>,
       loadState: loadState == freezed
           ? _value.loadState
           : loadState // ignore: cast_nullable_to_non_nullable
@@ -253,24 +240,20 @@ class __$DocStateCopyWithImpl<$Res> extends _$DocStateCopyWithImpl<$Res>
 /// @nodoc
 
 class _$_DocState implements _DocState {
-  const _$_DocState({required this.doc, required this.loadState});
+  const _$_DocState({required this.loadState});
 
-  @override
-  final Option<FlowyDoc> doc;
   @override
   final DocLoadState loadState;
 
   @override
   String toString() {
-    return 'DocState(doc: $doc, loadState: $loadState)';
+    return 'DocState(loadState: $loadState)';
   }
 
   @override
   bool operator ==(dynamic other) {
     return identical(this, other) ||
         (other is _DocState &&
-            (identical(other.doc, doc) ||
-                const DeepCollectionEquality().equals(other.doc, doc)) &&
             (identical(other.loadState, loadState) ||
                 const DeepCollectionEquality()
                     .equals(other.loadState, loadState)));
@@ -278,9 +261,7 @@ class _$_DocState implements _DocState {
 
   @override
   int get hashCode =>
-      runtimeType.hashCode ^
-      const DeepCollectionEquality().hash(doc) ^
-      const DeepCollectionEquality().hash(loadState);
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(loadState);
 
   @JsonKey(ignore: true)
   @override
@@ -289,12 +270,8 @@ class _$_DocState implements _DocState {
 }
 
 abstract class _DocState implements DocState {
-  const factory _DocState(
-      {required Option<FlowyDoc> doc,
-      required DocLoadState loadState}) = _$_DocState;
+  const factory _DocState({required DocLoadState loadState}) = _$_DocState;
 
-  @override
-  Option<FlowyDoc> get doc => throw _privateConstructorUsedError;
   @override
   DocLoadState get loadState => throw _privateConstructorUsedError;
   @override
@@ -311,7 +288,7 @@ class _$DocLoadStateTearOff {
     return const _Loading();
   }
 
-  _Finish finish(Either<FlowyDoc, WorkspaceError> successOrFail) {
+  _Finish finish(Either<Unit, WorkspaceError> successOrFail) {
     return _Finish(
       successOrFail,
     );
@@ -326,14 +303,14 @@ mixin _$DocLoadState {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() loading,
-    required TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)
+    required TResult Function(Either<Unit, WorkspaceError> successOrFail)
         finish,
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? loading,
-    TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)? finish,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
     required TResult orElse(),
   }) =>
       throw _privateConstructorUsedError;
@@ -406,7 +383,7 @@ class _$_Loading implements _Loading {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() loading,
-    required TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)
+    required TResult Function(Either<Unit, WorkspaceError> successOrFail)
         finish,
   }) {
     return loading();
@@ -416,7 +393,7 @@ class _$_Loading implements _Loading {
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? loading,
-    TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)? finish,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
     required TResult orElse(),
   }) {
     if (loading != null) {
@@ -456,7 +433,7 @@ abstract class _Loading implements DocLoadState {
 abstract class _$FinishCopyWith<$Res> {
   factory _$FinishCopyWith(_Finish value, $Res Function(_Finish) then) =
       __$FinishCopyWithImpl<$Res>;
-  $Res call({Either<FlowyDoc, WorkspaceError> successOrFail});
+  $Res call({Either<Unit, WorkspaceError> successOrFail});
 }
 
 /// @nodoc
@@ -476,7 +453,7 @@ class __$FinishCopyWithImpl<$Res> extends _$DocLoadStateCopyWithImpl<$Res>
       successOrFail == freezed
           ? _value.successOrFail
           : successOrFail // ignore: cast_nullable_to_non_nullable
-              as Either<FlowyDoc, WorkspaceError>,
+              as Either<Unit, WorkspaceError>,
     ));
   }
 }
@@ -487,7 +464,7 @@ class _$_Finish implements _Finish {
   const _$_Finish(this.successOrFail);
 
   @override
-  final Either<FlowyDoc, WorkspaceError> successOrFail;
+  final Either<Unit, WorkspaceError> successOrFail;
 
   @override
   String toString() {
@@ -516,7 +493,7 @@ class _$_Finish implements _Finish {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() loading,
-    required TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)
+    required TResult Function(Either<Unit, WorkspaceError> successOrFail)
         finish,
   }) {
     return finish(successOrFail);
@@ -526,7 +503,7 @@ class _$_Finish implements _Finish {
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? loading,
-    TResult Function(Either<FlowyDoc, WorkspaceError> successOrFail)? finish,
+    TResult Function(Either<Unit, WorkspaceError> successOrFail)? finish,
     required TResult orElse(),
   }) {
     if (finish != null) {
@@ -559,10 +536,9 @@ class _$_Finish implements _Finish {
 }
 
 abstract class _Finish implements DocLoadState {
-  const factory _Finish(Either<FlowyDoc, WorkspaceError> successOrFail) =
-      _$_Finish;
+  const factory _Finish(Either<Unit, WorkspaceError> successOrFail) = _$_Finish;
 
-  Either<FlowyDoc, WorkspaceError> get successOrFail =>
+  Either<Unit, WorkspaceError> get successOrFail =>
       throw _privateConstructorUsedError;
   @JsonKey(ignore: true)
   _$FinishCopyWith<_Finish> get copyWith => throw _privateConstructorUsedError;

+ 34 - 20
app_flowy/lib/workspace/application/view/view_bloc.dart

@@ -18,26 +18,39 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
 
   @override
   Stream<ViewState> mapEventToState(ViewEvent event) async* {
-    yield* event.map(initial: (e) async* {
-      listener.start(updatedCallback: (result) => add(ViewEvent.viewDidUpdate(result)));
-      yield state;
-    }, setIsEditing: (e) async* {
-      yield state.copyWith(isEditing: e.isEditing);
-    }, viewDidUpdate: (e) async* {
-      yield* _handleViewDidUpdate(e.result);
-    }, rename: (e) async* {
-      final result = await viewManager.rename(e.newName);
-      yield result.fold(
-        (l) => state.copyWith(successOrFailure: left(unit)),
-        (error) => state.copyWith(successOrFailure: right(error)),
-      );
-    }, delete: (e) async* {
-      final result = await viewManager.delete();
-      yield result.fold(
-        (l) => state.copyWith(successOrFailure: left(unit)),
-        (error) => state.copyWith(successOrFailure: right(error)),
-      );
-    });
+    yield* event.map(
+      initial: (e) async* {
+        listener.start(updatedCallback: (result) => add(ViewEvent.viewDidUpdate(result)));
+        yield state;
+      },
+      setIsEditing: (e) async* {
+        yield state.copyWith(isEditing: e.isEditing);
+      },
+      viewDidUpdate: (e) async* {
+        yield* _handleViewDidUpdate(e.result);
+      },
+      rename: (e) async* {
+        final result = await viewManager.rename(e.newName);
+        yield result.fold(
+          (l) => state.copyWith(successOrFailure: left(unit)),
+          (error) => state.copyWith(successOrFailure: right(error)),
+        );
+      },
+      delete: (e) async* {
+        final result = await viewManager.delete();
+        yield result.fold(
+          (l) => state.copyWith(successOrFailure: left(unit)),
+          (error) => state.copyWith(successOrFailure: right(error)),
+        );
+      },
+      duplicate: (e) async* {
+        final result = await viewManager.duplicate();
+        yield result.fold(
+          (l) => state.copyWith(successOrFailure: left(unit)),
+          (error) => state.copyWith(successOrFailure: right(error)),
+        );
+      },
+    );
   }
 
   Stream<ViewState> _handleViewDidUpdate(Either<View, WorkspaceError> result) async* {
@@ -60,6 +73,7 @@ class ViewEvent with _$ViewEvent {
   const factory ViewEvent.setIsEditing(bool isEditing) = SetEditing;
   const factory ViewEvent.rename(String newName) = Rename;
   const factory ViewEvent.delete() = Delete;
+  const factory ViewEvent.duplicate() = Duplicate;
   const factory ViewEvent.viewDidUpdate(Either<View, WorkspaceError> result) = ViewDidUpdate;
 }
 

+ 128 - 0
app_flowy/lib/workspace/application/view/view_bloc.freezed.dart

@@ -36,6 +36,10 @@ class _$ViewEventTearOff {
     return const Delete();
   }
 
+  Duplicate duplicate() {
+    return const Duplicate();
+  }
+
   ViewDidUpdate viewDidUpdate(Either<View, WorkspaceError> result) {
     return ViewDidUpdate(
       result,
@@ -54,6 +58,7 @@ mixin _$ViewEvent {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) =>
@@ -64,6 +69,7 @@ mixin _$ViewEvent {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) =>
@@ -74,6 +80,7 @@ mixin _$ViewEvent {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) =>
       throw _privateConstructorUsedError;
@@ -83,6 +90,7 @@ mixin _$ViewEvent {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) =>
@@ -145,6 +153,7 @@ class _$Initial implements Initial {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) {
@@ -158,6 +167,7 @@ class _$Initial implements Initial {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -174,6 +184,7 @@ class _$Initial implements Initial {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) {
     return initial(this);
@@ -186,6 +197,7 @@ class _$Initial implements Initial {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -268,6 +280,7 @@ class _$SetEditing implements SetEditing {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) {
@@ -281,6 +294,7 @@ class _$SetEditing implements SetEditing {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -297,6 +311,7 @@ class _$SetEditing implements SetEditing {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) {
     return setIsEditing(this);
@@ -309,6 +324,7 @@ class _$SetEditing implements SetEditing {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -394,6 +410,7 @@ class _$Rename implements Rename {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) {
@@ -407,6 +424,7 @@ class _$Rename implements Rename {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -423,6 +441,7 @@ class _$Rename implements Rename {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) {
     return rename(this);
@@ -435,6 +454,7 @@ class _$Rename implements Rename {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -494,6 +514,7 @@ class _$Delete implements Delete {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) {
@@ -507,6 +528,7 @@ class _$Delete implements Delete {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -523,6 +545,7 @@ class _$Delete implements Delete {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) {
     return delete(this);
@@ -535,6 +558,7 @@ class _$Delete implements Delete {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -549,6 +573,106 @@ abstract class Delete implements ViewEvent {
   const factory Delete() = _$Delete;
 }
 
+/// @nodoc
+abstract class $DuplicateCopyWith<$Res> {
+  factory $DuplicateCopyWith(Duplicate value, $Res Function(Duplicate) then) =
+      _$DuplicateCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$DuplicateCopyWithImpl<$Res> extends _$ViewEventCopyWithImpl<$Res>
+    implements $DuplicateCopyWith<$Res> {
+  _$DuplicateCopyWithImpl(Duplicate _value, $Res Function(Duplicate) _then)
+      : super(_value, (v) => _then(v as Duplicate));
+
+  @override
+  Duplicate get _value => super._value as Duplicate;
+}
+
+/// @nodoc
+
+class _$Duplicate implements Duplicate {
+  const _$Duplicate();
+
+  @override
+  String toString() {
+    return 'ViewEvent.duplicate()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is Duplicate);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function(bool isEditing) setIsEditing,
+    required TResult Function(String newName) rename,
+    required TResult Function() delete,
+    required TResult Function() duplicate,
+    required TResult Function(Either<View, WorkspaceError> result)
+        viewDidUpdate,
+  }) {
+    return duplicate();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(bool isEditing)? setIsEditing,
+    TResult Function(String newName)? rename,
+    TResult Function()? delete,
+    TResult Function()? duplicate,
+    TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
+    required TResult orElse(),
+  }) {
+    if (duplicate != null) {
+      return duplicate();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(Initial value) initial,
+    required TResult Function(SetEditing value) setIsEditing,
+    required TResult Function(Rename value) rename,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
+    required TResult Function(ViewDidUpdate value) viewDidUpdate,
+  }) {
+    return duplicate(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(SetEditing value)? setIsEditing,
+    TResult Function(Rename value)? rename,
+    TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
+    TResult Function(ViewDidUpdate value)? viewDidUpdate,
+    required TResult orElse(),
+  }) {
+    if (duplicate != null) {
+      return duplicate(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class Duplicate implements ViewEvent {
+  const factory Duplicate() = _$Duplicate;
+}
+
 /// @nodoc
 abstract class $ViewDidUpdateCopyWith<$Res> {
   factory $ViewDidUpdateCopyWith(
@@ -617,6 +741,7 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     required TResult Function(bool isEditing) setIsEditing,
     required TResult Function(String newName) rename,
     required TResult Function() delete,
+    required TResult Function() duplicate,
     required TResult Function(Either<View, WorkspaceError> result)
         viewDidUpdate,
   }) {
@@ -630,6 +755,7 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     TResult Function(bool isEditing)? setIsEditing,
     TResult Function(String newName)? rename,
     TResult Function()? delete,
+    TResult Function()? duplicate,
     TResult Function(Either<View, WorkspaceError> result)? viewDidUpdate,
     required TResult orElse(),
   }) {
@@ -646,6 +772,7 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     required TResult Function(SetEditing value) setIsEditing,
     required TResult Function(Rename value) rename,
     required TResult Function(Delete value) delete,
+    required TResult Function(Duplicate value) duplicate,
     required TResult Function(ViewDidUpdate value) viewDidUpdate,
   }) {
     return viewDidUpdate(this);
@@ -658,6 +785,7 @@ class _$ViewDidUpdate implements ViewDidUpdate {
     TResult Function(SetEditing value)? setIsEditing,
     TResult Function(Rename value)? rename,
     TResult Function(Delete value)? delete,
+    TResult Function(Duplicate value)? duplicate,
     TResult Function(ViewDidUpdate value)? viewDidUpdate,
     required TResult orElse(),
   }) {

+ 0 - 49
app_flowy/lib/workspace/domain/i_doc.dart

@@ -1,57 +1,8 @@
-import 'dart:convert';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
-// ignore: implementation_imports
-import 'package:editor/flutter_quill.dart';
-// import 'package:flowy_editor/flowy_editor.dart';
-import 'package:flowy_log/flowy_log.dart';
 import 'package:flowy_sdk/protobuf/flowy-document/doc.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 
-class FlowyDoc {
-  final DocDelta doc;
-  final IDoc iDocImpl;
-  late Document document;
-  late StreamSubscription _subscription;
-
-  FlowyDoc({required this.doc, required this.iDocImpl}) {
-    document = _decodeJsonToDocument(doc.data);
-
-    _subscription = document.changes.listen((event) {
-      final delta = event.item2;
-      final documentDelta = document.toDelta();
-      _composeDelta(delta, documentDelta);
-    });
-  }
-
-  String get id => doc.docId;
-
-  Future<void> close() async {
-    await _subscription.cancel();
-  }
-
-  void _composeDelta(Delta composedDelta, Delta documentDelta) async {
-    final json = jsonEncode(composedDelta.toJson());
-    Log.debug("Send json: $json");
-    final result = await iDocImpl.composeDelta(json: json);
-
-    result.fold((rustDoc) {
-      // final json = utf8.decode(doc.data);
-      final rustDelta = Delta.fromJson(jsonDecode(rustDoc.data));
-      if (documentDelta != rustDelta) {
-        Log.error("Receive : $rustDelta");
-        Log.error("Expected : $documentDelta");
-      }
-    }, (r) => null);
-  }
-
-  Document _decodeJsonToDocument(String data) {
-    final json = jsonDecode(data);
-    final document = Document.fromJson(json);
-    return document;
-  }
-}
-
 abstract class IDoc {
   Future<Either<DocDelta, WorkspaceError>> readDoc();
   Future<Either<DocDelta, WorkspaceError>> composeDelta({required String json});

+ 2 - 0
app_flowy/lib/workspace/domain/i_view.dart

@@ -10,6 +10,8 @@ abstract class IView {
   Future<Either<Unit, WorkspaceError>> delete();
 
   Future<Either<View, WorkspaceError>> rename(String newName);
+
+  Future<Either<Unit, WorkspaceError>> duplicate();
 }
 
 abstract class IViewListener {

+ 17 - 2
app_flowy/lib/workspace/domain/view_edit.dart

@@ -1,6 +1,10 @@
+import 'package:flowy_infra/image.dart';
+import 'package:flutter/material.dart';
+
 enum ViewAction {
   rename,
   delete,
+  duplicate,
 }
 
 extension ViewActionExtension on ViewAction {
@@ -10,8 +14,19 @@ extension ViewActionExtension on ViewAction {
         return 'rename';
       case ViewAction.delete:
         return 'delete';
-      default:
-        return '';
+      case ViewAction.duplicate:
+        return 'duplicate';
+    }
+  }
+
+  Widget get icon {
+    switch (this) {
+      case ViewAction.rename:
+        return svg('editor/edit');
+      case ViewAction.delete:
+        return svg('editor/delete');
+      case ViewAction.duplicate:
+        return svg('editor/copy');
     }
   }
 }

+ 5 - 0
app_flowy/lib/workspace/infrastructure/i_view_impl.dart

@@ -26,6 +26,11 @@ class IViewImpl extends IView {
   Future<Either<View, WorkspaceError>> rename(String newName) {
     return repo.updateView(name: newName);
   }
+
+  @override
+  Future<Either<Unit, WorkspaceError>> duplicate() {
+    return repo.duplicate();
+  }
 }
 
 class IViewListenerImpl extends IViewListener {

+ 5 - 0
app_flowy/lib/workspace/infrastructure/repos/view_repo.dart

@@ -43,6 +43,11 @@ class ViewRepository {
     final request = QueryViewRequest.create()..viewIds.add(view.id);
     return WorkspaceEventDeleteView(request).send();
   }
+
+  Future<Either<Unit, WorkspaceError>> duplicate() {
+    final request = QueryViewRequest.create()..viewIds.add(view.id);
+    return WorkspaceEventDuplicateView(request).send();
+  }
 }
 
 class ViewListenerRepository {

+ 5 - 5
app_flowy/lib/workspace/presentation/stack_page/doc/doc_page.dart

@@ -1,13 +1,13 @@
 import 'dart:io';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/doc/doc_bloc.dart';
-import 'package:app_flowy/workspace/domain/i_doc.dart';
 import 'package:editor/flutter_quill.dart';
 import 'package:flowy_infra_ui/style_widget/progress_indicator.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:styled_widget/styled_widget.dart';
 
 class DocPage extends StatefulWidget {
   final View view;
@@ -38,7 +38,7 @@ class _DocPageState extends State<DocPage> {
         return state.loadState.map(
           loading: (_) => const FlowyProgressIndicator(),
           finish: (result) => result.successOrFail.fold(
-            (doc) => _renderDoc(context, doc),
+            (_) => _renderDoc(context),
             (err) => FlowyErrorPage(err.toString()),
           ),
         );
@@ -52,9 +52,9 @@ class _DocPageState extends State<DocPage> {
     super.dispose();
   }
 
-  Widget _renderDoc(BuildContext context, FlowyDoc doc) {
+  Widget _renderDoc(BuildContext context) {
     QuillController controller = QuillController(
-      document: doc.document,
+      document: context.read<DocBloc>().document,
       selection: const TextSelection.collapsed(offset: 0),
     );
     return Column(
@@ -63,7 +63,7 @@ class _DocPageState extends State<DocPage> {
         _renderEditor(controller),
         _renderToolbar(controller),
       ],
-    );
+    ).padding(horizontal: 80, vertical: 48);
   }
 
   Widget _renderEditor(QuillController controller) {

+ 13 - 6
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/action.dart

@@ -3,6 +3,7 @@ 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/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
@@ -33,8 +34,8 @@ class ViewActionList implements FlowyOverlayDelegate {
       itemBuilder: (context, index) => items[index],
       anchorContext: anchorContext,
       anchorDirection: AnchorDirection.bottomRight,
-      maxWidth: 120,
-      maxHeight: 80,
+      maxWidth: 162,
+      maxHeight: ViewAction.values.length * 32,
       delegate: this,
     );
   }
@@ -63,11 +64,17 @@ class ActionItem extends StatelessWidget {
       builder: (context, onHover) {
         return GestureDetector(
           onTap: () => onSelected(action),
-          child: FlowyText.medium(
-            action.name,
-            fontSize: 12,
+          child: Row(
+            children: [
+              action.icon,
+              const HSpace(10),
+              FlowyText.medium(
+                action.name,
+                fontSize: 12,
+              ),
+            ],
           ).padding(
-            horizontal: 10,
+            horizontal: 6,
             vertical: 6,
           ),
         );

+ 3 - 0
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart

@@ -103,6 +103,9 @@ class ViewSectionItem extends StatelessWidget {
         case ViewAction.delete:
           context.read<ViewBloc>().add(const ViewEvent.delete());
           break;
+        case ViewAction.duplicate:
+          context.read<ViewBloc>().add(const ViewEvent.duplicate());
+          break;
       }
     });
   }

+ 2 - 0
app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart

@@ -163,6 +163,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
     FlowyOverlayDelegate? delegate,
     OverlapBehaviour? overlapBehaviour,
     FlowyOverlayStyle? style,
+    Offset? anchorPosition,
   }) {
     this.style = style ?? FlowyOverlayStyle();
 
@@ -174,6 +175,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
       anchorContext: anchorContext,
       anchorDirection: anchorDirection,
       overlapBehaviour: overlapBehaviour,
+      anchorPosition: anchorPosition,
     );
   }
 

+ 1 - 1
app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart

@@ -108,7 +108,7 @@ class StyledDialogRoute<T> extends PopupRoute<T> {
   StyledDialogRoute({
     required RoutePageBuilder pageBuilder,
     required this.barrier,
-    Duration transitionDuration = const Duration(milliseconds: 360),
+    Duration transitionDuration = const Duration(milliseconds: 300),
     RouteTransitionsBuilder? transitionBuilder,
     RouteSettings? settings,
   })  : _pageBuilder = pageBuilder,

+ 47 - 16
app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart

@@ -237,52 +237,66 @@ class WorkspaceEventDeleteView {
     }
 }
 
-class WorkspaceEventOpenView {
+class WorkspaceEventDuplicateView {
      QueryViewRequest request;
-     WorkspaceEventOpenView(this.request);
+     WorkspaceEventDuplicateView(this.request);
 
-    Future<Either<DocDelta, WorkspaceError>> send() {
+    Future<Either<Unit, WorkspaceError>> send() {
     final request = FFIRequest.create()
-          ..event = WorkspaceEvent.OpenView.toString()
+          ..event = WorkspaceEvent.DuplicateView.toString()
           ..payload = requestToBytes(this.request);
 
     return Dispatch.asyncRequest(request)
         .then((bytesResult) => bytesResult.fold(
-           (okBytes) => left(DocDelta.fromBuffer(okBytes)),
+           (bytes) => left(unit),
            (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
         ));
     }
 }
 
-class WorkspaceEventCloseView {
-     QueryViewRequest request;
-     WorkspaceEventCloseView(this.request);
+class WorkspaceEventCopyLink {
+    WorkspaceEventCopyLink();
 
     Future<Either<Unit, WorkspaceError>> send() {
+     final request = FFIRequest.create()
+        ..event = WorkspaceEvent.CopyLink.toString();
+
+     return Dispatch.asyncRequest(request).then((bytesResult) => bytesResult.fold(
+        (bytes) => left(unit),
+        (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
+      ));
+    }
+}
+
+class WorkspaceEventOpenView {
+     QueryViewRequest request;
+     WorkspaceEventOpenView(this.request);
+
+    Future<Either<DocDelta, WorkspaceError>> send() {
     final request = FFIRequest.create()
-          ..event = WorkspaceEvent.CloseView.toString()
+          ..event = WorkspaceEvent.OpenView.toString()
           ..payload = requestToBytes(this.request);
 
     return Dispatch.asyncRequest(request)
         .then((bytesResult) => bytesResult.fold(
-           (bytes) => left(unit),
+           (okBytes) => left(DocDelta.fromBuffer(okBytes)),
            (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
         ));
     }
 }
 
-class WorkspaceEventApplyDocDelta {
-     DocDelta request;
-     WorkspaceEventApplyDocDelta(this.request);
+class WorkspaceEventCloseView {
+     QueryViewRequest request;
+     WorkspaceEventCloseView(this.request);
 
-    Future<Either<DocDelta, WorkspaceError>> send() {
+    Future<Either<Unit, WorkspaceError>> send() {
     final request = FFIRequest.create()
-          ..event = WorkspaceEvent.ApplyDocDelta.toString()
+          ..event = WorkspaceEvent.CloseView.toString()
           ..payload = requestToBytes(this.request);
 
     return Dispatch.asyncRequest(request)
         .then((bytesResult) => bytesResult.fold(
-           (okBytes) => left(DocDelta.fromBuffer(okBytes)),
+           (bytes) => left(unit),
            (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
         ));
     }
@@ -364,6 +378,23 @@ class WorkspaceEventDeleteAll {
     }
 }
 
+class WorkspaceEventApplyDocDelta {
+     DocDelta request;
+     WorkspaceEventApplyDocDelta(this.request);
+
+    Future<Either<DocDelta, WorkspaceError>> send() {
+    final request = FFIRequest.create()
+          ..event = WorkspaceEvent.ApplyDocDelta.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (okBytes) => left(DocDelta.fromBuffer(okBytes)),
+           (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class WorkspaceEventInitWorkspace {
     WorkspaceEventInitWorkspace();
 

+ 8 - 4
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbenum.dart

@@ -24,14 +24,16 @@ class WorkspaceEvent extends $pb.ProtobufEnum {
   static const WorkspaceEvent ReadView = WorkspaceEvent._(202, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadView');
   static const WorkspaceEvent UpdateView = WorkspaceEvent._(203, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateView');
   static const WorkspaceEvent DeleteView = WorkspaceEvent._(204, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteView');
-  static const WorkspaceEvent OpenView = WorkspaceEvent._(205, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'OpenView');
-  static const WorkspaceEvent CloseView = WorkspaceEvent._(206, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CloseView');
-  static const WorkspaceEvent ApplyDocDelta = WorkspaceEvent._(207, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDocDelta');
+  static const WorkspaceEvent DuplicateView = WorkspaceEvent._(205, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DuplicateView');
+  static const WorkspaceEvent CopyLink = WorkspaceEvent._(206, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CopyLink');
+  static const WorkspaceEvent OpenView = WorkspaceEvent._(207, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'OpenView');
+  static const WorkspaceEvent CloseView = WorkspaceEvent._(208, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CloseView');
   static const WorkspaceEvent ReadTrash = WorkspaceEvent._(300, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadTrash');
   static const WorkspaceEvent PutbackTrash = WorkspaceEvent._(301, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PutbackTrash');
   static const WorkspaceEvent DeleteTrash = WorkspaceEvent._(302, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteTrash');
   static const WorkspaceEvent RestoreAll = WorkspaceEvent._(303, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RestoreAll');
   static const WorkspaceEvent DeleteAll = WorkspaceEvent._(304, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteAll');
+  static const WorkspaceEvent ApplyDocDelta = WorkspaceEvent._(400, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDocDelta');
   static const WorkspaceEvent InitWorkspace = WorkspaceEvent._(1000, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'InitWorkspace');
 
   static const $core.List<WorkspaceEvent> values = <WorkspaceEvent> [
@@ -49,14 +51,16 @@ class WorkspaceEvent extends $pb.ProtobufEnum {
     ReadView,
     UpdateView,
     DeleteView,
+    DuplicateView,
+    CopyLink,
     OpenView,
     CloseView,
-    ApplyDocDelta,
     ReadTrash,
     PutbackTrash,
     DeleteTrash,
     RestoreAll,
     DeleteAll,
+    ApplyDocDelta,
     InitWorkspace,
   ];
 

+ 6 - 4
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbjson.dart

@@ -26,17 +26,19 @@ const WorkspaceEvent$json = const {
     const {'1': 'ReadView', '2': 202},
     const {'1': 'UpdateView', '2': 203},
     const {'1': 'DeleteView', '2': 204},
-    const {'1': 'OpenView', '2': 205},
-    const {'1': 'CloseView', '2': 206},
-    const {'1': 'ApplyDocDelta', '2': 207},
+    const {'1': 'DuplicateView', '2': 205},
+    const {'1': 'CopyLink', '2': 206},
+    const {'1': 'OpenView', '2': 207},
+    const {'1': 'CloseView', '2': 208},
     const {'1': 'ReadTrash', '2': 300},
     const {'1': 'PutbackTrash', '2': 301},
     const {'1': 'DeleteTrash', '2': 302},
     const {'1': 'RestoreAll', '2': 303},
     const {'1': 'DeleteAll', '2': 304},
+    const {'1': 'ApplyDocDelta', '2': 400},
     const {'1': 'InitWorkspace', '2': 1000},
   ],
 };
 
 /// Descriptor for `WorkspaceEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List workspaceEventDescriptor = $convert.base64Decode('Cg5Xb3Jrc3BhY2VFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSFQoRUmVhZFdvcmtzcGFjZUFwcHMQBRINCglDcmVhdGVBcHAQZRINCglEZWxldGVBcHAQZhILCgdSZWFkQXBwEGcSDQoJVXBkYXRlQXBwEGgSDwoKQ3JlYXRlVmlldxDJARINCghSZWFkVmlldxDKARIPCgpVcGRhdGVWaWV3EMsBEg8KCkRlbGV0ZVZpZXcQzAESDQoIT3BlblZpZXcQzQESDgoJQ2xvc2VWaWV3EM4BEhIKDUFwcGx5RG9jRGVsdGEQzwESDgoJUmVhZFRyYXNoEKwCEhEKDFB1dGJhY2tUcmFzaBCtAhIQCgtEZWxldGVUcmFzaBCuAhIPCgpSZXN0b3JlQWxsEK8CEg4KCURlbGV0ZUFsbBCwAhISCg1Jbml0V29ya3NwYWNlEOgH');
+final $typed_data.Uint8List workspaceEventDescriptor = $convert.base64Decode('Cg5Xb3Jrc3BhY2VFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSFQoRUmVhZFdvcmtzcGFjZUFwcHMQBRINCglDcmVhdGVBcHAQZRINCglEZWxldGVBcHAQZhILCgdSZWFkQXBwEGcSDQoJVXBkYXRlQXBwEGgSDwoKQ3JlYXRlVmlldxDJARINCghSZWFkVmlldxDKARIPCgpVcGRhdGVWaWV3EMsBEg8KCkRlbGV0ZVZpZXcQzAESEgoNRHVwbGljYXRlVmlldxDNARINCghDb3B5TGluaxDOARINCghPcGVuVmlldxDPARIOCglDbG9zZVZpZXcQ0AESDgoJUmVhZFRyYXNoEKwCEhEKDFB1dGJhY2tUcmFzaBCtAhIQCgtEZWxldGVUcmFzaBCuAhIPCgpSZXN0b3JlQWxsEK8CEg4KCURlbGV0ZUFsbBCwAhISCg1BcHBseURvY0RlbHRhEJADEhIKDUluaXRXb3Jrc3BhY2UQ6Ac=');

+ 10 - 0
rust-lib/flowy-document/src/module.rs

@@ -52,6 +52,16 @@ impl FlowyDocument {
         Ok(())
     }
 
+    pub async fn read_document_data(
+        &self,
+        params: DocIdentifier,
+        pool: Arc<ConnectionPool>,
+    ) -> Result<DocDelta, DocError> {
+        let edit_context = self.doc_ctrl.open(params, pool).await?;
+        let delta = edit_context.delta().await?;
+        Ok(delta)
+    }
+
     pub async fn apply_doc_delta(&self, params: DocDelta) -> Result<DocDelta, DocError> {
         // workaround: compare the rust's delta with flutter's delta. Will be removed
         // very soon

+ 11 - 5
rust-lib/flowy-workspace/src/event.rs

@@ -46,14 +46,17 @@ pub enum WorkspaceEvent {
     #[event(input = "QueryViewRequest")]
     DeleteView        = 204,
 
+    #[event(input = "QueryViewRequest")]
+    DuplicateView     = 205,
+
+    #[event()]
+    CopyLink          = 206,
+
     #[event(input = "QueryViewRequest", output = "DocDelta")]
-    OpenView          = 205,
+    OpenView          = 207,
 
     #[event(input = "QueryViewRequest")]
-    CloseView         = 206,
-
-    #[event(input = "DocDelta", output = "DocDelta")]
-    ApplyDocDelta     = 207,
+    CloseView         = 208,
 
     #[event(output = "RepeatedTrash")]
     ReadTrash         = 300,
@@ -70,6 +73,9 @@ pub enum WorkspaceEvent {
     #[event()]
     DeleteAll         = 304,
 
+    #[event(input = "DocDelta", output = "DocDelta")]
+    ApplyDocDelta     = 400,
+
     #[event()]
     InitWorkspace     = 1000,
 }

+ 10 - 0
rust-lib/flowy-workspace/src/handlers/view_handler.rs

@@ -102,3 +102,13 @@ pub(crate) async fn close_view_handler(
     let _ = controller.close_view(params.into()).await?;
     Ok(())
 }
+
+#[tracing::instrument(skip(data, controller), err)]
+pub(crate) async fn duplicate_view_handler(
+    data: Data<QueryViewRequest>,
+    controller: Unit<Arc<ViewController>>,
+) -> Result<(), WorkspaceError> {
+    let params: ViewIdentifier = data.into_inner().try_into()?;
+    let _ = controller.duplicate_view(params.into()).await?;
+    Ok(())
+}

+ 1 - 0
rust-lib/flowy-workspace/src/module.rs

@@ -88,6 +88,7 @@ pub fn create(workspace: Arc<WorkspaceController>) -> Module {
         .event(WorkspaceEvent::ReadView, read_view_handler)
         .event(WorkspaceEvent::UpdateView, update_view_handler)
         .event(WorkspaceEvent::DeleteView, delete_view_handler)
+        .event(WorkspaceEvent::DuplicateView, duplicate_view_handler)
         .event(WorkspaceEvent::OpenView, open_view_handler)
         .event(WorkspaceEvent::CloseView, close_view_handler)
         .event(WorkspaceEvent::ApplyDocDelta, apply_doc_delta_handler);

+ 63 - 52
rust-lib/flowy-workspace/src/protobuf/model/event.rs

@@ -39,14 +39,16 @@ pub enum WorkspaceEvent {
     ReadView = 202,
     UpdateView = 203,
     DeleteView = 204,
-    OpenView = 205,
-    CloseView = 206,
-    ApplyDocDelta = 207,
+    DuplicateView = 205,
+    CopyLink = 206,
+    OpenView = 207,
+    CloseView = 208,
     ReadTrash = 300,
     PutbackTrash = 301,
     DeleteTrash = 302,
     RestoreAll = 303,
     DeleteAll = 304,
+    ApplyDocDelta = 400,
     InitWorkspace = 1000,
 }
 
@@ -71,14 +73,16 @@ impl ::protobuf::ProtobufEnum for WorkspaceEvent {
             202 => ::std::option::Option::Some(WorkspaceEvent::ReadView),
             203 => ::std::option::Option::Some(WorkspaceEvent::UpdateView),
             204 => ::std::option::Option::Some(WorkspaceEvent::DeleteView),
-            205 => ::std::option::Option::Some(WorkspaceEvent::OpenView),
-            206 => ::std::option::Option::Some(WorkspaceEvent::CloseView),
-            207 => ::std::option::Option::Some(WorkspaceEvent::ApplyDocDelta),
+            205 => ::std::option::Option::Some(WorkspaceEvent::DuplicateView),
+            206 => ::std::option::Option::Some(WorkspaceEvent::CopyLink),
+            207 => ::std::option::Option::Some(WorkspaceEvent::OpenView),
+            208 => ::std::option::Option::Some(WorkspaceEvent::CloseView),
             300 => ::std::option::Option::Some(WorkspaceEvent::ReadTrash),
             301 => ::std::option::Option::Some(WorkspaceEvent::PutbackTrash),
             302 => ::std::option::Option::Some(WorkspaceEvent::DeleteTrash),
             303 => ::std::option::Option::Some(WorkspaceEvent::RestoreAll),
             304 => ::std::option::Option::Some(WorkspaceEvent::DeleteAll),
+            400 => ::std::option::Option::Some(WorkspaceEvent::ApplyDocDelta),
             1000 => ::std::option::Option::Some(WorkspaceEvent::InitWorkspace),
             _ => ::std::option::Option::None
         }
@@ -100,14 +104,16 @@ impl ::protobuf::ProtobufEnum for WorkspaceEvent {
             WorkspaceEvent::ReadView,
             WorkspaceEvent::UpdateView,
             WorkspaceEvent::DeleteView,
+            WorkspaceEvent::DuplicateView,
+            WorkspaceEvent::CopyLink,
             WorkspaceEvent::OpenView,
             WorkspaceEvent::CloseView,
-            WorkspaceEvent::ApplyDocDelta,
             WorkspaceEvent::ReadTrash,
             WorkspaceEvent::PutbackTrash,
             WorkspaceEvent::DeleteTrash,
             WorkspaceEvent::RestoreAll,
             WorkspaceEvent::DeleteAll,
+            WorkspaceEvent::ApplyDocDelta,
             WorkspaceEvent::InitWorkspace,
         ];
         values
@@ -137,38 +143,39 @@ impl ::protobuf::reflect::ProtobufValue for WorkspaceEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0bevent.proto*\xa7\x03\n\x0eWorkspaceEvent\x12\x13\n\x0fCreateWorksp\
+    \n\x0bevent.proto*\xca\x03\n\x0eWorkspaceEvent\x12\x13\n\x0fCreateWorksp\
     ace\x10\0\x12\x14\n\x10ReadCurWorkspace\x10\x01\x12\x12\n\x0eReadWorkspa\
     ces\x10\x02\x12\x13\n\x0fDeleteWorkspace\x10\x03\x12\x11\n\rOpenWorkspac\
     e\x10\x04\x12\x15\n\x11ReadWorkspaceApps\x10\x05\x12\r\n\tCreateApp\x10e\
     \x12\r\n\tDeleteApp\x10f\x12\x0b\n\x07ReadApp\x10g\x12\r\n\tUpdateApp\
     \x10h\x12\x0f\n\nCreateView\x10\xc9\x01\x12\r\n\x08ReadView\x10\xca\x01\
     \x12\x0f\n\nUpdateView\x10\xcb\x01\x12\x0f\n\nDeleteView\x10\xcc\x01\x12\
-    \r\n\x08OpenView\x10\xcd\x01\x12\x0e\n\tCloseView\x10\xce\x01\x12\x12\n\
-    \rApplyDocDelta\x10\xcf\x01\x12\x0e\n\tReadTrash\x10\xac\x02\x12\x11\n\
-    \x0cPutbackTrash\x10\xad\x02\x12\x10\n\x0bDeleteTrash\x10\xae\x02\x12\
-    \x0f\n\nRestoreAll\x10\xaf\x02\x12\x0e\n\tDeleteAll\x10\xb0\x02\x12\x12\
-    \n\rInitWorkspace\x10\xe8\x07J\xd9\x07\n\x06\x12\x04\0\0\x1a\x01\n\x08\n\
-    \x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x1a\x01\n\n\n\x03\
-    \x05\0\x01\x12\x03\x02\x05\x13\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\
-    \x18\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x13\n\x0c\n\x05\x05\0\
-    \x02\0\x02\x12\x03\x03\x16\x17\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\
-    \x19\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x14\n\x0c\n\x05\x05\0\
-    \x02\x01\x02\x12\x03\x04\x17\x18\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\
-    \x04\x17\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x12\n\x0c\n\x05\
-    \x05\0\x02\x02\x02\x12\x03\x05\x15\x16\n\x0b\n\x04\x05\0\x02\x03\x12\x03\
-    \x06\x04\x18\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x13\n\x0c\n\
-    \x05\x05\0\x02\x03\x02\x12\x03\x06\x16\x17\n\x0b\n\x04\x05\0\x02\x04\x12\
-    \x03\x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x11\n\x0c\
-    \n\x05\x05\0\x02\x04\x02\x12\x03\x07\x14\x15\n\x0b\n\x04\x05\0\x02\x05\
-    \x12\x03\x08\x04\x1a\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x15\n\
-    \x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x18\x19\n\x0b\n\x04\x05\0\x02\
-    \x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\r\n\
-    \x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x10\x13\n\x0b\n\x04\x05\0\x02\x07\
-    \x12\x03\n\x04\x14\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\r\n\x0c\n\
-    \x05\x05\0\x02\x07\x02\x12\x03\n\x10\x13\n\x0b\n\x04\x05\0\x02\x08\x12\
-    \x03\x0b\x04\x12\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\x0b\n\x0c\
-    \n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x0e\x11\n\x0b\n\x04\x05\0\x02\t\x12\
+    \x12\n\rDuplicateView\x10\xcd\x01\x12\r\n\x08CopyLink\x10\xce\x01\x12\r\
+    \n\x08OpenView\x10\xcf\x01\x12\x0e\n\tCloseView\x10\xd0\x01\x12\x0e\n\tR\
+    eadTrash\x10\xac\x02\x12\x11\n\x0cPutbackTrash\x10\xad\x02\x12\x10\n\x0b\
+    DeleteTrash\x10\xae\x02\x12\x0f\n\nRestoreAll\x10\xaf\x02\x12\x0e\n\tDel\
+    eteAll\x10\xb0\x02\x12\x12\n\rApplyDocDelta\x10\x90\x03\x12\x12\n\rInitW\
+    orkspace\x10\xe8\x07J\xab\x08\n\x06\x12\x04\0\0\x1c\x01\n\x08\n\x01\x0c\
+    \x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x1c\x01\n\n\n\x03\x05\0\
+    \x01\x12\x03\x02\x05\x13\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\x18\n\
+    \x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x13\n\x0c\n\x05\x05\0\x02\0\
+    \x02\x12\x03\x03\x16\x17\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\x19\n\
+    \x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x14\n\x0c\n\x05\x05\0\x02\
+    \x01\x02\x12\x03\x04\x17\x18\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\x04\
+    \x17\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x12\n\x0c\n\x05\x05\0\
+    \x02\x02\x02\x12\x03\x05\x15\x16\n\x0b\n\x04\x05\0\x02\x03\x12\x03\x06\
+    \x04\x18\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x13\n\x0c\n\x05\
+    \x05\0\x02\x03\x02\x12\x03\x06\x16\x17\n\x0b\n\x04\x05\0\x02\x04\x12\x03\
+    \x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x11\n\x0c\n\
+    \x05\x05\0\x02\x04\x02\x12\x03\x07\x14\x15\n\x0b\n\x04\x05\0\x02\x05\x12\
+    \x03\x08\x04\x1a\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x15\n\x0c\
+    \n\x05\x05\0\x02\x05\x02\x12\x03\x08\x18\x19\n\x0b\n\x04\x05\0\x02\x06\
+    \x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\r\n\x0c\n\
+    \x05\x05\0\x02\x06\x02\x12\x03\t\x10\x13\n\x0b\n\x04\x05\0\x02\x07\x12\
+    \x03\n\x04\x14\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\r\n\x0c\n\x05\
+    \x05\0\x02\x07\x02\x12\x03\n\x10\x13\n\x0b\n\x04\x05\0\x02\x08\x12\x03\
+    \x0b\x04\x12\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\x0b\n\x0c\n\
+    \x05\x05\0\x02\x08\x02\x12\x03\x0b\x0e\x11\n\x0b\n\x04\x05\0\x02\t\x12\
     \x03\x0c\x04\x14\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x0c\x04\r\n\x0c\n\
     \x05\x05\0\x02\t\x02\x12\x03\x0c\x10\x13\n\x0b\n\x04\x05\0\x02\n\x12\x03\
     \r\x04\x15\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\r\x04\x0e\n\x0c\n\x05\x05\
@@ -179,25 +186,29 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x05\0\x02\x0c\x02\x12\x03\x0f\x11\x14\n\x0b\n\x04\x05\0\x02\r\x12\x03\
     \x10\x04\x15\n\x0c\n\x05\x05\0\x02\r\x01\x12\x03\x10\x04\x0e\n\x0c\n\x05\
     \x05\0\x02\r\x02\x12\x03\x10\x11\x14\n\x0b\n\x04\x05\0\x02\x0e\x12\x03\
-    \x11\x04\x13\n\x0c\n\x05\x05\0\x02\x0e\x01\x12\x03\x11\x04\x0c\n\x0c\n\
-    \x05\x05\0\x02\x0e\x02\x12\x03\x11\x0f\x12\n\x0b\n\x04\x05\0\x02\x0f\x12\
-    \x03\x12\x04\x14\n\x0c\n\x05\x05\0\x02\x0f\x01\x12\x03\x12\x04\r\n\x0c\n\
-    \x05\x05\0\x02\x0f\x02\x12\x03\x12\x10\x13\n\x0b\n\x04\x05\0\x02\x10\x12\
-    \x03\x13\x04\x18\n\x0c\n\x05\x05\0\x02\x10\x01\x12\x03\x13\x04\x11\n\x0c\
-    \n\x05\x05\0\x02\x10\x02\x12\x03\x13\x14\x17\n\x0b\n\x04\x05\0\x02\x11\
-    \x12\x03\x14\x04\x14\n\x0c\n\x05\x05\0\x02\x11\x01\x12\x03\x14\x04\r\n\
-    \x0c\n\x05\x05\0\x02\x11\x02\x12\x03\x14\x10\x13\n\x0b\n\x04\x05\0\x02\
-    \x12\x12\x03\x15\x04\x17\n\x0c\n\x05\x05\0\x02\x12\x01\x12\x03\x15\x04\
-    \x10\n\x0c\n\x05\x05\0\x02\x12\x02\x12\x03\x15\x13\x16\n\x0b\n\x04\x05\0\
-    \x02\x13\x12\x03\x16\x04\x16\n\x0c\n\x05\x05\0\x02\x13\x01\x12\x03\x16\
-    \x04\x0f\n\x0c\n\x05\x05\0\x02\x13\x02\x12\x03\x16\x12\x15\n\x0b\n\x04\
-    \x05\0\x02\x14\x12\x03\x17\x04\x15\n\x0c\n\x05\x05\0\x02\x14\x01\x12\x03\
-    \x17\x04\x0e\n\x0c\n\x05\x05\0\x02\x14\x02\x12\x03\x17\x11\x14\n\x0b\n\
-    \x04\x05\0\x02\x15\x12\x03\x18\x04\x14\n\x0c\n\x05\x05\0\x02\x15\x01\x12\
-    \x03\x18\x04\r\n\x0c\n\x05\x05\0\x02\x15\x02\x12\x03\x18\x10\x13\n\x0b\n\
-    \x04\x05\0\x02\x16\x12\x03\x19\x04\x19\n\x0c\n\x05\x05\0\x02\x16\x01\x12\
-    \x03\x19\x04\x11\n\x0c\n\x05\x05\0\x02\x16\x02\x12\x03\x19\x14\x18b\x06p\
-    roto3\
+    \x11\x04\x18\n\x0c\n\x05\x05\0\x02\x0e\x01\x12\x03\x11\x04\x11\n\x0c\n\
+    \x05\x05\0\x02\x0e\x02\x12\x03\x11\x14\x17\n\x0b\n\x04\x05\0\x02\x0f\x12\
+    \x03\x12\x04\x13\n\x0c\n\x05\x05\0\x02\x0f\x01\x12\x03\x12\x04\x0c\n\x0c\
+    \n\x05\x05\0\x02\x0f\x02\x12\x03\x12\x0f\x12\n\x0b\n\x04\x05\0\x02\x10\
+    \x12\x03\x13\x04\x13\n\x0c\n\x05\x05\0\x02\x10\x01\x12\x03\x13\x04\x0c\n\
+    \x0c\n\x05\x05\0\x02\x10\x02\x12\x03\x13\x0f\x12\n\x0b\n\x04\x05\0\x02\
+    \x11\x12\x03\x14\x04\x14\n\x0c\n\x05\x05\0\x02\x11\x01\x12\x03\x14\x04\r\
+    \n\x0c\n\x05\x05\0\x02\x11\x02\x12\x03\x14\x10\x13\n\x0b\n\x04\x05\0\x02\
+    \x12\x12\x03\x15\x04\x14\n\x0c\n\x05\x05\0\x02\x12\x01\x12\x03\x15\x04\r\
+    \n\x0c\n\x05\x05\0\x02\x12\x02\x12\x03\x15\x10\x13\n\x0b\n\x04\x05\0\x02\
+    \x13\x12\x03\x16\x04\x17\n\x0c\n\x05\x05\0\x02\x13\x01\x12\x03\x16\x04\
+    \x10\n\x0c\n\x05\x05\0\x02\x13\x02\x12\x03\x16\x13\x16\n\x0b\n\x04\x05\0\
+    \x02\x14\x12\x03\x17\x04\x16\n\x0c\n\x05\x05\0\x02\x14\x01\x12\x03\x17\
+    \x04\x0f\n\x0c\n\x05\x05\0\x02\x14\x02\x12\x03\x17\x12\x15\n\x0b\n\x04\
+    \x05\0\x02\x15\x12\x03\x18\x04\x15\n\x0c\n\x05\x05\0\x02\x15\x01\x12\x03\
+    \x18\x04\x0e\n\x0c\n\x05\x05\0\x02\x15\x02\x12\x03\x18\x11\x14\n\x0b\n\
+    \x04\x05\0\x02\x16\x12\x03\x19\x04\x14\n\x0c\n\x05\x05\0\x02\x16\x01\x12\
+    \x03\x19\x04\r\n\x0c\n\x05\x05\0\x02\x16\x02\x12\x03\x19\x10\x13\n\x0b\n\
+    \x04\x05\0\x02\x17\x12\x03\x1a\x04\x18\n\x0c\n\x05\x05\0\x02\x17\x01\x12\
+    \x03\x1a\x04\x11\n\x0c\n\x05\x05\0\x02\x17\x02\x12\x03\x1a\x14\x17\n\x0b\
+    \n\x04\x05\0\x02\x18\x12\x03\x1b\x04\x19\n\x0c\n\x05\x05\0\x02\x18\x01\
+    \x12\x03\x1b\x04\x11\n\x0c\n\x05\x05\0\x02\x18\x02\x12\x03\x1b\x14\x18b\
+    \x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 5 - 3
rust-lib/flowy-workspace/src/protobuf/proto/event.proto

@@ -15,13 +15,15 @@ enum WorkspaceEvent {
     ReadView = 202;
     UpdateView = 203;
     DeleteView = 204;
-    OpenView = 205;
-    CloseView = 206;
-    ApplyDocDelta = 207;
+    DuplicateView = 205;
+    CopyLink = 206;
+    OpenView = 207;
+    CloseView = 208;
     ReadTrash = 300;
     PutbackTrash = 301;
     DeleteTrash = 302;
     RestoreAll = 303;
     DeleteAll = 304;
+    ApplyDocDelta = 400;
     InitWorkspace = 1000;
 }

+ 21 - 0
rust-lib/flowy-workspace/src/services/view_controller.rs

@@ -119,6 +119,27 @@ impl ViewController {
         Ok(())
     }
 
+    #[tracing::instrument(level = "debug", skip(self), err)]
+    pub(crate) async fn duplicate_view(&self, params: DocIdentifier) -> Result<(), WorkspaceError> {
+        let view: View = ViewTableSql::read_view(&params.doc_id, &*self.database.db_connection()?)?.into();
+        let delta_data = self
+            .document
+            .read_document_data(params, self.database.db_pool()?)
+            .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(),
+            data: delta_data.data,
+        };
+
+        let _ = self.create_view(duplicate_params).await?;
+        Ok(())
+    }
+
     // belong_to_id will be the app_id or view_id.
     #[tracing::instrument(level = "debug", skip(self), err)]
     pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, WorkspaceError> {