From a8a9589af8a3c3321258ac47abb03b3fb50881ab Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 4 Oct 2023 02:23:01 +0700 Subject: [PATCH] TW-692: Implement `UploadProfile` in presentation layer --- assets/l10n/intl_en.arb | 7 +- lib/config/go_routes/go_router.dart | 4 +- lib/di/global/get_it_initializer.dart | 6 +- ...ilure.dart => update_profile_failure.dart} | 4 +- ...ading.dart => update_profile_loading.dart} | 4 +- .../settings/update_profile_success.dart | 21 + .../settings/upload_profile_success.dart | 19 - .../settings/update_profile_interactor.dart | 47 ++ .../settings/upload_profile_interactor.dart | 44 -- .../settings_dashboard/settings/settings.dart | 27 +- .../settings/settings_view.dart | 167 ++++--- .../settings_profile/settings_profile.dart | 417 ++++++++++++++---- .../settings_profile_item.dart | 55 ++- .../get_avatar_ui_state.dart | 25 ++ .../get_profile_ui_state.dart | 11 + .../settings_profile_ui_state.dart | 3 + .../settings_profile_view.dart | 28 +- .../settings_profile_view_mobile.dart | 188 +++++--- .../settings_profile_view_mobile_style.dart | 1 + .../settings_profile_view_style.dart | 17 + .../settings_profile_view_web.dart | 380 ++++++++-------- .../settings_profile_view_web_style.dart | 1 + lib/presentation/state/failure.dart | 10 + lib/presentation/state/success.dart | 16 + lib/utils/dialog/twake_loading_dialog.dart | 57 +++ .../adaptive_scaffold_primary_navigation.dart | 5 +- 26 files changed, 1019 insertions(+), 545 deletions(-) rename lib/domain/app_state/settings/{upload_profile_failure.dart => update_profile_failure.dart} (57%) rename lib/domain/app_state/settings/{upload_profile_loading.dart => update_profile_loading.dart} (56%) create mode 100644 lib/domain/app_state/settings/update_profile_success.dart delete mode 100644 lib/domain/app_state/settings/upload_profile_success.dart create mode 100644 lib/domain/usecase/settings/update_profile_interactor.dart delete mode 100644 lib/domain/usecase/settings/upload_profile_interactor.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart create mode 100644 lib/presentation/state/failure.dart create mode 100644 lib/presentation/state/success.dart create mode 100644 lib/utils/dialog/twake_loading_dialog.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 7916b90bde..4c38a9ffef 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1689,7 +1689,7 @@ "type": "text", "placeholders": {} }, - "search": "Search for people and channels", + "searchForPeopleAndChannels": "Search for people and channels", "@search": { "type": "text", "placeholders": {} @@ -2647,7 +2647,6 @@ "tapToAllowAccessToYourGallery": "Tap to allow access to your Gallery", "tapToAllowAccessToYourCamera": "You can enable camera access in the Settings app to make video calls in", "twake": "Twake", - "dismiss": "Dismiss", "permissionAccess": "Permission access", "allow": "Allow", "explainStoragePermission": "Twake need access to your storage to preview file", @@ -2669,7 +2668,6 @@ } }, "keyboard": "Keyboard", - "tapToAllowAccessToYourGallery": "Tap to allow access to your Gallery", "changeChatAvatar": "Change the Chat avatar", "roomAvatarMaxFileSize": "The avatar size is too large", "@roomAvatarMaxFileSize": {}, @@ -2755,5 +2753,6 @@ "editProfileDescriptions": "Update your profile with a new name, picture and a short introduction.", "workIdentitiesInfo": "WORK IDENTITIES INFO", "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", - "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard." + "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", + "changeProfilePhoto": "Change profile photo" } \ No newline at end of file diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 4e30640889..22955d5ef9 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -248,9 +248,7 @@ abstract class AppRoutes { path: 'profile', pageBuilder: (context, state) => defaultPageBuilder( context, - SettingsProfile( - profile: state.extra as Profile?, - ), + const SettingsProfile(), ), ), GoRoute( diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 78ccce98c0..cc728b5864 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -41,7 +41,7 @@ import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; -import 'package:fluffychat/domain/usecase/settings/upload_profile_interactor.dart'; +import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -178,8 +178,8 @@ class GetItInitializer { getIt.registerSingleton( TimelineSearchEventInteractor(), ); - getIt.registerSingleton( - UploadProfileInteractor(), + getIt.registerSingleton( + UpdateProfileInteractor(), ); } } diff --git a/lib/domain/app_state/settings/upload_profile_failure.dart b/lib/domain/app_state/settings/update_profile_failure.dart similarity index 57% rename from lib/domain/app_state/settings/upload_profile_failure.dart rename to lib/domain/app_state/settings/update_profile_failure.dart index 884b03e4c9..53e8b56a47 100644 --- a/lib/domain/app_state/settings/upload_profile_failure.dart +++ b/lib/domain/app_state/settings/update_profile_failure.dart @@ -1,9 +1,9 @@ import 'package:fluffychat/app_state/failure.dart'; -class UploadProfileFailure extends Failure { +class UpdateProfileFailure extends Failure { final dynamic exception; - const UploadProfileFailure(this.exception) : super(); + const UpdateProfileFailure(this.exception) : super(); @override List get props => [exception]; diff --git a/lib/domain/app_state/settings/upload_profile_loading.dart b/lib/domain/app_state/settings/update_profile_loading.dart similarity index 56% rename from lib/domain/app_state/settings/upload_profile_loading.dart rename to lib/domain/app_state/settings/update_profile_loading.dart index 05eede9199..badd69e078 100644 --- a/lib/domain/app_state/settings/upload_profile_loading.dart +++ b/lib/domain/app_state/settings/update_profile_loading.dart @@ -1,7 +1,7 @@ import 'package:fluffychat/app_state/success.dart'; -class UploadProfileLoading extends Success { - const UploadProfileLoading(); +class UpdateProfileLoading extends Success { + const UpdateProfileLoading(); @override List get props => []; diff --git a/lib/domain/app_state/settings/update_profile_success.dart b/lib/domain/app_state/settings/update_profile_success.dart new file mode 100644 index 0000000000..eebecda062 --- /dev/null +++ b/lib/domain/app_state/settings/update_profile_success.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/app_state/success.dart'; + +class UpdateProfileInitial extends Success { + @override + List get props => []; +} + +class UpdateProfileSuccess extends Success { + final Uri? avatar; + final String? displayName; + final bool isDeleteAvatar; + + const UpdateProfileSuccess({ + this.avatar, + this.displayName, + this.isDeleteAvatar = false, + }); + + @override + List get props => [avatar, displayName, isDeleteAvatar]; +} diff --git a/lib/domain/app_state/settings/upload_profile_success.dart b/lib/domain/app_state/settings/upload_profile_success.dart deleted file mode 100644 index 580f6ae630..0000000000 --- a/lib/domain/app_state/settings/upload_profile_success.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:fluffychat/app_state/success.dart'; - -class UploadProfileInitial extends Success { - @override - List get props => []; -} - -class UploadProfileSuccess extends Success { - final Uri? avatar; - final String? displayName; - - const UploadProfileSuccess({ - this.avatar, - this.displayName, - }); - - @override - List get props => [avatar, displayName]; -} diff --git a/lib/domain/usecase/settings/update_profile_interactor.dart b/lib/domain/usecase/settings/update_profile_interactor.dart new file mode 100644 index 0000000000..ff4f7aa7d3 --- /dev/null +++ b/lib/domain/usecase/settings/update_profile_interactor.dart @@ -0,0 +1,47 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_failure.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_loading.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; +import 'package:matrix/matrix.dart'; + +class UpdateProfileInteractor { + Stream> execute({ + required Client client, + Uri? avatarUrl, + bool isDeleteAvatar = false, + String? displayName, + }) async* { + yield const Right(UpdateProfileLoading()); + try { + Logs().d( + 'UploadProfileInteractor::execute(): Uri - $avatarUrl - displayName - $displayName', + ); + if (avatarUrl != null || isDeleteAvatar) { + await client.setAvatarUrl( + client.userID!, + avatarUrl ?? Uri.parse(''), + ); + } + if (displayName != null) { + await client.setDisplayName( + client.userID!, + displayName, + ); + } + yield Right( + UpdateProfileSuccess( + displayName: displayName, + avatar: avatarUrl, + isDeleteAvatar: isDeleteAvatar, + ), + ); + } catch (e) { + Logs().d( + 'UploadAvatarInteractor::execute(): Exception - $e}', + ); + yield Left(UpdateProfileFailure(e)); + } + } +} diff --git a/lib/domain/usecase/settings/upload_profile_interactor.dart b/lib/domain/usecase/settings/upload_profile_interactor.dart deleted file mode 100644 index 34b8f75973..0000000000 --- a/lib/domain/usecase/settings/upload_profile_interactor.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dartz/dartz.dart'; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/domain/app_state/settings/upload_profile_failure.dart'; -import 'package:fluffychat/domain/app_state/settings/upload_profile_loading.dart'; -import 'package:fluffychat/domain/app_state/settings/upload_profile_success.dart'; -import 'package:matrix/matrix.dart'; - -class UploadProfileInteractor { - Stream> execute({ - required Client client, - required String userId, - Uri? avatarUrl, - bool isUpdateDisPlayName = false, - String? displayName, - }) async* { - yield const Right(UploadProfileLoading()); - try { - Logs().d( - 'UploadAvatarInteractor::execute(): Uri - $avatarUrl - displayName - $displayName', - ); - if (avatarUrl != null) { - await client.setAvatarUrl( - userId, - avatarUrl, - ); - } - if (displayName != null) { - await client.setDisplayName(userId, displayName); - } - yield Right( - UploadProfileSuccess( - displayName: displayName, - avatar: avatarUrl, - ), - ); - } catch (e) { - Logs().d( - 'UploadAvatarInteractor::execute(): Exception - $e}', - ); - yield Left(UploadProfileFailure(e)); - } - } -} diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 720b0c28c3..17ae8a0342 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -33,9 +33,8 @@ class Settings extends StatefulWidget { } class SettingsController extends State with ConnectPageMixin { - final ValueNotifier profileNotifier = ValueNotifier( - Profile(userId: ''), - ); + final ValueNotifier avatarUriNotifier = ValueNotifier(Uri()); + final ValueNotifier displayNameNotifier = ValueNotifier(''); StreamSubscription? onAccountDataSubscription; @@ -53,7 +52,7 @@ class SettingsController extends State with ConnectPageMixin { final ValueNotifier optionsSelectNotifier = ValueNotifier(null); String get displayName => - profileNotifier.value.displayName ?? + displayNameNotifier.value ?? client.mxid(context).localpart ?? client.mxid(context); @@ -94,7 +93,8 @@ class SettingsController extends State with ConnectPageMixin { Logs().d( 'Settings::_getCurrentProfile() - currentProfile: $profile', ); - profileNotifier.value = profile; + avatarUriNotifier.value = profile.avatarUrl; + displayNameNotifier.value = profile.displayName; } void checkBootstrap() async { @@ -135,12 +135,9 @@ class SettingsController extends State with ConnectPageMixin { checkBootstrap(); } - void goToSettingsProfile(Profile? profile) async { + void goToSettingsProfile() async { optionsSelectNotifier.value = SettingEnum.profile; - context.push( - '/rooms/profile', - extra: profile, - ); + context.go('/rooms/profile'); } void onClickToSettingsItem(SettingEnum settingEnum) { @@ -179,7 +176,13 @@ class SettingsController extends State with ConnectPageMixin { void _handleOnAccountDataSubscription() { onAccountDataSubscription = client.onAccountData.stream.listen((event) { if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { - profileNotifier.value = Profile.fromJson(event.content); + final newProfile = Profile.fromJson(event.content); + if (newProfile.avatarUrl != avatarUriNotifier.value) { + avatarUriNotifier.value = newProfile.avatarUrl; + } + if (newProfile.displayName != displayNameNotifier.value) { + displayNameNotifier.value = newProfile.displayName; + } } }); } @@ -197,6 +200,8 @@ class SettingsController extends State with ConnectPageMixin { @override void dispose() { onAccountDataSubscription?.cancel(); + avatarUriNotifier.dispose(); + displayNameNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index bd76e69b33..668b3c7b0d 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -39,70 +39,66 @@ class SettingsView extends StatelessWidget { child: ListView( key: const Key('SettingsListViewContent'), children: [ - ValueListenableBuilder( - valueListenable: controller.profileNotifier, - builder: (context, profile, _) { - return Padding( - padding: SettingsViewStyle.bodySettingsScreenPadding, - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - color: controller.optionsSelectNotifier.value == - SettingEnum.profile - ? Theme.of(context).colorScheme.secondaryContainer - : null, - child: InkWell( - onTap: () => controller.goToSettingsProfile(profile), - child: Padding( - padding: SettingsViewStyle.itemBuilderPadding, - child: Row( - children: [ - Padding( + Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: controller.optionsSelectNotifier.value == + SettingEnum.profile + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: InkWell( + onTap: () => controller.goToSettingsProfile(), + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: controller.avatarUriNotifier, + builder: (context, avatarUrl, __) { + return Padding( padding: SettingsViewStyle.avatarPadding, - child: Stack( - children: [ - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) + child: Material( + elevation: Theme.of(context) .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: profile.avatarUrl, - name: controller.displayName, - size: AvatarStyle.defaultSize, - fontSize: - SettingsViewStyle.fontSizeAvatar, - ), + .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: avatarUrl, + name: controller.displayName, + size: AvatarStyle.defaultSize, + fontSize: SettingsViewStyle.fontSizeAvatar, + ), ), - ), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - profile.displayName ?? - controller.displayName, + ); + }, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: + controller.displayNameNotifier, + builder: (context, displayName, _) { + return Text( + displayName ?? controller.displayName, style: Theme.of(context) .textTheme .titleLarge @@ -113,37 +109,36 @@ class SettingsView extends StatelessWidget { ), maxLines: 1, overflow: TextOverflow.ellipsis, - ), - Text( - controller.client.mxid(context), - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - LinagoraRefColors.material() - .neutral[40], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + ); + }, ), - ), - const Icon( - Icons.chevron_right_outlined, - size: SettingsViewStyle.iconSize, - ), - ], + Text( + controller.client.mxid(context), + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: LinagoraRefColors.material() + .neutral[40], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, ), - ), - ], + ], + ), ), - ), + ], ), ), - ); - }, + ), + ), ), const Divider(thickness: 1), Column( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index dc0fa09e40..d05daeef85 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -1,53 +1,70 @@ -import 'dart:async'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:dartz/dartz.dart' hide State; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/room/upload_content_state.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; +import 'package:fluffychat/domain/usecase/room/upload_content_for_web_interactor.dart'; +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/settings_dashboard/settings_profile/settings_profile_item_style.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'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; 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/dialog/twake_loading_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:image_picker/image_picker.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'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfile extends StatefulWidget { - final Profile? profile; - const SettingsProfile({ super.key, - required this.profile, }); @override State createState() => SettingsProfileController(); } -class SettingsProfileController extends State { - final ValueNotifier profileNotifier = ValueNotifier( - Profile(userId: ''), - ); +class SettingsProfileController extends State + with CommonMediaPickerMixin, SingleImagePickerMixin { + final uploadProfileInteractor = getIt.get(); + final uploadContentInteractor = getIt.get(); + final uploadContentWebInteractor = + getIt.get(); + + Profile? currentProfile; + AssetEntity? assetEntity; + FilePickerResult? filePickerResult; final TwakeEventDispatcher twakeEventDispatcher = getIt.get(); final ValueNotifier isEditedProfileNotifier = ValueNotifier(false); + final ValueNotifier> settingsProfileUIState = + ValueNotifier>(Right(GetAvatarInitialUIState())); Client get client => Matrix.of(context).client; - MatrixState get matrix => Matrix.of(context); + bool get _hasEditedDisplayName => + displayNameEditingController.text != displayName; String get displayName => - profileNotifier.value.displayName ?? + currentProfile?.displayName ?? client.mxid(context).localpart ?? client.mxid(context); @@ -72,19 +89,14 @@ class SettingsProfileController extends State { ]; List> actions() => [ - if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, - isDefaultAction: true, - icon: Icons.camera_alt_outlined, - ), SheetAction( key: AvatarAction.file, - label: L10n.of(context)!.openGallery, - icon: Icons.photo_outlined, + label: L10n.of(context)!.changeProfilePhoto, + icon: Icons.add_a_photo_outlined, ), - if (profileNotifier.value.avatarUrl != null) + if (currentProfile?.avatarUrl != null || + assetEntity != null || + filePickerResult != null) SheetAction( key: AvatarAction.remove, label: L10n.of(context)!.removeYourAvatar, @@ -116,65 +128,95 @@ class SettingsProfileController extends State { } void _handleRemoveAvatarAction() async { - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(null), - ); - if (success.error == null) { - _getCurrentProfile(client, isUpdated: true); + if ((assetEntity != null || filePickerResult != null) && + currentProfile?.avatarUrl == null) { + _clearImageInLocal(); + return; } + TwakeLoadingDialog.showLoadingDialog(context); + final newProfile = Profile( + userId: client.userID!, + displayName: displayNameEditingController.text, + avatarUrl: null, + ); + settingsProfileUIState.value = + Right(GetProfileUIStateSuccess(newProfile)); + _uploadProfile(isDeleteAvatar: true); return; } - Future _handleGetAvatarInByte() async { + void _getImageOnWeb( + BuildContext context, + ) async { final result = await FilePicker.platform.pickFiles( type: FileType.image, - withData: true, ); - final pickedFile = result?.files.firstOrNull; - if (pickedFile == null || pickedFile.bytes == null) return null; - return MatrixFile( - bytes: pickedFile.bytes!, - name: pickedFile.name, + Logs().d( + 'SettingsProfile::_getImageOnWeb(): FilePickerResult - $result', ); + if (result == null || result.files.single.bytes == null) { + return; + } else { + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + settingsProfileUIState.value = Right( + GetAvatarInBytesUIStateSuccess( + filePickerResult: result, + ), + ); + Logs().d( + 'SettingsProfile::_getImageOnWeb(): AvatarWebNotifier - $result', + ); + } } - Future _handleGetAvatarInStream(AvatarAction action) async { - final result = await ImagePicker().pickImage( - source: action == AvatarAction.camera - ? ImageSource.camera - : ImageSource.gallery, - imageQuality: AppConfig.imageQuality, - ); - if (result == null) return null; - return MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); + void _showImagesPickerAction() async { + if (PlatformInfos.isWeb) { + _getImageOnWeb(context); + return; + } + final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionCamera = await getCurrentCameraPermission(); + if (currentPermissionPhotos != null && currentPermissionCamera != null) { + final imagePickerController = createImagePickerController(); + showImagePickerBottomSheet( + context, + currentPermissionPhotos, + currentPermissionCamera, + imagePickerController, + ); + } } - void _handleGetAvatarAction(AvatarAction action) async { - MatrixFile file; - if (PlatformInfos.isMobile) { - final matrixFile = await _handleGetAvatarInStream(action); - if (matrixFile == null) return; - file = matrixFile; - } else { - final matrixFile = await _handleGetAvatarInByte(); - if (matrixFile == null) return; - file = matrixFile; - } - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(file), + ImagePickerGridController createImagePickerController() { + final imagePickerController = ImagePickerGridController( + AssetCounter(imagePickerMode: ImagePickerMode.single), ); - if (success.error == null) { - _getCurrentProfile(client, isUpdated: true); - } + + imagePickerController.addListener(() { + final selectedAsset = imagePickerController.selectedAssets.firstOrNull; + if (selectedAsset?.asset.type == AssetType.image) { + if (!imagePickerController.pickFromCamera()) { + Navigator.pop(context); + } + settingsProfileUIState.value = Right( + GetAvatarInStreamUIStateSuccess( + assetEntity: selectedAsset?.asset, + ), + ); + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + imagePickerController.removeAllSelectedItem(); + } + }); + + return imagePickerController; } void setAvatarAction() async { - final action = actions().length == 1 + final action = actions().isEmpty ? actions().single.key : await showModalActionSheet( context: context, @@ -184,11 +226,14 @@ class SettingsProfileController extends State { if (action == null) return; if (action == AvatarAction.remove) { _handleRemoveAvatarAction(); + return; } - _handleGetAvatarAction(action); + _showImagesPickerAction(); } - void _handleSyncProfile() async { + void _sendAccountDataEvent({ + required Profile profile, + }) async { Logs().d( 'SettingsProfileController::_handleSyncProfile() - Syncing profile', ); @@ -196,7 +241,7 @@ class SettingsProfileController extends State { client: client, basicEvent: BasicEvent( type: TwakeInappEventTypes.uploadAvatarEvent, - content: profileNotifier.value.toJson(), + content: profile.toJson(), ), ); Logs().d( @@ -204,24 +249,170 @@ class SettingsProfileController extends State { ); } - void setDisplayNameAction() async { - if (displayNameFocusNode.hasFocus) { - displayNameFocusNode.unfocus(); + void _setAvatarInStream() { + if (assetEntity != null) { + uploadContentInteractor + .execute( + matrixClient: client, + entity: assetEntity!, + ) + .listen( + (event) => _handleUploadAvatarOnData(context, event), + onDone: _handleUploadAvatarOnDone, + onError: _handleUploadAvatarOnError, + ); + } else { + _uploadProfile(displayName: displayNameEditingController.text); } - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setDisplayName( - matrix.client.userID!, - displayNameEditingController.text, - ), - ); - if (success.error == null) { - isEditedProfileNotifier.toggle(); - _getCurrentProfile(client, isUpdated: true); + } + + void _setAvatarInBytes() { + if (filePickerResult != null) { + uploadContentWebInteractor + .execute( + matrixClient: client, + filePickerResult: filePickerResult!, + ) + .listen( + (event) => _handleUploadAvatarOnData(context, event), + onDone: _handleUploadAvatarOnDone, + onError: _handleUploadAvatarOnError, + ); + } else { + _uploadProfile(displayName: displayNameEditingController.text); + } + } + + void onUploadProfileAction() { + displayNameFocusNode.unfocus(); + TwakeLoadingDialog.showLoadingDialog(context); + if (PlatformInfos.isMobile) { + _setAvatarInStream(); + } else { + _setAvatarInBytes(); } } + void _clearImageInLocal() { + if (assetEntity != null) { + assetEntity = null; + } + if (filePickerResult != null) { + filePickerResult = null; + } + } + + void _handleUploadAvatarOnDone() { + Logs().d( + 'SettingsProfile::_handleUploadAvatarOnDone() - done', + ); + } + + void _handleUploadAvatarOnError( + dynamic error, + StackTrace? stackTrace, + ) { + TwakeLoadingDialog.hideLoadingDialog(context); + Logs().e( + 'SettingsProfile::_handleUploadAvatarOnError() - error: $error | stackTrace: $stackTrace', + ); + } + + void _handleUploadAvatarOnData( + BuildContext context, + Either event, + ) { + Logs().d('SettingsProfile::_handleUploadAvatarOnData()'); + event.fold( + (failure) { + Logs().e( + 'SettingsProfile::_handleUploadAvatarOnData() - failure: $failure', + ); + }, + (success) { + Logs().d( + 'SettingsProfile::_handleUploadAvatarOnData() - success: $success', + ); + if (success is UploadContentSuccess) { + _uploadProfile( + avatarUr: success.uri, + displayName: _hasEditedDisplayName + ? displayNameEditingController.text + : null, + ); + } + }, + ); + } + + void _uploadProfile({ + Uri? avatarUr, + String? displayName, + bool isDeleteAvatar = false, + }) async { + uploadProfileInteractor + .execute( + client: client, + avatarUrl: avatarUr, + isDeleteAvatar: isDeleteAvatar, + displayName: displayName, + ) + .listen( + (event) => _handleUploadProfileOnData(context, event), + onDone: _handleUploadProfileOnDone, + onError: _handleUploadProfileOnError, + ); + } + + void _handleUploadProfileOnDone() { + Logs().d( + 'SettingsProfile::_handleUploadProfileOnDone() - done', + ); + } + + void _handleUploadProfileOnError( + dynamic error, + StackTrace? stackTrace, + ) { + TwakeLoadingDialog.hideLoadingDialog(context); + Logs().e( + 'SettingsProfile::_handleUploadProfileOnError() - error: $error | stackTrace: $stackTrace', + ); + } + + void _handleUploadProfileOnData( + BuildContext context, + Either event, + ) { + Logs().d('SettingsProfile::_handleUploadProfileOnData()'); + event.fold( + (failure) { + Logs().e( + 'SettingsProfile::_handleUploadProfileOnData() - failure: $failure', + ); + }, + (success) { + Logs().d( + 'SettingsProfile::_handleUploadProfileOnData() - success: $success', + ); + if (success is UpdateProfileSuccess) { + _clearImageInLocal(); + final newProfile = Profile( + userId: client.userID!, + displayName: success.displayName ?? displayName, + avatarUrl: success.avatar ?? currentProfile?.avatarUrl, + ); + _sendAccountDataEvent(profile: newProfile); + if (!success.isDeleteAvatar) { + isEditedProfileNotifier.toggle(); + } + _getCurrentProfile(client, isUpdated: true); + TwakeLoadingDialog.hideLoadingDialog(context); + } + }, + ); + } + void _getCurrentProfile( Client client, { isUpdated = false, @@ -234,10 +425,16 @@ class SettingsProfileController extends State { Logs().d( 'SettingsProfileController::_getCurrentProfile() - currentProfile: $profile', ); - profileNotifier.value = profile; + settingsProfileUIState.value = + Right(GetProfileUIStateSuccess(profile)); + Logs().d( + 'SettingsProfileController::_getCurrentProfile() - currentProfile: ${settingsProfileUIState.value}', + ); + if (profile.avatarUrl == null) { + _clearImageInLocal(); + } displayNameEditingController.text = displayName; matrixIdEditingController.text = client.mxid(context); - _handleSyncProfile(); } void handleTextEditOnChange(SettingsProfileEnum settingsProfileEnum) { @@ -251,6 +448,10 @@ class SettingsProfileController extends State { } void _listeningDisplayNameHasChange() { + if (displayNameEditingController.text.isEmpty) { + isEditedProfileNotifier.value = false; + return; + } isEditedProfileNotifier.value = displayNameEditingController.text != displayName; Logs().d( @@ -258,16 +459,6 @@ class SettingsProfileController extends State { ); } - void _initProfile() { - if (widget.profile == null) { - _getCurrentProfile(client); - return; - } - profileNotifier.value = widget.profile!; - displayNameEditingController.text = displayName; - matrixIdEditingController.text = client.mxid(context); - } - void copyEventsAction(SettingsProfileEnum settingsProfileEnum) { switch (settingsProfileEnum) { case SettingsProfileEnum.matrixId: @@ -290,9 +481,39 @@ class SettingsProfileController extends State { } } + void _handleViewState() { + settingsProfileUIState.addListener(() { + Logs().d( + "settingsProfileUIState()::_handleViewState(): ${settingsProfileUIState.value}", + ); + settingsProfileUIState.value.fold( + (failure) => null, + (success) { + switch (success.runtimeType) { + case GetAvatarInStreamUIStateSuccess: + final uiState = success as GetAvatarInStreamUIStateSuccess; + assetEntity = uiState.assetEntity; + break; + case GetAvatarInBytesUIStateSuccess: + final uiState = success as GetAvatarInBytesUIStateSuccess; + filePickerResult = uiState.filePickerResult; + break; + case GetProfileUIStateSuccess: + final uiState = success as GetProfileUIStateSuccess; + currentProfile = uiState.profile; + break; + default: + break; + } + }, + ); + }); + } + @override void initState() { - _initProfile(); + _handleViewState(); + _getCurrentProfile(client); super.initState(); } @@ -301,6 +522,8 @@ class SettingsProfileController extends State { displayNameEditingController.dispose(); matrixIdEditingController.dispose(); displayNameFocusNode.dispose(); + settingsProfileUIState.dispose(); + isEditedProfileNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart index 3ca594a2c1..a3054c9c05 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart @@ -1,3 +1,6 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; @@ -14,6 +17,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { final IconData? leadingIcon; final void Function(String, SettingsProfileEnum)? onChange; final VoidCallback? onCopyAction; + final ValueNotifier> settingsProfileUIState; const SettingsProfileItemBuilder({ super.key, @@ -26,6 +30,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { this.leadingIcon, this.onChange, this.onCopyAction, + required this.settingsProfileUIState, }); @override @@ -57,29 +62,35 @@ class SettingsProfileItemBuilder extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - TextField( - onChanged: (value) => - onChange!(value, settingsProfileEnum), - readOnly: !settingsProfilePresentation.isEditable, - autofocus: false, - focusNode: focusNode, - controller: textEditingController, - decoration: InputDecoration( - suffixIcon: IconButton( - onPressed: settingsProfilePresentation.isEditable - ? () { - focusNode?.requestFocus(); - } - : onCopyAction, - icon: Icon( - suffixIcon, - size: SettingsProfileItemStyle.iconSize, - color: - Theme.of(context).colorScheme.onSurfaceVariant, + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, _, __) { + return TextField( + onChanged: (value) => + onChange!(value, settingsProfileEnum), + readOnly: !settingsProfilePresentation.isEditable, + autofocus: false, + focusNode: focusNode, + controller: textEditingController, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: settingsProfilePresentation.isEditable + ? () { + focusNode?.requestFocus(); + } + : onCopyAction, + icon: Icon( + suffixIcon, + size: SettingsProfileItemStyle.iconSize, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + hintText: textEditingController?.text, ), - ), - hintText: textEditingController?.text, - ), + ); + }, ), Divider( height: SettingsProfileItemStyle.dividerSize, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart new file mode 100644 index 0000000000..8dcaa68b54 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart @@ -0,0 +1,25 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; + +class GetAvatarInitialUIState extends SettingsProfileUIState {} + +class GetAvatarInStreamUIStateSuccess extends SettingsProfileUIState { + final AssetEntity? assetEntity; + + GetAvatarInStreamUIStateSuccess({ + this.assetEntity, + }); + + @override + List get props => [assetEntity]; +} + +class GetAvatarInBytesUIStateSuccess extends SettingsProfileUIState { + final FilePickerResult? filePickerResult; + + GetAvatarInBytesUIStateSuccess({this.filePickerResult}); + + @override + List get props => [filePickerResult]; +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart new file mode 100644 index 0000000000..7e488049b5 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart @@ -0,0 +1,11 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart'; +import 'package:matrix/matrix.dart'; + +class GetProfileUIStateSuccess extends SettingsProfileUIState { + final Profile profile; + + GetProfileUIStateSuccess(this.profile); + + @override + List get props => [profile]; +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart new file mode 100644 index 0000000000..54f00f7e64 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart @@ -0,0 +1,3 @@ +import 'package:fluffychat/presentation/state/success.dart'; + +abstract class SettingsProfileUIState extends UIState {} 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 eda5b7388a..90df760cc3 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart'; import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; @@ -32,7 +33,7 @@ class SettingsProfileView extends StatelessWidget { leading: IconButton( icon: const Icon( Icons.arrow_back, - size: 24, + size: SettingsProfileViewStyle.sizeIcon, ), onPressed: () => context.pop(), ), @@ -49,14 +50,11 @@ class SettingsProfileView extends StatelessWidget { if (!edited) return const SizedBox(); return InkWell( borderRadius: BorderRadius.circular( - 20, + SettingsProfileViewStyle.borderRadius, ), - onTap: () => controller.setDisplayNameAction(), + onTap: () => controller.onUploadProfileAction(), child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - vertical: 14, - horizontal: 12, - ), + padding: SettingsProfileViewStyle.paddingTextButton, child: Text( L10n.of(context)!.done, style: Theme.of(context).textTheme.labelLarge?.copyWith( @@ -74,7 +72,7 @@ class SettingsProfileView extends StatelessWidget { ? Theme.of(context).colorScheme.surface : null, body: SingleChildScrollView( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 16), + padding: SettingsProfileViewStyle.paddingBody, child: SlotLayout( config: { const WidthPlatformBreakpoint( @@ -83,8 +81,8 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewMobileKey, builder: (_) { return SettingsProfileViewMobile( - profileNotifier: controller.profileNotifier, - displayName: controller.displayName, + client: controller.client, + settingsProfileUIState: controller.settingsProfileUIState, onAvatarTap: () => controller.setAvatarAction(), settingsProfileOptions: ListView.separated( shrinkWrap: true, @@ -95,6 +93,8 @@ class SettingsProfileView extends StatelessWidget { controller.getListProfileMobile[index], title: controller.getListProfileMobile[index] .getTitle(context), + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfilePresentation: SettingsProfilePresentation( settingsProfileType: controller @@ -134,13 +134,15 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewWebKey, builder: (_) { return SettingsProfileViewWeb( - profileNotifier: controller.profileNotifier, - displayName: controller.displayName, + settingsProfileUIState: controller.settingsProfileUIState, + client: controller.client, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return SettingsProfileItemBuilder( + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfileEnum: controller.getListProfileBasicInfo[index], title: controller.getListProfileBasicInfo[index] @@ -175,6 +177,8 @@ class SettingsProfileView extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return SettingsProfileItemBuilder( + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfileEnum: controller.getListProfileWorkIdentitiesInfo[index], title: controller 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 44ae1e804f..27fa0e98e6 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 @@ -1,98 +1,152 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.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'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; 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'; class SettingsProfileViewMobile extends StatelessWidget { - final ValueNotifier profileNotifier; + final ValueNotifier> settingsProfileUIState; final Widget settingsProfileOptions; final VoidCallback onAvatarTap; - final String displayName; + final Client client; const SettingsProfileViewMobile({ super.key, - required this.profileNotifier, required this.settingsProfileOptions, required this.onAvatarTap, - required this.displayName, + required this.settingsProfileUIState, + required this.client, }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: profileNotifier, - builder: (context, profile, __) { - 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, - ), - Material( - elevation: - Theme.of(context).appBarTheme.scrolledUnderElevation ?? + 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) { + if (success.assetEntity == null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: AssetEntityImage( + 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), + ); + }, + ), + ), + ); + } + 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: profileNotifier.value.avatarUrl, - name: displayName, - size: SettingsProfileViewMobileStyle.avatarSize, - fontSize: SettingsProfileViewMobileStyle.avatarFontSize, - ), - ), - Positioned( - bottom: SettingsProfileViewMobileStyle.positionedBottomSize, - right: SettingsProfileViewMobileStyle.positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewMobileStyle.avatarSize, + shadowColor: Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, ), - border: Border.all( - color: Theme.of(context).colorScheme.onPrimary, - width: SettingsProfileViewMobileStyle - .iconEditBorderWidth, + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), ), - padding: SettingsProfileViewMobileStyle.editIconPadding, - child: Icon( - Icons.edit, - size: SettingsProfileViewMobileStyle.iconEditSize, - color: Theme.of(context).colorScheme.onPrimary, + 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: InkWell( + onTap: onAvatarTap, + 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, ), ), - ], + ), ), - ), - settingsProfileOptions, - ], - ); - }, + ], + ), + ), + settingsProfileOptions, + ], ); } } 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 3ba1345b5e..76f99dfdef 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 @@ -9,6 +9,7 @@ class SettingsProfileViewMobileStyle { static const double iconEditBorderWidth = 4; static const double iconEditSize = 24; static const double dividerHeight = 2; + static const int thumbnailSize = 28; static EdgeInsetsDirectional padding = const EdgeInsetsDirectional.symmetric(vertical: 16.0); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart new file mode 100644 index 0000000000..628e507718 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class SettingsProfileViewStyle { + static const sizeIcon = 24.0; + static const borderRadius = 20.0; + + static const EdgeInsetsDirectional paddingTextButton = + EdgeInsetsDirectional.symmetric( + vertical: 14, + horizontal: 12, + ); + + static const EdgeInsetsDirectional paddingBody = + EdgeInsetsDirectional.symmetric( + horizontal: 16, + ); +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart index 8af3562d0c..2a4435ff63 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -1,4 +1,10 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.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'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -7,215 +13,245 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class SettingsProfileViewWeb extends StatelessWidget { - final ValueNotifier profileNotifier; + final ValueNotifier> settingsProfileUIState; final Widget basicInfoWidget; final Widget workIdentitiesInfoWidget; final VoidCallback onAvatarTap; - final String displayName; + final Client client; const SettingsProfileViewWeb({ super.key, - required this.profileNotifier, required this.basicInfoWidget, required this.onAvatarTap, required this.workIdentitiesInfoWidget, - required this.displayName, + required this.client, + required this.settingsProfileUIState, }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: profileNotifier, - builder: (context, profile, __) { - return Padding( - padding: SettingsProfileViewWebStyle.paddingBody, - child: Center( - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: SettingsProfileViewWebStyle.bodyWidth, - padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - SettingsProfileViewWebStyle.radiusCircular, - ), + return Padding( + padding: SettingsProfileViewWebStyle.paddingBody, + child: Center( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.basicInfo, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), ), - child: Column( - mainAxisSize: MainAxisSize.min, + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: - SettingsProfileViewWebStyle.paddingBasicInfoTitle, - child: Text( - L10n.of(context)!.basicInfo, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetBasicInfo, - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - const SizedBox( - width: - SettingsProfileViewWebStyle.widthSize, - ), - 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: - profileNotifier.value.avatarUrl, - name: displayName, - size: SettingsProfileViewWebStyle - .avatarSize, - fontSize: SettingsProfileViewWebStyle - .avatarFontSize, - ), - ), - Positioned( - bottom: SettingsProfileViewWebStyle - .positionedBottomSize, - right: SettingsProfileViewWebStyle - .positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary, - borderRadius: BorderRadius.circular( + padding: SettingsProfileViewWebStyle + .paddingWidgetBasicInfo, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: SettingsProfileViewWebStyle.widthSize, + ), + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, uiState, child) => + uiState.fold( + (failure) => child!, + (success) { + if (success + is GetAvatarInBytesUIStateSuccess) { + if (success.filePickerResult == null || + success.filePickerResult?.files.single + .bytes == + null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( SettingsProfileViewWebStyle - .avatarSize, + .radiusImageMemory, + ), + 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, ), - border: Border.all( - color: Theme.of(context) - .colorScheme - .onPrimary, - width: 4, + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), ), - padding: SettingsProfileViewWebStyle - .paddingEditIcon, - child: Icon( - Icons.edit, + child: Avatar( + mxContent: success.profile.avatarUrl, + name: displayName, size: SettingsProfileViewWebStyle - .iconEditSize, - color: Theme.of(context) - .colorScheme - .onPrimary, + .avatarSize, + fontSize: SettingsProfileViewWebStyle + .avatarFontSize, ), + ); + } + return child!; + }, + ), + child: const SizedBox.shrink(), + ), + Positioned( + bottom: SettingsProfileViewWebStyle + .positionedBottomSize, + right: SettingsProfileViewWebStyle + .positionedRightSize, + child: InkWell( + onTap: onAvatarTap, + child: Container( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.avatarSize, + ), + border: Border.all( + color: Theme.of(context) + .colorScheme + .onPrimary, + width: 4, ), ), + padding: SettingsProfileViewWebStyle + .paddingEditIcon, + child: Icon( + Icons.edit, + size: SettingsProfileViewWebStyle + .iconEditSize, + color: Theme.of(context) + .colorScheme + .onPrimary, + ), ), - ], + ), ), - ), - Expanded( - child: basicInfoWidget, - ), - ], - ), - ], - ), - ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetEditProfileInfo, - child: Text( - L10n.of(context)!.editProfileDescriptions, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: LinagoraRefColors.material().tertiary[30], + ], ), - ), - ), - Container( - width: SettingsProfileViewWebStyle.bodyWidth, - padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - SettingsProfileViewWebStyle.radiusCircular, ), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: - SettingsProfileViewWebStyle.paddingBasicInfoTitle, - child: Text( - L10n.of(context)!.workIdentitiesInfo, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ), + Expanded( + child: basicInfoWidget, ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWorkIdentitiesInfoWidget, - child: workIdentitiesInfoWidget, - ) ], ), - ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetEditProfileInfo, - child: Text( - L10n.of(context)!.editWorkIdentitiesDescriptions, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: LinagoraRefColors.material().tertiary[30], - ), + ], + ), + ), + Padding( + padding: + SettingsProfileViewWebStyle.paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editProfileDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ), + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, ), ), - ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.workIdentitiesInfo, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Padding( + padding: SettingsProfileViewWebStyle + .paddingWorkIdentitiesInfoWidget, + child: workIdentitiesInfoWidget, + ) + ], + ), + ), + Padding( + padding: + SettingsProfileViewWebStyle.paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editWorkIdentitiesDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), ), - ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart index a6a4680f2f..6c99274d42 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart @@ -11,6 +11,7 @@ class SettingsProfileViewWebStyle { static const double iconEditSize = 24; static const double dividerHeight = 2; static const double radiusCircular = 16; + static const double radiusImageMemory = 48; static const EdgeInsetsDirectional paddingBody = EdgeInsetsDirectional.all(32); diff --git a/lib/presentation/state/failure.dart b/lib/presentation/state/failure.dart new file mode 100644 index 0000000000..40b74011ae --- /dev/null +++ b/lib/presentation/state/failure.dart @@ -0,0 +1,10 @@ +import 'package:fluffychat/app_state/failure.dart'; + +abstract class FeatureFailure extends Failure { + final dynamic exception; + + const FeatureFailure({this.exception}); + + @override + List get props => [exception]; +} diff --git a/lib/presentation/state/success.dart b/lib/presentation/state/success.dart new file mode 100644 index 0000000000..333c5147f3 --- /dev/null +++ b/lib/presentation/state/success.dart @@ -0,0 +1,16 @@ +import 'package:fluffychat/app_state/success.dart'; + +abstract class ViewState extends Success {} + +abstract class ViewEvent extends Success {} + +class UIState extends ViewState { + static final idle = UIState(); + + UIState() : super(); + + @override + List get props => []; +} + +class LoadingState extends UIState {} diff --git a/lib/utils/dialog/twake_loading_dialog.dart b/lib/utils/dialog/twake_loading_dialog.dart new file mode 100644 index 0000000000..6e1acbe63e --- /dev/null +++ b/lib/utils/dialog/twake_loading_dialog.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; + +class TwakeLoadingDialog { + static void hideLoadingDialog(BuildContext context) { + if (PlatformInfos.isWeb) { + TwakeApp.router.routerDelegate.pop(); + } else { + Navigator.pop(context); + } + } + + static void showLoadingDialog(BuildContext context) { + showGeneralDialog( + useRootNavigator: PlatformInfos.isWeb, + transitionDuration: const Duration(milliseconds: 700), + transitionBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: Tween(begin: 0, end: 1).animate(animation), + child: WillPopScope( + onWillPop: () async => false, + child: const ProgressDialog(), + ), + ); + }, + context: context, + pageBuilder: (c, a1, a2) { + return const SizedBox(); + }, + ); + } +} + +class ProgressDialog extends StatelessWidget { + const ProgressDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const AlertDialog( + content: Row( + children: [ + Padding( + padding: EdgeInsets.only(right: 16.0), + child: CircularProgressIndicator.adaptive(), + ), + Expanded( + child: Text( + 'Loading... Please Wait!', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart index 3739c32ec4..5a883f93e3 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart @@ -76,7 +76,10 @@ class _AdaptiveScaffoldPrimaryNavigationState void _handleOnAccountDataSubscription() { onAccountDataSubscription = client.onAccountData.stream.listen((event) { if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { - profileNotifier.value = Profile.fromJson(event.content); + final newProfile = Profile.fromJson(event.content); + if (newProfile.avatarUrl != profileNotifier.value.avatarUrl) { + profileNotifier.value = Profile.fromJson(event.content); + } } }); }