diff --git a/frontend/appflowy_flutter/lib/user/application/historical_user_bloc.dart b/frontend/appflowy_flutter/lib/user/application/historical_user_bloc.dart new file mode 100644 index 0000000000..99525d56e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/historical_user_bloc.dart @@ -0,0 +1,64 @@ +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'historical_user_bloc.freezed.dart'; + +class HistoricalUserBloc + extends Bloc { + HistoricalUserBloc() : super(HistoricalUserState.initial()) { + on((event, emit) async { + await event.when( + initial: () async { + await _loadHistoricalUsers(); + }, + didLoadHistoricalUsers: (List historicalUsers) { + emit(state.copyWith(historicalUsers: historicalUsers)); + }, + openHistoricalUser: (HistoricalUserPB historicalUser) async { + await UserBackendService.openHistoricalUser(historicalUser); + emit(state.copyWith(openedHistoricalUser: historicalUser)); + }, + ); + }); + } + + Future _loadHistoricalUsers() async { + final result = await UserBackendService.loadHistoricalUsers(); + result.fold( + (historicalUsers) { + historicalUsers + .retainWhere((element) => element.authType == AuthTypePB.Local); + add(HistoricalUserEvent.didLoadHistoricalUsers(historicalUsers)); + }, + (error) => Log.error(error), + ); + } +} + +@freezed +class HistoricalUserEvent with _$HistoricalUserEvent { + const factory HistoricalUserEvent.initial() = _Initial; + const factory HistoricalUserEvent.didLoadHistoricalUsers( + List historicalUsers, + ) = _DidLoadHistoricalUsers; + const factory HistoricalUserEvent.openHistoricalUser( + HistoricalUserPB historicalUser, + ) = _OpenHistoricalUser; +} + +@freezed +class HistoricalUserState with _$HistoricalUserState { + const factory HistoricalUserState({ + required List historicalUsers, + required HistoricalUserPB? openedHistoricalUser, + }) = _HistoricalUserState; + + factory HistoricalUserState.initial() => const HistoricalUserState( + historicalUsers: [], + openedHistoricalUser: null, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 14a81c6bbf..9c00bcc681 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -70,7 +70,7 @@ class UserBackendService { return UserEventInitUser().send(); } - Future, FlowyError>> + static Future, FlowyError>> loadHistoricalUsers() async { return UserEventGetHistoricalUsers().send().then( (result) { @@ -82,7 +82,7 @@ class UserBackendService { ); } - Future> openHistoricalUser( + static Future> openHistoricalUser( HistoricalUserPB user, ) async { return UserEventOpenHistoricalUser(user).send(); diff --git a/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart b/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart new file mode 100644 index 0000000000..7fe2ee85e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart @@ -0,0 +1,100 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/historical_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 BlocProvider( + create: (context) => HistoricalUserBloc() + ..add( + const HistoricalUserEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + if (state.historicalUsers.isEmpty) { + return const SizedBox.shrink(); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Opacity( + opacity: 0.6, + child: FlowyText.regular( + LocaleKeys.settings_menu_historicalUserListTooltip.tr(), + fontSize: 13, + maxLines: null, + ), + ), + const VSpace(6), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + final user = state.historicalUsers[index]; + return HistoricalUserItem( + key: ValueKey(user.userId), + user: user, + isSelected: false, + 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 hh:mm a'); + final date = + DateTime.fromMillisecondsSinceEpoch(user.lastTime.toInt() * 1000); + final lastTime = outputFormat.format(date); + final desc = "${user.userName}\t ${user.authType}\t$lastTime"; + final child = SizedBox( + height: 30, + child: FlowyButton( + disable: isDisabled, + text: FlowyText.medium( + desc, + fontSize: 12, + ), + rightIcon: icon, + onTap: () { + context + .read() + .add(HistoricalUserEvent.openHistoricalUser(user)); + didOpenUser(); + }, + ), + ); + return child; + } +} 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 0616e91455..d29dac34c8 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -1,7 +1,9 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/historical_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/widgets/background.dart'; @@ -118,7 +120,18 @@ class SignInForm extends StatelessWidget { : [ const VSpace(indicatorMinHeight * 2.0) ], // add the same space when there's no loading status. - const VSpace(20) + // ConstrainedBox( + // constraints: const BoxConstraints(maxHeight: 140), + // child: HistoricalUserList( + // didOpenUser: () async { + // await FlowyRunner.run( + // FlowyApp(), + // integrationEnv(), + // ); + // }, + // ), + // ), + const VSpace(20), ], ), ); @@ -183,14 +196,49 @@ class SignInAsGuestButton extends StatelessWidget { @override Widget build(BuildContext context) { - return RoundedTextButton( - title: LocaleKeys.signIn_loginAsGuestButtonText.tr(), - height: 48, - borderRadius: Corners.s6Border, - onPressed: () { - getIt().set(KVKeys.loginType, 'local'); - context.read().add(const SignInEvent.signedInAsGuest()); - }, + return BlocProvider( + create: (context) => HistoricalUserBloc() + ..add( + const HistoricalUserEvent.initial(), + ), + child: BlocListener( + listenWhen: (previous, current) => + previous.openedHistoricalUser != current.openedHistoricalUser, + listener: (context, state) async { + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + ); + }, + child: BlocBuilder( + builder: (context, state) { + if (state.historicalUsers.isEmpty) { + return RoundedTextButton( + title: LocaleKeys.signIn_loginAsGuestButtonText.tr(), + height: 48, + borderRadius: Corners.s6Border, + onPressed: () { + getIt().set(KVKeys.loginType, 'local'); + context + .read() + .add(const SignInEvent.signedInAsGuest()); + }, + ); + } else { + return RoundedTextButton( + title: LocaleKeys.signIn_continueAnonymousUser.tr(), + height: 48, + borderRadius: Corners.s6Border, + onPressed: () { + final bloc = context.read(); + final user = bloc.state.historicalUsers.first; + bloc.add(HistoricalUserEvent.openHistoricalUser(user)); + }, + ); + } + }, + ), + ), ); } } 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 1b97e8024b..6ee570851b 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 @@ -56,7 +56,7 @@ class SettingsUserViewBloc extends Bloc { emit(state.copyWith(historicalUsers: historicalUsers)); }, openHistoricalUser: (HistoricalUserPB historicalUser) async { - await _userService.openHistoricalUser(historicalUser); + await UserBackendService.openHistoricalUser(historicalUser); }, ); }); @@ -74,7 +74,7 @@ class SettingsUserViewBloc extends Bloc { } Future _loadHistoricalUsers() async { - final result = await _userService.loadHistoricalUsers(); + final result = await UserBackendService.loadHistoricalUsers(); result.fold( (historicalUsers) { add(SettingsUserEvent.didLoadHistoricalUsers(historicalUsers)); 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 deleted file mode 100644 index 121f49c2bd..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/historical_user.dart +++ /dev/null @@ -1,110 +0,0 @@ -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_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index d73e6cf7c1..6509a7ac7c 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 @@ -31,7 +31,11 @@ class SettingThirdPartyLogin extends StatelessWidget { builder: (_, __) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.medium(LocaleKeys.signIn_signInWith.tr()), + FlowyText.medium( + LocaleKeys.signIn_signInWith.tr(), + fontSize: 16, + ), + const VSpace(6), const ThirdPartySignInButtons( mainAxisAlignment: MainAxisAlignment.start, ), 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 0a66c5022b..f103ff799f 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,7 +16,6 @@ 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'; @@ -56,7 +55,7 @@ class SettingsUserView extends StatelessWidget { const VSpace(20), _renderCurrentOpenaiKey(context), const VSpace(20), - _renderHistoricalUser(context), + // _renderHistoricalUser(context), _renderLoginOrLogoutButton(context, state), const VSpace(20), ], @@ -129,16 +128,6 @@ class SettingsUserView extends StatelessWidget { ), ); } - - Widget _renderHistoricalUser(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return HistoricalUserList( - didOpenUser: didOpenUser, - ); - }, - ); - } } @visibleForTesting diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 608f145407..c213dce6cc 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -33,6 +33,7 @@ "loginTitle": "Login to @:appName", "loginButtonText": "Login", "loginAsGuestButtonText": "Get Started", + "continueAnonymousUser": "Continue in an anonymous session", "buttonText": "Sign In", "forgotPassword": "Forgot Password?", "emailHint": "Email", @@ -227,9 +228,9 @@ "logoutPrompt": "Are you sure to logout?", "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" + "historicalUserList": "User login history", + "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button", + "openHistoricalUser": "Click to open the anonymous account" }, "appearance": { "fontFamily": { 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 5e7f680c56..38914b6895 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -616,6 +616,7 @@ impl UserSession { user_id: uid, user_workspace, }; + self.cloud_services.set_auth_type(AuthType::Local); self.set_current_session(Some(session))?; Ok(()) }