Compare commits

..
19 changed files with 112 additions and 215 deletions

17
.vscode/launch.json vendored
View file

@ -1,17 +0,0 @@
{
"configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart"
},
{
"name": "Flutter (Headless)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"env": { "FLUTTER_HEADLESS": "1" }
}
]
}

View file

@ -127,7 +127,6 @@ If you want to try out Nexus, grab one of the following artifacts from CI:
- Flatpak - Flatpak
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip) - [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
- [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip) - [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip)
- [NixOS Module](linux/nix/module.nix)
## Build it yourself ## Build it yourself

View file

@ -16,12 +16,67 @@
... ...
}@inputs: }@inputs:
flake-parts.lib.mkFlake { inherit inputs; } { flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ ./linux/nix ];
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"
"aarch64-darwin" "aarch64-darwin"
"x86_64-darwin" "x86_64-darwin"
]; ];
perSystem =
{
lib,
pkgs,
system,
...
}:
{
_module.args.pkgs = import nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
packages =
let
default = pkgs.callPackage ./linux/nix/pkg {
src = self;
};
in
{
inherit default;
flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak {
appName = "Nexus";
developer = "QuadRadical";
appId = "nexus.federated.nexus";
package = default;
runtime = "org.gnome.Platform/49";
permissions = {
share = [ "network" ];
sockets = [
"pulseaudio"
"fallback-x11"
"wayland"
];
talk-names = [
"org.unifiedpush.Distributor.*"
"org.freedesktop.Notifications"
];
devices = [ "dri" ];
};
};
gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix {
src = self;
};
};
devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { };
};
}; };
} }

View file

@ -1,28 +1,28 @@
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/shared_prefs.dart"; import "package:nexus/controllers/shared_prefs.dart";
class KeyController(final String key) extends AsyncNotifier<String?> { class KeyController(final String key) extends Notifier<String?> {
static const String spaceKey = "space"; static const String spaceKey = "space";
static const String roomKey = "room"; static const String roomKey = "room";
static const String pushKeyKey = "pushKey"; static const String pushKeyKey = "pushKey";
@override @override
Future<String?> build() => String? build() =>
ref.watch(SharedPrefsController.provider).getString(key); ref.watch(SharedPrefsController.provider).requireValue.getString(key);
Future<void> set(String? value) async { Future<void> set(String? value) async {
final prefs = ref.watch(SharedPrefsController.provider); final prefs = ref.watch(SharedPrefsController.provider).requireValue;
state = .data(value); state = value;
if (value == null) { if (value == null) {
await prefs.remove(key); prefs.remove(key);
} else { } else {
await prefs.setString(key, value); prefs.setString(key, value);
} }
} }
static final provider = static final provider =
AsyncNotifierProvider.family<KeyController, String?, String>( NotifierProvider.family<KeyController, String?, String>(
KeyController.new, KeyController.new,
); );
} }

View file

@ -1,20 +1,22 @@
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/shared_prefs.dart"; import "package:nexus/controllers/shared_prefs.dart";
class MemberListOpenedController extends AsyncNotifier<bool> { class MemberListOpenedController extends Notifier<bool> {
static const String key = "memberListOpened"; static const String key = "memberListOpened";
@override @override
Future<bool> build() async => bool build() =>
await ref.watch(SharedPrefsController.provider).getBool(key) ?? true; ref.watch(SharedPrefsController.provider).requireValue.getBool(key) ??
true;
Future<void> set(bool value) async { Future<void> set(bool value) async {
state = .data(value); final prefs = ref.watch(SharedPrefsController.provider).requireValue;
await ref.watch(SharedPrefsController.provider).setBool(key, value); state = value;
prefs.setBool(key, value);
} }
static final provider = static final provider = NotifierProvider<MemberListOpenedController, bool>(
AsyncNotifierProvider<MemberListOpenedController, bool>(
MemberListOpenedController.new, MemberListOpenedController.new,
); );
} }

View file

@ -83,7 +83,6 @@ class NotificationController
File? icon, File? icon,
String? payload, String? payload,
}) async { }) async {
debugPrint("Sending notification for $id");
final notificationDetails = NotificationDetails( final notificationDetails = NotificationDetails(
android: .new( android: .new(
"messages", "messages",

View file

@ -4,19 +4,18 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
class PushKeyController(final String instance) extends AsyncNotifier<String?> { class PushKeyController(final String instance) extends Notifier<String?> {
@override @override
Future<String?> build() async => json.decode( String? build() => json.decode(
await ref.watch(KeyController.provider(KeyController.pushKeyKey).future) ?? ref.watch(KeyController.provider(KeyController.pushKeyKey)) ?? "{}",
"{}",
)[instance]; )[instance];
Future<void> set(String? value) async { Future<void> set(String? value) async {
final provider = KeyController.provider(KeyController.pushKeyKey); final provider = KeyController.provider(KeyController.pushKeyKey);
final notifier = ref.watch(provider.notifier); final notifier = ref.watch(provider.notifier);
final prefs = IMap(json.decode(await ref.watch(provider.future) ?? "{}")); final prefs = IMap(json.decode(ref.watch(provider) ?? "{}"));
state = .data(value); state = value;
notifier.set( notifier.set(
json.encode( json.encode(
prefs.add(instance, value).where((_, value) => value != null).unlock, prefs.add(instance, value).where((_, value) => value != null).unlock,
@ -25,7 +24,7 @@ class PushKeyController(final String instance) extends AsyncNotifier<String?> {
} }
static final provider = static final provider =
AsyncNotifierProvider.family<PushKeyController, String?, String>( NotifierProvider.family<PushKeyController, String?, String>(
PushKeyController.new, PushKeyController.new,
); );
} }

View file

@ -1,12 +1,12 @@
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:shared_preferences/shared_preferences.dart"; import "package:shared_preferences/shared_preferences.dart";
class SharedPrefsController extends Notifier<SharedPreferencesAsync> { class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
@override @override
SharedPreferencesAsync build() => SharedPreferencesAsync(); Future<SharedPreferences> build() async => .getInstance();
static final provider = static final provider =
NotifierProvider<SharedPrefsController, SharedPreferencesAsync>( AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
SharedPrefsController.new, SharedPrefsController.new,
); );
} }

View file

@ -13,6 +13,7 @@ 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/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";
import "package:window_manager/window_manager.dart"; import "package:window_manager/window_manager.dart";
@ -28,7 +29,6 @@ class UnifiedPushController extends AsyncNotifier<bool> {
dbusName: "nexus.federated.nexus.UnifiedPush", dbusName: "nexus.federated.nexus.UnifiedPush",
storage: UnifiedPushStorageSharedPreferences(), storage: UnifiedPushStorageSharedPreferences(),
background: isInBackground, background: isInBackground,
shouldWriteService: false,
), ),
onNewEndpoint: (endpoint, instance) async { onNewEndpoint: (endpoint, instance) async {
final pushKey = endpoint.pubKeySet!.pubKey; final pushKey = endpoint.pubKeySet!.pubKey;
@ -40,8 +40,8 @@ class UnifiedPushController extends AsyncNotifier<bool> {
.new( .new(
appDisplayName: "Nexus", appDisplayName: "Nexus",
appId: "nexus.federated.nexus", appId: "nexus.federated.nexus",
data: .webPush( data: PusherData.webPush(
url: .parse(endpoint.url), url: Uri.parse(endpoint.url),
auth: endpoint.pubKeySet!.auth, auth: endpoint.pubKeySet!.auth,
), ),
deviceDisplayName: deviceDisplayName:
@ -53,7 +53,6 @@ class UnifiedPushController extends AsyncNotifier<bool> {
); );
}, },
onMessage: (message, instance) async { onMessage: (message, instance) async {
debugPrint("UP message received for $instance");
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.",
@ -67,11 +66,8 @@ class UnifiedPushController extends AsyncNotifier<bool> {
event.unreadType?.shouldNotify() != true || event.unreadType?.shouldNotify() != true ||
(!isInBackground && (!isInBackground &&
await windowManager.isFocused().onError((_, _) => false) && await windowManager.isFocused().onError((_, _) => false) &&
await ref.watch( ref.watch(KeyController.provider(KeyController.roomKey)) ==
KeyController.provider(KeyController.roomKey).future,
) ==
event.roomId)) { event.roomId)) {
if (isInBackground) exit(0);
return; return;
} }
@ -148,9 +144,7 @@ class UnifiedPushController extends AsyncNotifier<bool> {
Future<void> deregister() async { Future<void> deregister() async {
final clientState = ref.watch(ClientStateController.provider); final clientState = ref.watch(ClientStateController.provider);
final key = await ref.watch( final key = ref.watch(PushKeyController.provider(clientState!.deviceId!));
PushKeyController.provider(clientState!.deviceId!).future,
);
if (key != null) { if (key != null) {
await ref await ref

View file

@ -7,11 +7,10 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:media_kit/media_kit.dart"; 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/key.dart";
import "package:nexus/controllers/member_list_opened.dart";
import "package:nexus/controllers/multi_provider.dart"; import "package:nexus/controllers/multi_provider.dart";
import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/notification.dart";
import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/shared_prefs.dart";
import "package:nexus/controllers/unified_push.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";
@ -82,20 +81,14 @@ void main(List<String> args) async {
await windowManager.setMinimumSize(Size.square(500)); await windowManager.setMinimumSize(Size.square(500));
} }
isInBackground = Platform.environment["FLUTTER_HEADLESS"] != null; isInBackground = args.contains("--unifiedpush-bg");
LicenseRegistry.addLicense(() => .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) { if (!isInBackground) {
await ProviderContainer().read(UnifiedPushController.provider.future);
// In case it didn't exit for some reason
await Future.delayed(Duration(seconds: 20));
exit(0);
} else {
runApp( runApp(
ProviderScope( ProviderScope(
retry: (_, _) => null, retry: (_, _) => null,
@ -104,13 +97,14 @@ void main(List<String> args) async {
// ignore: dead_code // ignore: dead_code
if (false && kDebugMode) Logger(), if (false && kDebugMode) Logger(),
], ],
child: 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(
@ -151,12 +145,10 @@ class const App({super.key}) extends StatelessWidget {
.watch( .watch(
MultiProviderController.provider( MultiProviderController.provider(
IListConst([ IListConst([
SharedPrefsController.provider,
ClientController.provider, ClientController.provider,
NotificationController.provider, NotificationController.provider,
UnifiedPushController.provider, UnifiedPushController.provider,
MemberListOpenedController.provider,
KeyController.provider(KeyController.roomKey),
KeyController.provider(KeyController.spaceKey),
]), ]),
), ),
) )

View file

@ -14,9 +14,7 @@ class const ChatPage({super.key}) extends ConsumerWidget {
final isDesktop = constraints.maxWidth > 650; final isDesktop = constraints.maxWidth > 650;
final showMembersByDefault = constraints.maxWidth > 1000; final showMembersByDefault = constraints.maxWidth > 1000;
final initComplete = ref.watch(InitCompleteController.provider); final initComplete = ref.watch(InitCompleteController.provider);
final roomId = ref final roomId = ref.watch(KeyController.provider(KeyController.roomKey));
.watch(KeyController.provider(KeyController.roomKey))
.requireValue;
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(

View file

@ -43,9 +43,7 @@ final class const RoomChat({
final composerSize = useState<double>(64); final composerSize = useState<double>(64);
final userId = ref.watch(ClientStateController.provider)?.userId; final userId = ref.watch(ClientStateController.provider)?.userId;
final memberListOpened = ref final memberListOpened = ref.watch(MemberListOpenedController.provider);
.watch(MemberListOpenedController.provider)
.requireValue;
final theme = Theme.of(context); final theme = Theme.of(context);
final nothing = Center( final nothing = Center(

View file

@ -23,13 +23,13 @@ class const Sidebar({required final bool isDesktop, super.key})
final selectedSpaceProvider = KeyController.provider( final selectedSpaceProvider = KeyController.provider(
KeyController.spaceKey, KeyController.spaceKey,
); );
final selectedSpaceId = ref.watch(selectedSpaceProvider).requireValue; final selectedSpaceId = ref.watch(selectedSpaceProvider);
final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier);
final selectedRoomController = KeyController.provider( final selectedRoomController = KeyController.provider(
KeyController.roomKey, KeyController.roomKey,
); );
final selectedRoomId = ref.watch(selectedRoomController).requireValue; final selectedRoomId = ref.watch(selectedRoomController);
final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier);
final spaces = ref.watch(SpacesController.provider); final spaces = ref.watch(SpacesController.provider);

View file

@ -1,3 +0,0 @@
[D-BUS Service]
Name=nexus.federated.nexus.UnifiedPush
Exec=/usr/bin/env FLUTTER_HEADLESS=1 nexus

View file

@ -1,59 +0,0 @@
{ self, inputs, ... }: {
perSystem =
{
pkgs,
system,
lib,
...
}:
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
packages =
let
default = pkgs.callPackage ./pkg {
src = self;
};
in
{
inherit default;
flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak {
appName = "Nexus";
developer = "QuadRadical";
appId = "nexus.federated.nexus";
package = default;
runtime = "org.gnome.Platform/49";
permissions = {
share = [ "network" ];
sockets = [
"pulseaudio"
"fallback-x11"
"wayland"
];
talk-names = [
"org.unifiedpush.Distributor.*"
"org.freedesktop.Notifications"
];
devices = [ "dri" ];
};
};
gomuks = pkgs.callPackage ./pkg/gomuks.nix {
src = self;
};
};
devShells.default = pkgs.callPackage ./devshell.nix { };
};
flake.nixosModules.default = import ./module.nix self;
}

View file

@ -1,45 +0,0 @@
self:
{
pkgs,
lib,
config,
...
}:
let
inherit (lib)
mkIf
mkMerge
mkEnableOption
mkPackageOption
;
cfg = config.programs.nexus;
in
{
options.programs.nexus = {
enable = mkEnableOption "Nexus, a simple and user-friendly Matrix client";
package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "default" { };
enableNotifications = mkEnableOption "notifications support via UnifiedPush";
};
config = mkIf cfg.enable (mkMerge [
{
environment.systemPackages = [ cfg.package ];
}
(mkIf cfg.enableNotifications {
environment.systemPackages = [
pkgs.kdePackages.kunifiedpush
];
systemd.packages = [
pkgs.kdePackages.kunifiedpush
];
services.dbus.packages = [
cfg.package
];
})
]);
}

View file

@ -32,14 +32,11 @@ flutter.buildFlutterApplication {
emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo=";
linkify = "sha256-TpMD6+0zyY6i9l+6d8ErnVufmepCv362rCtnbOht/z4="; linkify = "sha256-TpMD6+0zyY6i9l+6d8ErnVufmepCv362rCtnbOht/z4=";
navigation_rail_m3e = "sha256-+2awDTQnK58gGRY1nuHckG/jjxarsYSRu9ovR4i4TEc="; navigation_rail_m3e = "sha256-+2awDTQnK58gGRY1nuHckG/jjxarsYSRu9ovR4i4TEc=";
unifiedpush_linux = "sha256-aIF/8oabSuuZo7K6qr4r7UBmf57qgoGAXyJ40fdntuQ=";
unifiedpush_platform_interface = "sha256-aIF/8oabSuuZo7K6qr4r7UBmf57qgoGAXyJ40fdntuQ=";
}; };
postInstall = '' postInstall = ''
install -D assets/bundled/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg install -D assets/bundled/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg
install -Dm755 linux/nexus.federated.nexus.desktop -t $out/share/applications install -Dm755 linux/nexus.federated.nexus.desktop -t $out/share/applications
install -Dm644 linux/*.service -t $out/share/dbus-1/services
wrapProgram $out/bin/nexus \ wrapProgram $out/bin/nexus \
--suffix LD_LIBRARY_PATH : $out/app/nexus/lib --suffix LD_LIBRARY_PATH : $out/app/nexus/lib
''; '';

View file

@ -1501,22 +1501,20 @@ packages:
source: hosted source: hosted
version: "3.5.0" version: "3.5.0"
unifiedpush_linux: unifiedpush_linux:
dependency: "direct overridden" dependency: transitive
description: description:
path: unifiedpush_linux name: unifiedpush_linux
ref: "quad/feat/should-write-service-option" sha256: c062d5eedd1cec70bcd33270cc4e01ae0ff6501f33d471167c06b34a968adfeb
resolved-ref: "7589b07ef4280820e4adea165d3d86b219f6022c" url: "https://pub.dev"
url: "https://codeberg.org/Henry-Hiles/flutter-connector" source: hosted
source: git version: "1.0.0"
version: "1.0.1"
unifiedpush_platform_interface: unifiedpush_platform_interface:
dependency: "direct overridden" dependency: transitive
description: description:
path: unifiedpush_platform_interface name: unifiedpush_platform_interface
ref: "quad/feat/should-write-service-option" sha256: "83372bc8d794b8b12ef6993b518d7be907dcfc2191bdf6de0ece5c4445d89880"
resolved-ref: "7589b07ef4280820e4adea165d3d86b219f6022c" url: "https://pub.dev"
url: "https://codeberg.org/Henry-Hiles/flutter-connector" source: hosted
source: git
version: "4.0.0" version: "4.0.0"
unifiedpush_storage_interface: unifiedpush_storage_interface:
dependency: transitive dependency: transitive

View file

@ -20,16 +20,6 @@ environment:
dependency_overrides: dependency_overrides:
dynamic_color: 2.1.0 dynamic_color: 2.1.0
unifiedpush_linux:
git:
url: https://codeberg.org/Henry-Hiles/flutter-connector
ref: quad/feat/should-write-service-option
path: unifiedpush_linux
unifiedpush_platform_interface:
git:
url: https://codeberg.org/Henry-Hiles/flutter-connector
ref: quad/feat/should-write-service-option
path: unifiedpush_platform_interface
path_provider_android: 2.2.23 # Pinned to avoid JNI path_provider_android: 2.2.23 # Pinned to avoid JNI
linkify: linkify:
git: git: