From 3c04b72932b06db5539e4e0a3ae9e2d31983a48f Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 7 Aug 2023 22:24:04 +0800 Subject: [PATCH] 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 --- .../lib/startup/tasks/app_widget.dart | 1 + .../auth/supabase_auth_service.dart | 23 +++- .../lib/user/application/user_service.dart | 18 +++ .../lib/user/presentation/router.dart | 13 +++ .../lib/user/presentation/sign_in_screen.dart | 4 +- .../settings/setting_supabase_bloc.dart | 29 +++-- .../settings/settings_dialog_bloc.dart | 2 +- .../application/user/settings_user_bloc.dart | 31 ++++- .../presentation/home/menu/menu_user.dart | 4 +- .../home/menu/sidebar/sidebar_user.dart | 15 ++- .../settings/settings_dialog.dart | 9 +- .../settings/widgets/historical_user.dart | 110 ++++++++++++++++++ .../widgets/setting_supabase_view.dart | 30 ----- .../widgets/setting_third_party_login.dart | 13 ++- .../settings/widgets/settings_menu.dart | 4 +- .../settings/widgets/settings_user_view.dart | 40 +++++-- .../settings/widgets/sync_setting_view.dart | 36 ++++++ .../lib/widget/route/animation.dart | 3 +- frontend/resources/translations/en.json | 12 +- .../flowy-core/src/integrate/server.rs | 23 +++- frontend/rust-lib/flowy-core/src/lib.rs | 2 +- frontend/rust-lib/flowy-error/src/code.rs | 4 +- .../src/local_server/impls/user.rs | 8 +- .../src/supabase/api/collab_storage.rs | 13 ++- .../src/supabase/api/postgres_server.rs | 4 +- .../flowy-server/src/supabase/api/user.rs | 12 +- .../rust-lib/flowy-user-deps/src/entities.rs | 1 + frontend/rust-lib/flowy-user-deps/src/lib.rs | 2 + .../flowy-user/src/entities/user_profile.rs | 44 +++++++ .../rust-lib/flowy-user/src/event_handler.rs | 20 ++++ frontend/rust-lib/flowy-user/src/event_map.rs | 13 +++ .../flowy-user/src/services/session_serde.rs | 11 +- .../flowy-user/src/services/user_session.rs | 74 ++++++++---- .../src/services/user_workspace_sql.rs | 21 +++- frontend/scripts/makefile/desktop.toml | 2 +- 35 files changed, 528 insertions(+), 123 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 5c4ea62276..7b926b2b98 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -89,6 +89,7 @@ class ApplicationWidget extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) => MaterialApp( + key: UniqueKey(), builder: overlayManagerBuilder(), debugShowCheckedModeBanner: false, theme: state.lightTheme, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart index f64e500d05..6c2b490505 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ b/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`. This completer completes +/// with the response from the [onSuccess] callback when a user signs in. Completer> supabaseLoginCompleter({ required Future> Function( String userId, @@ -227,16 +243,15 @@ Completer> 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; } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index c6877aa866..14a81c6bbf 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -70,6 +70,24 @@ class UserBackendService { return UserEventInitUser().send(); } + Future, FlowyError>> + loadHistoricalUsers() async { + return UserEventGetHistoricalUsers().send().then( + (result) { + return result.fold( + (historicalUsers) => left(historicalUsers.items), + (error) => right(error), + ); + }, + ); + } + + Future> openHistoricalUser( + HistoricalUserPB user, + ) async { + return UserEventOpenHistoricalUser(user).send(); + } + Future, FlowyError>> getWorkspaces() { final request = WorkspaceIdPB.create(); diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 8546172b36..fa8308154a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/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()), + 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()), + const RouteSettings(name: routerNameSignIn), RouteDurations.slow.inMilliseconds * .001, ), ); @@ -120,6 +132,7 @@ class SplashRoute { router: getIt(), authService: getIt(), ), + const RouteSettings(name: routerNameSkipLogIn), RouteDurations.slow.inMilliseconds * .001, ), ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart index c4aacf05cd..8548a483a8 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/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', diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart index e87e2dc623..898ffde02f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart +++ b/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 { - SettingSupabaseBloc() : super(SettingSupabaseState.initial()) { - on((event, emit) async { +class SyncSettingBloc extends Bloc { + SyncSettingBloc() : super(SyncSettingState.initial()) { + on((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 successOrFailure, - }) = _SettingSupabaseState; + }) = _SyncSettingState; - factory SettingSupabaseState.initial() => SettingSupabaseState( + factory SyncSettingState.initial() => SyncSettingState( successOrFailure: left(unit), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 6350d236b6..51597845cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -13,7 +13,7 @@ enum SettingsPage { language, files, user, - supabaseSetting, + syncSetting, shortcuts, } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 1c1e8b4689..1b97e8024b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -23,6 +23,7 @@ class SettingsUserViewBloc extends Bloc { 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 { ); }); }, + didLoadHistoricalUsers: (List historicalUsers) { + emit(state.copyWith(historicalUsers: historicalUsers)); + }, + openHistoricalUser: (HistoricalUserPB historicalUser) async { + await _userService.openHistoricalUser(historicalUser); + }, ); }); } @@ -66,10 +73,22 @@ class SettingsUserViewBloc extends Bloc { result.fold((l) => null, (error) => Log.error(error)); } + Future _loadHistoricalUsers() async { + final result = await _userService.loadHistoricalUsers(); + result.fold( + (historicalUsers) { + add(SettingsUserEvent.didLoadHistoricalUsers(historicalUsers)); + }, + (error) => Log.error(error), + ); + } + void _profileUpdated(Either 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 historicalUsers, + ) = _DidLoadHistoricalUsers; + const factory SettingsUserEvent.openHistoricalUser( + HistoricalUserPB historicalUser, + ) = _OpenHistoricalUser; } @freezed class SettingsUserState with _$SettingsUserState { const factory SettingsUserState({ required UserProfilePB userProfile, + required List historicalUsers, required Either successOrFailure, }) = _SettingsUserState; factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( userProfile: userProfile, + historicalUsers: [], successOrFailure: left(unit), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart index f3abcb7874..cf60ce7356 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_user.dart +++ b/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.value( value: BlocProvider.of(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: () {}, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index c3425aefcc..f95d111d64 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/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.value( value: BlocProvider.of(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(), + ); + }, ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 26aedf783d..c3a2b90e90 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/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: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart new file mode 100644 index 0000000000..121f49c2bd --- /dev/null +++ b/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( + 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().userProfile.id) { + return; + } + context + .read() + .add(SettingsUserEvent.openHistoricalUser(user)); + didOpenUser(); + }, + ), + ); + + if (isSelected) { + return child; + } else { + return Tooltip( + message: LocaleKeys.settings_menu_openHistoricalUser.tr(), + child: child, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart deleted file mode 100644 index c25031bfe4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart +++ /dev/null @@ -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( - builder: (context, state) { - return Align( - alignment: Alignment.topRight, - child: Switch( - onChanged: (bool value) { - context.read().add( - SettingSupabaseEvent.enableSync(value), - ); - }, - value: state.config?.enableSync ?? false, - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index 4e24e072fa..d73e6cf7c1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/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, + ), + ], + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 103a80af94..2490a47577 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -64,9 +64,9 @@ class SettingsMenu extends StatelessWidget { context.read().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, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 9f0d4d5205..a639e06985 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/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( + builder: (context, state) { + return HistoricalUserList( + didOpenUser: didOpenUser, + ); + }, + ); + } } @visibleForTesting diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart new file mode 100644 index 0000000000..d1fd29a8c4 --- /dev/null +++ b/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( + builder: (context, state) { + return Row( + children: [ + FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), + const Spacer(), + Switch( + onChanged: (bool value) { + context.read().add( + SyncSettingEvent.enableSync(value), + ); + }, + value: state.config?.enableSync ?? false, + ) + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart index e0f328afc9..257c7c0cf6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart +++ b/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 fade(PageBuilder pageBuilder, + static Route fade(PageBuilder pageBuilder, RouteSettings? settings, [double duration = kDefaultDuration]) { return PageRouteBuilder( + settings: settings, transitionDuration: Duration(milliseconds: (duration * 1000).round()), pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(), transitionsBuilder: (context, animation, secondaryAnimation, child) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7f94e5a8af..7b8772564c 100644 --- a/frontend/resources/translations/en.json +++ b/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": { diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 053c66ba1f..39d2cc52a4 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/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::, 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 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, } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 2232a7766c..07d0a17413 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -415,7 +415,7 @@ impl From 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, } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index e89b7ec511..24b8c275ec 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/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, diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 501a612c08..fc67f38291 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/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, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index 363e0efa60..a08c26dfe3 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -102,11 +102,14 @@ where _id: MsgId, update: Vec, ) -> 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( diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs b/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs index 7aaa752dbc..6f76dc7699 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs +++ b/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() }) diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index b2fef41df7..b190e27cbc 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/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, diff --git a/frontend/rust-lib/flowy-user-deps/src/entities.rs b/frontend/rust-lib/flowy-user-deps/src/entities.rs index edbda22811..49653ce700 100644 --- a/frontend/rust-lib/flowy-user-deps/src/entities.rs +++ b/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 diff --git a/frontend/rust-lib/flowy-user-deps/src/lib.rs b/frontend/rust-lib/flowy-user-deps/src/lib.rs index ee0ade69c4..ff679f66fb 100644 --- a/frontend/rust-lib/flowy-user-deps/src/lib.rs +++ b/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(); diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 9b238ebb7b..684af5ac78 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/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, +} + +#[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> for RepeatedHistoricalUserPB { + fn from(historical_users: Vec) -> Self { + Self { + items: historical_users + .into_iter() + .map(HistoricalUserPB::from) + .collect(), + } + } +} + +impl From 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(), + } + } +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 253bfb0f74..1bd660902a 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/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>, +) -> DataResult { + 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, + session: AFPluginState>, +) -> Result<(), FlowyError> { + let user = user.into_inner(); + let session = upgrade_session(session)?; + session.open_historical_user(user.user_id)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 745beae8c2..572eedeec3 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -47,6 +47,8 @@ pub fn init(user_session: Weak) -> 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, FlowyError>; + fn service_name(&self) -> String; } impl UserCloudServiceProvider for Arc @@ -102,6 +105,10 @@ where fn get_user_service(&self) -> Result, 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, } diff --git a/frontend/rust-lib/flowy-user/src/services/session_serde.rs b/frontend/rust-lib/flowy-user/src/services/session_serde.rs index a291b4d9d8..0c4aff96dc 100644 --- a/frontend/rust-lib/flowy-user/src/services/session_serde.rs +++ b/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 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::*; diff --git a/frontend/rust-lib/flowy-user/src/services/user_session.rs b/frontend/rust-lib/flowy-user/src/services/user_session.rs index e0530dd455..5e7f680c56 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/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 { - 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))?; - let user_workspace = resp.latest_workspace.clone(); + self.log_user(uid, response.name.clone(), &auth_type, self.user_dir(uid)); + + 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) -> Result<(), FlowyError> { - tracing::debug!("Set user session: {:?}", session); + fn set_current_session(&self, session: Option) -> 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::(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 { - self + let mut users = self .store_preferences .get_object::(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::(&*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; diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs index 35a8614a9a..f9a5f6c70f 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs +++ b/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 { + 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 for UserWorkspace { .timestamp_opt(value.created_at, 0) .single() .unwrap_or_default(), - database_storage_id: "".to_string(), + database_storage_id: value.database_storage_id, } } } diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml index 20ca08c944..e7784f6594 100644 --- a/frontend/scripts/makefile/desktop.toml +++ b/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 ../ """,