Przeglądaj źródła

feat: realtime user event (#3241)

* feat: update user profile after receiving realtime user event

* chore: logout if other deivce enable encyrption

* test: fix test

* chore: fix checkbox UI

* chore: fix tauri build

* chore: fix device id

* chore: fix duplicate run appflowy
Nathan.fooo 1 rok temu
rodzic
commit
a1647bee78
39 zmienionych plików z 817 dodań i 868 usunięć
  1. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  2. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart
  3. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart
  4. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart
  5. 0 6
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  6. 5 0
      frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart
  7. 3 1
      frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart
  8. 49 19
      frontend/appflowy_flutter/lib/user/application/supabase_realtime.dart
  9. 68 0
      frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart
  10. 0 29
      frontend/appflowy_flutter/lib/user/application/user_listener.dart
  11. 4 22
      frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart
  12. 6 11
      frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart
  13. 13 15
      frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart
  14. 1 15
      frontend/appflowy_flutter/lib/workspace/application/workspace/welcome_bloc.dart
  15. 0 10
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart
  16. 11 15
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart
  17. 173 455
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  18. 1 16
      frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts
  19. 1 0
      frontend/rust-lib/Cargo.lock
  20. 22 18
      frontend/rust-lib/flowy-core/src/integrate/server.rs
  21. 2 7
      frontend/rust-lib/flowy-core/src/lib.rs
  22. 0 3
      frontend/rust-lib/flowy-server/src/lib.rs
  23. 9 3
      frontend/rust-lib/flowy-server/src/supabase/api/request.rs
  24. 139 7
      frontend/rust-lib/flowy-server/src/supabase/api/user.rs
  25. 22 13
      frontend/rust-lib/flowy-server/src/supabase/api/util.rs
  26. 16 39
      frontend/rust-lib/flowy-server/src/supabase/entities.rs
  27. 32 60
      frontend/rust-lib/flowy-server/src/supabase/server.rs
  28. 1 1
      frontend/rust-lib/flowy-server/tests/supabase_test/util.rs
  29. 1 1
      frontend/rust-lib/flowy-test/tests/util.rs
  30. 1 0
      frontend/rust-lib/flowy-user-deps/Cargo.toml
  31. 17 0
      frontend/rust-lib/flowy-user-deps/src/cloud.rs
  32. 7 2
      frontend/rust-lib/flowy-user-deps/src/entities.rs
  33. 21 0
      frontend/rust-lib/flowy-user/src/entities/auth.rs
  34. 4 3
      frontend/rust-lib/flowy-user/src/event_handler.rs
  35. 0 2
      frontend/rust-lib/flowy-user/src/event_map.rs
  36. 150 86
      frontend/rust-lib/flowy-user/src/manager.rs
  37. 10 3
      frontend/rust-lib/flowy-user/src/notification.rs
  38. 2 2
      frontend/rust-lib/flowy-user/src/services/historical_user.rs
  39. 12 0
      frontend/rust-lib/flowy-user/src/services/user_sql.rs

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -382,7 +382,8 @@ Widget? _buildHeaderIcon(GroupData customData) {
     case FieldType.Checkbox:
       final group = customData.asCheckboxGroup()!;
       if (group.isCheck) {
-        widget = const FlowySvg(FlowySvgs.check_filled_s);
+        widget =
+            const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
       } else {
         widget = const FlowySvg(FlowySvgs.uncheck_s);
       }

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart

@@ -41,7 +41,10 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
             previous.isSelected != current.isSelected,
         builder: (context, state) {
           final icon = state.isSelected
-              ? const FlowySvg(FlowySvgs.check_filled_s)
+              ? const FlowySvg(
+                  FlowySvgs.check_filled_s,
+                  blendMode: BlendMode.dst,
+                )
               : const FlowySvg(FlowySvgs.uncheck_s);
           return Align(
             alignment: Alignment.centerLeft,

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart

@@ -89,7 +89,10 @@ class CheckboxCellCheck extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return const FlowySvg(FlowySvgs.check_filled_s);
+    return const FlowySvg(
+      FlowySvgs.check_filled_s,
+      blendMode: BlendMode.dst,
+    );
   }
 }
 

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart

@@ -109,7 +109,10 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
   @override
   Widget build(BuildContext context) {
     final icon = widget.option.isSelected
-        ? const FlowySvg(FlowySvgs.check_filled_s)
+        ? const FlowySvg(
+            FlowySvgs.check_filled_s,
+            blendMode: BlendMode.dst,
+          )
         : const FlowySvg(FlowySvgs.uncheck_s);
     return _wrapPopover(
       SizedBox(

+ 0 - 6
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -21,7 +21,6 @@ import 'package:appflowy/workspace/application/user/prelude.dart';
 import 'package:appflowy/workspace/application/workspace/prelude.dart';
 import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
 import 'package:appflowy/workspace/application/view/prelude.dart';
-import 'package:appflowy/workspace/application/menu/prelude.dart';
 import 'package:appflowy/workspace/application/settings/prelude.dart';
 import 'package:appflowy/user/application/prelude.dart';
 import 'package:appflowy/user/presentation/router.dart';
@@ -116,7 +115,6 @@ void _resolveHomeDeps(GetIt getIt) {
   getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
     (user, _) => WelcomeBloc(
       userService: UserBackendService(userId: user.id),
-      userWorkspaceListener: UserWorkspaceListener(userProfile: user),
     ),
   );
 
@@ -141,10 +139,6 @@ void _resolveFolderDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<MenuUserBloc, UserProfilePB, void>(
-    (user, _) => MenuUserBloc(user),
-  );
-
   //Settings
   getIt.registerFactoryParam<SettingsDialogBloc, UserProfilePB, void>(
     (user, _) => SettingsDialogBloc(user),

+ 5 - 0
frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart

@@ -40,6 +40,11 @@ class InitSupabaseTask extends LaunchTask {
       debug: kDebugMode,
       localStorage: const SupabaseLocalStorage(),
     );
+
+    if (realtimeService != null) {
+      await realtimeService?.dispose();
+      realtimeService = null;
+    }
     realtimeService = SupbaseRealtimeService(supabase: initializedSupabase);
     supabase = initializedSupabase;
 

+ 3 - 1
frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart

@@ -108,7 +108,9 @@ class SupabaseAuthService implements AuthService {
       return _appFlowyAuthService.signUpWithOAuth(platform: platform);
     }
     // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
-    await _auth.signOut();
+    if (_auth.currentUser != null) {
+      await _auth.signOut();
+    }
 
     final provider = platform.toProvider();
     final completer = supabaseLoginCompleter(

+ 49 - 19
frontend/appflowy_flutter/lib/user/application/supabase_realtime.dart

@@ -1,12 +1,16 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/user_auth_listener.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
 import 'package:supabase_flutter/supabase_flutter.dart';
 
+import 'auth/auth_service.dart';
+
 /// A service to manage realtime interactions with Supabase.
 ///
 /// `SupbaseRealtimeService` handles subscribing to table changes in Supabase
@@ -15,53 +19,73 @@ import 'package:supabase_flutter/supabase_flutter.dart';
 /// accordingly.
 class SupbaseRealtimeService {
   final Supabase supabase;
+  final _authStateListener = UserAuthStateListener();
+
+  bool isLoggingOut = false;
+
   RealtimeChannel? channel;
   StreamSubscription<AuthState>? authStateSubscription;
 
   SupbaseRealtimeService({required this.supabase}) {
     _subscribeAuthState();
+    _subscribeTablesChanges();
+
+    _authStateListener.start(
+      didSignIn: () {
+        _subscribeTablesChanges();
+        isLoggingOut = false;
+      },
+      onForceLogout: (message) async {
+        await getIt<AuthService>().signOut();
+        channel?.unsubscribe();
+        channel = null;
+        if (!isLoggingOut) {
+          await runAppFlowy();
+        }
+      },
+    );
   }
 
   void _subscribeAuthState() {
     final auth = Supabase.instance.client.auth;
     authStateSubscription = auth.onAuthStateChange.listen((state) async {
-      switch (state.event) {
-        case AuthChangeEvent.signedIn:
-          _subscribeTablesChanges();
-          break;
-        case AuthChangeEvent.signedOut:
-          channel?.unsubscribe();
-          break;
-        case AuthChangeEvent.tokenRefreshed:
-          _subscribeTablesChanges();
-          break;
-        default:
-          break;
-      }
+      Log.info("Supabase auth state change: ${state.event}");
     });
   }
 
   Future<void> _subscribeTablesChanges() async {
     final result = await UserBackendService.getCurrentUserProfile();
     result.fold((l) => null, (userProfile) {
-      Log.info("Start listening to table changes");
+      Log.info("Start listening supabase table changes");
       // https://supabase.com/docs/guides/realtime/postgres-changes
-      final filters = [
+      final List<ChannelFilter> filters = [
         "document",
         "folder",
         "database",
         "database_row",
         "w_database",
-      ].map(
-        (name) => ChannelFilter(
-          event: 'INSERT',
+      ]
+          .map(
+            (name) => ChannelFilter(
+              event: 'INSERT',
+              schema: 'public',
+              table: "af_collab_update_$name",
+              filter: 'uid=eq.${userProfile.id}',
+            ),
+          )
+          .toList();
+
+      filters.add(
+        ChannelFilter(
+          event: 'UPDATE',
           schema: 'public',
-          table: "af_collab_update_$name",
+          table: "af_user",
           filter: 'uid=eq.${userProfile.id}',
         ),
       );
 
       const ops = RealtimeChannelConfig(ack: true);
+      channel?.unsubscribe();
       channel = supabase.client.channel("table-db-changes", opts: ops);
       for (final filter in filters) {
         channel?.on(
@@ -88,4 +112,10 @@ class SupbaseRealtimeService {
       );
     });
   }
+
+  Future<void> dispose() async {
+    await _authStateListener.stop();
+    await authStateSubscription?.cancel();
+    await channel?.unsubscribe();
+  }
 }

+ 68 - 0
frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart

@@ -0,0 +1,68 @@
+import 'dart:async';
+import 'package:appflowy/core/notification/user_notification.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'dart:typed_data';
+import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'
+    as user;
+import 'package:appflowy_backend/rust_stream.dart';
+
+class UserAuthStateListener {
+  void Function(String)? _onForceLogout;
+  void Function()? _didSignIn;
+  StreamSubscription<SubscribeObject>? _subscription;
+  UserNotificationParser? _userParser;
+
+  void start({
+    void Function(String)? onForceLogout,
+    void Function()? didSignIn,
+  }) {
+    _onForceLogout = onForceLogout;
+    _didSignIn = didSignIn;
+
+    _userParser = UserNotificationParser(
+      id: "auth_state_change_notification",
+      callback: _userNotificationCallback,
+    );
+    _subscription = RustStreamReceiver.listen((observable) {
+      _userParser?.parse(observable);
+    });
+  }
+
+  Future<void> stop() async {
+    _userParser = null;
+    await _subscription?.cancel();
+    _onForceLogout = null;
+  }
+
+  void _userNotificationCallback(
+    user.UserNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case user.UserNotification.UserAuthStateChanged:
+        result.fold(
+          (payload) {
+            final pb = AuthStateChangedPB.fromBuffer(payload);
+            switch (pb.state) {
+              case AuthStatePB.AuthStateSignIn:
+                _didSignIn?.call();
+                break;
+              case AuthStatePB.AuthStateForceSignOut:
+                _onForceLogout?.call("");
+                break;
+              default:
+                break;
+            }
+          },
+          (r) => Log.error(r),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+}

+ 0 - 29
frontend/appflowy_flutter/lib/user/application/user_listener.dart

@@ -18,7 +18,6 @@ typedef AuthNotifyValue = Either<Unit, FlowyError>;
 
 class UserListener {
   StreamSubscription<SubscribeObject>? _subscription;
-  PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
   PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
 
   UserNotificationParser? _userParser;
@@ -28,17 +27,12 @@ class UserListener {
   }) : _userProfile = userProfile;
 
   void start({
-    void Function(AuthNotifyValue)? onAuthChanged,
     void Function(UserProfileNotifyValue)? onProfileUpdated,
   }) {
     if (onProfileUpdated != null) {
       _profileNotifier?.addPublishListener(onProfileUpdated);
     }
 
-    if (onAuthChanged != null) {
-      _authNotifier?.addPublishListener(onAuthChanged);
-    }
-
     _userParser = UserNotificationParser(
       id: _userProfile.id.toString(),
       callback: _userNotificationCallback,
@@ -53,9 +47,6 @@ class UserListener {
     await _subscription?.cancel();
     _profileNotifier?.dispose();
     _profileNotifier = null;
-
-    _authNotifier?.dispose();
-    _authNotifier = null;
   }
 
   void _userNotificationCallback(
@@ -76,13 +67,9 @@ class UserListener {
   }
 }
 
-typedef WorkspaceListNotifyValue = Either<List<WorkspacePB>, FlowyError>;
 typedef WorkspaceSettingNotifyValue = Either<WorkspaceSettingPB, FlowyError>;
 
 class UserWorkspaceListener {
-  PublishNotifier<AuthNotifyValue>? _authNotifier = PublishNotifier();
-  PublishNotifier<WorkspaceListNotifyValue>? _workspacesChangedNotifier =
-      PublishNotifier();
   PublishNotifier<WorkspaceSettingNotifyValue>? _settingChangedNotifier =
       PublishNotifier();
 
@@ -93,18 +80,8 @@ class UserWorkspaceListener {
   });
 
   void start({
-    void Function(AuthNotifyValue)? onAuthChanged,
-    void Function(WorkspaceListNotifyValue)? onWorkspacesUpdated,
     void Function(WorkspaceSettingNotifyValue)? onSettingUpdated,
   }) {
-    if (onAuthChanged != null) {
-      _authNotifier?.addPublishListener(onAuthChanged);
-    }
-
-    if (onWorkspacesUpdated != null) {
-      _workspacesChangedNotifier?.addPublishListener(onWorkspacesUpdated);
-    }
-
     if (onSettingUpdated != null) {
       _settingChangedNotifier?.addPublishListener(onSettingUpdated);
     }
@@ -122,7 +99,6 @@ class UserWorkspaceListener {
     Either<Uint8List, FlowyError> result,
   ) {
     switch (ty) {
-      case FolderNotification.DidCreateWorkspace:
       case FolderNotification.DidUpdateWorkspaceSetting:
         result.fold(
           (payload) => _settingChangedNotifier?.value =
@@ -137,13 +113,8 @@ class UserWorkspaceListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    _workspacesChangedNotifier?.dispose();
-    _workspacesChangedNotifier = null;
 
     _settingChangedNotifier?.dispose();
     _settingChangedNotifier = null;
-
-    _authNotifier?.dispose();
-    _authNotifier = null;
   }
 }

+ 4 - 22
frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart

@@ -1,24 +1,21 @@
 import 'package:appflowy/user/application/user_listener.dart';
 import 'package:flowy_infra/time/duration.dart';
 import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'
     show WorkspaceSettingPB;
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:dartz/dartz.dart';
 part 'home_bloc.freezed.dart';
 
 class HomeBloc extends Bloc<HomeEvent, HomeState> {
-  final UserWorkspaceListener _listener;
+  final UserWorkspaceListener _workspaceListener;
 
   HomeBloc(
     UserProfilePB user,
     WorkspaceSettingPB workspaceSetting,
-  )   : _listener = UserWorkspaceListener(userProfile: user),
+  )   : _workspaceListener = UserWorkspaceListener(userProfile: user),
         super(HomeState.initial(workspaceSetting)) {
     on<HomeEvent>(
       (event, emit) async {
@@ -30,8 +27,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
               }
             });
 
-            _listener.start(
-              onAuthChanged: (result) => _authDidChanged(result),
+            _workspaceListener.start(
               onSettingUpdated: (result) {
                 result.fold(
                   (setting) =>
@@ -56,9 +52,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
               ),
             );
           },
-          unauthorized: (_Unauthorized value) {
-            emit(state.copyWith(unauthorized: true));
-          },
         );
       },
     );
@@ -66,17 +59,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
 
   @override
   Future<void> close() async {
-    await _listener.stop();
+    await _workspaceListener.stop();
     return super.close();
   }
-
-  void _authDidChanged(Either<Unit, FlowyError> errorOrNothing) {
-    errorOrNothing.fold((_) {}, (error) {
-      if (error.code == ErrorCode.UserUnauthorized.value) {
-        add(HomeEvent.unauthorized(error.msg));
-      }
-    });
-  }
 }
 
 enum MenuResizeType {
@@ -102,7 +87,6 @@ class HomeEvent with _$HomeEvent {
   const factory HomeEvent.didReceiveWorkspaceSetting(
     WorkspaceSettingPB setting,
   ) = _DidReceiveWorkspaceSetting;
-  const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
 }
 
 @freezed
@@ -111,13 +95,11 @@ class HomeState with _$HomeState {
     required bool isLoading,
     required WorkspaceSettingPB workspaceSetting,
     ViewPB? latestView,
-    required bool unauthorized,
   }) = _HomeState;
 
   factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
         isLoading: false,
         workspaceSetting: workspaceSetting,
         latestView: null,
-        unauthorized: false,
       );
 }

+ 6 - 11
frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart

@@ -26,9 +26,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
       await event.when(
         initial: () async {
           _userListener.start(onProfileUpdated: _profileUpdated);
-          _userWorkspaceListener.start(
-            onWorkspacesUpdated: _workspaceListUpdated,
-          );
           await _initUser();
         },
         fetchWorkspaces: () async {
@@ -62,18 +59,16 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
   }
 
   void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
+    if (isClosed) {
+      return;
+    }
     userProfileOrFailed.fold(
-      (newUserProfile) =>
-          add(MenuUserEvent.didReceiveUserProfile(newUserProfile)),
+      (newUserProfile) => add(
+        MenuUserEvent.didReceiveUserProfile(newUserProfile),
+      ),
       (err) => Log.error(err),
     );
   }
-
-  void _workspaceListUpdated(
-    Either<List<WorkspacePB>, FlowyError> workspacesOrFailed,
-  ) {
-    // Do nothing by now
-  }
 }
 
 @freezed

+ 13 - 15
frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart

@@ -21,9 +21,8 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
     on<SettingsUserEvent>((event, emit) async {
       await event.when(
         initial: () async {
+          _loadUserProfile();
           _userListener.start(onProfileUpdated: _profileUpdated);
-          await _initUser();
-          _loadHistoricalUsers();
         },
         didReceiveUserProfile: (UserProfilePB newUserProfile) {
           emit(state.copyWith(userProfile: newUserProfile));
@@ -68,26 +67,25 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
     super.close();
   }
 
-  Future<void> _initUser() async {
-    final result = await _userService.initUser();
-    result.fold((l) => null, (error) => Log.error(error));
-  }
+  void _loadUserProfile() {
+    UserBackendService.getCurrentUserProfile().then((result) {
+      if (isClosed) {
+        return;
+      }
 
-  Future<void> _loadHistoricalUsers() async {
-    final result = await UserBackendService.loadHistoricalUsers();
-    result.fold(
-      (historicalUsers) {
-        add(SettingsUserEvent.didLoadHistoricalUsers(historicalUsers));
-      },
-      (error) => Log.error(error),
-    );
+      result.fold(
+        (err) => Log.error(err),
+        (userProfile) => add(
+          SettingsUserEvent.didReceiveUserProfile(userProfile),
+        ),
+      );
+    });
   }
 
   void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
     userProfileOrFailed.fold(
       (newUserProfile) {
         add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
-        _loadHistoricalUsers();
       },
       (err) => Log.error(err),
     );

+ 1 - 15
frontend/appflowy_flutter/lib/workspace/application/workspace/welcome_bloc.dart

@@ -1,4 +1,3 @@
-import 'package:appflowy/user/application/user_listener.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
@@ -11,18 +10,11 @@ part 'welcome_bloc.freezed.dart';
 
 class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
   final UserBackendService userService;
-  final UserWorkspaceListener userWorkspaceListener;
-  WelcomeBloc({required this.userService, required this.userWorkspaceListener})
-      : super(WelcomeState.initial()) {
+  WelcomeBloc({required this.userService}) : super(WelcomeState.initial()) {
     on<WelcomeEvent>(
       (event, emit) async {
         await event.map(
           initial: (e) async {
-            userWorkspaceListener.start(
-              onWorkspacesUpdated: (result) =>
-                  add(WelcomeEvent.workspacesReveived(result)),
-            );
-            //
             await _fetchWorkspaces(emit);
           },
           openWorkspace: (e) async {
@@ -47,12 +39,6 @@ class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
     );
   }
 
-  @override
-  Future<void> close() async {
-    await userWorkspaceListener.stop();
-    super.close();
-  }
-
   Future<void> _fetchWorkspaces(Emitter<WelcomeState> emit) async {
     final workspacesOrFailed = await userService.getWorkspaces();
     emit(

+ 0 - 10
frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart

@@ -61,16 +61,6 @@ class _HomeScreenState extends State<HomeScreen> {
         child: Scaffold(
           body: MultiBlocListener(
             listeners: [
-              BlocListener<HomeBloc, HomeState>(
-                listenWhen: (p, c) => p.unauthorized != c.unauthorized,
-                listener: (context, state) {
-                  if (state.unauthorized) {
-                    Log.error(
-                      "Push to login screen when user token was invalid",
-                    );
-                  }
-                },
-              ),
               BlocListener<HomeBloc, HomeState>(
                 listenWhen: (p, c) => p.latestView != c.latestView,
                 listener: (context, state) {

+ 11 - 15
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart

@@ -26,7 +26,7 @@ class SidebarUser extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider<MenuUserBloc>(
-      create: (context) => getIt<MenuUserBloc>(param1: user)
+      create: (context) => MenuUserBloc(user)
         ..add(
           const MenuUserEvent.initial(),
         ),
@@ -34,25 +34,23 @@ class SidebarUser extends StatelessWidget {
         builder: (context, state) => Row(
           crossAxisAlignment: CrossAxisAlignment.center,
           children: [
-            _buildAvatar(context),
+            _buildAvatar(context, state),
             const HSpace(10),
             Expanded(
-              child: _buildUserName(context),
+              child: _buildUserName(context, state),
             ),
-            _buildSettingsButton(context),
+            _buildSettingsButton(context, state),
           ],
         ),
       ),
     );
   }
 
-  Widget _buildAvatar(BuildContext context) {
-    String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
+  Widget _buildAvatar(BuildContext context, MenuUserState state) {
+    String iconUrl = state.userProfile.iconUrl;
     if (iconUrl.isEmpty) {
       iconUrl = defaultUserAvatar;
-      final String name = _userName(
-        context.read<MenuUserBloc>().state.userProfile,
-      );
+      final String name = _userName(state.userProfile);
       final Color color = ColorGenerator().generateColorFromString(name);
       const initialsCount = 2;
       // Taking the first letters of the name components and limiting to 2 elements
@@ -92,10 +90,8 @@ class SidebarUser extends StatelessWidget {
     );
   }
 
-  Widget _buildUserName(BuildContext context) {
-    final String name = _userName(
-      context.read<MenuUserBloc>().state.userProfile,
-    );
+  Widget _buildUserName(BuildContext context, MenuUserState state) {
+    final String name = _userName(state.userProfile);
     return FlowyText.medium(
       name,
       overflow: TextOverflow.ellipsis,
@@ -103,8 +99,8 @@ class SidebarUser extends StatelessWidget {
     );
   }
 
-  Widget _buildSettingsButton(BuildContext context) {
-    final userProfile = context.read<MenuUserBloc>().state.userProfile;
+  Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
+    final userProfile = state.userProfile;
     return Tooltip(
       message: LocaleKeys.settings_menu_open.tr(),
       child: IconButton(

+ 173 - 455
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -17,6 +17,41 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
 [[package]]
 name = "ahash"
 version = "0.7.6"
@@ -98,14 +133,14 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.71"
+version = "1.0.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
 
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "collab",
@@ -218,324 +253,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
-[[package]]
-name = "aws-config"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9"
-dependencies = [
- "aws-credential-types",
- "aws-http",
- "aws-sdk-sso",
- "aws-sdk-sts",
- "aws-smithy-async",
- "aws-smithy-client",
- "aws-smithy-http",
- "aws-smithy-http-tower",
- "aws-smithy-json",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "fastrand",
- "hex",
- "http",
- "hyper",
- "ring",
- "time 0.3.22",
- "tokio",
- "tower",
- "tracing",
- "zeroize",
-]
-
-[[package]]
-name = "aws-credential-types"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae"
-dependencies = [
- "aws-smithy-async",
- "aws-smithy-types",
- "fastrand",
- "tokio",
- "tracing",
- "zeroize",
-]
-
-[[package]]
-name = "aws-endpoint"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04"
-dependencies = [
- "aws-smithy-http",
- "aws-smithy-types",
- "aws-types",
- "http",
- "regex",
- "tracing",
-]
-
-[[package]]
-name = "aws-http"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44"
-dependencies = [
- "aws-credential-types",
- "aws-smithy-http",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "http",
- "http-body",
- "lazy_static",
- "percent-encoding",
- "pin-project-lite",
- "tracing",
-]
-
-[[package]]
-name = "aws-sdk-dynamodb"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67fb64867fe098cffee7e34352b01bbfa2beb3aa1b2ff0e0a7bf9ff293557852"
-dependencies = [
- "aws-credential-types",
- "aws-endpoint",
- "aws-http",
- "aws-sig-auth",
- "aws-smithy-async",
- "aws-smithy-client",
- "aws-smithy-http",
- "aws-smithy-http-tower",
- "aws-smithy-json",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "fastrand",
- "http",
- "regex",
- "tokio-stream",
- "tower",
- "tracing",
-]
-
-[[package]]
-name = "aws-sdk-sso"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4"
-dependencies = [
- "aws-credential-types",
- "aws-endpoint",
- "aws-http",
- "aws-sig-auth",
- "aws-smithy-async",
- "aws-smithy-client",
- "aws-smithy-http",
- "aws-smithy-http-tower",
- "aws-smithy-json",
- "aws-smithy-types",
- "aws-types",
- "bytes",
- "http",
- "regex",
- "tokio-stream",
- "tower",
- "tracing",
-]
-
-[[package]]
-name = "aws-sdk-sts"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b"
-dependencies = [
- "aws-credential-types",
- "aws-endpoint",
- "aws-http",
- "aws-sig-auth",
- "aws-smithy-async",
- "aws-smithy-client",
- "aws-smithy-http",
- "aws-smithy-http-tower",
- "aws-smithy-json",
- "aws-smithy-query",
- "aws-smithy-types",
- "aws-smithy-xml",
- "aws-types",
- "bytes",
- "http",
- "regex",
- "tower",
- "tracing",
-]
-
-[[package]]
-name = "aws-sig-auth"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61"
-dependencies = [
- "aws-credential-types",
- "aws-sigv4",
- "aws-smithy-http",
- "aws-types",
- "http",
- "tracing",
-]
-
-[[package]]
-name = "aws-sigv4"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
-dependencies = [
- "aws-smithy-http",
- "form_urlencoded",
- "hex",
- "hmac",
- "http",
- "once_cell",
- "percent-encoding",
- "regex",
- "sha2",
- "time 0.3.22",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-async"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880"
-dependencies = [
- "futures-util",
- "pin-project-lite",
- "tokio",
- "tokio-stream",
-]
-
-[[package]]
-name = "aws-smithy-client"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd"
-dependencies = [
- "aws-smithy-async",
- "aws-smithy-http",
- "aws-smithy-http-tower",
- "aws-smithy-types",
- "bytes",
- "fastrand",
- "http",
- "http-body",
- "hyper",
- "hyper-rustls 0.23.2",
- "lazy_static",
- "pin-project-lite",
- "rustls 0.20.8",
- "tokio",
- "tower",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-http"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
-dependencies = [
- "aws-smithy-types",
- "bytes",
- "bytes-utils",
- "futures-core",
- "http",
- "http-body",
- "hyper",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "pin-utils",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-http-tower"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9"
-dependencies = [
- "aws-smithy-http",
- "aws-smithy-types",
- "bytes",
- "http",
- "http-body",
- "pin-project-lite",
- "tower",
- "tracing",
-]
-
-[[package]]
-name = "aws-smithy-json"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8"
-dependencies = [
- "aws-smithy-types",
-]
-
-[[package]]
-name = "aws-smithy-query"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d"
-dependencies = [
- "aws-smithy-types",
- "urlencoding",
-]
-
-[[package]]
-name = "aws-smithy-types"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
-dependencies = [
- "base64-simd",
- "itoa 1.0.6",
- "num-integer",
- "ryu",
- "time 0.3.22",
-]
-
-[[package]]
-name = "aws-smithy-xml"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b"
-dependencies = [
- "xmlparser",
-]
-
-[[package]]
-name = "aws-types"
-version = "0.55.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829"
-dependencies = [
- "aws-credential-types",
- "aws-smithy-async",
- "aws-smithy-client",
- "aws-smithy-http",
- "aws-smithy-types",
- "http",
- "rustc_version",
- "tracing",
-]
-
 [[package]]
 name = "backtrace"
 version = "0.3.67"
@@ -563,16 +280,6 @@ version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
 
-[[package]]
-name = "base64-simd"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
-dependencies = [
- "outref",
- "vsimd",
-]
-
 [[package]]
 name = "bincode"
 version = "1.3.3"
@@ -776,16 +483,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "bytes-utils"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
-dependencies = [
- "bytes",
- "either",
-]
-
 [[package]]
 name = "bzip2-sys"
 version = "0.1.11+1.0.8"
@@ -951,6 +648,16 @@ dependencies = [
  "phf_codegen 0.11.2",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.6.1"
@@ -1021,7 +728,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1039,7 +746,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1057,7 +764,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1081,10 +788,18 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "collab-define"
+version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
+dependencies = [
+ "uuid",
+]
+
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1096,7 +811,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "collab",
@@ -1115,7 +830,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "chrono",
@@ -1135,7 +850,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "bincode",
  "chrono",
@@ -1155,15 +870,13 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "anyhow",
  "async-trait",
- "aws-config",
- "aws-credential-types",
- "aws-sdk-dynamodb",
  "collab",
  "collab-client-ws",
+ "collab-define",
  "collab-persistence",
  "collab-sync",
  "futures-util",
@@ -1178,6 +891,7 @@ dependencies = [
  "tokio-retry",
  "tokio-stream",
  "tracing",
+ "uuid",
  "y-sync",
  "yrs",
 ]
@@ -1185,7 +899,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
 dependencies = [
  "bytes",
  "collab",
@@ -1204,6 +918,21 @@ dependencies = [
  "yrs",
 ]
 
+[[package]]
+name = "collab-user"
+version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cff1b9#cff1b99f4ed51f65dab73492eac4da8e7907f079"
+dependencies = [
+ "anyhow",
+ "collab",
+ "parking_lot 0.12.1",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+]
+
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -1361,6 +1090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
+ "rand_core 0.6.4",
  "typenum",
 ]
 
@@ -1422,6 +1152,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "darling"
 version = "0.20.1"
@@ -1951,6 +1690,19 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "flowy-encrypt"
+version = "0.1.0"
+dependencies = [
+ "aes-gcm",
+ "anyhow",
+ "base64 0.21.2",
+ "hmac",
+ "pbkdf2",
+ "rand 0.8.5",
+ "sha2",
+]
+
 [[package]]
 name = "flowy-error"
 version = "0.1.0"
@@ -2050,6 +1802,7 @@ dependencies = [
  "config",
  "flowy-database-deps",
  "flowy-document-deps",
+ "flowy-encrypt",
  "flowy-error",
  "flowy-folder-deps",
  "flowy-server-config",
@@ -2122,11 +1875,13 @@ dependencies = [
  "collab",
  "collab-document",
  "collab-folder",
+ "collab-user",
  "diesel",
  "diesel_derives",
  "fancy-regex 0.11.0",
  "flowy-codegen",
  "flowy-derive",
+ "flowy-encrypt",
  "flowy-error",
  "flowy-notification",
  "flowy-server-config",
@@ -2156,11 +1911,13 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
+ "collab-define",
  "flowy-error",
  "lib-infra",
  "serde",
  "serde_json",
  "serde_repr",
+ "tokio",
  "uuid",
 ]
 
@@ -2461,6 +2218,16 @@ dependencies = [
  "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
+[[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
 [[package]]
 name = "gimli"
 version = "0.27.3"
@@ -2813,21 +2580,6 @@ dependencies = [
  "want",
 ]
 
-[[package]]
-name = "hyper-rustls"
-version = "0.23.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
-dependencies = [
- "http",
- "hyper",
- "log",
- "rustls 0.20.8",
- "rustls-native-certs",
- "tokio",
- "tokio-rustls 0.23.4",
-]
-
 [[package]]
 name = "hyper-rustls"
 version = "0.24.0"
@@ -2836,9 +2588,9 @@ checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7"
 dependencies = [
  "http",
  "hyper",
- "rustls 0.21.2",
+ "rustls",
  "tokio",
- "tokio-rustls 0.24.1",
+ "tokio-rustls",
 ]
 
 [[package]]
@@ -2963,6 +2715,15 @@ dependencies = [
  "cfb",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -3655,6 +3416,12 @@ version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
 [[package]]
 name = "open"
 version = "3.2.0"
@@ -3719,12 +3486,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "outref"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a"
-
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -3819,6 +3580,16 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
 
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
@@ -4087,6 +3858,18 @@ dependencies = [
  "miniz_oxide 0.7.1",
 ]
 
+[[package]]
+name = "polyval"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
 [[package]]
 name = "postgres-protocol"
 version = "0.6.5"
@@ -4547,7 +4330,7 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
- "hyper-rustls 0.24.0",
+ "hyper-rustls",
  "hyper-tls",
  "ipnet",
  "js-sys",
@@ -4557,14 +4340,14 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls 0.21.2",
+ "rustls",
  "rustls-pemfile",
  "serde",
  "serde_json",
  "serde_urlencoded",
  "tokio",
  "tokio-native-tls",
- "tokio-rustls 0.24.1",
+ "tokio-rustls",
  "tower-service",
  "url",
  "wasm-bindgen",
@@ -4708,18 +4491,6 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
-[[package]]
-name = "rustls"
-version = "0.20.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
-dependencies = [
- "log",
- "ring",
- "sct",
- "webpki",
-]
-
 [[package]]
 name = "rustls"
 version = "0.21.2"
@@ -4732,18 +4503,6 @@ dependencies = [
  "sct",
 ]
 
-[[package]]
-name = "rustls-native-certs"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
-dependencies = [
- "openssl-probe",
- "rustls-pemfile",
- "schannel",
- "security-framework",
-]
-
 [[package]]
 name = "rustls-pemfile"
 version = "1.0.2"
@@ -5849,24 +5608,13 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "tokio-rustls"
-version = "0.23.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
-dependencies = [
- "rustls 0.20.8",
- "tokio",
- "webpki",
-]
-
 [[package]]
 name = "tokio-rustls"
 version = "0.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
 dependencies = [
- "rustls 0.21.2",
+ "rustls",
  "tokio",
 ]
 
@@ -5951,28 +5699,6 @@ dependencies = [
  "winnow",
 ]
 
-[[package]]
-name = "tower"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
-dependencies = [
- "futures-core",
- "futures-util",
- "pin-project",
- "pin-project-lite",
- "tokio",
- "tower-layer",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "tower-layer"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
-
 [[package]]
 name = "tower-service"
 version = "0.3.2"
@@ -6240,6 +5966,16 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
 
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -6258,12 +5994,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "urlencoding"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
-
 [[package]]
 name = "utf-8"
 version = "0.7.6"
@@ -6325,12 +6055,6 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
-[[package]]
-name = "vsimd"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
-
 [[package]]
 name = "vswhom"
 version = "0.1.0"
@@ -6923,12 +6647,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "xmlparser"
-version = "0.13.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
-
 [[package]]
 name = "y-sync"
 version = "0.3.1"

+ 1 - 16
frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts

@@ -4,18 +4,11 @@ import { UserNotificationParser } from './parser';
 import { Ok, Result } from 'ts-results';
 
 declare type OnUserProfileUpdate = (result: Result<UserProfilePB, FlowyError>) => void;
-declare type OnUserSignIn = (result: Result<UserProfilePB, FlowyError>) => void;
 
 export class UserNotificationListener extends AFNotificationObserver<UserNotification> {
   onProfileUpdate?: OnUserProfileUpdate;
-  onUserSignIn?: OnUserSignIn;
 
-  constructor(params: {
-    userId?: string;
-    onUserSignIn?: OnUserSignIn;
-    onProfileUpdate?: OnUserProfileUpdate;
-    onError?: OnNotificationError;
-  }) {
+  constructor(params: { userId?: string; onProfileUpdate?: OnUserProfileUpdate; onError?: OnNotificationError }) {
     const parser = new UserNotificationParser({
       callback: (notification, result) => {
         switch (notification) {
@@ -26,13 +19,6 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific
               this.onProfileUpdate?.(result);
             }
             break;
-          case UserNotification.DidUserSignIn:
-            if (result.ok) {
-              this.onUserSignIn?.(Ok(UserProfilePB.deserializeBinary(result.val)));
-            } else {
-              this.onUserSignIn?.(result);
-            }
-            break;
           default:
             break;
         }
@@ -42,6 +28,5 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific
     });
     super(parser);
     this.onProfileUpdate = params.onProfileUpdate;
-    this.onUserSignIn = params.onUserSignIn;
   }
 }

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

@@ -1791,6 +1791,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_repr",
+ "tokio",
  "uuid",
 ]
 

+ 22 - 18
frontend/rust-lib/flowy-core/src/integrate/server.rs

@@ -4,8 +4,7 @@ use std::sync::{Arc, Weak};
 
 use appflowy_integrate::collab_builder::{CollabStorageProvider, CollabStorageType};
 use appflowy_integrate::{CollabObject, CollabType, RemoteCollabStorage, YrsDocAction};
-use parking_lot::{Mutex, RwLock};
-use serde_json::Value;
+use parking_lot::RwLock;
 use serde_repr::*;
 
 use flowy_database_deps::cloud::*;
@@ -64,11 +63,12 @@ impl Display for ServerProviderType {
 pub struct AppFlowyServerProvider {
   config: AppFlowyCoreConfig,
   provider_type: RwLock<ServerProviderType>,
-  device_id: Mutex<String>,
+  device_id: Arc<RwLock<String>>,
   providers: RwLock<HashMap<ServerProviderType, Arc<dyn AppFlowyServer>>>,
   enable_sync: RwLock<bool>,
   encryption: RwLock<Arc<dyn AppFlowyEncryption>>,
   store_preferences: Weak<StorePreferences>,
+  cache_user_service: RwLock<HashMap<ServerProviderType, Arc<dyn UserService>>>,
 }
 
 impl AppFlowyServerProvider {
@@ -86,11 +86,12 @@ impl AppFlowyServerProvider {
       enable_sync: RwLock::new(true),
       encryption: RwLock::new(Arc::new(encryption)),
       store_preferences,
+      cache_user_service: Default::default(),
     }
   }
 
   pub fn set_sync_device(&self, device_id: &str) {
-    *self.device_id.lock() = device_id.to_string();
+    *self.device_id.write() = device_id.to_string();
   }
 
   pub fn provider_type(&self) -> ServerProviderType {
@@ -134,11 +135,11 @@ impl AppFlowyServerProvider {
         Ok::<Arc<dyn AppFlowyServer>, FlowyError>(Arc::new(SupabaseServer::new(
           config,
           *self.enable_sync.read(),
+          self.device_id.clone(),
           encryption,
         )))
       },
     }?;
-    server.set_sync_device_id(&self.device_id.lock());
 
     self
       .providers
@@ -146,13 +147,6 @@ impl AppFlowyServerProvider {
       .insert(provider_type.clone(), server.clone());
     Ok(server)
   }
-
-  pub fn handle_realtime_event(&self, json: Value) {
-    let provider_type = self.provider_type.read().clone();
-    if let Some(server) = self.providers.read().get(&provider_type) {
-      server.handle_realtime_event(json);
-    }
-  }
 }
 
 impl UserCloudServiceProvider for AppFlowyServerProvider {
@@ -195,17 +189,27 @@ impl UserCloudServiceProvider for AppFlowyServerProvider {
   }
 
   fn set_device_id(&self, device_id: &str) {
-    *self.device_id.lock() = device_id.to_string();
+    *self.device_id.write() = device_id.to_string();
   }
 
   /// Returns the [UserService] base on the current [ServerProviderType].
   /// Creates a new [AppFlowyServer] if it doesn't exist.
   fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError> {
-    Ok(
-      self
-        .get_provider(&self.provider_type.read())?
-        .user_service(),
-    )
+    if let Some(user_service) = self
+      .cache_user_service
+      .read()
+      .get(&self.provider_type.read())
+    {
+      return Ok(user_service.clone());
+    }
+
+    let provider_type = self.provider_type.read().clone();
+    let user_service = self.get_provider(&provider_type)?.user_service();
+    self
+      .cache_user_service
+      .write()
+      .insert(provider_type, user_service.clone());
+    Ok(user_service)
   }
 
   fn service_name(&self) -> String {

+ 2 - 7
frontend/rust-lib/flowy-core/src/lib.rs

@@ -11,7 +11,6 @@ use std::{
 };
 
 use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CollabStorageType};
-use serde_json::Value;
 use tokio::sync::RwLock;
 
 use flowy_database2::DatabaseManager;
@@ -268,12 +267,12 @@ fn mk_user_session(
   collab_builder: Weak<AppFlowyCollabBuilder>,
 ) -> Arc<UserManager> {
   let user_config = UserSessionConfig::new(&config.name, &config.storage_path);
-  Arc::new(UserManager::new(
+  UserManager::new(
     user_config,
     user_cloud_service_provider,
     storage_preference.clone(),
     collab_builder,
-  ))
+  )
 }
 
 struct UserStatusCallbackImpl {
@@ -439,10 +438,6 @@ impl UserStatusCallback for UserStatusCallbackImpl {
   fn did_update_network(&self, reachable: bool) {
     self.collab_builder.update_network(reachable);
   }
-
-  fn receive_realtime_event(&self, json: Value) {
-    self.server_provider.handle_realtime_event(json);
-  }
 }
 
 impl From<ServerProviderType> for CollabStorageType {

+ 0 - 3
frontend/rust-lib/flowy-server/src/lib.rs

@@ -2,7 +2,6 @@ use std::sync::Arc;
 
 use collab_plugins::cloud_storage::{CollabObject, RemoteCollabStorage};
 use parking_lot::RwLock;
-use serde_json::Value;
 
 use flowy_database_deps::cloud::DatabaseCloudService;
 use flowy_document_deps::cloud::DocumentCloudService;
@@ -36,13 +35,11 @@ where
 
 pub trait AppFlowyServer: Send + Sync + 'static {
   fn set_enable_sync(&self, _enable: bool) {}
-  fn set_sync_device_id(&self, _device_id: &str) {}
   fn user_service(&self) -> Arc<dyn UserService>;
   fn folder_service(&self) -> Arc<dyn FolderCloudService>;
   fn database_service(&self) -> Arc<dyn DatabaseCloudService>;
   fn document_service(&self) -> Arc<dyn DocumentCloudService>;
   fn collab_storage(&self, collab_object: &CollabObject) -> Option<Arc<dyn RemoteCollabStorage>>;
-  fn handle_realtime_event(&self, _json: Value) {}
 }
 
 pub struct EncryptionImpl {

+ 9 - 3
frontend/rust-lib/flowy-server/src/supabase/api/request.rs

@@ -16,7 +16,8 @@ use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
 use lib_infra::util::md5;
 
 use crate::supabase::api::util::{
-  ExtendedResponse, InsertParamsBuilder, SupabaseBinaryColumnDecoder, SupabaseBinaryColumnEncoder,
+  BinaryColumnDecoder, ExtendedResponse, InsertParamsBuilder, SupabaseBinaryColumnDecoder,
+  SupabaseBinaryColumnEncoder,
 };
 use crate::supabase::api::PostgresWrapper;
 use crate::supabase::define::*;
@@ -220,7 +221,8 @@ fn parser_snapshot(
       .and_then(|value| value.as_str()),
   ) {
     (Some(encrypt), Some(value)) => {
-      SupabaseBinaryColumnDecoder::decode(value, encrypt as i32, secret).ok()
+      SupabaseBinaryColumnDecoder::decode::<_, BinaryColumnDecoder>(value, encrypt as i32, secret)
+        .ok()
     },
     _ => None,
   }?;
@@ -364,7 +366,11 @@ fn parser_update_from_json(
     json.get("value").and_then(|value| value.as_str()),
   ) {
     (Some(encrypt), Some(value)) => {
-      match SupabaseBinaryColumnDecoder::decode(value, encrypt as i32, encryption_secret) {
+      match SupabaseBinaryColumnDecoder::decode::<_, BinaryColumnDecoder>(
+        value,
+        encrypt as i32,
+        encryption_secret,
+      ) {
         Ok(value) => Some(value),
         Err(err) => {
           tracing::error!("Decode value column failed: {:?}", err);

+ 139 - 7
frontend/rust-lib/flowy-server/src/supabase/api/user.rs

@@ -1,8 +1,10 @@
 use std::str::FromStr;
-use std::sync::Arc;
+use std::sync::{Arc, Weak};
 
 use anyhow::Error;
 use collab_plugins::cloud_storage::CollabObject;
+use parking_lot::RwLock;
+use serde_json::Value;
 use tokio::sync::oneshot::channel;
 use uuid::Uuid;
 
@@ -13,20 +15,34 @@ use lib_infra::box_any::BoxAny;
 use lib_infra::future::FutureResult;
 
 use crate::supabase::api::request::FetchObjectUpdateAction;
-use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
+use crate::supabase::api::util::{
+  ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder,
+};
 use crate::supabase::api::{send_update, PostgresWrapper, SupabaseServerService};
 use crate::supabase::define::*;
-use crate::supabase::entities::GetUserProfileParams;
-use crate::supabase::entities::UidResponse;
 use crate::supabase::entities::UserProfileResponse;
+use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent};
+use crate::supabase::entities::{RealtimeCollabUpdateEvent, RealtimeEvent, UidResponse};
+use crate::supabase::CollabUpdateSenderByOid;
+use crate::AppFlowyEncryption;
 
 pub struct SupabaseUserServiceImpl<T> {
   server: T,
+  realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>,
+  user_update_tx: Option<UserUpdateSender>,
 }
 
 impl<T> SupabaseUserServiceImpl<T> {
-  pub fn new(server: T) -> Self {
-    Self { server }
+  pub fn new(
+    server: T,
+    realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>,
+    user_update_tx: Option<UserUpdateSender>,
+  ) -> Self {
+    Self {
+      server,
+      realtime_event_handlers,
+      user_update_tx,
+    }
   }
 }
 
@@ -67,7 +83,11 @@ where
       }
 
       // Query the user profile and workspaces
-      tracing::debug!("user uuid: {}", params.uuid);
+      tracing::debug!(
+        "user uuid: {}, device_id: {}",
+        params.uuid,
+        params.device_id
+      );
       let user_profile =
         get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(params.uuid))
           .await?
@@ -226,6 +246,26 @@ where
     FutureResult::new(async { rx.await? })
   }
 
+  fn receive_realtime_event(&self, json: Value) {
+    match serde_json::from_value::<RealtimeEvent>(json) {
+      Ok(event) => {
+        tracing::trace!("Realtime event: {}", event);
+        for handler in &self.realtime_event_handlers {
+          if event.table.as_str().starts_with(handler.table_name()) {
+            handler.handler_event(&event);
+          }
+        }
+      },
+      Err(e) => {
+        tracing::error!("parser realtime event error: {}", e);
+      },
+    }
+  }
+
+  fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> {
+    self.user_update_tx.as_ref().map(|tx| tx.subscribe())
+  }
+
   fn create_collab_object(
     &self,
     collab_object: &CollabObject,
@@ -384,3 +424,95 @@ async fn check_user(
   }
   Ok(())
 }
+
+pub trait RealtimeEventHandler: Send + Sync + 'static {
+  fn table_name(&self) -> &str;
+
+  fn handler_event(&self, event: &RealtimeEvent);
+}
+
+pub struct RealtimeUserHandler(pub UserUpdateSender);
+impl RealtimeEventHandler for RealtimeUserHandler {
+  fn table_name(&self) -> &str {
+    "af_user"
+  }
+
+  fn handler_event(&self, event: &RealtimeEvent) {
+    if let Ok(user_event) = serde_json::from_value::<RealtimeUserEvent>(event.new.clone()) {
+      let _ = self.0.send(UserUpdate {
+        uid: user_event.uid,
+        name: user_event.name,
+        email: user_event.email,
+        encryption_sign: user_event.encryption_sign,
+      });
+    }
+  }
+}
+
+pub struct RealtimeCollabUpdateHandler {
+  sender_by_oid: Weak<CollabUpdateSenderByOid>,
+  device_id: Arc<RwLock<String>>,
+  encryption: Weak<dyn AppFlowyEncryption>,
+}
+
+impl RealtimeCollabUpdateHandler {
+  pub fn new(
+    sender_by_oid: Weak<CollabUpdateSenderByOid>,
+    device_id: Arc<RwLock<String>>,
+    encryption: Weak<dyn AppFlowyEncryption>,
+  ) -> Self {
+    Self {
+      sender_by_oid,
+      device_id,
+      encryption,
+    }
+  }
+}
+impl RealtimeEventHandler for RealtimeCollabUpdateHandler {
+  fn table_name(&self) -> &str {
+    "af_collab_update"
+  }
+
+  fn handler_event(&self, event: &RealtimeEvent) {
+    if let Ok(collab_update) =
+      serde_json::from_value::<RealtimeCollabUpdateEvent>(event.new.clone())
+    {
+      if let Some(sender_by_oid) = self.sender_by_oid.upgrade() {
+        if let Some(sender) = sender_by_oid.read().get(collab_update.oid.as_str()) {
+          tracing::trace!(
+            "current device: {}, event device: {}",
+            self.device_id.read(),
+            collab_update.did.as_str()
+          );
+          if *self.device_id.read() != collab_update.did.as_str() {
+            let encryption_secret = self
+              .encryption
+              .upgrade()
+              .and_then(|encryption| encryption.get_secret());
+
+            tracing::trace!(
+              "Parse collab update with len: {}, encrypt: {}",
+              collab_update.value.len(),
+              collab_update.encrypt,
+            );
+
+            match SupabaseBinaryColumnDecoder::decode::<_, RealtimeBinaryColumnDecoder>(
+              collab_update.value.as_str(),
+              collab_update.encrypt,
+              &encryption_secret,
+            ) {
+              Ok(value) => {
+                if let Err(e) = sender.send(value) {
+                  tracing::debug!("send realtime update error: {}", e);
+                }
+              },
+              Err(err) => {
+                tracing::error!("decode collab update error: {}", err);
+              },
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 22 - 13
frontend/rust-lib/flowy-server/src/supabase/api/util.rs

@@ -171,7 +171,7 @@ impl SupabaseBinaryColumnDecoder {
   /// # Returns
   /// Returns an `Option` containing the decoded binary data if decoding is successful.
   /// Otherwise, returns `None`.
-  pub fn decode<T: AsRef<str>>(
+  pub fn decode<T: AsRef<str>, D: HexDecoder>(
     value: T,
     encrypt: i32,
     encryption_secret: &Option<String>,
@@ -182,7 +182,7 @@ impl SupabaseBinaryColumnDecoder {
       .ok_or(anyhow::anyhow!("Value is not start with: \\x",))?;
 
     if encrypt == 0 {
-      let bytes = hex::decode(s)?;
+      let bytes = D::decode(s)?;
       Ok(bytes)
     } else {
       match encryption_secret {
@@ -190,7 +190,7 @@ impl SupabaseBinaryColumnDecoder {
           "encryption_secret is None, but encrypt is 1"
         )),
         Some(encryption_secret) => {
-          let encrypt_data = hex::decode(s)?;
+          let encrypt_data = D::decode(s)?;
           decrypt_bytes(encrypt_data, encryption_secret)
         },
       }
@@ -198,15 +198,24 @@ impl SupabaseBinaryColumnDecoder {
   }
 }
 
-/// A decoder specifically tailored for realtime event binary columns in Supabase.
-///
-pub struct SupabaseRealtimeEventBinaryColumnDecoder;
-
-impl SupabaseRealtimeEventBinaryColumnDecoder {
-  /// The realtime event binary column string is encoded twice. So it needs to be decoded twice.
-  pub fn decode<T: AsRef<str>>(value: T) -> Option<Vec<u8>> {
-    let s = value.as_ref().strip_prefix("\\x")?;
-    let bytes = hex::decode(s).ok()?;
-    hex::decode(bytes).ok()
+pub trait HexDecoder {
+  fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error>;
+}
+
+pub struct RealtimeBinaryColumnDecoder;
+impl HexDecoder for RealtimeBinaryColumnDecoder {
+  fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error> {
+    // The realtime event binary column string is encoded twice. So it needs to be decoded twice.
+    let bytes = hex::decode(data)?;
+    let bytes = hex::decode(bytes)?;
+    Ok(bytes)
+  }
+}
+
+pub struct BinaryColumnDecoder;
+impl HexDecoder for BinaryColumnDecoder {
+  fn decode<T: AsRef<[u8]>>(data: T) -> Result<Vec<u8>, Error> {
+    let bytes = hex::decode(data)?;
+    Ok(bytes)
   }
 }

+ 16 - 39
frontend/rust-lib/flowy-server/src/supabase/entities.rs

@@ -1,11 +1,10 @@
 use std::fmt;
 use std::fmt::Display;
 
-use serde::de::{Error, Visitor};
-use serde::{Deserialize, Deserializer};
+use serde::Deserialize;
+use serde_json::Value;
 use uuid::Uuid;
 
-use crate::supabase::api::util::SupabaseRealtimeEventBinaryColumnDecoder;
 use crate::util::deserialize_null_or_default;
 
 pub enum GetUserProfileParams {
@@ -40,16 +39,14 @@ pub(crate) struct UidResponse {
 }
 
 #[derive(Debug, Deserialize)]
-pub struct RealtimeCollabUpdateEvent {
+pub struct RealtimeEvent {
   pub schema: String,
   pub table: String,
   #[serde(rename = "eventType")]
   pub event_type: String,
-  #[serde(rename = "new")]
-  pub payload: RealtimeCollabUpdate,
+  pub new: Value,
 }
-
-impl Display for RealtimeCollabUpdateEvent {
+impl Display for RealtimeEvent {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     write!(
       f,
@@ -60,43 +57,23 @@ impl Display for RealtimeCollabUpdateEvent {
 }
 
 #[derive(Debug, Deserialize)]
-pub struct RealtimeCollabUpdate {
+pub struct RealtimeCollabUpdateEvent {
   pub oid: String,
   pub uid: i64,
   pub key: i64,
   pub did: String,
-  #[serde(deserialize_with = "deserialize_value")]
-  pub value: Vec<u8>,
+  pub value: String,
   #[serde(default)]
   pub encrypt: i32,
 }
 
-pub fn deserialize_value<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
-where
-  D: Deserializer<'de>,
-{
-  struct ValueVisitor();
-
-  impl<'de> Visitor<'de> for ValueVisitor {
-    type Value = Vec<u8>;
-
-    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-      formatter.write_str("Expect NodeBody")
-    }
-
-    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
-    where
-      E: Error,
-    {
-      Ok(SupabaseRealtimeEventBinaryColumnDecoder::decode(v).unwrap_or_default())
-    }
-
-    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
-    where
-      E: Error,
-    {
-      Ok(SupabaseRealtimeEventBinaryColumnDecoder::decode(v).unwrap_or_default())
-    }
-  }
-  deserializer.deserialize_any(ValueVisitor())
+#[derive(Debug, Deserialize)]
+pub struct RealtimeUserEvent {
+  pub uid: i64,
+  #[serde(deserialize_with = "deserialize_null_or_default")]
+  pub name: String,
+  #[serde(deserialize_with = "deserialize_null_or_default")]
+  pub email: String,
+  #[serde(deserialize_with = "deserialize_null_or_default")]
+  pub encryption_sign: String,
 }

+ 32 - 60
frontend/rust-lib/flowy-server/src/supabase/server.rs

@@ -2,22 +2,19 @@ use std::collections::HashMap;
 use std::sync::{Arc, Weak};
 
 use collab_plugins::cloud_storage::{CollabObject, RemoteCollabStorage, RemoteUpdateSender};
-use parking_lot::{Mutex, RwLock};
-use serde_json::Value;
+use parking_lot::RwLock;
 
 use flowy_database_deps::cloud::DatabaseCloudService;
 use flowy_document_deps::cloud::DocumentCloudService;
-use flowy_encrypt::decrypt_bytes;
 use flowy_folder_deps::cloud::FolderCloudService;
 use flowy_server_config::supabase_config::SupabaseConfiguration;
 use flowy_user_deps::cloud::UserService;
 
 use crate::supabase::api::{
-  RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl,
-  SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl,
-  SupabaseUserServiceImpl,
+  RESTfulPostgresServer, RealtimeCollabUpdateHandler, RealtimeEventHandler, RealtimeUserHandler,
+  SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl,
+  SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl,
 };
-use crate::supabase::entities::RealtimeCollabUpdateEvent;
 use crate::{AppFlowyEncryption, AppFlowyServer};
 
 /// https://www.pgbouncer.org/features.html
@@ -53,14 +50,16 @@ impl PgPoolMode {
     matches!(self, PgPoolMode::Session)
   }
 }
+
+pub type CollabUpdateSenderByOid = RwLock<HashMap<String, RemoteUpdateSender>>;
 /// Supabase server is used to provide the implementation of the [AppFlowyServer] trait.
 /// It contains the configuration of the supabase server and the postgres server.
 pub struct SupabaseServer {
   #[allow(dead_code)]
   config: SupabaseConfiguration,
   /// did represents as the device id is used to identify the device that is currently using the app.
-  did: Mutex<String>,
-  update_tx: RwLock<HashMap<String, RemoteUpdateSender>>,
+  device_id: Arc<RwLock<String>>,
+  collab_update_sender: Arc<CollabUpdateSenderByOid>,
   restful_postgres: Arc<RwLock<Option<Arc<RESTfulPostgresServer>>>>,
   encryption: Weak<dyn AppFlowyEncryption>,
 }
@@ -69,9 +68,10 @@ impl SupabaseServer {
   pub fn new(
     config: SupabaseConfiguration,
     enable_sync: bool,
+    device_id: Arc<RwLock<String>>,
     encryption: Weak<dyn AppFlowyEncryption>,
   ) -> Self {
-    let update_tx = RwLock::new(HashMap::new());
+    let collab_update_sender = Default::default();
     let restful_postgres = if enable_sync {
       Some(Arc::new(RESTfulPostgresServer::new(
         config.clone(),
@@ -82,8 +82,8 @@ impl SupabaseServer {
     };
     Self {
       config,
-      did: Default::default(),
-      update_tx,
+      device_id,
+      collab_update_sender,
       restful_postgres: Arc::new(RwLock::new(restful_postgres)),
       encryption,
     }
@@ -108,14 +108,25 @@ impl AppFlowyServer for SupabaseServer {
     self.set_enable_sync(enable);
   }
 
-  fn set_sync_device_id(&self, device_id: &str) {
-    *self.did.lock() = device_id.to_string();
-  }
-
   fn user_service(&self) -> Arc<dyn UserService> {
-    Arc::new(SupabaseUserServiceImpl::new(SupabaseServerServiceImpl(
-      self.restful_postgres.clone(),
-    )))
+    // handle the realtime collab update event.
+    let (user_update_tx, _) = tokio::sync::broadcast::channel(100);
+
+    let collab_update_handler = Box::new(RealtimeCollabUpdateHandler::new(
+      Arc::downgrade(&self.collab_update_sender),
+      self.device_id.clone(),
+      self.encryption.clone(),
+    ));
+
+    // handle the realtime user event.
+    let user_handler = Box::new(RealtimeUserHandler(user_update_tx.clone()));
+
+    let handlers: Vec<Box<dyn RealtimeEventHandler>> = vec![collab_update_handler, user_handler];
+    Arc::new(SupabaseUserServiceImpl::new(
+      SupabaseServerServiceImpl(self.restful_postgres.clone()),
+      handlers,
+      Some(user_update_tx),
+    ))
   }
 
   fn folder_service(&self) -> Arc<dyn FolderCloudService> {
@@ -139,53 +150,14 @@ impl AppFlowyServer for SupabaseServer {
   fn collab_storage(&self, collab_object: &CollabObject) -> Option<Arc<dyn RemoteCollabStorage>> {
     let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
     self
-      .update_tx
+      .collab_update_sender
       .write()
       .insert(collab_object.object_id.clone(), tx);
+
     Some(Arc::new(SupabaseCollabStorageImpl::new(
       SupabaseServerServiceImpl(self.restful_postgres.clone()),
       Some(rx),
       self.encryption.clone(),
     )))
   }
-
-  fn handle_realtime_event(&self, json: Value) {
-    match serde_json::from_value::<RealtimeCollabUpdateEvent>(json) {
-      Ok(event) => {
-        if let Some(tx) = self.update_tx.read().get(event.payload.oid.as_str()) {
-          tracing::trace!(
-            "current device: {}, event device: {}",
-            self.did.lock().as_str(),
-            event.payload.did.as_str()
-          );
-
-          if self.did.lock().as_str() != event.payload.did.as_str() {
-            tracing::trace!("Did receive realtime event: {}", event);
-            let value = if event.payload.encrypt == 1 {
-              match self
-                .encryption
-                .upgrade()
-                .and_then(|encryption| encryption.get_secret())
-              {
-                None => vec![],
-                Some(secret) => decrypt_bytes(event.payload.value, &secret).unwrap_or_default(),
-              }
-            } else {
-              event.payload.value
-            };
-
-            if !value.is_empty() {
-              tracing::trace!("Parse payload with len: {} success", value.len());
-              if let Err(e) = tx.send(value) {
-                tracing::trace!("send realtime update error: {}", e);
-              }
-            }
-          }
-        }
-      },
-      Err(e) => {
-        tracing::error!("parser realtime event error: {}", e);
-      },
-    }
-  }
 }

+ 1 - 1
frontend/rust-lib/flowy-server/tests/supabase_test/util.rs

@@ -48,7 +48,7 @@ pub fn database_service() -> Arc<dyn DatabaseCloudService> {
 
 pub fn user_auth_service() -> Arc<dyn UserService> {
   let (server, _encryption_impl) = appflowy_server(None);
-  Arc::new(SupabaseUserServiceImpl::new(server))
+  Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
 }
 
 pub fn folder_service() -> Arc<dyn FolderCloudService> {

+ 1 - 1
frontend/rust-lib/flowy-test/tests/util.rs

@@ -112,7 +112,7 @@ pub fn database_service() -> Arc<dyn DatabaseCloudService> {
 
 pub fn user_auth_service() -> Arc<dyn UserService> {
   let (server, _encryption_impl) = appflowy_server(None);
-  Arc::new(SupabaseUserServiceImpl::new(server))
+  Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
 }
 
 pub fn folder_service() -> Arc<dyn FolderCloudService> {

+ 1 - 0
frontend/rust-lib/flowy-user-deps/Cargo.toml

@@ -15,3 +15,4 @@ serde_json = {version = "1.0"}
 serde_repr = "0.1"
 chrono = { version = "0.4.22", default-features = false, features = ["clock"] }
 anyhow = "1.0.71"
+tokio = { version = "1.26", features = ["sync"] }

+ 17 - 0
frontend/rust-lib/flowy-user-deps/src/cloud.rs

@@ -5,6 +5,7 @@ use std::str::FromStr;
 use anyhow::Error;
 use collab_define::CollabObject;
 use serde::{Deserialize, Serialize};
+use serde_json::Value;
 use uuid::Uuid;
 
 use flowy_error::{ErrorCode, FlowyError};
@@ -103,6 +104,12 @@ pub trait UserService: Send + Sync {
 
   fn get_user_awareness_updates(&self, uid: i64) -> FutureResult<Vec<Vec<u8>>, Error>;
 
+  fn receive_realtime_event(&self, _json: Value) {}
+
+  fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> {
+    None
+  }
+
   fn create_collab_object(
     &self,
     collab_object: &CollabObject,
@@ -110,6 +117,16 @@ pub trait UserService: Send + Sync {
   ) -> FutureResult<(), Error>;
 }
 
+pub type UserUpdateReceiver = tokio::sync::broadcast::Receiver<UserUpdate>;
+pub type UserUpdateSender = tokio::sync::broadcast::Sender<UserUpdate>;
+#[derive(Debug, Clone)]
+pub struct UserUpdate {
+  pub uid: i64,
+  pub name: String,
+  pub email: String,
+  pub encryption_sign: String,
+}
+
 pub fn third_party_params_from_box_any(any: BoxAny) -> Result<ThirdPartyParams, Error> {
   let map: HashMap<String, String> = any.unbox_or_error()?;
   let uuid = uuid_from_map(&map)?;

+ 7 - 2
frontend/rust-lib/flowy-user-deps/src/entities.rs

@@ -211,15 +211,20 @@ impl EncryptionType {
       EncryptionType::SelfEncryption(sign.to_owned())
     }
   }
-}
 
-impl EncryptionType {
   pub fn is_need_encrypt_secret(&self) -> bool {
     match self {
       EncryptionType::NoEncryption => false,
       EncryptionType::SelfEncryption(sign) => !sign.is_empty(),
     }
   }
+
+  pub fn sign(&self) -> String {
+    match self {
+      EncryptionType::NoEncryption => "".to_owned(),
+      EncryptionType::SelfEncryption(sign) => sign.to_owned(),
+    }
+  }
 }
 
 impl FromStr for EncryptionType {

+ 21 - 0
frontend/rust-lib/flowy-user/src/entities/auth.rs

@@ -154,3 +154,24 @@ pub struct UserStatePB {
   #[pb(index = 1)]
   pub auth_type: AuthTypePB,
 }
+
+#[derive(ProtoBuf, Debug, Default, Clone)]
+pub struct AuthStateChangedPB {
+  #[pb(index = 1)]
+  pub state: AuthStatePB,
+}
+
+#[derive(ProtoBuf_Enum, Debug, Clone)]
+pub enum AuthStatePB {
+  // adding AuthState prefix to avoid conflict with other enums
+  AuthStateUnknown = 0,
+  AuthStateSignIn = 1,
+  AuthStateSignOut = 2,
+  AuthStateForceSignOut = 3,
+}
+
+impl Default for AuthStatePB {
+  fn default() -> Self {
+    Self::AuthStateUnknown
+  }
+}

+ 4 - 3
frontend/rust-lib/flowy-user/src/event_handler.rs

@@ -95,8 +95,9 @@ pub async fn get_user_profile_handler(
 ) -> DataResult<UserProfilePB, FlowyError> {
   let manager = upgrade_manager(manager)?;
   let uid = manager.get_session()?.user_id;
-  let user_profile: UserProfilePB = manager.get_user_profile(uid, true).await?.into();
-  data_result_ok(user_profile)
+  let user_profile = manager.get_user_profile(uid).await?;
+  let _ = manager.refresh_user_profile(&user_profile).await;
+  data_result_ok(user_profile.into())
 }
 
 #[tracing::instrument(level = "debug", skip(manager))]
@@ -222,7 +223,7 @@ pub async fn check_encrypt_secret_handler(
 ) -> DataResult<UserEncryptionSecretCheckPB, FlowyError> {
   let manager = upgrade_manager(manager)?;
   let uid = manager.get_session()?.user_id;
-  let profile = manager.get_user_profile(uid, false).await?;
+  let profile = manager.get_user_profile(uid).await?;
 
   let is_need_secret = match profile.encryption_type {
     EncryptionType::NoEncryption => false,

+ 0 - 2
frontend/rust-lib/flowy-user/src/event_map.rs

@@ -1,7 +1,6 @@
 use std::sync::{Arc, Weak};
 
 use collab_folder::core::FolderData;
-use serde_json::Value;
 use strum_macros::Display;
 
 use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
@@ -97,7 +96,6 @@ pub trait UserStatusCallback: Send + Sync + 'static {
   fn did_expired(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>>;
   fn open_workspace(&self, user_id: i64, user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>>;
   fn did_update_network(&self, _reachable: bool) {}
-  fn receive_realtime_event(&self, _json: Value) {}
 }
 
 /// The user cloud service provider.

+ 150 - 86
frontend/rust-lib/flowy-user/src/manager.rs

@@ -14,10 +14,11 @@ use flowy_sqlite::kv::StorePreferences;
 use flowy_sqlite::schema::user_table;
 use flowy_sqlite::ConnectionPool;
 use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
+use flowy_user_deps::cloud::UserUpdate;
 use flowy_user_deps::entities::*;
 use lib_infra::box_any::BoxAny;
 
-use crate::entities::{UserProfilePB, UserSettingPB};
+use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB};
 use crate::event_map::{
   DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserStatusCallback,
 };
@@ -61,6 +62,7 @@ pub struct UserManager {
   pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
   pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
   resumable_sign_up: Mutex<Option<ResumableSignUp>>,
+  current_session: parking_lot::RwLock<Option<Session>>,
 }
 
 impl UserManager {
@@ -69,11 +71,12 @@ impl UserManager {
     cloud_services: Arc<dyn UserCloudServiceProvider>,
     store_preferences: Arc<StorePreferences>,
     collab_builder: Weak<AppFlowyCollabBuilder>,
-  ) -> Self {
+  ) -> Arc<Self> {
     let database = UserDB::new(&session_config.root_dir);
     let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
       RwLock::new(Arc::new(DefaultUserStatusCallback));
-    Self {
+
+    let user_manager = Arc::new(Self {
       database,
       session_config,
       cloud_services,
@@ -82,7 +85,25 @@ impl UserManager {
       user_status_callback,
       collab_builder,
       resumable_sign_up: Default::default(),
+      current_session: Default::default(),
+    });
+
+    let weak_user_manager = Arc::downgrade(&user_manager);
+    if let Ok(user_service) = user_manager.cloud_services.get_user_service() {
+      if let Some(mut rx) = user_service.subscribe_user_update() {
+        tokio::spawn(async move {
+          while let Ok(update) = rx.recv().await {
+            if let Some(user_manager) = weak_user_manager.upgrade() {
+              if let Err(err) = user_manager.handler_user_update(update).await {
+                tracing::error!("handler_user_update failed: {:?}", err);
+              }
+            }
+          }
+        });
+      }
     }
+
+    user_manager
   }
 
   pub fn get_store_preferences(&self) -> Weak<StorePreferences> {
@@ -121,6 +142,7 @@ impl UserManager {
       self
         .initialize_user_awareness(&session, UserAwarenessDataSource::Local)
         .await;
+
       let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
       if let Err(e) = user_status_callback
         .did_init(
@@ -191,9 +213,10 @@ impl UserManager {
     {
       tracing::error!("Failed to call did_sign_in callback: {:?}", e);
     }
-    send_sign_in_notification()
-      .payload::<UserProfilePB>(user_profile.clone().into())
-      .send();
+    send_auth_state_notification(AuthStateChangedPB {
+      state: AuthStatePB::AuthStateSignIn,
+    })
+    .send();
     Ok(user_profile)
   }
 
@@ -322,6 +345,11 @@ impl UserManager {
     self
       .save_auth_data(&response, auth_type, &new_session)
       .await?;
+
+    send_auth_state_notification(AuthStateChangedPB {
+      state: AuthStatePB::AuthStateSignIn,
+    })
+    .send();
     Ok(())
   }
 
@@ -329,7 +357,7 @@ impl UserManager {
   pub async fn sign_out(&self) -> Result<(), FlowyError> {
     let session = self.get_session()?;
     self.database.close(session.user_id)?;
-    self.set_current_session(None)?;
+    self.set_session(None)?;
 
     let server = self.cloud_services.get_user_service()?;
     tokio::spawn(async move {
@@ -352,27 +380,10 @@ impl UserManager {
     &self,
     params: UpdateUserProfileParams,
   ) -> Result<(), FlowyError> {
-    let old_user_profile = self.get_user_profile(params.uid, false).await?;
-    let auth_type = old_user_profile.auth_type.clone();
-    let session = self.get_session()?;
     let changeset = UserTableChangeset::new(params.clone());
-    diesel_update_table!(
-      user_table,
-      changeset,
-      &*self.db_connection(session.user_id)?
-    );
-
     let session = self.get_session()?;
-    let new_user_profile = self.get_user_profile(session.user_id, false).await?;
-    send_notification(
-      &session.user_id.to_string(),
-      UserNotification::DidUpdateUserProfile,
-    )
-    .payload(UserProfilePB::from(new_user_profile))
-    .send();
-    self
-      .update_user(&auth_type, session.user_id, None, params)
-      .await?;
+    save_user_profile_change(session.user_id, self.db_pool(session.user_id)?, changeset)?;
+    self.update_user(session.user_id, None, params).await?;
     Ok(())
   }
 
@@ -396,44 +407,38 @@ impl UserManager {
   }
 
   /// Fetches the user profile for the given user ID.
-  ///
-  /// This function retrieves the user profile from the local database. If the `refresh` flag is set to `true`,
-  /// it also attempts to update the user profile from a cloud service, and then sends a notification about the
-  /// profile update.
-  pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result<UserProfile, FlowyError> {
-    let user_id = uid.to_string();
-    let user = user_table::dsl::user_table
-      .filter(user_table::id.eq(&user_id))
-      .first::<UserTable>(&*(self.db_connection(uid)?))?;
-
-    if refresh {
-      let weak_auth_service = Arc::downgrade(&self.cloud_services.get_user_service()?);
-      let weak_pool = Arc::downgrade(&self.database.get_pool(uid)?);
-      tokio::spawn(async move {
-        if let (Some(auth_service), Some(pool)) = (weak_auth_service.upgrade(), weak_pool.upgrade())
-        {
-          if let Ok(Some(user_profile)) = auth_service
-            .get_user_profile(UserCredentials::from_uid(uid))
-            .await
-          {
-            let changeset = UserTableChangeset::from_user_profile(user_profile.clone());
-            if let Ok(conn) = pool.get() {
-              let filter =
-                user_table::dsl::user_table.filter(user_table::dsl::id.eq(changeset.id.clone()));
-              let _ = diesel::update(filter).set(changeset).execute(&*conn);
-
-              // Send notification to the client
-              let user_profile_pb: UserProfilePB = user_profile.into();
-              send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
-                .payload(user_profile_pb)
-                .send();
-            }
-          }
-        }
-      });
+  pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> {
+    let user: UserProfile = user_table::dsl::user_table
+      .filter(user_table::id.eq(&uid.to_string()))
+      .first::<UserTable>(&*(self.db_connection(uid)?))?
+      .into();
+
+    Ok(user)
+  }
+
+  #[tracing::instrument(level = "info", skip_all)]
+  pub async fn refresh_user_profile(
+    &self,
+    old_user_profile: &UserProfile,
+  ) -> FlowyResult<UserProfile> {
+    let uid = old_user_profile.uid;
+    let new_user_profile: UserProfile = self
+      .cloud_services
+      .get_user_service()?
+      .get_user_profile(UserCredentials::from_uid(uid))
+      .await?
+      .ok_or_else(|| FlowyError::new(ErrorCode::RecordNotFound, "User not found"))?;
+
+    if !is_user_encryption_sign_valid(old_user_profile, &new_user_profile.encryption_type.sign()) {
+      return Err(FlowyError::new(
+        ErrorCode::InvalidEncryptSecret,
+        "Invalid encryption sign",
+      ));
     }
 
-    Ok(user.into())
+    let changeset = UserTableChangeset::from_user_profile(new_user_profile.clone());
+    let _ = save_user_profile_change(uid, self.database.get_pool(uid)?, changeset);
+    Ok(new_user_profile)
   }
 
   pub fn user_dir(&self, uid: i64) -> String {
@@ -458,7 +463,6 @@ impl UserManager {
 
   async fn update_user(
     &self,
-    _auth_type: &AuthType,
     uid: i64,
     token: Option<String>,
     params: UpdateUserProfileParams,
@@ -490,32 +494,18 @@ impl UserManager {
     Ok(())
   }
 
-  pub(crate) fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
-    tracing::debug!("Set current user: {:?}", session);
-    match &session {
-      None => self
-        .store_preferences
-        .remove(&self.session_config.session_cache_key),
-      Some(session) => {
-        self
-          .store_preferences
-          .set_object(&self.session_config.session_cache_key, session.clone())
-          .map_err(internal_error)?;
-      },
-    }
-    Ok(())
-  }
-
   pub async fn receive_realtime_event(&self, json: Value) {
-    self
-      .user_status_callback
-      .read()
-      .await
-      .receive_realtime_event(json);
+    if let Ok(user_service) = self.cloud_services.get_user_service() {
+      user_service.receive_realtime_event(json)
+    }
   }
 
   /// Returns the current user session.
   pub fn get_session(&self) -> Result<Session, FlowyError> {
+    if let Some(session) = (self.current_session.read()).clone() {
+      return Ok(session);
+    }
+
     match self
       .store_preferences
       .get_object::<Session>(&self.session_config.session_cache_key)
@@ -524,10 +514,33 @@ impl UserManager {
         ErrorCode::RecordNotFound,
         "User is not logged in",
       )),
-      Some(session) => Ok(session),
+      Some(session) => {
+        self.current_session.write().replace(session.clone());
+        Ok(session)
+      },
     }
   }
 
+  pub(crate) fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
+    tracing::debug!("Set current user: {:?}", session);
+    match &session {
+      None => {
+        self.current_session.write().take();
+        self
+          .store_preferences
+          .remove(&self.session_config.session_cache_key)
+      },
+      Some(session) => {
+        self.current_session.write().replace(session.clone());
+        self
+          .store_preferences
+          .set_object(&self.session_config.session_cache_key, session.clone())
+          .map_err(internal_error)?;
+      },
+    }
+    Ok(())
+  }
+
   async fn save_auth_data(
     &self,
     response: &impl UserAuthResponse,
@@ -547,7 +560,7 @@ impl UserManager {
     self
       .save_user(uid, (user_profile, auth_type.clone()).into())
       .await?;
-    self.set_current_session(Some(session.clone()))?;
+    self.set_session(Some(session.clone()))?;
     Ok(())
   }
 
@@ -558,6 +571,27 @@ impl UserManager {
     self.cloud_services.set_device_id(&session.device_id);
   }
 
+  async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> {
+    let session = self.get_session()?;
+    if session.user_id == user_update.uid {
+      tracing::debug!("Receive user update: {:?}", user_update);
+      let user_profile = self.get_user_profile(user_update.uid).await?;
+
+      if !is_user_encryption_sign_valid(&user_profile, &user_update.encryption_sign) {
+        return Ok(());
+      }
+
+      // Save the user profile change
+      save_user_profile_change(
+        user_update.uid,
+        self.db_pool(user_update.uid)?,
+        UserTableChangeset::from(user_update),
+      )?;
+    }
+
+    Ok(())
+  }
+
   async fn migrate_local_user_to_cloud(
     &self,
     old_user: &MigrationUser,
@@ -575,3 +609,33 @@ impl UserManager {
     Ok(folder_data)
   }
 }
+
+fn is_user_encryption_sign_valid(user_profile: &UserProfile, encryption_sign: &str) -> bool {
+  // If the local user profile's encryption sign is not equal to the user update's encryption sign,
+  // which means the user enable encryption in another device, we should logout the current user.
+  let is_valid = user_profile.encryption_type.sign() == encryption_sign;
+  if !is_valid {
+    send_auth_state_notification(AuthStateChangedPB {
+      state: AuthStatePB::AuthStateForceSignOut,
+    })
+    .send();
+  }
+  is_valid
+}
+
+fn save_user_profile_change(
+  uid: i64,
+  pool: Arc<ConnectionPool>,
+  changeset: UserTableChangeset,
+) -> FlowyResult<()> {
+  let conn = pool.get()?;
+  diesel_update_table!(user_table, changeset, &*conn);
+  let user: UserProfile = user_table::dsl::user_table
+    .filter(user_table::id.eq(&uid.to_string()))
+    .first::<UserTable>(&*conn)?
+    .into();
+  send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
+    .payload(UserProfilePB::from(user))
+    .send();
+  Ok(())
+}

+ 10 - 3
frontend/rust-lib/flowy-user/src/notification.rs

@@ -1,13 +1,15 @@
 use flowy_derive::ProtoBuf_Enum;
 use flowy_notification::NotificationBuilder;
 
+use crate::entities::AuthStateChangedPB;
+
 const USER_OBSERVABLE_SOURCE: &str = "User";
 
 #[derive(ProtoBuf_Enum, Debug, Default)]
 pub(crate) enum UserNotification {
   #[default]
   Unknown = 0,
-  DidUserSignIn = 1,
+  UserAuthStateChanged = 1,
   DidUpdateUserProfile = 2,
   DidUpdateUserWorkspaces = 3,
   DidUpdateCloudConfig = 4,
@@ -23,6 +25,11 @@ pub(crate) fn send_notification(id: &str, ty: UserNotification) -> NotificationB
   NotificationBuilder::new(id, ty, USER_OBSERVABLE_SOURCE)
 }
 
-pub(crate) fn send_sign_in_notification() -> NotificationBuilder {
-  NotificationBuilder::new("", UserNotification::DidUserSignIn, USER_OBSERVABLE_SOURCE)
+pub(crate) fn send_auth_state_notification(payload: AuthStateChangedPB) -> NotificationBuilder {
+  NotificationBuilder::new(
+    "auth_state_change_notification",
+    UserNotification::UserAuthStateChanged,
+    USER_OBSERVABLE_SOURCE,
+  )
+  .payload(payload)
 }

+ 2 - 2
frontend/rust-lib/flowy-user/src/services/historical_user.rs

@@ -17,7 +17,7 @@ impl UserManager {
     // Only migrate the data if the user is login in as a guest and sign up as a new user if the current
     // auth type is not [AuthType::Local].
     let session = self.get_session().ok()?;
-    let user_profile = self.get_user_profile(session.user_id, false).await.ok()?;
+    let user_profile = self.get_user_profile(session.user_id).await.ok()?;
     if user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
       Some(MigrationUser {
         user_profile,
@@ -100,7 +100,7 @@ impl UserManager {
       device_id,
       user_workspace,
     };
-    self.set_current_session(Some(session))?;
+    self.set_session(Some(session))?;
     Ok(())
   }
 }

+ 12 - 0
frontend/rust-lib/flowy-user/src/services/user_sql.rs

@@ -1,6 +1,7 @@
 use std::str::FromStr;
 
 use flowy_sqlite::schema::user_table;
+use flowy_user_deps::cloud::UserUpdate;
 use flowy_user_deps::entities::*;
 
 /// The order of the fields in the struct must be the same as the order of the fields in the table.
@@ -102,3 +103,14 @@ impl UserTableChangeset {
     }
   }
 }
+
+impl From<UserUpdate> for UserTableChangeset {
+  fn from(value: UserUpdate) -> Self {
+    UserTableChangeset {
+      id: value.uid.to_string(),
+      name: Some(value.name),
+      email: Some(value.email),
+      ..Default::default()
+    }
+  }
+}