diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ef763c4c99..1be249dc65 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3060,6 +3060,8 @@ "user": {} } }, + "switchAccounts": "Switch accounts", + "selectAccount": "Select account", "privacyPolicy": "Privacy Policy", "byContinuingYourAgreeingToOur": "By continuing, you're agreeing to our" } diff --git a/lib/pages/multiple_accounts/multiple_accounts_picker.dart b/lib/pages/multiple_accounts/multiple_accounts_picker.dart index 49c17e8a64..1cd1e4cfe6 100644 --- a/lib/pages/multiple_accounts/multiple_accounts_picker.dart +++ b/lib/pages/multiple_accounts/multiple_accounts_picker.dart @@ -13,6 +13,8 @@ import 'package:linagora_design_flutter/multiple_account/models/twake_presentati import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +typedef OnGoToAccountSettings = void Function(TwakePresentationAccount account); + class MultipleAccountsPickerController { final BuildContext context; diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index bd33e7b0aa..18e3c2534b 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -305,6 +305,14 @@ class SettingsController extends State with ConnectPageMixin { super.initState(); } + @override + void didUpdateWidget(Settings oldWidget) { + if (oldWidget != widget) { + _getCurrentProfile(client); + } + super.didUpdateWidget(oldWidget); + } + @override void dispose() { onAccountDataSubscription?.cancel(); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 251850d393..168b8c934e 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; +import 'package:fluffychat/pages/multiple_accounts/multiple_accounts_picker.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; @@ -20,6 +21,7 @@ import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/single_image_picker_mixin.dart'; +import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -29,6 +31,7 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; @@ -61,6 +64,8 @@ class SettingsProfileController extends State AssetEntity? assetEntity; FilePickerResult? filePickerResult; + ValueNotifier haveMultipleAccountsNotifier = ValueNotifier(false); + final TwakeEventDispatcher twakeEventDispatcher = getIt.get(); @@ -538,6 +543,21 @@ class SettingsProfileController extends State } } + Future get accountsCount async + // FIXME: Change to false after merging TW-1262 + => + (await ClientManager.getClients(initialize: true)).length; + + void onBottomButtonTap() { + MultipleAccountsPickerController(context: context) + .showMultipleAccountsPicker( + client, + onGoToAccountSettings: () { + context.go('/rooms/profile'); + }, + ); + } + void _handleViewState() { settingsProfileUIState.addListener(() { Logs().d( @@ -583,6 +603,9 @@ class SettingsProfileController extends State void initState() { _handleViewState(); _getCurrentProfile(client); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + haveMultipleAccountsNotifier.value = await accountsCount > 1; + }); super.initState(); } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index 2c4aac22c2..2f65da80d7 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -75,7 +75,7 @@ class SettingsProfileView extends StatelessWidget { backgroundColor: responsive.isWebDesktop(context) ? Theme.of(context).colorScheme.surface : LinagoraSysColors.material().onPrimary, - body: SingleChildScrollView( + body: Padding( padding: SettingsProfileViewStyle.paddingBody, child: SlotLayout( config: { @@ -88,6 +88,9 @@ class SettingsProfileView extends StatelessWidget { client: controller.client, settingsProfileUIState: controller.settingsProfileUIState, onTapAvatar: controller.onTapAvatarInMobile, + onBottomButtonTap: controller.onBottomButtonTap, + haveMultipleAccountsNotifier: + controller.haveMultipleAccountsNotifier, menuChildren: controller.listContextMenuBuilder(context), menuController: controller.menuController, settingsProfileOptions: ListView.separated( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index c3d36bcc7b..3280a52e03 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -12,13 +12,16 @@ import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class SettingsProfileViewMobile extends StatelessWidget { final ValueNotifier> settingsProfileUIState; + final VoidCallback onBottomButtonTap; final Widget settingsProfileOptions; final VoidCallback? onTapAvatar; final List? menuChildren; final MenuController? menuController; + final ValueNotifier haveMultipleAccountsNotifier; final Client client; const SettingsProfileViewMobile({ @@ -27,6 +30,8 @@ class SettingsProfileViewMobile extends StatelessWidget { required this.onTapAvatar, required this.settingsProfileUIState, required this.client, + required this.onBottomButtonTap, + required this.haveMultipleAccountsNotifier, this.menuChildren, this.menuController, }); @@ -35,167 +40,218 @@ class SettingsProfileViewMobile extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Divider( - height: SettingsProfileViewMobileStyle.dividerHeight, - color: LinagoraStateLayer( - LinagoraSysColors.material().surfaceTint, - ).opacityLayer3, - ), - Padding( - padding: SettingsProfileViewMobileStyle.padding, - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - const SizedBox( - width: SettingsProfileViewMobileStyle.widthSize, - ), - ValueListenableBuilder( - valueListenable: settingsProfileUIState, - builder: (context, uiState, child) => uiState.fold( - (failure) => child!, - (success) { - if (success is GetAvatarInStreamUIStateSuccess && - PlatformInfos.isMobile) { - if (success.assetEntity == null) { - return child!; - } - return ClipOval( - child: SizedBox.fromSize( - size: const Size.fromRadius( - AvatarStyle.defaultSize, - ), - child: AssetEntityImage( - width: AvatarStyle.defaultSize, - height: AvatarStyle.defaultSize, - success.assetEntity!, - thumbnailSize: const ThumbnailSize( - SettingsProfileViewMobileStyle.thumbnailSize, - SettingsProfileViewMobileStyle.thumbnailSize, + Column( + children: [ + Divider( + height: SettingsProfileViewMobileStyle.dividerHeight, + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer3, + ), + Padding( + padding: SettingsProfileViewMobileStyle.padding, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: SettingsProfileViewMobileStyle.widthSize, + ), + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, uiState, child) => uiState.fold( + (failure) => child!, + (success) { + if (success is GetAvatarInStreamUIStateSuccess && + PlatformInfos.isMobile) { + if (success.assetEntity == null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: AssetEntityImage( + width: AvatarStyle.defaultSize, + height: AvatarStyle.defaultSize, + success.assetEntity!, + thumbnailSize: const ThumbnailSize( + SettingsProfileViewMobileStyle.thumbnailSize, + SettingsProfileViewMobileStyle.thumbnailSize, + ), + fit: BoxFit.cover, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress != null && + loadingProgress.cumulativeBytesLoaded != + loadingProgress.expectedTotalBytes) { + return const Center( + child: + CircularProgressIndicator.adaptive(), + ); + } + return child; + }, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon(Icons.error_outline), + ); + }, + ), ), - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress != null && - loadingProgress.cumulativeBytesLoaded != - loadingProgress.expectedTotalBytes) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - return child; - }, - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon(Icons.error_outline), - ); - }, - ), - ), - ); - } - if (success is GetAvatarInBytesUIStateSuccess && - PlatformInfos.isWeb) { - if (success.filePickerResult == null || - success.filePickerResult?.files.single.bytes == - null) { - return child!; - } - return ClipOval( - child: SizedBox.fromSize( - size: const Size.fromRadius( - AvatarStyle.defaultSize, - ), - child: Image.memory( - success.filePickerResult!.files.single.bytes!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon(Icons.error_outline), - ); - }, - ), - ), - ); - } - if (success is GetProfileUIStateSuccess) { - final displayName = success.profile.displayName ?? - client.mxid(context).localpart ?? - client.mxid(context); - return Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context).appBarTheme.shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: success.profile.avatarUrl, - name: displayName, - size: SettingsProfileViewMobileStyle.avatarSize, - fontSize: - SettingsProfileViewMobileStyle.avatarFontSize, - ), - ); - } - return child!; - }, - ), - child: const SizedBox.shrink(), - ), - Positioned( - bottom: SettingsProfileViewMobileStyle.positionedBottomSize, - right: SettingsProfileViewMobileStyle.positionedRightSize, - child: MenuAnchor( - controller: menuController, - builder: ( - BuildContext context, - MenuController menuController, - Widget? child, - ) { - return GestureDetector( - onTap: () { - if (PlatformInfos.isWeb) { - menuController.isOpen - ? menuController.close() - : menuController.open(); - } else { - onTapAvatar?.call(); + ); + } + if (success is GetAvatarInBytesUIStateSuccess && + PlatformInfos.isWeb) { + if (success.filePickerResult == null || + success.filePickerResult?.files.single.bytes == + null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: Image.memory( + success.filePickerResult!.files.single.bytes!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon(Icons.error_outline), + ); + }, + ), + ), + ); + } + if (success is GetProfileUIStateSuccess) { + final displayName = success.profile.displayName ?? + client.mxid(context).localpart ?? + client.mxid(context); + return Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: + Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: success.profile.avatarUrl, + name: displayName, + size: SettingsProfileViewMobileStyle.avatarSize, + fontSize: + SettingsProfileViewMobileStyle.avatarFontSize, + ), + ); } + return child!; }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewMobileStyle.avatarSize, + ), + child: const SizedBox.shrink(), + ), + Positioned( + bottom: SettingsProfileViewMobileStyle.positionedBottomSize, + right: SettingsProfileViewMobileStyle.positionedRightSize, + child: MenuAnchor( + controller: menuController, + builder: ( + BuildContext context, + MenuController menuController, + Widget? child, + ) { + return GestureDetector( + onTap: () { + if (PlatformInfos.isWeb) { + menuController.isOpen + ? menuController.close() + : menuController.open(); + } else { + onTapAvatar?.call(); + } + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.avatarSize, + ), + border: Border.all( + color: Theme.of(context).colorScheme.onPrimary, + width: SettingsProfileViewMobileStyle + .iconEditBorderWidth, + ), + ), + padding: + SettingsProfileViewMobileStyle.editIconPadding, + child: Icon( + Icons.edit, + size: SettingsProfileViewMobileStyle.iconEditSize, + color: Theme.of(context).colorScheme.onPrimary, + ), ), - border: Border.all( + ); + }, + menuChildren: menuChildren ?? [], + ), + ), + ], + ), + ), + settingsProfileOptions, + ], + ), + const Expanded(child: SizedBox()), + InkWell( + onTap: onBottomButtonTap, + child: Container( + width: double.infinity, + height: SettingsProfileViewMobileStyle.bottomButtonHeight, + padding: SettingsProfileViewMobileStyle.paddingBottomButton, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.bottomButtonRadius, + ), + color: Theme.of(context).colorScheme.primary, + ), + alignment: Alignment.center, + child: ValueListenableBuilder( + valueListenable: haveMultipleAccountsNotifier, + builder: (context, haveMultipleAccounts, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + haveMultipleAccounts + ? Icons.group_outlined + : Icons.person_add_alt_outlined, + size: SettingsProfileViewMobileStyle.iconSize, + color: Theme.of(context).colorScheme.onPrimary, + ), + SettingsProfileViewMobileStyle.paddingIconAndText, + Text( + haveMultipleAccounts + ? L10n.of(context)!.switchAccounts + : L10n.of(context)!.addAnotherAccount, + style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.onPrimary, - width: SettingsProfileViewMobileStyle - .iconEditBorderWidth, ), - ), - padding: SettingsProfileViewMobileStyle.editIconPadding, - child: Icon( - Icons.edit, - size: SettingsProfileViewMobileStyle.iconEditSize, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ); - }, - menuChildren: menuChildren ?? [], - ), - ), - ], + ), + ], + ); + }, + ), ), ), - settingsProfileOptions, + SettingsProfileViewMobileStyle.paddingBottomBottomButton, ], ); } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart index 76f99dfdef..7bdc84a8ad 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart @@ -16,4 +16,13 @@ class SettingsProfileViewMobileStyle { static EdgeInsetsDirectional editIconPadding = const EdgeInsetsDirectional.all(8); + + static const bottomButtonHeight = 48.0; + static const paddingBottomButton = EdgeInsets.only(left: 16.0, right: 16.0); + static const bottomButtonRadius = 100.0; + static const iconSize = 18.0; + + static const paddingIconAndText = SizedBox(width: 8.0); + + static const paddingBottomBottomButton = SizedBox(height: 32.0); } diff --git a/lib/widgets/twake_components/twake_header_style.dart b/lib/widgets/twake_components/twake_header_style.dart index 922c98bff9..caf0da79f6 100644 --- a/lib/widgets/twake_components/twake_header_style.dart +++ b/lib/widgets/twake_components/twake_header_style.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; class TwakeHeaderStyle { static ResponsiveUtils responsive = getIt.get(); @@ -16,8 +16,6 @@ class TwakeHeaderStyle { static double get avatarFontSizeInAppBar => 14.0; static const double avatarOfMultipleAccountSize = 48.0; - static const double logoAppOfMultipleHeight = 28.0; - static const double logoAppOfMultipleWidth = 152.0; static bool isDesktop(BuildContext context) => responsive.isDesktop(context); @@ -46,4 +44,9 @@ class TwakeHeaderStyle { static const EdgeInsetsDirectional counterSelectionPadding = EdgeInsetsDirectional.only(start: 4); + + static TextStyle? selectAccountTextStyle(BuildContext context) => + Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ); }