diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f718dcb1c1..6a84810e3b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3041,6 +3041,10 @@ } } }, + "explainPermissionToAccessContacts": "Twake chat needs access to your contacts to find out whether your friends are on the matrix server. This is done locally, and your contacts are not synchronized with our server.", + "explainPermissionToAccessMedias": "Twake chat needs access to storage so you can send and save photos, videos, music and other documents. Press Settings > Authorizations, then activate Storage authorization: Photos and videos.", + "explainPermissionToAccessPhotos": "Twake chat needs access to your photos so you can send and save images. Press Settings > Permissions, then enable Storage permission: Photos.", + "explainPermissionToAccessVideos": "Twake chat needs access to your videos so you can send and save videos. Press Settings > Permissions, then enable Storage permission: Videos.", "downloading": "Downloading", "settingUpYourTwake": "Setting up your Twake\nIt could take a while", "performingAutomaticalLogin": "Performing automatical login via SSO", diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index ab65dd7510..8278672104 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -2714,6 +2714,10 @@ "count": {} } }, + "explainPermissionToAccessContacts": "Twake chat doit accéder à vos contacts pour savoir si vos amis sont sur le serveur matrix. Cela se fait localement et vos contacts ne sont pas synchronisés avec notre serveur.", + "explainPermissionToAccessMedias": "Twake chat a besoin d'accéder au stockage pour que vous puissiez envoyer et enregistrer des photos, vidéos, musiques et autres documents. Appuyez sur Paramètres > Autorisations puis activez l'autorisation de stockage: Photos et vidéos.", + "explainPermissionToAccessPhotos": "Twake chat a besoin d'accéder à vos photos pour que vous puissiez envoyer et de enregistrer des images. Allez dans Paramètres > Autorisations, puis activez l'autorisation de stockage : Photos.", + "explainPermissionToAccessVideos": "Twake chat a besoin d'accéder à vos vidéos pour que vous puissiez envoyer et de sauvegarder des vidéos. Allez dans Paramètres > Autorisations, puis activez l'autorisation de stockage : Vidéos.", "recentChat": "DISCUSSION RÉCENTE", "@recentChat": {}, "muteThisMessage": "Couper le son de ce salon", diff --git a/lib/domain/contact_manager/contacts_manager.dart b/lib/domain/contact_manager/contacts_manager.dart index 81a86b2fdd..1cac10b553 100644 --- a/lib/domain/contact_manager/contacts_manager.dart +++ b/lib/domain/contact_manager/contacts_manager.dart @@ -17,6 +17,8 @@ class ContactsManager { bool _doNotShowWarningContactsBannerAgain = false; + bool _doNotShowWarningContactsDialogAgain = false; + final ValueNotifierCustom> _contactsNotifier = ValueNotifierCustom(const Right(ContactsInitial())); @@ -41,10 +43,17 @@ class ContactsManager { bool get isDoNotShowWarningContactsBannerAgain => _doNotShowWarningContactsBannerAgain; + bool get isDoNotShowWarningContactsDialogAgain => + _doNotShowWarningContactsDialogAgain; + set updateNotShowWarningContactsBannerAgain(bool value) { _doNotShowWarningContactsBannerAgain = value; } + set updateNotShowWarningContactsDialogAgain(bool value) { + _doNotShowWarningContactsDialogAgain = value; + } + Future reSyncContacts() async { _contactsNotifier.value = const Right(ContactsInitial()); _phonebookContactsNotifier.value = @@ -92,4 +101,7 @@ class ContactsManager { }, ); } + + void refreshPhonebookContacts() => + _fetchPhonebookContacts(isAvailableSupportPhonebookContacts: true); } diff --git a/lib/pages/chat_details/chat_details_edit.dart b/lib/pages/chat_details/chat_details_edit.dart index ef44388ddb..bb5534c47b 100644 --- a/lib/pages/chat_details/chat_details_edit.dart +++ b/lib/pages/chat_details/chat_details_edit.dart @@ -136,7 +136,7 @@ class ChatDetailsEditController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); showImagePickerBottomSheet( diff --git a/lib/pages/contacts_tab/contacts_tab.dart b/lib/pages/contacts_tab/contacts_tab.dart index 7e02a631dd..cf5e4e429b 100644 --- a/lib/pages/contacts_tab/contacts_tab.dart +++ b/lib/pages/contacts_tab/contacts_tab.dart @@ -7,11 +7,12 @@ import 'package:fluffychat/presentation/model/contact/presentation_contact_const import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class ContactsTab extends StatefulWidget { @@ -27,7 +28,10 @@ class ContactsTab extends StatefulWidget { } class ContactsTabController extends State - with ComparablePresentationContactMixin, ContactsViewControllerMixin { + with + ComparablePresentationContactMixin, + ContactsViewControllerMixin, + WidgetsBindingObserver { final responsive = getIt.get(); Client get client => Matrix.of(context).client; @@ -35,8 +39,10 @@ class ContactsTabController extends State @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: Matrix.of(context).client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -100,9 +106,15 @@ class ContactsTabController extends State } } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { disposeContactsMixin(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/pages/contacts_tab/contacts_tab_body_view.dart b/lib/pages/contacts_tab/contacts_tab_body_view.dart index 5ea6f239b4..1483528858 100644 --- a/lib/pages/contacts_tab/contacts_tab_body_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_body_view.dart @@ -418,8 +418,8 @@ class _SliverWarningBanner extends StatelessWidget { child: ContactsWarningBannerView( warningBannerNotifier: controller.warningBannerNotifier, closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), ), ); } diff --git a/lib/pages/new_group/contacts_selection.dart b/lib/pages/new_group/contacts_selection.dart index b8cd478738..aeb9a6c8e0 100644 --- a/lib/pages/new_group/contacts_selection.dart +++ b/lib/pages/new_group/contacts_selection.dart @@ -6,14 +6,17 @@ import 'package:fluffychat/pages/new_group/selected_contacts_map_change_notifier import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter/cupertino.dart'; import 'package:matrix/matrix.dart'; abstract class ContactsSelectionController extends State - with InviteExternalContactMixin, ContactsViewControllerMixin { + with + InviteExternalContactMixin, + ContactsViewControllerMixin, + WidgetsBindingObserver { final selectedContactsMapNotifier = SelectedContactsMapChangeNotifier(); String getTitle(BuildContext context); @@ -37,8 +40,10 @@ abstract class ContactsSelectionController @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -47,9 +52,16 @@ abstract class ContactsSelectionController super.initState(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); disposeContactsMixin(); + selectedContactsMapNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/new_group/contacts_selection_view.dart b/lib/pages/new_group/contacts_selection_view.dart index 7c9ac986f4..81bc505041 100644 --- a/lib/pages/new_group/contacts_selection_view.dart +++ b/lib/pages/new_group/contacts_selection_view.dart @@ -54,8 +54,8 @@ class ContactsSelectionView extends StatelessWidget { warningBannerNotifier: controller.warningBannerNotifier, closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), ), ), SliverToBoxAdapter( diff --git a/lib/pages/new_group/new_group_chat_info.dart b/lib/pages/new_group/new_group_chat_info.dart index ccdfd22f69..fdb43d036b 100644 --- a/lib/pages/new_group/new_group_chat_info.dart +++ b/lib/pages/new_group/new_group_chat_info.dart @@ -284,7 +284,7 @@ class NewGroupChatInfoController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); groupNameFocusNode.unfocus(); diff --git a/lib/pages/new_private_chat/new_private_chat.dart b/lib/pages/new_private_chat/new_private_chat.dart index 05140cd85b..f8484b3e75 100644 --- a/lib/pages/new_private_chat/new_private_chat.dart +++ b/lib/pages/new_private_chat/new_private_chat.dart @@ -25,6 +25,7 @@ class NewPrivateChatController extends State ComparablePresentationContactMixin, ContactsViewControllerMixin, GoToDraftChatMixin, + WidgetsBindingObserver, InviteExternalContactMixin, GoToGroupChatMixin { final isShowContactsNotifier = ValueNotifier(true); @@ -34,8 +35,10 @@ class NewPrivateChatController extends State void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: Matrix.of(context).client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -84,9 +87,16 @@ class NewPrivateChatController extends State }); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { super.dispose(); + WidgetsBinding.instance.removeObserver(this); + isShowContactsNotifier.dispose(); disposeContactsMixin(); scrollController.dispose(); } diff --git a/lib/pages/new_private_chat/new_private_chat_style.dart b/lib/pages/new_private_chat/new_private_chat_style.dart new file mode 100644 index 0000000000..ff06436bc8 --- /dev/null +++ b/lib/pages/new_private_chat/new_private_chat_style.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class NewPrivateChatStyle { + static const EdgeInsets paddingBody = EdgeInsets.only(left: 8.0, right: 10.0); + + static const EdgeInsets paddingWarningBanner = EdgeInsets.only(top: 16.0); +} diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index 652d19b696..996a8a51e1 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,8 +1,10 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; +import 'package:fluffychat/pages/new_private_chat/new_private_chat_style.dart'; import 'package:fluffychat/pages/new_private_chat/widget/expansion_list.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar_style.dart'; +import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -31,20 +33,36 @@ class NewPrivateChatView extends StatelessWidget { keyboardDismissBehavior: PlatformInfos.isMobile ? ScrollViewKeyboardDismissBehavior.manual : ScrollViewKeyboardDismissBehavior.onDrag, - padding: const EdgeInsets.only(left: 8.0, right: 10.0), + padding: NewPrivateChatStyle.paddingBody, controller: controller.scrollController, - child: ExpansionList( - presentationContactsNotifier: controller.presentationContactNotifier, - goToNewGroupChat: () => controller.goToNewGroupChat(context), - isShowContactsNotifier: controller.isShowContactsNotifier, - onContactTap: controller.onContactAction, - onExternalContactTap: controller.onExternalContactAction, - toggleContactsList: controller.toggleContactsList, - textEditingController: controller.textEditingController, - warningBannerNotifier: controller.warningBannerNotifier, - closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + child: Column( + children: [ + Padding( + padding: NewPrivateChatStyle.paddingWarningBanner, + child: ContactsWarningBannerView( + warningBannerNotifier: controller.warningBannerNotifier, + closeContactsWarningBanner: + controller.closeContactsWarningBanner, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), + isShowMargin: false, + ), + ), + ExpansionList( + presentationContactsNotifier: + controller.presentationContactNotifier, + goToNewGroupChat: () => controller.goToNewGroupChat(context), + isShowContactsNotifier: controller.isShowContactsNotifier, + onContactTap: controller.onContactAction, + onExternalContactTap: controller.onExternalContactAction, + toggleContactsList: controller.toggleContactsList, + textEditingController: controller.textEditingController, + warningBannerNotifier: controller.warningBannerNotifier, + closeContactsWarningBanner: controller.closeContactsWarningBanner, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), + ), + ], ), ), ); diff --git a/lib/pages/new_private_chat/widget/expansion_list.dart b/lib/pages/new_private_chat/widget/expansion_list.dart index c4ef272367..4120b62d8c 100644 --- a/lib/pages/new_private_chat/widget/expansion_list.dart +++ b/lib/pages/new_private_chat/widget/expansion_list.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_ import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; @@ -164,7 +163,6 @@ class ExpansionList extends StatelessWidget { const SizedBox( height: 12, ), - _contactsWarningBannerViewBuilder(), ..._buildResponsiveButtons(context), for (final child in expansionList) ...[child], ] else ...[ @@ -188,15 +186,6 @@ class ExpansionList extends StatelessWidget { ); } - Widget _contactsWarningBannerViewBuilder() { - return ContactsWarningBannerView( - warningBannerNotifier: warningBannerNotifier, - isShowMargin: false, - closeContactsWarningBanner: closeContactsWarningBanner, - goToSettingsForPermissionActions: goToSettingsForPermissionActions, - ); - } - Widget _buildTitle(BuildContext context, int countContacts) { return Padding( padding: const EdgeInsets.only(left: 8.0), diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 63bdb72b24..8fb0d4bc22 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -208,7 +208,7 @@ class SettingsProfileController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); showImagePickerBottomSheet( diff --git a/lib/presentation/mixins/common_media_picker_mixin.dart b/lib/presentation/mixins/common_media_picker_mixin.dart index e856d93ec3..19f43c81d9 100644 --- a/lib/presentation/mixins/common_media_picker_mixin.dart +++ b/lib/presentation/mixins/common_media_picker_mixin.dart @@ -14,8 +14,8 @@ mixin CommonMediaPickerMixin { final PermissionHandlerService _permissionHandlerService = PermissionHandlerService(); - Future? getCurrentMediaPermission() { - return _permissionHandlerService.requestPermissionForMediaActions(); + Future? getCurrentMediaPermission(BuildContext context) { + return _permissionHandlerService.requestPermissionForMediaActions(context); } Future? getCurrentCameraPermission() { @@ -23,7 +23,7 @@ mixin CommonMediaPickerMixin { } Future? getCurrentMicroPermission() { - return _permissionHandlerService.requestPermissionForMircoActions(); + return _permissionHandlerService.requestPermissionForMicroActions(); } void goToSettings( diff --git a/lib/presentation/mixins/contacts_view_controller_mixin.dart b/lib/presentation/mixins/contacts_view_controller_mixin.dart index 983192ba5f..a7eb750de5 100644 --- a/lib/presentation/mixins/contacts_view_controller_mixin.dart +++ b/lib/presentation/mixins/contacts_view_controller_mixin.dart @@ -20,9 +20,11 @@ import 'package:fluffychat/presentation/model/contact/presentation_contact.dart' import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/presentation/model/search/presentation_search_state_extension.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -67,15 +69,106 @@ mixin class ContactsViewControllerMixin { final contactsManager = getIt.get(); - PermissionStatus contactsPermissionStatus = PermissionStatus.granted; + PermissionStatus? contactsPermissionStatus; + + Future displayContactPermissionDialog(BuildContext context) async { + final fetchContactsPermissionStatus = + await _permissionHandlerService.contactsPermissionStatus; + + contactsPermissionStatus = fetchContactsPermissionStatus; + + if (PlatformInfos.isMobile && !fetchContactsPermissionStatus.isGranted) { + await showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.contact_page_outlined), + permission: Permission.contacts, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessContacts, + ), + onRefuseTap: _handleDenyPermissionDialog, + onAcceptButton: () async { + Navigator.of(dialogContext).pop(); + await _handleRequestContactsPermission(); + }, + ); + }, + ); + } + } + + void _handleDenyPermissionDialog() { + warningBannerNotifier.value = WarningContactsBannerState.display; + contactsManager.updateNotShowWarningContactsDialogAgain = true; + } + + Future _initWarningBanner() async { + final currentContactPermission = + await _permissionHandlerService.contactsPermissionStatus; + Logs().i( + 'ContactsViewControllerMixin::_initWarningBanner: Contact Permission $currentContactPermission', + ); + + if (currentContactPermission.isGranted) { + contactsPermissionStatus = currentContactPermission; + warningBannerNotifier.value = WarningContactsBannerState.hide; + return; + } + + if (!contactsManager.isDoNotShowWarningContactsBannerAgain && + contactsManager.isDoNotShowWarningContactsDialogAgain) { + warningBannerNotifier.value = WarningContactsBannerState.display; + return; + } + } + + Future handleDidChangeAppLifecycleState(AppLifecycleState state) async { + if (!PlatformInfos.isMobile) { + return; + } + Logs().i( + 'ContactsViewControllerMixin::handleDidChangeAppLifecycleState: $state', + ); + + if (state == AppLifecycleState.resumed) { + final currentContactPermission = + await _permissionHandlerService.contactsPermissionStatus; + + Logs().i( + 'ContactsViewControllerMixin::handleDidChangeAppLifecycleState: Contact Permission $currentContactPermission', + ); + + if (currentContactPermission != contactsPermissionStatus && + currentContactPermission.isDenied) { + if (!contactsManager.isDoNotShowWarningContactsBannerAgain) { + warningBannerNotifier.value = WarningContactsBannerState.display; + } + contactsPermissionStatus = currentContactPermission; + return; + } + + if (currentContactPermission != contactsPermissionStatus && + currentContactPermission.isGranted) { + contactsPermissionStatus = currentContactPermission; + warningBannerNotifier.value = WarningContactsBannerState.hide; + contactsManager.refreshPhonebookContacts(); + return; + } + } + } void initialFetchContacts({ + required BuildContext context, required Client client, required MatrixLocalizations matrixLocalizations, }) async { if (PlatformInfos.isMobile && - !contactsManager.isDoNotShowWarningContactsBannerAgain) { - await _handleRequestContactsPermission(); + !contactsManager.isDoNotShowWarningContactsDialogAgain) { + await displayContactPermissionDialog(context); + } else { + await _initWarningBanner(); } _refreshAllContacts( client: client, @@ -97,6 +190,7 @@ mixin class ContactsViewControllerMixin { }); contactsManager.initialSynchronizeContacts( isAvailableSupportPhonebookContacts: PlatformInfos.isMobile && + contactsPermissionStatus != null && contactsPermissionStatus == PermissionStatus.granted, ); } @@ -296,8 +390,11 @@ mixin class ContactsViewControllerMixin { final currentContactsPermissionStatus = await _permissionHandlerService.requestContactsPermissionActions(); if (currentContactsPermissionStatus == PermissionStatus.granted) { + contactsManager.refreshPhonebookContacts(); warningBannerNotifier.value = WarningContactsBannerState.hide; } else { + contactsManager.updateNotShowWarningContactsDialogAgain = true; + if (!contactsManager.isDoNotShowWarningContactsBannerAgain) { warningBannerNotifier.value = WarningContactsBannerState.display; } diff --git a/lib/presentation/mixins/media_picker_mixin.dart b/lib/presentation/mixins/media_picker_mixin.dart index 4877314899..90c854b978 100644 --- a/lib/presentation/mixins/media_picker_mixin.dart +++ b/lib/presentation/mixins/media_picker_mixin.dart @@ -44,7 +44,7 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { TextEditingController? captionController, ValueKey? typeAheadKey, }) async { - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { showMediasPickerBottomSheet( context: context, diff --git a/lib/utils/permission_dialog.dart b/lib/utils/permission_dialog.dart index 59aa1ed099..c935dac392 100644 --- a/lib/utils/permission_dialog.dart +++ b/lib/utils/permission_dialog.dart @@ -3,6 +3,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; typedef OnAcceptButton = void Function()?; +typedef OnRefuseTap = void Function()?; class PermissionDialog extends StatefulWidget { final Permission permission; @@ -13,12 +14,15 @@ class PermissionDialog extends StatefulWidget { final OnAcceptButton onAcceptButton; + final OnRefuseTap onRefuseTap; + const PermissionDialog({ super.key, required this.permission, required this.explainTextRequestPermission, this.icon, this.onAcceptButton, + this.onRefuseTap, }); @override @@ -74,6 +78,7 @@ class _PermissionDialogState extends State context: context, text: L10n.of(context)!.deny, onPressed: () { + widget.onRefuseTap?.call(); Navigator.of(context).pop(); }, ), diff --git a/lib/utils/permission_service.dart b/lib/utils/permission_service.dart index 795dd2c6d7..57c770e0f4 100644 --- a/lib/utils/permission_service.dart +++ b/lib/utils/permission_service.dart @@ -1,5 +1,8 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; class PermissionHandlerService { @@ -14,14 +17,16 @@ class PermissionHandlerService { PermissionHandlerService._internal(); - Future? requestPermissionForMediaActions() async { + Future? requestPermissionForMediaActions( + BuildContext context, + ) async { if (Platform.isIOS) { - return _handlePhotosPermissionIOSAction(); + return _handlePhotosPermissionIOSAction(context); } else if (Platform.isAndroid) { if (await _getCurrentAndroidVersion() >= 33) { - return _handleMediaPickerPermissionAndroidHigher33Action(); + return _handleMediaPickerPermissionAndroidHigher33Action(context); } - return _handleMediaPermissionAndroidAction(); + return _handleMediaPermissionAndroidAction(context); } else { return null; } @@ -45,7 +50,7 @@ class PermissionHandlerService { } } - Future requestPermissionForMircoActions() async { + Future requestPermissionForMicroActions() async { final currentStatus = await Permission.microphone.status; if (currentStatus == PermissionStatus.denied || currentStatus == PermissionStatus.permanentlyDenied) { @@ -55,28 +60,72 @@ class PermissionHandlerService { } } - Future _handlePhotosPermissionIOSAction() async { + Future _handlePhotosPermissionIOSAction( + BuildContext context, + ) async { final currentStatus = await Permission.photos.status; - return _handlePhotoPermission(currentStatus); + return _handlePhotoPermission( + currentStatus: currentStatus, + context: context, + ); } - Future _handleMediaPermissionAndroidAction() async { + Future _handleMediaPermissionAndroidAction( + BuildContext context, + ) async { final currentStatus = await Permission.storage.status; - return _handlePhotoPermission(currentStatus); + return _handlePhotoPermission( + currentStatus: currentStatus, + context: context, + ); } - Future - _handleMediaPickerPermissionAndroidHigher33Action() async { - PermissionStatus? photoPermission = await Permission.photos.status; - if (photoPermission == PermissionStatus.denied) { - photoPermission = await Permission.photos.request(); + Future _handleMediaPickerPermissionAndroidHigher33Action( + BuildContext context, + ) async { + if (await Permission.photos.status == PermissionStatus.denied) { + await showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: Permission.contacts, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessPhotos, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(); + await Permission.photos.request(); + }, + ); + }, + ); } - PermissionStatus? videosPermission = await Permission.videos.status; - if (videosPermission == PermissionStatus.denied) { - videosPermission = await Permission.videos.request(); + if (await Permission.videos.status == PermissionStatus.denied) { + await showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.video_camera_back_outlined), + permission: Permission.contacts, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessVideos, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(); + await Permission.videos.request(); + }, + ); + }, + ); } + final photoPermission = await Permission.photos.status; + final videosPermission = await Permission.videos.status; + if (photoPermission == PermissionStatus.granted || videosPermission == PermissionStatus.granted) { return PermissionStatus.granted; @@ -85,15 +134,37 @@ class PermissionHandlerService { return PermissionStatus.denied; } - Future _handlePhotoPermission( - PermissionStatus currentStatus, - ) async { + Future _handlePhotoPermission({ + required PermissionStatus currentStatus, + required BuildContext context, + }) async { switch (currentStatus) { case PermissionStatus.permanentlyDenied: case PermissionStatus.denied: + await showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: + Platform.isIOS ? Permission.photos : Permission.storage, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessMedias, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(); + Platform.isIOS + ? await Permission.photos.request() + : await Permission.storage.request(); + }, + ); + }, + ); final newStatus = Platform.isIOS - ? await Permission.photos.request() - : await Permission.storage.request(); + ? await Permission.photos.status + : await Permission.storage.status; + return newStatus.isGranted ? PermissionStatus.granted : newStatus; case PermissionStatus.granted: @@ -118,10 +189,12 @@ class PermissionHandlerService { Future requestContactsPermissionActions() async { final currentStatus = await contactsPermissionStatus; - if (currentStatus == PermissionStatus.denied || - currentStatus == PermissionStatus.permanentlyDenied) { + if (currentStatus == PermissionStatus.denied) { final newStatus = await Permission.contacts.request(); return newStatus.isGranted ? PermissionStatus.granted : newStatus; + } else if (currentStatus == PermissionStatus.permanentlyDenied) { + goToSettingsForPermissionActions(); + return await contactsPermissionStatus; } else { return currentStatus; } diff --git a/test/mixin/contacts_view_controller_mixin_test.dart b/test/mixin/contacts_view_controller_mixin_test.dart index 6b12c652ae..6dfa7d9f2f 100644 --- a/test/mixin/contacts_view_controller_mixin_test.dart +++ b/test/mixin/contacts_view_controller_mixin_test.dart @@ -33,6 +33,7 @@ class ConcretePresentationSearch extends PresentationSearch { } @GenerateNiceMocks([ + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -114,11 +115,13 @@ void main() { late MockContactsViewControllerMixin mockContactsViewControllerMixin; late Client mockClient; late MatrixLocalizations mockMatrixLocalizations; + late BuildContext mockBuildContext; setUp(() { mockContactsViewControllerMixin = MockContactsViewControllerMixin(); mockMatrixLocalizations = MockMatrixLocalizations(); mockClient = MockClient(); + mockBuildContext = MockBuildContext(); }); group('Test ContactsViewControllerMixin on Web', () { @@ -157,12 +160,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -230,12 +235,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -305,12 +312,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -396,12 +405,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -493,12 +504,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -663,12 +676,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -833,12 +848,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1003,12 +1020,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1202,12 +1221,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1469,12 +1490,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1542,12 +1565,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1619,12 +1644,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1722,12 +1749,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1819,12 +1848,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1989,12 +2020,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2161,12 +2194,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2333,12 +2368,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2534,12 +2571,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ),