handle permission denials more gracefully
This commit is contained in:
parent
e24e99fd8e
commit
deac9ef24e
5 changed files with 123 additions and 71 deletions
|
|
@ -143,6 +143,19 @@ class GomuksListenerController extends AsyncNotifier<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Waits for gomuks to report the client's device ID
|
||||||
|
Future<String> deviceId() {
|
||||||
|
final completer = Completer<String>();
|
||||||
|
final subscription = ref.listen(
|
||||||
|
ClientStateController.provider.select((value) => value?.deviceId),
|
||||||
|
(_, next) {
|
||||||
|
if (next != null && !completer.isCompleted) completer.complete(next);
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
return completer.future.whenComplete(subscription.close);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<int> _start(
|
static Future<int> _start(
|
||||||
int handle,
|
int handle,
|
||||||
Pointer<
|
Pointer<
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,42 @@ class NotificationController
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether notifications can currently be shown, without prompting
|
||||||
|
Future<bool> permissionsGranted() async {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final controller = await future;
|
||||||
|
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
return (await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.checkPermissions())
|
||||||
|
?.isEnabled ??
|
||||||
|
true;
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return (await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
MacOSFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.checkPermissions())
|
||||||
|
?.isEnabled ??
|
||||||
|
true;
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
return await controller
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>()
|
||||||
|
?.areNotificationsEnabled() ??
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> send({
|
Future<void> send({
|
||||||
required int id,
|
required int id,
|
||||||
required String title,
|
required String title,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class SettingsSectionsController
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
initialValue: settings.theme,
|
value: settings.theme,
|
||||||
options: ThemeMode.values,
|
options: ThemeMode.values,
|
||||||
getName: (option) => toBeginningOfSentenceCase(option.name),
|
getName: (option) => toBeginningOfSentenceCase(option.name),
|
||||||
onChanged: (value) => ref
|
onChanged: (value) => ref
|
||||||
|
|
@ -105,7 +105,7 @@ class SettingsSectionsController
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
initialValue: settings.roomOrder,
|
value: settings.roomOrder,
|
||||||
options: RoomOrder.values,
|
options: RoomOrder.values,
|
||||||
getName: (option) => switch (option) {
|
getName: (option) => switch (option) {
|
||||||
.unreadFirst => "Mentioned, then unread",
|
.unreadFirst => "Mentioned, then unread",
|
||||||
|
|
@ -194,7 +194,6 @@ class SettingsSectionsController
|
||||||
final current = pusherRegistered.value;
|
final current = pusherRegistered.value;
|
||||||
|
|
||||||
return DialogListTile<String?>(
|
return DialogListTile<String?>(
|
||||||
key: ValueKey(current),
|
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
unifiedPushAllowed.maybeWhen(
|
unifiedPushAllowed.maybeWhen(
|
||||||
|
|
@ -204,8 +203,7 @@ class SettingsSectionsController
|
||||||
description,
|
description,
|
||||||
),
|
),
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
required: false,
|
value: current,
|
||||||
initialValue: current,
|
|
||||||
options: [
|
options: [
|
||||||
null,
|
null,
|
||||||
...?distributors.value,
|
...?distributors.value,
|
||||||
|
|
@ -227,21 +225,32 @@ class SettingsSectionsController
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
if (await ref
|
final messenger = ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (!await ref
|
||||||
.read(
|
.read(
|
||||||
NotificationController
|
NotificationController
|
||||||
.provider
|
.provider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.requestPermissions()) {
|
.requestPermissions()) {
|
||||||
await ref
|
messenger.showSnackBar(
|
||||||
.read(
|
.new(
|
||||||
UnifiedPushController
|
content: Text(
|
||||||
.provider
|
"Notifications are disabled. Enable them for Nexus in your system settings.",
|
||||||
.notifier,
|
),
|
||||||
)
|
),
|
||||||
.register(value);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
UnifiedPushController
|
||||||
|
.provider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.register(value);
|
||||||
} else {
|
} else {
|
||||||
await ref
|
await ref
|
||||||
.read(
|
.read(
|
||||||
|
|
@ -283,7 +292,7 @@ class SettingsSectionsController
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
initialValue: ref
|
value: ref
|
||||||
.watch(AccountDataController.provider)
|
.watch(AccountDataController.provider)
|
||||||
.invitePermissionConfig
|
.invitePermissionConfig
|
||||||
.defaultAction,
|
.defaultAction,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/push_key.dart";
|
import "package:nexus/controllers/push_key.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/gomuks_listener.dart";
|
||||||
import "package:unifiedpush/unifiedpush.dart";
|
import "package:unifiedpush/unifiedpush.dart";
|
||||||
import "package:unifiedpush_storage_shared_preferences/storage.dart";
|
import "package:unifiedpush_storage_shared_preferences/storage.dart";
|
||||||
import "package:window_manager/window_manager.dart";
|
import "package:window_manager/window_manager.dart";
|
||||||
|
|
@ -72,6 +72,11 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
},
|
},
|
||||||
onMessage: (message, instance) async {
|
onMessage: (message, instance) async {
|
||||||
debugPrint("UP message received for $instance");
|
debugPrint("UP message received for $instance");
|
||||||
|
if (await deregisterIfDisabled(instance)) {
|
||||||
|
if (isInBackground) exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.decrypted == false) {
|
if (message.decrypted == false) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
"Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.",
|
"Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.",
|
||||||
|
|
@ -135,9 +140,11 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
onUnregistered: deregister,
|
onUnregistered: deregister,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!registered) return null;
|
if (!registered || isInBackground) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (await deregisterIfDisabled()) return null;
|
||||||
|
|
||||||
return await register();
|
return await register();
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
showError(error, stackTrace);
|
showError(error, stackTrace);
|
||||||
|
|
@ -156,19 +163,9 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
|
|
||||||
state = .loading();
|
state = .loading();
|
||||||
try {
|
try {
|
||||||
final deviceIdCompleter = Completer<String>();
|
final deviceId = await ref
|
||||||
final subscription = ref.listen(
|
.read(GomuksListenerController.provider.notifier)
|
||||||
ClientStateController.provider.select((value) => value?.deviceId),
|
.deviceId();
|
||||||
(_, next) {
|
|
||||||
if (next != null && !deviceIdCompleter.isCompleted) {
|
|
||||||
deviceIdCompleter.complete(next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fireImmediately: true,
|
|
||||||
);
|
|
||||||
final deviceId = await deviceIdCompleter.future.whenComplete(
|
|
||||||
subscription.close,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Future(() async {
|
await Future(() async {
|
||||||
final capabilities = await ref
|
final capabilities = await ref
|
||||||
|
|
@ -214,12 +211,27 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deregister([String? instance]) async {
|
Future<bool> deregisterIfDisabled([String? notificationInstance]) async {
|
||||||
final clientState = ref.read(ClientStateController.provider);
|
if (await ref
|
||||||
|
.read(NotificationController.provider.notifier)
|
||||||
|
.permissionsGranted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final keyProvider = PushKeyController.provider(
|
final instance =
|
||||||
instance ?? clientState!.deviceId!,
|
notificationInstance ??
|
||||||
);
|
await ref.read(GomuksListenerController.provider.notifier).deviceId();
|
||||||
|
final key = await ref.read(PushKeyController.provider(instance).future);
|
||||||
|
if (key != null) await deregister(instance);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deregister([String? instance]) async {
|
||||||
|
instance ??= await ref
|
||||||
|
.read(GomuksListenerController.provider.notifier)
|
||||||
|
.deviceId();
|
||||||
|
|
||||||
|
final keyProvider = PushKeyController.provider(instance);
|
||||||
final key = await ref.read(keyProvider.future);
|
final key = await ref.read(keyProvider.future);
|
||||||
|
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
|
|
@ -233,7 +245,7 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UnifiedPush.unregister(instance ?? clientState!.deviceId!);
|
await UnifiedPush.unregister(instance);
|
||||||
state = .data(null);
|
state = .data(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,53 +4,35 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/widgets/settings/radio_sheet.dart";
|
import "package:nexus/widgets/settings/radio_sheet.dart";
|
||||||
|
|
||||||
final class const DialogListTile<T>({
|
final class const DialogListTile<T>({
|
||||||
required final T? initialValue,
|
required final T? value,
|
||||||
required final Icon icon,
|
required final Icon icon,
|
||||||
required final String title,
|
required final String title,
|
||||||
required final List<T> options,
|
required final List<T> options,
|
||||||
required final void Function(T value)? onChanged,
|
required final void Function(T value)? onChanged,
|
||||||
required final String Function(T option) getName,
|
required final String Function(T option) getName,
|
||||||
final Widget? subtitle,
|
final Widget? subtitle,
|
||||||
final bool required = true,
|
|
||||||
super.key,
|
super.key,
|
||||||
}) extends ConsumerWidget {
|
}) extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => FormField(
|
Widget build(BuildContext context, WidgetRef ref) => ListTile(
|
||||||
validator: (value) =>
|
enabled: onChanged != null,
|
||||||
value == null && required == true ? "This field is required." : null,
|
onTap: () => context.showAdaptiveSheet(
|
||||||
initialValue: initialValue,
|
(context) => RadioSheet<T>(
|
||||||
builder: (field) => InputDecorator(
|
title: title,
|
||||||
decoration: InputDecoration(
|
getName: getName,
|
||||||
errorText: field.errorText,
|
onChanged: onChanged,
|
||||||
contentPadding: EdgeInsets.zero,
|
value: value,
|
||||||
enabledBorder: InputBorder.none,
|
options: options,
|
||||||
),
|
),
|
||||||
child: ListTile(
|
),
|
||||||
enabled: onChanged != null,
|
title: Text(title),
|
||||||
onTap: () => context.showAdaptiveSheet(
|
subtitle: subtitle,
|
||||||
(context) => RadioSheet<T>(
|
leading: icon,
|
||||||
title: title,
|
trailing: Chip(
|
||||||
getName: getName,
|
label: Text(
|
||||||
onChanged: onChanged == null
|
value == null ? "None" : getName(value as T),
|
||||||
? null
|
overflow: TextOverflow.ellipsis,
|
||||||
: (value) {
|
style: onChanged == null ? .new(color: Colors.grey) : null,
|
||||||
field.didChange(value);
|
|
||||||
onChanged!.call(value);
|
|
||||||
},
|
|
||||||
value: field.value,
|
|
||||||
options: options,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(title),
|
|
||||||
subtitle: subtitle,
|
|
||||||
leading: icon,
|
|
||||||
trailing: Chip(
|
|
||||||
label: Text(
|
|
||||||
field.value == null ? "None" : getName(field.value as T),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: onChanged == null ? .new(color: Colors.grey) : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue