1
0
Fork 0
forked from Nexus/nexus

Compare commits

..

No commits in common. "a1401f20ea3238e20c15c4a0329da2c0abd09442" and "4c69831c54d422726cc92de7a5201f27ed384c52" have entirely different histories.

64 changed files with 465 additions and 948 deletions

View file

@ -1,39 +0,0 @@
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

View file

@ -1,37 +0,0 @@
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

View file

@ -1,50 +1,46 @@
name: "Build EXE" name: "Build Windows Version"
on: on:
push:
branches: ["main"]
tags: ["*"]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build-exe: build-windows:
runs-on: windows-latest runs-on: "windows-latest"
steps: steps:
- name: Checkout repository - name: "Checkout repository"
uses: actions/checkout@v6 uses: "actions/checkout@v4"
- name: "Set up Flutter"
uses: "subosito/flutter-action@v2"
- name: "Set up Rust"
uses: "dtolnay/rust-toolchain@stable"
with: with:
submodules: recursive targets: "x86_64-pc-windows-msvc"
- name: Set up Flutter - name: "Install Flutter dependencies"
uses: subosito/flutter-action@v2 run: flutter pub get
with:
flutter-version: 3.41.5
- name: Set up Go - name: "Run build_runner & build Windows EXE"
uses: actions/setup-go@v6
- name: Build with Flutter
run: | run: |
flutter pub get flutter pub run build_runner build --delete-conflicting-outputs
dart scripts/generate.dart
flutter pub run build_runner build
flutter build windows --release flutter build windows --release
- name: Upload exe zip - name: "Upload exe zip"
uses: actions/upload-artifact@v6 uses: "actions/upload-artifact@v4"
with: with:
name: windows-portable name: "windows-portable"
path: build/windows/x64/runner/Release/ path: "build/windows/x64/runner/Release/"
- name: Install Inno Setup - name: "Install Inno Setup"
run: choco install innosetup -y run: choco install innosetup -y
- name: Build Inno Setup installer - name: "Build Inno Setup installer"
run: iscc windows/installer.iss run: iscc windows/installer.iss
- name: Upload installer artifact - name: "Upload installer artifact"
uses: actions/upload-artifact@v6 uses: "actions/upload-artifact@v4"
with: with:
name: windows-installer name: "windows-installer"
path: windows/dist/Nexus-Setup.exe path: "windows/dist/Nexus-Setup.exe"

4
.gitignore vendored
View file

@ -36,9 +36,7 @@ key.properties
# Generated Files # Generated Files
*.g.dart *.g.dart
*.freezed.dart *.freezed.dart
src/
# Devel Password # Devel Password
password.txt password.txt
# Nix
/result

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "gomuks"]
path = gomuks
url = https://github.com/zachatrocity/gomuks
branch = init-root-dir

View file

@ -5,7 +5,7 @@
## Description ## Description
A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. A simple and user-friendly Matrix client made with Flutter and the Matrix Dart SDK.
## Screenshots ## Screenshots
@ -17,8 +17,8 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [ ] New logo - [ ] New logo
- [ ] Make context menus appear as bottom sheets on mobile - [ ] Make context menus appear as bottom sheets on mobile
- [x] Move from the Dart SDK to the Gomuks Backend with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2 - [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 - [ ] Allow using remote gomuks over websocket
- [ ] Platform Support - [ ] Platform Support
- [x] Linux - [x] Linux
- [x] Windows - [x] Windows
@ -54,7 +54,6 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [x] HTML/Markdown - [x] HTML/Markdown
- [x] Replies - [x] Replies
- [x] Choose ping on/off - [x] Choose ping on/off
- [ ] Per message profiles
- [ ] Attachments - [ ] Attachments
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391) - [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
- [x] Mentions - [x] Mentions
@ -65,7 +64,6 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
- [ ] GIFs using Gomuks' GIF proxies - [ ] GIFs using Gomuks' GIF proxies
- [x] Recieving - [x] Recieving
- [x] Plain text - [x] Plain text
- [x] Per message profiles
- [x] HTML - [x] HTML
- [x] Replies - [x] Replies
- [x] Viewing - [x] Viewing
@ -130,7 +128,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
First, clone and open the repo: First, clone and open the repo:
```sh ```sh
git clone --recurse-submodules https://git.federated.nexus/Henry-Hiles/nexus git clone https://git.federated.nexus/Henry-Hiles/nexus
cd nexus cd nexus
``` ```
@ -138,12 +136,12 @@ cd nexus
#### Linux #### Linux
- With Nix: Either use direnv and `direnv allow`, or `nix flake develop` - With Nix: Either use direnv, or `nix flake develop`
- Without Nix: Install Flutter, Go, Olm, Git, Clang, and GLibc. - Without Nix: Install Flutter, Go, Olm, Git, Clang, and GLibc.
#### Windows / MacOS #### Windows / MacOS
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. 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.
### Set up Flutter ### Set up Flutter
@ -153,7 +151,13 @@ Get dependencies:
flutter pub get flutter pub get
``` ```
Generate Gomuks bindings: Get dependencies:
```sh
flutter pub get
```
Clone Gomuks and generate bindings:
```sh ```sh
scripts/generate.sh scripts/generate.sh

View file

@ -39,11 +39,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions {
// do we want to update.. eventually?
jvmTarget = "17"
}
defaultConfig { defaultConfig {
applicationId = "nexus.federated.Nexus" applicationId = "nexus.federated.Nexus"
minSdk = 29 minSdk = 29
@ -55,8 +50,7 @@ android {
signingConfigs { signingConfigs {
release { release {
keyAlias "key" keyAlias "key"
def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH") storeFile keystoreProperties['path'] ? file(keystoreProperties['path']) : file(System.getenv("KEYSTORE_PATH"))
storeFile storePath ? file(storePath) : null
keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
} }

View file

@ -10,7 +10,7 @@
android:label="Nexus" android:label="Nexus"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/nexus_round"
android:allowBackup="false" android:allowBackup="false"
android:fullBackupContent="false"> android:fullBackupContent="false">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View file

@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
sodipodi:docname="background.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="1.0847363"
inkscape:cx="57.156749"
inkscape:cy="214.33781"
inkscape:window-width="1904"
inkscape:window-height="971"
inkscape:window-x="35"
inkscape:window-y="32"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1"><linearGradient
id="linearGradient10"
inkscape:collect="always"><stop
style="stop-color:#c7a312;stop-opacity:1;"
offset="0"
id="stop10" /><stop
style="stop-color:#26a0b3;stop-opacity:1;"
offset="1"
id="stop11" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient10"
id="linearGradient11"
x1="20.031296"
y1="32.697563"
x2="90.709213"
y2="66.3423"
gradientUnits="userSpaceOnUse" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><rect
style="fill:url(#linearGradient11);fill-opacity:1;stroke:none;stroke-width:7.99999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="rect10"
width="100"
height="100"
x="0"
y="0"
ry="28.294127" /></g></svg>

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

BIN
assets/reply-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/reply.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

81
flake.lock generated
View file

@ -18,54 +18,17 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1773389992, "lastModified": 1767640445,
"narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "nixos",
"ref": "nixos-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
@ -86,42 +49,10 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nix2flatpak": "nix2flatpak", "nixpkgs": "nixpkgs"
"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"
} }
} }
}, },

View file

@ -2,10 +2,8 @@
description = "Nexus Flutter Flake"; description = "Nexus Flutter Flake";
inputs = { inputs = {
self.submodules = true;
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.url = "github:hercules-ci/flake-parts";
nix2flatpak.url = "github:neobrain/nix2flatpak";
}; };
outputs = outputs =
@ -35,42 +33,36 @@
_module.args.pkgs = import nixpkgs { _module.args.pkgs = import nixpkgs {
inherit system; inherit system;
config = { config = {
permittedInsecurePackages = [ "olm-3.2.16" ];
android_sdk.accept_license = true; android_sdk.accept_license = true;
allowUnfree = true; allowUnfree = true;
}; };
}; };
packages = devShells =
let let
default = pkgs.callPackage ./linux/nix/pkg { packages = with pkgs; [
src = self; 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 ];
}; };
in in
{ {
inherit default; default = pkgs.mkShell {
inherit env;
flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak { packages = packages ++ [
appName = "Nexus"; pkgs.flutter
developer = "QuadRadical";
appId = "nexus.federated.Nexus";
package = default;
runtime = "org.gnome.Platform/49";
permissions = {
share = [ "network" ];
sockets = [
"fallback-x11"
"wayland"
]; ];
devices = [ "dri" ];
};
}; };
gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix { nix = pkgs.mkShell { inherit packages env; };
src = self;
}; };
}; };
devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { };
};
}; };
} }

1
gomuks

@ -1 +0,0 @@
Subproject commit daa0ba028e7d89ba9fc7580fc8099348e6145cb3

View file

@ -3,12 +3,11 @@ import "package:hooks/hooks.dart";
import "package:code_assets/code_assets.dart"; import "package:code_assets/code_assets.dart";
Future<void> main(List<String> args) => build(args, (input, output) async { Future<void> main(List<String> args) => build(args, (input, output) async {
final codeConfig = input.config.code; final buildDir = input.packageRoot.resolve("src/");
final targetOS = codeConfig.targetOS; if (await File(buildDir.resolve("lock").toFilePath()).exists()) return;
final targetArch = codeConfig.targetArchitecture;
final targetOS = input.config.code.targetOS;
String libFileName; String libFileName;
Map<String, String> env = {};
switch (targetOS) { switch (targetOS) {
case OS.linux: case OS.linux:
libFileName = "libgomuks.so"; libFileName = "libgomuks.so";
@ -18,60 +17,24 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
break; break;
case OS.windows: case OS.windows:
libFileName = "libgomuks.dll"; 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; break;
default: default:
throw UnsupportedError("Unsupported OS: $targetOS"); throw UnsupportedError("Unsupported OS: $targetOS");
} }
var libFile = input.packageRoot.resolve(libFileName); final gomuksBuildDir = buildDir.resolve("gomuks/");
final gomuksBuildDir = input.packageRoot.resolve("gomuks/"); final libFile = gomuksBuildDir.resolve(libFileName);
if (!(await File.fromUri(libFile).exists())) { print("Building Gomuks shared library $libFileName from source...");
final buildDir = input.packageRoot.resolve("build/"); final result = await Process.run("go", [
libFile = buildDir.resolve("${targetArch.name}/$libFileName"); "build",
"-o",
// goheif/dav1d supported on Android would need to fix upstream libFile.path,
final tags = targetOS == OS.android ? "goolm,noheic" : "goolm"; "-buildmode=c-shared",
], workingDirectory: gomuksBuildDir.resolve("source/pkg/ffi/").toFilePath());
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) { if (result.exitCode != 0) {
throw Exception( throw Exception("Failed to build Gomuks shared library\n${result.stderr}");
"Failed to build Gomuks shared library\n${result.stderr}",
);
}
} }
final generatedFile = "src/third_party/gomuks.g.dart"; final generatedFile = "src/third_party/gomuks.g.dart";
@ -89,43 +52,3 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
..dependencies.add(gomuksBuildDir); ..dependencies.add(gomuksBuildDir);
print("Done!"); print("Done!");
}); });
Future<String?> _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");
}
}

View file

@ -387,7 +387,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -519,7 +519,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -545,7 +545,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus; PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View file

@ -1,44 +0,0 @@
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<Membership> {
final AuthorConfig config;
AuthorController(this.config);
@override
Future<Membership> 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, Membership, AuthorConfig>(
AuthorController.new,
);
}

View file

@ -1,6 +1,5 @@
import "dart:developer"; import "dart:developer";
import "dart:ffi"; import "dart:ffi";
import "dart:io";
import "dart:isolate"; import "dart:isolate";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
@ -32,20 +31,11 @@ import "package:nexus/models/sync_data.dart";
import "package:nexus/models/sync_status.dart"; import "package:nexus/models/sync_status.dart";
import "package:nexus/src/third_party/gomuks.g.dart"; import "package:nexus/src/third_party/gomuks.g.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path_provider/path_provider.dart";
class ClientController extends AsyncNotifier<int> { class ClientController extends AsyncNotifier<int> {
@override @override
Future<int> build() async { Future<int> build() async {
final Pointer<Char> root; final handle = await Isolate.run(GomuksInit);
if (Platform.isAndroid) {
final dir = await getApplicationSupportDirectory();
root = "${dir.path}/gomuks".toNativeUtf8().cast();
} else {
root = nullptr.cast();
}
final handle = GomuksInit(root);
final callable = final callable =
NativeCallable< NativeCallable<
@ -153,12 +143,12 @@ class ClientController extends AsyncNotifier<int> {
Future<void> sendMessage(SendMessageRequest request) => Future<void> sendMessage(SendMessageRequest request) =>
_sendCommand("send_message", request.toJson()); _sendCommand("send_message", request.toJson());
Future<String?> verify(String recoveryKey) async { Future<bool> verify(String recoveryKey) async {
try { try {
await _sendCommand("verify", {"recovery_key": recoveryKey}); await _sendCommand("verify", {"recovery_key": recoveryKey});
return null; return true;
} catch (error) { } catch (error) {
return error.toString(); return false;
} }
} }
@ -228,12 +218,12 @@ class ClientController extends AsyncNotifier<int> {
}); });
} }
Future<String?> login(LoginRequest login) async { Future<bool> login(LoginRequest login) async {
try { try {
await _sendCommand("login", login.toJson()); await _sendCommand("login", login.toJson());
return null; return true;
} catch (error) { } catch (error) {
return error.toString(); return false;
} }
} }

View file

@ -1,39 +1,25 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/membership.dart";
import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
class MembersController extends AsyncNotifier<IList<Membership>> { class MembersController extends Notifier<IList<Event>> {
final Room room; final Room room;
MembersController(this.room); MembersController(this.room);
@override @override
Future<IList<Membership>> build() async { IList<Event> build() => (room.state["m.room.member"]?.values ?? [])
if (room.metadata == null) return const IList.empty();
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( .map(
(membership) => (eventRowId) =>
Membership.fromContent(membership.content, membership.stateKey!), room.events.firstWhereOrNull((event) => event.rowId == eventRowId),
) )
.nonNulls
.where((member) => member.content["membership"] == "join")
.toIList(); .toIList();
}
static final provider = static final provider = NotifierProvider.family
AsyncNotifierProvider.family<MembersController, IList<Membership>, Room>( .autoDispose<MembersController, IList<Event>, Room>(
MembersController.new, MembersController.new,
); );
} }

View file

@ -2,8 +2,9 @@ import "package:collection/collection.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.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/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/message_config.dart";
class MessageController extends AsyncNotifier<Message?> { class MessageController extends AsyncNotifier<Message?> {
final MessageConfig config; final MessageConfig config;
@ -26,6 +27,12 @@ class MessageController extends AsyncNotifier<Message?> {
if (!ref.mounted) return null; 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 content = (event.decrypted ?? event.content);
final type = (config.event.decryptedType ?? config.event.type); final type = (config.event.decryptedType ?? config.event.type);
final newContent = content["m.new_content"] as Map?; final newContent = content["m.new_content"] as Map?;
@ -45,11 +52,14 @@ class MessageController extends AsyncNotifier<Message?> {
"timelineId": event.timelineRowId, "timelineId": event.timelineRowId,
"big": event.localContent?.bigEmoji == true, "big": event.localContent?.bigEmoji == true,
"eventType": type, "eventType": type,
"pmp": event.content["com.beeper.per_message_profile"], "avatarUrl": author?.content["avatar_url"],
"editSource": "editSource":
event.localContent?.editSource ?? event.localContent?.editSource ??
newContent?["body"] ?? newContent?["body"] ??
content["body"], content["body"],
"displayName": author?.content["displayname"]?.isNotEmpty == true
? author?.content["displayname"]
: event.authorId.substring(1).split(":")[0],
"txnId": config.event.transactionId, "txnId": config.event.transactionId,
}; };

View file

@ -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_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/message_controller.dart"; import "package:nexus/controllers/message_controller.dart";
import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/message_config.dart";
import "package:nexus/models/configs/messages_config.dart"; import "package:nexus/models/messages_config.dart";
class MessagesController extends AsyncNotifier<IList<Message>> { class MessagesController extends AsyncNotifier<IList<Message>> {
final MessagesConfig config; final MessagesConfig config;

View file

@ -1,4 +1,5 @@
import "dart:async"; import "dart:async";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
@ -10,8 +11,8 @@ import "package:nexus/controllers/message_controller.dart";
import "package:nexus/controllers/messages_controller.dart"; import "package:nexus/controllers/messages_controller.dart";
import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/models/configs/messages_config.dart"; import "package:nexus/models/message_config.dart";
import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/messages_config.dart";
import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/paginate_request.dart";
import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/requests/redact_event_request.dart";
@ -30,7 +31,11 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
if (room == null) return InMemoryChatController(); if (room == null) return InMemoryChatController();
final state = await client.getRoomState( final state = await client.getRoomState(
GetRoomStateRequest(roomId: roomId), GetRoomStateRequest(
roomId: roomId,
fetchMembers: room.metadata?.hasMemberList == false,
includeMembers: true,
),
); );
ref ref

View file

@ -36,7 +36,6 @@ class RoomsController extends Notifier<IMap<String, Room>> {
return acc.add( return acc.add(
roomId, roomId,
existing?.copyWith( existing?.copyWith(
hasMore: incoming.hasMore,
metadata: incoming.metadata ?? existing.metadata, metadata: incoming.metadata ?? existing.metadata,
events: events!, events: events!,
state: incoming.state.entries.fold( state: incoming.state.entries.fold(

View file

@ -15,7 +15,6 @@ extension JoinRoomWithSnackbars on ClientController {
WidgetRef ref, WidgetRef ref,
) async { ) async {
final roomIdOrAlias = roomAlias.mention ?? roomAlias; final roomIdOrAlias = roomAlias.mention ?? roomAlias;
// TODO: Parse vias properly
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);

View file

@ -18,6 +18,7 @@ import "package:nexus/widgets/loading.dart";
import "package:window_manager/window_manager.dart"; import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:dynamic_system_colors/dynamic_system_colors.dart"; import "package:dynamic_system_colors/dynamic_system_colors.dart";
import "package:window_size/window_size.dart";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -58,11 +59,14 @@ void showError(Object error, [StackTrace? stackTrace]) {
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await windowManager.waitUntilReadyToShow( await windowManager.waitUntilReadyToShow(
WindowOptions(titleBarStyle: TitleBarStyle.hidden), WindowOptions(titleBarStyle: TitleBarStyle.hidden),
); );
if (Platform.isLinux) {
setWindowMinSize(const Size.square(500));
} else {
await windowManager.setMinimumSize(Size.square(500)); await windowManager.setMinimumSize(Size.square(500));
} }

View file

@ -1,14 +0,0 @@
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<String, Object?> json) =>
_$AuthorConfigFromJson(json);
}

View file

@ -1,22 +0,0 @@
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<String, dynamic> content,
String userId,
) => Membership(
avatarUrl: Uri.tryParse(content["avatar_url"] ?? ""),
userId: userId,
displayName: content["displayname"] ?? userId.substring(1).split(":").first,
);
}

View file

@ -6,7 +6,7 @@ part "get_room_state_request.g.dart";
abstract class GetRoomStateRequest with _$GetRoomStateRequest { abstract class GetRoomStateRequest with _$GetRoomStateRequest {
const factory GetRoomStateRequest({ const factory GetRoomStateRequest({
required String roomId, required String roomId,
@Default(false) bool fetchMembers, required bool fetchMembers,
@Default(false) bool includeMembers, @Default(false) bool includeMembers,
}) = _GetRoomStateRequest; }) = _GetRoomStateRequest;

View file

@ -16,7 +16,7 @@ class ChatPage extends ConsumerWidget {
body: Builder( body: Builder(
builder: (context) => Row( builder: (context) => Row(
children: [ children: [
if (isDesktop) Sidebar(isDesktop: isDesktop), if (isDesktop) Sidebar(),
Expanded( Expanded(
child: RoomChat( child: RoomChat(
isDesktop: isDesktop, isDesktop: isDesktop,
@ -26,7 +26,7 @@ class ChatPage extends ConsumerWidget {
], ],
), ),
), ),
drawer: isDesktop ? null : Sidebar(isDesktop: isDesktop), drawer: isDesktop ? null : Sidebar(),
); );
}, },
); );

View file

@ -175,7 +175,7 @@ class LoginPage extends HookConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
isLoading.value = true; isLoading.value = true;
final error = await client.login( final succeeded = await client.login(
LoginRequest( LoginRequest(
username: username.text, username: username.text,
password: password.text, password: password.text,
@ -183,11 +183,11 @@ class LoginPage extends HookConsumerWidget {
), ),
); );
if (error != null && context.mounted) { if (!succeeded && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
"Login failed. Is your password right?\nError: $error", "Login failed. Is your password right?",
style: TextStyle( style: TextStyle(
color: theme.colorScheme.onErrorContainer, color: theme.colorScheme.onErrorContainer,
), ),

View file

@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/form_text_input.dart";
class VerifyPage extends HookConsumerWidget { class VerifyPage extends HookConsumerWidget {
@ -12,9 +11,7 @@ class VerifyPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final passphraseController = useTextEditingController(); final passphraseController = useTextEditingController();
final isVerifying = useState(false); final isVerifying = useState(false);
return Scaffold( return AlertDialog(
appBar: Appbar(),
body: AlertDialog(
title: Text("Verify"), title: Text("Verify"),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -51,12 +48,12 @@ class VerifyPage extends HookConsumerWidget {
isVerifying.value = true; isVerifying.value = true;
final error = await ref final success = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.verify(passphraseController.text); .verify(passphraseController.text);
snackbar.close(); snackbar.close();
if (error != null) { if (!success) {
isVerifying.value = false; isVerifying.value = false;
if (context.mounted) { if (context.mounted) {
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
@ -65,7 +62,7 @@ class VerifyPage extends HookConsumerWidget {
context, context,
).colorScheme.errorContainer, ).colorScheme.errorContainer,
content: Text( content: Text(
"Verification failed. Is your passphrase correct?\nError: $error", "Verification failed. Is your passphrase correct?",
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
context, context,
@ -80,7 +77,6 @@ class VerifyPage extends HookConsumerWidget {
child: Text("Verify"), child: Text("Verify"),
), ),
], ],
),
); );
} }
} }

View file

@ -35,14 +35,15 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget {
} }
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: maximize,
onPanStart: (_) => windowManager.startDragging(), onPanStart: (_) => windowManager.startDragging(),
child: AppBar( child: AppBar(
leading: leading, leading: leading,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
scrolledUnderElevation: scrolledUnderElevation, scrolledUnderElevation: scrolledUnderElevation,
actionsPadding: const EdgeInsets.symmetric(horizontal: 8), actionsPadding: const EdgeInsets.symmetric(horizontal: 8),
title: IgnorePointer(child: title), title: title,
flexibleSpace: GestureDetector(onDoubleTap: maximize),
actions: [ actions: [
...actions, ...actions,
if (!(Platform.isAndroid || Platform.isIOS)) ...[ if (!(Platform.isAndroid || Platform.isIOS)) ...[

View file

@ -1,3 +1,4 @@
import "dart:io";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter/services.dart"; import "package:flutter/services.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
@ -7,8 +8,8 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/composer/mention_overlay.dart"; import "package:nexus/widgets/chat_page/mention_overlay.dart";
import "package:nexus/widgets/chat_page/composer/relation_preview.dart"; import "package:nexus/widgets/chat_page/relation_preview.dart";
class ChatBox extends HookConsumerWidget { class ChatBox extends HookConsumerWidget {
final Message? relatedMessage; final Message? relatedMessage;
@ -54,15 +55,20 @@ class ChatBox extends HookConsumerWidget {
final node = useFocusNode( final node = useFocusNode(
onKeyEvent: (_, event) { onKeyEvent: (_, event) {
if (event is KeyDownEvent && if (event is KeyDownEvent && !Platform.isAndroid && !Platform.isIOS) {
event.logicalKey == LogicalKeyboardKey.escape) { if (event.logicalKey == LogicalKeyboardKey.enter &&
!HardwareKeyboard.instance.isShiftPressed) {
send();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
onDismiss(); onDismiss();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
}
return KeyEventResult.ignored; return KeyEventResult.ignored;
}, },
); )..requestFocus();
final style = TextStyle( final style = TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -80,11 +86,10 @@ class ChatBox extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
RelationPreview( RelationPreview(
relatedMessage,
room: room,
shouldMention: shouldMention.value, shouldMention: shouldMention.value,
toggleShouldMention: () => toggleShouldMention: () =>
shouldMention.value = !shouldMention.value, shouldMention.value = !shouldMention.value,
relatedMessage: relatedMessage,
relationType: relationType, relationType: relationType,
onDismiss: onDismiss, onDismiss: onDismiss,
), ),
@ -150,9 +155,7 @@ class ChatBox extends HookConsumerWidget {
), ),
controller: controller.value, controller: controller.value,
key: key, key: key,
// TODO: Setting for send on enter on / off autofocus: true,
onFieldSubmitted: (_) => send(),
textInputAction: TextInputAction.done,
focusNode: node, focusNode: node,
), ),
), ),

View file

@ -22,10 +22,6 @@ class Html extends ConsumerWidget {
html, html,
textStyle: textStyle, textStyle: textStyle,
customWidgetBuilder: (element) { customWidgetBuilder: (element) {
if (element.attributes.keys.contains("data-mx-profile-fallback")) {
return SizedBox.shrink();
}
if (element.attributes.keys.contains("data-mx-spoiler")) { if (element.attributes.keys.contains("data-mx-spoiler")) {
return InlineCustomWidget(child: SpoilerText(text: element.text)); return InlineCustomWidget(child: SpoilerText(text: element.text));
} }

View file

@ -1,30 +0,0 @@
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),
);
}

View file

@ -1,28 +0,0 @@
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(""),
);
}

View file

@ -1,7 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.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/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
@ -11,17 +10,15 @@ class MemberList extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final membersProvider = ref.watch(MembersController.provider(room)); final members = ref.watch(MembersController.provider(room));
return Drawer( return Drawer(
shape: Border(), shape: Border(),
child: Column( child: ListView(
children: [ children: [
AppBar( AppBar(
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
leading: Icon(Icons.people), leading: Icon(Icons.people),
title: Text( title: Text("Members (${members.length})"),
"Members ${membersProvider.when(data: (members) => "${members.length}", error: (_, _) => "", loading: () => "")}",
),
actionsPadding: EdgeInsets.only(right: 4), actionsPadding: EdgeInsets.only(right: 4),
actions: [ actions: [
if (Scaffold.of(context).hasEndDrawer) if (Scaffold.of(context).hasEndDrawer)
@ -32,11 +29,7 @@ class MemberList extends ConsumerWidget {
), ),
], ],
), ),
membersProvider.betterWhen( ...members.map(
data: (members) => Expanded(
child: ListView(
children: members
.map(
(member) => ListTile( (member) => ListTile(
onTap: () => showDialog( onTap: () => showDialog(
context: context, context: context,
@ -44,22 +37,18 @@ class MemberList extends ConsumerWidget {
Dialog(child: Text("TODO: Open member popover")), Dialog(child: Text("TODO: Open member popover")),
), ),
leading: AvatarOrHash( leading: AvatarOrHash(
member.avatarUrl, Uri.tryParse(member.content["avatar_url"] ?? ""),
member.displayName, member.content["displayname"].toString(),
), ),
title: Text( title: Text(
member.displayName, member.content["displayname"].toString(),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: Text( subtitle: Text(
member.userId, member.stateKey ?? "Unknown User",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
)
.toList(),
),
),
), ),
], ],
), ),

View file

@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/rooms_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/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
@ -32,46 +31,54 @@ class MentionOverlay extends ConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: switch (triggerCharacter) { child: switch (triggerCharacter) {
"@" => "@" => Consumer(
ref builder: (_, ref, _) {
.watch(MembersController.provider(room)) final members = ref.watch(MembersController.provider(room));
.betterWhen( return ListView(
data: (members) => ListView(
children: children:
(query.isEmpty (query.isEmpty
? members ? members
: members.where( : members.where(
(member) => (member) =>
member.userId.toLowerCase().contains( member.stateKey?.toLowerCase().contains(
query.toLowerCase(), query.toLowerCase(),
) == ) ==
true || true ||
member.displayName (member.content["displayname"] as String?)
.toLowerCase() ?.toLowerCase()
.contains( .contains(query.toLowerCase()) ==
query.toLowerCase(),
) ==
true, true,
)) ))
.map( .map(
(member) => ListTile( (member) => ListTile(
leading: AvatarOrHash( leading: AvatarOrHash(
member.avatarUrl, Uri.tryParse(
member.displayName, member.content["avatar_url"] ?? "",
), ),
title: Text(member.displayName), member.content["displayname"] ?? "",
subtitle: Text(member.userId), ),
title: Text(
member.content["displayname"] as String? ??
member.stateKey ??
"Unknown User",
),
subtitle: member.stateKey != null
? Text(member.stateKey!)
: null,
onTap: () => addTag( onTap: () => addTag(
id: "[@${member.displayName}](https://matrix.to/#/${member.userId})", id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})",
name: member.userId name:
.substring(1) member.stateKey
?.substring(1)
.split(":") .split(":")
.first, .first ??
"Unknown User",
), ),
), ),
) )
.toList(), .toList(),
), );
},
), ),
"#" => ListView( "#" => ListView(
children: children:

View file

@ -1,21 +1,12 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.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 { class MessageWrapper extends StatelessWidget {
final Message message; final Message message;
final Widget child; final Widget child;
final Room room;
final MessageGroupStatus? groupStatus; final MessageGroupStatus? groupStatus;
const MessageWrapper( const MessageWrapper(this.message, this.child, this.groupStatus, {super.key});
this.message,
this.child,
this.groupStatus,
this.room, {
super.key,
});
@override @override
Widget build(BuildContext context) => ClipRRect( Widget build(BuildContext context) => ClipRRect(
@ -33,7 +24,11 @@ class MessageWrapper extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
groupStatus?.isFirst != false groupStatus?.isFirst != false
? MessageAvatar(message, room, height: 40) ? AvatarOrHash(
Uri.parse(message.metadata?["avatarUrl"] ?? ""),
height: 40,
message.metadata?["displayName"] ?? "",
)
: SizedBox(width: 40), : SizedBox(width: 40),
Expanded( Expanded(
child: Column( child: Column(
@ -41,9 +36,9 @@ class MessageWrapper extends StatelessWidget {
spacing: 4, spacing: 4,
children: [ children: [
if (groupStatus?.isFirst != false) if (groupStatus?.isFirst != false)
MessageDisplayname( Text(
message, message.metadata?["displayName"] ?? message.authorId,
room, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),

View file

@ -2,9 +2,7 @@ import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.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 { class RelationPreview extends ConsumerWidget {
final Message? relatedMessage; final Message? relatedMessage;
@ -12,11 +10,8 @@ class RelationPreview extends ConsumerWidget {
final VoidCallback onDismiss; final VoidCallback onDismiss;
final bool shouldMention; final bool shouldMention;
final VoidCallback toggleShouldMention; final VoidCallback toggleShouldMention;
final Room room; const RelationPreview({
required this.relatedMessage,
const RelationPreview(
this.relatedMessage, {
required this.room,
required this.relationType, required this.relationType,
required this.onDismiss, required this.onDismiss,
required this.shouldMention, required this.shouldMention,
@ -41,10 +36,14 @@ class RelationPreview extends ConsumerWidget {
"Editing message:", "Editing message:",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
MessageAvatar(relatedMessage!, room), AvatarOrHash(
MessageDisplayname( Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""),
relatedMessage!, relatedMessage?.metadata?["displayName"]?.toString() ?? "",
room, height: 16,
),
Text(
relatedMessage!.metadata?["displayName"] ??
relatedMessage!.authorId,
style: theme.textTheme.labelMedium?.copyWith( style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),

View file

@ -1,15 +1,15 @@
import "dart:math";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/event_controller.dart"; import "package:nexus/controllers/event_controller.dart";
import "package:nexus/controllers/message_controller.dart"; import "package:nexus/controllers/message_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/configs/message_config.dart"; import "package:nexus/models/message_config.dart";
import "package:nexus/models/requests/get_event_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/models/room.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/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)?; typedef OnTapReply = void Function(Message message)?;
@ -61,28 +61,73 @@ class ReplyWidget extends ConsumerWidget {
return SizedBox.shrink(); 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( return InkWell(
onTap: () => onTapReply?.call(replyMessage), onTap: () => onTapReply?.call(replyMessage),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
MessageAvatar(replyMessage, room), AvatarOrHash(
Uri.tryParse(
replyMessage.metadata?["avatarUrl"] ??
"",
),
replyMessage.metadata?["displayName"] ??
"",
height: 16,
),
Flexible( Flexible(
child: MessageDisplayname( child: Text(
replyMessage, replyMessage
room, .metadata?["displayName"] ??
replyMessage.authorId,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.labelMedium .labelMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ),
Flexible( Flexible(
child: Text( child: Text(
replyMessage.metadata!["body"], replyText,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of( style: Theme.of(
context, context,

View file

@ -13,14 +13,15 @@ import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/report_request.dart"; import "package:nexus/models/requests/report_request.dart";
import "package:nexus/widgets/chat_page/composer/chat_box.dart"; import "package:nexus/widgets/chat_page/chat_box.dart";
import "package:nexus/widgets/chat_page/image_message.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/member_list.dart";
import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/message_wrapper.dart";
import "package:nexus/widgets/chat_page/room_appbar.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart";
import "package:nexus/widgets/chat_page/wrappers/text_message_wrapper.dart"; import "package:nexus/widgets/chat_page/text_message_wrapper.dart";
import "package:nexus/widgets/chat_page/reply_widget.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart";
import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/form_text_input.dart";
import "package:nexus/widgets/loading.dart";
// import "package:dynamic_polls/dynamic_polls.dart"; // import "package:dynamic_polls/dynamic_polls.dart";
class RoomChat extends HookConsumerWidget { class RoomChat extends HookConsumerWidget {
@ -232,7 +233,7 @@ class RoomChat extends HookConsumerWidget {
children: getMessageOptions(message), children: getMessageOptions(message),
), ),
builders: Builders( builders: Builders(
loadMoreBuilder: (_) => SizedBox.shrink(), loadMoreBuilder: (_) => Loading(),
chatAnimatedListBuilder: (_, itemBuilder) => chatAnimatedListBuilder: (_, itemBuilder) =>
ChatAnimatedList( ChatAnimatedList(
@ -319,7 +320,6 @@ class RoomChat extends HookConsumerWidget {
), ),
), ),
groupStatus, groupStatus,
room,
), ),
systemMessageBuilder: systemMessageBuilder:

View file

@ -12,8 +12,7 @@ import "package:nexus/widgets/chat_page/room_menu.dart";
import "package:nexus/widgets/form_text_input.dart"; import "package:nexus/widgets/form_text_input.dart";
class Sidebar extends HookConsumerWidget { class Sidebar extends HookConsumerWidget {
final bool isDesktop; const Sidebar({super.key});
const Sidebar({required this.isDesktop, super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -221,12 +220,9 @@ class Sidebar extends HookConsumerWidget {
), ),
) )
.toList(), .toList(),
onDestinationSelected: (value) { onDestinationSelected: (value) => selectedRoomIdNotifier.set(
selectedRoomIdNotifier.set(
selectedSpace.children[value].metadata?.id, selectedSpace.children[value].metadata?.id,
); ),
if (!isDesktop) Navigator.of(context).pop();
},
), ),
), ),
), ),

View file

@ -3,7 +3,7 @@ import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/html/html.dart";
import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/message_wrapper.dart";
import "package:nexus/widgets/chat_page/reply_widget.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart";
class TextMessageWrapper extends StatelessWidget { class TextMessageWrapper extends StatelessWidget {
@ -109,7 +109,6 @@ class TextMessageWrapper extends StatelessWidget {
), ),
), ),
groupStatus, groupStatus,
room,
); );
} }
} }

View file

@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "nexus") set(BINARY_NAME "nexus")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # 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 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View file

@ -11,6 +11,7 @@
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
#include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar = g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar =
@ -28,4 +29,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) window_manager_registrar = g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar); 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);
} }

View file

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_linux screen_retriever_linux
url_launcher_linux url_launcher_linux
window_manager window_manager
window_size
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View file

@ -1,9 +0,0 @@
[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;

View file

@ -1,41 +0,0 @@
{ 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";
};
}

View file

@ -1,44 +0,0 @@
{
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 ];
};
}

View file

@ -1,31 +0,0 @@
{
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
'';
})

View file

@ -43,7 +43,6 @@ static void my_application_activate(GApplication* application) {
} }
} }
#endif #endif
gtk_widget_set_size_request(GTK_WIDGET(window), 500, 500);
if (use_header_bar) { if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar)); gtk_widget_show(GTK_WIDGET(header_bar));

20
nix/android.nix Normal file
View file

@ -0,0 +1,20 @@
{
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" ];
}

View file

@ -29,10 +29,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: analyzer_buffer name: analyzer_buffer
sha256: "5fcd06b0715ebeee99f03e3f437b3412249969d8d12b191ea8a1d76e42a4e4a1" sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.1.11"
analyzer_plugin: analyzer_plugin:
dependency: transitive dependency: transitive
description: description:
@ -348,11 +348,10 @@ packages:
dynamic_system_colors: dynamic_system_colors:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: dynamic_system_colors
ref: HEAD sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c"
resolved-ref: "3b61760d5e0ac1229eefde5b61247947eede4110" url: "https://pub.dev"
url: "https://github.com/hasali19/flutter_dynamic_system_colors" source: hosted
source: git
version: "1.8.0" version: "1.8.0"
encrypt: encrypt:
dependency: transitive dependency: transitive
@ -522,10 +521,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_riverpod name: flutter_riverpod
sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.1.0"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -644,10 +643,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: hooks_riverpod name: hooks_riverpod
sha256: "08527ec06aaef75e4b78694e045ef0cd8346594eaf9cc18b0f866398b07b93b1" sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.1.0"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -1052,26 +1051,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: riverpod name: riverpod
sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.1.0"
riverpod_analyzer_utils: riverpod_analyzer_utils:
dependency: transitive dependency: transitive
description: description:
name: riverpod_analyzer_utils name: riverpod_analyzer_utils
sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0-dev.9" version: "1.0.0-dev.8"
riverpod_lint: riverpod_lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: riverpod_lint name: riverpod_lint
sha256: "64e8debf5b719a37d48b9785dd595d34133fdcd84b8fd07157a621c54ab2156f" sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.0"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1549,6 +1548,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.1" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View file

@ -21,8 +21,8 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_riverpod: ^3.3.1 flutter_riverpod: ^3.0.3
hooks_riverpod: ^3.3.1 hooks_riverpod: ^3.0.3
intl: ^0.20.1 intl: ^0.20.1
fast_immutable_collections: ^11.0.0 fast_immutable_collections: ^11.0.0
path_provider: ^2.1.3 path_provider: ^2.1.3
@ -31,11 +31,13 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
file_picker: ^10.3.3 file_picker: ^10.3.3
path: ^1.9.0 path: ^1.9.0
dynamic_system_colors: dynamic_system_colors: ^1.8.0
git:
url: https://github.com/hasali19/flutter_dynamic_system_colors
collection: ^1.19.1 collection: ^1.19.1
window_manager: ^0.5.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 flutter_chat_core: ^2.0.0
flyer_chat_image_message: ^2.2.2 flyer_chat_image_message: ^2.2.2
flyer_chat_system_message: ^2.1.13 flyer_chat_system_message: ^2.1.13
@ -67,7 +69,7 @@ dev_dependencies:
custom_lint: ^0.8.0 custom_lint: ^0.8.0
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
freezed: ^3.2.3 freezed: ^3.2.3
riverpod_lint: ^3.1.3 riverpod_lint: ^3.0.3
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
json_serializable: ^6.11.1 json_serializable: ^6.11.1
@ -75,7 +77,7 @@ flutter_launcher_icons:
ios: true ios: true
android: true android: true
image_path: assets/icon.png image_path: assets/icon.png
adaptive_icon_background: assets/background.png adaptive_icon_background: "#000000"
adaptive_icon_foreground: assets/foreground.png adaptive_icon_foreground: assets/foreground.png
remove_alpha_ios: true remove_alpha_ios: true
windows: windows:

View file

@ -3,7 +3,26 @@ import "package:ffigen/ffigen.dart";
import "package:path/path.dart"; import "package:path/path.dart";
void main(List<String> args) async { void main(List<String> args) async {
final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks")); 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}",
);
}
print("Generating FFI Bindings..."); print("Generating FFI Bindings...");

9
scripts/generate.sh Executable file
View file

@ -0,0 +1,9 @@
#!/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

View file

@ -11,6 +11,7 @@
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
#include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
@ -23,4 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar( WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin")); registry->GetRegistrarForPlugin("WindowManagerPlugin"));
WindowSizePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowSizePlugin"));
} }

View file

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_windows screen_retriever_windows
url_launcher_windows url_launcher_windows
window_manager window_manager
window_size
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View file

@ -89,11 +89,11 @@ BEGIN
BEGIN BEGIN
BLOCK "040904e4" BLOCK "040904e4"
BEGIN BEGIN
VALUE "CompanyName", "nexus.federated.Nexus" "\0" VALUE "CompanyName", "nexus.federated.nexus" "\0"
VALUE "FileDescription", "nexus" "\0" VALUE "FileDescription", "nexus" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "nexus" "\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 "OriginalFilename", "nexus.exe" "\0"
VALUE "ProductName", "nexus" "\0" VALUE "ProductName", "nexus" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"