Forráskód Böngészése

Merge pull request #2004 from AppFlowy-IO/chore/0.1.1

release 0.1.1
Lucas.Xu 2 éve
szülő
commit
b0d525a48f
16 módosított fájl, 276 hozzáadás és 120 törlés
  1. 1 1
      frontend/Makefile.toml
  2. 3 3
      frontend/appflowy_flutter/assets/translations/en.json
  3. 17 8
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
  4. 5 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart
  5. 20 1
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
  6. 102 61
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
  7. 6 4
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
  8. 7 4
      frontend/appflowy_flutter/lib/user/application/user_listener.dart
  9. 24 0
      frontend/appflowy_flutter/lib/util/debounce.dart
  10. 23 5
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  11. 2 2
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
  12. 9 11
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart
  13. 2 2
      frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart
  14. 37 0
      frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart
  15. 17 15
      frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs
  16. 1 1
      frontend/scripts/windows_installer/inno_setup_config.iss

+ 1 - 1
frontend/Makefile.toml

@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
 CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
 CARGO_MAKE_CRATE_NAME = "dart-ffi"
 LIB_NAME = "dart_ffi"
-CURRENT_APP_VERSION = "0.1.0"
+CURRENT_APP_VERSION = "0.1.1"
 FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
 PRODUCT_NAME = "AppFlowy"
 # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

+ 3 - 3
frontend/appflowy_flutter/assets/translations/en.json

@@ -138,7 +138,8 @@
     "keep": "Keep",
     "tryAgain": "Try again",
     "discard": "Discard",
-    "replace": "Replace"
+    "replace": "Replace",
+    "insertBelow": "Insert Below"
   },
   "label": {
     "welcome": "Welcome!",
@@ -342,8 +343,7 @@
     "plugins": {
       "referencedBoard": "Referenced Board",
       "referencedGrid": "Referenced Grid",
-      "autoCompletionMenuItemName": "Auto Completion",
-      "autoGeneratorMenuItemName": "Auto Generator",
+      "autoGeneratorMenuItemName": "OpenAI Writer",
       "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
       "autoGeneratorLearnMore": "Learn more",
       "autoGeneratorGenerate": "Generate",

+ 17 - 8
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

@@ -1,7 +1,6 @@
 import 'dart:convert';
 
 import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
 
 import 'text_completion.dart';
 import 'package:dartz/dartz.dart';
@@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
     String? suffix,
     int maxTokens = 2048,
     double temperature = 0.3,
+    bool useAction = false,
   }) async {
     final parameters = {
       'model': 'text-davinci-003',
@@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
           .transform(const Utf8Decoder())
           .transform(const LineSplitter())) {
         syntax += 1;
-        if (syntax == 3) {
-          await onStart();
-          continue;
-        } else if (syntax < 3) {
-          continue;
+        if (!useAction) {
+          if (syntax == 3) {
+            await onStart();
+            continue;
+          } else if (syntax < 3) {
+            continue;
+          }
+        } else {
+          if (syntax == 2) {
+            await onStart();
+            continue;
+          } else if (syntax < 2) {
+            continue;
+          }
         }
         final data = chunk.trim().split('data: ');
-        Log.editor.info(data.toString());
         if (data.length > 1) {
           if (data[1] != '[DONE]') {
             final response = TextCompletionResponse.fromJson(
@@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
               previousSyntax = response.choices.first.text;
             }
           } else {
-            onEnd();
+            await onEnd();
           }
         }
       }
@@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
         OpenAIError.fromJson(json.decode(body)['error']),
       );
     }
+    return;
   }
 
   @override

+ 5 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart

@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
 
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
 SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
-  name: 'Auto Generator',
+  name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
   iconData: Icons.generating_tokens,
-  keywords: ['autogenerator', 'auto generator'],
+  keywords: ['ai', 'openai' 'writer', 'autogenerator'],
   nodeBuilder: (editorState) {
     final node = Node(
       type: kAutoCompletionInputType,

+ 20 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

@@ -10,11 +10,30 @@ enum SmartEditAction {
   String get toInstruction {
     switch (this) {
       case SmartEditAction.summarize:
-        return 'Make this shorter and more concise:';
+        return 'Tl;dr';
       case SmartEditAction.fixSpelling:
         return 'Correct this to standard English:';
     }
   }
+
+  String prompt(String input) {
+    switch (this) {
+      case SmartEditAction.summarize:
+        return '$input\n\nTl;dr';
+      case SmartEditAction.fixSpelling:
+        return 'Correct this to standard English:\n\n$input';
+    }
+  }
+
+  static SmartEditAction from(int index) {
+    switch (index) {
+      case 0:
+        return SmartEditAction.summarize;
+      case 1:
+        return SmartEditAction.fixSpelling;
+    }
+    return SmartEditAction.fixSpelling;
+  }
 }
 
 class SmartEditActionWrapper extends ActionCell {

+ 102 - 61
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

@@ -1,6 +1,4 @@
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
 import 'package:appflowy/user/application/user_service.dart';
@@ -12,8 +10,6 @@ import 'package:flutter/material.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:http/http.dart' as http;
-import 'package:dartz/dartz.dart' as dartz;
-import 'package:appflowy/util/either_extension.dart';
 
 const String kSmartEditType = 'smart_edit_input';
 const String kSmartEditInstructionType = 'smart_edit_instruction';
@@ -22,9 +18,9 @@ const String kSmartEditInputType = 'smart_edit_input';
 class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
   @override
   NodeValidator<Node> get nodeValidator => (node) {
-        return SmartEditAction.values.map((e) => e.toInstruction).contains(
-                  node.attributes[kSmartEditInstructionType],
-                ) &&
+        return SmartEditAction.values
+                .map((e) => e.index)
+                .contains(node.attributes[kSmartEditInstructionType]) &&
             node.attributes[kSmartEditInputType] is String;
       };
 
@@ -53,13 +49,14 @@ class _SmartEditInput extends StatefulWidget {
 }
 
 class _SmartEditInputState extends State<_SmartEditInput> {
-  String get instruction => widget.node.attributes[kSmartEditInstructionType];
+  SmartEditAction get action =>
+      SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
   String get input => widget.node.attributes[kSmartEditInputType];
 
   final focusNode = FocusNode();
   final client = http.Client();
-  dartz.Either<OpenAIError, TextEditResponse>? result;
   bool loading = true;
+  String result = '';
 
   @override
   void initState() {
@@ -72,12 +69,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
         widget.editorState.service.keyboardService?.enable();
       }
     });
-    _requestEdits().then(
-      (value) => setState(() {
-        result = value;
-        loading = false;
-      }),
-    );
+    _requestCompletions();
   }
 
   @override
@@ -141,25 +133,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
         child: const CircularProgressIndicator(),
       ),
     );
-    if (result == null) {
+    if (result.isEmpty) {
       return loading;
     }
-    return result!.fold((error) {
-      return Flexible(
-        child: Text(
-          error.message,
-          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                color: Colors.red,
-              ),
-        ),
-      );
-    }, (response) {
-      return Flexible(
-        child: Text(
-          response.choices.map((e) => e.text).join('\n'),
-        ),
-      );
-    });
+    return Flexible(
+      child: Text(
+        result,
+      ),
+    );
   }
 
   Widget _buildInputFooterWidget(BuildContext context) {
@@ -174,8 +155,23 @@ class _SmartEditInputState extends State<_SmartEditInput> {
               ),
             ],
           ),
-          onPressed: () {
-            _onReplace();
+          onPressed: () async {
+            await _onReplace();
+            _onExit();
+          },
+        ),
+        const Space(10, 0),
+        FlowyRichTextButton(
+          TextSpan(
+            children: [
+              TextSpan(
+                text: LocaleKeys.button_insertBelow.tr(),
+                style: Theme.of(context).textTheme.bodyMedium,
+              ),
+            ],
+          ),
+          onPressed: () async {
+            await _onInsertBelow();
             _onExit();
           },
         ),
@@ -201,12 +197,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     final selectedNodes = widget
         .editorState.service.selectionService.currentSelectedNodes.normalized
         .whereType<TextNode>();
-    if (selection == null || result == null || result!.isLeft()) {
+    if (selection == null || result.isEmpty) {
       return;
     }
 
-    final texts = result!.asRight().choices.first.text.split('\n')
-      ..removeWhere((element) => element.isEmpty);
+    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
     final transaction = widget.editorState.transaction;
     transaction.replaceTexts(
       selectedNodes.toList(growable: false),
@@ -216,6 +211,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     return widget.editorState.apply(transaction);
   }
 
+  Future<void> _onInsertBelow() async {
+    final selection = widget.editorState.service.selectionService
+        .currentSelection.value?.normalized;
+    if (selection == null || result.isEmpty) {
+      return;
+    }
+    final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
+    final transaction = widget.editorState.transaction;
+    transaction.insertNodes(
+      selection.normalized.end.path.next,
+      texts.map(
+        (e) => TextNode(
+          delta: Delta()..insert(e),
+        ),
+      ),
+    );
+    return widget.editorState.apply(transaction);
+  }
+
   Future<void> _onExit() async {
     final transaction = widget.editorState.transaction;
     transaction.deleteNode(widget.node);
@@ -228,35 +242,62 @@ class _SmartEditInputState extends State<_SmartEditInput> {
     );
   }
 
-  Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
+  Future<void> _requestCompletions() async {
     final result = await UserBackendService.getCurrentUserProfile();
-    return result.fold((userProfile) async {
+    return result.fold((l) async {
       final openAIRepository = HttpOpenAIRepository(
         client: client,
-        apiKey: userProfile.openaiKey,
-      );
-      final edits = await openAIRepository.getEdits(
-        input: input,
-        instruction: instruction,
-        n: 1,
+        apiKey: l.openaiKey,
       );
-      return edits.fold((error) async {
-        return dartz.Left(
-          OpenAIError(
-            message:
-                LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
-          ),
+      var lines = input.split('\n\n');
+      if (action == SmartEditAction.summarize) {
+        lines = [lines.join('\n')];
+      }
+      for (var i = 0; i < lines.length; i++) {
+        final element = lines[i];
+        await openAIRepository.getStreamedCompletions(
+          useAction: true,
+          prompt: action.prompt(element),
+          onStart: () async {
+            setState(() {
+              loading = false;
+            });
+          },
+          onProcess: (response) async {
+            setState(() {
+              this.result += response.choices.first.text;
+            });
+          },
+          onEnd: () async {
+            setState(() {
+              if (i != lines.length - 1) {
+                this.result += '\n';
+              }
+            });
+          },
+          onError: (error) async {
+            await _showError(error.message);
+            await _onExit();
+          },
         );
-      }, (textEdit) async {
-        return dartz.Right(textEdit);
-      });
-    }, (error) async {
-      // error
-      return dartz.Left(
-        OpenAIError(
-          message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
-        ),
-      );
+      }
+    }, (r) async {
+      await _showError(r.msg);
+      await _onExit();
     });
   }
+
+  Future<void> _showError(String message) async {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        action: SnackBarAction(
+          label: LocaleKeys.button_Cancel.tr(),
+          onPressed: () {
+            ScaffoldMessenger.of(context).hideCurrentSnackBar();
+          },
+        ),
+        content: FlowyText(message),
+      ),
+    );
+  }
 }

+ 6 - 4
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -16,8 +16,7 @@ ToolbarItem smartEditItem = ToolbarItem(
   validator: (editorState) {
     // All selected nodes must be text.
     final nodes = editorState.service.selectionService.currentSelectedNodes;
-    return nodes.whereType<TextNode>().length == nodes.length &&
-        nodes.length == 1;
+    return nodes.whereType<TextNode>().length == nodes.length;
   },
   itemBuilder: (context, editorState) {
     return _SmartEditWidget(
@@ -102,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
       textNodes.normalized,
       selection.normalized,
     );
+    while (input.last.isEmpty) {
+      input.removeLast();
+    }
     final transaction = widget.editorState.transaction;
     transaction.insertNode(
       selection.normalized.end.path.next,
       Node(
         type: kSmartEditType,
         attributes: {
-          kSmartEditInstructionType: actionWrapper.inner.toInstruction,
-          kSmartEditInputType: input,
+          kSmartEditInstructionType: actionWrapper.inner.index,
+          kSmartEditInputType: input.join('\n\n'),
         },
       ),
     );

+ 7 - 4
frontend/appflowy_flutter/lib/user/application/user_listener.dart

@@ -83,11 +83,10 @@ class UserWorkspaceListener {
       PublishNotifier();
 
   FolderNotificationListener? _listener;
-  final UserProfilePB _userProfile;
 
   UserWorkspaceListener({
     required UserProfilePB userProfile,
-  }) : _userProfile = userProfile;
+  });
 
   void start({
     void Function(AuthNotifyValue)? onAuthChanged,
@@ -106,14 +105,18 @@ class UserWorkspaceListener {
       _settingChangedNotifier?.addPublishListener(onSettingUpdated);
     }
 
+    // The "current-workspace" is predefined in the backend. Do not try to
+    // modify it
     _listener = FolderNotificationListener(
-      objectId: _userProfile.token,
+      objectId: "current-workspace",
       handler: _handleObservableType,
     );
   }
 
   void _handleObservableType(
-      FolderNotification ty, Either<Uint8List, FlowyError> result) {
+    FolderNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
     switch (ty) {
       case FolderNotification.DidCreateWorkspace:
       case FolderNotification.DidDeleteWorkspace:

+ 24 - 0
frontend/appflowy_flutter/lib/util/debounce.dart

@@ -0,0 +1,24 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class Debounce {
+  final Duration duration;
+  Timer? _timer;
+
+  Debounce({
+    this.duration = const Duration(milliseconds: 1000),
+  });
+
+  void call(VoidCallback action) {
+    dispose();
+    _timer = Timer(duration, () {
+      action();
+    });
+  }
+
+  void dispose() {
+    _timer?.cancel();
+    _timer = null;
+  }
+}

+ 23 - 5
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/util/debounce.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
@@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget {
 
 class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
   bool visible = false;
+  final textEditingController = TextEditingController();
+  final debounce = Debounce();
+
+  @override
+  void initState() {
+    super.initState();
+
+    textEditingController.text = widget.openAIKey;
+  }
 
   @override
   Widget build(BuildContext context) {
     return TextField(
-      controller: TextEditingController()..text = widget.openAIKey,
+      controller: textEditingController,
       obscureText: !visible,
       decoration: InputDecoration(
         labelText: 'OpenAI Key',
@@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
           },
         ),
       ),
-      onSubmitted: (val) {
-        context
-            .read<SettingsUserViewBloc>()
-            .add(SettingsUserEvent.updateUserOpenAIKey(val));
+      onChanged: (value) {
+        debounce.call(() {
+          context
+              .read<SettingsUserViewBloc>()
+              .add(SettingsUserEvent.updateUserOpenAIKey(value));
+        });
       },
     );
   }
+
+  @override
+  void dispose() {
+    debounce.dispose();
+    super.dispose();
+  }
 }
 
 class _CurrentIcon extends StatelessWidget {

+ 2 - 2
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart

@@ -52,7 +52,7 @@ extension CommandExtension on EditorState {
     throw Exception('path and textNode cannot be null at the same time');
   }
 
-  String getTextInSelection(
+  List<String> getTextInSelection(
     List<TextNode> textNodes,
     Selection selection,
   ) {
@@ -77,6 +77,6 @@ extension CommandExtension on EditorState {
         }
       }
     }
-    return res.join('\n');
+    return res;
   }
 }

+ 9 - 11
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart

@@ -347,24 +347,22 @@ extension TextTransaction on Transaction {
             textNode.toPlainText().length,
             texts.first,
           );
-        } else if (i == length - 1) {
+        } else if (i == length - 1 && texts.length >= 2) {
           replaceText(
             textNode,
             0,
             selection.endIndex,
             texts.last,
           );
+        } else if (i < texts.length - 1) {
+          replaceText(
+            textNode,
+            0,
+            textNode.toPlainText().length,
+            texts[i],
+          );
         } else {
-          if (i < texts.length - 1) {
-            replaceText(
-              textNode,
-              0,
-              textNode.toPlainText().length,
-              texts[i],
-            );
-          } else {
-            deleteNode(textNode);
-          }
+          deleteNode(textNode);
         }
       }
       afterSelection = null;

+ 2 - 2
frontend/appflowy_flutter/packages/appflowy_editor/test/command/command_extension_test.dart

@@ -26,11 +26,11 @@ void main() {
           .editorState.service.selectionService.currentSelectedNodes
           .whereType<TextNode>()
           .toList(growable: false);
-      final text = editor.editorState.getTextInSelection(
+      final texts = editor.editorState.getTextInSelection(
         textNodes.normalized,
         selection.normalized,
       );
-      expect(text, 'me\nto\nAppfl');
+      expect(texts, ['me', 'to', 'Appfl']);
     });
   });
 }

+ 37 - 0
frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart

@@ -91,6 +91,43 @@ void main() async {
       expect(textNodes[3].toPlainText(), 'ABC456789');
     });
 
+    testWidgets('test replaceTexts, textNodes.length >> texts.length',
+        (tester) async {
+      TestWidgetsFlutterBinding.ensureInitialized();
+
+      final editor = tester.editor
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789');
+      await editor.startTesting();
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 5);
+
+      final selection = Selection(
+        start: Position(path: [0], offset: 4),
+        end: Position(path: [4], offset: 4),
+      );
+      final transaction = editor.editorState.transaction;
+      var textNodes = [0, 1, 2, 3, 4]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      final texts = ['ABC'];
+      transaction.replaceTexts(textNodes, selection, texts);
+      editor.editorState.apply(transaction);
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 1);
+      textNodes = [0]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      expect(textNodes[0].toPlainText(), '0123ABC');
+    });
+
     testWidgets('test replaceTexts, textNodes.length < texts.length',
         (tester) async {
       TestWidgetsFlutterBinding.ensureInitialized();

+ 17 - 15
frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs

@@ -11,6 +11,7 @@ use crate::{
 };
 use flowy_sqlite::kv::KV;
 use folder_model::{AppRevision, WorkspaceRevision};
+use lib_dispatch::prelude::ToBytes;
 use std::sync::Arc;
 
 pub struct WorkspaceController {
@@ -41,7 +42,6 @@ impl WorkspaceController {
   ) -> Result<WorkspaceRevision, FlowyError> {
     let workspace = self.create_workspace_on_server(params.clone()).await?;
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let workspaces = self
       .persistence
       .begin_transaction(|transaction| {
@@ -53,9 +53,7 @@ impl WorkspaceController {
       .map(|workspace_rev| workspace_rev.into())
       .collect();
     let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
-    send_notification(&token, FolderNotification::DidCreateWorkspace)
-      .payload(repeated_workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace);
     set_current_workspace(&user_id, &workspace.id);
     Ok(workspace)
   }
@@ -76,9 +74,7 @@ impl WorkspaceController {
       })
       .await?;
 
-    send_notification(&workspace_id, FolderNotification::DidUpdateWorkspace)
-      .payload(workspace)
-      .send();
+    send_workspace_notification(FolderNotification::DidUpdateWorkspace, workspace);
     self.update_workspace_on_server(params)?;
 
     Ok(())
@@ -87,7 +83,6 @@ impl WorkspaceController {
   #[allow(dead_code)]
   pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
     let user_id = self.user.user_id()?;
-    let token = self.user.token()?;
     let repeated_workspace = self
       .persistence
       .begin_transaction(|transaction| {
@@ -95,9 +90,8 @@ impl WorkspaceController {
         self.read_workspaces(None, &user_id, &transaction)
       })
       .await?;
-    send_notification(&token, FolderNotification::DidDeleteWorkspace)
-      .payload(repeated_workspace)
-      .send();
+
+    send_workspace_notification(FolderNotification::DidDeleteWorkspace, repeated_workspace);
     self.delete_workspace_on_server(workspace_id)?;
     Ok(())
   }
@@ -224,7 +218,6 @@ pub async fn notify_workspace_setting_did_change(
   view_id: &str,
 ) -> FlowyResult<()> {
   let user_id = folder_manager.user.user_id()?;
-  let token = folder_manager.user.token()?;
   let workspace_id = get_current_workspace(&user_id)?;
 
   let workspace_setting = folder_manager
@@ -250,11 +243,20 @@ pub async fn notify_workspace_setting_did_change(
       Ok(setting)
     })
     .await?;
+  send_workspace_notification(
+    FolderNotification::DidUpdateWorkspaceSetting,
+    workspace_setting,
+  );
+  Ok(())
+}
 
-  send_notification(&token, FolderNotification::DidUpdateWorkspaceSetting)
-    .payload(workspace_setting)
+/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
+/// user. Only one workspace can be opened at a time.
+const CURRENT_WORKSPACE: &str = "current-workspace";
+fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
+  send_notification(CURRENT_WORKSPACE, ty)
+    .payload(payload)
     .send();
-  Ok(())
 }
 
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";

+ 1 - 1
frontend/scripts/windows_installer/inno_setup_config.iss

@@ -13,7 +13,7 @@ AppPublisher=AppFlowy-IO
 VersionInfoVersion={#AppVersion}
 
 [Files]
-Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "appflowy_flutter.exe"
+Source: "AppFlowy\appflowy_flutter.exe";DestDir: "{app}";DestName: "appflowy_flutter.exe"
 Source: "AppFlowy\*";DestDir: "{app}"
 Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs