add wip deregistering

This commit is contained in:
Henry Hiles 2026-08-26 16:27:43 -04:00
commit 129f0794f2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
7 changed files with 113 additions and 40 deletions

View file

@ -22,6 +22,7 @@ import "package:nexus/models/event.dart";
import "package:nexus/models/oauth_auth_code_response.dart"; import "package:nexus/models/oauth_auth_code_response.dart";
import "package:nexus/models/open_graph_data.dart"; import "package:nexus/models/open_graph_data.dart";
import "package:nexus/models/paginate.dart"; import "package:nexus/models/paginate.dart";
import "package:nexus/models/requests/deregister_pusher.dart";
import "package:nexus/models/requests/download_media.dart"; import "package:nexus/models/requests/download_media.dart";
import "package:nexus/models/requests/get_event.dart"; import "package:nexus/models/requests/get_event.dart";
import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_related_events.dart";
@ -178,13 +179,15 @@ class ClientController extends AsyncNotifier<int> {
return json; return json;
} }
Future<Event> handlePush(Map<String, dynamic> data) async => .fromJson( Future<Event?> handlePush(Map<String, dynamic> data) async {
await callGomuksMethod( final response = await callGomuksMethod(
data, data,
(handle, data) async => GomuksHandlePush(handle, data), (handle, data) async => GomuksHandlePush(handle, data),
),
); );
return response == null ? null : .fromJson(response);
}
Future<dynamic> _sendCommand( Future<dynamic> _sendCommand(
String command, [ String command, [
Map<String, dynamic> data = const {}, Map<String, dynamic> data = const {},
@ -293,6 +296,12 @@ class ClientController extends AsyncNotifier<int> {
Future<void> registerPusher(RegisterPusherRequest request) => Future<void> registerPusher(RegisterPusherRequest request) =>
_sendCommand("register_homeserver_push", request.toJson()); _sendCommand("register_homeserver_push", request.toJson());
Future<void> deregisterPusher(DeregisterPusherRequest request) =>
_sendCommand("register_homeserver_push", {
...request.toJson(),
"kind": null,
});
Future<MessageContent> uploadMedia(UploadMediaRequest request) async => Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
.fromJson(await _sendCommand("upload_media", request.toJson())); .fromJson(await _sendCommand("upload_media", request.toJson()));

View file

@ -1,11 +1,12 @@
import "dart:convert";
import "dart:io"; import "dart:io";
import "package:material_ui/material_ui.dart";
import "package:nexus/main.dart";
import "package:flutter_local_notifications/flutter_local_notifications.dart"; import "package:flutter_local_notifications/flutter_local_notifications.dart";
// ignore: implementation_imports // ignore: implementation_imports
import "package:flutter_local_notifications_linux/src/model/hint.dart"; import "package:flutter_local_notifications_linux/src/model/hint.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/event.dart"; import "package:nexus/pages/notifications.dart";
class NotificationsController class NotificationsController
extends AsyncNotifier<FlutterLocalNotificationsPlugin> { extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
@ -22,17 +23,21 @@ class NotificationsController
appUserModelId: "nexus.federated.nexus", appUserModelId: "nexus.federated.nexus",
guid: "dde78daf-130f-4e46-a80a-e31deeab45d7", guid: "dde78daf-130f-4e46-a80a-e31deeab45d7",
), ),
android: .new("ic_launcher"), android: .new("ic_launcher_foreground"),
iOS: darwin, iOS: darwin,
macOS: darwin, macOS: darwin,
linux: .new(defaultActionName: "Open"), linux: .new(defaultActionName: "Open"),
), ),
onDidReceiveNotificationResponse: (details) { onDidReceiveNotificationResponse: (details) {
if (details.payload case final payload?) { if (details.payload case final eventId?) {
final event = ref.watch( if (navigatorKey.currentContext case final context?) {
EventController.provider(.fromJson(json.decode(payload))), Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NotificationsPage(eventId: eventId),
),
); );
} }
}
}, },
); );

View file

@ -1,8 +1,9 @@
import "dart:io"; import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:m3e_buttons/m3e_buttons.dart"; import "package:m3e_buttons/m3e_buttons.dart";
import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/account_data.dart";
@ -105,14 +106,16 @@ class SettingsSectionsController
.new( .new(
title: "Push notifications", title: "Push notifications",
description: "Enable push notifications using Web Push", description: "Enable push notifications using Web Push",
builder: (title, description, icon) => Consumer( builder: (title, description, icon) => HookConsumer(
builder: (context, ref, _) { builder: (context, ref, _) {
final loading = useState(false);
final pusherRegistered = ref.watch( final pusherRegistered = ref.watch(
UnifiedPushController.provider, UnifiedPushController.provider,
); );
final unifiedPushAllowed = ref.watch( final unifiedPushAllowed = ref.watch(
UnifiedPushAllowedController.provider, UnifiedPushAllowedController.provider,
); );
return SwitchListTile( return SwitchListTile(
title: Text(title), title: Text(title),
subtitle: Text( subtitle: Text(
@ -127,13 +130,16 @@ class SettingsSectionsController
data: (value) => value, data: (value) => value,
orElse: () => false, orElse: () => false,
), ),
onChanged: onChanged: loading.value
unifiedPushAllowed.maybeWhen( ? null
: unifiedPushAllowed.maybeWhen(
data: (data) => data == null, data: (data) => data == null,
orElse: () => false, orElse: () => false,
) )
? pusherRegistered.maybeWhen( ? pusherRegistered.maybeWhen(
data: (_) => (value) async { data: (_) => (value) async {
try {
loading.value = true;
if (value) { if (value) {
if (await ref if (await ref
.watch( .watch(
@ -142,19 +148,29 @@ class SettingsSectionsController
.notifier, .notifier,
) )
.requestPermissions()) { .requestPermissions()) {
ref await ref
.watch( .watch(
UnifiedPushController UnifiedPushController
.provider .provider
.notifier, .notifier,
) )
.register() .register();
.onError(showError);
} else { } else {
// TODO: Handle not granted // TODO: Handle not granted
} }
} else { } else {
// TODO: Deregister await ref
.watch(
UnifiedPushController
.provider
.notifier,
)
.deregister();
}
} catch (error, stackTrace) {
showError(error, stackTrace);
} finally {
loading.value = false;
} }
}, },
orElse: () => null, orElse: () => null,

View file

@ -11,7 +11,6 @@ import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/client_state.dart";
import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/message.dart";
import "package:nexus/models/content/sticker.dart"; import "package:nexus/models/content/sticker.dart";
import "package:nexus/models/requests/get_event.dart";
import "package:nexus/models/requests/register_pusher.dart"; import "package:nexus/models/requests/register_pusher.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";
@ -52,9 +51,10 @@ class UnifiedPushController extends AsyncNotifier<bool> {
json.decode(String.fromCharCodes(message.content)), json.decode(String.fromCharCodes(message.content)),
); );
if (event.unreadType?.shouldNotify != true || if (event == null ||
event.unreadType?.shouldNotify != true ||
(!isInBackground && (!isInBackground &&
await windowManager.isFocused() && await windowManager.isFocused().onError((_, _) => false) &&
ref.watch(KeyController.provider(KeyController.roomKey)) == ref.watch(KeyController.provider(KeyController.roomKey)) ==
event.roomId)) { event.roomId)) {
return; return;
@ -77,9 +77,7 @@ class UnifiedPushController extends AsyncNotifier<bool> {
id: event.eventId.hashCode & 0x7fffffff, id: event.eventId.hashCode & 0x7fffffff,
title: room?.metadata?.name ?? "New Event", title: room?.metadata?.name ?? "New Event",
icon: icon, icon: icon,
payload: json.encode( payload: event.eventId,
GetEventRequest(eventId: event.eventId, roomId: event.roomId),
),
body: switch (event.content) { body: switch (event.content) {
MessageContent(:final body?) || MessageContent(:final body?) ||
StickerContent(:final body) => body, StickerContent(:final body) => body,
@ -128,6 +126,24 @@ class UnifiedPushController extends AsyncNotifier<bool> {
if (!alreadyRegistered) ref.invalidateSelf(); if (!alreadyRegistered) ref.invalidateSelf();
} }
Future<void> deregister() async {
final clientState = ref.watch(ClientStateController.provider);
final key = await UnifiedPushStorageSharedPreferences().keys.get(
clientState!.deviceId!,
);
if (key == null) return;
await ref
.watch(ClientController.provider.notifier)
.deregisterPusher(
.new(appId: "nexus.federated.nexus", pushKey: key.split("=").first),
);
await UnifiedPush.unregister(clientState.deviceId!);
ref.invalidateSelf();
}
static final provider = AsyncNotifierProvider<UnifiedPushController, bool>( static final provider = AsyncNotifierProvider<UnifiedPushController, bool>(
UnifiedPushController.new, UnifiedPushController.new,
); );

View file

@ -0,0 +1,13 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "deregister_pusher.freezed.dart";
part "deregister_pusher.g.dart";
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const DeregisterPusherRequest({
required final String appId,
@JsonKey(name: "pushkey") required final String pushKey,
}) with _$DeregisterPusherRequest {
Map<String, Object?> toJson() => _$DeregisterPusherRequestToJson(this);
}

View file

@ -0,0 +1,14 @@
import "package:material_ui/material_ui.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/widgets/appbar.dart";
class const NotificationsPage({final String? eventId, super.key})
extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: Appbar(title: Text("Notifications")),
body: Placeholder(),
);
}
}

View file

@ -39,7 +39,7 @@ final class const Appbar({
: (_) => windowManager.startDragging(), : (_) => windowManager.startDragging(),
), ),
child: AppBar( child: AppBar(
leading: InkWell(onTap: onTap, child: leading), leading: leading == null ? null : InkWell(onTap: onTap, child: leading),
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
scrolledUnderElevation: scrolledUnderElevation, scrolledUnderElevation: scrolledUnderElevation,
actionsPadding: const .symmetric(horizontal: 8), actionsPadding: const .symmetric(horizontal: 8),