Compare commits

...
Author SHA1 Message Date
81ba35d9d7
add wip deregistering 2026-09-09 15:41:57 -04:00
a1a03f94ce
smarter notif logic 2026-09-09 15:41:57 -04:00
ea5c38f892
include avatars on notifs 2026-09-09 15:41:57 -04:00
b3a4eb07b7
send notifications natively 2026-09-09 15:41:57 -04:00
5bfc415468
override webcrypto deps 2026-09-09 15:41:57 -04:00
7b93cf4d12
pass pushes to gomuks 2026-09-09 15:41:57 -04:00
ad93769d14
working registration 2026-09-09 15:41:57 -04:00
842b232a46
slight cleanup
`onNewEndpoint` is still not getting called for some reason (help appreciated)
2026-09-09 15:41:57 -04:00
08eddc612f
add UnifiedPushAllowedController 2026-09-09 15:41:57 -04:00
a3981fdbd1
support for registering pushers 2026-09-09 15:41:57 -04:00
da352cd035
cap max length of embeds 2026-09-09 15:37:41 -04:00
0201f71e5d
allow banning left users 2026-09-09 15:26:04 -04:00
4314c88d5c
fallback to empty profile if fetch fails 2026-09-09 15:25:10 -04:00
32 changed files with 775 additions and 73 deletions

View file

@ -11,6 +11,8 @@
"muks", "muks",
"prefs", "prefs",
"unban", "unban",
"unredact" "unifiedpush",
"unredact",
"webpush"
] ]
} }

View file

@ -190,6 +190,12 @@ dart scripts/generate.dart
> export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" > export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"
> ``` > ```
Build webcrypto:
```sh
flutter pub run webcrypto:setup
```
Build generated files, and watch for new changes: Build generated files, and watch for new changes:
```sh ```sh

View file

@ -20,16 +20,21 @@ android {
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21
} }
defaultConfig { defaultConfig {
applicationId = "nexus.federated.nexus" applicationId = "nexus.federated.nexus"
minSdk = 29 minSdk = 29
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
multiDexEnabled = true
} }
signingConfigs { signingConfigs {
@ -63,6 +68,11 @@ android {
} }
} }
dependencies {
implementation("org.unifiedpush.android:embedded-fcm-distributor:3.1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21 jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21

2
gomuks

@ -1 +1 @@
Subproject commit 9444dd293664c300c6e710bb6cddcd9c6cab365d Subproject commit 0e06c7b74f96d3e29a8101893ca1921d13a90573

View file

@ -7,6 +7,7 @@ import UIKit
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View file

@ -1,7 +1,9 @@
import "dart:async";
import "dart:ffi"; import "dart:ffi";
import "dart:io"; import "dart:io";
import "dart:isolate"; import "dart:isolate";
import "dart:math"; import "dart:math";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
@ -14,11 +16,13 @@ import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart"; import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/models/capabilities.dart";
import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart"; 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";
@ -30,6 +34,7 @@ import "package:nexus/models/requests/oauth/get_auth_url.dart";
import "package:nexus/models/requests/oauth/register_client.dart"; import "package:nexus/models/requests/oauth/register_client.dart";
import "package:nexus/models/requests/paginate.dart"; import "package:nexus/models/requests/paginate.dart";
import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/redact_event.dart";
import "package:nexus/models/requests/register_pusher.dart";
import "package:nexus/models/requests/report.dart"; import "package:nexus/models/requests/report.dart";
import "package:nexus/models/requests/send_event.dart"; import "package:nexus/models/requests/send_event.dart";
import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/requests/send_message.dart";
@ -153,18 +158,15 @@ class ClientController extends AsyncNotifier<int> {
throw Exception("GomuksStart returned error code $errorCode"); throw Exception("GomuksStart returned error code $errorCode");
} }
Future<dynamic> _sendCommand( Future<dynamic> callGomuksMethod(
String command, [ Map<String, dynamic> data,
Map<String, dynamic> data = const {}, FutureOr<GomuksResponse> Function(int handle, GomuksBorrowedBuffer data)
]) async { callback,
) async {
final bufferPointer = data.toGomuksBufferPtr(); final bufferPointer = data.toGomuksBufferPtr();
final handle = await future; final handle = await future;
final response = await Isolate.run( final response = await Isolate.run(
() => GomuksSubmitCommand( () => callback(handle, bufferPointer.ref),
handle,
command.toNativeUtf8().cast<Char>(),
bufferPointer.ref,
),
); );
calloc.free(bufferPointer); calloc.free(bufferPointer);
@ -177,6 +179,24 @@ class ClientController extends AsyncNotifier<int> {
return json; return json;
} }
Future<Event?> handlePush(Map<String, dynamic> data) async {
final response = await callGomuksMethod(
data,
(handle, data) async => GomuksHandlePush(handle, data),
);
return response == null ? null : .fromJson(response);
}
Future<dynamic> _sendCommand(
String command, [
Map<String, dynamic> data = const {},
]) => callGomuksMethod(
data,
(handle, data) async =>
GomuksSubmitCommand(handle, command.toNativeUtf8().cast<Char>(), data),
);
Future<void> redactEvent(RedactEventRequest report) => Future<void> redactEvent(RedactEventRequest report) =>
_sendCommand("redact_event", report.toJson()); _sendCommand("redact_event", report.toJson());
@ -261,8 +281,13 @@ class ClientController extends AsyncNotifier<int> {
Future<Paginate> paginate(PaginateRequest request) async => Future<Paginate> paginate(PaginateRequest request) async =>
.fromJson(await _sendCommand("paginate", request.toJson())); .fromJson(await _sendCommand("paginate", request.toJson()));
Future<ProfileResponse> getProfile(String userId) async => Future<ProfileResponse> getProfile(String userId) async {
.fromJson(await _sendCommand("get_profile", {"user_id": userId})); try {
return .fromJson(await _sendCommand("get_profile", {"user_id": userId}));
} catch (_) {
return ProfileResponse(profile: .new(id: userId));
}
}
Future<void> reportEvent(ReportRequest request) => Future<void> reportEvent(ReportRequest request) =>
_sendCommand("report_event", request.toJson()); _sendCommand("report_event", request.toJson());
@ -273,6 +298,15 @@ class ClientController extends AsyncNotifier<int> {
Future<void> setAccountData(SetAccountDataRequest request) => Future<void> setAccountData(SetAccountDataRequest request) =>
_sendCommand("set_account_data", request.toJson()); _sendCommand("set_account_data", request.toJson());
Future<void> registerPusher(RegisterPusherRequest request) =>
_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()));
@ -310,6 +344,10 @@ class ClientController extends AsyncNotifier<int> {
Future<SpecVersionsResponse> getSpecVersions() async => Future<SpecVersionsResponse> getSpecVersions() async =>
.fromJson(await _sendCommand("get_versions")); .fromJson(await _sendCommand("get_versions"));
Future<Capabilities> getCapabilities() async => Capabilities.fromJson(
(await _sendCommand("get_capabilities"))["capabilities"],
);
Future<Uri?> discoverHomeserver(Uri homeserver) async { Future<Uri?> discoverHomeserver(Uri homeserver) async {
try { try {
final response = await _sendCommand("discover_homeserver", { final response = await _sendCommand("discover_homeserver", {

View file

@ -0,0 +1,120 @@
import "dart:io";
import "package:material_ui/material_ui.dart";
import "package:nexus/main.dart";
import "package:flutter_local_notifications/flutter_local_notifications.dart";
// ignore: implementation_imports
import "package:flutter_local_notifications_linux/src/model/hint.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/pages/notifications.dart";
class NotificationsController
extends AsyncNotifier<FlutterLocalNotificationsPlugin> {
@override
Future<FlutterLocalNotificationsPlugin> build() async {
final notifications = FlutterLocalNotificationsPlugin();
final darwin = DarwinInitializationSettings();
await notifications.initialize(
settings: .new(
windows: .new(
appName: "Nexus",
appUserModelId: "nexus.federated.nexus",
guid: "dde78daf-130f-4e46-a80a-e31deeab45d7",
),
android: .new("ic_launcher_foreground"),
iOS: darwin,
macOS: darwin,
linux: .new(defaultActionName: "Open"),
),
onDidReceiveNotificationResponse: (details) {
if (details.payload case final eventId?) {
if (navigatorKey.currentContext case final context?) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NotificationsPage(eventId: eventId),
),
);
}
}
},
);
return notifications;
}
Future<bool> requestPermissions() async {
final controller = await future;
if (Platform.isIOS) {
return await controller
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true) ??
true;
} else if (Platform.isMacOS) {
return await controller
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true) ??
true;
} else if (Platform.isAndroid) {
return await controller
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission() ??
true;
}
return true;
}
Future<void> send({
required int id,
required String title,
String? body,
File? icon,
String? payload,
}) async {
final notificationDetails = NotificationDetails(
android: .new(
"messages",
"Messages",
largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path),
),
linux: .new(
customHints: icon == null
? null
: [
.new(
name: "image-path",
value: LinuxHintStringValue(icon.path),
),
],
),
// TODO: See if icons can be added to iOS, macOS, and Windows notifications (#68)
iOS: .new(),
macOS: .new(),
windows: .new(),
);
final controller = await future;
await controller.show(
id: id,
title: title,
body: body,
notificationDetails: notificationDetails,
payload: payload,
);
}
static final provider =
AsyncNotifierProvider<
NotificationsController,
FlutterLocalNotificationsPlugin
>(NotificationsController.new);
}

View file

@ -1,15 +1,19 @@
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";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/unified_push.dart";
import "package:nexus/controllers/spec_versions.dart"; import "package:nexus/controllers/spec_versions.dart";
import "package:nexus/controllers/unified_push_allowed.dart";
import "package:nexus/models/account_data.dart"; import "package:nexus/models/account_data.dart";
import "package:nexus/models/settings_category.dart"; import "package:nexus/models/settings_category.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
@ -95,6 +99,90 @@ class SettingsSectionsController
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true) if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
"Account": .new([ "Account": .new([
.new(title: "Profile", icon: Icons.person, settings: .new([])), .new(title: "Profile", icon: Icons.person, settings: .new([])),
.new(
title: "Notifications",
icon: Icons.notifications,
settings: .new([
.new(
title: "Push notifications",
description: "Enable push notifications using Web Push",
builder: (title, description, icon) => HookConsumer(
builder: (context, ref, _) {
final loading = useState(false);
final pusherRegistered = ref.watch(
UnifiedPushController.provider,
);
final unifiedPushAllowed = ref.watch(
UnifiedPushAllowedController.provider,
);
return SwitchListTile(
title: Text(title),
subtitle: Text(
unifiedPushAllowed.maybeWhen(
data: (data) => data,
orElse: () => null,
) ??
description,
),
secondary: Icon(icon),
value: pusherRegistered.maybeWhen(
data: (value) => value,
orElse: () => false,
),
onChanged: loading.value
? null
: unifiedPushAllowed.maybeWhen(
data: (data) => data == null,
orElse: () => false,
)
? pusherRegistered.maybeWhen(
data: (_) => (value) async {
try {
loading.value = true;
if (value) {
if (await ref
.watch(
NotificationsController
.provider
.notifier,
)
.requestPermissions()) {
await ref
.watch(
UnifiedPushController
.provider
.notifier,
)
.register();
} else {
// TODO: Handle not granted
}
} else {
await ref
.watch(
UnifiedPushController
.provider
.notifier,
)
.deregister();
}
} catch (error, stackTrace) {
showError(error, stackTrace);
} finally {
loading.value = false;
}
},
orElse: () => null,
)
: null,
);
},
),
icon: Icons.notification_add,
),
]),
),
.new( .new(
title: "Safety", title: "Safety",
icon: Icons.gpp_good, icon: Icons.gpp_good,
@ -166,6 +254,8 @@ class SettingsSectionsController
onPressed: () async { onPressed: () async {
Navigator.of(context) Navigator.of(context)
.popUntil((route) => route.isFirst); .popUntil((route) => route.isFirst);
Navigator.of(context)
.popUntil((route) => route.isFirst);
await WidgetsBinding.instance.endOfFrame; await WidgetsBinding.instance.endOfFrame;

View file

@ -0,0 +1,150 @@
import "dart:convert";
import "dart:io";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:intl/intl.dart";
import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/main.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/content/sticker.dart";
import "package:nexus/models/requests/register_pusher.dart";
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> {
@override
Future<bool> build() async {
final client = ref.watch(ClientController.provider.notifier);
final registered = await UnifiedPush.initialize(
linuxOptions: .new(
dbusName: "nexus.federated.nexus.UnifiedPush",
storage: UnifiedPushStorageSharedPreferences(),
background: isInBackground,
),
onNewEndpoint: (endpoint, instance) => client.registerPusher(
.new(
appDisplayName: "Nexus",
appId: "nexus.federated.nexus",
data: PusherData.webPush(
url: Uri.parse(endpoint.url),
auth: endpoint.pubKeySet!.auth,
),
deviceDisplayName:
"Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}",
kind: .webPush,
lang: "en",
pushKey: endpoint.pubKeySet!.pubKey,
),
),
onMessage: (message, instance) async {
if (message.decrypted == false) {
throw Exception(
"Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.",
);
}
final event = await client.handlePush(
json.decode(String.fromCharCodes(message.content)),
);
if (event == null ||
event.unreadType?.shouldNotify != true ||
(!isInBackground &&
await windowManager.isFocused().onError((_, _) => false) &&
ref.watch(KeyController.provider(KeyController.roomKey)) ==
event.roomId)) {
return;
}
final room = ref.read(
RoomsController.provider.select((rooms) => rooms[event.roomId]),
);
final avatar = room?.metadata?.avatar;
final icon = avatar == null
? null
: await ref
.read(ClientController.provider.notifier)
.downloadMedia(.new(mxc: avatar, isAvatar: true));
await ref
.read(NotificationsController.provider.notifier)
.send(
id: event.eventId.hashCode & 0x7fffffff,
title: room?.metadata?.name ?? "New Event",
icon: icon,
payload: event.eventId,
body: switch (event.content) {
MessageContent(:final body?) ||
StickerContent(:final body) => body,
_ => null,
},
);
if (isInBackground) exit(0);
},
onRegistrationFailed: (error, instance) => throw error,
onUnregistered: (instance) => ref.invalidateSelf(),
);
if (registered) {
// Needs to be registered every startup
await register(true);
}
return registered;
}
Future<void> register([bool alreadyRegistered = false]) async {
final clientState = ref.watch(ClientStateController.provider);
if (clientState?.deviceId == null) ref.invalidateSelf();
final capabilities = await ref
.watch(ClientController.provider.notifier)
.getCapabilities();
if (capabilities.webpush?.vapid == null) {
throw UnsupportedError(
"Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.",
);
}
if (!alreadyRegistered &&
!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
throw Exception("No UnifiedPush distributors found");
}
await UnifiedPush.register(
instance: clientState!.deviceId!,
vapid: capabilities.webpush?.vapid,
);
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>(
UnifiedPushController.new,
);
}

View file

@ -0,0 +1,33 @@
import "dart:io";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:unifiedpush/unifiedpush.dart";
class UnifiedPushAllowedController extends AsyncNotifier<String?> {
@override
Future<String?> build() async {
if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) {
return "No valid distributors found. Try installing ${Platform.isLinux
? "KUnifiedPush"
: Platform.isAndroid
? "FCM or NTFY"
: "NTFY"}.";
}
final capabilities = await ref
.watch(ClientController.provider.notifier)
.getCapabilities();
if (capabilities.webpush?.vapid == null) {
return "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.";
}
return null;
}
static final provider =
AsyncNotifierProvider.autoDispose<UnifiedPushAllowedController, String?>(
UnifiedPushAllowedController.new,
);
}

View file

@ -8,8 +8,10 @@ import "package:media_kit/media_kit.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/multi_provider.dart"; import "package:nexus/controllers/multi_provider.dart";
import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/shared_prefs.dart"; import "package:nexus/controllers/shared_prefs.dart";
import "package:nexus/controllers/unified_push.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/helpers/font_licenses.dart"; import "package:nexus/helpers/font_licenses.dart";
@ -22,6 +24,7 @@ import "package:window_manager/window_manager.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late final bool isInBackground;
final class Logger extends ProviderObserver { final class Logger extends ProviderObserver {
@override @override
@ -45,6 +48,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
error.toString().contains("Invalid source") || error.toString().contains("Invalid source") ||
error.toString().contains("UTF-16") || error.toString().contains("UTF-16") ||
error.toString().contains("HTTP request failed") || error.toString().contains("HTTP request failed") ||
error.toString().contains("'_nextFrame != null': is not true.") ||
error.toString().contains("Invalid image data")) { error.toString().contains("Invalid image data")) {
return; return;
} }
@ -62,7 +66,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
} }
} }
void main() async { void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
@ -77,11 +81,14 @@ void main() async {
await windowManager.setMinimumSize(Size.square(500)); await windowManager.setMinimumSize(Size.square(500));
} }
isInBackground = args.contains("--unifiedpush-bg");
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses)); LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
FlutterError.onError = (FlutterErrorDetails details) => FlutterError.onError = (FlutterErrorDetails details) =>
showError(details.exception.toString(), details.stack); showError(details.exception.toString(), details.stack);
if (!isInBackground) {
runApp( runApp(
ProviderScope( ProviderScope(
retry: (_, _) => null, retry: (_, _) => null,
@ -90,12 +97,14 @@ void main() async {
// ignore: dead_code // ignore: dead_code
if (false && kDebugMode) Logger(), if (false && kDebugMode) Logger(),
], ],
child: const App(), child: App(isInBackground),
), ),
); );
} }
}
class const App({super.key}) extends StatelessWidget { class const App(final bool isInBackground, {super.key})
extends StatelessWidget {
@override @override
Widget build(BuildContext context) => DynamicColorBuilder( Widget build(BuildContext context) => DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => Consumer( builder: (lightDynamic, darkDynamic) => Consumer(
@ -138,6 +147,8 @@ class const App({super.key}) extends StatelessWidget {
IListConst([ IListConst([
SharedPrefsController.provider, SharedPrefsController.provider,
ClientController.provider, ClientController.provider,
NotificationsController.provider,
UnifiedPushController.provider,
]), ]),
), ),
) )

View file

@ -0,0 +1,25 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "capabilities.freezed.dart";
part "capabilities.g.dart";
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const Capabilities({
@JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush,
}) with _$Capabilities {
Map<String, Object?> toJson() => _$CapabilitiesToJson(this);
factory Capabilities.fromJson(Map<String, Object?> json) =>
_$CapabilitiesFromJson(json);
}
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const WebPush({required final bool enabled, final String? vapid})
with _$WebPush {
Map<String, Object?> toJson() => _$WebPushToJson(this);
factory WebPush.fromJson(Map<String, Object?> json) =>
_$WebPushFromJson(json);
}

View file

@ -10,6 +10,7 @@ class const ClientState({
required final bool isLoggedIn, required final bool isLoggedIn,
required final bool isVerified, required final bool isVerified,
required final String? userId, required final String? userId,
required final String? deviceId,
required final String? homeserverUrl, required final String? homeserverUrl,
}) with _$ClientState { }) with _$ClientState {
Map<String, Object?> toJson() => _$ClientStateToJson(this); Map<String, Object?> toJson() => _$ClientStateToJson(this);

View file

@ -1,4 +1,4 @@
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum(fieldRename: FieldRename.snake) @JsonEnum(fieldRename: .snake)
enum JoinRule { public, knock, invite, private, restricted, knockRestricted } enum JoinRule { public, knock, invite, private, restricted, knockRestricted }

View file

@ -1,4 +1 @@
import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum()
enum MembershipAction { ban, kick, unban, invite } enum MembershipAction { ban, kick, unban, invite }

View file

@ -1,4 +1 @@
import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum()
enum MembershipStatus { leave, invite, ban, join, knock } enum MembershipStatus { leave, invite, ban, join, knock }

View file

@ -9,7 +9,7 @@ part "profile_response.g.dart";
@JsonSerializable() @JsonSerializable()
class const ProfileResponse({ class const ProfileResponse({
@JsonKey(fromJson: Profile.fromJson) required final Profile profile, @JsonKey(fromJson: Profile.fromJson) required final Profile profile,
required final Bio? bio, final Bio? bio,
}) with _$ProfileResponse { }) with _$ProfileResponse {
Map<String, Object?> toJson() => _$ProfileResponseToJson(this); Map<String, Object?> toJson() => _$ProfileResponseToJson(this);

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,56 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "register_pusher.freezed.dart";
part "register_pusher.g.dart";
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const RegisterPusherRequest({
required final String appDisplayName,
required final String appId,
final bool append = false,
required final PusherData data,
required final String deviceDisplayName,
required final PusherKind kind,
required final String lang,
/// TODO: What does this do?
final String? profileTag,
@JsonKey(name: "pushkey") required final String pushKey,
}) with _$RegisterPusherRequest {
Map<String, Object?> toJson() => _$RegisterPusherRequestToJson(this);
}
@freezed
sealed class PusherData with _$PusherData {
const factory PusherData.http({
required Uri url,
@Default(PushFormat.eventIdOnly) PushFormat format,
}) = HttpPusherData;
const factory PusherData.webPush({
required Uri url,
@Default(PushFormat.eventIdOnly) PushFormat format,
/// `data.auth`: RFC8291 authentication secret.
required String auth,
}) = WebPushPusherData;
factory PusherData.fromJson(Map<String, Object?> json) =>
_$PusherDataFromJson(json);
}
@JsonEnum(fieldRename: .snake)
enum PushFormat {
@JsonValue(null)
all,
eventIdOnly,
}
enum PusherKind {
http,
email,
@JsonValue("org.matrix.msc4174.webpush")
webPush,
}

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(),
);
}
}

Some files were not shown because too many files have changed in this diff Show more