From 16fdf0c4202ea16c30fee06a842a55e604cf3aff Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 27 Jul 2026 15:46:08 -0400 Subject: [PATCH 01/20] download audio and video using gomuks --- .gitmodules | 2 +- gomuks | 2 +- lib/helpers/extensions/get_headers.dart | 7 ------ lib/widgets/players/audio.dart | 33 +++++++++++++++---------- lib/widgets/players/video.dart | 20 ++++++++------- lib/widgets/renderers/message.dart | 23 ++++++++++++++--- 6 files changed, 52 insertions(+), 35 deletions(-) delete mode 100644 lib/helpers/extensions/get_headers.dart diff --git a/.gitmodules b/.gitmodules index dc5e02e..145276a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "gomuks"] path = gomuks - url = https://github.com/henry-hiles/gomuks + url = https://github.com/gomuks/gomuks branch = main diff --git a/gomuks b/gomuks index 1f11743..0f8fb26 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit 1f11743884a64765b1444b250377a09ebcaba93c +Subproject commit 0f8fb261ab57977fa24a4f63b7aafbb9a4ff1b7b diff --git a/lib/helpers/extensions/get_headers.dart b/lib/helpers/extensions/get_headers.dart deleted file mode 100644 index 0fcec48..0000000 --- a/lib/helpers/extensions/get_headers.dart +++ /dev/null @@ -1,7 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/header.dart"; - -extension GetHeaders on WidgetRef { - Map get headers => - watch(HeaderController.provider).requireValue; -} diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart index 0c96579..22a91f7 100644 --- a/lib/widgets/players/audio.dart +++ b/lib/widgets/players/audio.dart @@ -1,17 +1,17 @@ import "dart:async"; - import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:media_kit/media_kit.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/controllers/client.dart"; import "package:nexus/models/info/audio.dart"; class AudioPlayer extends HookConsumerWidget { - final Uri url; + final Uri uri; final AudioInfo? info; + final bool encrypted; - const AudioPlayer(this.url, this.info, {super.key}); + const AudioPlayer(this.uri, this.info, {this.encrypted = false, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,11 +24,12 @@ class AudioPlayer extends HookConsumerWidget { final duration = useState(Duration.zero); useEffect(() { + player.platform?.state = player.platform!.state.copyWith(buffering: true); scheduleMicrotask(() async { - await player.open( - Media(url.toString(), httpHeaders: ref.headers), - play: false, - ); + final video = await ref + .watch(ClientController.provider.notifier) + .downloadMedia(.new(mxc: uri, encrypted: encrypted)); + await player.open(Media(video.path), play: false); player.stream.playing.listen((value) { playing.value = value; @@ -67,12 +68,18 @@ class AudioPlayer extends HookConsumerWidget { padding: .only(left: 8, right: 16), child: Row( children: [ - IconButton( - onPressed: player.playOrPause, - icon: Icon( - playing.value ? Icons.pause_circle : Icons.play_circle, + if (player.state.buffering) + SizedBox.square( + dimension: 24, + child: CircularProgressIndicator(padding: .all(4)), + ) + else + IconButton( + onPressed: player.playOrPause, + icon: Icon( + playing.value ? Icons.pause_circle : Icons.play_circle, + ), ), - ), SizedBox(width: 8), Text( format(position.value), diff --git a/lib/widgets/players/video.dart b/lib/widgets/players/video.dart index 8083860..cd4e569 100644 --- a/lib/widgets/players/video.dart +++ b/lib/widgets/players/video.dart @@ -2,16 +2,17 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client.dart"; import "package:nexus/models/info/video.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; class VideoPlayer extends HookConsumerWidget { final VideoInfo? info; - final Uri url; - const VideoPlayer(this.url, this.info, {super.key}); + final Uri uri; + final bool encrypted; + const VideoPlayer(this.uri, this.info, {this.encrypted = false, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -21,12 +22,13 @@ class VideoPlayer extends HookConsumerWidget { final controller = useMemoized(() => VideoController(player)); useEffect(() { - scheduleMicrotask( - () => player.open( - Media(url.toString(), httpHeaders: ref.headers), - play: false, - ), - ); + player.platform?.state = player.platform!.state.copyWith(buffering: true); + scheduleMicrotask(() async { + final video = await ref + .watch(ClientController.provider.notifier) + .downloadMedia(.new(mxc: uri, encrypted: encrypted)); + await player.open(Media(video.path), play: false); + }); return player.dispose; }, []); diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 6d660af..0e0f6fa 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -226,10 +226,25 @@ class MessageRenderer extends ConsumerWidget { ConstrainedBox( constraints: .loose(.square(500)), child: switch (event.content) { - VideoMessageContent(:final info) => - VideoPlayer(url, info), - AudioMessageContent(:final info) => - AudioPlayer(url, info), + VideoMessageContent( + :final info, + :final file, + ) => + VideoPlayer( + url, + info, + encrypted: file != null, + ), + AudioMessageContent( + :final info, + :final file, + ) => + AudioPlayer( + url, + info, + + encrypted: file != null, + ), FileMessageContent( :final info, :final filename, From eca9d3b411dc47c94254b4c60cbbf2d70146a1da Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 27 Jul 2026 16:01:40 -0400 Subject: [PATCH 02/20] use gomuks for URL previews --- lib/controllers/client.dart | 9 +++--- lib/controllers/header.dart | 20 ------------- lib/controllers/url_preview.dart | 48 +++++++----------------------- lib/main.dart | 2 -- lib/widgets/renderers/message.dart | 3 +- lib/widgets/url_preview.dart | 5 ++-- 6 files changed, 18 insertions(+), 69 deletions(-) delete mode 100644 lib/controllers/header.dart diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 2cb1d66..97c3ed6 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -17,6 +17,7 @@ import "package:nexus/main.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/download_media.dart"; import "package:nexus/models/requests/get_event.dart"; @@ -215,11 +216,6 @@ class ClientController extends AsyncNotifier { return response["room_id"]; } - Future getAccessToken() async { - final response = await _sendCommand("get_account_info", {}); - return response?["access_token"]; - } - Future leaveRoom(Room room) async { if (room.metadata == null) return; await _sendCommand("leave_room", {"room_id": room.metadata!.id}); @@ -255,6 +251,9 @@ class ClientController extends AsyncNotifier { return json == null ? null : .fromJson(json); } + Future getUrlPreview(Uri url) async => + .fromJson(await _sendCommand("get_url_preview", {"url": url.toString()})); + Future paginate(PaginateRequest request) async => .fromJson(await _sendCommand("paginate", request.toJson())); diff --git a/lib/controllers/header.dart b/lib/controllers/header.dart deleted file mode 100644 index 3e173ad..0000000 --- a/lib/controllers/header.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; - -class HeaderController extends AsyncNotifier> { - @override - Future> build() async { - if (ref.watch(ClientStateController.provider)?.isLoggedIn != true) { - return {}; - } - final client = ref.watch(ClientController.provider.notifier); - final accessToken = await client.getAccessToken(); - return {"authorization": "Bearer $accessToken"}; - } - - static final provider = - AsyncNotifierProvider>( - HeaderController.new, - ); -} diff --git a/lib/controllers/url_preview.dart b/lib/controllers/url_preview.dart index 2a81a3c..4e7d3a0 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -1,50 +1,22 @@ -import "dart:convert"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/header.dart"; +import "package:nexus/controllers/client.dart"; import "package:nexus/models/open_graph_data.dart"; class UrlPreviewController extends AsyncNotifier { - final String link; - UrlPreviewController(this.link); + final Uri url; + UrlPreviewController(this.url); @override Future build() async { - try { - final homeserver = ref.watch( - ClientStateController.provider.select((value) => value?.homeserverUrl), - ); + if (url.host == "matrix.to") return null; - if (homeserver != null && !link.contains("matrix.to")) { - { - final response = await get( - .parse(homeserver) - .resolve("/_matrix/client/v1/media/preview_url") - .replace(queryParameters: {"url": link}), - headers: await ref.watch(HeaderController.provider.future), - ); - - if (response.statusCode == 200) { - final decodedValue = json.decode(response.body); - if (decodedValue is! Map) return null; - - return .fromJson( - decodedValue, - ).copyWith(imageUrl: decodedValue["og:image"]); - } - } - } - } catch (_) { - return null; - } - return null; + return await ref + .watch(ClientController.provider.notifier) + .getUrlPreview(url); } static final provider = - AsyncNotifierProvider.family< - UrlPreviewController, - OpenGraphData?, - String - >(UrlPreviewController.new); + AsyncNotifierProvider.family( + UrlPreviewController.new, + ); } diff --git a/lib/main.dart b/lib/main.dart index f50d22d..53cc57b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:media_kit/media_kit.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/header.dart"; import "package:nexus/controllers/multi_provider.dart"; import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/shared_prefs.dart"; @@ -144,7 +143,6 @@ class App extends StatelessWidget { IListConst([ SharedPrefsController.provider, ClientController.provider, - HeaderController.provider, ]), ), ) diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 0e0f6fa..09ef00b 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -273,7 +273,8 @@ class MessageRenderer extends ConsumerWidget { (element) => element is UrlElement, ) case final UrlElement link?) - UrlPreview(link.url), + if (Uri.tryParse(link.url) case final Uri url?) + UrlPreview(url), ReactionRow(event), ], diff --git a/lib/widgets/url_preview.dart b/lib/widgets/url_preview.dart index 5831300..3a20e19 100644 --- a/lib/widgets/url_preview.dart +++ b/lib/widgets/url_preview.dart @@ -6,7 +6,7 @@ import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/mxc_image.dart"; class UrlPreview extends ConsumerWidget { - final String link; + final Uri link; const UrlPreview(this.link, {super.key}); @override @@ -18,8 +18,7 @@ class UrlPreview extends ConsumerWidget { data: (preview) => preview == null ? SizedBox.shrink() : InkWell( - onTap: () => - ref.watch(LaunchHelper.provider).launchUrl(.parse(link)), + onTap: () => ref.watch(LaunchHelper.provider).launchUrl(link), child: Card( margin: .symmetric(vertical: 4), color: Theme.of( From 5f13d3fbf5cb76872aa95b3d2918addf087a8168 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 27 Jul 2026 22:02:20 -0400 Subject: [PATCH 03/20] don't show errors when url preview fails --- lib/controllers/url_preview.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/controllers/url_preview.dart b/lib/controllers/url_preview.dart index 4e7d3a0..1b17870 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -1,3 +1,4 @@ +import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/open_graph_data.dart"; @@ -10,9 +11,14 @@ class UrlPreviewController extends AsyncNotifier { Future build() async { if (url.host == "matrix.to") return null; - return await ref - .watch(ClientController.provider.notifier) - .getUrlPreview(url); + try { + return await ref + .watch(ClientController.provider.notifier) + .getUrlPreview(url); + } catch (error, stackTrace) { + debugPrintStack(label: error.toString(), stackTrace: stackTrace); + return null; + } } static final provider = From 2a1cc33ee3a2e073af6f997913090be9e51d6440 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 15:13:14 -0400 Subject: [PATCH 04/20] add macos ci hopefully this works first try :3 --- .github/workflows/macos.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/macos.yml diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..105f778 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,31 @@ +name: "Build APK" + +on: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: + +jobs: + build-apk: + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Lix GHA Installer Action + uses: samueldr/lix-gha-installer-action@v2026-02-22 + with: + extra_nix_config: experimental-features = nix-command flakes flake-self-attrs + + - name: Build app + run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build macos --release" + + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: App + path: build/macos/Build/Products/Release/Nexus.app \ No newline at end of file From afd6168535a03f7bb329cfc04f9de01d93fa32b2 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 15:15:02 -0400 Subject: [PATCH 05/20] fix macos CI name [skip ci] --- .github/workflows/macos.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 105f778..e7040fa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,4 @@ -name: "Build APK" +name: "Build MacOS App" on: push: @@ -7,7 +7,7 @@ on: workflow_dispatch: jobs: - build-apk: + build-app: runs-on: macos-latest steps: From 60968f5f3eeb6685fa73f4bf7ae88c1439dd7de5 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 15:29:41 -0400 Subject: [PATCH 06/20] make macos builder build app from nix --- .github/workflows/macos.yml | 8 +++----- linux/nix/pkg/default.nix | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e7040fa..9d76b1b 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -13,8 +13,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - submodules: recursive - name: Lix GHA Installer Action uses: samueldr/lix-gha-installer-action@v2026-02-22 @@ -22,10 +20,10 @@ jobs: extra_nix_config: experimental-features = nix-command flakes flake-self-attrs - name: Build app - run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build macos --release" + run: nix build .#default - name: Upload installer artifact uses: actions/upload-artifact@v6 with: - name: App - path: build/macos/Build/Products/Release/Nexus.app \ No newline at end of file + name: flatpak-${{ matrix.arch }} + path: result/Nexus.app \ No newline at end of file diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix index 4e00959..c026606 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -44,7 +44,7 @@ flutter.buildFlutterApplication { meta = { description = "A simple and user-friendly Matrix client"; mainProgram = "nexus"; - platforms = lib.platforms.linux; + platforms = lib.platforms.unix; maintainers = with lib.maintainers; [ quadradical ]; }; } From 06075f75c561b5a1bdad0cb3c52c1ba9bf9a58f3 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 15:49:27 -0400 Subject: [PATCH 07/20] make gomuks nix package build to a .dylib on macos --- linux/nix/pkg/gomuks.nix | 41 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix index 40fe29c..9d7770b 100644 --- a/linux/nix/pkg/gomuks.nix +++ b/linux/nix/pkg/gomuks.nix @@ -1,31 +1,38 @@ { src, + stdenv, buildGoModule, }: -buildGoModule (finalAttrs: { - pname = "gomuks-ffi"; - version = "submodule"; +buildGoModule ( + finalAttrs: + let + filename = "libgomuks${stdenv.hostPlatform.extensions.sharedLibrary}"; + in + { + pname = "gomuks-ffi"; + version = "submodule"; - doCheck = false; + doCheck = false; - src = "${src}/gomuks"; + src = "${src}/gomuks"; - vendorHash = "sha256-4QU51WGiNKYm7z/ajnas2qQaMK5BgyHwPfmNpQvW0qg="; + vendorHash = "sha256-4QU51WGiNKYm7z/ajnas2qQaMK5BgyHwPfmNpQvW0qg="; - buildPhase = '' - runHook preBuild + buildPhase = '' + runHook preBuild - go build -buildmode=c-shared -o libgomuks.so -tags goolm,noheic,sqlite_fts5 ./pkg/ffi + go build -buildmode=c-shared -o ${filename} -tags goolm,noheic,sqlite_fts5 ./pkg/ffi - runHook postBuild - ''; + runHook postBuild + ''; - installPhase = '' - runHook preInstall + installPhase = '' + runHook preInstall - install -Dm0644 libgomuks.so -t $out/lib + install -Dm0644 ${filename} -t $out/lib - runHook postInstall - ''; -}) + runHook postInstall + ''; + } +) From 9298eb815db74bbc7e7b9a78a785f2a9e958e006 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 15:57:39 -0400 Subject: [PATCH 08/20] use libclang.dylib on macos --- scripts/generate.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/generate.dart b/scripts/generate.dart index 0b0cf7c..c381230 100644 --- a/scripts/generate.dart +++ b/scripts/generate.dart @@ -24,7 +24,18 @@ void main(List args) async { ).generate( libclangDylib: libclangPath == null ? null - : Uri.file(join(libclangPath, "libclang.so")), + : Uri.file( + join( + libclangPath, + "libclang.${(Platform.isLinux || Platform.isAndroid) + ? "so" + : Platform.isMacOS + ? "dylib" + : Platform.isWindows + ? "dll" + : throw UnsupportedError("Unsupported Platform")}", + ), + ), ); print("Done!"); } From b2813a0eb511b891d35d8fbcefbf69feb3d7184e Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 17:04:06 -0400 Subject: [PATCH 09/20] use devshell for macos --- .github/workflows/macos.yml | 6 +++--- linux/nix/devshell.nix | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 9d76b1b..958458b 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -20,10 +20,10 @@ jobs: extra_nix_config: experimental-features = nix-command flakes flake-self-attrs - name: Build app - run: nix build .#default + run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build macos --release" - name: Upload installer artifact uses: actions/upload-artifact@v6 with: - name: flatpak-${{ matrix.arch }} - path: result/Nexus.app \ No newline at end of file + name: App + path: build/macos/Build/Products/Release/Nexus.app \ No newline at end of file diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix index ae77467..c29ac4d 100644 --- a/linux/nix/devshell.nix +++ b/linux/nix/devshell.nix @@ -35,8 +35,6 @@ pkgs.mkShell { env = rec { LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ]; - LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; - CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; ANDROID_SDK_ROOT = ANDROID_HOME; @@ -44,5 +42,9 @@ pkgs.mkShell { TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; + LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; }; } From e8b90591d5145dad3ecfab957060f3c9f8b79f7a Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 17:13:55 -0400 Subject: [PATCH 10/20] only include wayland in devshell on macos --- linux/nix/devshell.nix | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix index c29ac4d..812082c 100644 --- a/linux/nix/devshell.nix +++ b/linux/nix/devshell.nix @@ -18,20 +18,22 @@ let }; in pkgs.mkShell { - packages = with pkgs; [ - go - git - jdk17 - libGL - wayland - (flutter.override { - extraPkgConfigPackages = [ - mpv-unwrapped - libass - ]; - }) - android.platform-tools - ]; + packages = + with pkgs; + [ + go + git + jdk17 + libGL + (flutter.override { + extraPkgConfigPackages = [ + mpv-unwrapped + libass + ]; + }) + android.platform-tools + ] + ++ lib.optional pkgs.stdenv.isLinux wayland; env = rec { LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ]; From 9bf664624a041f5c408aac0f1d4691a67498d1cd Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 17:43:51 -0400 Subject: [PATCH 11/20] clone submodules on macos --- .github/workflows/macos.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 958458b..e7040fa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + submodules: recursive - name: Lix GHA Installer Action uses: samueldr/lix-gha-installer-action@v2026-02-22 From 8c25228bd987a52d417bf9e8854bd1bee28b45ae Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 28 Jul 2026 20:39:03 -0400 Subject: [PATCH 12/20] comment out macos CI auto running [skip ci] --- .github/workflows/macos.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e7040fa..8ed63cf 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,9 +1,10 @@ name: "Build MacOS App" on: - push: - branches: ["main"] - tags: ["*"] + # TODO: Uncomment this once the MacOS workflow actually works + # push: + # branches: ["main"] + # tags: ["*"] workflow_dispatch: jobs: From 80fa9673bb8dc9a5ced879ad4d901a2acea238bf Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 11:58:12 -0400 Subject: [PATCH 13/20] fix pending attachments disappearing when new events are sent --- lib/controllers/attachment.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index e33493e..220b5b3 100644 --- a/lib/controllers/attachment.dart +++ b/lib/controllers/attachment.dart @@ -17,7 +17,7 @@ class AttachmentController extends Notifier<(String, MessageContent?)?> { final filename = basename(file.path); state = (filename, null); - final room = ref.watch( + final room = ref.read( RoomsController.provider.select((value) => value[roomId]), ); From f0f426a6479fa6a93cae50e05be318d8ff3f73e6 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 12:06:19 -0400 Subject: [PATCH 14/20] improve matcher for encrypted media --- lib/controllers/attachment.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index 220b5b3..2a99ee5 100644 --- a/lib/controllers/attachment.dart +++ b/lib/controllers/attachment.dart @@ -17,18 +17,16 @@ class AttachmentController extends Notifier<(String, MessageContent?)?> { final filename = basename(file.path); state = (filename, null); - final room = ref.read( - RoomsController.provider.select((value) => value[roomId]), + final isEncrypted = ref.read( + RoomsController.provider.select( + (value) => + value[roomId]?.state[EventType.encryption.type]?.isNotEmpty == true, + ), ); final content = await ref .watch(ClientController.provider.notifier) - .uploadMedia( - .new( - path: file.path, - encrypt: room?.state[EventType.encryption.type]?.isNotEmpty == true, - ), - ); + .uploadMedia(.new(path: file.path, encrypt: isEncrypted)); state = (filename, content); } From d7f49568a7dd5d7ccff8986986668458b2712464 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 12:11:57 -0400 Subject: [PATCH 15/20] move attachment sending check to be earlier fixes sending with keyboard before attachment uploaded --- lib/controllers/room_chat.dart | 8 +++----- lib/widgets/composer/composer.dart | 5 ++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 558817c..3e531bb 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -125,11 +125,9 @@ class RoomChatController extends AsyncNotifier?> { if (relationType == .edit) { baseContent = relation?.content; } else { - final attachment = ref.watch(AttachmentController.provider(roomId)); - if (attachment != null && attachment.$2 == null) return; - - baseContent = attachment?.$2; - ref.invalidate(AttachmentController.provider(roomId)); + final provider = AttachmentController.provider(roomId); + baseContent = ref.watch(provider)?.$2; + ref.invalidate(provider); } var taggedMessage = text; diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart index c63d939..ca3de8a 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -59,7 +59,10 @@ class Composer extends HookConsumerWidget { final attachment = ref.watch(AttachmentController.provider(roomId)); void send() { - if (controller.value.text.isEmpty && attachment == null) return; + if (controller.value.text.isEmpty && attachment == null || + attachment != null && attachment.$2 == null) { + return; + } onSend( controller.value.formattedText, shouldMention: shouldMention.value, From 9831446c666d98d547071e691f96b1b2a87be57e Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 16:18:07 -0400 Subject: [PATCH 16/20] add a little padding to send errors --- lib/widgets/renderers/event.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index bcca961..2eb8bc3 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -216,10 +216,13 @@ class EventRenderer extends HookConsumerWidget { if (event.content is! MessageContent) ReactionRow(event), if (event.sendError != null && event.sendError != "not sent") - Text( - event.sendError!, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.error, + Padding( + padding: .only(bottom: 4), + child: Text( + event.sendError!, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.error, + ), ), ), ].map((child) => Padding(padding: .only(left: 4), child: child)), From 419d75ca83f0a63a191983fc3f69fdbcd55247bf Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 19:46:39 -0400 Subject: [PATCH 17/20] wrap messages with unknown message types --- lib/widgets/renderers/message.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 09ef00b..012dcc9 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -286,9 +286,8 @@ class MessageRenderer extends ConsumerWidget { "This message is redacted", style: errorStyle, ) - : Row( + : Wrap( spacing: 8, - mainAxisSize: .min, children: [ Text( "Unknown message type:", From f3c31db244fe98b3cb2a0a1c302e6911643f44cd Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 29 Jul 2026 21:57:03 -0400 Subject: [PATCH 18/20] update build requirements --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d191d6e..6eb021b 100644 --- a/README.md +++ b/README.md @@ -127,15 +127,16 @@ If you want to try out Nexus, grab one of the following artifacts from CI: #### Linux - With Nix: Either use direnv and `direnv allow`, or `nix flake develop` -- Without Nix: Install Flutter, Go, Git, Libclang, and Glibc. Do not use any Snap packages, they cause various compilation issues. +- Without Nix: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues. #### Windows You will need: - Flutter -- Android SDK + NDK - Git +- Libass +- MPV - Go - Visual Studio 2022 (Desktop development with C++) - [MSYS2/MinGW-w64 GCC](https://www.msys2.org/) (for CGO) From fc4aa5a10d79a899e8b4b77bef7d3eef77bc1cb1 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 31 Jul 2026 10:20:06 -0400 Subject: [PATCH 19/20] persist member list opened status --- lib/controllers/member_list_opened.dart | 22 ++++++++++++++++++++++ lib/widgets/room_chat.dart | 10 ++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 lib/controllers/member_list_opened.dart diff --git a/lib/controllers/member_list_opened.dart b/lib/controllers/member_list_opened.dart new file mode 100644 index 0000000..e3509f0 --- /dev/null +++ b/lib/controllers/member_list_opened.dart @@ -0,0 +1,22 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/shared_prefs.dart"; + +class MemberListOpenedController extends Notifier { + static const String key = "memberListOpened"; + + @override + bool build() => + ref.watch(SharedPrefsController.provider).requireValue.getBool(key) ?? + true; + + Future set(bool value) async { + final prefs = ref.watch(SharedPrefsController.provider).requireValue; + state = value; + + prefs.setBool(key, value); + } + + static final provider = NotifierProvider( + MemberListOpenedController.new, + ); +} diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 0fdd7c7..b1f29c4 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -7,6 +7,7 @@ import "package:measure_size/measure_size.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/member_list_opened.dart"; import "package:nexus/controllers/pinned_ids.dart"; import "package:nexus/controllers/power_level.dart"; import "package:nexus/controllers/rooms.dart"; @@ -46,9 +47,8 @@ class RoomChat extends HookConsumerWidget { final composerSize = useState(64); - final memberListOpened = useState(showMembersByDefault); - final userId = ref.watch(ClientStateController.provider)?.userId; + final memberListOpened = ref.watch(MemberListOpenedController.provider); final theme = Theme.of(context); final nothing = Center( @@ -448,7 +448,9 @@ class RoomChat extends HookConsumerWidget { isDesktop: isDesktop, onOpenDrawer: Scaffold.of(context).openDrawer, onOpenMemberList: (thisContext) { - memberListOpened.value = !memberListOpened.value; + ref + .watch(MemberListOpenedController.provider.notifier) + .set(!memberListOpened); Scaffold.of(thisContext).openEndDrawer(); }, onOpenPinnedMessagesList: () { @@ -553,7 +555,7 @@ class RoomChat extends HookConsumerWidget { ), ), - if (memberListOpened.value == true && showMembersByDefault) + if (memberListOpened == true && showMembersByDefault) MemberList(roomId), ], ), From 115bec5f055a9fa3892bc4d7f2ec95e5f5e43b9d Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 31 Jul 2026 11:55:48 -0400 Subject: [PATCH 20/20] slightly tweak readme [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6eb021b..57b42d8 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Development instructions can be found in [DEVELOPMENT.md](./DEVELOPMENT.md). ## Community -Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. +Join the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. # Credits