diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..dc1e9c7 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,39 @@ +name: "Build APK" + +on: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: + +jobs: + build-apk: + runs-on: ubuntu-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: Decode keystore + run: echo "$KEYSTORE_CONTENT" | base64 --decode > keystore.jks + env: + KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }} + + - name: Build app + run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --release" + env: + KEYSTORE_PATH: ../../keystore.jks + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: APK + path: build/app/outputs/flutter-apk/app-release.apk \ No newline at end of file diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 0000000..5e693f0 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,37 @@ +name: "Build Flatpaks" + +on: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: + +jobs: + build-flatpak: + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 build .#flatpak + + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: flatpak-${{ matrix.arch }} + path: result/nexus.federated.Nexus.flatpak \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c8099d1..e66e45a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,46 +1,50 @@ -name: "Build Windows Version" +name: "Build EXE" on: + push: + branches: ["main"] + tags: ["*"] workflow_dispatch: jobs: - build-windows: - runs-on: "windows-latest" + build-exe: + runs-on: windows-latest steps: - - name: "Checkout repository" - uses: "actions/checkout@v4" - - - name: "Set up Flutter" - uses: "subosito/flutter-action@v2" - - - name: "Set up Rust" - uses: "dtolnay/rust-toolchain@stable" + - name: Checkout repository + uses: actions/checkout@v6 with: - targets: "x86_64-pc-windows-msvc" + submodules: recursive - - name: "Install Flutter dependencies" - run: flutter pub get + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.41.5 - - name: "Run build_runner & build Windows EXE" + - name: Set up Go + uses: actions/setup-go@v6 + + - name: Build with Flutter run: | - flutter pub run build_runner build --delete-conflicting-outputs + flutter pub get + dart scripts/generate.dart + flutter pub run build_runner build flutter build windows --release - - name: "Upload exe zip" - uses: "actions/upload-artifact@v4" + - name: Upload exe zip + uses: actions/upload-artifact@v6 with: - name: "windows-portable" - path: "build/windows/x64/runner/Release/" + name: windows-portable + path: build/windows/x64/runner/Release/ - - name: "Install Inno Setup" + - name: Install Inno Setup run: choco install innosetup -y - - name: "Build Inno Setup installer" + - name: Build Inno Setup installer run: iscc windows/installer.iss - - name: "Upload installer artifact" - uses: "actions/upload-artifact@v4" + - name: Upload installer artifact + uses: actions/upload-artifact@v6 with: - name: "windows-installer" - path: "windows/dist/Nexus-Setup.exe" + name: windows-installer + path: windows/dist/Nexus-Setup.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore index d6616e1..2bec583 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,9 @@ key.properties # Generated Files *.g.dart *.freezed.dart -src/ # Devel Password -password.txt \ No newline at end of file +password.txt + +# Nix +/result \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..17d64ba --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "gomuks"] + path = gomuks + url = https://github.com/zachatrocity/gomuks + branch = init-root-dir diff --git a/README.md b/README.md index 1299c71..7af23a0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Description -A simple and user-friendly Matrix client made with Flutter and the Matrix Dart SDK. +A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. ## Screenshots @@ -17,8 +17,8 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] New logo - [ ] Make context menus appear as bottom sheets on mobile -- [x] Move from the Dart SDK to the Gomuks SDK with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2 - - [ ] Allow using remote gomuks over websocket +- [x] Move from the Dart SDK to the Gomuks Backend with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2 + - [ ] Allow using remote Gomuks over websocket - [ ] Platform Support - [x] Linux - [x] Windows @@ -54,6 +54,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [x] HTML/Markdown - [x] Replies - [x] Choose ping on/off + - [ ] Per message profiles - [ ] Attachments - [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391) - [x] Mentions @@ -64,6 +65,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] GIFs using Gomuks' GIF proxies - [x] Recieving - [x] Plain text + - [x] Per message profiles - [x] HTML - [x] Replies - [x] Viewing @@ -128,7 +130,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S First, clone and open the repo: ```sh -git clone https://git.federated.nexus/Henry-Hiles/nexus +git clone --recurse-submodules https://git.federated.nexus/Henry-Hiles/nexus cd nexus ``` @@ -136,12 +138,12 @@ cd nexus #### Linux -- With Nix: Either use direnv, or `nix flake develop` +- With Nix: Either use direnv and `direnv allow`, or `nix flake develop` - Without Nix: Install Flutter, Go, Olm, Git, Clang, and GLibc. #### Windows / MacOS -I don't really know. You will need Flutter, Git, Olm, Go, and Visual Studio tools, and otherwise I guess just keep installing stuff until there aren't any errors. I will look into this sometimeTM. +I don't really know. You will need Flutter, Git, Go, and Visual Studio tools, and otherwise I guess just keep installing stuff until there aren't any errors. I will look into this sometimeTM. ### Set up Flutter @@ -151,13 +153,7 @@ Get dependencies: flutter pub get ``` -Get dependencies: - -```sh -flutter pub get -``` - -Clone Gomuks and generate bindings: +Generate Gomuks bindings: ```sh scripts/generate.sh diff --git a/android/app/build.gradle b/android/app/build.gradle index ce5f465..fd51ea0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,6 +39,11 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { + // do we want to update.. eventually? + jvmTarget = "17" + } + defaultConfig { applicationId = "nexus.federated.Nexus" minSdk = 29 @@ -50,7 +55,8 @@ android { signingConfigs { release { keyAlias "key" - storeFile keystoreProperties['path'] ? file(keystoreProperties['path']) : file(System.getenv("KEYSTORE_PATH")) + def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH") + storeFile storePath ? file(storePath) : null keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1c369c9..666977e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:label="Nexus" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" - android:roundIcon="@mipmap/nexus_round" + android:roundIcon="@mipmap/ic_launcher" android:allowBackup="false" android:fullBackupContent="false"> + + + diff --git a/assets/foreground.png b/assets/foreground.png index 4249989..76a446a 100644 Binary files a/assets/foreground.png and b/assets/foreground.png differ diff --git a/assets/reply-preview.png b/assets/reply-preview.png deleted file mode 100644 index 3c4cc3e..0000000 Binary files a/assets/reply-preview.png and /dev/null differ diff --git a/assets/reply.webp b/assets/reply.webp deleted file mode 100644 index e8f139e..0000000 Binary files a/assets/reply.webp and /dev/null differ diff --git a/flake.lock b/flake.lock index 7826732..8070c6c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,17 +18,54 @@ "type": "github" } }, - "nixpkgs": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1767640445, - "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { - "owner": "nixos", + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix2flatpak": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1774604963, + "narHash": "sha256-MtAW1FIdirSlUAAO7s1u9auv5y3I6t3uJ+GeEbqiqxI=", + "owner": "neobrain", + "repo": "nix2flatpak", + "rev": "3e04657fbcb49956ac301410b071a7f0b2ad5988", + "type": "github" + }, + "original": { + "owner": "neobrain", + "repo": "nix2flatpak", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773389992, + "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "type": "github" + }, + "original": { + "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -49,10 +86,42 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1767640445, + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs" + "nix2flatpak": "nix2flatpak", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index de21b13..4c06ac6 100644 --- a/flake.nix +++ b/flake.nix @@ -2,8 +2,10 @@ description = "Nexus Flutter Flake"; inputs = { + self.submodules = true; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; + nix2flatpak.url = "github:neobrain/nix2flatpak"; }; outputs = @@ -33,36 +35,42 @@ _module.args.pkgs = import nixpkgs { inherit system; config = { - permittedInsecurePackages = [ "olm-3.2.16" ]; android_sdk.accept_license = true; allowUnfree = true; }; }; - devShells = + packages = let - packages = with pkgs; [ - go - olm - git - ]; - - env = { - LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ]; - LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; - CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; + default = pkgs.callPackage ./linux/nix/pkg { + src = self; }; in { - default = pkgs.mkShell { - inherit env; - packages = packages ++ [ - pkgs.flutter - ]; + 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 = [ + "fallback-x11" + "wayland" + ]; + devices = [ "dri" ]; + }; }; - nix = pkgs.mkShell { inherit packages env; }; + gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix { + src = self; + }; }; + + devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { }; }; }; } diff --git a/gomuks b/gomuks new file mode 160000 index 0000000..daa0ba0 --- /dev/null +++ b/gomuks @@ -0,0 +1 @@ +Subproject commit daa0ba028e7d89ba9fc7580fc8099348e6145cb3 diff --git a/hook/build.dart b/hook/build.dart index 4cb2f91..13eb804 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -3,11 +3,12 @@ import "package:hooks/hooks.dart"; import "package:code_assets/code_assets.dart"; Future main(List args) => build(args, (input, output) async { - final buildDir = input.packageRoot.resolve("src/"); - if (await File(buildDir.resolve("lock").toFilePath()).exists()) return; + final codeConfig = input.config.code; + final targetOS = codeConfig.targetOS; + final targetArch = codeConfig.targetArchitecture; - final targetOS = input.config.code.targetOS; String libFileName; + Map env = {}; switch (targetOS) { case OS.linux: libFileName = "libgomuks.so"; @@ -17,24 +18,60 @@ Future main(List args) => build(args, (input, output) async { break; case OS.windows: libFileName = "libgomuks.dll"; + env = {"GOCACHE": r"C:\Users\runneradmin\AppData\Local\go-build"}; + break; + case OS.android: + libFileName = "libgomuks.so"; + + final targetNdkApi = codeConfig.android.targetNdkApi; + + final ndkHome = + Platform.environment["ANDROID_NDK_HOME"] ?? + Platform.environment["ANDROID_NDK_ROOT"] ?? + Platform.environment["NDK_HOME"] ?? + await _findNdkFromSdk(); + if (ndkHome == null) { + throw Exception( + "Could not find Android NDK. Set ANDROID_NDK_HOME or install via sdkmanager.", + ); + } + + final hostTag = _ndkHostTag(); + final (goArch, ccTriple) = _androidArch(targetArch); + final cc = + "$ndkHome/toolchains/llvm/prebuilt/$hostTag/bin/$ccTriple$targetNdkApi-clang"; + + env = {"CGO_ENABLED": "1", "GOOS": "android", "GOARCH": goArch, "CC": cc}; break; default: throw UnsupportedError("Unsupported OS: $targetOS"); } - final gomuksBuildDir = buildDir.resolve("gomuks/"); - final libFile = gomuksBuildDir.resolve(libFileName); + var libFile = input.packageRoot.resolve(libFileName); + final gomuksBuildDir = input.packageRoot.resolve("gomuks/"); - print("Building Gomuks shared library $libFileName from source..."); - final result = await Process.run("go", [ - "build", - "-o", - libFile.path, - "-buildmode=c-shared", - ], workingDirectory: gomuksBuildDir.resolve("source/pkg/ffi/").toFilePath()); + if (!(await File.fromUri(libFile).exists())) { + final buildDir = input.packageRoot.resolve("build/"); + libFile = buildDir.resolve("${targetArch.name}/$libFileName"); - if (result.exitCode != 0) { - throw Exception("Failed to build Gomuks shared library\n${result.stderr}"); + // goheif/dav1d supported on Android would need to fix upstream + final tags = targetOS == OS.android ? "goolm,noheic" : "goolm"; + + print( + "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) from source...", + ); + final result = await Process.run( + "go", + ["build", "-tags", tags, "-o", libFile.path, "-buildmode=c-shared"], + workingDirectory: gomuksBuildDir.resolve("pkg/ffi/").toFilePath(), + environment: env.isNotEmpty ? env : null, + ); + + if (result.exitCode != 0) { + throw Exception( + "Failed to build Gomuks shared library\n${result.stderr}", + ); + } } final generatedFile = "src/third_party/gomuks.g.dart"; @@ -52,3 +89,43 @@ Future main(List args) => build(args, (input, output) async { ..dependencies.add(gomuksBuildDir); print("Done!"); }); + +Future _findNdkFromSdk() async { + // pretty sure this wont be needed with nix, i'll get this removed + final androidHome = + Platform.environment["ANDROID_HOME"] ?? + Platform.environment["ANDROID_SDK_ROOT"]; + if (androidHome == null) return null; + final ndkDir = Directory("$androidHome/ndk"); + if (!await ndkDir.exists()) return null; + final versions = await ndkDir.list().toList(); + if (versions.isEmpty) return null; + versions.sort((a, b) => a.path.compareTo(b.path)); + return versions.last.path; +} + +String _ndkHostTag() { + if (Platform.isMacOS) { + return "darwin-x86_64"; + } else if (Platform.isLinux) { + return "linux-x86_64"; + } else if (Platform.isWindows) { + return "windows-x86_64"; + } + throw UnsupportedError("Unsupported host platform for Android NDK"); +} + +(String goArch, String ccTriple) _androidArch(Architecture arch) { + switch (arch) { + case Architecture.arm64: + return ("arm64", "aarch64-linux-android"); + case Architecture.arm: + return ("arm", "armv7a-linux-androideabi"); + case Architecture.x64: + return ("amd64", "x86_64-linux-android"); + case Architecture.ia32: + return ("386", "i686-linux-android"); + default: + throw UnsupportedError("Unsupported Android architecture: $arch"); + } +} diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d4af5a1..44f96da 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -387,7 +387,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -519,7 +519,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -545,7 +545,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/controllers/author_controller.dart b/lib/controllers/author_controller.dart new file mode 100644 index 0000000..c7e4e05 --- /dev/null +++ b/lib/controllers/author_controller.dart @@ -0,0 +1,44 @@ +import "dart:async"; +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/models/configs/author_config.dart"; +import "package:nexus/models/membership.dart"; + +class AuthorController extends AsyncNotifier { + final AuthorConfig config; + AuthorController(this.config); + + @override + Future build() async { + var member = await ref.watch( + MembersController.provider(config.room).selectAsync( + (value) => value.firstWhereOrNull( + (membership) => membership.userId == config.message.authorId, + ), + ), + ); + + final pmp = config.message.metadata?["pmp"] == null + ? null + : Membership.fromContent( + IMap(config.message.metadata?["pmp"]), + config.message.authorId, + ); + + return Membership( + avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl, + displayName: + pmp?.displayName ?? + member?.displayName ?? + config.message.authorId.substring(1).split(":").first, + userId: config.message.authorId, + ); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + AuthorController.new, + ); +} diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index de6e909..37dde3d 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,5 +1,6 @@ import "dart:developer"; import "dart:ffi"; +import "dart:io"; import "dart:isolate"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; @@ -31,11 +32,20 @@ import "package:nexus/models/sync_data.dart"; import "package:nexus/models/sync_status.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:path_provider/path_provider.dart"; class ClientController extends AsyncNotifier { @override Future build() async { - final handle = await Isolate.run(GomuksInit); + final Pointer root; + if (Platform.isAndroid) { + final dir = await getApplicationSupportDirectory(); + root = "${dir.path}/gomuks".toNativeUtf8().cast(); + } else { + root = nullptr.cast(); + } + + final handle = GomuksInit(root); final callable = NativeCallable< @@ -143,12 +153,12 @@ class ClientController extends AsyncNotifier { Future sendMessage(SendMessageRequest request) => _sendCommand("send_message", request.toJson()); - Future verify(String recoveryKey) async { + Future verify(String recoveryKey) async { try { await _sendCommand("verify", {"recovery_key": recoveryKey}); - return true; + return null; } catch (error) { - return false; + return error.toString(); } } @@ -218,12 +228,12 @@ class ClientController extends AsyncNotifier { }); } - Future login(LoginRequest login) async { + Future login(LoginRequest login) async { try { await _sendCommand("login", login.toJson()); - return true; + return null; } catch (error) { - return false; + return error.toString(); } } diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 268a30d..80e73a0 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,25 +1,39 @@ -import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/models/event.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/models/membership.dart"; +import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/room.dart"; -class MembersController extends Notifier> { +class MembersController extends AsyncNotifier> { final Room room; MembersController(this.room); @override - IList build() => (room.state["m.room.member"]?.values ?? []) - .map( - (eventRowId) => - room.events.firstWhereOrNull((event) => event.rowId == eventRowId), - ) - .nonNulls - .where((member) => member.content["membership"] == "join") - .toIList(); + Future> build() async { + if (room.metadata == null) return const IList.empty(); - static final provider = NotifierProvider.family - .autoDispose, Room>( + final state = await ref + .watch(ClientController.provider.notifier) + .getRoomState( + GetRoomStateRequest( + roomId: room.metadata!.id, + fetchMembers: room.metadata!.hasMemberList == false, + includeMembers: true, + ), + ); + + return state.nonNulls + .where((member) => member.content["membership"] == "join") + .map( + (membership) => + Membership.fromContent(membership.content, membership.stateKey!), + ) + .toIList(); + } + + static final provider = + AsyncNotifierProvider.family, Room>( MembersController.new, ); } diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index f3ef13b..d84aabb 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -2,9 +2,8 @@ import "package:collection/collection.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/members_controller.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/message_config.dart"; +import "package:nexus/models/configs/message_config.dart"; class MessageController extends AsyncNotifier { final MessageConfig config; @@ -27,12 +26,6 @@ class MessageController extends AsyncNotifier { if (!ref.mounted) return null; - final members = ref.read(MembersController.provider(config.room)); - final author = members.firstWhereOrNull( - (member) => member.stateKey == event.authorId, - ); - if (!ref.mounted) return null; - final content = (event.decrypted ?? event.content); final type = (config.event.decryptedType ?? config.event.type); final newContent = content["m.new_content"] as Map?; @@ -52,14 +45,11 @@ class MessageController extends AsyncNotifier { "timelineId": event.timelineRowId, "big": event.localContent?.bigEmoji == true, "eventType": type, - "avatarUrl": author?.content["avatar_url"], + "pmp": event.content["com.beeper.per_message_profile"], "editSource": event.localContent?.editSource ?? newContent?["body"] ?? content["body"], - "displayName": author?.content["displayname"]?.isNotEmpty == true - ? author?.content["displayname"] - : event.authorId.substring(1).split(":")[0], "txnId": config.event.transactionId, }; diff --git a/lib/controllers/messages_controller.dart b/lib/controllers/messages_controller.dart index 83bd815..28885fb 100644 --- a/lib/controllers/messages_controller.dart +++ b/lib/controllers/messages_controller.dart @@ -2,8 +2,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/message_controller.dart"; -import "package:nexus/models/message_config.dart"; -import "package:nexus/models/messages_config.dart"; +import "package:nexus/models/configs/message_config.dart"; +import "package:nexus/models/configs/messages_config.dart"; class MessagesController extends AsyncNotifier> { final MessagesConfig config; diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 4a4dba2..d737154 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,5 +1,4 @@ import "dart:async"; - import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -11,8 +10,8 @@ import "package:nexus/controllers/message_controller.dart"; import "package:nexus/controllers/messages_controller.dart"; import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; -import "package:nexus/models/message_config.dart"; -import "package:nexus/models/messages_config.dart"; +import "package:nexus/models/configs/messages_config.dart"; +import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; @@ -31,11 +30,7 @@ class RoomChatController extends AsyncNotifier { if (room == null) return InMemoryChatController(); final state = await client.getRoomState( - GetRoomStateRequest( - roomId: roomId, - fetchMembers: room.metadata?.hasMemberList == false, - includeMembers: true, - ), + GetRoomStateRequest(roomId: roomId), ); ref diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 0945644..3c6e287 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -36,6 +36,7 @@ class RoomsController extends Notifier> { return acc.add( roomId, existing?.copyWith( + hasMore: incoming.hasMore, metadata: incoming.metadata ?? existing.metadata, events: events!, state: incoming.state.entries.fold( diff --git a/lib/helpers/extensions/join_room_with_snackbars.dart b/lib/helpers/extensions/join_room_with_snackbars.dart index 05b045d..eaa0659 100644 --- a/lib/helpers/extensions/join_room_with_snackbars.dart +++ b/lib/helpers/extensions/join_room_with_snackbars.dart @@ -15,6 +15,7 @@ extension JoinRoomWithSnackbars on ClientController { WidgetRef ref, ) async { final roomIdOrAlias = roomAlias.mention ?? roomAlias; + // TODO: Parse vias properly final scaffoldMessenger = ScaffoldMessenger.of(context); diff --git a/lib/main.dart b/lib/main.dart index 5ad6c24..192ca29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,6 @@ import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; import "package:flutter/material.dart"; import "package:dynamic_system_colors/dynamic_system_colors.dart"; -import "package:window_size/window_size.dart"; final GlobalKey navigatorKey = GlobalKey(); @@ -59,14 +58,11 @@ void showError(Object error, [StackTrace? stackTrace]) { void main() async { WidgetsFlutterBinding.ensureInitialized(); - await windowManager.ensureInitialized(); - await windowManager.waitUntilReadyToShow( - WindowOptions(titleBarStyle: TitleBarStyle.hidden), - ); - - if (Platform.isLinux) { - setWindowMinSize(const Size.square(500)); - } else { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + await windowManager.ensureInitialized(); + await windowManager.waitUntilReadyToShow( + WindowOptions(titleBarStyle: TitleBarStyle.hidden), + ); await windowManager.setMinimumSize(Size.square(500)); } diff --git a/lib/models/configs/author_config.dart b/lib/models/configs/author_config.dart new file mode 100644 index 0000000..af63c63 --- /dev/null +++ b/lib/models/configs/author_config.dart @@ -0,0 +1,14 @@ +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/room.dart"; +part "author_config.freezed.dart"; +part "author_config.g.dart"; + +@freezed +abstract class AuthorConfig with _$AuthorConfig { + const factory AuthorConfig({required Message message, required Room room}) = + _AuthorConfig; + + factory AuthorConfig.fromJson(Map json) => + _$AuthorConfigFromJson(json); +} diff --git a/lib/models/message_config.dart b/lib/models/configs/message_config.dart similarity index 100% rename from lib/models/message_config.dart rename to lib/models/configs/message_config.dart diff --git a/lib/models/messages_config.dart b/lib/models/configs/messages_config.dart similarity index 100% rename from lib/models/messages_config.dart rename to lib/models/configs/messages_config.dart diff --git a/lib/models/membership.dart b/lib/models/membership.dart new file mode 100644 index 0000000..ec18be7 --- /dev/null +++ b/lib/models/membership.dart @@ -0,0 +1,22 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +part "membership.freezed.dart"; + +@freezed +abstract class Membership with _$Membership { + const Membership._(); + const factory Membership({ + required Uri? avatarUrl, + required String displayName, + required String userId, + }) = _Membership; + + factory Membership.fromContent( + IMap content, + String userId, + ) => Membership( + avatarUrl: Uri.tryParse(content["avatar_url"] ?? ""), + userId: userId, + displayName: content["displayname"] ?? userId.substring(1).split(":").first, + ); +} diff --git a/lib/models/requests/get_room_state_request.dart b/lib/models/requests/get_room_state_request.dart index a154d5f..de66b72 100644 --- a/lib/models/requests/get_room_state_request.dart +++ b/lib/models/requests/get_room_state_request.dart @@ -6,7 +6,7 @@ part "get_room_state_request.g.dart"; abstract class GetRoomStateRequest with _$GetRoomStateRequest { const factory GetRoomStateRequest({ required String roomId, - required bool fetchMembers, + @Default(false) bool fetchMembers, @Default(false) bool includeMembers, }) = _GetRoomStateRequest; diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index ee2f4d0..7aa8156 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -16,7 +16,7 @@ class ChatPage extends ConsumerWidget { body: Builder( builder: (context) => Row( children: [ - if (isDesktop) Sidebar(), + if (isDesktop) Sidebar(isDesktop: isDesktop), Expanded( child: RoomChat( isDesktop: isDesktop, @@ -26,7 +26,7 @@ class ChatPage extends ConsumerWidget { ], ), ), - drawer: isDesktop ? null : Sidebar(), + drawer: isDesktop ? null : Sidebar(isDesktop: isDesktop), ); }, ); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index bd41d51..fda53d0 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -175,7 +175,7 @@ class LoginPage extends HookConsumerWidget { ElevatedButton( onPressed: () async { isLoading.value = true; - final succeeded = await client.login( + final error = await client.login( LoginRequest( username: username.text, password: password.text, @@ -183,11 +183,11 @@ class LoginPage extends HookConsumerWidget { ), ); - if (!succeeded && context.mounted) { + if (error != null && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "Login failed. Is your password right?", + "Login failed. Is your password right?\nError: $error", style: TextStyle( color: theme.colorScheme.onErrorContainer, ), diff --git a/lib/pages/verify_page.dart b/lib/pages/verify_page.dart index 1011f80..962701c 100644 --- a/lib/pages/verify_page.dart +++ b/lib/pages/verify_page.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/form_text_input.dart"; class VerifyPage extends HookConsumerWidget { @@ -11,72 +12,75 @@ class VerifyPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final passphraseController = useTextEditingController(); final isVerifying = useState(false); - return AlertDialog( - title: Text("Verify"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enter your recovery key or passphrase below to unlock encrypted messages.\nYour passphrase is usually not the same as your password.", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - autofocus: true, - capitalize: true, - controller: passphraseController, - obscure: true, - title: "Recovery Key or Passphrase", + return Scaffold( + appBar: Appbar(), + body: AlertDialog( + title: Text("Verify"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your recovery key or passphrase below to unlock encrypted messages.\nYour passphrase is usually not the same as your password.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + autofocus: true, + capitalize: true, + controller: passphraseController, + obscure: true, + title: "Recovery Key or Passphrase", + ), + ], + ), + actions: [ + TextButton( + onPressed: isVerifying.value + ? null + : () async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final snackbar = scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + "Attempting to verify with recovery key...", + ), + duration: Duration(days: 999), + ), + ); + + isVerifying.value = true; + + final error = await ref + .watch(ClientController.provider.notifier) + .verify(passphraseController.text); + + snackbar.close(); + if (error != null) { + isVerifying.value = false; + if (context.mounted) { + scaffoldMessenger.showSnackBar( + SnackBar( + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, + content: Text( + "Verification failed. Is your passphrase correct?\nError: $error", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onErrorContainer, + ), + ), + ), + ); + } + } + }, + child: Text("Verify"), ), ], ), - actions: [ - TextButton( - onPressed: isVerifying.value - ? null - : () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final snackbar = scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "Attempting to verify with recovery key...", - ), - duration: Duration(days: 999), - ), - ); - - isVerifying.value = true; - - final success = await ref - .watch(ClientController.provider.notifier) - .verify(passphraseController.text); - - snackbar.close(); - if (!success) { - isVerifying.value = false; - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - content: Text( - "Verification failed. Is your passphrase correct?", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } - }, - child: Text("Verify"), - ), - ], ); } } diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 5b14244..aae6c13 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -35,15 +35,14 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { } return GestureDetector( - behavior: HitTestBehavior.translucent, - onDoubleTap: maximize, onPanStart: (_) => windowManager.startDragging(), child: AppBar( leading: leading, backgroundColor: backgroundColor, scrolledUnderElevation: scrolledUnderElevation, actionsPadding: const EdgeInsets.symmetric(horizontal: 8), - title: title, + title: IgnorePointer(child: title), + flexibleSpace: GestureDetector(onDoubleTap: maximize), actions: [ ...actions, if (!(Platform.isAndroid || Platform.isIOS)) ...[ diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/composer/chat_box.dart similarity index 89% rename from lib/widgets/chat_page/chat_box.dart rename to lib/widgets/chat_page/composer/chat_box.dart index b9e7dbb..a688fa7 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/composer/chat_box.dart @@ -1,4 +1,3 @@ -import "dart:io"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -8,8 +7,8 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/widgets/chat_page/mention_overlay.dart"; -import "package:nexus/widgets/chat_page/relation_preview.dart"; +import "package:nexus/widgets/chat_page/composer/mention_overlay.dart"; +import "package:nexus/widgets/chat_page/composer/relation_preview.dart"; class ChatBox extends HookConsumerWidget { final Message? relatedMessage; @@ -55,20 +54,15 @@ class ChatBox extends HookConsumerWidget { final node = useFocusNode( onKeyEvent: (_, event) { - if (event is KeyDownEvent && !Platform.isAndroid && !Platform.isIOS) { - if (event.logicalKey == LogicalKeyboardKey.enter && - !HardwareKeyboard.instance.isShiftPressed) { - send(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - onDismiss(); - return KeyEventResult.handled; - } + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + onDismiss(); + return KeyEventResult.handled; } return KeyEventResult.ignored; }, - )..requestFocus(); + ); final style = TextStyle( color: theme.colorScheme.primary, @@ -86,10 +80,11 @@ class ChatBox extends HookConsumerWidget { child: Column( children: [ RelationPreview( + relatedMessage, + room: room, shouldMention: shouldMention.value, toggleShouldMention: () => shouldMention.value = !shouldMention.value, - relatedMessage: relatedMessage, relationType: relationType, onDismiss: onDismiss, ), @@ -155,7 +150,9 @@ class ChatBox extends HookConsumerWidget { ), controller: controller.value, key: key, - autofocus: true, + // TODO: Setting for send on enter on / off + onFieldSubmitted: (_) => send(), + textInputAction: TextInputAction.done, focusNode: node, ), ), diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/composer/mention_overlay.dart similarity index 56% rename from lib/widgets/chat_page/mention_overlay.dart rename to lib/widgets/chat_page/composer/mention_overlay.dart index 9858574..d95253d 100644 --- a/lib/widgets/chat_page/mention_overlay.dart +++ b/lib/widgets/chat_page/composer/mention_overlay.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/loading.dart"; @@ -31,55 +32,47 @@ class MentionOverlay extends ConsumerWidget { color: Theme.of(context).colorScheme.surfaceContainerHigh, padding: EdgeInsets.all(8), child: switch (triggerCharacter) { - "@" => Consumer( - builder: (_, ref, _) { - final members = ref.watch(MembersController.provider(room)); - return ListView( - children: - (query.isEmpty - ? members - : members.where( - (member) => - member.stateKey?.toLowerCase().contains( - query.toLowerCase(), - ) == - true || - (member.content["displayname"] as String?) - ?.toLowerCase() - .contains(query.toLowerCase()) == - true, - )) - .map( - (member) => ListTile( - leading: AvatarOrHash( - Uri.tryParse( - member.content["avatar_url"] ?? "", - ), - member.content["displayname"] ?? "", - ), - title: Text( - member.content["displayname"] as String? ?? - member.stateKey ?? - "Unknown User", - ), - subtitle: member.stateKey != null - ? Text(member.stateKey!) - : null, - onTap: () => addTag( - id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})", - name: - member.stateKey - ?.substring(1) + "@" => + ref + .watch(MembersController.provider(room)) + .betterWhen( + data: (members) => ListView( + children: + (query.isEmpty + ? members + : members.where( + (member) => + member.userId.toLowerCase().contains( + query.toLowerCase(), + ) == + true || + member.displayName + .toLowerCase() + .contains( + query.toLowerCase(), + ) == + true, + )) + .map( + (member) => ListTile( + leading: AvatarOrHash( + member.avatarUrl, + member.displayName, + ), + title: Text(member.displayName), + subtitle: Text(member.userId), + onTap: () => addTag( + id: "[@${member.displayName}](https://matrix.to/#/${member.userId})", + name: member.userId + .substring(1) .split(":") - .first ?? - "Unknown User", - ), - ), - ) - .toList(), - ); - }, - ), + .first, + ), + ), + ) + .toList(), + ), + ), "#" => ListView( children: (query.isEmpty diff --git a/lib/widgets/chat_page/relation_preview.dart b/lib/widgets/chat_page/composer/relation_preview.dart similarity index 83% rename from lib/widgets/chat_page/relation_preview.dart rename to lib/widgets/chat_page/composer/relation_preview.dart index 7aa3ae8..7fded20 100644 --- a/lib/widgets/chat_page/relation_preview.dart +++ b/lib/widgets/chat_page/composer/relation_preview.dart @@ -2,7 +2,9 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; class RelationPreview extends ConsumerWidget { final Message? relatedMessage; @@ -10,8 +12,11 @@ class RelationPreview extends ConsumerWidget { final VoidCallback onDismiss; final bool shouldMention; final VoidCallback toggleShouldMention; - const RelationPreview({ - required this.relatedMessage, + final Room room; + + const RelationPreview( + this.relatedMessage, { + required this.room, required this.relationType, required this.onDismiss, required this.shouldMention, @@ -36,14 +41,10 @@ class RelationPreview extends ConsumerWidget { "Editing message:", style: TextStyle(fontWeight: FontWeight.bold), ), - AvatarOrHash( - Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""), - relatedMessage?.metadata?["displayName"]?.toString() ?? "", - height: 16, - ), - Text( - relatedMessage!.metadata?["displayName"] ?? - relatedMessage!.authorId, + MessageAvatar(relatedMessage!, room), + MessageDisplayname( + relatedMessage!, + room, style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart index 1e1ab82..dcc1d49 100644 --- a/lib/widgets/chat_page/html/html.dart +++ b/lib/widgets/chat_page/html/html.dart @@ -22,6 +22,10 @@ class Html extends ConsumerWidget { html, textStyle: textStyle, customWidgetBuilder: (element) { + if (element.attributes.keys.contains("data-mx-profile-fallback")) { + return SizedBox.shrink(); + } + if (element.attributes.keys.contains("data-mx-spoiler")) { return InlineCustomWidget(child: SpoilerText(text: element.text)); } diff --git a/lib/widgets/chat_page/lazy_loading/message_avatar.dart b/lib/widgets/chat_page/lazy_loading/message_avatar.dart new file mode 100644 index 0000000..71fcf84 --- /dev/null +++ b/lib/widgets/chat_page/lazy_loading/message_avatar.dart @@ -0,0 +1,30 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/author_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/configs/author_config.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class MessageAvatar extends ConsumerWidget { + final Message message; + final Room room; + final double height; + const MessageAvatar(this.message, this.room, {this.height = 16, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref + .watch( + AuthorController.provider(AuthorConfig(room: room, message: message)), + ) + .betterWhen( + data: (membership) => AvatarOrHash( + membership.avatarUrl, + membership.displayName, + height: height, + ), + loading: () => + AvatarOrHash(null, message.authorId.substring(1), height: height), + ); +} diff --git a/lib/widgets/chat_page/lazy_loading/message_displayname.dart b/lib/widgets/chat_page/lazy_loading/message_displayname.dart new file mode 100644 index 0000000..7c10df3 --- /dev/null +++ b/lib/widgets/chat_page/lazy_loading/message_displayname.dart @@ -0,0 +1,28 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/author_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/configs/author_config.dart"; +import "package:nexus/models/room.dart"; + +class MessageDisplayname extends ConsumerWidget { + final Message message; + final Room room; + final TextStyle? style; + const MessageDisplayname(this.message, this.room, {this.style, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => ref + .watch( + AuthorController.provider(AuthorConfig(room: room, message: message)), + ) + .betterWhen( + data: (membership) => Text( + "${membership.displayName} ${message.metadata?["pmp"] == null ? "" : "(via ${message.authorId})"}", + style: style, + overflow: TextOverflow.ellipsis, + ), + loading: () => Text(""), + ); +} diff --git a/lib/widgets/chat_page/member_list.dart b/lib/widgets/chat_page/member_list.dart index 24d22e4..8cdbbb9 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,6 +1,7 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; @@ -10,15 +11,17 @@ class MemberList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final members = ref.watch(MembersController.provider(room)); + final membersProvider = ref.watch(MembersController.provider(room)); return Drawer( shape: Border(), - child: ListView( + child: Column( children: [ AppBar( scrolledUnderElevation: 0, leading: Icon(Icons.people), - title: Text("Members (${members.length})"), + title: Text( + "Members ${membersProvider.when(data: (members) => "${members.length}", error: (_, _) => "", loading: () => "")}", + ), actionsPadding: EdgeInsets.only(right: 4), actions: [ if (Scaffold.of(context).hasEndDrawer) @@ -29,24 +32,32 @@ class MemberList extends ConsumerWidget { ), ], ), - ...members.map( - (member) => ListTile( - onTap: () => showDialog( - context: context, - builder: (context) => - Dialog(child: Text("TODO: Open member popover")), - ), - leading: AvatarOrHash( - Uri.tryParse(member.content["avatar_url"] ?? ""), - member.content["displayname"].toString(), - ), - title: Text( - member.content["displayname"].toString(), - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - member.stateKey ?? "Unknown User", - overflow: TextOverflow.ellipsis, + membersProvider.betterWhen( + data: (members) => Expanded( + child: ListView( + children: members + .map( + (member) => ListTile( + onTap: () => showDialog( + context: context, + builder: (context) => + Dialog(child: Text("TODO: Open member popover")), + ), + leading: AvatarOrHash( + member.avatarUrl, + member.displayName, + ), + title: Text( + member.displayName, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + member.userId, + overflow: TextOverflow.ellipsis, + ), + ), + ) + .toList(), ), ), ), diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart index cd30acc..b9fa2e1 100644 --- a/lib/widgets/chat_page/reply_widget.dart +++ b/lib/widgets/chat_page/reply_widget.dart @@ -1,15 +1,15 @@ -import "dart:math"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/event_controller.dart"; import "package:nexus/controllers/message_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/message_config.dart"; +import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/chat_page/html/quoted.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; typedef OnTapReply = void Function(Message message)?; @@ -61,73 +61,28 @@ class ReplyWidget extends ConsumerWidget { return SizedBox.shrink(); } - final smallerText = - message is TextMessage && - replyMessage.metadata?["body"] != null - ? replyMessage.metadata!["body"].substring( - 0, - min( - max( - max( - (message as TextMessage) - .text - .length - - (replyMessage - .metadata?["displayName"] - as String) - .length - - 5, - message - .metadata?["displayName"] - .length, - ), - 5, - ), - replyMessage.metadata!["body"].length, - ), - ) - : null; - final replyText = - (smallerText == null || - smallerText.length == - replyMessage - .metadata!["body"] - .length) - ? replyMessage.metadata!["body"] - : "$smallerText..."; - return InkWell( onTap: () => onTapReply?.call(replyMessage), child: Row( mainAxisSize: MainAxisSize.min, spacing: 8, children: [ - AvatarOrHash( - Uri.tryParse( - replyMessage.metadata?["avatarUrl"] ?? - "", - ), - replyMessage.metadata?["displayName"] ?? - "", - height: 16, - ), + MessageAvatar(replyMessage, room), Flexible( - child: Text( - replyMessage - .metadata?["displayName"] ?? - replyMessage.authorId, + child: MessageDisplayname( + replyMessage, + room, style: Theme.of(context) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.bold, ), - overflow: TextOverflow.ellipsis, ), ), Flexible( child: Text( - replyText, + replyMessage.metadata!["body"], overflow: TextOverflow.ellipsis, style: Theme.of( context, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 839109f..6b3839a 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -13,15 +13,14 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/report_request.dart"; -import "package:nexus/widgets/chat_page/chat_box.dart"; +import "package:nexus/widgets/chat_page/composer/chat_box.dart"; import "package:nexus/widgets/chat_page/image_message.dart"; import "package:nexus/widgets/chat_page/member_list.dart"; -import "package:nexus/widgets/chat_page/message_wrapper.dart"; +import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart"; -import "package:nexus/widgets/chat_page/text_message_wrapper.dart"; +import "package:nexus/widgets/chat_page/wrappers/text_message_wrapper.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; -import "package:nexus/widgets/loading.dart"; // import "package:dynamic_polls/dynamic_polls.dart"; class RoomChat extends HookConsumerWidget { @@ -233,7 +232,7 @@ class RoomChat extends HookConsumerWidget { children: getMessageOptions(message), ), builders: Builders( - loadMoreBuilder: (_) => Loading(), + loadMoreBuilder: (_) => SizedBox.shrink(), chatAnimatedListBuilder: (_, itemBuilder) => ChatAnimatedList( @@ -320,6 +319,7 @@ class RoomChat extends HookConsumerWidget { ), ), groupStatus, + room, ), systemMessageBuilder: diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index 4642a58..771a7ae 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -12,7 +12,8 @@ import "package:nexus/widgets/chat_page/room_menu.dart"; import "package:nexus/widgets/form_text_input.dart"; class Sidebar extends HookConsumerWidget { - const Sidebar({super.key}); + final bool isDesktop; + const Sidebar({required this.isDesktop, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -220,9 +221,12 @@ class Sidebar extends HookConsumerWidget { ), ) .toList(), - onDestinationSelected: (value) => selectedRoomIdNotifier.set( - selectedSpace.children[value].metadata?.id, - ), + onDestinationSelected: (value) { + selectedRoomIdNotifier.set( + selectedSpace.children[value].metadata?.id, + ); + if (!isDesktop) Navigator.of(context).pop(); + }, ), ), ), diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart similarity index 73% rename from lib/widgets/chat_page/message_wrapper.dart rename to lib/widgets/chat_page/wrappers/message_wrapper.dart index da53be0..1be6c2b 100644 --- a/lib/widgets/chat_page/message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart @@ -1,12 +1,21 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; +import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; class MessageWrapper extends StatelessWidget { final Message message; final Widget child; + final Room room; final MessageGroupStatus? groupStatus; - const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); + const MessageWrapper( + this.message, + this.child, + this.groupStatus, + this.room, { + super.key, + }); @override Widget build(BuildContext context) => ClipRRect( @@ -24,11 +33,7 @@ class MessageWrapper extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ groupStatus?.isFirst != false - ? AvatarOrHash( - Uri.parse(message.metadata?["avatarUrl"] ?? ""), - height: 40, - message.metadata?["displayName"] ?? "", - ) + ? MessageAvatar(message, room, height: 40) : SizedBox(width: 40), Expanded( child: Column( @@ -36,9 +41,9 @@ class MessageWrapper extends StatelessWidget { spacing: 4, children: [ if (groupStatus?.isFirst != false) - Text( - message.metadata?["displayName"] ?? message.authorId, - overflow: TextOverflow.ellipsis, + MessageDisplayname( + message, + room, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/chat_page/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart similarity index 97% rename from lib/widgets/chat_page/text_message_wrapper.dart rename to lib/widgets/chat_page/wrappers/text_message_wrapper.dart index 9734a34..41bc01e 100644 --- a/lib/widgets/chat_page/text_message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart @@ -3,7 +3,7 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:nexus/models/room.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; -import "package:nexus/widgets/chat_page/message_wrapper.dart"; +import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart"; class TextMessageWrapper extends StatelessWidget { @@ -109,6 +109,7 @@ class TextMessageWrapper extends StatelessWidget { ), ), groupStatus, + room, ); } } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 2e0c766..fee47c5 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "nexus") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "nexus.federated.nexus") +set(APPLICATION_ID "nexus.federated.Nexus") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f70fb6e..5485b95 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -11,7 +11,6 @@ #include #include #include -#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar = @@ -29,7 +28,4 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); - g_autoptr(FlPluginRegistrar) window_size_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); - window_size_plugin_register_with_registrar(window_size_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 78dcf40..13ef2de 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever_linux url_launcher_linux window_manager - window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/linux/nexus.federated.Nexus.desktop b/linux/nexus.federated.Nexus.desktop new file mode 100644 index 0000000..d3fa575 --- /dev/null +++ b/linux/nexus.federated.Nexus.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Nexus +GenericName=Matrix Client +Comment=A simple and user-friendly Matrix client +Exec=nexus +Icon=nexus +Terminal=false +Type=Application +Categories=Chat;Network;InstantMessaging; \ No newline at end of file diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix new file mode 100644 index 0000000..91ba95a --- /dev/null +++ b/linux/nix/devshell.nix @@ -0,0 +1,41 @@ +{ pkgs, lib }: +let + android = pkgs.androidenv.composeAndroidPackages { + toolsVersion = "26.1.1"; + platformToolsVersion = "36.0.1"; + buildToolsVersions = [ + "35.0.0" + "36.0.0" + ]; + cmakeVersions = [ "3.22.1" ]; + platformVersions = [ "36" ]; + abiVersions = [ + "armeabi-v7a" + "arm64-v8a" + ]; + includeNDK = true; + ndkVersions = [ "28.2.13676358" ]; + }; +in +pkgs.mkShell { + packages = with pkgs; [ + go + git + jdk17 + flutter + android.platform-tools + ]; + + 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; + JAVA_HOME = pkgs.jdk17; + + TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; + }; +} diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix new file mode 100644 index 0000000..ae1ddeb --- /dev/null +++ b/linux/nix/pkg/default.nix @@ -0,0 +1,44 @@ +{ + lib, + callPackage, + libclang, + flutter, + src, +}: + +flutter.buildFlutterApplication { + pname = "nexus"; + version = "0.1.0"; + inherit src; + + preBuild = '' + cp ${callPackage ./gomuks.nix { inherit src; }}/lib/* . + packageRunCustom nexus generate source/scripts test + packageRun build_runner build + ''; + + env.LIBCLANG_PATH = lib.makeLibraryPath [ libclang ]; + + autoPubspecLock = src + "/pubspec.lock"; + + gitHashes = { + window_size = "sha256-XelNtp7tpZ91QCEcvewVphNUtgQX7xrp5QP0oFo6DgM="; + dynamic_system_colors = "sha256-es6rjMK1drkqZBKYUP77yw/q5+0uLwWOEDOXRawy3Dc="; + flutter_chat_ui = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk="; + flutter_link_previewer = "sha256-4fuag7lRH5cMBFD3fUzj2K541JwXLoz8HF/4OMr3uhk="; + }; + + postInstall = '' + install -D assets/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg + install -Dm755 linux/nexus.federated.Nexus.desktop -t $out/share/applications + wrapProgram $out/bin/nexus \ + --suffix LD_LIBRARY_PATH : $out/app/nexus/lib + ''; + + meta = { + description = "A simple and user-friendly Matrix client"; + mainProgram = "nexus"; + platforms = lib.platforms.linux; + maintainers = with lib.maintainers; [ quadradical ]; + }; +} diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix new file mode 100644 index 0000000..1bc92bf --- /dev/null +++ b/linux/nix/pkg/gomuks.nix @@ -0,0 +1,31 @@ +{ + src, + buildGoModule, +}: + +buildGoModule (finalAttrs: { + pname = "gomuks-ffi"; + version = "submodule"; + + doCheck = false; + + src = "${src}/gomuks"; + + vendorHash = "sha256-zBDfBZqUoHIfZ0AajZEvSBbskjpFB7yIsomt0KYDo7Y="; + + buildPhase = '' + runHook preBuild + + go build -buildmode=c-shared -o libgomuks.so -tags goolm,noheic ./pkg/ffi + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + install -Dm0644 libgomuks.so -t $out/lib + + runHook postInstall + ''; +}) diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 58cd859..c702551 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -43,6 +43,7 @@ static void my_application_activate(GApplication* application) { } } #endif + gtk_widget_set_size_request(GTK_WIDGET(window), 500, 500); if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); diff --git a/nix/android.nix b/nix/android.nix deleted file mode 100644 index f373968..0000000 --- a/nix/android.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ - androidenv, -}: -androidenv.composeAndroidPackages { - toolsVersion = "26.1.1"; - platformToolsVersion = "36.0.1"; - buildToolsVersions = [ - "35.0.0" - "36.0.0" - ]; - cmakeVersions = [ "3.22.1" ]; - platformVersions = [ "36" ]; - abiVersions = [ - "armeabi-v7a" - "arm64-v8a" - ]; - includeNDK = true; - ndkVersions = [ "27.0.12077973" ]; - -} diff --git a/pubspec.lock b/pubspec.lock index da5de89..de807aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer_buffer - sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 + sha256: "5fcd06b0715ebeee99f03e3f437b3412249969d8d12b191ea8a1d76e42a4e4a1" url: "https://pub.dev" source: hosted - version: "0.1.11" + version: "0.3.1" analyzer_plugin: dependency: transitive description: @@ -348,10 +348,11 @@ packages: dynamic_system_colors: dependency: "direct main" description: - name: dynamic_system_colors - sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "3b61760d5e0ac1229eefde5b61247947eede4110" + url: "https://github.com/hasali19/flutter_dynamic_system_colors" + source: git version: "1.8.0" encrypt: dependency: transitive @@ -521,10 +522,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" + sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.1" flutter_svg: dependency: "direct main" description: @@ -643,10 +644,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9 + sha256: "08527ec06aaef75e4b78694e045ef0cd8346594eaf9cc18b0f866398b07b93b1" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.1" html: dependency: transitive description: @@ -1051,26 +1052,26 @@ packages: dependency: transitive description: name: riverpod - sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" + sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" + sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 url: "https://pub.dev" source: hosted - version: "1.0.0-dev.8" + version: "1.0.0-dev.9" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" + sha256: "64e8debf5b719a37d48b9785dd595d34133fdcd84b8fd07157a621c54ab2156f" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" rxdart: dependency: transitive description: @@ -1548,15 +1549,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" - window_size: - dependency: "direct main" - description: - path: "plugins/window_size" - ref: HEAD - resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 - url: "https://github.com/google/flutter-desktop-embedding" - source: git - version: "0.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3c0198d..80c3687 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,8 +21,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_riverpod: ^3.0.3 - hooks_riverpod: ^3.0.3 + flutter_riverpod: ^3.3.1 + hooks_riverpod: ^3.3.1 intl: ^0.20.1 fast_immutable_collections: ^11.0.0 path_provider: ^2.1.3 @@ -31,13 +31,11 @@ dependencies: image_picker: ^1.1.2 file_picker: ^10.3.3 path: ^1.9.0 - dynamic_system_colors: ^1.8.0 + dynamic_system_colors: + git: + url: https://github.com/hasali19/flutter_dynamic_system_colors collection: ^1.19.1 window_manager: ^0.5.1 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding - path: plugins/window_size flutter_chat_core: ^2.0.0 flyer_chat_image_message: ^2.2.2 flyer_chat_system_message: ^2.1.13 @@ -69,7 +67,7 @@ dev_dependencies: custom_lint: ^0.8.0 flutter_lints: ^6.0.0 freezed: ^3.2.3 - riverpod_lint: ^3.0.3 + riverpod_lint: ^3.1.3 flutter_launcher_icons: ^0.14.1 json_serializable: ^6.11.1 @@ -77,7 +75,7 @@ flutter_launcher_icons: ios: true android: true image_path: assets/icon.png - adaptive_icon_background: "#000000" + adaptive_icon_background: assets/background.png adaptive_icon_foreground: assets/foreground.png remove_alpha_ios: true windows: diff --git a/scripts/generate.dart b/scripts/generate.dart index b240d98..446a469 100644 --- a/scripts/generate.dart +++ b/scripts/generate.dart @@ -3,26 +3,7 @@ import "package:ffigen/ffigen.dart"; import "package:path/path.dart"; void main(List args) async { - final repoDir = Directory.fromUri( - Platform.script.resolve("../src/gomuks/source"), - ); - if (await repoDir.exists()) await repoDir.delete(recursive: true); - await repoDir.create(recursive: true); - - print("Cloning Gomuks repository..."); - final cloneResult = await Process.run("git", [ - "clone", - "--depth", - "1", - "https://mau.dev/gomuks/gomuks", - repoDir.path, - ]); - - if (cloneResult.exitCode != 0) { - throw Exception( - "Failed to clone Gomuks repository: \n${cloneResult.stderr}", - ); - } + final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks")); print("Generating FFI Bindings..."); diff --git a/scripts/generate.sh b/scripts/generate.sh deleted file mode 100755 index 6076ab8..0000000 --- a/scripts/generate.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -pushd "$(dirname "$(readlink -f "$0")")"/.. > /dev/null || exit - -mkdir -p build -touch build/lock -dart scripts/generate.dart -rm build/lock - -popd > /dev/null || exit \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 55fb066..bde1c28 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,7 +11,6 @@ #include #include #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( @@ -24,6 +23,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); - WindowSizePluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowSizePlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9333a2f..7b6b425 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever_windows url_launcher_windows window_manager - window_size ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 24405eb..3583d23 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,11 +89,11 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "nexus.federated.nexus" "\0" + VALUE "CompanyName", "nexus.federated.Nexus" "\0" VALUE "FileDescription", "nexus" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "nexus" "\0" - VALUE "LegalCopyright", "Copyright (C) 2025 nexus.federated.nexus. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 nexus.federated.Nexus. All rights reserved." "\0" VALUE "OriginalFilename", "nexus.exe" "\0" VALUE "ProductName", "nexus" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"