Watch
1
0
Fork
You've already forked nexus
0
forked from Nexus/nexus

Compare commits

..
Author SHA1 Message Date
129f0794f2
add wip deregistering 2026-08-26 16:27:43 -04:00
0122feae1d
smarter notif logic 2026-08-26 11:55:20 -04:00
74207e405b
include avatars on notifs 2026-08-26 00:46:57 -04:00
2beb6dcb9b
send notifications natively 2026-08-26 00:25:47 -04:00
11d83ee067
override webcrypto deps 2026-08-25 20:18:47 -04:00
02704e97ef
pass pushes to gomuks 2026-08-25 19:51:43 -04:00
adf3da8d77
working registration 2026-08-25 17:30:55 -04:00
5ed6e5e083
slight cleanup
`onNewEndpoint` is still not getting called for some reason (help appreciated)
2026-08-23 22:13:38 -04:00
7218eb5195
add UnifiedPushAllowedController 2026-08-23 14:26:51 -04:00
fe871217b2
support for registering pushers 2026-08-23 14:09:53 -04:00
33 changed files with 765 additions and 84 deletions

View file

@ -11,6 +11,8 @@
"muks",
"prefs",
"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"
> ```
Build webcrypto:
```sh
flutter pub run webcrypto:setup
```
Build generated files, and watch for new changes:
```sh

View file

@ -20,16 +20,21 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
defaultConfig {
applicationId = "nexus.federated.nexus"
minSdk = 29
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
multiDexEnabled = true
}
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 {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21

2
gomuks

@ -1 +1 @@
Subproject commit 0e06c7b74f96d3e29a8101893ca1921d13a90573
Subproject commit 6c8b2781e02f27b64f37735cba6634784f117b88

View file

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

View file

@ -1,3 +1,4 @@
import "dart:async";
import "dart:ffi";
import "dart:io";
import "dart:isolate";
@ -15,11 +16,13 @@ import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart";
import "package:nexus/models/capabilities.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/oauth_auth_code_response.dart";
import "package:nexus/models/open_graph_data.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/get_event.dart";
import "package:nexus/models/requests/get_related_events.dart";
@ -31,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/paginate.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/send_event.dart";
import "package:nexus/models/requests/send_message.dart";
@ -154,18 +158,15 @@ class ClientController extends AsyncNotifier<int> {
throw Exception("GomuksStart returned error code $errorCode");
}
Future<dynamic> _sendCommand(
String command, [
Map<String, dynamic> data = const {},
]) async {
Future<dynamic> callGomuksMethod(
Map<String, dynamic> data,
FutureOr<GomuksResponse> Function(int handle, GomuksBorrowedBuffer data)
callback,
) async {
final bufferPointer = data.toGomuksBufferPtr();
final handle = await future;
final response = await Isolate.run(
() => GomuksSubmitCommand(
handle,
command.toNativeUtf8().cast<Char>(),
bufferPointer.ref,
),
() => callback(handle, bufferPointer.ref),
);
calloc.free(bufferPointer);
@ -178,6 +179,24 @@ class ClientController extends AsyncNotifier<int> {
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) =>
_sendCommand("redact_event", report.toJson());
@ -262,13 +281,8 @@ class ClientController extends AsyncNotifier<int> {
Future<Paginate> paginate(PaginateRequest request) async =>
.fromJson(await _sendCommand("paginate", request.toJson()));
Future<ProfileResponse> getProfile(String userId) async {
try {
return .fromJson(await _sendCommand("get_profile", {"user_id": userId}));
} catch (_) {
return ProfileResponse(profile: .new(id: userId));
}
}
Future<ProfileResponse> getProfile(String userId) async =>
.fromJson(await _sendCommand("get_profile", {"user_id": userId}));
Future<void> reportEvent(ReportRequest request) =>
_sendCommand("report_event", request.toJson());
@ -279,6 +293,15 @@ class ClientController extends AsyncNotifier<int> {
Future<void> setAccountData(SetAccountDataRequest request) =>
_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 =>
.fromJson(await _sendCommand("upload_media", request.toJson()));
@ -316,6 +339,10 @@ class ClientController extends AsyncNotifier<int> {
Future<SpecVersionsResponse> getSpecVersions() async =>
.fromJson(await _sendCommand("get_versions"));
Future<Capabilities> getCapabilities() async => Capabilities.fromJson(
(await _sendCommand("get_capabilities"))["capabilities"],
);
Future<Uri?> discoverHomeserver(Uri homeserver) async {
try {
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 "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:flutter_riverpod/flutter_riverpod.dart";
import "package:intl/intl.dart";
import "package:m3e_buttons/m3e_buttons.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/notifications.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/unified_push_allowed.dart";
import "package:nexus/models/account_data.dart";
import "package:nexus/models/settings_category.dart";
import "package:nexus/main.dart";
@ -95,6 +99,90 @@ class SettingsSectionsController
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
"Account": .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(
title: "Safety",
icon: Icons.gpp_good,
@ -166,6 +254,8 @@ class SettingsSectionsController
onPressed: () async {
Navigator.of(context)
.popUntil((route) => route.isFirst);
Navigator.of(context)
.popUntil((route) => route.isFirst);
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_state.dart";
import "package:nexus/controllers/multi_provider.dart";
import "package:nexus/controllers/notifications.dart";
import "package:nexus/controllers/settings.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/scheme_to_theme.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";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late final bool isInBackground;
final class Logger extends ProviderObserver {
@override
@ -45,6 +48,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
error.toString().contains("Invalid source") ||
error.toString().contains("UTF-16") ||
error.toString().contains("HTTP request failed") ||
error.toString().contains("'_nextFrame != null': is not true.") ||
error.toString().contains("Invalid image data")) {
return;
}
@ -62,7 +66,7 @@ void showError(Object error, [StackTrace? stackTrace]) {
}
}
void main() async {
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
@ -77,11 +81,14 @@ void main() async {
await windowManager.setMinimumSize(Size.square(500));
}
isInBackground = args.contains("--unifiedpush-bg");
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
FlutterError.onError = (FlutterErrorDetails details) =>
showError(details.exception.toString(), details.stack);
if (!isInBackground) {
runApp(
ProviderScope(
retry: (_, _) => null,
@ -90,12 +97,14 @@ void main() async {
// ignore: dead_code
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
Widget build(BuildContext context) => DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => Consumer(
@ -138,6 +147,8 @@ class const App({super.key}) extends StatelessWidget {
IListConst([
SharedPrefsController.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 isVerified,
required final String? userId,
required final String? deviceId,
required final String? homeserverUrl,
}) with _$ClientState {
Map<String, Object?> toJson() => _$ClientStateToJson(this);

View file

@ -1,4 +1,4 @@
import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum(fieldRename: FieldRename.snake)
@JsonEnum(fieldRename: .snake)
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 }

View file

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

View file

@ -9,7 +9,7 @@ part "profile_response.g.dart";
@JsonSerializable()
class const ProfileResponse({
@JsonKey(fromJson: Profile.fromJson) required final Profile profile,
final Bio? bio,
required final Bio? bio,
}) with _$ProfileResponse {
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(),
);
}
}

View file

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

View file

@ -31,15 +31,9 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget {
Text(
preview.title!,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 3,
overflow: .ellipsis,
),
if (preview.description != null) ...[
Text(
preview.description!,
maxLines: 20,
overflow: .ellipsis,
),
Text(preview.description!),
SizedBox(height: 4),
],
if (preview.imageUrl != null)

View file

@ -207,14 +207,15 @@ final class const UserBottomSheet(
targetUser: userId,
),
),
))
) &&
member.status == .join ||
member.status == .invite)
Padding(
padding: .only(top: 4),
child: Row(
mainAxisSize: MainAxisSize.max,
spacing: 8,
children: [
if (member.status == .join || member.status == .invite)
M3EButton.icon(
onPressed: () => showMembershipDialog(.kick),
shape: .square,

View file

@ -13,6 +13,7 @@
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <webcrypto/webcrypto_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@ -37,6 +38,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) webcrypto_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin");
webcrypto_plugin_register_with_registrar(webcrypto_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);

View file

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video
screen_retriever_linux
url_launcher_linux
webcrypto
window_manager
)

View file

@ -45,7 +45,6 @@ pkgs.mkShell {
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2";
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ];
LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}";
};
}

View file

@ -17,7 +17,7 @@ buildGoModule (
src = "${src}/gomuks";
vendorHash = "sha256-9CS/lAVT8gLWACLomwcEa8YNlAXvBOskPPx4zERg0wQ=";
vendorHash = "sha256-C03ss88QAnmZu+XxoHhc1M/261xFl8hR/pzLbU37Qe8=";
buildPhase = ''
runHook preBuild

View file

@ -1,5 +1,6 @@
#include "my_application.h"
#include <cstdlib>
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@ -72,6 +73,7 @@ static void my_application_activate(GApplication* application) {
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
if (std::getenv("FLUTTER_HEADLESS")) gtk_widget_hide(GTK_WIDGET(window));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.

View file

@ -8,6 +8,7 @@ import Foundation
import app_links
import dynamic_color
import file_selector_macos
import flutter_local_notifications
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
@ -15,12 +16,14 @@ import screen_retriever_macos
import shared_preferences_foundation
import url_launcher_macos
import wakelock_plus
import webcrypto
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
@ -28,5 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View file

@ -77,10 +77,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: be169cf6ac481e052c4538715d88841d567150dfe1df38aaec76461a4e7b39f2
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.2.0"
args:
dependency: transitive
description:
@ -253,10 +253,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
version: "0.3.5+5"
crypto:
dependency: transitive
description:
@ -277,10 +277,10 @@ packages:
dependency: transitive
description:
name: cupertino_ui
sha256: "7ed8ce4159d342eec4c65f4ea6eec57adaf9365404378541f38efc1da20a5b3d"
sha256: "2137c0d41f3b62cc4d3d2495e6c354bd6b6133cd21c6bb155736cc31dbd49a11"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.1"
dart_style:
dependency: transitive
description:
@ -511,6 +511,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "1447ba911c60f2ba3f25dae1af151ec187162566b0f57e37771bf0b400f013ad"
url: "https://pub.dev"
source: hosted
version: "22.3.0"
flutter_local_notifications_linux:
dependency: "direct main"
description:
name: flutter_local_notifications_linux
sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b"
url: "https://pub.dev"
source: hosted
version: "8.0.1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd"
url: "https://pub.dev"
source: hosted
version: "12.2.0"
flutter_local_notifications_web:
dependency: transitive
description:
name: flutter_local_notifications_web
sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -891,10 +931,10 @@ packages:
dependency: "direct main"
description:
name: material_ui
sha256: "4f3f38b9953df0a87d6bf5f21880029f77c47048487d5339410c39936be4683b"
sha256: "7ba1ca315d50a004791dbab290417c52a61466d56db2822fe3c5c2994903110c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.1.0"
measure_size:
dependency: "direct main"
description:
@ -1501,6 +1541,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.7.1"
timezone:
dependency: transitive
description:
name: timezone
sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
typed_data:
dependency: transitive
description:
@ -1509,6 +1557,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
unifiedpush:
dependency: "direct main"
description:
name: unifiedpush
sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
unifiedpush_android:
dependency: transitive
description:
name: unifiedpush_android
sha256: a902e505e91c8e28b54b379bb27f255f66df409274a97af24d6719f12f0d13dc
url: "https://pub.dev"
source: hosted
version: "3.5.0"
unifiedpush_linux:
dependency: transitive
description:
name: unifiedpush_linux
sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
unifiedpush_platform_interface:
dependency: transitive
description:
name: unifiedpush_platform_interface
sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
unifiedpush_storage_interface:
dependency: transitive
description:
name: unifiedpush_storage_interface
sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9
url: "https://pub.dev"
source: hosted
version: "1.0.0"
unifiedpush_storage_shared_preferences:
dependency: "direct main"
description:
name: unifiedpush_storage_shared_preferences
sha256: eda9c52bac0058f81e0d9c65e33eedc12c5901d2e68ad47fdf68175ddf00a3b4
url: "https://pub.dev"
source: hosted
version: "1.0.0"
universal_platform:
dependency: transitive
description:
@ -1685,6 +1781,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webcrypto:
dependency: "direct dev"
description:
name: webcrypto
sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
webkit_inspection_protocol:
dependency: transitive
description:
@ -1693,6 +1797,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
webpush_encryption:
dependency: transitive
description:
name: webpush_encryption
sha256: "5b83272b91acda6ae515fcd980c94f06bf413702282497c5a68f5dfc64fed27f"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
win32:
dependency: transitive
description:

View file

@ -52,7 +52,7 @@ dependencies:
dynamic_polls: 0.0.8
flutter_hooks: 0.21.3+1
ffi: 2.2.0
hooks: ^2.2.0
hooks: 2.2.0
code_assets: 1.2.1
ffigen: 21.0.0
timeago: 3.7.1
@ -78,7 +78,11 @@ dependencies:
package_info_plus: 10.2.1
app_links: 7.2.1
file_selector: 1.1.0
material_ui: 1.0.1
material_ui: 1.1.0
unifiedpush: 6.2.0
unifiedpush_storage_shared_preferences: 1.0.0
flutter_local_notifications: 22.3.0
flutter_local_notifications_linux: 8.0.1
dev_dependencies:
build_runner: 2.16.0
@ -87,6 +91,8 @@ dev_dependencies:
riverpod_lint: 3.1.8
flutter_launcher_icons: 0.14.4
json_serializable: 6.14.1
# Transitive dep but needed for commands
webcrypto: any
flutter_launcher_icons:
image_path: assets/icon.png

View file

@ -13,6 +13,7 @@
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <webcrypto/webcrypto_plugin.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -30,6 +31,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WebcryptoPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WebcryptoPlugin"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View file

@ -10,10 +10,12 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video
screen_retriever_windows
url_launcher_windows
webcrypto
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)