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
This commit is contained in:
Nathan.fooo 2023-08-07 22:24:04 +08:00 committed by GitHub
parent a3bea472bf
commit 3c04b72932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 528 additions and 123 deletions

View File

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

View File

@ -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({ Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
required Future<Either<FlowyError, UserProfilePB>> Function( required Future<Either<FlowyError, UserProfilePB>> Function(
String userId, String userId,
@ -227,16 +243,15 @@ Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
subscription = auth.onAuthStateChange.listen((event) async { subscription = auth.onAuthStateChange.listen((event) async {
final user = event.session?.user; final user = event.session?.user;
if (event.event != AuthChangeEvent.signedIn || user == null) { if (event.event == AuthChangeEvent.signedIn && user != null) {
completer.complete(left(AuthError.supabaseSignInWithOauthError));
} else {
final response = await onSuccess( final response = await onSuccess(
user.id, user.id,
user.email ?? user.newEmail ?? '', user.email ?? user.newEmail ?? '',
); );
// Only cancel the subscription if the Event is signedIn.
subscription.cancel();
completer.complete(response); completer.complete(response);
} }
subscription.cancel();
}); });
return completer; return completer;
} }

View File

@ -70,6 +70,24 @@ class UserBackendService {
return UserEventInitUser().send(); 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() { Future<Either<List<WorkspacePB>, FlowyError>> getWorkspaces() {
final request = WorkspaceIdPB.create(); final request = WorkspaceIdPB.create();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/generated/locale_keys.g.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_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_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'; 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 { class SettingsDialog extends StatelessWidget {
final VoidCallback dismissDialog; final VoidCallback dismissDialog;
final VoidCallback didLogout; final VoidCallback didLogout;
final VoidCallback didOpenUser;
final UserProfilePB user; final UserProfilePB user;
SettingsDialog( SettingsDialog(
this.user, { this.user, {
required this.dismissDialog, required this.dismissDialog,
required this.didLogout, required this.didLogout,
required this.didOpenUser,
Key? key, Key? key,
}) : super(key: ValueKey(user.id)); }) : super(key: ValueKey(user.id));
@ -97,9 +99,10 @@ class SettingsDialog extends StatelessWidget {
user, user,
didLogin: () => dismissDialog(), didLogin: () => dismissDialog(),
didLogout: didLogout, didLogout: didLogout,
didOpenUser: didOpenUser,
); );
case SettingsPage.supabaseSetting: case SettingsPage.syncSetting:
return const SupabaseSettingView(); return const SyncSettingView();
case SettingsPage.shortcuts: case SettingsPage.shortcuts:
return const SettingsCustomizeShortcutsWrapper(); return const SettingsCustomizeShortcutsWrapper();
default: default:

View File

@ -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,
);
}
}
}

View File

@ -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,
),
);
},
),
);
}
}

View File

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

View File

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

View File

@ -16,19 +16,25 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'historical_user.dart';
import 'setting_third_party_login.dart'; import 'setting_third_party_login.dart';
const defaultUserAvatar = '1F600'; const defaultUserAvatar = '1F600';
const _iconSize = Size(60, 60); const _iconSize = Size(60, 60);
class SettingsUserView extends StatelessWidget { class SettingsUserView extends StatelessWidget {
// Called when the user login in the setting dialog
final VoidCallback didLogin; final VoidCallback didLogin;
// Called when the user logout in the setting dialog
final VoidCallback didLogout; final VoidCallback didLogout;
// Called when the user open a historical user in the setting dialog
final VoidCallback didOpenUser;
final UserProfilePB user; final UserProfilePB user;
SettingsUserView( SettingsUserView(
this.user, { this.user, {
required this.didLogin, required this.didLogin,
required this.didLogout, required this.didLogout,
required this.didOpenUser,
Key? key, Key? key,
}) : super(key: ValueKey(user.id)); }) : super(key: ValueKey(user.id));
@ -47,6 +53,8 @@ class SettingsUserView extends StatelessWidget {
_renderCurrentIcon(context), _renderCurrentIcon(context),
const VSpace(20), const VSpace(20),
_renderCurrentOpenaiKey(context), _renderCurrentOpenaiKey(context),
const VSpace(20),
_renderHistoricalUser(context),
const Spacer(), const Spacer(),
_renderLoginOrLogoutButton(context, state), _renderLoginOrLogoutButton(context, state),
const VSpace(20), const VSpace(20),
@ -56,23 +64,27 @@ 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( Widget _renderLoginOrLogoutButton(
BuildContext context, BuildContext context,
SettingsUserState state, SettingsUserState state,
) { ) {
if (!isSupabaseEnabled) { if (isSupabaseEnabled) {
return _renderLogoutButton(context); // If the user is logged in locally, render a third-party login button.
}
if (state.userProfile.authType == AuthTypePB.Local) { if (state.userProfile.authType == AuthTypePB.Local) {
return SettingThirdPartyLogin( return SettingThirdPartyLogin(
didLogin: didLogin, didLogin: didLogin,
); );
} else {
return _renderLogoutButton(context);
} }
} }
return _renderLogoutButton(context);
}
Widget _renderUserNameInput(BuildContext context) { Widget _renderUserNameInput(BuildContext context) {
final String name = final String name =
context.read<SettingsUserViewBloc>().state.userProfile.name; context.read<SettingsUserViewBloc>().state.userProfile.name;
@ -111,6 +123,16 @@ class SettingsUserView extends StatelessWidget {
}, },
); );
} }
Widget _renderHistoricalUser(BuildContext context) {
return BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
builder: (context, state) {
return HistoricalUserList(
didOpenUser: didOpenUser,
);
},
);
}
} }
@visibleForTesting @visibleForTesting

View File

@ -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,
)
],
);
},
),
);
}
}

View File

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

View File

@ -26,7 +26,8 @@
"alreadyHaveAnAccount": "Already have an account?", "alreadyHaveAnAccount": "Already have an account?",
"emailHint": "Email", "emailHint": "Email",
"passwordHint": "Password", "passwordHint": "Password",
"repeatPasswordHint": "Repeat password" "repeatPasswordHint": "Repeat password",
"signUpWith": "Sign up with:"
}, },
"signIn": { "signIn": {
"loginTitle": "Login to @:appName", "loginTitle": "Login to @:appName",
@ -38,7 +39,8 @@
"passwordHint": "Password", "passwordHint": "Password",
"dontHaveAnAccount": "Don't have an account?", "dontHaveAnAccount": "Don't have an account?",
"repeatPasswordEmptyError": "Repeat password can't be empty", "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": { "workspace": {
"create": "Create workspace", "create": "Create workspace",
@ -223,7 +225,11 @@
"open": "Open Settings", "open": "Open Settings",
"logout": "Logout", "logout": "Logout",
"logoutPrompt": "Are you sure to 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": { "appearance": {
"fontFamily": { "fontFamily": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ use flowy_user_deps::entities::*;
use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword};
use crate::entities::AuthTypePB; use crate::entities::AuthTypePB;
use crate::errors::ErrorCode; use crate::errors::ErrorCode;
use crate::services::HistoricalUser;
#[derive(Default, ProtoBuf)] #[derive(Default, ProtoBuf)]
pub struct UserTokenPB { pub struct UserTokenPB {
@ -205,3 +206,46 @@ pub struct RemoveWorkspaceUserPB {
#[pb(index = 2)] #[pb(index = 2)]
pub workspace_id: String, 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(),
}
}
}

View File

@ -260,3 +260,23 @@ pub async fn update_network_state_handler(
.did_update_network(reachable); .did_update_network(reachable);
Ok(()) 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(())
}

View File

@ -47,6 +47,8 @@ pub fn init(user_session: Weak<UserSession>) -> AFPlugin {
remove_user_from_workspace_handler, remove_user_from_workspace_handler,
) )
.event(UserEvent::UpdateNetworkState, update_network_state_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 { pub struct SignUpContext {
@ -85,6 +87,7 @@ pub trait UserCloudServiceProvider: Send + Sync + 'static {
fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration); fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration);
fn set_auth_type(&self, auth_type: AuthType); fn set_auth_type(&self, auth_type: AuthType);
fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError>; fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError>;
fn service_name(&self) -> String;
} }
impl<T> UserCloudServiceProvider for Arc<T> impl<T> UserCloudServiceProvider for Arc<T>
@ -102,6 +105,10 @@ where
fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError> { fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError> {
(**self).get_user_service() (**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 /// 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")] #[event(input = "NetworkStatePB")]
UpdateNetworkState = 24, UpdateNetworkState = 24,
#[event(output = "RepeatedHistoricalUserPB")]
GetHistoricalUsers = 25,
#[event(input = "HistoricalUserPB")]
OpenHistoricalUser = 26,
} }

View File

@ -8,7 +8,7 @@ use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use flowy_user_deps::entities::{SignInResponse, UserWorkspace}; use flowy_user_deps::entities::{SignInResponse, SignUpResponse, UserWorkspace};
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Session { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

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

View File

@ -1,4 +1,6 @@
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use flowy_error::FlowyError;
use std::convert::TryFrom;
use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::schema::user_workspace_table;
use flowy_user_deps::entities::UserWorkspace; use flowy_user_deps::entities::UserWorkspace;
@ -13,15 +15,24 @@ pub struct UserWorkspaceTable {
pub database_storage_id: String, pub database_storage_id: String,
} }
impl From<(i64, &UserWorkspace)> for UserWorkspaceTable { impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
fn from(value: (i64, &UserWorkspace)) -> Self { type Error = FlowyError;
Self {
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(), id: value.1.id.clone(),
name: value.1.name.clone(), name: value.1.name.clone(),
uid: value.0, uid: value.0,
created_at: value.1.created_at.timestamp(), created_at: value.1.created_at.timestamp(),
database_storage_id: value.1.database_storage_id.clone(), database_storage_id: value.1.database_storage_id.clone(),
} })
} }
} }
@ -34,7 +45,7 @@ impl From<UserWorkspaceTable> for UserWorkspace {
.timestamp_opt(value.created_at, 0) .timestamp_opt(value.created_at, 0)
.single() .single()
.unwrap_or_default(), .unwrap_or_default(),
database_storage_id: "".to_string(), database_storage_id: value.database_storage_id,
} }
} }
} }

View File

@ -65,7 +65,7 @@ script = [
""" """
cd rust-lib/ cd rust-lib/
rustup show 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}" 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 ../ cd ../
""", """,