Allow swapping UnifiedPush distributor

Fixes #120
This commit is contained in:
Henry Hiles 2026-10-14 16:41:01 -04:00
commit 3a33bf7bfe
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 86 additions and 29 deletions

View file

@ -15,6 +15,8 @@ import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/unified_push.dart";
import "package:nexus/controllers/spec_versions.dart";
import "package:nexus/controllers/unified_push_allowed.dart";
import "package:nexus/controllers/unified_push_distributors.dart";
import "package:nexus/helpers/extensions/distributor_name.dart";
import "package:nexus/models/account_data.dart";
import "package:nexus/models/settings_category.dart";
import "package:nexus/main.dart";
@ -138,7 +140,7 @@ class SettingsSectionsController
settings: .new([
.new(
title: "Push notifications",
description: "Enable push notifications using Web Push",
description: "Enable push notifications using Web Push, and choose which UnifiedPush distributor to use.",
builder: (title, description, icon) => HookConsumer(
builder: (context, ref, _) {
final loading = useState(false);
@ -148,9 +150,14 @@ class SettingsSectionsController
final unifiedPushAllowed = ref.watch(
UnifiedPushAllowedController.provider,
);
final distributors = ref.watch(
UnifiedPushDistributorsController.provider,
);
final current = pusherRegistered.value;
return SwitchListTile(
title: Text(title),
return DialogListTile<String?>(
key: ValueKey(current),
title: title,
subtitle: Text(
unifiedPushAllowed.maybeWhen(
data: (data) => data,
@ -158,11 +165,18 @@ class SettingsSectionsController
) ??
description,
),
secondary: Icon(icon),
value: pusherRegistered.maybeWhen(
data: (value) => value,
orElse: () => false,
),
icon: Icon(icon),
required: false,
initialValue: current,
options: [
null,
...?distributors.value,
if (current != null &&
distributors.value?.contains(current) != true)
current,
],
getName: (option) =>
option == null ? "Off" : option.distributorName,
onChanged: loading.value
? null
: unifiedPushAllowed.maybeWhen(
@ -171,9 +185,10 @@ class SettingsSectionsController
)
? pusherRegistered.maybeWhen(
data: (_) => (value) async {
if (value == current) return;
try {
loading.value = true;
if (value) {
if (value != null) {
if (await ref
.watch(
NotificationController
@ -187,7 +202,7 @@ class SettingsSectionsController
.provider
.notifier,
)
.register();
.register(value);
} else {
// TODO: Handle not granted
}

View file

@ -17,12 +17,12 @@ import "package:unifiedpush/unifiedpush.dart";
import "package:unifiedpush_storage_shared_preferences/storage.dart";
import "package:window_manager/window_manager.dart";
class UnifiedPushController extends AsyncNotifier<bool> {
class UnifiedPushController extends AsyncNotifier<String?> {
Completer<void>? _endpoint;
@override
Future<bool> build() async {
if (!Platform.isLinux && !Platform.isAndroid) return false;
Future<String?> build() async {
if (!Platform.isLinux && !Platform.isAndroid) return null;
final registered = await UnifiedPush.initialize(
linuxOptions: .new(
@ -56,7 +56,7 @@ class UnifiedPushController extends AsyncNotifier<bool> {
),
);
state = .data(true);
state = .data(await UnifiedPush.getDistributor());
});
if (_endpoint case final completer? when !completer.isCompleted) {
@ -125,17 +125,25 @@ class UnifiedPushController extends AsyncNotifier<bool> {
onUnregistered: deregister,
);
if (!registered) return false;
if (!registered) return null;
try {
return await register();
} catch (error, stackTrace) {
showError(error, stackTrace);
return false;
return null;
}
}
Future<bool> register() async {
/// Registers with [distributor], or the current/default distributor if none
/// is given. Returns the distributor that was registered with.
Future<String?> register([String? distributor]) async {
if (distributor != null &&
state.value != null &&
state.value != distributor) {
await deregister();
}
state = .loading();
try {
final deviceIdCompleter = Completer<String>();
@ -163,7 +171,9 @@ class UnifiedPushController extends AsyncNotifier<bool> {
);
}
if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
if (distributor != null) {
await UnifiedPush.saveDistributor(distributor);
} else if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
throw Exception("No UnifiedPush distributors found.");
}
@ -185,10 +195,11 @@ class UnifiedPushController extends AsyncNotifier<bool> {
onTimeout: () => throw Exception("UnifiedPush registration timed out."),
);
state = .data(true);
return true;
final current = await UnifiedPush.getDistributor();
state = .data(current);
return current;
} catch (_) {
state = .data(false);
state = .data(null);
rethrow;
}
}
@ -213,10 +224,10 @@ class UnifiedPushController extends AsyncNotifier<bool> {
}
await UnifiedPush.unregister(instance ?? clientState!.deviceId!);
state = .data(false);
state = .data(null);
}
static final provider = AsyncNotifierProvider<UnifiedPushController, bool>(
static final provider = AsyncNotifierProvider<UnifiedPushController, String?>(
UnifiedPushController.new,
);
}

View file

@ -2,16 +2,16 @@ import "dart:io";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:unifiedpush/unifiedpush.dart";
import "package:nexus/controllers/unified_push_distributors.dart";
class UnifiedPushAllowedController extends AsyncNotifier<String?> {
@override
Future<String?> build() async {
if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor().timeout(
.new(seconds: 10),
onTimeout: () =>
throw Exception("UnifiedPush distributor check timed out."),
)) {
final distributors = await ref.watch(
UnifiedPushDistributorsController.provider.future,
);
if (distributors.isEmpty) {
return "No valid distributors found. ${Platform.isLinux
? "Try installing KUnifiedPush"
: Platform.isAndroid

View file

@ -0,0 +1,26 @@
import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:unifiedpush/unifiedpush.dart";
class UnifiedPushDistributorsController extends AsyncNotifier<IList<String>> {
@override
Future<IList<String>> build() async {
if (!Platform.isLinux && !Platform.isAndroid) return .new();
return IList(
await UnifiedPush.getDistributors().timeout(
.new(seconds: 10),
onTimeout: () =>
throw Exception("UnifiedPush distributor check timed out."),
),
);
}
static final provider =
AsyncNotifierProvider.autoDispose<
UnifiedPushDistributorsController,
IList<String>
>(UnifiedPushDistributorsController.new);
}