feat(mobile): iOS background sync notifications (#1811)

* adds notification handling logic

* notification on background updates for iOS

* fixed regression where i accidentally removed load translations from the background sync

* fixed ios translations

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
martyfuhry
2023-02-21 07:28:52 -05:00
committed by GitHub
parent 2d2cfb0349
commit e9c9b7a3e2
16 changed files with 396 additions and 63 deletions

View File

@@ -0,0 +1,44 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
NotificationPermissionNotifier() :
super(Platform.isAndroid
? PermissionStatus.granted
: PermissionStatus.restricted,
) {
// Sets the initial state
getNotificationPermission().then((p) => state = p);
}
/// Requests the notification permission
/// Note: In Android, this is always granted
Future<PermissionStatus> requestNotificationPermission() async {
final permission = await Permission.notification.request();
state = permission;
return permission;
}
/// Whether the user has the permission or not
/// Note: In Android, this is always true
Future<bool> hasNotificationPermission() {
return Permission.notification.isGranted;
}
Future<PermissionStatus> getNotificationPermission() async {
final status = await Permission.notification.status;
state = status;
return status;
}
/// Either the permission was granted already or else ask for the permission
Future<bool> hasOrAskForNotificationPermission() {
return requestNotificationPermission().then((p) => p.isGranted);
}
}
final notificationPermissionProvider
= StateNotifierProvider<NotificationPermissionNotifier, PermissionStatus>
((ref) => NotificationPermissionNotifier());

View File

@@ -0,0 +1,21 @@
import 'package:permission_handler/permission_handler.dart';
/// This class is for requesting permissions in the app
class PermissionService {
/// Requests the notification permission
/// Note: In Android, this is always granted
Future<PermissionStatus> requestNotificationPermission() {
return Permission.notification.request();
}
/// Whether the user has the permission or not
/// Note: In Android, this is always true
Future<bool> hasNotificationPermission() {
return Permission.notification.isGranted;
}
/// Either the permission was granted already or else ask for the permission
Future<bool> hasOrAskForNotificationPermission() {
return requestNotificationPermission().then((p) => p.isGranted);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
SwitchListTile buildSwitchListTile(
BuildContext context,
AppSettingsService appSettingService,
ValueNotifier<bool> valueNotifier,
AppSettingsEnum settingsEnum, {
required String title,
String? subtitle,
}) {
return SwitchListTile.adaptive(
key: Key(settingsEnum.name),
value: valueNotifier.value,
onChanged: (value) {
valueNotifier.value = value;
appSettingService.setSetting(settingsEnum, value);
},
activeColor: Theme.of(context).primaryColor,
dense: true,
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: subtitle != null ? Text(subtitle) : null,
);
}

View File

@@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/common.dart';
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
class ImageViewerQualitySetting extends HookConsumerWidget {
const ImageViewerQualitySetting({
@@ -44,19 +44,17 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
title: const Text('setting_image_viewer_help').tr(),
dense: true,
),
buildSwitchListTile(
context,
settings,
isPreview,
AppSettingsEnum.loadPreview,
SettingsSwitchListTile(
appSettingService: settings,
valueNotifier: isPreview,
settingsEnum: AppSettingsEnum.loadPreview,
title: "setting_image_viewer_preview_title".tr(),
subtitle: "setting_image_viewer_preview_subtitle".tr(),
),
buildSwitchListTile(
context,
settings,
isOriginal,
AppSettingsEnum.loadOriginal,
SettingsSwitchListTile(
appSettingService: settings,
valueNotifier: isOriginal,
settingsEnum: AppSettingsEnum.loadOriginal,
title: "setting_image_viewer_original_title".tr(),
subtitle: "setting_image_viewer_original_subtitle".tr(),
),

View File

@@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/providers/permission.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/common.dart';
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
import 'package:permission_handler/permission_handler.dart';
class NotificationSetting extends HookConsumerWidget {
const NotificationSetting({
@@ -14,12 +16,14 @@ class NotificationSetting extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final permissionService = ref.watch(notificationPermissionProvider);
final sliderValue = useState(0.0);
final totalProgressValue =
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
final singleProgressValue =
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
final hasPermission = permissionService == PermissionStatus.granted;
useEffect(
() {
@@ -35,6 +39,30 @@ class NotificationSetting extends HookConsumerWidget {
[],
);
// When permissions are permanently denied, you need to go to settings to
// allow them
showPermissionsDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: const Text('notification_permission_dialog_content').tr(),
actions: [
TextButton(
child: const Text('notification_permission_dialog_cancel').tr(),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('notification_permission_dialog_settings').tr(),
onPressed: () {
Navigator.of(context).pop();
openAppSettings();
},
),
],
),
);
}
final String formattedValue = _formatSliderValue(sliderValue.value);
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
@@ -51,23 +79,49 @@ class NotificationSetting extends HookConsumerWidget {
),
).tr(),
children: [
buildSwitchListTile(
context,
appSettingService,
totalProgressValue,
AppSettingsEnum.backgroundBackupTotalProgress,
if (!hasPermission)
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: const Text('notification_permission_list_tile_title').tr(),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('notification_permission_list_tile_content').tr(),
const SizedBox(height: 8),
ElevatedButton(
onPressed: ()
=> ref.watch(notificationPermissionProvider.notifier)
.requestNotificationPermission().then((permission) {
if (permission == PermissionStatus.permanentlyDenied) {
showPermissionsDialog();
}
}),
child:
const Text('notification_permission_list_tile_enable_button')
.tr(),
),
],
),
isThreeLine: true,
),
SettingsSwitchListTile(
enabled: hasPermission,
appSettingService: appSettingService,
valueNotifier: totalProgressValue,
settingsEnum: AppSettingsEnum.backgroundBackupTotalProgress,
title: 'setting_notifications_total_progress_title'.tr(),
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
),
buildSwitchListTile(
context,
appSettingService,
singleProgressValue,
AppSettingsEnum.backgroundBackupSingleProgress,
SettingsSwitchListTile(
enabled: hasPermission,
appSettingService: appSettingService,
valueNotifier: singleProgressValue,
settingsEnum: AppSettingsEnum.backgroundBackupSingleProgress,
title: 'setting_notifications_single_progress_title'.tr(),
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
),
ListTile(
enabled: hasPermission,
isThreeLine: false,
dense: true,
title: const Text(
@@ -76,7 +130,7 @@ class NotificationSetting extends HookConsumerWidget {
).tr(args: [formattedValue]),
subtitle: Slider(
value: sliderValue.value,
onChanged: (double v) => sliderValue.value = v,
onChanged: !hasPermission ? null : (double v) => sliderValue.value = v,
onChangeEnd: (double v) => appSettingService.setSetting(
AppSettingsEnum.uploadErrorNotificationGracePeriod,
v.toInt(),

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class SettingsSwitchListTile extends StatelessWidget {
final AppSettingsService appSettingService;
final ValueNotifier<bool> valueNotifier;
final AppSettingsEnum settingsEnum;
final String title;
final bool enabled;
final String? subtitle;
SettingsSwitchListTile({
required this.appSettingService,
required this.valueNotifier,
required this.settingsEnum,
required this.title,
this.subtitle,
this.enabled = true,
}) : super(key: Key(settingsEnum.name));
@override
Widget build(BuildContext context) {
return SwitchListTile.adaptive(
value: valueNotifier.value,
onChanged: !enabled ? null : (value) {
valueNotifier.value = value;
appSettingService.setSetting(settingsEnum, value);
},
activeColor: Theme
.of(context)
.primaryColor,
dense: true,
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: subtitle != null ? Text(subtitle!) : null,
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -41,7 +39,7 @@ class SettingsPage extends HookConsumerWidget {
const ImageViewerQualitySetting(),
const ThemeSetting(),
const AssetListSettings(),
if (Platform.isAndroid) const NotificationSetting(),
const NotificationSetting(),
//const ExperimentalSettings(),
],
).toList(),