From 34893f16e3e329b6375247044252b48d6790f8de Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su <30667958+hjiangsu@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:27:51 -0700 Subject: [PATCH] cleaned up logic, added additional comments --- lib/l10n/app_en.arb | 14 +- lib/main.dart | 13 +- .../shared/android_notification.dart | 13 +- .../shared/notification_server.dart | 35 +- lib/notification/utils/apns.dart | 19 +- .../utils/local_notifications.dart | 1 - lib/notification/utils/unified_push.dart | 4 +- lib/settings/pages/general_settings_page.dart | 402 +++++++++--------- 8 files changed, 254 insertions(+), 247 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 19bea23fb..4151051d2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -629,7 +629,7 @@ }, "failedToBlock": "Failed to block: {errorMessage}", "@failedToBlock": {}, - "failedToDisablePushNotifications": "Failed to disable existing push notifications. Will try again when app restarts.", + "failedToDisablePushNotifications": "Failed to disable push notifications", "@failedToDisablePushNotifications": { "description": "Error message when failed to disable push notifications." }, @@ -1281,14 +1281,18 @@ "@pushNotification": { "description": "Setting for push notifications" }, - "pushNotificationDescription": "If enabled, Thunder will send your JWT token(s) to the server in order to poll for new notifications. \\n\\n **NOTE:** This will not take effect until the next time the app is launched.", + "pushNotificationDescription": "If enabled, Thunder will send your JWT token(s) to the server in order to poll for new notifications. \n\n **NOTE:** This will not take effect until the next time the app is launched.", "@pushNotificationDescription": { "description": "Description of push notification setting" }, "pushNotificationServer": "Push Notification Server", - "@pushNotificationServer": {}, - "pushNotificationServerDescription": "Configure the notification server to use for sending push notifications.", - "@pushNotificationServerDescription": {}, + "@pushNotificationServer": { + "description": "Setting for choosing push notification server" + }, + "pushNotificationServerDescription": "Configure the push notification server. The server must be properly configured to send push notifications to your device.\n\n **Only enter a server that you trust with your credentials.**", + "@pushNotificationServerDescription": { + "description": "Description of choosing push notification server setting" + }, "reachedTheBottom": "Hmmm. It seems like you've reached the bottom.", "@reachedTheBottom": {}, "readAll": "Read All", diff --git a/lib/main.dart b/lib/main.dart index c4cda6805..822e618f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -117,13 +117,18 @@ class _ThunderAppState extends State { SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; String? inboxNotificationType = prefs.getString(LocalSettings.inboxNotificationType.name); - if (NotificationType.values.byName(inboxNotificationType ?? NotificationType.none.name) != NotificationType.none) { + // If notification type is null, then don't perform any logic + if (inboxNotificationType == null) return; + + if (NotificationType.values.byName(inboxNotificationType) != NotificationType.none) { // Initialize notification logic initPushNotificationLogic(controller: notificationsStreamController); - } else if (inboxNotificationType != null && inboxNotificationType == NotificationType.none.name) { - // Attempt to remove tokens from notification server. When inboxNotificationType == NotificationType.none.name, that means at some point in time - // removing the tokens was unsuccessful. When there is a successful removal, the inboxNotificationType will be set to null. + } else { + // Attempt to remove tokens from notification server. When inboxNotificationType == NotificationType.none, + // this indicates that removing token was unsuccessful previously. We will attempt to remove it again. + // When there is a successful removal, the inboxNotificationType will be set to null. bool success = await deleteAccountFromNotificationServer(); + if (success) { prefs.remove(LocalSettings.inboxNotificationType.name); debugPrint('Removed tokens from notification server'); diff --git a/lib/notification/shared/android_notification.dart b/lib/notification/shared/android_notification.dart index f73969843..7cec93e72 100644 --- a/lib/notification/shared/android_notification.dart +++ b/lib/notification/shared/android_notification.dart @@ -8,9 +8,10 @@ const String _inboxMessagesChannelName = 'Inbox Messages'; const String repliesGroupKey = 'replies'; /// Displays a new notification group on Android based on the accounts passed in. -void showNotificationGroups({ - List accounts = const [], -}) async { +/// +/// This displays an empty notification which will be used in conjunction with the [showAndroidNotification] +/// to help display a group of notifications on Android. +void showNotificationGroups({List accounts = const []}) async { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); for (Account account in accounts) { @@ -42,9 +43,8 @@ void showNotificationGroups({ } } -/// Displays a single push notification on Android -/// -/// The notification will be grouped based on the account id. +/// Displays a single push notification on Android. When a notification is displayed, it will be grouped by the account id. +/// This allows us to group notifications for a single account on Android. void showAndroidNotification({ required int id, required BigTextStyleInformation bigTextStyleInformation, @@ -52,7 +52,6 @@ void showAndroidNotification({ String title = '', String content = '', String payload = '', - String summaryText = '', }) async { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); diff --git a/lib/notification/shared/notification_server.dart b/lib/notification/shared/notification_server.dart index dcc04cb47..0c69d847f 100644 --- a/lib/notification/shared/notification_server.dart +++ b/lib/notification/shared/notification_server.dart @@ -1,41 +1,41 @@ -// Dart imports: +// Dart imports import 'dart:convert'; -// Flutter imports: +// Flutter imports import 'package:flutter/foundation.dart'; -// Package imports: +// Package imports import 'package:http/http.dart' as http; -// Project imports: +// Project imports import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/enums/local_settings.dart'; import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/notification/enums/notification_type.dart'; import 'package:thunder/utils/constants.dart'; +/// Sends a request to the push notification server, including the [NotificationType], [jwt], and [instance]. +/// +/// The [token] describes the endpoint to send the notification to. This is generally the UnifiedPush endpoint, or device token for APNs. +/// The [instance] and [jwt] are required in order for the push server to act on behalf of the user to poll for notifications. Future sendAuthTokenToNotificationServer({ required NotificationType type, required String token, - required List jwts, + required String jwt, required String instance, }) async { try { final prefs = (await UserPreferences.instance).sharedPreferences; - String pushNotificationServer = prefs.getString(LocalSettings.pushNotificationServer.name) ?? THUNDER_SERVER_URL; - pushNotificationServer = 'http://192.168.50.195:5100/notifications'; // Send POST request to notification server http.Response response = await http.post( Uri.parse(pushNotificationServer), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, + headers: {'Content-Type': 'application/json; charset=UTF-8'}, body: jsonEncode({ 'type': type.name, 'token': token, - 'jwts': jwts, + 'jwt': jwt, 'instance': instance, }), ); @@ -49,12 +49,13 @@ Future sendAuthTokenToNotificationServer({ } } +/// Sends a request to the push notification server to remove any account tokens from the server. This will remove push notifications for all accounts active on the app. +/// +/// This is generally called when the user changes push notification types, or disables all push notifications. Future deleteAccountFromNotificationServer() async { try { final prefs = (await UserPreferences.instance).sharedPreferences; - String pushNotificationServer = prefs.getString(LocalSettings.pushNotificationServer.name) ?? THUNDER_SERVER_URL; - pushNotificationServer = 'http://192.168.50.195:5100/notifications'; List accounts = await Account.accounts(); List jwts = accounts.map((Account account) => account.jwt!).toList(); @@ -62,12 +63,8 @@ Future deleteAccountFromNotificationServer() async { // Send POST request to notification server http.Response response = await http.delete( Uri.parse(pushNotificationServer), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: jsonEncode({ - 'jwts': jwts, - }), + headers: {'Content-Type': 'application/json; charset=UTF-8'}, + body: jsonEncode({'jwts': jwts}), ); // Check if the request was successful diff --git a/lib/notification/utils/apns.dart b/lib/notification/utils/apns.dart index a8b19f716..0867176e2 100644 --- a/lib/notification/utils/apns.dart +++ b/lib/notification/utils/apns.dart @@ -20,13 +20,12 @@ import 'package:thunder/notification/shared/notification_server.dart'; void initAPNs({required StreamController controller}) async { const String repliesGroupKey = 'replies'; - // Fetch device token for APNs - // We need to send this device token along with the jwt so that the server can poll for new notifications and send them to this device. + // Fetch device token for APNs. We need to send this device token along with the jwt so that the server can poll for new notifications and send them to this device. String? token = await Push.instance.token; debugPrint("Device token: $token"); if (token == null) { - debugPrint("No device token, skipping APNs initialization"); + debugPrint("No device token found, skipping APNs initialization"); return; } @@ -35,7 +34,7 @@ void initAPNs({required StreamController controller}) asyn // TODO: Select accounts to enable push notifications for (Account account in accounts) { - bool success = await sendAuthTokenToNotificationServer(type: NotificationType.apn, token: token, jwts: [account.jwt!], instance: account.instance!); + bool success = await sendAuthTokenToNotificationServer(type: NotificationType.apn, token: token, jwt: account.jwt!, instance: account.instance!); if (!success) debugPrint("Failed to send device token to server for account ${account.id}. Skipping."); } @@ -49,7 +48,7 @@ void initAPNs({required StreamController controller}) asyn // TODO: Select accounts to enable push notifications for (Account account in accounts) { - bool success = await sendAuthTokenToNotificationServer(type: NotificationType.apn, token: token, jwts: [account.jwt!], instance: account.instance!); + bool success = await sendAuthTokenToNotificationServer(type: NotificationType.apn, token: token, jwt: account.jwt!, instance: account.instance!); if (!success) debugPrint("Failed to send device token to server for account ${account.id}. Skipping."); } }); @@ -59,14 +58,20 @@ void initAPNs({required StreamController controller}) asyn if (data == null) return; if (data.containsKey(repliesGroupKey)) { - controller.add(NotificationResponse(payload: data[repliesGroupKey] as String, notificationResponseType: NotificationResponseType.selectedNotification)); + controller.add(NotificationResponse( + payload: data[repliesGroupKey] as String, + notificationResponseType: NotificationResponseType.selectedNotification, + )); } }); /// Handle notification taps. This triggers when the user taps on a notification when the app is on the foreground or background. Push.instance.onNotificationTap.listen((data) { if (data.containsKey(repliesGroupKey)) { - controller.add(NotificationResponse(payload: data[repliesGroupKey] as String, notificationResponseType: NotificationResponseType.selectedNotification)); + controller.add(NotificationResponse( + payload: data[repliesGroupKey] as String, + notificationResponseType: NotificationResponseType.selectedNotification, + )); } }); } diff --git a/lib/notification/utils/local_notifications.dart b/lib/notification/utils/local_notifications.dart index 62da23965..8346e80be 100644 --- a/lib/notification/utils/local_notifications.dart +++ b/lib/notification/utils/local_notifications.dart @@ -105,7 +105,6 @@ Future pollRepliesAndShowNotifications() async { title: generateUserFullName(null, commentReplyView.creator.name, fetchInstanceNameFromUrl(commentReplyView.creator.actorId), userSeparator: userSeparator), content: plaintextComment, payload: '$repliesGroupKey-${commentReplyView.comment.id}', - summaryText: generateUserFullName(null, commentReplyView.recipient.name, fetchInstanceNameFromUrl(commentReplyView.recipient.actorId), userSeparator: userSeparator), ); } } diff --git a/lib/notification/utils/unified_push.dart b/lib/notification/utils/unified_push.dart index 108139053..860973099 100644 --- a/lib/notification/utils/unified_push.dart +++ b/lib/notification/utils/unified_push.dart @@ -44,7 +44,7 @@ void initUnifiedPushNotifications({required StreamController with SingleTi GlobalKey settingToHighlightKey = GlobalKey(); LocalSettings? settingToHighlight; - /// List of authenticated accounts + /// List of authenticated accounts. Used to determine if push notifications are enabled List accounts = []; - /// Controller for the navigation server URL + /// Controller for the push notification server URL TextEditingController controller = TextEditingController(); Future setPreferences(attribute, value) async { @@ -235,8 +236,8 @@ class _GeneralSettingsPageState extends State with SingleTi void _initPreferences() async { final prefs = (await UserPreferences.instance).sharedPreferences; - // Get all accounts - List allAccounts = await Account.accounts(); + // Get all currently active accounts + List accountList = await Account.accounts(); setState(() { // Default Sorts and Listing @@ -276,7 +277,7 @@ class _GeneralSettingsPageState extends State with SingleTi pushNotificationServer = prefs.getString(LocalSettings.pushNotificationServer.name) ?? THUNDER_SERVER_URL; controller.text = pushNotificationServer; - accounts = allAccounts; + accounts = accountList; }); } @@ -679,210 +680,209 @@ class _GeneralSettingsPageState extends State with SingleTi highlightKey: settingToHighlight == LocalSettings.showUpdateChangelogs ? settingToHighlightKey : null, ), ), - SliverToBoxAdapter( - child: ListOption( - description: l10n.enableInboxNotifications, - subtitle: accounts.isEmpty ? l10n.disabled : inboxNotificationType.toString(), - value: const ListPickerItem(payload: -1), - disabled: accounts.isEmpty, - icon: inboxNotificationType == NotificationType.none ? Icons.notifications_off_rounded : Icons.notifications_on_rounded, - highlightKey: settingToHighlight == LocalSettings.inboxNotificationType ? settingToHighlightKey : null, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.pushNotification, - heading: Align( - alignment: Alignment.centerLeft, - child: CommonMarkdownBody(body: l10n.pushNotificationDescription), - ), - previouslySelected: inboxNotificationType, - items: Platform.isAndroid - ? [ - ListPickerItem( - icon: Icons.notifications_off_rounded, - label: l10n.none, - payload: NotificationType.none, - ), - ListPickerItem( - icon: Icons.notifications_rounded, - label: l10n.useLocalNotifications, - subtitle: l10n.useLocalNotificationsDescription, - payload: NotificationType.local, - ), - ListPickerItem( - icon: Icons.notifications_active_rounded, - label: l10n.useUnifiedPushNotifications, - subtitle: l10n.useUnifiedPushNotificationsDescription, - payload: NotificationType.unifiedPush, - ), - ] - : [ - ListPickerItem( - icon: Icons.notifications_off_rounded, - label: l10n.disablePushNotifications, - payload: NotificationType.none, - ), - ListPickerItem( - icon: Icons.notifications_active_rounded, - label: l10n.useApplePushNotifications, - subtitle: l10n.useApplePushNotificationsDescription, - payload: NotificationType.apn, - ), - ], - onSelect: (ListPickerItem notificationType) async { - if (notificationType.payload == inboxNotificationType) return; - - // Disable all notifications since the option has changed. - if (Platform.isAndroid) { - disableBackgroundFetch(); - UnifiedPush.unregister(); - } - - bool successfullyRemovedExistingTokens = false; - - // Delete all server tokens related to all accounts if the option was previously unified push or apns - if (inboxNotificationType == NotificationType.unifiedPush || inboxNotificationType == NotificationType.apn) { - successfullyRemovedExistingTokens = await deleteAccountFromNotificationServer(); - } - - if (notificationType.payload == NotificationType.none && successfullyRemovedExistingTokens) { - // If we have successfully removed all tokens from the server, we'll remove the preference altogether - SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; - prefs.remove(LocalSettings.inboxNotificationType.name); - debugPrint('Removed tokens from notification server'); - } else if (notificationType.payload == NotificationType.none && !successfullyRemovedExistingTokens) { - showSnackbar(l10n.failedToDisablePushNotifications); - return setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); - } - - // If using local notifications, show a warning - if (notificationType.payload == NotificationType.local) { - bool res = false; - - await showThunderDialog( - context: context, - title: l10n.warning, - contentWidgetBuilder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - CommonMarkdownBody(body: l10n.notificationsWarningDialog), - const SizedBox(height: 5), - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () => handleLink(context, url: 'https://dontkillmyapp.com/'), - child: Text( - 'https://dontkillmyapp.com/', - style: theme.textTheme.bodyMedium?.copyWith(color: Colors.blue), - ), - ), + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) ...[ + SliverToBoxAdapter( + child: ListOption( + description: l10n.enableInboxNotifications, + subtitle: accounts.isEmpty ? l10n.disabled : inboxNotificationType.toString(), + value: const ListPickerItem(payload: -1), + disabled: accounts.isEmpty, + icon: inboxNotificationType == NotificationType.none ? Icons.notifications_off_rounded : Icons.notifications_on_rounded, + highlightKey: settingToHighlight == LocalSettings.inboxNotificationType ? settingToHighlightKey : null, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.pushNotification, + heading: Align( + alignment: Alignment.centerLeft, + child: CommonMarkdownBody(body: l10n.pushNotificationDescription), + ), + previouslySelected: inboxNotificationType, + items: Platform.isAndroid + ? [ + ListPickerItem( + icon: Icons.notifications_off_rounded, + label: l10n.none, + payload: NotificationType.none, + ), + ListPickerItem( + icon: Icons.notifications_rounded, + label: l10n.useLocalNotifications, + subtitle: l10n.useLocalNotificationsDescription, + payload: NotificationType.local, + ), + ListPickerItem( + icon: Icons.notifications_active_rounded, + label: l10n.useUnifiedPushNotifications, + subtitle: l10n.useUnifiedPushNotificationsDescription, + payload: NotificationType.unifiedPush, + ), + ] + : [ + ListPickerItem( + icon: Icons.notifications_off_rounded, + label: l10n.disablePushNotifications, + payload: NotificationType.none, + ), + ListPickerItem( + icon: Icons.notifications_active_rounded, + label: l10n.useApplePushNotifications, + subtitle: l10n.useApplePushNotificationsDescription, + payload: NotificationType.apn, ), ], + onSelect: (ListPickerItem notificationType) async { + if (notificationType.payload == inboxNotificationType) return; + + // Disable all notifications since the option has changed. + if (Platform.isAndroid) { + disableBackgroundFetch(); + UnifiedPush.unregister(); + } + + bool successfullyRemovedExistingTokens = false; + + // Delete all server tokens related to all accounts if the option was previously unified push or apns + if (inboxNotificationType == NotificationType.unifiedPush || inboxNotificationType == NotificationType.apn) { + successfullyRemovedExistingTokens = await deleteAccountFromNotificationServer(); + } + + if (notificationType.payload == NotificationType.none && successfullyRemovedExistingTokens) { + // If we have successfully removed all tokens from the server, we'll remove the preference altogether + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.remove(LocalSettings.inboxNotificationType.name); + debugPrint('Removed tokens from notification server'); + } else if (notificationType.payload == NotificationType.none && !successfullyRemovedExistingTokens) { + // If we failed to remove all tokens from the server, we'll set the preference to NotificationType.none + // The next time the app is opened, it will attempt to remove tokens from the server + showSnackbar(l10n.failedToDisablePushNotifications); + return setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); + } + + // If using local notifications, show a warning + if (notificationType.payload == NotificationType.local) { + bool res = false; + + await showThunderDialog( + context: context, + title: l10n.warning, + contentWidgetBuilder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + CommonMarkdownBody(body: l10n.notificationsWarningDialog), + const SizedBox(height: 5), + Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () => handleLink(context, url: 'https://dontkillmyapp.com/'), + child: Text( + 'https://dontkillmyapp.com/', + style: theme.textTheme.bodyMedium?.copyWith(color: Colors.blue), + ), + ), + ), + ], + ), + primaryButtonText: l10n.understandEnable, + onPrimaryButtonPressed: (dialogContext, _) { + res = true; + dialogContext.pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => dialogContext.pop(), + ); + + if (!res) return; + } + + // Check notifications permissions and enable them if needed + switch (notificationType.payload) { + case NotificationType.local: + case NotificationType.unifiedPush: + // We're on Android. Request notifications permissions if needed. This is a no-op if on SDK version < 33 + AndroidFlutterLocalNotificationsPlugin? androidFlutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin().resolvePlatformSpecificImplementation(); + + bool? areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.areNotificationsEnabled(); + + if (areAndroidNotificationsAllowed != true) { + areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.requestNotificationsPermission(); + if (areAndroidNotificationsAllowed != true) return showSnackbar(l10n.permissionDenied); + } + + // Permissions have been granted, so we can enable notifications + setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); + break; + case NotificationType.apn: + // We're on iOS. Request notifications permissions if needed. + IOSFlutterLocalNotificationsPlugin? iosFlutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin().resolvePlatformSpecificImplementation(); + + NotificationsEnabledOptions? notificationsEnabledOptions = await iosFlutterLocalNotificationsPlugin?.checkPermissions(); + + if (notificationsEnabledOptions?.isEnabled != true) { + bool? isEnabled = await iosFlutterLocalNotificationsPlugin?.requestPermissions(alert: true, badge: true, sound: true); + if (isEnabled != true) return showSnackbar(l10n.permissionDenied); + } + + setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); + break; + default: + break; + } + }, + ); + }, + ), + ), + ), + SliverToBoxAdapter( + child: SettingsListTile( + icon: Icons.electrical_services_rounded, + description: l10n.pushNotificationServer, + subtitle: pushNotificationServer, + widget: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.pushNotificationServer, + contentWidgetBuilder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CommonMarkdownBody(body: l10n.pushNotificationServerDescription), + const SizedBox(height: 32.0), + TextField( + textInputAction: TextInputAction.done, + keyboardType: TextInputType.url, + autocorrect: false, + controller: controller, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: l10n.url, + hintText: THUNDER_SERVER_URL, + ), + enableSuggestions: false, ), - primaryButtonText: l10n.understandEnable, - onPrimaryButtonPressed: (dialogContext, _) { - res = true; - dialogContext.pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => dialogContext.pop(), - ); - - if (!res) return; - } - - // Check notifications permissions and enable them if needed - switch (notificationType.payload) { - case NotificationType.local: - case NotificationType.unifiedPush: - // We're on Android. Request notifications permissions if needed. This is a no-op if on SDK version < 33 - AndroidFlutterLocalNotificationsPlugin? androidFlutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin().resolvePlatformSpecificImplementation(); - - bool? areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.areNotificationsEnabled(); - - if (areAndroidNotificationsAllowed != true) { - areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.requestNotificationsPermission(); - if (areAndroidNotificationsAllowed != true) return showSnackbar(l10n.permissionDenied); - } - - // Permissions have been granted, so we can enable notifications - setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); - break; - case NotificationType.apn: - // We're on iOS. Request notifications permissions if needed. - IOSFlutterLocalNotificationsPlugin? iosFlutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin().resolvePlatformSpecificImplementation(); - - NotificationsEnabledOptions? notificationsEnabledOptions = await iosFlutterLocalNotificationsPlugin?.checkPermissions(); - - if (notificationsEnabledOptions?.isEnabled != true) { - bool? isEnabled = await iosFlutterLocalNotificationsPlugin?.requestPermissions(alert: true, badge: true, sound: true); - if (isEnabled != true) return showSnackbar(l10n.permissionDenied); - } - - setPreferences(LocalSettings.inboxNotificationType, notificationType.payload); - break; - default: - break; - } + ], + ); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + primaryButtonText: l10n.confirm, + onPrimaryButtonPressed: (dialogContext, _) { + setPreferences(LocalSettings.pushNotificationServer, controller.text); + Navigator.of(dialogContext).pop(); }, ); }, ), ), - ), - SliverToBoxAdapter( - child: SettingsListTile( - icon: Icons.electrical_services_rounded, - description: l10n.pushNotificationServer, - subtitle: pushNotificationServer, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - showThunderDialog( - context: context, - title: l10n.pushNotificationServer, - contentWidgetBuilder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.pushNotificationServerDescription, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color?.withOpacity(0.8), - ), - ), - const SizedBox(height: 16.0), - TextField( - textInputAction: TextInputAction.done, - keyboardType: TextInputType.url, - autocorrect: false, - controller: controller, - decoration: InputDecoration( - isDense: true, - border: const OutlineInputBorder(), - labelText: l10n.url, - hintText: THUNDER_SERVER_URL, - ), - enableSuggestions: false, - ), - ], - ); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - primaryButtonText: l10n.confirm, - onPrimaryButtonPressed: (dialogContext, _) { - setPreferences(LocalSettings.pushNotificationServer, controller.text); - Navigator.of(dialogContext).pop(); - }, - ); - }, - ), - ), + ], const SliverToBoxAdapter(child: SizedBox(height: 16.0)), SliverToBoxAdapter( child: Padding(