Ver código fonte

fix: push to sign in screen when logout (#3127)

* fix: push to sign in screen when logout

* chore: show historical login users

* chore: open historical user

* chore: show historical user

* chore: reload app widget with unique key

* chore: add tooltip for user history
Nathan.fooo 1 ano atrás
pai
commit
3c04b72932
35 arquivos alterados com 528 adições e 123 exclusões
  1. 1 0
      frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart
  2. 19 4
      frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart
  3. 18 0
      frontend/appflowy_flutter/lib/user/application/user_service.dart
  4. 13 0
      frontend/appflowy_flutter/lib/user/presentation/router.dart
  5. 3 1
      frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart
  6. 14 15
      frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart
  7. 1 1
      frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart
  8. 29 2
      frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart
  9. 3 1
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart
  10. 13 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart
  11. 6 3
      frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart
  12. 110 0
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart
  13. 0 30
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart
  14. 12 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart
  15. 2 2
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart
  16. 31 9
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  17. 36 0
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart
  18. 2 1
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart
  19. 9 3
      frontend/resources/translations/en.json
  20. 19 4
      frontend/rust-lib/flowy-core/src/integrate/server.rs
  21. 1 1
      frontend/rust-lib/flowy-core/src/lib.rs
  22. 2 2
      frontend/rust-lib/flowy-error/src/code.rs
  23. 7 1
      frontend/rust-lib/flowy-server/src/local_server/impls/user.rs
  24. 8 5
      frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs
  25. 2 2
      frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs
  26. 10 2
      frontend/rust-lib/flowy-server/src/supabase/api/user.rs
  27. 1 0
      frontend/rust-lib/flowy-user-deps/src/entities.rs
  28. 2 0
      frontend/rust-lib/flowy-user-deps/src/lib.rs
  29. 44 0
      frontend/rust-lib/flowy-user/src/entities/user_profile.rs
  30. 20 0
      frontend/rust-lib/flowy-user/src/event_handler.rs
  31. 13 0
      frontend/rust-lib/flowy-user/src/event_map.rs
  32. 10 1
      frontend/rust-lib/flowy-user/src/services/session_serde.rs
  33. 50 24
      frontend/rust-lib/flowy-user/src/services/user_session.rs
  34. 16 5
      frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs
  35. 1 1
      frontend/scripts/makefile/desktop.toml

+ 1 - 0
frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart

@@ -89,6 +89,7 @@ class ApplicationWidget extends StatelessWidget {
       ],
       child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
         builder: (context, state) => MaterialApp(
+          key: UniqueKey(),
           builder: overlayManagerBuilder(),
           debugShowCheckedModeBanner: false,
           theme: state.lightTheme,

+ 19 - 4
frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart

@@ -215,6 +215,22 @@ extension on String {
   }
 }
 
+/// Creates a completer that listens to Supabase authentication state changes and
+/// completes when a user signs in.
+///
+/// This function sets up a listener on Supabase's authentication state. When a user
+/// signs in, it triggers the provided [onSuccess] callback with the user's `id` and
+/// `email`. Once the [onSuccess] callback is executed and a response is received,
+/// the completer completes with the response, and the listener is canceled.
+///
+/// Parameters:
+/// - [onSuccess]: A callback function that's executed when a user signs in. It
+///   should take in a user's `id` and `email` and return a `Future` containing either
+///   a `FlowyError` or a `UserProfilePB`.
+///
+/// Returns:
+/// A completer of type `Either<FlowyError, UserProfilePB>`. This completer completes
+/// with the response from the [onSuccess] callback when a user signs in.
 Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
   required Future<Either<FlowyError, UserProfilePB>> Function(
     String userId,
@@ -227,16 +243,15 @@ Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
 
   subscription = auth.onAuthStateChange.listen((event) async {
     final user = event.session?.user;
-    if (event.event != AuthChangeEvent.signedIn || user == null) {
-      completer.complete(left(AuthError.supabaseSignInWithOauthError));
-    } else {
+    if (event.event == AuthChangeEvent.signedIn && user != null) {
       final response = await onSuccess(
         user.id,
         user.email ?? user.newEmail ?? '',
       );
+      // Only cancel the subscription if the Event is signedIn.
+      subscription.cancel();
       completer.complete(response);
     }
-    subscription.cancel();
   });
   return completer;
 }

+ 18 - 0
frontend/appflowy_flutter/lib/user/application/user_service.dart

@@ -70,6 +70,24 @@ class UserBackendService {
     return UserEventInitUser().send();
   }
 
+  Future<Either<List<HistoricalUserPB>, FlowyError>>
+      loadHistoricalUsers() async {
+    return UserEventGetHistoricalUsers().send().then(
+      (result) {
+        return result.fold(
+          (historicalUsers) => left(historicalUsers.items),
+          (error) => right(error),
+        );
+      },
+    );
+  }
+
+  Future<Either<Unit, FlowyError>> openHistoricalUser(
+    HistoricalUserPB user,
+  ) async {
+    return UserEventOpenHistoricalUser(user).send();
+  }
+
   Future<Either<List<WorkspacePB>, FlowyError>> getWorkspaces() {
     final request = WorkspaceIdPB.create();
 

+ 13 - 0
frontend/appflowy_flutter/lib/user/presentation/router.dart

@@ -13,6 +13,13 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
 import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
 import 'package:flutter/material.dart';
 
+const routerNameRoot = '/';
+const routerNameSignUp = '/signUp';
+const routerNameSignIn = '/signIn';
+const routerNameSkipLogIn = '/skipLogIn';
+const routerNameWelcome = '/welcome';
+const routerNameHome = '/home';
+
 class AuthRouter {
   void pushForgetPasswordScreen(BuildContext context) {}
 
@@ -24,6 +31,7 @@ class AuthRouter {
     Navigator.of(context).push(
       PageRoutes.fade(
         () => SignUpScreen(router: getIt<AuthRouter>()),
+        const RouteSettings(name: routerNameSignUp),
       ),
     );
   }
@@ -41,6 +49,7 @@ class AuthRouter {
           workspaceSetting,
           key: ValueKey(profile.id),
         ),
+        const RouteSettings(name: routerNameHome),
         RouteDurations.slow.inMilliseconds * .001,
       ),
     );
@@ -71,6 +80,7 @@ class SplashRoute {
     await Navigator.of(context).push(
       PageRoutes.fade(
         () => screen,
+        const RouteSettings(name: routerNameWelcome),
         RouteDurations.slow.inMilliseconds * .001,
       ),
     );
@@ -97,6 +107,7 @@ class SplashRoute {
           workspaceSetting,
           key: ValueKey(userProfile.id),
         ),
+        const RouteSettings(name: routerNameWelcome),
         RouteDurations.slow.inMilliseconds * .001,
       ),
     );
@@ -107,6 +118,7 @@ class SplashRoute {
       context,
       PageRoutes.fade(
         () => SignInScreen(router: getIt<AuthRouter>()),
+        const RouteSettings(name: routerNameSignIn),
         RouteDurations.slow.inMilliseconds * .001,
       ),
     );
@@ -120,6 +132,7 @@ class SplashRoute {
           router: getIt<AuthRouter>(),
           authService: getIt<AuthService>(),
         ),
+        const RouteSettings(name: routerNameSkipLogIn),
         RouteDurations.slow.inMilliseconds * .001,
       ),
     );

+ 3 - 1
frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart

@@ -320,14 +320,16 @@ class ThirdPartySignInButton extends StatelessWidget {
 }
 
 class ThirdPartySignInButtons extends StatelessWidget {
+  final MainAxisAlignment mainAxisAlignment;
   const ThirdPartySignInButtons({
+    this.mainAxisAlignment = MainAxisAlignment.center,
     super.key,
   });
 
   @override
   Widget build(BuildContext context) {
     return Row(
-      mainAxisAlignment: MainAxisAlignment.center,
+      mainAxisAlignment: mainAxisAlignment,
       children: [
         ThirdPartySignInButton(
           icon: 'login/google-mark',

+ 14 - 15
frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart

@@ -8,10 +8,9 @@ import 'package:protobuf/protobuf.dart';
 
 part 'setting_supabase_bloc.freezed.dart';
 
-class SettingSupabaseBloc
-    extends Bloc<SettingSupabaseEvent, SettingSupabaseState> {
-  SettingSupabaseBloc() : super(SettingSupabaseState.initial()) {
-    on<SettingSupabaseEvent>((event, emit) async {
+class SyncSettingBloc extends Bloc<SyncSettingEvent, SyncSettingState> {
+  SyncSettingBloc() : super(SyncSettingState.initial()) {
+    on<SyncSettingEvent>((event, emit) async {
       await event.when(
         initial: () async {
           await getSupabaseConfig();
@@ -27,7 +26,7 @@ class SettingSupabaseBloc
             emit(state.copyWith(config: newConfig));
           }
         },
-        didReceiveSupabseConfig: (SupabaseConfigPB config) {
+        didReceiveSyncConfig: (SupabaseConfigPB config) {
           emit(state.copyWith(config: config));
         },
       );
@@ -43,7 +42,7 @@ class SettingSupabaseBloc
     result.fold(
       (config) {
         if (!isClosed) {
-          add(SettingSupabaseEvent.didReceiveSupabseConfig(config));
+          add(SyncSettingEvent.didReceiveSyncConfig(config));
         }
       },
       (r) => Log.error(r),
@@ -52,22 +51,22 @@ class SettingSupabaseBloc
 }
 
 @freezed
-class SettingSupabaseEvent with _$SettingSupabaseEvent {
-  const factory SettingSupabaseEvent.initial() = _Initial;
-  const factory SettingSupabaseEvent.didReceiveSupabseConfig(
+class SyncSettingEvent with _$SyncSettingEvent {
+  const factory SyncSettingEvent.initial() = _Initial;
+  const factory SyncSettingEvent.didReceiveSyncConfig(
     SupabaseConfigPB config,
-  ) = _DidReceiveSupabaseConfig;
-  const factory SettingSupabaseEvent.enableSync(bool enable) = _EnableSync;
+  ) = _DidSyncSupabaseConfig;
+  const factory SyncSettingEvent.enableSync(bool enable) = _EnableSync;
 }
 
 @freezed
-class SettingSupabaseState with _$SettingSupabaseState {
-  const factory SettingSupabaseState({
+class SyncSettingState with _$SyncSettingState {
+  const factory SyncSettingState({
     SupabaseConfigPB? config,
     required Either<Unit, String> successOrFailure,
-  }) = _SettingSupabaseState;
+  }) = _SyncSettingState;
 
-  factory SettingSupabaseState.initial() => SettingSupabaseState(
+  factory SyncSettingState.initial() => SyncSettingState(
         successOrFailure: left(unit),
       );
 }

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart

@@ -13,7 +13,7 @@ enum SettingsPage {
   language,
   files,
   user,
-  supabaseSetting,
+  syncSetting,
   shortcuts,
 }
 

+ 29 - 2
frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart

@@ -23,6 +23,7 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
         initial: () async {
           _userListener.start(onProfileUpdated: _profileUpdated);
           await _initUser();
+          _loadHistoricalUsers();
         },
         didReceiveUserProfile: (UserProfilePB newUserProfile) {
           emit(state.copyWith(userProfile: newUserProfile));
@@ -51,6 +52,12 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
             );
           });
         },
+        didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
+          emit(state.copyWith(historicalUsers: historicalUsers));
+        },
+        openHistoricalUser: (HistoricalUserPB historicalUser) async {
+          await _userService.openHistoricalUser(historicalUser);
+        },
       );
     });
   }
@@ -66,10 +73,22 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
     result.fold((l) => null, (error) => Log.error(error));
   }
 
+  Future<void> _loadHistoricalUsers() async {
+    final result = await _userService.loadHistoricalUsers();
+    result.fold(
+      (historicalUsers) {
+        add(SettingsUserEvent.didLoadHistoricalUsers(historicalUsers));
+      },
+      (error) => Log.error(error),
+    );
+  }
+
   void _profileUpdated(Either<UserProfilePB, FlowyError> userProfileOrFailed) {
     userProfileOrFailed.fold(
-      (newUserProfile) =>
-          add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)),
+      (newUserProfile) {
+        add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
+        _loadHistoricalUsers();
+      },
       (err) => Log.error(err),
     );
   }
@@ -86,18 +105,26 @@ class SettingsUserEvent with _$SettingsUserEvent {
   const factory SettingsUserEvent.didReceiveUserProfile(
     UserProfilePB newUserProfile,
   ) = _DidReceiveUserProfile;
+  const factory SettingsUserEvent.didLoadHistoricalUsers(
+    List<HistoricalUserPB> historicalUsers,
+  ) = _DidLoadHistoricalUsers;
+  const factory SettingsUserEvent.openHistoricalUser(
+    HistoricalUserPB historicalUser,
+  ) = _OpenHistoricalUser;
 }
 
 @freezed
 class SettingsUserState with _$SettingsUserState {
   const factory SettingsUserState({
     required UserProfilePB userProfile,
+    required List<HistoricalUserPB> historicalUsers,
     required Either<Unit, String> successOrFailure,
   }) = _SettingsUserState;
 
   factory SettingsUserState.initial(UserProfilePB userProfile) =>
       SettingsUserState(
         userProfile: userProfile,
+        historicalUsers: [],
         successOrFailure: left(unit),
       );
 }

+ 3 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart

@@ -105,12 +105,13 @@ class MenuUser extends StatelessWidget {
         onPressed: () {
           showDialog(
             context: context,
-            builder: (context) {
+            builder: (dialogContext) {
               return BlocProvider<DocumentAppearanceCubit>.value(
                 value: BlocProvider.of<DocumentAppearanceCubit>(context),
                 child: SettingsDialog(
                   userProfile,
                   didLogout: () async {
+                    Navigator.of(dialogContext).pop();
                     Navigator.of(context).pop();
                     await FlowyRunner.run(
                       FlowyApp(),
@@ -118,6 +119,7 @@ class MenuUser extends StatelessWidget {
                     );
                   },
                   dismissDialog: () => Navigator.of(context).pop(),
+                  didOpenUser: () {},
                 ),
               );
             },

+ 13 - 2
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart

@@ -109,19 +109,30 @@ class SidebarUser extends StatelessWidget {
         onPressed: () {
           showDialog(
             context: context,
-            builder: (context) {
+            builder: (dialogContext) {
               return BlocProvider<DocumentAppearanceCubit>.value(
                 value: BlocProvider.of<DocumentAppearanceCubit>(context),
                 child: SettingsDialog(
                   userProfile,
                   didLogout: () async {
-                    Navigator.of(context).pop();
+                    // Pop the dialog using the dialog context
+                    Navigator.of(dialogContext).pop();
+
                     await FlowyRunner.run(
                       FlowyApp(),
                       integrationEnv(),
                     );
                   },
                   dismissDialog: () => Navigator.of(context).pop(),
+                  didOpenUser: () async {
+                    // Pop the dialog using the dialog context
+                    Navigator.of(dialogContext).pop();
+
+                    await FlowyRunner.run(
+                      FlowyApp(),
+                      integrationEnv(),
+                    );
+                  },
                 ),
               );
             },

+ 6 - 3
frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_view.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
 import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
@@ -20,11 +20,13 @@ const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
 class SettingsDialog extends StatelessWidget {
   final VoidCallback dismissDialog;
   final VoidCallback didLogout;
+  final VoidCallback didOpenUser;
   final UserProfilePB user;
   SettingsDialog(
     this.user, {
     required this.dismissDialog,
     required this.didLogout,
+    required this.didOpenUser,
     Key? key,
   }) : super(key: ValueKey(user.id));
 
@@ -97,9 +99,10 @@ class SettingsDialog extends StatelessWidget {
           user,
           didLogin: () => dismissDialog(),
           didLogout: didLogout,
+          didOpenUser: didOpenUser,
         );
-      case SettingsPage.supabaseSetting:
-        return const SupabaseSettingView();
+      case SettingsPage.syncSetting:
+        return const SyncSettingView();
       case SettingsPage.shortcuts:
         return const SettingsCustomizeShortcutsWrapper();
       default:

+ 110 - 0
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart

@@ -0,0 +1,110 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class HistoricalUserList extends StatelessWidget {
+  final VoidCallback didOpenUser;
+  const HistoricalUserList({required this.didOpenUser, super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
+      builder: (context, state) {
+        return ConstrainedBox(
+          constraints: const BoxConstraints(maxHeight: 200),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                children: [
+                  FlowyText.medium(
+                    LocaleKeys.settings_menu_historicalUserList.tr(),
+                    fontSize: 13,
+                  ),
+                  const Spacer(),
+                  Tooltip(
+                    message:
+                        LocaleKeys.settings_menu_historicalUserListTooltip.tr(),
+                    child: const Icon(
+                      Icons.question_mark_rounded,
+                      size: 16,
+                    ),
+                  ),
+                ],
+              ),
+              Expanded(
+                child: ListView.builder(
+                  itemBuilder: (context, index) {
+                    final user = state.historicalUsers[index];
+                    return HistoricalUserItem(
+                      key: ValueKey(user.userId),
+                      user: user,
+                      isSelected: state.userProfile.id == user.userId,
+                      didOpenUser: didOpenUser,
+                    );
+                  },
+                  itemCount: state.historicalUsers.length,
+                ),
+              )
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+class HistoricalUserItem extends StatelessWidget {
+  final VoidCallback didOpenUser;
+  final bool isSelected;
+  final HistoricalUserPB user;
+  const HistoricalUserItem({
+    required this.user,
+    required this.isSelected,
+    required this.didOpenUser,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final icon = isSelected ? const FlowySvg(name: "grid/checkmark") : null;
+    final isDisabled = isSelected || user.authType != AuthTypePB.Local;
+    final outputFormat = DateFormat('MM/dd/yyyy');
+    final date =
+        DateTime.fromMillisecondsSinceEpoch(user.lastTime.toInt() * 1000);
+    final lastTime = outputFormat.format(date);
+    final desc = "${user.userName}  ${user.authType}  $lastTime";
+    final child = SizedBox(
+      height: 30,
+      child: FlowyButton(
+        disable: isDisabled,
+        text: FlowyText.medium(desc),
+        rightIcon: icon,
+        onTap: () {
+          if (user.userId ==
+              context.read<SettingsUserViewBloc>().userProfile.id) {
+            return;
+          }
+          context
+              .read<SettingsUserViewBloc>()
+              .add(SettingsUserEvent.openHistoricalUser(user));
+          didOpenUser();
+        },
+      ),
+    );
+
+    if (isSelected) {
+      return child;
+    } else {
+      return Tooltip(
+        message: LocaleKeys.settings_menu_openHistoricalUser.tr(),
+        child: child,
+      );
+    }
+  }
+}

+ 0 - 30
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart

@@ -1,30 +0,0 @@
-import 'package:appflowy/workspace/application/settings/setting_supabase_bloc.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-class SupabaseSettingView extends StatelessWidget {
-  const SupabaseSettingView({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) =>
-          SettingSupabaseBloc()..add(const SettingSupabaseEvent.initial()),
-      child: BlocBuilder<SettingSupabaseBloc, SettingSupabaseState>(
-        builder: (context, state) {
-          return Align(
-            alignment: Alignment.topRight,
-            child: Switch(
-              onChanged: (bool value) {
-                context.read<SettingSupabaseBloc>().add(
-                      SettingSupabaseEvent.enableSync(value),
-                    );
-              },
-              value: state.config?.enableSync ?? false,
-            ),
-          );
-        },
-      ),
-    );
-  }
-}

+ 12 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/startup/entry_point.dart';
 import 'package:appflowy/startup/launch_configuration.dart';
 import 'package:appflowy/startup/startup.dart';
@@ -6,6 +7,8 @@ import 'package:appflowy/user/presentation/sign_in_screen.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:dartz/dartz.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -25,7 +28,15 @@ class SettingThirdPartyLogin extends StatelessWidget {
             (result) => _handleSuccessOrFail(result, context),
           );
         },
-        builder: (_, __) => const ThirdPartySignInButtons(),
+        builder: (_, __) => Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            FlowyText.medium(LocaleKeys.signIn_signInWith.tr()),
+            const ThirdPartySignInButtons(
+              mainAxisAlignment: MainAxisAlignment.start,
+            ),
+          ],
+        ),
       ),
     );
   }

+ 2 - 2
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart

@@ -64,9 +64,9 @@ class SettingsMenu extends StatelessWidget {
             context.read<SettingsDialogBloc>().state.userProfile.authType !=
                 AuthTypePB.Local)
           SettingsMenuElement(
-            page: SettingsPage.supabaseSetting,
+            page: SettingsPage.syncSetting,
             selectedPage: currentPage,
-            label: LocaleKeys.settings_menu_supabaseSetting.tr(),
+            label: LocaleKeys.settings_menu_syncSetting.tr(),
             icon: Icons.sync,
             changeSelectedPage: changeSelectedPage,
           ),

+ 31 - 9
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -16,19 +16,25 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
+import 'historical_user.dart';
 import 'setting_third_party_login.dart';
 
 const defaultUserAvatar = '1F600';
 const _iconSize = Size(60, 60);
 
 class SettingsUserView extends StatelessWidget {
+  // Called when the user login in the setting dialog
   final VoidCallback didLogin;
+  // Called when the user logout in the setting dialog
   final VoidCallback didLogout;
+  // Called when the user open a historical user in the setting dialog
+  final VoidCallback didOpenUser;
   final UserProfilePB user;
   SettingsUserView(
     this.user, {
     required this.didLogin,
     required this.didLogout,
+    required this.didOpenUser,
     Key? key,
   }) : super(key: ValueKey(user.id));
 
@@ -47,6 +53,8 @@ class SettingsUserView extends StatelessWidget {
             _renderCurrentIcon(context),
             const VSpace(20),
             _renderCurrentOpenaiKey(context),
+            const VSpace(20),
+            _renderHistoricalUser(context),
             const Spacer(),
             _renderLoginOrLogoutButton(context, state),
             const VSpace(20),
@@ -56,21 +64,25 @@ class SettingsUserView extends StatelessWidget {
     );
   }
 
+  /// Renders either a login or logout button based on the user's authentication status.
+  ///
+  /// This function checks the current user's authentication type and Supabase
+  /// configuration to determine whether to render a third-party login button
+  /// or a logout button.
   Widget _renderLoginOrLogoutButton(
     BuildContext context,
     SettingsUserState state,
   ) {
-    if (!isSupabaseEnabled) {
-      return _renderLogoutButton(context);
+    if (isSupabaseEnabled) {
+      // If the user is logged in locally, render a third-party login button.
+      if (state.userProfile.authType == AuthTypePB.Local) {
+        return SettingThirdPartyLogin(
+          didLogin: didLogin,
+        );
+      }
     }
 
-    if (state.userProfile.authType == AuthTypePB.Local) {
-      return SettingThirdPartyLogin(
-        didLogin: didLogin,
-      );
-    } else {
-      return _renderLogoutButton(context);
-    }
+    return _renderLogoutButton(context);
   }
 
   Widget _renderUserNameInput(BuildContext context) {
@@ -111,6 +123,16 @@ class SettingsUserView extends StatelessWidget {
       },
     );
   }
+
+  Widget _renderHistoricalUser(BuildContext context) {
+    return BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
+      builder: (context, state) {
+        return HistoricalUserList(
+          didOpenUser: didOpenUser,
+        );
+      },
+    );
+  }
 }
 
 @visibleForTesting

+ 36 - 0
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart

@@ -0,0 +1,36 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/workspace/application/settings/setting_supabase_bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class SyncSettingView extends StatelessWidget {
+  const SyncSettingView({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) =>
+          SyncSettingBloc()..add(const SyncSettingEvent.initial()),
+      child: BlocBuilder<SyncSettingBloc, SyncSettingState>(
+        builder: (context, state) {
+          return Row(
+            children: [
+              FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()),
+              const Spacer(),
+              Switch(
+                onChanged: (bool value) {
+                  context.read<SyncSettingBloc>().add(
+                        SyncSettingEvent.enableSync(value),
+                      );
+                },
+                value: state.config?.enableSync ?? false,
+              )
+            ],
+          );
+        },
+      ),
+    );
+  }
+}

+ 2 - 1
frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart

@@ -8,9 +8,10 @@ class PageRoutes {
   static const Curve kDefaultEaseFwd = Curves.easeOut;
   static const Curve kDefaultEaseReverse = Curves.easeOut;
 
-  static Route<T> fade<T>(PageBuilder pageBuilder,
+  static Route<T> fade<T>(PageBuilder pageBuilder, RouteSettings? settings,
       [double duration = kDefaultDuration]) {
     return PageRouteBuilder<T>(
+      settings: settings,
       transitionDuration: Duration(milliseconds: (duration * 1000).round()),
       pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),
       transitionsBuilder: (context, animation, secondaryAnimation, child) {

+ 9 - 3
frontend/resources/translations/en.json

@@ -26,7 +26,8 @@
     "alreadyHaveAnAccount": "Already have an account?",
     "emailHint": "Email",
     "passwordHint": "Password",
-    "repeatPasswordHint": "Repeat password"
+    "repeatPasswordHint": "Repeat password",
+    "signUpWith": "Sign up with:"
   },
   "signIn": {
     "loginTitle": "Login to @:appName",
@@ -38,7 +39,8 @@
     "passwordHint": "Password",
     "dontHaveAnAccount": "Don't have an account?",
     "repeatPasswordEmptyError": "Repeat password can't be empty",
-    "unmatchedPasswordError": "Repeat password is not the same as password"
+    "unmatchedPasswordError": "Repeat password is not the same as password",
+    "signInWith": "Sign in with:"
   },
   "workspace": {
     "create": "Create workspace",
@@ -223,7 +225,11 @@
       "open": "Open Settings",
       "logout": "Logout",
       "logoutPrompt": "Are you sure to logout?",
-      "supabaseSetting": "Supabase Setting"
+      "syncSetting": "Sync Setting",
+      "enableSync": "Enable sync",
+      "historicalUserList": "User history",
+      "historicalUserListTooltip": "This list shows your login history. You can click to login if it's a local user",
+      "openHistoricalUser": "Click to open user"
     },
     "appearance": {
       "fontFamily": {

+ 19 - 4
frontend/rust-lib/flowy-core/src/integrate/server.rs

@@ -1,4 +1,5 @@
 use std::collections::HashMap;
+use std::fmt::{Display, Formatter};
 use std::sync::{Arc, Weak};
 
 use appflowy_integrate::collab_builder::{CollabStorageProvider, CollabStorageType};
@@ -37,14 +38,24 @@ pub enum ServerProviderType {
   /// Offline mode, no user authentication and the data is stored locally.
   Local = 0,
   /// Self-hosted server provider.
-  /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) is still a work in
+  /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Cloud) is still a work in
   /// progress.
-  SelfHosted = 1,
+  AppFlowyCloud = 1,
   /// Supabase server provider.
   /// It uses supabase's postgresql database to store data and user authentication.
   Supabase = 2,
 }
 
+impl Display for ServerProviderType {
+  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+    match self {
+      ServerProviderType::Local => write!(f, "Local"),
+      ServerProviderType::AppFlowyCloud => write!(f, "AppFlowyCloud"),
+      ServerProviderType::Supabase => write!(f, "Supabase"),
+    }
+  }
+}
+
 /// The [AppFlowyServerProvider] provides list of [AppFlowyServer] base on the [AuthType]. Using
 /// the auth type, the [AppFlowyServerProvider] will create a new [AppFlowyServer] if it doesn't
 /// exist.
@@ -95,7 +106,7 @@ impl AppFlowyServerProvider {
 
         Ok::<Arc<dyn AppFlowyServer>, FlowyError>(server)
       },
-      ServerProviderType::SelfHosted => {
+      ServerProviderType::AppFlowyCloud => {
         let config = self_host_server_configuration().map_err(|e| {
           FlowyError::new(
             ErrorCode::InvalidAuthConfig,
@@ -170,6 +181,10 @@ impl UserCloudServiceProvider for AppFlowyServerProvider {
         .user_service(),
     )
   }
+
+  fn service_name(&self) -> String {
+    self.provider_type.read().to_string()
+  }
 }
 
 impl FolderCloudService for AppFlowyServerProvider {
@@ -336,7 +351,7 @@ impl From<AuthType> for ServerProviderType {
   fn from(auth_provider: AuthType) -> Self {
     match auth_provider {
       AuthType::Local => ServerProviderType::Local,
-      AuthType::SelfHosted => ServerProviderType::SelfHosted,
+      AuthType::SelfHosted => ServerProviderType::AppFlowyCloud,
       AuthType::Supabase => ServerProviderType::Supabase,
     }
   }

+ 1 - 1
frontend/rust-lib/flowy-core/src/lib.rs

@@ -415,7 +415,7 @@ impl From<ServerProviderType> for CollabStorageType {
   fn from(server_provider: ServerProviderType) -> Self {
     match server_provider {
       ServerProviderType::Local => CollabStorageType::Local,
-      ServerProviderType::SelfHosted => CollabStorageType::Local,
+      ServerProviderType::AppFlowyCloud => CollabStorageType::Local,
       ServerProviderType::Supabase => CollabStorageType::Supabase,
     }
   }

+ 2 - 2
frontend/rust-lib/flowy-error/src/code.rs

@@ -215,8 +215,8 @@ pub enum ErrorCode {
   #[error("Postgres transaction error")]
   PgTransactionError = 71,
 
-  #[error("Enable supabase sync")]
-  SupabaseSyncRequired = 72,
+  #[error("Enable data sync")]
+  DataSyncRequired = 72,
 
   #[error("Conflict")]
   Conflict = 73,

+ 7 - 1
frontend/rust-lib/flowy-server/src/local_server/impls/user.rs

@@ -6,6 +6,7 @@ use parking_lot::Mutex;
 
 use flowy_user_deps::cloud::UserService;
 use flowy_user_deps::entities::*;
+use flowy_user_deps::DEFAULT_USER_NAME;
 use lib_infra::box_any::BoxAny;
 use lib_infra::future::FutureResult;
 
@@ -28,9 +29,14 @@ impl UserService for LocalServerUserAuthServiceImpl {
       let uid = ID_GEN.lock().next_id();
       let workspace_id = uuid::Uuid::new_v4().to_string();
       let user_workspace = UserWorkspace::new(&workspace_id, uid);
+      let user_name = if params.name.is_empty() {
+        DEFAULT_USER_NAME()
+      } else {
+        params.name.clone()
+      };
       Ok(SignUpResponse {
         user_id: uid,
-        name: params.name,
+        name: user_name,
         latest_workspace: user_workspace.clone(),
         user_workspaces: vec![user_workspace],
         is_new: true,

+ 8 - 5
frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs

@@ -102,11 +102,14 @@ where
     _id: MsgId,
     update: Vec<u8>,
   ) -> Result<(), Error> {
-    let postgrest = self.0.try_get_postgrest()?;
-    let workspace_id = object
-      .get_workspace_id()
-      .ok_or(anyhow::anyhow!("Invalid workspace id"))?;
-    send_update(workspace_id, object, update, &postgrest).await
+    if let Some(postgrest) = self.0.get_postgrest() {
+      let workspace_id = object
+        .get_workspace_id()
+        .ok_or(anyhow::anyhow!("Invalid workspace id"))?;
+      send_update(workspace_id, object, update, &postgrest).await?;
+    }
+
+    Ok(())
   }
 
   async fn send_init_sync(

+ 2 - 2
frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs

@@ -68,8 +68,8 @@ impl SupabaseServerService for SupabaseServerServiceImpl {
       .map(|server| server.postgrest.clone())
       .ok_or_else(|| {
         FlowyError::new(
-          ErrorCode::SupabaseSyncRequired,
-          "Supabase sync is disabled, please enable it first",
+          ErrorCode::DataSyncRequired,
+          "Data Sync is disabled, please enable it first",
         )
         .into()
       })

+ 10 - 2
frontend/rust-lib/flowy-server/src/supabase/api/user.rs

@@ -6,6 +6,7 @@ use uuid::Uuid;
 
 use flowy_user_deps::cloud::*;
 use flowy_user_deps::entities::*;
+use flowy_user_deps::DEFAULT_USER_NAME;
 use lib_infra::box_any::BoxAny;
 use lib_infra::future::FutureResult;
 
@@ -74,9 +75,15 @@ where
         .find(|user_workspace| user_workspace.id == user_profile.latest_workspace_id)
         .cloned();
 
+      let user_name = if user_profile.name.is_empty() {
+        DEFAULT_USER_NAME()
+      } else {
+        user_profile.name
+      };
+
       Ok(SignUpResponse {
         user_id: user_profile.uid,
-        name: user_profile.name,
+        name: user_name,
         latest_workspace: latest_workspace.unwrap(),
         user_workspaces,
         is_new: is_new_user,
@@ -100,9 +107,10 @@ where
         .iter()
         .find(|user_workspace| user_workspace.id == user_profile.latest_workspace_id)
         .cloned();
+
       Ok(SignInResponse {
         user_id: user_profile.uid,
-        name: "".to_string(),
+        name: DEFAULT_USER_NAME(),
         latest_workspace: latest_workspace.unwrap(),
         user_workspaces,
         email: None,

+ 1 - 0
frontend/rust-lib/flowy-user-deps/src/entities.rs

@@ -164,6 +164,7 @@ pub enum AuthType {
   /// It uses Supabase as the backend.
   Supabase = 2,
 }
+
 impl Default for AuthType {
   fn default() -> Self {
     Self::Local

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

@@ -1,2 +1,4 @@
 pub mod cloud;
 pub mod entities;
+
+pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string();

+ 44 - 0
frontend/rust-lib/flowy-user/src/entities/user_profile.rs

@@ -6,6 +6,7 @@ use flowy_user_deps::entities::*;
 use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword};
 use crate::entities::AuthTypePB;
 use crate::errors::ErrorCode;
+use crate::services::HistoricalUser;
 
 #[derive(Default, ProtoBuf)]
 pub struct UserTokenPB {
@@ -205,3 +206,46 @@ pub struct RemoveWorkspaceUserPB {
   #[pb(index = 2)]
   pub workspace_id: String,
 }
+
+#[derive(ProtoBuf, Default, Clone)]
+pub struct RepeatedHistoricalUserPB {
+  #[pb(index = 1)]
+  pub items: Vec<HistoricalUserPB>,
+}
+
+#[derive(ProtoBuf, Default, Clone)]
+pub struct HistoricalUserPB {
+  #[pb(index = 1)]
+  pub user_id: i64,
+
+  #[pb(index = 2)]
+  pub user_name: String,
+
+  #[pb(index = 3)]
+  pub last_time: i64,
+
+  #[pb(index = 4)]
+  pub auth_type: AuthTypePB,
+}
+
+impl From<Vec<HistoricalUser>> for RepeatedHistoricalUserPB {
+  fn from(historical_users: Vec<HistoricalUser>) -> Self {
+    Self {
+      items: historical_users
+        .into_iter()
+        .map(HistoricalUserPB::from)
+        .collect(),
+    }
+  }
+}
+
+impl From<HistoricalUser> for HistoricalUserPB {
+  fn from(historical_user: HistoricalUser) -> Self {
+    Self {
+      user_id: historical_user.user_id,
+      user_name: historical_user.user_name,
+      last_time: historical_user.sign_in_timestamp,
+      auth_type: historical_user.auth_type.into(),
+    }
+  }
+}

+ 20 - 0
frontend/rust-lib/flowy-user/src/event_handler.rs

@@ -260,3 +260,23 @@ pub async fn update_network_state_handler(
     .did_update_network(reachable);
   Ok(())
 }
+
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub async fn get_historical_users_handler(
+  session: AFPluginState<Weak<UserSession>>,
+) -> DataResult<RepeatedHistoricalUserPB, FlowyError> {
+  let session = upgrade_session(session)?;
+  let users = RepeatedHistoricalUserPB::from(session.get_historical_users());
+  data_result_ok(users)
+}
+
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub async fn open_historical_users_handler(
+  user: AFPluginData<HistoricalUserPB>,
+  session: AFPluginState<Weak<UserSession>>,
+) -> Result<(), FlowyError> {
+  let user = user.into_inner();
+  let session = upgrade_session(session)?;
+  session.open_historical_user(user.user_id)?;
+  Ok(())
+}

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

@@ -47,6 +47,8 @@ pub fn init(user_session: Weak<UserSession>) -> AFPlugin {
       remove_user_from_workspace_handler,
     )
     .event(UserEvent::UpdateNetworkState, update_network_state_handler)
+    .event(UserEvent::GetHistoricalUsers, get_historical_users_handler)
+    .event(UserEvent::OpenHistoricalUser, open_historical_users_handler)
 }
 
 pub struct SignUpContext {
@@ -85,6 +87,7 @@ pub trait UserCloudServiceProvider: Send + Sync + 'static {
   fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration);
   fn set_auth_type(&self, auth_type: AuthType);
   fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError>;
+  fn service_name(&self) -> String;
 }
 
 impl<T> UserCloudServiceProvider for Arc<T>
@@ -102,6 +105,10 @@ where
   fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError> {
     (**self).get_user_service()
   }
+
+  fn service_name(&self) -> String {
+    (**self).service_name()
+  }
 }
 
 /// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function
@@ -208,4 +215,10 @@ pub enum UserEvent {
 
   #[event(input = "NetworkStatePB")]
   UpdateNetworkState = 24,
+
+  #[event(output = "RepeatedHistoricalUserPB")]
+  GetHistoricalUsers = 25,
+
+  #[event(input = "HistoricalUserPB")]
+  OpenHistoricalUser = 26,
 }

+ 10 - 1
frontend/rust-lib/flowy-user/src/services/session_serde.rs

@@ -8,7 +8,7 @@ use serde::Deserialize;
 use serde::Serialize;
 use serde_json::Value;
 
-use flowy_user_deps::entities::{SignInResponse, UserWorkspace};
+use flowy_user_deps::entities::{SignInResponse, SignUpResponse, UserWorkspace};
 
 #[derive(Debug, Clone, Serialize)]
 pub struct Session {
@@ -102,6 +102,15 @@ impl std::convert::From<Session> for String {
   }
 }
 
+impl From<&SignUpResponse> for Session {
+  fn from(value: &SignUpResponse) -> Self {
+    Session {
+      user_id: value.user_id,
+      user_workspace: value.latest_workspace.clone(),
+    }
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use super::*;

+ 50 - 24
frontend/rust-lib/flowy-user/src/services/user_session.rs

@@ -1,3 +1,5 @@
+use std::convert::TryFrom;
+use std::string::ToString;
 use std::sync::{Arc, Weak};
 
 use appflowy_integrate::RocksCollabDB;
@@ -152,27 +154,30 @@ impl UserSession {
     params: BoxAny,
     auth_type: AuthType,
   ) -> Result<UserProfile, FlowyError> {
-    let resp: SignInResponse = self
+    let response: SignInResponse = self
       .cloud_services
       .get_user_service()?
       .sign_in(params)
       .await?;
-
-    let session: Session = resp.clone().into();
+    let session: Session = response.clone().into();
     let uid = session.user_id;
-    self.set_session(Some(session))?;
-    self.log_user(uid, self.user_dir(uid));
+    self.set_current_session(Some(session))?;
+
+    self.log_user(uid, response.name.clone(), &auth_type, self.user_dir(uid));
 
-    let user_workspace = resp.latest_workspace.clone();
+    let user_workspace = response.latest_workspace.clone();
     save_user_workspaces(
       self.db_pool(uid)?,
-      resp
+      response
         .user_workspaces
         .iter()
-        .map(|user_workspace| UserWorkspaceTable::from((uid, user_workspace)))
+        .flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok())
         .collect(),
     )?;
-    let user_profile: UserProfile = self.save_user(uid, (resp, auth_type).into()).await?.into();
+    let user_profile: UserProfile = self
+      .save_user(uid, (response, auth_type).into())
+      .await?
+      .into();
     if let Err(e) = self
       .user_status_callback
       .read()
@@ -226,19 +231,16 @@ impl UserSession {
       is_new: response.is_new,
       local_folder: None,
     };
-    let new_session = Session {
-      user_id: response.user_id,
-      user_workspace: response.latest_workspace.clone(),
-    };
-    let uid = new_session.user_id;
-    self.set_session(Some(new_session.clone()))?;
-    self.log_user(uid, self.user_dir(uid));
+    let new_session = Session::from(&response);
+    self.set_current_session(Some(new_session.clone()))?;
+    let uid = response.user_id;
+    self.log_user(uid, response.name.clone(), &auth_type, self.user_dir(uid));
     save_user_workspaces(
       self.db_pool(uid)?,
       response
         .user_workspaces
         .iter()
-        .map(|user_workspace| UserWorkspaceTable::from((uid, user_workspace)))
+        .flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok())
         .collect(),
     )?;
     let user_table = self
@@ -289,7 +291,7 @@ impl UserSession {
   pub async fn sign_out(&self) -> Result<(), FlowyError> {
     let session = self.get_session()?;
     self.database.close(session.user_id)?;
-    self.set_session(None)?;
+    self.set_current_session(None)?;
 
     let server = self.cloud_services.get_user_service()?;
     tokio::spawn(async move {
@@ -513,7 +515,7 @@ impl UserSession {
               pool,
               new_user_workspaces
                 .iter()
-                .map(|user_workspace| UserWorkspaceTable::from((uid, user_workspace)))
+                .flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok())
                 .collect(),
             );
 
@@ -561,8 +563,8 @@ impl UserSession {
     })
   }
 
-  fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
-    tracing::debug!("Set user session: {:?}", session);
+  fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
+    tracing::debug!("Set current user: {:?}", session);
     match &session {
       None => self
         .store_preferences
@@ -577,13 +579,15 @@ impl UserSession {
     Ok(())
   }
 
-  fn log_user(&self, uid: i64, storage_path: String) {
+  fn log_user(&self, uid: i64, user_name: String, auth_type: &AuthType, storage_path: String) {
     let mut logger_users = self
       .store_preferences
       .get_object::<HistoricalUsers>(HISTORICAL_USER)
       .unwrap_or_default();
     logger_users.add_user(HistoricalUser {
       user_id: uid,
+      user_name,
+      auth_type: auth_type.clone(),
       sign_in_timestamp: timestamp(),
       storage_path,
     });
@@ -593,11 +597,27 @@ impl UserSession {
   }
 
   pub fn get_historical_users(&self) -> Vec<HistoricalUser> {
-    self
+    let mut users = self
       .store_preferences
       .get_object::<HistoricalUsers>(HISTORICAL_USER)
       .unwrap_or_default()
-      .users
+      .users;
+    users.sort_by(|a, b| b.sign_in_timestamp.cmp(&a.sign_in_timestamp));
+    users
+  }
+
+  pub fn open_historical_user(&self, uid: i64) -> FlowyResult<()> {
+    let conn = self.db_connection(uid)?;
+    let row = user_workspace_table::dsl::user_workspace_table
+      .filter(user_workspace_table::uid.eq(uid))
+      .first::<UserWorkspaceTable>(&*conn)?;
+    let user_workspace = UserWorkspace::from(row);
+    let session = Session {
+      user_id: uid,
+      user_workspace,
+    };
+    self.set_current_session(Some(session))?;
+    Ok(())
   }
 
   /// Returns the current user session.
@@ -691,6 +711,12 @@ impl HistoricalUsers {
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 pub struct HistoricalUser {
   pub user_id: i64,
+  #[serde(default = "flowy_user_deps::DEFAULT_USER_NAME")]
+  pub user_name: String,
+  #[serde(default = "DEFAULT_AUTH_TYPE")]
+  pub auth_type: AuthType,
   pub sign_in_timestamp: i64,
   pub storage_path: String,
 }
+
+const DEFAULT_AUTH_TYPE: fn() -> AuthType = || AuthType::Local;

+ 16 - 5
frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs

@@ -1,4 +1,6 @@
 use chrono::{TimeZone, Utc};
+use flowy_error::FlowyError;
+use std::convert::TryFrom;
 
 use flowy_sqlite::schema::user_workspace_table;
 use flowy_user_deps::entities::UserWorkspace;
@@ -13,15 +15,24 @@ pub struct UserWorkspaceTable {
   pub database_storage_id: String,
 }
 
-impl From<(i64, &UserWorkspace)> for UserWorkspaceTable {
-  fn from(value: (i64, &UserWorkspace)) -> Self {
-    Self {
+impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
+  type Error = FlowyError;
+
+  fn try_from(value: (i64, &UserWorkspace)) -> Result<Self, Self::Error> {
+    if value.1.id.is_empty() {
+      return Err(FlowyError::invalid_data().context("The id is empty"));
+    }
+    if value.1.database_storage_id.is_empty() {
+      return Err(FlowyError::invalid_data().context("The database storage id is empty"));
+    }
+
+    Ok(Self {
       id: value.1.id.clone(),
       name: value.1.name.clone(),
       uid: value.0,
       created_at: value.1.created_at.timestamp(),
       database_storage_id: value.1.database_storage_id.clone(),
-    }
+    })
   }
 }
 
@@ -34,7 +45,7 @@ impl From<UserWorkspaceTable> for UserWorkspace {
         .timestamp_opt(value.created_at, 0)
         .single()
         .unwrap_or_default(),
-      database_storage_id: "".to_string(),
+      database_storage_id: value.database_storage_id,
     }
   }
 }

+ 1 - 1
frontend/scripts/makefile/desktop.toml

@@ -65,7 +65,7 @@ script = [
   """
     cd rust-lib/
     rustup show
-    echo cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
+    echo  RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
     RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
     cd ../
   """,