From 601ca0efe6398357ef5a5d4b793e74231ea4c13f Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 20 Nov 2025 21:00:31 +0700 Subject: [PATCH 01/13] TW-2710: Add settings for displaying and managing contact info visibility --- assets/l10n/intl_en.arb | 9 + lib/config/go_routes/go_router.dart | 9 + .../user_info/user_info_datasource.dart | 3 + .../user_info/user_info_datasource_impl.dart | 6 + lib/data/network/user_info/user_info_api.dart | 26 +++ .../repository/user_info_repository_impl.dart | 6 + lib/di/global/get_it_initializer.dart | 4 + .../get_user_info_visibility_state.dart | 26 +++ .../model/user_info/user_info_visibility.dart | 37 ++++ .../user_info_visibility_request.dart | 29 +++ .../user_info/user_info_repository.dart | 3 + .../get_user_info_visibility_interactor.dart | 24 +++ .../settings_contacts_visibility.dart | 30 ++++ .../settings_contacts_visibility_enum.dart | 30 ++++ .../settings_contacts_visibility_view.dart | 169 ++++++++++++++++++ .../settings_security_view.dart | 14 ++ 16 files changed, 425 insertions(+) create mode 100644 lib/domain/app_state/user_info/get_user_info_visibility_state.dart create mode 100644 lib/domain/model/user_info/user_info_visibility.dart create mode 100644 lib/domain/model/user_info/user_info_visibility_request.dart create mode 100644 lib/domain/usecase/user_info/get_user_info_visibility_interactor.dart create mode 100644 lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart create mode 100644 lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart create mode 100644 lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 9ff39eacc3..c6cc8e8ea6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3483,6 +3483,15 @@ "count": {} } }, + "contactsVisibility": "Contacts visibility", + "whoCanFindMeByMyContacts": "WHO CAN SEE MY PHONE/EMAIL", + "whoCanSeeMyPhoneEmail": "Who can SEE MY phone/email", + "everyOne": "Everyone", + "myContacts": "My Contacts", + "nobody": "Nobody", + "chooseWhichDetailsAreVisibleToOtherUsers": "CHOOSE WHICH DETAILS ARE VISIBLE TO OTHER USERS", + "youNumberIsVisibleAccordingToTheSettingAbove": "Your number is visible according to the setting above", + "youEmailIsVisibleAccordingToTheSettingAbove": "Your email is visible according to the setting above", "unblockUserToSendMessages": "Unblock user to send messages", "supportChat": "Support Chat", "supportChatDescription": "I’ve got your back! If you encounter bugs or need any help using Twake Workplace, send a message, I will reply to you swiftly" diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 4c3a5236d2..12a938c2c3 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_blocked_users/settings_blocked_user.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; import 'package:fluffychat/pages/splash/splash.dart'; @@ -448,6 +449,14 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), + GoRoute( + path: 'contactsVisibility', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const SettingsContactsVisibility(), + ), + redirect: loggedOutRedirect, + ), ], ), GoRoute( diff --git a/lib/data/datasource/user_info/user_info_datasource.dart b/lib/data/datasource/user_info/user_info_datasource.dart index 9f2b453111..26eb02eaf8 100644 --- a/lib/data/datasource/user_info/user_info_datasource.dart +++ b/lib/data/datasource/user_info/user_info_datasource.dart @@ -1,5 +1,8 @@ import 'package:fluffychat/domain/model/user_info/user_info.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; abstract class UserInfoDatasource { Future getUserInfo(String userId); + + Future getUserVisibility(String userId); } diff --git a/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart b/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart index bcd3069b84..613964d119 100644 --- a/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart +++ b/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/data/datasource/user_info/user_info_datasource.dart'; import 'package:fluffychat/data/network/user_info/user_info_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; class UserInfoDatasourceImpl implements UserInfoDatasource { const UserInfoDatasourceImpl(); @@ -10,4 +11,9 @@ class UserInfoDatasourceImpl implements UserInfoDatasource { Future getUserInfo(String userId) { return getIt.get().getUserInfo(userId); } + + @override + Future getUserVisibility(String userId) { + return getIt.get().getUserVisibility(userId); + } } diff --git a/lib/data/network/user_info/user_info_api.dart b/lib/data/network/user_info/user_info_api.dart index 47ef00d5d8..1bc1ab84d7 100644 --- a/lib/data/network/user_info/user_info_api.dart +++ b/lib/data/network/user_info/user_info_api.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/data/network/tom_endpoint.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/di/global/network_di.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; class UserInfoApi { const UserInfoApi(); @@ -40,4 +41,29 @@ class UserInfoApi { }); return UserInfo.fromJson(response); } + + Future getUserVisibility(String userId) async { + final client = getIt.get( + instanceName: NetworkDI.tomDioClientName, + ); + + final uri = + TomEndpoint.userInfoServicePath.generateTomUserInfoEndpoint(userId); + + final response = + await client.get("$uri/visibility").onError((error, stackTrace) { + if (error is DioException) { + throw DioException( + requestOptions: error.requestOptions, + response: error.response, + type: error.type, + error: error.error, + stackTrace: error.stackTrace, + ); + } else { + throw Exception(error); + } + }); + return UserInfoVisibility.fromJson(response); + } } diff --git a/lib/data/repository/user_info_repository_impl.dart b/lib/data/repository/user_info_repository_impl.dart index 744df8fbf9..5e4cab29e4 100644 --- a/lib/data/repository/user_info_repository_impl.dart +++ b/lib/data/repository/user_info_repository_impl.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/data/datasource/user_info/user_info_datasource.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; import 'package:fluffychat/domain/repository/user_info/user_info_repository.dart'; class UserInfoRepositoryImpl implements UserInfoRepository { @@ -10,4 +11,9 @@ class UserInfoRepositoryImpl implements UserInfoRepository { Future getUserInfo(String userId) { return getIt.get().getUserInfo(userId); } + + @override + Future getUserVisibility(String userId) { + return getIt.get().getUserVisibility(userId); + } } diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 5b1006fb6d..f2afcd412b 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -135,6 +135,7 @@ import 'package:fluffychat/domain/usecase/search/server_search_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/save_language_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/domain/usecase/user_info/get_user_info_interactor.dart'; +import 'package:fluffychat/domain/usecase/user_info/get_user_info_visibility_interactor.dart'; import 'package:fluffychat/domain/usecase/verify_name_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/modules/federation_identity_lookup/manager/federation_identity_lookup_manager.dart'; @@ -533,6 +534,9 @@ class GetItInitializer { getIt.registerFactory(() => const GetUserInfoInteractor()); getIt.registerFactory(() => InviteUserInteractor()); + + getIt.registerFactory(() => const GetUserInfoVisibilityInteractor()); + getIt.registerFactory(() => const CreateSupportChatInteractor()); } diff --git a/lib/domain/app_state/user_info/get_user_info_visibility_state.dart b/lib/domain/app_state/user_info/get_user_info_visibility_state.dart new file mode 100644 index 0000000000..1d6a999972 --- /dev/null +++ b/lib/domain/app_state/user_info/get_user_info_visibility_state.dart @@ -0,0 +1,26 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/presentation/state/success.dart'; + +class GettingUserInfoVisibility extends LoadingState {} + +class GetUserInfoVisibilitySuccess extends Success { + const GetUserInfoVisibilitySuccess(this.userInfoVisibility); + + final UserInfoVisibility userInfoVisibility; + + @override + List get props => [userInfoVisibility]; +} + +class GetUserInfoVisibilityFailure extends Failure { + final dynamic exception; + + const GetUserInfoVisibilityFailure({ + this.exception, + }); + + @override + List get props => [exception]; +} diff --git a/lib/domain/model/user_info/user_info_visibility.dart b/lib/domain/model/user_info/user_info_visibility.dart new file mode 100644 index 0000000000..f172dc8b45 --- /dev/null +++ b/lib/domain/model/user_info/user_info_visibility.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_info_visibility.g.dart'; + +enum VisibleEnum { + email, + phone; +} + +@JsonSerializable() +class UserInfoVisibility with EquatableMixin { + @JsonKey(name: "matrix_id") + final String? matrixId; + @JsonKey(name: "visibility") + final String? visibility; + @JsonKey(name: "visible_fields") + final List? visibleFields; + + UserInfoVisibility({ + this.matrixId, + this.visibility, + this.visibleFields, + }); + + factory UserInfoVisibility.fromJson(Map json) => + _$UserInfoVisibilityFromJson(json); + + Map toJson() => _$UserInfoVisibilityToJson(this); + + @override + List get props => [ + matrixId, + visibility, + visibleFields, + ]; +} diff --git a/lib/domain/model/user_info/user_info_visibility_request.dart b/lib/domain/model/user_info/user_info_visibility_request.dart new file mode 100644 index 0000000000..9b36aeee89 --- /dev/null +++ b/lib/domain/model/user_info/user_info_visibility_request.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_info_visibility_request.g.dart'; + +@JsonSerializable() +class UserInfoVisibilityRequest with EquatableMixin { + @JsonKey(name: "visibility") + final String? visibility; + @JsonKey(name: "visible_fields") + final List? visibleFields; + + UserInfoVisibilityRequest({ + this.visibility, + this.visibleFields, + }); + + factory UserInfoVisibilityRequest.fromJson(Map json) => + _$UserInfoVisibilityRequestFromJson(json); + + Map toJson() => _$UserInfoVisibilityRequestToJson(this); + + @override + List get props => [ + visibility, + visibleFields, + ]; +} diff --git a/lib/domain/repository/user_info/user_info_repository.dart b/lib/domain/repository/user_info/user_info_repository.dart index 082bd7db32..d3d0c74636 100644 --- a/lib/domain/repository/user_info/user_info_repository.dart +++ b/lib/domain/repository/user_info/user_info_repository.dart @@ -1,5 +1,8 @@ import 'package:fluffychat/domain/model/user_info/user_info.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; abstract class UserInfoRepository { Future getUserInfo(String userId); + + Future getUserVisibility(String userId); } diff --git a/lib/domain/usecase/user_info/get_user_info_visibility_interactor.dart b/lib/domain/usecase/user_info/get_user_info_visibility_interactor.dart new file mode 100644 index 0000000000..67cfc474cb --- /dev/null +++ b/lib/domain/usecase/user_info/get_user_info_visibility_interactor.dart @@ -0,0 +1,24 @@ +import 'package:dartz/dartz.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/user_info/get_user_info_visibility_state.dart'; +import 'package:fluffychat/domain/repository/user_info/user_info_repository.dart'; + +class GetUserInfoVisibilityInteractor { + const GetUserInfoVisibilityInteractor(); + + Stream> execute({ + required String userId, + }) async* { + try { + yield Right(GettingUserInfoVisibility()); + + final result = + await getIt.get().getUserVisibility(userId); + yield Right(GetUserInfoVisibilitySuccess(result)); + } catch (e) { + yield Left(GetUserInfoVisibilityFailure(exception: e)); + } + } +} diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart new file mode 100644 index 0000000000..087e02cad9 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -0,0 +1,30 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SettingsContactsVisibility extends StatefulWidget { + const SettingsContactsVisibility({super.key}); + + @override + State createState() => + SettingsContactsVisibilityController(); +} + +class SettingsContactsVisibilityController + extends State { + void onBack() { + context.go('/rooms/security'); + } + + List visibilityOptions = [ + SettingsContactsVisibilityEnum.everyone, + SettingsContactsVisibilityEnum.myContacts, + SettingsContactsVisibilityEnum.nobody, + ]; + + @override + Widget build(BuildContext context) { + return SettingsContactsVisibilityView(controller: this); + } +} diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart new file mode 100644 index 0000000000..3706500d92 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart @@ -0,0 +1,30 @@ +import 'package:fluffychat/generated/l10n/app_localizations.dart'; +import 'package:flutter/cupertino.dart'; + +enum SettingsContactsVisibilityEnum { + everyone, + myContacts, + nobody; + + String title(BuildContext context) { + switch (this) { + case SettingsContactsVisibilityEnum.everyone: + return L10n.of(context)!.everyOne; + case SettingsContactsVisibilityEnum.myContacts: + return L10n.of(context)!.myContacts; + case SettingsContactsVisibilityEnum.nobody: + return L10n.of(context)!.nobody; + } + } + + bool enableDivider() { + switch (this) { + case SettingsContactsVisibilityEnum.everyone: + return true; + case SettingsContactsVisibilityEnum.myContacts: + return true; + case SettingsContactsVisibilityEnum.nobody: + return false; + } + } +} diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart new file mode 100644 index 0000000000..92947e35d2 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -0,0 +1,169 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart'; +import 'package:fluffychat/widgets/app_bars/twake_app_bar.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/generated/l10n/app_localizations.dart'; +import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class SettingsContactsVisibilityView extends StatelessWidget { + final SettingsContactsVisibilityController controller; + + const SettingsContactsVisibilityView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: LinagoraSysColors.material().onPrimary, + resizeToAvoidBottomInset: false, + appBar: TwakeAppBar( + title: L10n.of(context)!.contactsVisibility, + centerTitle: true, + withDivider: true, + context: context, + enableLeftTitle: true, + onBack: controller.onBack, + leading: TwakeIconButton( + paddingAll: 8, + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: controller.onBack, + icon: Icons.arrow_back_ios, + ), + ), + body: MaxWidthBody( + withScrolling: true, + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 24, + bottom: 16, + ), + child: Text( + L10n.of(context)!.whoCanSeeMyPhoneEmail, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: LinagoraSysColors.material().tertiary, + ), + textAlign: TextAlign.center, + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: LinagoraRefColors.material().neutral[90] ?? + Colors.transparent, + width: 1, + ), + ), + child: Column( + children: controller.visibilityOptions + .map( + (option) => _buildOptionItem( + context: context, + title: option.title(context), + enableDivider: option.enableDivider(), + isSelected: true, + ), + ) + .toList(), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildOptionItem({ + required BuildContext context, + required String title, + String? subtitle, + void Function()? onTap, + bool enableDivider = true, + bool isSelected = false, + }) { + return InkWell( + onTap: onTap, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + title, + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + ), + if (subtitle != null) + Text( + subtitle, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + color: LinagoraRefColors.material().tertiary, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + ), + ], + ), + ), + if (enableDivider) + Divider( + height: 1, + color: LinagoraRefColors.material().neutral[90], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8, + right: 16, + ), + child: SizedBox( + width: 24, + height: 24, + child: isSelected + ? Icon( + Icons.check, + color: LinagoraSysColors.material().primary, + size: 24, + ) + : null, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart index e7ca171130..dcc0b91943 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart @@ -60,6 +60,20 @@ class SettingsSecurityView extends StatelessWidget { // ), Column( children: [ + Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: SettingsItemBuilder( + title: L10n.of(context)!.contactsVisibility, + titleColor: Theme.of(context).colorScheme.onBackground, + subtitle: L10n.of(context)!.whoCanFindMeByMyContacts, + leading: Icons.phone_outlined, + leadingIconColor: + LinagoraRefColors.material().tertiary[30], + onTap: () { + context.push('/rooms/security/contactsVisibility'); + }, + ), + ), Padding( padding: SettingsViewStyle.bodySettingsScreenPadding, child: ValueListenableBuilder( From 867ee8ebca604ef1efe8a6b4e6ac518cc8e10699 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 21 Nov 2025 19:41:26 +0700 Subject: [PATCH 02/13] TW-2710: Implement update user info visibility settings --- assets/l10n/intl_en.arb | 4 +- .../user_info/user_info_datasource.dart | 6 + .../user_info/user_info_datasource_impl.dart | 12 ++ lib/data/network/user_info/user_info_api.dart | 30 +++ .../repository/user_info_repository_impl.dart | 12 ++ lib/di/global/get_it_initializer.dart | 3 + .../update_user_info_visibility_state.dart | 26 +++ .../model/user_info/user_info_visibility.dart | 29 +++ .../user_info/user_info_repository.dart | 6 + ...pdate_user_info_visibility_interactor.dart | 29 +++ .../settings_contacts_visibility.dart | 179 +++++++++++++++- .../settings_contacts_visibility_enum.dart | 18 +- .../settings_contacts_visibility_view.dart | 192 +++++++++++++++--- 13 files changed, 508 insertions(+), 38 deletions(-) create mode 100644 lib/domain/app_state/user_info/update_user_info_visibility_state.dart create mode 100644 lib/domain/usecase/user_info/update_user_info_visibility_interactor.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c6cc8e8ea6..4cfae3e568 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3484,8 +3484,8 @@ } }, "contactsVisibility": "Contacts visibility", - "whoCanFindMeByMyContacts": "WHO CAN SEE MY PHONE/EMAIL", - "whoCanSeeMyPhoneEmail": "Who can SEE MY phone/email", + "whoCanFindMeByMyContacts": "Who can find me by my contacts", + "whoCanSeeMyPhoneEmail": "WHO CAN SEE MY PHONE/EMAIL", "everyOne": "Everyone", "myContacts": "My Contacts", "nobody": "Nobody", diff --git a/lib/data/datasource/user_info/user_info_datasource.dart b/lib/data/datasource/user_info/user_info_datasource.dart index 26eb02eaf8..36f8c2c190 100644 --- a/lib/data/datasource/user_info/user_info_datasource.dart +++ b/lib/data/datasource/user_info/user_info_datasource.dart @@ -1,8 +1,14 @@ import 'package:fluffychat/domain/model/user_info/user_info.dart'; import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; abstract class UserInfoDatasource { Future getUserInfo(String userId); Future getUserVisibility(String userId); + + Future updateUserInfoVisibility( + String userId, + UserInfoVisibilityRequest userInfoVisibility, + ); } diff --git a/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart b/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart index 613964d119..d85615b835 100644 --- a/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart +++ b/lib/data/datasource_impl/user_info/user_info_datasource_impl.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/data/network/user_info/user_info_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; class UserInfoDatasourceImpl implements UserInfoDatasource { const UserInfoDatasourceImpl(); @@ -16,4 +17,15 @@ class UserInfoDatasourceImpl implements UserInfoDatasource { Future getUserVisibility(String userId) { return getIt.get().getUserVisibility(userId); } + + @override + Future updateUserInfoVisibility( + String userId, + UserInfoVisibilityRequest body, + ) { + return getIt.get().updateUserVisibility( + userId: userId, + body: body, + ); + } } diff --git a/lib/data/network/user_info/user_info_api.dart b/lib/data/network/user_info/user_info_api.dart index 1bc1ab84d7..3e2de17293 100644 --- a/lib/data/network/user_info/user_info_api.dart +++ b/lib/data/network/user_info/user_info_api.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/di/global/network_di.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; class UserInfoApi { const UserInfoApi(); @@ -66,4 +67,33 @@ class UserInfoApi { }); return UserInfoVisibility.fromJson(response); } + + Future updateUserVisibility({ + required String userId, + required UserInfoVisibilityRequest body, + }) async { + final client = getIt.get( + instanceName: NetworkDI.tomDioClientName, + ); + + final uri = + TomEndpoint.userInfoServicePath.generateTomUserInfoEndpoint(userId); + + final response = await client + .postToGetBody("$uri/visibility", data: body.toJson()) + .onError((error, stackTrace) { + if (error is DioException) { + throw DioException( + requestOptions: error.requestOptions, + response: error.response, + type: error.type, + error: error.error, + stackTrace: error.stackTrace, + ); + } else { + throw Exception(error); + } + }); + return UserInfoVisibility.fromJson(response); + } } diff --git a/lib/data/repository/user_info_repository_impl.dart b/lib/data/repository/user_info_repository_impl.dart index 5e4cab29e4..37a3104668 100644 --- a/lib/data/repository/user_info_repository_impl.dart +++ b/lib/data/repository/user_info_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/data/datasource/user_info/user_info_datasource.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/user_info/user_info.dart'; import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; import 'package:fluffychat/domain/repository/user_info/user_info_repository.dart'; class UserInfoRepositoryImpl implements UserInfoRepository { @@ -16,4 +17,15 @@ class UserInfoRepositoryImpl implements UserInfoRepository { Future getUserVisibility(String userId) { return getIt.get().getUserVisibility(userId); } + + @override + Future updateUserInfoVisibility( + String userId, + UserInfoVisibilityRequest body, + ) { + return getIt.get().updateUserInfoVisibility( + userId, + body, + ); + } } diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index f2afcd412b..58053dd14e 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -136,6 +136,7 @@ import 'package:fluffychat/domain/usecase/settings/save_language_interactor.dart import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/domain/usecase/user_info/get_user_info_interactor.dart'; import 'package:fluffychat/domain/usecase/user_info/get_user_info_visibility_interactor.dart'; +import 'package:fluffychat/domain/usecase/user_info/update_user_info_visibility_interactor.dart'; import 'package:fluffychat/domain/usecase/verify_name_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/modules/federation_identity_lookup/manager/federation_identity_lookup_manager.dart'; @@ -537,6 +538,8 @@ class GetItInitializer { getIt.registerFactory(() => const GetUserInfoVisibilityInteractor()); + getIt.registerFactory(() => const UpdateUserInfoVisibilityInteractor()); + getIt.registerFactory(() => const CreateSupportChatInteractor()); } diff --git a/lib/domain/app_state/user_info/update_user_info_visibility_state.dart b/lib/domain/app_state/user_info/update_user_info_visibility_state.dart new file mode 100644 index 0000000000..208458f1e0 --- /dev/null +++ b/lib/domain/app_state/user_info/update_user_info_visibility_state.dart @@ -0,0 +1,26 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/presentation/state/success.dart'; + +class UpdatingUserInfoVisibility extends LoadingState {} + +class UpdateUserInfoVisibilitySuccess extends Success { + const UpdateUserInfoVisibilitySuccess(this.userInfoVisibility); + + final UserInfoVisibility userInfoVisibility; + + @override + List get props => [userInfoVisibility]; +} + +class UpdateUserInfoVisibilityFailure extends Failure { + final dynamic exception; + + const UpdateUserInfoVisibilityFailure({ + this.exception, + }); + + @override + List get props => [exception]; +} diff --git a/lib/domain/model/user_info/user_info_visibility.dart b/lib/domain/model/user_info/user_info_visibility.dart index f172dc8b45..50413acaff 100644 --- a/lib/domain/model/user_info/user_info_visibility.dart +++ b/lib/domain/model/user_info/user_info_visibility.dart @@ -1,11 +1,40 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:fluffychat/generated/l10n/app_localizations.dart'; part 'user_info_visibility.g.dart'; enum VisibleEnum { email, phone; + + String title(BuildContext context) { + switch (this) { + case VisibleEnum.phone: + return L10n.of(context)!.phone; + case VisibleEnum.email: + return L10n.of(context)!.email; + } + } + + String subtitle(BuildContext context) { + switch (this) { + case VisibleEnum.phone: + return L10n.of(context)!.youNumberIsVisibleAccordingToTheSettingAbove; + case VisibleEnum.email: + return L10n.of(context)!.youEmailIsVisibleAccordingToTheSettingAbove; + } + } + + bool enableDivider() { + switch (this) { + case VisibleEnum.phone: + return true; + case VisibleEnum.email: + return false; + } + } } @JsonSerializable() diff --git a/lib/domain/repository/user_info/user_info_repository.dart b/lib/domain/repository/user_info/user_info_repository.dart index d3d0c74636..8377e34f50 100644 --- a/lib/domain/repository/user_info/user_info_repository.dart +++ b/lib/domain/repository/user_info/user_info_repository.dart @@ -1,8 +1,14 @@ import 'package:fluffychat/domain/model/user_info/user_info.dart'; import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; abstract class UserInfoRepository { Future getUserInfo(String userId); Future getUserVisibility(String userId); + + Future updateUserInfoVisibility( + String userId, + UserInfoVisibilityRequest userInfoVisibility, + ); } diff --git a/lib/domain/usecase/user_info/update_user_info_visibility_interactor.dart b/lib/domain/usecase/user_info/update_user_info_visibility_interactor.dart new file mode 100644 index 0000000000..6f437e0d45 --- /dev/null +++ b/lib/domain/usecase/user_info/update_user_info_visibility_interactor.dart @@ -0,0 +1,29 @@ +import 'package:dartz/dartz.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/user_info/update_user_info_visibility_state.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; +import 'package:fluffychat/domain/repository/user_info/user_info_repository.dart'; + +class UpdateUserInfoVisibilityInteractor { + const UpdateUserInfoVisibilityInteractor(); + + Stream> execute({ + required String userId, + required UserInfoVisibilityRequest body, + }) async* { + try { + yield Right(UpdatingUserInfoVisibility()); + + final result = + await getIt.get().updateUserInfoVisibility( + userId, + body, + ); + yield Right(UpdateUserInfoVisibilitySuccess(result)); + } catch (e) { + yield Left(UpdateUserInfoVisibilityFailure(exception: e)); + } + } +} diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index 087e02cad9..b4a56f5ec4 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -1,7 +1,21 @@ +import 'dart:async'; +import 'package:dartz/dartz.dart' hide State; +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/user_info/get_user_info_visibility_state.dart'; +import 'package:fluffychat/domain/app_state/user_info/update_user_info_visibility_state.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/domain/model/user_info/user_info_visibility_request.dart'; +import 'package:fluffychat/domain/usecase/user_info/get_user_info_visibility_interactor.dart'; +import 'package:fluffychat/domain/usecase/user_info/update_user_info_visibility_interactor.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; class SettingsContactsVisibility extends StatefulWidget { const SettingsContactsVisibility({super.key}); @@ -13,16 +27,175 @@ class SettingsContactsVisibility extends StatefulWidget { class SettingsContactsVisibilityController extends State { + Client get client => Matrix.read(context).client; + void onBack() { context.go('/rooms/security'); } + StreamSubscription? getUserInfoVisibilityStreamSub; + + StreamSubscription? updateUserInfoVisibilityStreamSub; + + final getUserInfoVisibilityInteractor = + getIt.get(); + + final updateUserInfoVisibilityInteractor = + getIt.get(); + + final ValueNotifier> getUserInfoVisibilityNotifier = + ValueNotifier>( + Right(GettingUserInfoVisibility()), + ); + + final ValueNotifier> + updateUserInfoVisibilityNotifier = + ValueNotifier>( + Right(UpdatingUserInfoVisibility()), + ); + List visibilityOptions = [ - SettingsContactsVisibilityEnum.everyone, - SettingsContactsVisibilityEnum.myContacts, - SettingsContactsVisibilityEnum.nobody, + SettingsContactsVisibilityEnum.public, + SettingsContactsVisibilityEnum.contacts, + SettingsContactsVisibilityEnum.private, ]; + List visibleFieldsOptions = [ + VisibleEnum.phone, + VisibleEnum.email, + ]; + + final ValueNotifier + selectedVisibilityOptionNotifier = + ValueNotifier(null); + + final ValueNotifier> selectedVisibleFieldNotifier = + ValueNotifier>([]); + + void onSelectVisibilityOption(SettingsContactsVisibilityEnum option) { + switch (option) { + case SettingsContactsVisibilityEnum.public: + updateUserInfoVisibility( + userInfoVisibility: UserInfoVisibilityRequest( + visibility: option.name, + visibleFields: [], + ), + ); + break; + case SettingsContactsVisibilityEnum.contacts: + selectedVisibilityOptionNotifier.value = option; + break; + case SettingsContactsVisibilityEnum.private: + updateUserInfoVisibility( + userInfoVisibility: UserInfoVisibilityRequest( + visibility: option.name, + ), + ); + break; + } + } + + void onUpdateVisibleFields(VisibleEnum selectedField) { + final currentVisibleFields = + selectedVisibleFieldNotifier.value.contains(selectedField) + ? selectedVisibleFieldNotifier.value + .where((field) => field != selectedField) + .toList() + : [...selectedVisibleFieldNotifier.value, selectedField]; + selectedVisibleFieldNotifier.value = currentVisibleFields; + updateUserInfoVisibility( + userInfoVisibility: UserInfoVisibilityRequest( + visibility: SettingsContactsVisibilityEnum.contacts.name, + visibleFields: currentVisibleFields, + ), + ); + } + + void updateUserInfoVisibility({ + required UserInfoVisibilityRequest userInfoVisibility, + }) { + updateUserInfoVisibilityStreamSub = updateUserInfoVisibilityInteractor + .execute( + userId: client.userID!, + body: userInfoVisibility, + ) + .listen((either) { + updateUserInfoVisibilityNotifier.value = either; + + either.fold( + (failure) { + if (failure is UpdateUserInfoVisibilityFailure) { + TwakeDialog.hideLoadingDialog(context); + } + }, + (success) { + if (success is UpdateUserInfoVisibilitySuccess) { + selectedVisibilityOptionNotifier.value = + SettingsContactsVisibilityEnum.values.firstWhere( + (option) => option.name == success.userInfoVisibility.visibility, + orElse: () => SettingsContactsVisibilityEnum.private, + ); + TwakeDialog.hideLoadingDialog(context); + } + + if (success is UpdatingUserInfoVisibility) { + TwakeDialog.showLoadingDialog(context); + } + }, + ); + }); + } + + void initialGetUserInfoVisibility() { + if (client.userID == null) { + return; + } + getUserInfoVisibilityStreamSub = getUserInfoVisibilityInteractor + .execute(userId: client.userID!) + .listen((either) { + getUserInfoVisibilityNotifier.value = either; + + either.fold( + (failure) { + if (failure is GetUserInfoVisibilityFailure) { + TwakeDialog.hideLoadingDialog(context); + } + }, + (success) { + if (success is GettingUserInfoVisibility) { + TwakeDialog.showLoadingDialog(context); + } + + if (success is GetUserInfoVisibilitySuccess) { + selectedVisibilityOptionNotifier.value = + SettingsContactsVisibilityEnum.values.firstWhere( + (option) => option.name == success.userInfoVisibility.visibility, + orElse: () => SettingsContactsVisibilityEnum.private, + ); + selectedVisibleFieldNotifier.value = + success.userInfoVisibility.visibleFields ?? []; + TwakeDialog.hideLoadingDialog(context); + } + }, + ); + }); + } + + @override + void initState() { + initialGetUserInfoVisibility(); + super.initState(); + } + + @override + void dispose() { + getUserInfoVisibilityStreamSub?.cancel(); + selectedVisibilityOptionNotifier.dispose(); + getUserInfoVisibilityNotifier.dispose(); + updateUserInfoVisibilityNotifier.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SettingsContactsVisibilityView(controller: this); diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart index 3706500d92..cfa7c8327b 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart @@ -2,28 +2,28 @@ import 'package:fluffychat/generated/l10n/app_localizations.dart'; import 'package:flutter/cupertino.dart'; enum SettingsContactsVisibilityEnum { - everyone, - myContacts, - nobody; + private, + public, + contacts; String title(BuildContext context) { switch (this) { - case SettingsContactsVisibilityEnum.everyone: + case SettingsContactsVisibilityEnum.public: return L10n.of(context)!.everyOne; - case SettingsContactsVisibilityEnum.myContacts: + case SettingsContactsVisibilityEnum.contacts: return L10n.of(context)!.myContacts; - case SettingsContactsVisibilityEnum.nobody: + case SettingsContactsVisibilityEnum.private: return L10n.of(context)!.nobody; } } bool enableDivider() { switch (this) { - case SettingsContactsVisibilityEnum.everyone: + case SettingsContactsVisibilityEnum.public: return true; - case SettingsContactsVisibilityEnum.myContacts: + case SettingsContactsVisibilityEnum.contacts: return true; - case SettingsContactsVisibilityEnum.nobody: + case SettingsContactsVisibilityEnum.private: return false; } } diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index 92947e35d2..18ef7bae6e 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -1,4 +1,6 @@ +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; import 'package:fluffychat/widgets/app_bars/twake_app_bar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/generated/l10n/app_localizations.dart'; @@ -71,16 +73,103 @@ class SettingsContactsVisibilityView extends StatelessWidget { child: Column( children: controller.visibilityOptions .map( - (option) => _buildOptionItem( - context: context, - title: option.title(context), - enableDivider: option.enableDivider(), - isSelected: true, + (option) => ValueListenableBuilder( + valueListenable: + controller.getUserInfoVisibilityNotifier, + builder: (context, state, _) { + return ValueListenableBuilder( + valueListenable: controller + .selectedVisibilityOptionNotifier, + builder: (context, _, __) { + final isVisibilitySelected = controller + .selectedVisibilityOptionNotifier + .value == + option; + return _buildVisibilityOptionItem( + context: context, + option: option, + enableDivider: option.enableDivider(), + isSelected: isVisibilitySelected, + onTap: + controller.onSelectVisibilityOption, + ); + }, + ); + }, ), ) .toList(), ), ), + ValueListenableBuilder( + valueListenable: + controller.selectedVisibilityOptionNotifier, + builder: (context, selectedOption, _) { + if (selectedOption != + SettingsContactsVisibilityEnum.contacts) { + return const SizedBox.shrink(); + } + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 32, + bottom: 8, + ), + child: Text( + L10n.of(context)! + .chooseWhichDetailsAreVisibleToOtherUsers, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: + LinagoraSysColors.material().tertiary, + ), + textAlign: TextAlign.center, + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + LinagoraRefColors.material().neutral[90] ?? + Colors.transparent, + width: 1, + ), + ), + child: Column( + children: controller.visibleFieldsOptions + .map( + (option) => ValueListenableBuilder( + valueListenable: controller + .getUserInfoVisibilityNotifier, + builder: (context, state, _) { + return _buildVisibleFieldIem( + context: context, + option: option, + enableDivider: option.enableDivider(), + isSelected: controller + .selectedVisibleFieldNotifier + .value + .contains(option), + onTap: + controller.onUpdateVisibleFields, + ); + }, + ), + ) + .toList(), + ), + ), + ], + ); + }, + ), ], ), ), @@ -90,16 +179,73 @@ class SettingsContactsVisibilityView extends StatelessWidget { ); } - Widget _buildOptionItem({ + Widget _buildVisibilityOptionItem({ + required BuildContext context, + required SettingsContactsVisibilityEnum option, + void Function(SettingsContactsVisibilityEnum)? onTap, + bool enableDivider = true, + bool isSelected = false, + }) { + return InkWell( + onTap: !isSelected ? () => onTap?.call(option) : null, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + option.title(context), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + ), + ), + if (enableDivider) + Divider( + height: 1, + color: LinagoraRefColors.material().neutral[90], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 8, + right: 16, + ), + child: SizedBox( + width: 24, + height: 24, + child: isSelected + ? Icon( + Icons.check, + color: LinagoraSysColors.material().primary, + size: 24, + ) + : null, + ), + ), + ], + ), + ); + } + + Widget _buildVisibleFieldIem({ required BuildContext context, - required String title, - String? subtitle, - void Function()? onTap, + required VisibleEnum option, + void Function(VisibleEnum)? onTap, bool enableDivider = true, bool isSelected = false, }) { return InkWell( - onTap: onTap, + onTap: !isSelected ? () => onTap?.call(option) : null, child: Row( children: [ Expanded( @@ -110,9 +256,10 @@ class SettingsContactsVisibilityView extends StatelessWidget { Padding( padding: const EdgeInsets.all(16), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + option.title(context), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: LinagoraSysColors.material().onSurface, @@ -121,19 +268,16 @@ class SettingsContactsVisibilityView extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - if (subtitle != null) - Text( - subtitle, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: LinagoraRefColors.material().tertiary, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - textAlign: TextAlign.center, - ), + Text( + option.subtitle(context), + style: + Theme.of(context).textTheme.labelMedium?.copyWith( + color: LinagoraSysColors.material().tertiary, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + textAlign: TextAlign.left, + ), ], ), ), From f9433e7dcd9a64006b52d2a552eb50e5d8a532f2 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 24 Nov 2025 21:40:24 +0700 Subject: [PATCH 03/13] TW-2710: Apply E2E for contacts visibility --- integration_test/robots/home_robot.dart | 7 +- .../robots/setting/setting_robot.dart | 7 +- .../settings_contacts_visibility_robot.dart | 161 ++++++++++++++++++ .../settings_privacy_and_security_robot.dart | 18 ++ .../settings_contacts_visibility_test.dart | 66 +++++++ .../settings/settings_item_builder.dart | 1 + .../settings/settings_view.dart | 1 + .../settings_contacts_visibility_view.dart | 4 + .../settings_security_view.dart | 1 + .../avatar/bottom_navigation_avatar.dart | 1 + .../enum/adaptive_destinations_enum.dart | 5 + 11 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 integration_test/robots/setting/settings_contacts_visibility_robot.dart create mode 100644 integration_test/robots/setting/settings_privacy_and_security_robot.dart create mode 100644 integration_test/tests/setting/settings_contacts_visibility_test.dart diff --git a/integration_test/robots/home_robot.dart b/integration_test/robots/home_robot.dart index dc3e6e03b7..0514bd20aa 100644 --- a/integration_test/robots/home_robot.dart +++ b/integration_test/robots/home_robot.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; import 'package:patrol/patrol.dart'; import '../base/core_robot.dart'; import 'chat_list_robot.dart'; @@ -18,7 +18,7 @@ class HomeRobot extends CoreRobot { } Future getSettingTab() async { - return $('Settings'); + return $(const Key('settings_navigation_destination')); } Future gotoContactListAndGrantContactPermission() async { @@ -37,7 +37,8 @@ class HomeRobot extends CoreRobot { } Future gotoSettingScreen() async { - await (await getSettingTab()).tap(); + final settingTab = await getSettingTab(); + await settingTab.tap(); await $.pumpAndSettle(); return SettingRobot($); } diff --git a/integration_test/robots/setting/setting_robot.dart b/integration_test/robots/setting/setting_robot.dart index a1031d09fe..409788cc07 100644 --- a/integration_test/robots/setting/setting_robot.dart +++ b/integration_test/robots/setting/setting_robot.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settin import 'package:fluffychat/pages/settings_dashboard/settings_chat/settings_chat_view.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security_view.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/dialog/confirmation_dialog_builder.dart'; import 'package:patrol/patrol.dart'; @@ -20,8 +21,8 @@ class SettingRobot extends HomeRobot { return $("Chat"); } - PatrolFinder privateAndSecuritySetting() { - return $("Privacy & Security"); + PatrolFinder privacyAndSecuritySetting() { + return $(Key(SettingEnum.privacyAndSecurity.name)); } PatrolFinder notificationsSetting() { @@ -54,7 +55,7 @@ class SettingRobot extends HomeRobot { } Future openPrivacyAndSecuritySetting() async { - await privateAndSecuritySetting().tap(); + await privacyAndSecuritySetting().tap(); await $.waitUntilVisible($(SettingsSecurityView)); } diff --git a/integration_test/robots/setting/settings_contacts_visibility_robot.dart b/integration_test/robots/setting/settings_contacts_visibility_robot.dart new file mode 100644 index 0000000000..83ac243d91 --- /dev/null +++ b/integration_test/robots/setting/settings_contacts_visibility_robot.dart @@ -0,0 +1,161 @@ +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart'; +import 'package:flutter/material.dart'; +import 'package:patrol/patrol.dart'; +import '../home_robot.dart'; + +class SettingsContactsVisibilityRobot extends HomeRobot { + SettingsContactsVisibilityRobot(super.$); + + PatrolFinder backButton() { + return $(IconButton).containing($(Icon)); + } + + // Visibility option finders + PatrolFinder visibilityOption( + SettingsContactsVisibilityEnum visibilityOption, + ) { + return $(Key('visibility_option_${visibilityOption.name}')); + } + + PatrolFinder visibilityOptionSelected( + SettingsContactsVisibilityEnum visibilityOption, + ) { + return $(Key('visibility_option_selected_${visibilityOption.name}')); + } + + // Specific visibility options + PatrolFinder everyoneOption() { + return visibilityOption(SettingsContactsVisibilityEnum.public); + } + + PatrolFinder contactsOption() { + return visibilityOption(SettingsContactsVisibilityEnum.contacts); + } + + PatrolFinder nobodyOption() { + return visibilityOption(SettingsContactsVisibilityEnum.private); + } + + // Selected state finders + PatrolFinder everyoneSelected() { + return visibilityOptionSelected(SettingsContactsVisibilityEnum.public); + } + + PatrolFinder contactsSelected() { + return visibilityOptionSelected(SettingsContactsVisibilityEnum.contacts); + } + + PatrolFinder nobodySelected() { + return visibilityOptionSelected(SettingsContactsVisibilityEnum.private); + } + + // Visible field option finders + PatrolFinder visibleFieldOption(VisibleEnum visibleEnum) { + return $(Key('visible_field_option_${visibleEnum.name}')); + } + + PatrolFinder visibleFieldOptionSelected(VisibleEnum visibleEnum) { + return $(Key('visible_field_option_selected_${visibleEnum.name}')); + } + + // Specific visible field options + PatrolFinder emailFieldOption() { + return visibleFieldOption(VisibleEnum.email); + } + + PatrolFinder phoneNumberFieldOption() { + return visibleFieldOption(VisibleEnum.phone); + } + + // Selected state finders for fields + PatrolFinder emailFieldSelected() { + return visibleFieldOptionSelected(VisibleEnum.email); + } + + PatrolFinder phoneNumberFieldSelected() { + return visibleFieldOptionSelected(VisibleEnum.phone); + } + + // Actions + Future selectEveryoneOption() async { + await everyoneOption().tap(); + await $.waitUntilVisible(everyoneSelected()); + } + + Future selectContactsOption() async { + await contactsOption().tap(); + await $.waitUntilVisible(contactsSelected()); + } + + Future selectNobodyOption() async { + await nobodyOption().tap(); + await $.waitUntilVisible(nobodySelected()); + } + + Future toggleEmailField() async { + await emailFieldOption().tap(); + await $.pumpAndSettle(); + } + + Future togglePhoneNumberField() async { + await phoneNumberFieldOption().tap(); + await $.pumpAndSettle(); + } + + Future backToSettingsScreen() async { + await backButton().tap(); + await $.pumpAndSettle(); + } + + // Helper methods to check states + bool isEveryoneSelected() { + try { + everyoneSelected().exists; + return true; + } catch (e) { + return false; + } + } + + bool isContactsSelected() { + try { + contactsSelected().exists; + return true; + } catch (e) { + return false; + } + } + + bool isNobodySelected() { + try { + nobodySelected().exists; + return true; + } catch (e) { + return false; + } + } + + bool isEmailFieldSelected() { + try { + emailFieldSelected().exists; + return true; + } catch (e) { + return false; + } + } + + bool isPhoneNumberFieldSelected() { + try { + phoneNumberFieldSelected().exists; + return true; + } catch (e) { + return false; + } + } + + Future waitForView() async { + await $.waitUntilVisible($(SettingsContactsVisibilityView)); + } +} diff --git a/integration_test/robots/setting/settings_privacy_and_security_robot.dart b/integration_test/robots/setting/settings_privacy_and_security_robot.dart new file mode 100644 index 0000000000..0a1953c706 --- /dev/null +++ b/integration_test/robots/setting/settings_privacy_and_security_robot.dart @@ -0,0 +1,18 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:patrol/patrol.dart'; + +import '../home_robot.dart'; + +class SettingsPrivacyAndSecurityRobot extends HomeRobot { + SettingsPrivacyAndSecurityRobot(super.$); + + PatrolFinder contactVisibilitySetting() { + return $(const Key('contacts_visibility_settings_item')); + } + + Future openContactVisibilitySetting() async { + await contactVisibilitySetting().tap(); + await $.waitUntilVisible($(SettingsContactsVisibilityView)); + } +} diff --git a/integration_test/tests/setting/settings_contacts_visibility_test.dart b/integration_test/tests/setting/settings_contacts_visibility_test.dart new file mode 100644 index 0000000000..78f0daff77 --- /dev/null +++ b/integration_test/tests/setting/settings_contacts_visibility_test.dart @@ -0,0 +1,66 @@ +import '../../base/test_base.dart'; +import '../../robots/home_robot.dart'; +import '../../robots/setting/settings_contacts_visibility_robot.dart'; +import '../../robots/setting/settings_privacy_and_security_robot.dart'; + +void main() { + TestBase().twakePatrolTest( + description: + 'Open settings contact visibility screen test and verify everyone option is present', + test: ($) async { + final settingsRobot = await HomeRobot($).gotoSettingScreen(); + + await settingsRobot.openPrivacyAndSecuritySetting(); + + await SettingsPrivacyAndSecurityRobot($).openContactVisibilitySetting(); + + final contactsVisibilityRobot = SettingsContactsVisibilityRobot($); + await contactsVisibilityRobot.waitForView(); + + // Verify everyone option is present + await contactsVisibilityRobot.everyoneOption().waitUntilVisible(); + }, + ); + + TestBase().twakePatrolTest( + description: + 'Open settings contact visibility screen test and my contacts option is present', + test: ($) async { + final settingsRobot = await HomeRobot($).gotoSettingScreen(); + + await settingsRobot.openPrivacyAndSecuritySetting(); + + await SettingsPrivacyAndSecurityRobot($).openContactVisibilitySetting(); + + final contactsVisibilityRobot = SettingsContactsVisibilityRobot($); + await contactsVisibilityRobot.waitForView(); + + // Verify contacts option is present + await contactsVisibilityRobot.contactsOption().waitUntilVisible(); + + /// Verify email and phone visible fields are present + + await contactsVisibilityRobot.emailFieldOption().waitUntilVisible(); + + await contactsVisibilityRobot.phoneNumberFieldOption().waitUntilVisible(); + }, + ); + + TestBase().twakePatrolTest( + description: + 'Open settings contact visibility screen test and nobody option is present', + test: ($) async { + final settingsRobot = await HomeRobot($).gotoSettingScreen(); + + await settingsRobot.openPrivacyAndSecuritySetting(); + + await SettingsPrivacyAndSecurityRobot($).openContactVisibilitySetting(); + + final contactsVisibilityRobot = SettingsContactsVisibilityRobot($); + await contactsVisibilityRobot.waitForView(); + + // Verify nobody option is present + await contactsVisibilityRobot.nobodyOption().waitUntilVisible(); + }, + ); +} diff --git a/lib/pages/settings_dashboard/settings/settings_item_builder.dart b/lib/pages/settings_dashboard/settings/settings_item_builder.dart index 8a55106220..6207ac567f 100644 --- a/lib/pages/settings_dashboard/settings/settings_item_builder.dart +++ b/lib/pages/settings_dashboard/settings/settings_item_builder.dart @@ -39,6 +39,7 @@ class SettingsItemBuilder extends StatelessWidget { return TwakeInkWell( isSelected: isSelected, onTap: onTap, + key: key, child: SizedBox( height: height ?? SettingsViewStyle.settingsItemHeight, child: Padding( diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index aa35e54961..98db375918 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -181,6 +181,7 @@ class SettingsView extends StatelessWidget { Padding( padding: SettingsViewStyle.bodySettingsScreenPadding, child: SettingsItemBuilder( + key: Key(item.name), title: item.titleSettings(context), titleColor: item.titleColor(context), leading: item.iconLeading(), diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index 18ef7bae6e..a514d95d18 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -187,6 +187,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { bool isSelected = false, }) { return InkWell( + key: Key('visibility_option_${option.name}'), onTap: !isSelected ? () => onTap?.call(option) : null, child: Row( children: [ @@ -225,6 +226,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { height: 24, child: isSelected ? Icon( + key: Key('visibility_option_selected_${option.name}'), Icons.check, color: LinagoraSysColors.material().primary, size: 24, @@ -245,6 +247,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { bool isSelected = false, }) { return InkWell( + key: Key('visible_field_option_${option.name}'), onTap: !isSelected ? () => onTap?.call(option) : null, child: Row( children: [ @@ -299,6 +302,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { height: 24, child: isSelected ? Icon( + key: Key('visible_field_option_selected_${option.name}'), Icons.check, color: LinagoraSysColors.material().primary, size: 24, diff --git a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart index dcc0b91943..c754bd9ea5 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart @@ -63,6 +63,7 @@ class SettingsSecurityView extends StatelessWidget { Padding( padding: SettingsViewStyle.bodySettingsScreenPadding, child: SettingsItemBuilder( + key: const Key('contacts_visibility_settings_item'), title: L10n.of(context)!.contactsVisibility, titleColor: Theme.of(context).colorScheme.onBackground, subtitle: L10n.of(context)!.whoCanFindMeByMyContacts, diff --git a/lib/widgets/avatar/bottom_navigation_avatar.dart b/lib/widgets/avatar/bottom_navigation_avatar.dart index 6d45ea4f71..59884b6064 100644 --- a/lib/widgets/avatar/bottom_navigation_avatar.dart +++ b/lib/widgets/avatar/bottom_navigation_avatar.dart @@ -16,6 +16,7 @@ class BottomNavigationAvatar extends StatelessWidget { @override Widget build(BuildContext context) { return ValueListenableBuilder( + key: key, valueListenable: profile, builder: (context, profile, child) { return Container( diff --git a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart index b44d50aaff..62a18bc85b 100644 --- a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart +++ b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart @@ -19,6 +19,7 @@ enum AdaptiveDestinationEnum { switch (this) { case AdaptiveDestinationEnum.contacts: return NavigationDestination( + key: const Key('contacts_navigation_destination'), icon: TwakeNavigationIcon( color: LinagoraSysColors.material().onBackground, icon: Icons.supervised_user_circle_outlined, @@ -31,6 +32,7 @@ enum AdaptiveDestinationEnum { ); case AdaptiveDestinationEnum.rooms: return NavigationDestination( + key: const Key('rooms_navigation_destination'), icon: UnreadRoomsBadge( color: LinagoraSysColors.material().onBackground, filter: (room) => !room.isSpace && !room.isStoryRoom, @@ -43,6 +45,7 @@ enum AdaptiveDestinationEnum { ); case AdaptiveDestinationEnum.settings: return NavigationDestination( + key: const Key('settings_navigation_destination'), icon: TwakeNavigationIcon( color: LinagoraSysColors.material().onBackground, icon: Icons.settings_outlined, @@ -88,10 +91,12 @@ enum AdaptiveDestinationEnum { case AdaptiveDestinationEnum.settings: return BottomNavigationBarItem( icon: BottomNavigationAvatar( + key: const Key('settings_navigation_destination'), profile: profile, isSelected: false, ), activeIcon: BottomNavigationAvatar( + key: const Key('settings_navigation_destination'), profile: profile, isSelected: true, ), From b361cea68ee611144156c36387c9814007a2438c Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 26 Nov 2025 10:56:31 +0700 Subject: [PATCH 04/13] fixup! TW-2710: Apply E2E for contacts visibility --- assets/l10n/intl_en.arb | 2 ++ .../settings_contacts_visibility.dart | 21 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 4cfae3e568..88017fa9af 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3492,6 +3492,8 @@ "chooseWhichDetailsAreVisibleToOtherUsers": "CHOOSE WHICH DETAILS ARE VISIBLE TO OTHER USERS", "youNumberIsVisibleAccordingToTheSettingAbove": "Your number is visible according to the setting above", "youEmailIsVisibleAccordingToTheSettingAbove": "Your email is visible according to the setting above", + "failedToChangeContactsVisibility": "Failed to change contacts visibility", + "failedToGetContactsVisibility": "Failed to get contacts visibility", "unblockUserToSendMessages": "Unblock user to send messages", "supportChat": "Support Chat", "supportChatDescription": "I’ve got your back! If you encounter bugs or need any help using Twake Workplace, send a message, I will reply to you swiftly" diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index b4a56f5ec4..a4c3293b4b 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -12,6 +12,8 @@ import 'package:fluffychat/domain/usecase/user_info/update_user_info_visibility_ import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/generated/l10n/app_localizations.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -83,12 +85,20 @@ class SettingsContactsVisibilityController ); break; case SettingsContactsVisibilityEnum.contacts: - selectedVisibilityOptionNotifier.value = option; + updateUserInfoVisibility( + userInfoVisibility: UserInfoVisibilityRequest( + visibility: option.name, + visibleFields: selectedVisibleFieldNotifier.value.isEmpty + ? visibleFieldsOptions + : selectedVisibleFieldNotifier.value, + ), + ); break; case SettingsContactsVisibilityEnum.private: updateUserInfoVisibility( userInfoVisibility: UserInfoVisibilityRequest( visibility: option.name, + visibleFields: selectedVisibleFieldNotifier.value, ), ); break; @@ -102,7 +112,6 @@ class SettingsContactsVisibilityController .where((field) => field != selectedField) .toList() : [...selectedVisibleFieldNotifier.value, selectedField]; - selectedVisibleFieldNotifier.value = currentVisibleFields; updateUserInfoVisibility( userInfoVisibility: UserInfoVisibilityRequest( visibility: SettingsContactsVisibilityEnum.contacts.name, @@ -126,6 +135,10 @@ class SettingsContactsVisibilityController (failure) { if (failure is UpdateUserInfoVisibilityFailure) { TwakeDialog.hideLoadingDialog(context); + TwakeSnackBar.show( + context, + L10n.of(context)!.failedToChangeContactsVisibility, + ); } }, (success) { @@ -159,6 +172,10 @@ class SettingsContactsVisibilityController (failure) { if (failure is GetUserInfoVisibilityFailure) { TwakeDialog.hideLoadingDialog(context); + TwakeSnackBar.show( + context, + L10n.of(context)!.failedToGetContactsVisibility, + ); } }, (success) { From 67343840d1edfe96017d10ddf31f78cf5eaa590c Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 26 Nov 2025 11:04:59 +0700 Subject: [PATCH 05/13] fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility.dart | 3 +++ .../settings_contacts_visibility_view.dart | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index a4c3293b4b..20e3ef15b3 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -75,6 +75,9 @@ class SettingsContactsVisibilityController ValueNotifier>([]); void onSelectVisibilityOption(SettingsContactsVisibilityEnum option) { + if (option == selectedVisibilityOptionNotifier.value) { + return; + } switch (option) { case SettingsContactsVisibilityEnum.public: updateUserInfoVisibility( diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index a514d95d18..2aac9b7933 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -188,7 +188,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { }) { return InkWell( key: Key('visibility_option_${option.name}'), - onTap: !isSelected ? () => onTap?.call(option) : null, + onTap: () => onTap?.call(option), child: Row( children: [ Expanded( @@ -248,7 +248,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { }) { return InkWell( key: Key('visible_field_option_${option.name}'), - onTap: !isSelected ? () => onTap?.call(option) : null, + onTap: () => onTap?.call(option), child: Row( children: [ Expanded( From dc3ee2fd7fbc74426ce4a235d5da6620a219095c Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 10 Dec 2025 12:41:59 +0700 Subject: [PATCH 06/13] fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility_robot.dart | 35 +++---------------- .../model/user_info/user_info_visibility.dart | 29 --------------- .../settings_contacts_visibility.dart | 12 +++++++ .../settings_contacts_visibility_view.dart | 28 +++++++++------ .../user_info_visibility_extension.dart | 27 ++++++++++++++ .../avatar/bottom_navigation_avatar.dart | 2 +- 6 files changed, 62 insertions(+), 71 deletions(-) create mode 100644 lib/presentation/extensions/settings/user_info_visibility_extension.dart diff --git a/integration_test/robots/setting/settings_contacts_visibility_robot.dart b/integration_test/robots/setting/settings_contacts_visibility_robot.dart index 83ac243d91..dd7d440d88 100644 --- a/integration_test/robots/setting/settings_contacts_visibility_robot.dart +++ b/integration_test/robots/setting/settings_contacts_visibility_robot.dart @@ -111,48 +111,23 @@ class SettingsContactsVisibilityRobot extends HomeRobot { // Helper methods to check states bool isEveryoneSelected() { - try { - everyoneSelected().exists; - return true; - } catch (e) { - return false; - } + return everyoneSelected().exists; } bool isContactsSelected() { - try { - contactsSelected().exists; - return true; - } catch (e) { - return false; - } + return contactsSelected().exists; } bool isNobodySelected() { - try { - nobodySelected().exists; - return true; - } catch (e) { - return false; - } + return nobodySelected().exists; } bool isEmailFieldSelected() { - try { - emailFieldSelected().exists; - return true; - } catch (e) { - return false; - } + return emailFieldSelected().exists; } bool isPhoneNumberFieldSelected() { - try { - phoneNumberFieldSelected().exists; - return true; - } catch (e) { - return false; - } + return phoneNumberFieldSelected().exists; } Future waitForView() async { diff --git a/lib/domain/model/user_info/user_info_visibility.dart b/lib/domain/model/user_info/user_info_visibility.dart index 50413acaff..f172dc8b45 100644 --- a/lib/domain/model/user_info/user_info_visibility.dart +++ b/lib/domain/model/user_info/user_info_visibility.dart @@ -1,40 +1,11 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:fluffychat/generated/l10n/app_localizations.dart'; part 'user_info_visibility.g.dart'; enum VisibleEnum { email, phone; - - String title(BuildContext context) { - switch (this) { - case VisibleEnum.phone: - return L10n.of(context)!.phone; - case VisibleEnum.email: - return L10n.of(context)!.email; - } - } - - String subtitle(BuildContext context) { - switch (this) { - case VisibleEnum.phone: - return L10n.of(context)!.youNumberIsVisibleAccordingToTheSettingAbove; - case VisibleEnum.email: - return L10n.of(context)!.youEmailIsVisibleAccordingToTheSettingAbove; - } - } - - bool enableDivider() { - switch (this) { - case VisibleEnum.phone: - return true; - case VisibleEnum.email: - return false; - } - } } @JsonSerializable() diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index 20e3ef15b3..94bf2ccb39 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -126,6 +126,14 @@ class SettingsContactsVisibilityController void updateUserInfoVisibility({ required UserInfoVisibilityRequest userInfoVisibility, }) { + if (client.userID == null) { + TwakeSnackBar.show( + context, + L10n.of(context)!.failedToChangeContactsVisibility, + ); + return; + } + updateUserInfoVisibilityStreamSub?.cancel(); updateUserInfoVisibilityStreamSub = updateUserInfoVisibilityInteractor .execute( userId: client.userID!, @@ -151,6 +159,8 @@ class SettingsContactsVisibilityController (option) => option.name == success.userInfoVisibility.visibility, orElse: () => SettingsContactsVisibilityEnum.private, ); + selectedVisibleFieldNotifier.value = + success.userInfoVisibility.visibleFields ?? []; TwakeDialog.hideLoadingDialog(context); } @@ -210,6 +220,8 @@ class SettingsContactsVisibilityController @override void dispose() { getUserInfoVisibilityStreamSub?.cancel(); + updateUserInfoVisibilityStreamSub?.cancel(); + selectedVisibleFieldNotifier.dispose(); selectedVisibilityOptionNotifier.dispose(); getUserInfoVisibilityNotifier.dispose(); updateUserInfoVisibilityNotifier.dispose(); diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index 2aac9b7933..5aee59f3c3 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_enum.dart'; +import 'package:fluffychat/presentation/extensions/settings/user_info_visibility_extension.dart'; import 'package:fluffychat/widgets/app_bars/twake_app_bar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/generated/l10n/app_localizations.dart'; @@ -35,7 +36,6 @@ class SettingsContactsVisibilityView extends StatelessWidget { ), ), body: MaxWidthBody( - withScrolling: true, child: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: SizedBox( @@ -149,16 +149,22 @@ class SettingsContactsVisibilityView extends StatelessWidget { valueListenable: controller .getUserInfoVisibilityNotifier, builder: (context, state, _) { - return _buildVisibleFieldIem( - context: context, - option: option, - enableDivider: option.enableDivider(), - isSelected: controller - .selectedVisibleFieldNotifier - .value - .contains(option), - onTap: - controller.onUpdateVisibleFields, + return ValueListenableBuilder( + valueListenable: controller + .selectedVisibleFieldNotifier, + builder: + (context, selectedFields, child) { + return _buildVisibleFieldIem( + context: context, + option: option, + enableDivider: + option.enableDivider(), + isSelected: selectedFields + .contains(option), + onTap: controller + .onUpdateVisibleFields, + ); + }, ); }, ), diff --git a/lib/presentation/extensions/settings/user_info_visibility_extension.dart b/lib/presentation/extensions/settings/user_info_visibility_extension.dart new file mode 100644 index 0000000000..08a400769a --- /dev/null +++ b/lib/presentation/extensions/settings/user_info_visibility_extension.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/domain/model/user_info/user_info_visibility.dart'; +import 'package:fluffychat/generated/l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; + +extension VisibleEnumExtension on VisibleEnum { + String title(BuildContext context) { + switch (this) { + case VisibleEnum.phone: + return L10n.of(context)!.phone; + case VisibleEnum.email: + return L10n.of(context)!.email; + } + } + + String subtitle(BuildContext context) { + switch (this) { + case VisibleEnum.phone: + return L10n.of(context)!.youNumberIsVisibleAccordingToTheSettingAbove; + case VisibleEnum.email: + return L10n.of(context)!.youEmailIsVisibleAccordingToTheSettingAbove; + } + } + + bool enableDivider() { + return this == VisibleEnum.phone; + } +} diff --git a/lib/widgets/avatar/bottom_navigation_avatar.dart b/lib/widgets/avatar/bottom_navigation_avatar.dart index 59884b6064..1efb0c8251 100644 --- a/lib/widgets/avatar/bottom_navigation_avatar.dart +++ b/lib/widgets/avatar/bottom_navigation_avatar.dart @@ -13,10 +13,10 @@ class BottomNavigationAvatar extends StatelessWidget { required this.isSelected, required this.profile, }); + @override Widget build(BuildContext context) { return ValueListenableBuilder( - key: key, valueListenable: profile, builder: (context, profile, child) { return Container( From 7776fc85d7cae2905bb8bebd884c4d7c3a76bc32 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 11 Dec 2025 14:07:26 +0700 Subject: [PATCH 07/13] fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility_robot.dart | 9 --------- lib/domain/model/user_info/user_info.dart | 6 +++--- .../chat_details/chat_details_view_style.dart | 6 +++--- .../chat_profile_info_view.dart | 8 ++++---- .../profile_info_contact_rows.dart | 8 ++++---- .../settings_contacts_visibility.dart | 16 ++++++---------- .../settings_contacts_visibility_view.dart | 4 ++-- 7 files changed, 22 insertions(+), 35 deletions(-) diff --git a/integration_test/robots/setting/settings_contacts_visibility_robot.dart b/integration_test/robots/setting/settings_contacts_visibility_robot.dart index dd7d440d88..bbb9a1801c 100644 --- a/integration_test/robots/setting/settings_contacts_visibility_robot.dart +++ b/integration_test/robots/setting/settings_contacts_visibility_robot.dart @@ -8,10 +8,6 @@ import '../home_robot.dart'; class SettingsContactsVisibilityRobot extends HomeRobot { SettingsContactsVisibilityRobot(super.$); - PatrolFinder backButton() { - return $(IconButton).containing($(Icon)); - } - // Visibility option finders PatrolFinder visibilityOption( SettingsContactsVisibilityEnum visibilityOption, @@ -104,11 +100,6 @@ class SettingsContactsVisibilityRobot extends HomeRobot { await $.pumpAndSettle(); } - Future backToSettingsScreen() async { - await backButton().tap(); - await $.pumpAndSettle(); - } - // Helper methods to check states bool isEveryoneSelected() { return everyoneSelected().exists; diff --git a/lib/domain/model/user_info/user_info.dart b/lib/domain/model/user_info/user_info.dart index 37d456435f..f191bc8812 100644 --- a/lib/domain/model/user_info/user_info.dart +++ b/lib/domain/model/user_info/user_info.dart @@ -11,7 +11,7 @@ class UserInfo extends Equatable { @JsonKey(name: 'avatar') final String? avatarUrl; final List? phones; - final List? mails; + final List? emails; final String? sn; final String? givenName; final String? language; @@ -26,7 +26,7 @@ class UserInfo extends Equatable { this.displayName, this.avatarUrl, this.phones, - this.mails, + this.emails, this.sn, this.givenName, this.language, @@ -46,7 +46,7 @@ class UserInfo extends Equatable { displayName, avatarUrl, phones, - mails, + emails, sn, givenName, language, diff --git a/lib/pages/chat_details/chat_details_view_style.dart b/lib/pages/chat_details/chat_details_view_style.dart index 964b5611b5..645b1c643e 100644 --- a/lib/pages/chat_details/chat_details_view_style.dart +++ b/lib/pages/chat_details/chat_details_view_style.dart @@ -15,9 +15,9 @@ class ChatDetailViewStyle { EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0); // Informations Content - static const double minToolbarHeightSliverAppBar = 340.0; - static const double mediumToolbarHeightSliverAppBar = 344.0; - static const double maxToolbarHeightSliverAppBar = 394.0; + static const double minToolbarHeightSliverAppBar = 376.0; + static const double mediumToolbarHeightSliverAppBar = 416.0; + static const double maxToolbarHeightSliverAppBar = 464.0; static const double groupToolbarHeightSliverAppBar = 360.0; static const double avatarSize = 96; static double chatDetailsPageViewWebBorderRadius = 16.0; diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index da37429880..3f3fe5c7ce 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -341,7 +341,7 @@ class _Information extends StatelessWidget { if (success is GetUserInfoSuccess) { return Column( children: [ - if (success.userInfo.mails?.firstOrNull != + if (success.userInfo.emails?.firstOrNull != null) ...{ const SizedBox( height: ChatProfileInfoStyle.textSpacing, @@ -349,7 +349,7 @@ class _Information extends StatelessWidget { _CopiableRowWithMaterialIcon( icon: Icons.alternate_email, text: - success.userInfo.mails?.firstOrNull ?? + success.userInfo.emails?.firstOrNull ?? '', ), }, @@ -717,12 +717,12 @@ class _SizedAppBar extends StatelessWidget { return ChatDetailViewStyle.mediumToolbarHeightSliverAppBar; } if (success is GetUserInfoSuccess) { - if (success.userInfo.mails != null && + if (success.userInfo.emails != null && success.userInfo.phones != null) { return ChatDetailViewStyle.maxToolbarHeightSliverAppBar; } - if (success.userInfo.mails != null || + if (success.userInfo.emails != null || success.userInfo.phones != null) { return ChatDetailViewStyle.mediumToolbarHeightSliverAppBar; } diff --git a/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart index 4074c7f6c3..0bf036de71 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart @@ -45,7 +45,7 @@ class ProfileInfoContactRows extends StatelessWidget { copiableText: user.id, enableDividerTop: userInfoModel != null && (userInfoModel.phones?.firstOrNull != null || - userInfoModel.mails?.firstOrNull != null), + userInfoModel.emails?.firstOrNull != null), ), if (isLoading) _LoadingPlaceholder() @@ -56,13 +56,13 @@ class ProfileInfoContactRows extends StatelessWidget { caption: L10n.of(context)!.phone, copiableText: userInfoModel!.phones!.firstOrNull ?? '', enableDividerTop: - userInfoModel.mails?.firstOrNull != null, + userInfoModel.emails?.firstOrNull != null, ), - if (userInfoModel?.mails?.firstOrNull != null) + if (userInfoModel?.emails?.firstOrNull != null) IconCopiableProfileRow( icon: Icons.alternate_email, caption: L10n.of(context)!.email, - copiableText: userInfoModel!.mails!.firstOrNull ?? '', + copiableText: userInfoModel!.emails!.firstOrNull ?? '', ), ], ], diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index 94bf2ccb39..ce96c500a2 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -80,10 +80,14 @@ class SettingsContactsVisibilityController } switch (option) { case SettingsContactsVisibilityEnum.public: + case SettingsContactsVisibilityEnum.private: updateUserInfoVisibility( userInfoVisibility: UserInfoVisibilityRequest( visibility: option.name, - visibleFields: [], + visibleFields: [ + VisibleEnum.phone, + VisibleEnum.email, + ], ), ); break; @@ -97,14 +101,6 @@ class SettingsContactsVisibilityController ), ); break; - case SettingsContactsVisibilityEnum.private: - updateUserInfoVisibility( - userInfoVisibility: UserInfoVisibilityRequest( - visibility: option.name, - visibleFields: selectedVisibleFieldNotifier.value, - ), - ); - break; } } @@ -213,8 +209,8 @@ class SettingsContactsVisibilityController @override void initState() { - initialGetUserInfoVisibility(); super.initState(); + initialGetUserInfoVisibility(); } @override diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index 5aee59f3c3..157485781b 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -154,7 +154,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { .selectedVisibleFieldNotifier, builder: (context, selectedFields, child) { - return _buildVisibleFieldIem( + return _buildVisibleFieldItem( context: context, option: option, enableDivider: @@ -245,7 +245,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { ); } - Widget _buildVisibleFieldIem({ + Widget _buildVisibleFieldItem({ required BuildContext context, required VisibleEnum option, void Function(VisibleEnum)? onTap, From d1b1b9d52f0659df06905b922e392e7fcc558459 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 11 Dec 2025 14:13:32 +0700 Subject: [PATCH 08/13] fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- lib/pages/chat_profile_info/chat_profile_info_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index 3f3fe5c7ce..3372ba1817 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -348,9 +348,9 @@ class _Information extends StatelessWidget { ), _CopiableRowWithMaterialIcon( icon: Icons.alternate_email, - text: - success.userInfo.emails?.firstOrNull ?? - '', + text: success + .userInfo.emails?.firstOrNull ?? + '', ), }, if (success.userInfo.phones?.firstOrNull != From 4de4996a3e2c7e3b1eb22290c3b8a06803ea4193 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 11 Dec 2025 14:16:51 +0700 Subject: [PATCH 09/13] fixup! fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index ce96c500a2..9059ce6a5f 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -56,13 +56,13 @@ class SettingsContactsVisibilityController Right(UpdatingUserInfoVisibility()), ); - List visibilityOptions = [ + final List visibilityOptions = [ SettingsContactsVisibilityEnum.public, SettingsContactsVisibilityEnum.contacts, SettingsContactsVisibilityEnum.private, ]; - List visibleFieldsOptions = [ + final List visibleFieldsOptions = [ VisibleEnum.phone, VisibleEnum.email, ]; @@ -84,10 +84,7 @@ class SettingsContactsVisibilityController updateUserInfoVisibility( userInfoVisibility: UserInfoVisibilityRequest( visibility: option.name, - visibleFields: [ - VisibleEnum.phone, - VisibleEnum.email, - ], + visibleFields: visibleFieldsOptions, ), ); break; From f369c4e940bae677edbc68f8378c7e5057893b4d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 12 Dec 2025 10:38:13 +0700 Subject: [PATCH 10/13] fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility_test.dart | 41 ++++++++++++++++++- lib/data/network/tom_endpoint.dart | 4 ++ lib/data/network/user_info/user_info_api.dart | 9 ++-- .../settings_contacts_visibility_view.dart | 38 +++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/integration_test/tests/setting/settings_contacts_visibility_test.dart b/integration_test/tests/setting/settings_contacts_visibility_test.dart index 78f0daff77..ba88164da7 100644 --- a/integration_test/tests/setting/settings_contacts_visibility_test.dart +++ b/integration_test/tests/setting/settings_contacts_visibility_test.dart @@ -1,3 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + import '../../base/test_base.dart'; import '../../robots/home_robot.dart'; import '../../robots/setting/settings_contacts_visibility_robot.dart'; @@ -19,6 +21,22 @@ void main() { // Verify everyone option is present await contactsVisibilityRobot.everyoneOption().waitUntilVisible(); + + // Select every one option + await contactsVisibilityRobot.selectEveryoneOption(); + + // Wait for UI to settle after selection + await $.pumpAndSettle(); + + // Verify email and phone fields are NOT visible when everyone is selected + expect( + contactsVisibilityRobot.emailFieldOption(), + findsNothing, + ); + expect( + contactsVisibilityRobot.phoneNumberFieldOption(), + findsNothing, + ); }, ); @@ -38,8 +56,13 @@ void main() { // Verify contacts option is present await contactsVisibilityRobot.contactsOption().waitUntilVisible(); - /// Verify email and phone visible fields are present + // Select contacts option + await contactsVisibilityRobot.selectContactsOption(); + // Wait for UI to settle after selection + await $.pumpAndSettle(); + + // Verify email and phone visible fields are present await contactsVisibilityRobot.emailFieldOption().waitUntilVisible(); await contactsVisibilityRobot.phoneNumberFieldOption().waitUntilVisible(); @@ -61,6 +84,22 @@ void main() { // Verify nobody option is present await contactsVisibilityRobot.nobodyOption().waitUntilVisible(); + + // Select nobody option + await contactsVisibilityRobot.selectNobodyOption(); + + // Wait for UI to settle after selection + await $.pumpAndSettle(); + + // Verify email and phone fields are NOT visible when nobody is selected + expect( + contactsVisibilityRobot.emailFieldOption(), + findsNothing, + ); + expect( + contactsVisibilityRobot.phoneNumberFieldOption(), + findsNothing, + ); }, ); } diff --git a/lib/data/network/tom_endpoint.dart b/lib/data/network/tom_endpoint.dart index 7fc357f750..034ebd24eb 100644 --- a/lib/data/network/tom_endpoint.dart +++ b/lib/data/network/tom_endpoint.dart @@ -41,4 +41,8 @@ extension ServicePathTom on ServicePath { }) { return '$rootPath/$apiVersion$path/$userId'; } + + String userInfoVisibilityServicePath(String userId) { + return '${generateTomUserInfoEndpoint(userId)}/visibility'; + } } diff --git a/lib/data/network/user_info/user_info_api.dart b/lib/data/network/user_info/user_info_api.dart index 3e2de17293..4d059d6b27 100644 --- a/lib/data/network/user_info/user_info_api.dart +++ b/lib/data/network/user_info/user_info_api.dart @@ -49,10 +49,9 @@ class UserInfoApi { ); final uri = - TomEndpoint.userInfoServicePath.generateTomUserInfoEndpoint(userId); + TomEndpoint.userInfoServicePath.userInfoVisibilityServicePath(userId); - final response = - await client.get("$uri/visibility").onError((error, stackTrace) { + final response = await client.get(uri).onError((error, stackTrace) { if (error is DioException) { throw DioException( requestOptions: error.requestOptions, @@ -77,10 +76,10 @@ class UserInfoApi { ); final uri = - TomEndpoint.userInfoServicePath.generateTomUserInfoEndpoint(userId); + TomEndpoint.userInfoServicePath.userInfoVisibilityServicePath(userId); final response = await client - .postToGetBody("$uri/visibility", data: body.toJson()) + .postToGetBody(uri, data: body.toJson()) .onError((error, stackTrace) { if (error is DioException) { throw DioException( diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart index 157485781b..4bb1200325 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility_view.dart @@ -185,6 +185,25 @@ class SettingsContactsVisibilityView extends StatelessWidget { ); } + BorderRadius? _buildVisibilityOptionBorderRadius({ + required SettingsContactsVisibilityEnum option, + }) { + switch (option) { + case SettingsContactsVisibilityEnum.public: + return const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ); + case SettingsContactsVisibilityEnum.contacts: + return null; + case SettingsContactsVisibilityEnum.private: + return const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ); + } + } + Widget _buildVisibilityOptionItem({ required BuildContext context, required SettingsContactsVisibilityEnum option, @@ -195,6 +214,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { return InkWell( key: Key('visibility_option_${option.name}'), onTap: () => onTap?.call(option), + borderRadius: _buildVisibilityOptionBorderRadius(option: option), child: Row( children: [ Expanded( @@ -245,6 +265,23 @@ class SettingsContactsVisibilityView extends StatelessWidget { ); } + BorderRadius? _buildVisibleFieldBorderRadius({ + required VisibleEnum option, + }) { + switch (option) { + case VisibleEnum.phone: + return const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ); + case VisibleEnum.email: + return const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ); + } + } + Widget _buildVisibleFieldItem({ required BuildContext context, required VisibleEnum option, @@ -255,6 +292,7 @@ class SettingsContactsVisibilityView extends StatelessWidget { return InkWell( key: Key('visible_field_option_${option.name}'), onTap: () => onTap?.call(option), + borderRadius: _buildVisibleFieldBorderRadius(option: option), child: Row( children: [ Expanded( From a7f9f44bf55d5c50107f99f7877f8e29bea51e79 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 12 Dec 2025 10:54:24 +0700 Subject: [PATCH 11/13] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- lib/config/go_routes/app_route_paths.dart | 9 +++++++++ lib/config/go_routes/go_router.dart | 3 ++- .../settings_contacts_visibility.dart | 2 ++ .../settings_security/settings_security_view.dart | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 lib/config/go_routes/app_route_paths.dart diff --git a/lib/config/go_routes/app_route_paths.dart b/lib/config/go_routes/app_route_paths.dart new file mode 100644 index 0000000000..e092613409 --- /dev/null +++ b/lib/config/go_routes/app_route_paths.dart @@ -0,0 +1,9 @@ +/// Route path constants for the application. +/// +/// This file contains all route paths used in go_router configuration +/// and navigation calls to ensure consistency and prevent typos. +abstract class AppRoutePaths { + // Security routes + static const String contactsVisibility = 'contactsVisibility'; + static const String contactsVisibilityFull = '/rooms/security/contactsVisibility'; +} \ 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 12a938c2c3..76e3da0b35 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fluffychat/config/first_column_inner_routes.dart'; +import 'package:fluffychat/config/go_routes/app_route_paths.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/add_story/add_story.dart'; import 'package:fluffychat/pages/archive/archive.dart'; @@ -450,7 +451,7 @@ abstract class AppRoutes { redirect: loggedOutRedirect, ), GoRoute( - path: 'contactsVisibility', + path: AppRoutePaths.contactsVisibility, pageBuilder: (context, state) => defaultPageBuilder( context, const SettingsContactsVisibility(), diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index 9059ce6a5f..587426b6b0 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -133,6 +133,7 @@ class SettingsContactsVisibilityController body: userInfoVisibility, ) .listen((either) { + if (!mounted) return; updateUserInfoVisibilityNotifier.value = either; either.fold( @@ -172,6 +173,7 @@ class SettingsContactsVisibilityController getUserInfoVisibilityStreamSub = getUserInfoVisibilityInteractor .execute(userId: client.userID!) .listen((either) { + if (!mounted) return; getUserInfoVisibilityNotifier.value = either; either.fold( diff --git a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart index c754bd9ea5..a5a56bcd8b 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/go_routes/app_route_paths.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_item_builder.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.dart'; @@ -71,7 +72,7 @@ class SettingsSecurityView extends StatelessWidget { leadingIconColor: LinagoraRefColors.material().tertiary[30], onTap: () { - context.push('/rooms/security/contactsVisibility'); + context.push(AppRoutePaths.contactsVisibilityFull); }, ), ), From fee2eddb363710efd7844781a81b38954f0c9a67 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 12 Dec 2025 11:01:46 +0700 Subject: [PATCH 12/13] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- .../settings_contacts_visibility.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart index 587426b6b0..133a4570e3 100644 --- a/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart +++ b/lib/pages/settings_dashboard/settings_contacts_visibility/settings_contacts_visibility.dart @@ -170,6 +170,7 @@ class SettingsContactsVisibilityController if (client.userID == null) { return; } + getUserInfoVisibilityStreamSub?.cancel(); getUserInfoVisibilityStreamSub = getUserInfoVisibilityInteractor .execute(userId: client.userID!) .listen((either) { From 121dcfccff544c8bf47e9c1c9a3aefd2b2010959 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 12 Dec 2025 11:05:30 +0700 Subject: [PATCH 13/13] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! TW-2710: Apply E2E for contacts visibility --- lib/config/go_routes/app_route_paths.dart | 8 +++++--- lib/config/go_routes/go_router.dart | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/config/go_routes/app_route_paths.dart b/lib/config/go_routes/app_route_paths.dart index e092613409..cfddc43fb8 100644 --- a/lib/config/go_routes/app_route_paths.dart +++ b/lib/config/go_routes/app_route_paths.dart @@ -4,6 +4,8 @@ /// and navigation calls to ensure consistency and prevent typos. abstract class AppRoutePaths { // Security routes - static const String contactsVisibility = 'contactsVisibility'; - static const String contactsVisibilityFull = '/rooms/security/contactsVisibility'; -} \ No newline at end of file + static const String roomsSecurityFull = '/rooms/security'; + static const String contactsVisibilitySegment = 'contactsVisibility'; + static const String contactsVisibilityFull = + '$roomsSecurityFull/$contactsVisibilitySegment'; +} diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 76e3da0b35..22f8b88325 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -451,7 +451,7 @@ abstract class AppRoutes { redirect: loggedOutRedirect, ), GoRoute( - path: AppRoutePaths.contactsVisibility, + path: AppRoutePaths.contactsVisibilitySegment, pageBuilder: (context, state) => defaultPageBuilder( context, const SettingsContactsVisibility(),