diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index dc1e9c7..0000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml deleted file mode 100644 index 5e693f0..0000000 --- a/.github/workflows/flatpak.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c07f0ad..c8099d1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,57 +1,46 @@ -name: "Build EXE" +name: "Build Windows Version" on: - push: - branches: ["main"] - tags: ["*"] workflow_dispatch: jobs: - build-exe: - runs-on: windows-latest + build-windows: + runs-on: "windows-latest" steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive + - name: "Checkout repository" + uses: "actions/checkout@v4" - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.41.5 + - name: "Set up Flutter" + uses: "subosito/flutter-action@v2" - - name: Set up Go - uses: actions/setup-go@v6 + - name: "Set up Rust" + uses: "dtolnay/rust-toolchain@stable" with: - go-version-file: gomuks/go.mod + targets: "x86_64-pc-windows-msvc" - - name: Go build + - name: "Install Flutter dependencies" + run: flutter pub get + + - name: "Run build_runner & build Windows EXE" run: | - cd gomuks/pkg/ffi - go build -tags goolm -o ../../../libgomuks.dll -buildmode=c-shared - - - name: Build with Flutter - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build + flutter pub run build_runner build --delete-conflicting-outputs flutter build windows --release - - name: Upload exe zip - uses: actions/upload-artifact@v6 + - name: "Upload exe zip" + uses: "actions/upload-artifact@v4" 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@v6 + - name: "Upload installer artifact" + uses: "actions/upload-artifact@v4" with: - name: windows-installer - path: windows/dist/Nexus-Setup.exe \ No newline at end of file + name: "windows-installer" + path: "windows/dist/Nexus-Setup.exe" diff --git a/.gitignore b/.gitignore index 2bec583..d6616e1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,7 @@ key.properties # Generated Files *.g.dart *.freezed.dart +src/ # Devel Password -password.txt - -# Nix -/result \ No newline at end of file +password.txt \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 145276a..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "gomuks"] - path = gomuks - url = https://github.com/gomuks/gomuks - branch = main diff --git a/.metadata b/.metadata index 6651909..0181500 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "nixpkgs000000000000000000000000000000000" + revision: "67323de285b00232883f53b84095eb72be97d35c" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: nixpkgs000000000000000000000000000000000 - base_revision: nixpkgs000000000000000000000000000000000 - - platform: windows - create_revision: nixpkgs000000000000000000000000000000000 - base_revision: nixpkgs000000000000000000000000000000000 + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c + - platform: macos + create_revision: 67323de285b00232883f53b84095eb72be97d35c + base_revision: 67323de285b00232883f53b84095eb72be97d35c # User provided section diff --git a/.vscode/settings.json b/.vscode/settings.json index da80f4b..25ea52b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,11 +2,8 @@ "cSpell.words": [ "Appbar", "Displayname", - "fluttertagger", - "Gomuks", "Homeserver", - "localpart", - "muks", - "prefs" + "prefs", + "vodozemac" ] } diff --git a/README.md b/README.md index 50060d0..1299c71 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Nexus Client > [!WARNING] -> Nexus Client is still in development, and doesn't support everything needed for daily use. +> Nexus Client is still heavily in development, and is not ready for use! ## 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 @@ -15,14 +15,15 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. ## Progress -- [x] New logo -- [x] Move from the Dart SDK to the Gomuks Backend with Dart bindings: https://git.federated.nexus/Nexus/nexus/pulls/2 - - [ ] Allow using remote Gomuks over websocket +- [ ] 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 - [ ] Platform Support - [x] Linux - - [ ] Windows (WIP) + - [x] Windows - [ ] MacOS - - [x] Android + - [ ] Android - [ ] iOS - [ ] Web (may not be possible) - [x] Login @@ -36,7 +37,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] Searching - [ ] Creating (Rooms, Spaces, and DMs) - [x] Joining - - [x] Parse vias + - [ ] Parse vias - [x] Using a text/uri/link - [x] Plain text - [x] `matrix:` Uri @@ -53,7 +54,6 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [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 @@ -62,11 +62,9 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions) - [ ] Custom emojis/stickers - [ ] GIFs using Gomuks' GIF proxies - - [x] Receiving + - [x] Recieving - [x] Plain text - - [x] Per message profiles - [x] HTML - - [x] URL Previews - [x] Replies - [x] Viewing - [ ] Jump to original message @@ -79,34 +77,28 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Blurhashing - [ ] Downloading attachments - [x] Opening attachments in their own view - - [ ] Polls + - [ ] Polls: Waiting on https://github.com/SwanFlutter/dynamic_polls/issues/1 - [x] Mentions - [x] Users - - [x] Clickable - [x] Rooms - - [x] Clickable + - [ ] Plain text (not sure if I want to add this or not, I probably won't unless there's interest) - [x] Matrix URIs - [x] Matrix.to links - - [x] Events - - [ ] Render more nicely - - [ ] Clickable + - [ ] Do some fancy fetching to get nice names + - [ ] Make clickable - [x] Custom emojis/stickers - [x] History loading - [x] Backwards - [ ] Forwards - [x] Editing - [x] Deleting -- [x] Reactions +- [ ] Reactions: Waiting on https://github.com/flyerhq/flutter_chat_ui/pull/838 or me doing a custom impl - [ ] Pins - [ ] Displaying - [ ] Creating - [ ] Threads -- [x] Profile popouts - - [x] Working actions -- [x] Copy link to: - - [x] Room - - [x] Space - - [x] Message +- [ ] Profile popouts +- [ ] Copy link to [room, space] - [ ] Reporting - [x] Events - [ ] Rooms @@ -114,16 +106,14 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites - [ ] Settings - - [ ] Matrix: URIs vs Matrix.to links - [ ] Light/Dark mode - [ ] SSD or CSD - - [ ] Align your message bubbles to left or right - [ ] Show media by default - [ ] Dynamic Theming - [ ] Devices - [ ] Viewing devices - [ ] Verifying devices - - [ ] URL preview: Server / Sending Client (Beeper spec) / None + - [ ] URL preview: Server / Client / None - [ ] Account changes - [ ] Display name - [ ] Profile picture @@ -133,64 +123,25 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] About - [x] Log Out -## Try it out +## Build Instructions -If you want to try out Nexus, grab one of the following artifacts from CI: +First, clone and open the repo: -- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip) -- Windows - - [Portable Build](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-portable.zip) - - [Installer](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip) -- Flatpak - - [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip) - - [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip) - -Or, try the Nix package: `nix run git+https://git.federated.nexus/Nexus/nexus` - -## Build it yourself +```sh +git clone https://git.federated.nexus/Henry-Hiles/nexus +cd nexus +``` ### Prerequisites #### Linux -- With Nix: Either use direnv and `direnv allow`, or `nix flake develop` -- Without Nix: Install Flutter, Go, Git, Libclang, and Glibc. Do not use any Snap packages, they cause various compilation issues. +- With Nix: Either use direnv, or `nix flake develop` +- Without Nix: Install Flutter, Go, Olm, Git, Clang, and GLibc. -#### Windows +#### Windows / MacOS -You will need: - -- Flutter -- Android SDK + NDK -- Git -- Go -- Visual Studio 2022 (Desktop development with C++) -- [MSYS2/MinGW-w64 GCC](https://www.msys2.org/) (for CGO) -- [LLVM/Clang + libclang](https://clang.llvm.org/get_started.html) (for `ffigen`) - -On Windows, make sure these are available in your shell `PATH`: - -- `C:\msys64\ucrt64\bin` (or your MinGW bin path containing `x86_64-w64-mingw32-gcc.exe`) -- `C:\Program Files\LLVM\bin` (contains `clang.exe` and `libclang.dll`) - -For `dart scripts/generate.dart`, you may also need: - -```powershell -$env:CPATH = "C:\msys64\ucrt64\include" -``` - -#### MacOS - -Similar prerequisites apply (Flutter, Git, Go, C toolchain, LLVM/libclang), but exact setup has not been fully documented yet. - -### Clone repo - -First, clone and open the repo: - -```sh -git clone --recurse-submodules https://git.federated.nexus/Nexus/nexus -cd nexus -``` +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 @@ -200,10 +151,16 @@ Get dependencies: flutter pub get ``` -Generate Gomuks bindings: +Get dependencies: ```sh -dart scripts/generate.dart +flutter pub get +``` + +Clone Gomuks and generate bindings: + +```sh +scripts/generate.sh ``` Build generated files, and watch for new changes: @@ -221,8 +178,3 @@ flutter run ## Community Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. - -# Credits - -Thank you Hylke Bons (https://planetpeanut.studio) for making the amazing icon for Nexus! -Thank you Tulir Asokan for making [Gomuks](https://github.com/gomuks/gomuks), and helping us integrate it into Nexus! diff --git a/android/app/build.gradle b/android/app/build.gradle index fd51ea0..ce5f465 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,11 +39,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - // do we want to update.. eventually? - jvmTarget = "17" - } - defaultConfig { applicationId = "nexus.federated.Nexus" minSdk = 29 @@ -55,8 +50,7 @@ android { signingConfigs { release { keyAlias "key" - def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH") - storeFile storePath ? file(storePath) : null + storeFile keystoreProperties['path'] ? file(keystoreProperties['path']) : file(System.getenv("KEYSTORE_PATH")) 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 666977e..1c369c9 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/ic_launcher" + android:roundIcon="@mipmap/nexus_round" android:allowBackup="false" android:fullBackupContent="false"> - + - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index e97fe0e..80efd04 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 4e9192d..b02e5ef 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index f18b718..54aed69 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 2f6a559..eb2221d 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 0118074..c5ac464 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/background.png b/assets/background.png deleted file mode 100644 index 9f1d8e7..0000000 Binary files a/assets/background.png and /dev/null differ diff --git a/assets/background.svg b/assets/background.svg deleted file mode 100644 index 749e03a..0000000 --- a/assets/background.svg +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/foreground.png b/assets/foreground.png index a98eb11..4249989 100644 Binary files a/assets/foreground.png and b/assets/foreground.png differ diff --git a/assets/foreground.svg b/assets/foreground.svg index 9aad561..4f2f2b2 100644 --- a/assets/foreground.svg +++ b/assets/foreground.svg @@ -1,19 +1,20 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:current-layer="layer1" /> diff --git a/assets/icon.png b/assets/icon.png index d6d4906..04b75cb 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg index b36fa26..0effd9a 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,22 +1,21 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="stop11" /> diff --git a/assets/mobile.png b/assets/mobile.png deleted file mode 100644 index 6b1b81c..0000000 Binary files a/assets/mobile.png and /dev/null differ diff --git a/assets/mobile.svg b/assets/mobile.svg deleted file mode 100644 index 7ca0a7d..0000000 --- a/assets/mobile.svg +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/monochrome.png b/assets/monochrome.png deleted file mode 100644 index 941c706..0000000 Binary files a/assets/monochrome.png and /dev/null differ diff --git a/assets/monochrome.svg b/assets/monochrome.svg deleted file mode 100644 index a86f36e..0000000 --- a/assets/monochrome.svg +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/reactions.png b/assets/reactions.png deleted file mode 100644 index c413051..0000000 Binary files a/assets/reactions.png and /dev/null differ diff --git a/assets/reply-preview.png b/assets/reply-preview.png new file mode 100644 index 0000000..3c4cc3e Binary files /dev/null and b/assets/reply-preview.png differ diff --git a/assets/reply.webp b/assets/reply.webp new file mode 100644 index 0000000..e8f139e Binary files /dev/null and b/assets/reply.webp differ diff --git a/flake.lock b/flake.lock index 8070c6c..7826732 100644 --- a/flake.lock +++ b/flake.lock @@ -18,54 +18,17 @@ "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": { "locked": { - "lastModified": 1773389992, - "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", - "owner": "NixOS", + "lastModified": 1767640445, + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -86,42 +49,10 @@ "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", - "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" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 4c06ac6..4922db7 100644 --- a/flake.nix +++ b/flake.nix @@ -2,10 +2,8 @@ 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 = @@ -40,37 +38,29 @@ }; }; - packages = + devShells = let - default = pkgs.callPackage ./linux/nix/pkg { - src = self; + packages = with pkgs; [ + go + 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 { - 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" ]; - }; + default = pkgs.mkShell { + inherit env; + packages = packages ++ [ + pkgs.flutter + ]; }; - gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix { - src = self; - }; + nix = pkgs.mkShell { inherit packages env; }; }; - - devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { }; }; }; } diff --git a/gomuks b/gomuks deleted file mode 160000 index daa0ba0..0000000 --- a/gomuks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit daa0ba028e7d89ba9fc7580fc8099348e6145cb3 diff --git a/hook/build.dart b/hook/build.dart index 165e613..a3a066c 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -3,6 +3,9 @@ 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; @@ -15,6 +18,15 @@ Future main(List args) => build(args, (input, output) async { break; case OS.macOS: libFileName = "libgomuks.dylib"; + final sdkRoot = await _macSdkRoot(); + final goArch = targetArch == Architecture.arm64 ? "arm64" : "amd64"; + env = { + "GOARCH": goArch, + "CGO_ENABLED": "1", + "CGO_CFLAGS": "-isysroot $sdkRoot", + "CGO_CXXFLAGS": "-isysroot $sdkRoot", + "CGO_LDFLAGS": "-isysroot $sdkRoot", + }; break; case OS.windows: libFileName = "libgomuks.dll"; @@ -22,54 +34,52 @@ Future main(List args) => build(args, (input, output) async { case OS.android: libFileName = "libgomuks.so"; + if (targetArch != Architecture.arm64) { + throw UnsupportedError( + "only arm64 for now... got: $targetArch", + ); + } + final targetNdkApi = codeConfig.android.targetNdkApi; - final ndkHome = - Platform.environment["ANDROID_NDK_HOME"] ?? - Platform.environment["ANDROID_NDK_ROOT"] ?? - Platform.environment["NDK_HOME"] ?? - await _findNdkFromSdk(); + final ndkHome = Platform.environment["ANDROID_NDK_HOME"] + ?? Platform.environment["ANDROID_NDK_ROOT"] + ?? Platform.environment["NDK_HOME"]; if (ndkHome == null) { throw Exception( - "Could not find Android NDK. Set ANDROID_NDK_HOME or install via sdkmanager.", + "ANDROID_NDK_HOME, ANDROID_NDK_ROOT, or NDK_HOME must be set for Android builds", ); } final hostTag = _ndkHostTag(); - final (goArch, ccTriple) = _androidArch(targetArch); - final cc = - "$ndkHome/toolchains/llvm/prebuilt/$hostTag/bin/$ccTriple$targetNdkApi-clang"; + final cc = "$ndkHome/toolchains/llvm/prebuilt/$hostTag/bin/aarch64-linux-android$targetNdkApi-clang"; - env = {"CGO_ENABLED": "1", "GOOS": "android", "GOARCH": goArch, "CC": cc}; + env = { + "CGO_ENABLED": "1", + "GOOS": "android", + "GOARCH": "arm64", + "CC": cc, + }; break; default: throw UnsupportedError("Unsupported OS: $targetOS"); } - var libFile = input.packageRoot.resolve(libFileName); - final gomuksBuildDir = input.packageRoot.resolve("gomuks/"); + final gomuksBuildDir = buildDir.resolve("gomuks/"); + final libFile = gomuksBuildDir.resolve("${targetArch.name}/$libFileName"); - if (!(await File.fromUri(libFile).exists())) { - final buildDir = input.packageRoot.resolve("build/"); - libFile = buildDir.resolve("${targetArch.name}/$libFileName"); + print("Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) from source..."); + final result = await Process.run("go", [ + "build", + "-tags", "goolm", + "-o", + libFile.path, + "-buildmode=c-shared", + ], workingDirectory: gomuksBuildDir.resolve("source/pkg/ffi/").toFilePath(), + environment: env.isNotEmpty ? env : null); - // 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}) to ${libFile.path}...", - ); - 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}", - ); - } + if (result.exitCode != 0) { + throw Exception("Failed to build Gomuks shared library\n${result.stderr}"); } final generatedFile = "src/third_party/gomuks.g.dart"; @@ -88,18 +98,12 @@ Future main(List args) => build(args, (input, output) async { 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; +Future _macSdkRoot() async { + final result = await Process.run("xcrun", ["--show-sdk-path"]); + if (result.exitCode != 0) { + throw Exception("Failed to find macOS SDK: ${result.stderr}"); + } + return (result.stdout as String).trim(); } String _ndkHostTag() { @@ -112,18 +116,3 @@ String _ndkHostTag() { } 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 44f96da..d4af5a1 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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 0d531c4..2b21522 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index da4acee..8471cd6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index a3cfb1d..c145b15 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index adbdcd5..5da5679 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index fee4302..cd2b74f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 4d21624..68cbdbf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 3e7a859..306efe8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index a3cfb1d..c145b15 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c11ce99..959cc28 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 25f2b47..d86b69c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index 8f79bb9..3a5c49b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index c48dec6..e563327 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index 99d44e8..30ae8c6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index 6f987f0..2fb68c4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 25f2b47..d86b69c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index fcf969a..151862a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index 1e0defa..c5ca065 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 3366fb5..a5880bd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index e280112..6ea8156 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index efda04b..657cf77 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 3774574..87d1ce7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/controllers/author_controller.dart b/lib/controllers/author_controller.dart deleted file mode 100644 index 70b7343..0000000 --- a/lib/controllers/author_controller.dart +++ /dev/null @@ -1,47 +0,0 @@ -import "dart:async"; -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/client_state_controller.dart"; -import "package:nexus/controllers/user_controller.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/models/membership_status.dart"; - -class AuthorController extends AsyncNotifier { - final Message message; - AuthorController(this.message); - - @override - Future build() async { - final member = await ref.watch( - UserController.provider(message.authorId).future, - ); - - final pmp = message.metadata?["pmp"] == null - ? null - : Membership.fromContent( - IMap(message.metadata?["pmp"]), - message.authorId, - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ); - - return Membership( - status: member?.status ?? MembershipStatus.leave, - avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl, - displayName: - pmp?.displayName ?? member?.displayName ?? message.authorId.localpart, - userId: message.authorId, - ); - } - - static final provider = - AsyncNotifierProvider.family( - AuthorController.new, - ); -} diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index cc68871..de6e909 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,6 +1,5 @@ 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"; @@ -9,13 +8,11 @@ import "package:flutter/foundation.dart"; import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/init_complete_controller.dart"; -import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/space_edges_controller.dart"; import "package:nexus/controllers/sync_status_controller.dart"; import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/main.dart"; import "package:nexus/models/client_state.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/paginate.dart"; @@ -28,28 +25,17 @@ import "package:nexus/models/profile.dart"; import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/requests/report_request.dart"; -import "package:nexus/models/requests/send_event_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; -import "package:nexus/models/requests/set_membership_request.dart"; import "package:nexus/models/room.dart"; 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 Pointer root; - if (Platform.isAndroid) { - final dir = await getApplicationSupportDirectory(); - root = "${dir.path}/gomuks".toNativeUtf8().cast(); - } else { - root = nullptr.cast(); - } - - final handle = GomuksInit(root); + final handle = await Isolate.run(GomuksInit); final callable = NativeCallable< @@ -78,17 +64,6 @@ class ClientController extends AsyncNotifier { case "init_complete": ref.watch(InitCompleteController.provider.notifier).complete(); break; - case "send_complete": - final event = Event.fromJson(decodedMuksEvent["event"]); - - if (event.type == "m.room.message") { - ref - .watch( - NewEventsController.provider(event.roomId).notifier, - ) - .add(IList([event])); - } - break; case "sync_complete": final syncData = SyncData.fromJson(decodedMuksEvent); final roomProvider = RoomsController.provider; @@ -128,7 +103,6 @@ class ClientController extends AsyncNotifier { debugPrint("Finished handling $muksEventType..."); } catch (error, stackTrace) { debugger(); - showError(error, stackTrace); debugPrintStack(stackTrace: stackTrace, label: error.toString()); } }); @@ -166,18 +140,15 @@ class ClientController extends AsyncNotifier { Future redactEvent(RedactEventRequest report) => _sendCommand("redact_event", report.toJson()); - Future sendMessage(SendMessageRequest request) async => - Event.fromJson(await _sendCommand("send_message", request.toJson())); + Future sendMessage(SendMessageRequest request) => + _sendCommand("send_message", request.toJson()); - Future sendEvent(SendEventRequest request) async => - Event.fromJson(await _sendCommand("send_event", request.toJson())); - - Future verify(String recoveryKey) async { + Future verify(String recoveryKey) async { try { await _sendCommand("verify", {"recovery_key": recoveryKey}); - return null; + return true; } catch (error) { - return error.toString(); + return false; } } @@ -202,13 +173,9 @@ class ClientController extends AsyncNotifier { // })); Future> getRoomState(GetRoomStateRequest request) async { - Future getState(GetRoomStateRequest request) async => - (await _sendCommand("get_room_state", request.toJson())) as List?; - final response = await getState(request); - - return (response ?? await getState(request.copyWith(refetch: true)) ?? []) - .map((event) => Event.fromJson(event)) - .toIList(); + final response = + (await _sendCommand("get_room_state", request.toJson())) as List; + return response.map((event) => Event.fromJson(event)).toIList(); } Future?> getRelatedEvents( @@ -235,11 +202,8 @@ class ClientController extends AsyncNotifier { Future getProfile(String userId) async => Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId})); - Future reportEvent(ReportRequest request) => - _sendCommand("report_event", request.toJson()); - - Future setMembership(SetMembershipRequest request) => - _sendCommand("set_membership", request.toJson()); + Future reportEvent(ReportRequest report) => + _sendCommand("report_event", report.toJson()); Future markRead(Room room) async { final event = room.events.firstWhereOrNull( @@ -254,19 +218,19 @@ class ClientController extends AsyncNotifier { }); } - Future login(LoginRequest login) async { + Future login(LoginRequest login) async { try { await _sendCommand("login", login.toJson()); - return null; + return true; } catch (error) { - return error.toString(); + return false; } } Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { - "user_id": "@fake-user:${homeserver.host}", + "user_id": "@fakeuser:${homeserver.host}", }); return response["m.homeserver"]?["base_url"]; } catch (error) { diff --git a/lib/controllers/emoji_controller.dart b/lib/controllers/emoji_controller.dart deleted file mode 100644 index 358f98b..0000000 --- a/lib/controllers/emoji_controller.dart +++ /dev/null @@ -1,88 +0,0 @@ -import "dart:convert"; -import "package:emoji_text_field/models/emoji_category.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/models/emoji.dart"; - -typedef EmojiTuple = (IMap, IMap>); - -class EmojiController extends AsyncNotifier { - @override - Future build() async { - final response = await get( - Uri.https( - "github.com", - "github/gemoji/raw/refs/heads/master/db/emoji.json", - ), - ); - - if (response.statusCode != 200) { - throw Exception("Failed to load emoji data"); - } - - final data = json.decode(response.body); - - final entries = (data as List) - .cast>() - .map(Emoji.fromJson) - .toIList(); - - final categoryMap = entries.fold>>( - const IMap.empty(), - (acc, entry) => acc.update( - entry.category, - (list) => list.add(entry.emoji), - ifAbsent: () => IList([entry.emoji]), - ), - ); - - final keywordMap = entries.fold>>( - const IMap.empty(), - (acc, entry) => acc.add( - entry.emoji, - IList([...entry.tags, ...entry.aliases, entry.description]), - ), - ); - - final customCategories = IMap.fromEntries( - categoryMap.entries.map( - (entry) => MapEntry( - entry.key, - EmojiCategory( - name: entry.key, - icon: switch (entry.key) { - "Smileys & Emotion" => Icons.emoji_emotions, - "People & Body" => Icons.emoji_people, - "Animals & Nature" => Icons.emoji_nature, - "Food & Drink" => Icons.emoji_food_beverage, - "Travel & Places" => Icons.travel_explore, - "Activities" => Icons.sports_soccer, - "Objects" => Icons.emoji_objects, - "Symbols" => Icons.emoji_symbols, - "Flags" => Icons.emoji_flags, - _ => Icons.category, - }, - emojis: entry.value.toList(growable: false), - ), - ), - ), - ); - - final customKeywords = IMap( - Map.fromEntries( - keywordMap.entries.map( - (e) => MapEntry(e.key, e.value.toList(growable: false)), - ), - ), - ); - - return (customCategories, customKeywords); - } - - static final provider = - AsyncNotifierProvider.autoDispose( - EmojiController.new, - ); -} diff --git a/lib/controllers/members_by_type_controller.dart b/lib/controllers/members_by_type_controller.dart deleted file mode 100644 index cdc8d07..0000000 --- a/lib/controllers/members_by_type_controller.dart +++ /dev/null @@ -1,25 +0,0 @@ -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/membership.dart"; -import "package:nexus/models/membership_status.dart"; - -class MembersByTypeController extends AsyncNotifier> { - final MembershipStatus status; - MembersByTypeController(this.status); - - @override - Future> build() => ref.watch( - MembersController.provider.selectAsync( - (members) => - members.where((membership) => membership.status == status).toIList(), - ), - ); - - static final provider = - AsyncNotifierProvider.family< - MembersByTypeController, - IList, - MembershipStatus - >(MembersByTypeController.new); -} diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 39666d4..268a30d 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,52 +1,25 @@ +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/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/models/requests/get_room_state_request.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/models/room.dart"; + +class MembersController extends Notifier> { + final Room room; + MembersController(this.room); -class MembersController extends AsyncNotifier> { @override - Future> build() async { - final data = ref.watch( - SelectedRoomController.provider.select( - (value) => value?.metadata == null - ? null - : (value!.metadata!.id, value.metadata!.hasMemberList), - ), - ); - if (data == null) return const IList.empty(); + 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(); - final state = await ref - .watch(ClientController.provider.notifier) - .getRoomState( - GetRoomStateRequest( - roomId: data.$1, - fetchMembers: data.$2 == false, - includeMembers: true, - ), - ); - - return state.nonNulls - .where((state) => state.type == "m.room.member") - .map( - (membership) => Membership.fromContent( - membership.content, - membership.stateKey!, - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ), - ) - .toIList(); - } - - static final provider = - AsyncNotifierProvider>( + static final provider = NotifierProvider.family + .autoDispose, Room>( MembersController.new, ); } diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index c65d18d..f3ef13b 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -1,12 +1,10 @@ import "package:collection/collection.dart"; -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/client_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/models/configs/message_config.dart"; -import "package:nexus/models/requests/get_related_events_request.dart"; +import "package:nexus/models/message_config.dart"; class MessageController extends AsyncNotifier { final MessageConfig config; @@ -15,11 +13,11 @@ class MessageController extends AsyncNotifier { @override Future build() async { try { - final isEdit = config.event.relationType == "m.replace"; - if ((isEdit && !config.includeEdits) || config.room.metadata == null) { + if (config.event.relationType == "m.replace" && !config.includeEdits) { return null; } + if (!ref.mounted) return null; final event = config.event.lastEditRowId == null ? config.event : config.room.events.firstWhereOrNull( @@ -27,11 +25,17 @@ class MessageController extends AsyncNotifier { ) ?? config.event; - final decrypted = (event.decrypted ?? event.content); + 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 content = decrypted["m.new_content"] == null - ? decrypted - : IMap(decrypted["m.new_content"]); + final newContent = content["m.new_content"] as Map?; final homeserver = ref .read(ClientStateController.provider) @@ -42,19 +46,25 @@ class MessageController extends AsyncNotifier { final metadata = { "body": config.event.redactedBy == null - ? (content["body"] ?? "") + ? (newContent?["body"] ?? content["body"] ?? "") : "Deleted Message", "flashing": false, "timelineId": event.timelineRowId, "big": event.localContent?.bigEmoji == true, "eventType": type, - "pmp": content["com.beeper.per_message_profile"], - "error": event.sendError, - "format": content["format"] ?? content["format"], - "editSource": event.localContent?.editSource ?? content["body"], + "avatarUrl": author?.content["avatar_url"], + "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, }; + if (!ref.mounted) return null; + final editedAt = event.relationType == "m.replace" ? event.timestamp : null; @@ -65,60 +75,32 @@ class MessageController extends AsyncNotifier { return null; } + // TODO: Use server-generated preview if enabled + + // final match = Uri.tryParse( + // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", + // ); + final replyId = config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; - final reactionEvents = config.event.reactions.isEmpty && !isEdit - ? null - : await ref - .watch(ClientController.provider.notifier) - .getRelatedEvents( - GetRelatedEventsRequest( - roomId: config.room.metadata!.id, - eventId: - (isEdit ? config.event.relatesTo : null) ?? - config.event.eventId, - relationType: "m.annotation", - ), - ); - - final reactions = reactionEvents - ?.where((event) => event.redactedBy == null) - .fold>>(IMap(), (acc, event) { - final key = event.content["m.relates_to"]?["key"]; - if (key == null) return acc; - - return acc.update( - key, - (list) => list.add(event.authorId), - ifAbsent: () => IList([event.authorId]), - ); - }) - .map((key, value) => MapEntry(key, value.unlock)) - .unlock; - final asText = Message.text( metadata: metadata, id: config.event.eventId, - reactions: reactions, authorId: event.authorId, - text: content["formatted_body"] ?? content["body"] ?? "", + text: + newContent?["formatted_body"] ?? + newContent?["body"] ?? + content["formatted_body"] ?? + content["body"] ?? + "", replyToMessageId: replyId, deliveredAt: config.event.timestamp, editedAt: editedAt, ) as TextMessage; - Message toSystemMessage(String content) => Message.system( - metadata: {...metadata, "body": content}, - id: config.event.eventId, - reactions: reactions, - authorId: event.authorId, - deliveredAt: config.event.timestamp, - text: content, - ); - return switch (type) { "m.room.encrypted" => asText.copyWith( text: "Unable to decrypt message.", @@ -138,7 +120,6 @@ class MessageController extends AsyncNotifier { null || "m.image" => Message.image( id: config.event.eventId, authorId: event.authorId, - reactions: reactions, source: source, replyToMessageId: replyId, metadata: metadata, @@ -151,7 +132,6 @@ class MessageController extends AsyncNotifier { size: content["info"]["size"], metadata: metadata, id: config.event.eventId, - reactions: reactions, authorId: event.authorId, source: source, replyToMessageId: replyId, @@ -162,21 +142,33 @@ class MessageController extends AsyncNotifier { "m.room.member" => content["membership"] == event.unsigned["prev_content"]?["membership"] ? null - : toSystemMessage( - "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { - "invite" => "was invited to", - "join" => "joined", - "leave" => event.authorId == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - "ban" => "was banned from", - "knock" => "asked to join", - _ => "did something relating to", - }} the room. ${content["reason"] ?? ""}", + : Message.system( + metadata: { + ...metadata, + "body": + "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { + "invite" => "was invited to", + "join" => "joined", + "leave" => "left", + "knock" => "asked to join", + "ban" => "was banned from", + _ => "did something relating to", + }} the room.", + }, + id: config.event.eventId, + authorId: event.authorId, + deliveredAt: config.event.timestamp, + text: + "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { + "invite" => "was invited to", + "join" => "joined", + "leave" => "left", + "knock" => "asked to join", + "ban" => "was banned from", + _ => "did something relating to", + }} the room.", ), - "m.room.server_acl" => toSystemMessage( - "${event.authorId} updated the server ban list.", - ), - "m.room.redaction" => config.alwaysReturn ? asText.copyWith( @@ -195,7 +187,6 @@ class MessageController extends AsyncNotifier { // ignore: dead_code ? Message.unsupported( metadata: metadata, - reactions: reactions, id: config.event.eventId, authorId: event.authorId, replyToMessageId: replyId, diff --git a/lib/controllers/messages_controller.dart b/lib/controllers/messages_controller.dart index 28885fb..83bd815 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/configs/message_config.dart"; -import "package:nexus/models/configs/messages_config.dart"; +import "package:nexus/models/message_config.dart"; +import "package:nexus/models/messages_config.dart"; class MessagesController extends AsyncNotifier> { final MessagesConfig config; diff --git a/lib/controllers/power_level_controller.dart b/lib/controllers/power_level_controller.dart deleted file mode 100644 index 41b5f19..0000000 --- a/lib/controllers/power_level_controller.dart +++ /dev/null @@ -1,70 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; -import "package:nexus/models/configs/power_level_config.dart"; -import "package:nexus/models/requests/membership_action.dart"; - -class PowerLevelController extends Notifier { - final PowerLevelConfig config; - PowerLevelController(this.config); - - @override - bool build() { - final room = ref.watch(SelectedRoomController.provider); - final event = room?.events.firstWhereOrNull( - (event) => event.rowId == room.state["m.room.power_levels"]?[""], - ); - final user = ref.watch(ClientStateController.provider)?.userId; - if (event == null || user == null) return false; - - final users = (event.content["users"] as Map? ?? {}); - final events = (event.content["events"] as Map? ?? {}); - - int powerLevelOf(String userId) => users.containsKey(userId) - ? (users[userId] as int) - : (event.content["users_default"] as int? ?? 0); - - final userLevel = powerLevelOf(user); - final targetLevel = config.targetUser != null - ? powerLevelOf(config.targetUser!) - : null; - - if (config.action != null) { - return switch (config.action!) { - MembershipAction.invite => - userLevel >= (event.content["invite"] as int? ?? 0), - - MembershipAction.kick => - targetLevel != null && - userLevel >= (event.content["kick"] as int? ?? 50) && - userLevel > targetLevel, - - MembershipAction.ban => - targetLevel != null && - userLevel >= (event.content["ban"] as int? ?? 50) && - userLevel > targetLevel, - - MembershipAction.unban => - userLevel >= (event.content["ban"] as int? ?? 50), - }; - } - - if (config.eventType == "m.room.redaction") { - return userLevel >= (event.content["redact"] as int? ?? 50); - } - - final requiredLevel = events.containsKey(config.eventType) - ? (events[config.eventType] as int) - : (config.isStateEvent - ? (event.content["state_default"] as int? ?? 50) - : (event.content["events_default"] as int? ?? 0)); - - return userLevel >= requiredLevel; - } - - static final provider = NotifierProvider.autoDispose - .family( - PowerLevelController.new, - ); -} diff --git a/lib/controllers/profile_controller.dart b/lib/controllers/profile_controller.dart deleted file mode 100644 index 120d4e4..0000000 --- a/lib/controllers/profile_controller.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/models/profile.dart"; - -class ProfileController extends AsyncNotifier { - final String userId; - ProfileController(this.userId); - - @override - Future build() { - final client = ref.watch(ClientController.provider.notifier); - return client.getProfile(userId); - } - - static final provider = AsyncNotifierProvider.autoDispose - .family(ProfileController.new); -} diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index fa32bf8..4a4dba2 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,7 +1,9 @@ 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"; +import "package:flutter_chat_core/flutter_chat_core.dart" as chat; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:fluttertagger/fluttertagger.dart"; import "package:nexus/controllers/client_controller.dart"; @@ -9,15 +11,12 @@ 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/controllers/selected_room_controller.dart"; -import "package:nexus/models/configs/messages_config.dart"; -import "package:nexus/models/configs/message_config.dart"; -import "package:nexus/models/requests/get_related_events_request.dart"; +import "package:nexus/models/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/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/models/requests/send_event_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; import "package:nexus/models/room.dart"; @@ -30,8 +29,13 @@ class RoomChatController extends AsyncNotifier { final client = ref.watch(ClientController.provider.notifier); var room = ref.read(RoomsController.provider)[roomId]; if (room == null) return InMemoryChatController(); + final state = await client.getRoomState( - GetRoomStateRequest(roomId: roomId), + GetRoomStateRequest( + roomId: roomId, + fetchMembers: room.metadata?.hasMemberList == false, + includeMembers: true, + ), ); ref @@ -79,68 +83,16 @@ class RoomChatController extends AsyncNotifier { ref.onDispose( ref.listen(NewEventsController.provider(roomId), (_, next) async { + final controller = await future; for (final event in next) { - if (event.type == "m.reaction") { - final message = controller.messages.firstWhereOrNull( - (message) => - message.id == event.content["m.relates_to"]?["event_id"], - ); - final key = event.content["m.relates_to"]?["key"]; - if (message == null || key == null || !ref.mounted) return; - - return await controller.updateMessage( - message, - message.copyWith( - reactions: IMap(message.reactions) - .update( - key, - (reactors) => [...reactors, event.authorId], - ifAbsent: () => [event.authorId], - ) - .unlock, - ), - ); - } - if (event.type == "m.room.redaction") { final controller = await future; - final redactsId = event.content["redacts"]; - final originalMessage = controller.messages.firstWhereOrNull( - (message) => message.id == redactsId, + final message = controller.messages.firstWhereOrNull( + (message) => message.id == event.content["redacts"], ); - if (!ref.mounted) return; + if (message == null || !ref.mounted) return; - if (originalMessage != null) { - return await controller.removeMessage(originalMessage); - } - - final redacts = ref - .read(SelectedRoomController.provider) - ?.events - .firstWhere((event) => event.eventId == redactsId); - - if (redacts?.type == "m.reaction") { - final message = controller.messages.firstWhereOrNull( - (message) => - message.id == redacts!.content["m.relates_to"]?["event_id"], - ); - final key = redacts!.content["m.relates_to"]?["key"]; - if (message == null || key == null || !ref.mounted) return; - - return await controller.updateMessage( - message, - message.copyWith( - reactions: IMap(message.reactions) - .update( - key, - (reactors) => - IList(reactors).remove(redacts.authorId).unlock, - ) - .where((_, value) => value.isNotEmpty) - .unlock, - ), - ); - } + await controller.removeMessage(message); } else { final message = await ref.watch( MessageController.provider( @@ -169,8 +121,12 @@ class RoomChatController extends AsyncNotifier { ), ); } - if (message != null && ref.mounted) { - await insertMessage(message); + if (message != null && + !controller.messages.any( + (oldMessage) => oldMessage.id == message.id, + ) && + ref.mounted) { + await controller.insertMessage(message); } } } @@ -179,9 +135,9 @@ class RoomChatController extends AsyncNotifier { ref.onDispose(controller.dispose); - // While there are under 20 messages, try up to load more messages until theres no more or we have 20 messages. - for (var more = true; more == true && controller.messages.length < 20;) { - more = await loadOlder(controller); + // While there are under 20 messages, try up to two times to load more messages. + for (var i = 0; i < 2 && messages.length < 20; i++) { + await loadOlder(controller); } return controller; @@ -201,13 +157,21 @@ class RoomChatController extends AsyncNotifier { : controller.updateMessage(oldMessage, message); } - Future deleteMessage(Message message, {String? reason}) => ref - .watch(ClientController.provider.notifier) - .redactEvent( - RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason), - ); + Future deleteMessage(Message message, {String? reason}) async { + final controller = await future; + await controller.removeMessage(message); + await ref + .watch(ClientController.provider.notifier) + .redactEvent( + RedactEventRequest( + eventId: message.id, + roomId: roomId, + reason: reason, + ), + ); + } - Future loadOlder([InMemoryChatController? chatController]) async { + Future loadOlder([InMemoryChatController? chatController]) async { final response = await ref .watch(ClientController.provider.notifier) .paginate( @@ -239,40 +203,38 @@ class RoomChatController extends AsyncNotifier { ), }), const ISet.empty(), - addToNewEvents: false, ); final room = ref.read(RoomsController.provider)[roomId]; - if (room != null) { - final messages = await ref.watch( - MessagesController.provider( - MessagesConfig(room: room, events: response.events.reversed), - ).future, - ); + if (room == null) return; - final controller = chatController ?? await future; - await controller.insertAllMessages( - messages - .where( - (newMessage) => !controller.messages.any( - (message) => message.id == newMessage.id, - ), - ) - .toList(), - index: 0, - ); - } - return response.hasMore; + final messages = await ref.watch( + MessagesController.provider( + MessagesConfig(room: room, events: response.events.reversed), + ).future, + ); + + final controller = chatController ?? await future; + await controller.insertAllMessages( + messages + .where( + (newMessage) => !controller.messages.any( + (message) => message.id == newMessage.id, + ), + ) + .toList(), + index: 0, + ); } Future send( - String text, { + String message, { bool shouldMention = true, - required IList tags, + required Iterable tags, required RelationType relationType, Message? relation, }) async { - var taggedMessage = text; + var taggedMessage = message; for (final tag in tags) { final escaped = RegExp.escape(tag.id); @@ -285,8 +247,7 @@ class RoomChatController extends AsyncNotifier { } final client = ref.watch(ClientController.provider.notifier); - final room = ref.read(RoomsController.provider)[roomId]; - final event = await client.sendMessage( + client.sendMessage( SendMessageRequest( roomId: roomId, mentions: Mentions( @@ -304,15 +265,21 @@ class RoomChatController extends AsyncNotifier { : Relation(eventId: relation.id, relationType: relationType), ), ); - final message = room == null - ? null - : await ref.watch( - MessageController.provider( - MessageConfig(room: room, event: event), - ).future, - ); + } - if (message != null) insertMessage(message); + Future resolveUser(String id) async { + final user = await ref + .watch(ClientController.provider.notifier) + .getProfile(id); + return chat.User( + id: id, + name: user.displayName, + // imageSource: user.avatarUrl == null + // ? null + // : (await ref.watch( + // AvatarController.provider(user.avatarUrl!.toString()).future, + // )).toString(), + ); } Future scrollToMessage(Message message) async { @@ -330,59 +297,6 @@ class RoomChatController extends AsyncNotifier { return await controller.scrollToMessage(message.id); } - Future removeReaction( - String reaction, - Message message, - String userId, - ) async { - final client = ref.watch(ClientController.provider.notifier); - final allReactionEvents = await client.getRelatedEvents( - GetRelatedEventsRequest( - roomId: roomId, - eventId: message.id, - relationType: "m.annotation", - ), - ); - - final reactionEvents = allReactionEvents - ?.where((event) => event.redactedBy == null) - .toIList(); - - final reactionEvent = reactionEvents?.firstWhereOrNull( - (event) => - event.authorId == userId && - event.content["m.relates_to"]?["key"] == reaction, - ); - - if (reactionEvent != null) { - await ref - .watch(ClientController.provider.notifier) - .redactEvent( - RedactEventRequest(eventId: reactionEvent.eventId, roomId: roomId), - ); - } - } - - Future sendReaction(String reaction, Message message) async { - final client = ref.watch(ClientController.provider.notifier); - - await client.sendEvent( - SendEventRequest( - roomId: roomId, - type: "m.reaction", - content: { - "m.relates_to": { - "event_id": message.id, - "rel_type": "m.annotation", - "key": reaction, - }, - }, - synchronous: true, - disableEncryption: true, - ), - ); - } - static final provider = AsyncNotifierProvider.family .autoDispose( RoomChatController.new, diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 7013de0..0945644 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,9 +1,7 @@ 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/client_state_controller.dart"; import "package:nexus/controllers/new_events_controller.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/room.dart"; @@ -11,18 +9,7 @@ class RoomsController extends Notifier> { @override IMap build() => const IMap.empty(); - void update( - IMap rooms, - ISet leftRooms, { - bool addToNewEvents = true, - }) { - final homeserver = - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - ""; + void update(IMap rooms, ISet leftRooms) { final merged = rooms.entries.fold(state, (acc, entry) { final roomId = entry.key; final incoming = entry.value; @@ -33,32 +20,23 @@ class RoomsController extends Notifier> { (item) => item.eventId, ); - if (addToNewEvents) { - ref - .watch(NewEventsController.provider(roomId).notifier) - .add( - incoming.timeline - .map( - (timelineTuple) => events?.firstWhereOrNull( - (event) => timelineTuple.eventRowId == event.rowId, - ), - ) - .nonNulls - .toIList(), - ); - } + ref + .watch(NewEventsController.provider(roomId).notifier) + .add( + incoming.timeline + .map( + (timelineTuple) => events?.firstWhereOrNull( + (event) => timelineTuple.eventRowId == event.rowId, + ), + ) + .nonNulls + .toIList(), + ); return acc.add( roomId, existing?.copyWith( - hasMore: incoming.hasMore, - metadata: - incoming.metadata?.copyWith( - avatar: - incoming.metadata?.avatar?.mxcToHttps(homeserver) ?? - existing.metadata?.avatar, - ) ?? - existing.metadata, + metadata: incoming.metadata ?? existing.metadata, events: events!, state: incoming.state.entries.fold( existing.state, @@ -88,11 +66,7 @@ class RoomsController extends Notifier> { ), ), ) ?? - incoming.copyWith( - metadata: incoming.metadata?.copyWith( - avatar: incoming.metadata?.avatar?.mxcToHttps(homeserver), - ), - ), + incoming, ); }); diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 7a503ad..ca217a5 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -1,4 +1,3 @@ -import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -100,37 +99,15 @@ class SpacesController extends Notifier> { .toIList(); return [ - Space( - id: "home", - title: "Home", - icon: Icons.home, - children: homeRooms, - ), - Space( - id: "dms", - title: "Direct Messages", - icon: Icons.people, - children: dmRooms, - ), - ...topLevelSpacesList, - ] - .map( - (space) => space.copyWith( - children: space.children - .sortedBy( - (element) => - element - .metadata - ?.sortingTimestamp - .millisecondsSinceEpoch ?? - 0, - ) - .sortedBy((room) => room.metadata?.unreadMessages ?? 0) - .reversed - .toIList(), - ), - ) - .toIList(); + Space(id: "home", title: "Home", icon: Icons.home, children: homeRooms), + Space( + id: "dms", + title: "Direct Messages", + icon: Icons.people, + children: dmRooms, + ), + ...topLevelSpacesList, + ].toIList(); } static final provider = NotifierProvider>( diff --git a/lib/controllers/sync_status_controller.dart b/lib/controllers/sync_status_controller.dart index 8475d9d..fe65732 100644 --- a/lib/controllers/sync_status_controller.dart +++ b/lib/controllers/sync_status_controller.dart @@ -1,17 +1,11 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/main.dart"; import "package:nexus/models/sync_status.dart"; class SyncStatusController extends Notifier { @override Null build() => null; - void set(SyncStatus newStatus) { - if (newStatus.type == SyncStatusType.permanentlyFailed) { - showError(newStatus.error ?? "Syncing failed"); - } - state = newStatus; - } + void set(SyncStatus newStatus) => state = newStatus; static final provider = NotifierProvider( SyncStatusController.new, diff --git a/lib/controllers/url_preview_controller.dart b/lib/controllers/url_preview_controller.dart deleted file mode 100644 index c2161d5..0000000 --- a/lib/controllers/url_preview_controller.dart +++ /dev/null @@ -1,60 +0,0 @@ -import "dart:convert"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/header_controller.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; - -class UrlPreviewController extends AsyncNotifier { - final String link; - UrlPreviewController(this.link); - - @override - Future build() async { - final homeserver = ref.watch(ClientStateController.provider)?.homeserverUrl; - - if (homeserver != null && !link.contains("matrix.to")) { - { - final response = await get( - Uri.parse(homeserver) - .resolve("/_matrix/client/v1/media/preview_url") - .replace(queryParameters: {"url": link}), - headers: await ref.watch(HeaderController.provider.future), - ); - - if (response.statusCode == 200) { - final decodedValue = json.decode(response.body); - final mxc = decodedValue["og:image"]; - final image = mxc == null - ? null - : Uri.tryParse(mxc)?.mxcToHttps(homeserver); - - return LinkPreviewData( - link: link, - title: decodedValue["og:title"], - description: decodedValue["og:description"], - image: image == null - ? null - : ImagePreviewData( - url: image.toString(), - width: - (decodedValue["og:image:width"] as int?)?.toDouble() ?? - 0, - height: - (decodedValue["og:image:height"] as int?)?.toDouble() ?? - 0, - ), - ); - } - } - } - - return null; - } - - static final provider = AsyncNotifierProvider.autoDispose - .family( - UrlPreviewController.new, - ); -} diff --git a/lib/controllers/user_controller.dart b/lib/controllers/user_controller.dart deleted file mode 100644 index e7ca973..0000000 --- a/lib/controllers/user_controller.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "dart:async"; -import "package:collection/collection.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/members_controller.dart"; -import "package:nexus/controllers/profile_controller.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/models/membership_status.dart"; - -class UserController extends AsyncNotifier { - final String userId; - UserController(this.userId); - - @override - Future build() async { - final member = await ref.watch( - MembersController.provider.selectAsync( - (value) => - value.firstWhereOrNull((membership) => membership.userId == userId), - ), - ); - - if (member != null) return member; - - final profile = await ref.watch(ProfileController.provider(userId).future); - return Membership( - status: MembershipStatus.leave, - avatarUrl: profile.avatarUrl == null - ? null - : Uri.tryParse(profile.avatarUrl!), - displayName: profile.displayName ?? userId.localpart, - userId: userId, - ); - } - - static final provider = - AsyncNotifierProvider.family( - UserController.new, - ); -} diff --git a/lib/controllers/via_controller.dart b/lib/controllers/via_controller.dart deleted file mode 100644 index b423947..0000000 --- a/lib/controllers/via_controller.dart +++ /dev/null @@ -1,54 +0,0 @@ -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/client_state_controller.dart"; -import "package:nexus/models/room.dart"; - -class ViaController extends Notifier { - final Room room; - ViaController(this.room); - - @override - String build() { - final servers = {}; - - void addUserId(String? userId) { - final server = userId?.split(":").lastOrNull; - if (server != null) { - servers.add(server); - } - } - - addUserId(ref.watch(ClientStateController.provider)?.userId); - - final powerLevels = room.events.firstWhereOrNull( - (event) => event.rowId == room.state["m.room.power_levels"]?[""], - ); - - for (final userId in IMap(powerLevels?.content["users"]).keys) { - addUserId(userId); - if (servers.length >= 5) break; - } - - final members = room.state["m.room.member"]?.values.toIList(); - for (var i = 0; servers.length < 5; i++) { - final member = room.events.firstWhereOrNull( - (event) => event.rowId == members?.getOrNull(i), - ); - - if (member?.content["membership"] == "join") { - addUserId(member?.stateKey); - } - - if (members?.getOrNull(i) == null) break; - } - - return servers.isEmpty - ? "" - : "?${servers.map((server) => "via=$server").join("&")}"; - } - - static final provider = NotifierProvider.family( - ViaController.new, - ); -} diff --git a/lib/helpers/extensions/get_localpart.dart b/lib/helpers/extensions/get_localpart.dart deleted file mode 100644 index 445351f..0000000 --- a/lib/helpers/extensions/get_localpart.dart +++ /dev/null @@ -1,3 +0,0 @@ -extension GetLocalpart on String { - String get localpart => substring(1).split(":").first; -} diff --git a/lib/helpers/extensions/join_room_with_snackbars.dart b/lib/helpers/extensions/join_room_with_snackbars.dart new file mode 100644 index 0000000..05b045d --- /dev/null +++ b/lib/helpers/extensions/join_room_with_snackbars.dart @@ -0,0 +1,90 @@ +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/key_controller.dart"; +import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/helpers/extensions/link_to_mention.dart"; +import "package:nexus/models/requests/join_room_request.dart"; + +extension JoinRoomWithSnackbars on ClientController { + Future joinRoomWithSnackBars( + BuildContext context, + String roomAlias, + WidgetRef ref, + ) async { + final roomIdOrAlias = roomAlias.mention ?? roomAlias; + + final scaffoldMessenger = ScaffoldMessenger.of(context); + + final snackbar = scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Joining room $roomIdOrAlias."), + duration: Duration(days: 999), + ), + ); + + try { + final id = await joinRoom( + JoinRoomRequest( + roomIdOrAlias: roomIdOrAlias, + via: IList(Uri.tryParse(roomAlias)?.queryParametersAll["via"] ?? []), + ), + ); + + snackbar.close(); + + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text("Room $roomIdOrAlias successfully joined."), + action: SnackBarAction( + label: "Open", + onPressed: () async { + final spaces = ref.watch(SpacesController.provider); + final space = spaces.firstWhereOrNull((space) => space.id == id); + + await ref + .watch( + KeyController.provider(KeyController.spaceKey).notifier, + ) + .set( + space?.id ?? + spaces + .firstWhere( + (space) => space.children.any( + (child) => child.metadata?.id == id, + ), + ) + .id, + ); + + if (space == null) { + await ref + .watch( + KeyController.provider(KeyController.roomKey).notifier, + ) + .set(id); + } + }, + ), + ), + ); + } catch (error) { + snackbar.close(); + if (context.mounted) { + scaffoldMessenger.showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).colorScheme.errorContainer, + content: Text( + error.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ); + } + } + } +} diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart index f4868d3..b0e62aa 100644 --- a/lib/helpers/extensions/link_to_mention.dart +++ b/lib/helpers/extensions/link_to_mention.dart @@ -30,8 +30,7 @@ extension LinkToMention on String { final identifier = uri.pathSegments.last; if (identifier.isNotEmpty) { return "${switch (uri.pathSegments.firstOrNull) { - "r" => "#", - "roomid" => "!", + "r" || "roomid" => "#", "u" => "@", _ => "", }}${Uri.decodeComponent(identifier)}"; diff --git a/lib/helpers/extensions/mxc_to_https.dart b/lib/helpers/extensions/mxc_to_https.dart index 910f87d..468da12 100644 --- a/lib/helpers/extensions/mxc_to_https.dart +++ b/lib/helpers/extensions/mxc_to_https.dart @@ -1,5 +1,4 @@ extension MxcToHttps on Uri { - Uri mxcToHttps(String homeserver) => Uri.parse( - homeserver, - ).resolve("_matrix/client/v1/media/download/$host$path"); + Uri mxcToHttps(String homeserver) => + Uri.parse("${homeserver}_matrix/client/v1/media/download/$host$path"); } diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index df68a05..aff5d52 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -7,13 +7,8 @@ extension SchemeToTheme on ColorScheme { titleSpacing: 0, backgroundColor: surfaceContainerLow, ), - menuTheme: MenuThemeData( - style: MenuStyle( - backgroundColor: WidgetStatePropertyAll(primaryContainer), - ), - ), textTheme: ThemeData( - fontFamilyFallback: ["sans", "emoji"], + fontFamilyFallback: ["sans"], brightness: brightness, ).textTheme, inputDecorationTheme: const InputDecorationTheme( diff --git a/lib/helpers/extensions/show_context_menu.dart b/lib/helpers/extensions/show_context_menu.dart index 7d8cab6..f4762c3 100644 --- a/lib/helpers/extensions/show_context_menu.dart +++ b/lib/helpers/extensions/show_context_menu.dart @@ -9,7 +9,6 @@ extension ShowContextMenu on BuildContext { showMenu( context: this, - constraints: BoxConstraints.loose(Size.infinite), position: RelativeRect.fromLTRB( globalPosition.dx, globalPosition.dy, diff --git a/lib/helpers/extensions/show_user_popover.dart b/lib/helpers/extensions/show_user_popover.dart deleted file mode 100644 index 1698879..0000000 --- a/lib/helpers/extensions/show_user_popover.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:flutter/material.dart"; -import "package:nexus/helpers/extensions/show_context_menu.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/widgets/chat_page/user_popover.dart"; - -extension ShowUserPopover on BuildContext { - void showUserPopover(Membership member, {required Offset globalPosition}) => - showContextMenu( - globalPosition: globalPosition, - children: [ - PopupMenuItem( - enabled: false, - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: IconTheme(data: IconThemeData(), child: UserPopover(member)), - ), - ], - ); -} diff --git a/lib/main.dart b/lib/main.dart index 846f075..5ad6c24 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/header_controller.dart"; +import "package:nexus/controllers/init_complete_controller.dart"; import "package:nexus/controllers/multi_provider_controller.dart"; import "package:nexus/controllers/shared_prefs_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; @@ -17,6 +18,7 @@ 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(); @@ -57,11 +59,14 @@ void showError(Object error, [StackTrace? stackTrace]) { void main() async { WidgetsFlutterBinding.ensureInitialized(); - if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - await windowManager.ensureInitialized(); - await windowManager.waitUntilReadyToShow( - WindowOptions(titleBarStyle: TitleBarStyle.hidden), - ); + await windowManager.ensureInitialized(); + await windowManager.waitUntilReadyToShow( + WindowOptions(titleBarStyle: TitleBarStyle.hidden), + ); + + if (Platform.isLinux) { + setWindowMinSize(const Size.square(500)); + } else { await windowManager.setMinimumSize(Size.square(500)); } @@ -126,7 +131,9 @@ class App extends StatelessWidget { } else if (!clientState.isVerified) { return VerifyPage(); } else { - return ChatPage(); + return ref.watch(InitCompleteController.provider) + ? ChatPage() + : Loading(); } }, ), diff --git a/lib/models/configs/power_level_config.dart b/lib/models/configs/power_level_config.dart deleted file mode 100644 index 31cc08c..0000000 --- a/lib/models/configs/power_level_config.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/requests/membership_action.dart"; -part "power_level_config.freezed.dart"; -part "power_level_config.g.dart"; - -@freezed -abstract class PowerLevelConfig with _$PowerLevelConfig { - const factory PowerLevelConfig({ - @Default(false) bool isStateEvent, - required String eventType, - MembershipAction? action, - String? targetUser, - }) = _PowerLevelConfig; - - factory PowerLevelConfig.fromJson(Map json) => - _$PowerLevelConfigFromJson(json); -} diff --git a/lib/models/emoji.dart b/lib/models/emoji.dart deleted file mode 100644 index 8e4eac6..0000000 --- a/lib/models/emoji.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -part "emoji.freezed.dart"; -part "emoji.g.dart"; - -@freezed -abstract class Emoji with _$Emoji { - const factory Emoji({ - required String emoji, - required String category, - required IList aliases, - required String description, - required IList tags, - }) = _Emoji; - - factory Emoji.fromJson(Map json) => _$EmojiFromJson(json); -} diff --git a/lib/models/membership.dart b/lib/models/membership.dart deleted file mode 100644 index ce0cc42..0000000 --- a/lib/models/membership.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/models/membership_status.dart"; -part "membership.freezed.dart"; - -@freezed -abstract class Membership with _$Membership { - const Membership._(); - const factory Membership({ - required MembershipStatus status, - required Uri? avatarUrl, - required String displayName, - required String userId, - }) = _Membership; - - factory Membership.fromContent( - IMap content, - String userId, - String homeserver, - ) => Membership( - status: MembershipStatus.values.firstWhere( - (status) => status.name == content["membership"], - orElse: () => MembershipStatus.leave, - ), - avatarUrl: Uri.tryParse( - content["avatar_url"] ?? "", - )?.mxcToHttps(homeserver), - userId: userId, - displayName: content["displayname"] ?? userId.substring(1).split(":").first, - ); -} diff --git a/lib/models/membership_status.dart b/lib/models/membership_status.dart deleted file mode 100644 index bc85e22..0000000 --- a/lib/models/membership_status.dart +++ /dev/null @@ -1,4 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -@JsonEnum() -enum MembershipStatus { leave, invite, ban, join } diff --git a/lib/models/configs/message_config.dart b/lib/models/message_config.dart similarity index 87% rename from lib/models/configs/message_config.dart rename to lib/models/message_config.dart index 66a437c..9020f78 100644 --- a/lib/models/configs/message_config.dart +++ b/lib/models/message_config.dart @@ -18,10 +18,10 @@ abstract class MessageConfig with _$MessageConfig { bool operator ==(Object other) => other.runtimeType == runtimeType && other is MessageConfig && - other.event == event; + other.event.eventId == event.eventId; @override - int get hashCode => Object.hash(runtimeType, event); + int get hashCode => Object.hash(runtimeType, event.eventId); factory MessageConfig.fromJson(Map json) => _$MessageConfigFromJson(json); diff --git a/lib/models/configs/messages_config.dart b/lib/models/messages_config.dart similarity index 100% rename from lib/models/configs/messages_config.dart rename to lib/models/messages_config.dart diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 584f27b..d92b4f6 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -3,22 +3,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; part "profile.freezed.dart"; part "profile.g.dart"; -Object? readPronouns(Map map, _) => - map["m.pronouns"] ?? map["io.fsky.nyx.pronouns"]; - -Object? readTimezone(Map map, _) => - map["m.tz"] ?? map["us.cloke.msc4175.tz"]; - @freezed abstract class Profile with _$Profile { const factory Profile({ String? avatarUrl, @JsonKey(name: "displayname") String? displayName, - - @JsonKey(readValue: readTimezone) String? timezone, + @JsonKey(name: "us.cloke.msc4175.tz") String? timezone, @Default(IList.empty()) - @JsonKey(readValue: readPronouns) + @JsonKey(name: "io.fsky.nyx.pronouns") IList pronouns, }) = _Profile; diff --git a/lib/models/requests/get_room_state_request.dart b/lib/models/requests/get_room_state_request.dart index 8ee05f0..a154d5f 100644 --- a/lib/models/requests/get_room_state_request.dart +++ b/lib/models/requests/get_room_state_request.dart @@ -6,8 +6,7 @@ part "get_room_state_request.g.dart"; abstract class GetRoomStateRequest with _$GetRoomStateRequest { const factory GetRoomStateRequest({ required String roomId, - @Default(false) bool refetch, - @Default(false) bool fetchMembers, + required bool fetchMembers, @Default(false) bool includeMembers, }) = _GetRoomStateRequest; diff --git a/lib/models/requests/membership_action.dart b/lib/models/requests/membership_action.dart deleted file mode 100644 index d852164..0000000 --- a/lib/models/requests/membership_action.dart +++ /dev/null @@ -1,4 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -@JsonEnum() -enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/models/requests/send_event_request.dart b/lib/models/requests/send_event_request.dart deleted file mode 100644 index da5de32..0000000 --- a/lib/models/requests/send_event_request.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -part "send_event_request.freezed.dart"; -part "send_event_request.g.dart"; - -@freezed -abstract class SendEventRequest with _$SendEventRequest { - const factory SendEventRequest({ - required String roomId, - required String type, - required Map content, - @Default(false) bool synchronous, - @Default(false) bool disableEncryption, - }) = _SendEventRequest; - - factory SendEventRequest.fromJson(Map json) => - _$SendEventRequestFromJson(json); -} diff --git a/lib/models/requests/set_membership_request.dart b/lib/models/requests/set_membership_request.dart deleted file mode 100644 index dd0e1f2..0000000 --- a/lib/models/requests/set_membership_request.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/requests/membership_action.dart"; -part "set_membership_request.freezed.dart"; -part "set_membership_request.g.dart"; - -@freezed -abstract class SetMembershipRequest with _$SetMembershipRequest { - const factory SetMembershipRequest({ - required String userId, - required String roomId, - - String? reason, - @JsonKey(name: "action") required MembershipAction action, - @Default(false) @JsonKey(name: "msc4293_redact_events") bool redact, - }) = _SetMembershipRequest; - - factory SetMembershipRequest.fromJson(Map json) => - _$SetMembershipRequestFromJson(json); -} diff --git a/lib/models/sync_status.dart b/lib/models/sync_status.dart index 7848fbe..42c5f2a 100644 --- a/lib/models/sync_status.dart +++ b/lib/models/sync_status.dart @@ -14,5 +14,5 @@ abstract class SyncStatus with _$SyncStatus { _$SyncStatusFromJson(json); } -@JsonEnum(fieldRename: FieldRename.kebab) +@JsonEnum(fieldRename: FieldRename.snake) enum SyncStatusType { ok, waiting, erroring, permanentlyFailed } diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 671891c..ee2f4d0 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,10 +1,7 @@ import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/init_complete_controller.dart"; -import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/chat_page/sidebar.dart"; import "package:nexus/widgets/chat_page/room_chat.dart"; -import "package:nexus/widgets/loading.dart"; class ChatPage extends ConsumerWidget { const ChatPage({super.key}); @@ -14,33 +11,22 @@ class ChatPage extends ConsumerWidget { builder: (context, constraints) { final isDesktop = constraints.maxWidth > 650; final showMembersByDefault = constraints.maxWidth > 1000; - final initComplete = ref.watch(InitCompleteController.provider); return Scaffold( - appBar: initComplete ? null : Appbar(), - body: initComplete - ? Builder( - builder: (context) => Row( - children: [ - if (isDesktop) Sidebar(isDesktop: isDesktop), - Expanded( - child: RoomChat( - isDesktop: isDesktop, - showMembersByDefault: showMembersByDefault, - ), - ), - ], - ), - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [Loading(), Text("Syncing...")], + body: Builder( + builder: (context) => Row( + children: [ + if (isDesktop) Sidebar(), + Expanded( + child: RoomChat( + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, ), ), - drawer: isDesktop || !initComplete - ? null - : Sidebar(isDesktop: isDesktop), + ], + ), + ), + drawer: isDesktop ? null : Sidebar(), ); }, ); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index d2153eb..bd41d51 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -62,7 +62,7 @@ class LoginPage extends HookConsumerWidget { children: [ Row( children: [ - SvgPicture.asset("assets/icon.svg", width: 128), + SvgPicture.asset("assets/icon.svg"), SizedBox(width: 12), Expanded( child: Column( @@ -175,7 +175,7 @@ class LoginPage extends HookConsumerWidget { ElevatedButton( onPressed: () async { isLoading.value = true; - final error = await client.login( + final succeeded = await client.login( LoginRequest( username: username.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( SnackBar( content: Text( - "Login failed. Is your password right?\nError: $error", + "Login failed. Is your password right?", style: TextStyle( color: theme.colorScheme.onErrorContainer, ), diff --git a/lib/pages/verify_page.dart b/lib/pages/verify_page.dart index 962701c..1011f80 100644 --- a/lib/pages/verify_page.dart +++ b/lib/pages/verify_page.dart @@ -2,7 +2,6 @@ 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 { @@ -12,75 +11,72 @@ class VerifyPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final passphraseController = useTextEditingController(); final isVerifying = useState(false); - 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"), + 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", ), ], ), + 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 aae6c13..5b14244 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -35,14 +35,15 @@ 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: IgnorePointer(child: title), - flexibleSpace: GestureDetector(onDoubleTap: maximize), + title: title, actions: [ ...actions, if (!(Platform.isAndroid || Platform.isIOS)) ...[ diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 28662e2..8e93b6b 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -2,8 +2,10 @@ import "package:color_hash/color_hash.dart"; import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/cross_cache_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/extensions/mxc_to_https.dart"; class AvatarOrHash extends ConsumerWidget { final Uri? avatar; @@ -46,11 +48,20 @@ class AvatarOrHash extends ConsumerWidget { ? fallback ?? box : Image( image: CachedNetworkImage( - avatar.toString(), + avatar! + .mxcToHttps( + ref.watch( + ClientStateController.provider.select( + (value) => value?.homeserverUrl, + ), + ) ?? + "", + ) + .toString(), ref.watch(CrossCacheController.provider), headers: ref.headers, ), - fit: BoxFit.cover, + fit: BoxFit.contain, errorBuilder: (_, _, _) => box, ), ), diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart new file mode 100644 index 0000000..b9e7dbb --- /dev/null +++ b/lib/widgets/chat_page/chat_box.dart @@ -0,0 +1,178 @@ +import "dart:io"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:fluttertagger/fluttertagger.dart"; +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"; + +class ChatBox extends HookConsumerWidget { + final Message? relatedMessage; + final RelationType relationType; + final VoidCallback onDismiss; + final Room room; + const ChatBox({ + required this.relatedMessage, + required this.relationType, + required this.onDismiss, + required this.room, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final controller = useRef(FlutterTaggerController()); + final triggerCharacter = useState(""); + final shouldMention = useState(true); + final query = useState(""); + + if (relationType == RelationType.edit && + relatedMessage is TextMessage && + controller.value.text.isEmpty) { + controller.value.text = relatedMessage?.metadata?["editSource"] ?? ""; + } + + void send() { + if (controller.value.text.trim().isEmpty || room.metadata == null) return; + ref + .watch(RoomChatController.provider(room.metadata!.id).notifier) + .send( + controller.value.formattedText, + shouldMention: shouldMention.value, + relation: relatedMessage, + relationType: relationType, + tags: controller.value.tags, + ); + onDismiss(); + controller.value.text = ""; + } + + 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; + } + } + + return KeyEventResult.ignored; + }, + )..requestFocus(); + + final style = TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ); + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: EdgeInsetsGeometry.all(12), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: Column( + children: [ + RelationPreview( + shouldMention: shouldMention.value, + toggleShouldMention: () => + shouldMention.value = !shouldMention.value, + relatedMessage: relatedMessage, + relationType: relationType, + onDismiss: onDismiss, + ), + Container( + color: theme.colorScheme.surfaceContainerHighest, + padding: EdgeInsets.symmetric(horizontal: 8), + child: Row( + spacing: 8, + children: [ + PopupMenuButton( + tooltip: "Add media", + itemBuilder: (context) => [ + PopupMenuItem( + child: ListTile( + title: Text("Camera"), + leading: Icon(Icons.add_a_photo), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text("Gallery"), + leading: Icon(Icons.add_photo_alternate), + ), + ), + PopupMenuItem( + child: ListTile( + title: Text("Files"), + leading: Icon(Icons.attachment), + ), + ), + ], + icon: Icon(Icons.add), + // enabled: room.canSendDefaultMessages, TODO: Permissions check + ), + Expanded( + child: FlutterTagger( + triggerStrategy: TriggerStrategy.eager, + overlay: MentionOverlay( + room, + query: query.value, + triggerCharacter: triggerCharacter.value, + addTag: ({required id, required name}) { + controller.value.addTag(id: id, name: name); + node.requestFocus(); + }, + ), + controller: controller.value, + onSearch: (newQuery, newTriggerCharacter) { + triggerCharacter.value = newTriggerCharacter; + query.value = newQuery; + }, + triggerCharacterAndStyles: {"@": style, "#": style}, + builder: (context, key) => TextFormField( + // enabled: room.canSendDefaultMessages, + maxLines: 12, + minLines: 1, + decoration: InputDecoration( + hintText: + true // TODO: room.canSendDefaultMessages + ? "Your message here..." + : "You don't have permission to send messages in this room...", + border: InputBorder.none, + ), + controller: controller.value, + key: key, + autofocus: true, + focusNode: node, + ), + ), + ), + IconButton( + onPressed: send, + // onPressed: room.canSendDefaultMessages ? send : null, + icon: Icon(Icons.send), + tooltip: "Send message", + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/chat_page/composer/chat_box.dart b/lib/widgets/chat_page/composer/chat_box.dart deleted file mode 100644 index dee52e1..0000000 --- a/lib/widgets/chat_page/composer/chat_box.dart +++ /dev/null @@ -1,188 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:fluttertagger/fluttertagger.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/power_level_controller.dart"; -import "package:nexus/models/configs/power_level_config.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/chat_page/composer/mention_overlay.dart"; -import "package:nexus/widgets/chat_page/composer/relation_preview.dart"; -import "package:nexus/widgets/chat_page/emoji_picker_button.dart"; - -class ChatBox extends HookConsumerWidget { - final Message? relatedMessage; - final RelationType relationType; - final VoidCallback onDismiss; - final FocusNode? node; - final Future Function( - String text, { - required bool shouldMention, - required IList tags, - }) - onSend; - const ChatBox({ - required this.relatedMessage, - required this.relationType, - required this.onDismiss, - required this.onSend, - this.node, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final controller = useRef(FlutterTaggerController()); - final triggerCharacter = useState(""); - final shouldMention = useState(true); - final query = useState(""); - - if (relationType == RelationType.edit && - relatedMessage is TextMessage && - controller.value.text.isEmpty) { - controller.value.text = relatedMessage?.metadata?["editSource"] ?? ""; - } - - void send() { - if (controller.value.text.isEmpty) return; - onSend( - controller.value.formattedText, - shouldMention: shouldMention.value, - tags: controller.value.tags.toIList(), - ); - - onDismiss(); - controller.value.text = ""; - } - - final style = TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ); - - final canSendMessages = ref.watch( - PowerLevelController.provider( - PowerLevelConfig(eventType: "m.room.message"), - ), - ); - - return Positioned( - bottom: 0, - left: 0, - right: 0, - child: Padding( - padding: EdgeInsetsGeometry.all(12), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: Column( - children: [ - RelationPreview( - relatedMessage, - shouldMention: shouldMention.value, - toggleShouldMention: () => - shouldMention.value = !shouldMention.value, - relationType: relationType, - onDismiss: onDismiss, - ), - Container( - color: theme.colorScheme.surfaceContainerHighest, - padding: EdgeInsets.symmetric(horizontal: 8), - child: Row( - spacing: 8, - mainAxisAlignment: MainAxisAlignment.center, - children: canSendMessages - ? [ - EmojiPickerButton( - context: context, - onSelection: (_) => node?.requestFocus(), - controller: controller.value, - ), - PopupMenuButton( - tooltip: "Add media", - enabled: canSendMessages, - itemBuilder: (context) => [ - PopupMenuItem( - child: ListTile( - title: Text("Camera"), - leading: Icon(Icons.add_a_photo), - ), - ), - PopupMenuItem( - child: ListTile( - title: Text("Gallery"), - leading: Icon(Icons.add_photo_alternate), - ), - ), - PopupMenuItem( - child: ListTile( - title: Text("Files"), - leading: Icon(Icons.attachment), - ), - ), - ], - icon: Icon(Icons.add), - ), - Expanded( - child: FlutterTagger( - triggerStrategy: TriggerStrategy.eager, - overlay: MentionOverlay( - query: query.value, - triggerCharacter: triggerCharacter.value, - addTag: ({required id, required name}) { - controller.value.addTag(id: id, name: name); - node?.requestFocus(); - }, - ), - controller: controller.value, - onSearch: (newQuery, newTriggerCharacter) { - triggerCharacter.value = newTriggerCharacter; - query.value = newQuery; - }, - triggerCharacterAndStyles: { - "@": style, - "#": style, - }, - builder: (context, key) => TextFormField( - enabled: canSendMessages, - maxLines: 12, - minLines: 1, - autofocus: true, - decoration: InputDecoration( - hintText: "Your message here...", - border: InputBorder.none, - ), - controller: controller.value, - key: key, - onFieldSubmitted: (_) => send(), - // Don't defocus on submit - onEditingComplete: () {}, - textInputAction: TextInputAction.done, - focusNode: node, - ), - ), - ), - IconButton( - onPressed: !canSendMessages ? null : send, - icon: Icon(Icons.send), - tooltip: "Send message", - ), - ] - : [ - Padding( - padding: EdgeInsetsGeometry.all(8), - child: Text( - "You don't have permission to send messages in this room...", - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/chat_page/composer/mention_overlay.dart b/lib/widgets/chat_page/composer/mention_overlay.dart deleted file mode 100644 index b650421..0000000 --- a/lib/widgets/chat_page/composer/mention_overlay.dart +++ /dev/null @@ -1,128 +0,0 @@ -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/members_by_type_controller.dart"; -import "package:nexus/controllers/rooms_controller.dart"; -import "package:nexus/controllers/via_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/membership_status.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/loading.dart"; - -class MentionOverlay extends ConsumerWidget { - final String? triggerCharacter; - final String query; - final void Function({required String id, required String name}) addTag; - const MentionOverlay({ - required this.query, - required this.addTag, - required this.triggerCharacter, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final rooms = ref.watch(RoomsController.provider); - - return Padding( - padding: EdgeInsets.all(8), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - padding: EdgeInsets.all(8), - child: switch (triggerCharacter) { - "@" => - ref - .watch( - MembersByTypeController.provider(MembershipStatus.join), - ) - .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}](matrix:u/${member.userId.substring(1)})", - name: member.userId - .substring(1) - .split(":") - .first, - ), - ), - ) - .toList(), - ), - ), - "#" => ListView( - children: - (query.isEmpty - ? rooms.values - : rooms.values.where( - (room) => - (room.metadata?.name ?? room.metadata!.id) - .toLowerCase() - .contains(query.toLowerCase()), - )) - .map((room) { - final name = - room.metadata?.name ?? - room.metadata!.canonicalAlias ?? - room.metadata!.id; - return ListTile( - leading: AvatarOrHash( - room.metadata?.avatar, - name, - fallback: Icon(Icons.numbers), - ), - title: Text(name), - subtitle: room.metadata?.topic == null - ? null - : Text(room.metadata!.topic!, maxLines: 1), - onTap: () { - final vias = ref.watch( - ViaController.provider(room), - ); - addTag( - id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)", - name: - (room.metadata?.canonicalAlias ?? - room.metadata?.id) - ?.substring(1) - .split(":") - .first ?? - "", - ); - }, - ); - }) - .toList(), - ), - - _ => Loading(), - }, - ), - ), - ); - } -} diff --git a/lib/widgets/chat_page/emoji_picker_button.dart b/lib/widgets/chat_page/emoji_picker_button.dart deleted file mode 100644 index e8805ca..0000000 --- a/lib/widgets/chat_page/emoji_picker_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import "package:emoji_text_field/emoji_text_field.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/emoji_controller.dart"; - -class EmojiPickerButton extends HookConsumerWidget { - final TextEditingController? controller; - final void Function(String emoji)? onSelection; - final VoidCallback? onPressed; - final BuildContext context; - const EmojiPickerButton({ - this.controller, - this.onPressed, - this.onSelection, - required this.context, - super.key, - }); - - @override - Widget build(_, WidgetRef ref) => IconButton( - onPressed: () async { - onPressed?.call(); - final controller = this.controller ?? TextEditingController(); - - final emojis = await ref.watch(EmojiController.provider.future); - if (context.mounted) { - showModalBottomSheet( - context: context, - builder: (context) => EmojiKeyboardView( - config: EmojiViewConfig( - showRecentTab: false, - customCategories: emojis.$1.unlock, - customKeywords: emojis.$2.unlock, - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - height: 600, - ), - textController: controller - ..addListener(() { - // Without this, there will sometimes be a debugLocked is not true error sometimes - Future.delayed(Duration.zero, () { - if (context.mounted) Navigator.of(context).pop(); - }); - onSelection?.call(controller.text); - }), - ), - ); - } - }, - icon: Icon(Icons.emoji_emotions), - ); -} diff --git a/lib/widgets/chat_page/expandable_image.dart b/lib/widgets/chat_page/expandable_image.dart deleted file mode 100644 index ac5bbe1..0000000 --- a/lib/widgets/chat_page/expandable_image.dart +++ /dev/null @@ -1,48 +0,0 @@ -import "dart:math"; -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/cross_cache_controller.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/widgets/error_dialog.dart"; - -class ExpandableImage extends ConsumerWidget { - final Widget child; - final String? source; - const ExpandableImage(this.source, {required this.child, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => InkWell( - onTap: source == null - ? null - : () => showDialog( - context: context, - builder: (_) => LayoutBuilder( - builder: (context, constraints) => Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.all(constraints.maxWidth / 100), - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: min(constraints.maxWidth, 1000), - ), - child: InteractiveViewer( - child: Image( - fit: BoxFit.contain, - errorBuilder: (_, error, stackTrace) => ErrorDialog( - "Loading failed for $source\nError: $error", - stackTrace, - ), - image: CachedNetworkImage( - source!, - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - ), - ), - ), - ), - ), - ), - child: child, - ); -} diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart index fb533ad..1e1ab82 100644 --- a/lib/widgets/chat_page/html/html.dart +++ b/lib/widgets/chat_page/html/html.dart @@ -1,15 +1,12 @@ -import "package:cross_cache/cross_cache.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/cross_cache_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/helpers/launch_helper.dart"; -import "package:nexus/widgets/chat_page/expandable_image.dart"; import "package:nexus/widgets/chat_page/html/mention_chip.dart"; import "package:nexus/widgets/chat_page/html/spoiler_text.dart"; import "package:nexus/widgets/chat_page/html/code_block.dart"; @@ -25,37 +22,24 @@ 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)); } - final height = - int.tryParse(element.attributes["height"] ?? "") ?? - (element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ?? - 300; + final height = int.tryParse(element.attributes["height"] ?? "") ?? 300; final width = int.tryParse(element.attributes["width"] ?? ""); - final src = Uri.tryParse(element.attributes["src"] ?? "") - ?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value?.homeserverUrl, - ), - ) ?? - "", - ) - .toString(); return switch (element.localName) { "code" => element.parent?.localName == "pre" - ? CodeBlock( - element.text, - lang: element.className.replaceAll("language-", ""), - ) + ? element.outerHtml.contains("
") + ? Html( + """
${element.outerHtml.replaceAll("
", "\n")}
""", + ) + : CodeBlock( + element.text, + lang: element.className.replaceAll("language-", ""), + ) : null, "blockquote" => Quoted(Html(element.innerHtml)), @@ -63,40 +47,39 @@ class Html extends ConsumerWidget { "a" => element.attributes["href"]?.mention == null ? null - : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!), - ), + : InlineCustomWidget(child: MentionChip(element.text)), "img" => - src == null + element.attributes["src"] == null ? SizedBox.shrink() : InlineCustomWidget( alignment: PlaceholderAlignment.middle, - child: ExpandableImage( - src, - child: Image( - image: CachedNetworkImage( - src, - ref.watch(CrossCacheController.provider), - headers: ref.headers, + child: Image.network( + Uri.parse(element.attributes["src"]!) + .mxcToHttps( + ref.watch( + ClientStateController.provider.select( + (value) => value?.homeserverUrl, + ), + ) ?? + "", + ) + .toString(), + headers: ref.headers, + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of(context).colorScheme.error, ), - errorBuilder: (_, error, _) => Text( - "Image Failed to Load", - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ), - height: height.toDouble(), - width: width?.toDouble(), - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null - ? child - : CircularProgressIndicator(), ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), ), ), - - // Allowed elements list ("del" || "h1" || "h2" || diff --git a/lib/widgets/chat_page/html/mention_chip.dart b/lib/widgets/chat_page/html/mention_chip.dart index 575ad03..c2b832d 100644 --- a/lib/widgets/chat_page/html/mention_chip.dart +++ b/lib/widgets/chat_page/html/mention_chip.dart @@ -1,44 +1,25 @@ import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/user_controller.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -class MentionChip extends ConsumerWidget { - final String content; - const MentionChip(this.content, {super.key}); +class MentionChip extends StatelessWidget { + final String label; + const MentionChip(this.label, {super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final membership = content.mention!.startsWith("@") == true - ? ref - .watch(UserController.provider(content.mention!)) - .whenOrNull(data: (data) => data) - : null; - - return InkWell( - onTapUp: (details) { - content.mention; - if (membership != null) { - context.showUserPopover( - membership, - globalPosition: details.globalPosition, - ); - } - }, - child: IgnorePointer( - child: Chip( - label: Text( - (membership == null ? null : "@${membership.displayName}") ?? - content.mention!, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - backgroundColor: Theme.of(context).colorScheme.primary, - ), + Widget build(BuildContext context) => ActionChip( + label: Text( + label.mention ?? label, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, ), - ); - } + ), + backgroundColor: Theme.of(context).colorScheme.primary, + onPressed: () => showDialog( + context: context, + builder: (_) => Dialog( + child: Text("TODO: Open room or join room dialog, or user popover"), + ), + ), + ); } diff --git a/lib/widgets/chat_page/expandable_image_message.dart b/lib/widgets/chat_page/image_message.dart similarity index 55% rename from lib/widgets/chat_page/expandable_image_message.dart rename to lib/widgets/chat_page/image_message.dart index f6e8a03..103fdd2 100644 --- a/lib/widgets/chat_page/expandable_image_message.dart +++ b/lib/widgets/chat_page/image_message.dart @@ -1,3 +1,4 @@ +import "dart:math"; import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; @@ -5,7 +6,6 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flyer_chat_image_message/flyer_chat_image_message.dart"; import "package:nexus/controllers/cross_cache_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/widgets/chat_page/expandable_image.dart"; class ExpandableImageMessage extends ConsumerWidget { final ImageMessage message; @@ -14,8 +14,31 @@ class ExpandableImageMessage extends ConsumerWidget { const ExpandableImageMessage(this.message, {required this.index, super.key}); @override - Widget build(BuildContext context, WidgetRef ref) => ExpandableImage( - message.source, + Widget build(BuildContext context, WidgetRef ref) => InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => LayoutBuilder( + builder: (context, constraints) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(constraints.maxWidth / 100), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: min(constraints.maxWidth, 1000), + ), + child: InteractiveViewer( + child: Image( + fit: BoxFit.contain, + image: CachedNetworkImage( + message.source, + ref.watch(CrossCacheController.provider), + headers: ref.headers, + ), + ), + ), + ), + ), + ), + ), child: FlyerChatImageMessage( customImageProvider: CachedNetworkImage( message.source, diff --git a/lib/widgets/chat_page/join_dialog.dart b/lib/widgets/chat_page/join_dialog.dart deleted file mode 100644 index e718200..0000000 --- a/lib/widgets/chat_page/join_dialog.dart +++ /dev/null @@ -1,137 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -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/controllers/key_controller.dart"; -import "package:nexus/controllers/spaces_controller.dart"; -import "package:nexus/helpers/extensions/link_to_mention.dart"; -import "package:nexus/models/requests/join_room_request.dart"; -import "package:nexus/widgets/form_text_input.dart"; - -class JoinDialog extends HookWidget { - final WidgetRef ref; - const JoinDialog(this.ref, {super.key}); - - @override - Widget build(BuildContext context) { - final roomAlias = useTextEditingController(); - return AlertDialog( - title: Text("Join a Room"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Enter the room alias, Matrix URI, or Matrix.to link."), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: roomAlias, - title: "#room:server", - ), - ], - ), - actions: [ - TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - if (context.mounted) { - final roomIdOrAlias = roomAlias.text.mention ?? roomAlias.text; - - final scaffoldMessenger = ScaffoldMessenger.of(context); - - final snackbar = scaffoldMessenger.showSnackBar( - SnackBar( - content: Text("Joining room $roomIdOrAlias."), - duration: Duration(days: 999), - ), - ); - - try { - final id = await ref - .watch(ClientController.provider.notifier) - .joinRoom( - JoinRoomRequest( - roomIdOrAlias: roomIdOrAlias, - via: IList( - Uri.tryParse( - roomAlias.text.replaceAll("/#", ""), - )?.queryParametersAll["via"] ?? - [], - ), - ), - ); - - snackbar.close(); - - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text("Room $roomIdOrAlias successfully joined."), - action: SnackBarAction( - label: "Open", - onPressed: () async { - final spaces = ref.watch(SpacesController.provider); - final space = spaces.firstWhereOrNull( - (space) => space.id == id, - ); - - await ref - .watch( - KeyController.provider( - KeyController.spaceKey, - ).notifier, - ) - .set( - space?.id ?? - spaces - .firstWhere( - (space) => space.children.any( - (child) => child.metadata?.id == id, - ), - ) - .id, - ); - - if (space == null) { - await ref - .watch( - KeyController.provider( - KeyController.roomKey, - ).notifier, - ) - .set(id); - } - }, - ), - ), - ); - } catch (error) { - snackbar.close(); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - backgroundColor: Theme.of( - context, - ).colorScheme.errorContainer, - content: Text( - error.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } - } - }, - child: Text("Join"), - ), - ], - ); - } -} diff --git a/lib/widgets/chat_page/lazy_loading/message_avatar.dart b/lib/widgets/chat_page/lazy_loading/message_avatar.dart deleted file mode 100644 index dc8dfef..0000000 --- a/lib/widgets/chat_page/lazy_loading/message_avatar.dart +++ /dev/null @@ -1,32 +0,0 @@ -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/author_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; - -class MessageAvatar extends ConsumerWidget { - final Message message; - final double height; - const MessageAvatar(this.message, {this.height = 16, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) => ref - .watch(AuthorController.provider(message)) - .betterWhen( - data: (membership) => InkWell( - onTapUp: (details) => context.showUserPopover( - membership, - globalPosition: details.globalPosition, - ), - child: 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 deleted file mode 100644 index 88d2fa6..0000000 --- a/lib/widgets/chat_page/lazy_loading/message_displayname.dart +++ /dev/null @@ -1,38 +0,0 @@ -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/author_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; - -class MessageDisplayname extends ConsumerWidget { - final Message message; - final TextStyle? style; - final bool clickable; - const MessageDisplayname( - this.message, { - this.clickable = true, - this.style, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) => ref - .watch(AuthorController.provider(message)) - .betterWhen( - data: (membership) => InkWell( - onTapUp: clickable - ? (details) => context.showUserPopover( - membership, - globalPosition: details.globalPosition, - ) - : null, - child: 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 8be1ddd..24d22e4 100644 --- a/lib/widgets/chat_page/member_list.dart +++ b/lib/widgets/chat_page/member_list.dart @@ -1,31 +1,24 @@ import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/members_by_type_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/models/membership_status.dart"; +import "package:nexus/controllers/members_controller.dart"; +import "package:nexus/models/room.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -class MemberList extends HookConsumerWidget { - const MemberList({super.key}); +class MemberList extends ConsumerWidget { + final Room room; + const MemberList(this.room, {super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final status = useState(MembershipStatus.join); - final membersProvider = ref.watch( - MembersByTypeController.provider(status.value), - ); - + final members = ref.watch(MembersController.provider(room)); return Drawer( shape: Border(), - child: Column( - spacing: 8, + child: ListView( children: [ AppBar( scrolledUnderElevation: 0, leading: Icon(Icons.people), - title: Text("Members"), + title: Text("Members (${members.length})"), actionsPadding: EdgeInsets.only(right: 4), actions: [ if (Scaffold.of(context).hasEndDrawer) @@ -36,54 +29,24 @@ class MemberList extends HookConsumerWidget { ), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - FilterChip( - label: Text("Joined"), - onSelected: (value) => status.value = MembershipStatus.join, - selected: status.value == MembershipStatus.join, + ...members.map( + (member) => ListTile( + onTap: () => showDialog( + context: context, + builder: (context) => + Dialog(child: Text("TODO: Open member popover")), ), - FilterChip( - label: Text("Invited"), - onSelected: (value) => status.value = MembershipStatus.invite, - selected: status.value == MembershipStatus.invite, + leading: AvatarOrHash( + Uri.tryParse(member.content["avatar_url"] ?? ""), + member.content["displayname"].toString(), ), - FilterChip( - label: Text("Banned"), - onSelected: (value) => status.value = MembershipStatus.ban, - selected: status.value == MembershipStatus.ban, + title: Text( + member.content["displayname"].toString(), + overflow: TextOverflow.ellipsis, ), - ], - ), - membersProvider.betterWhen( - data: (members) => Expanded( - child: ListView( - children: members - .map( - (member) => InkWell( - onTapUp: (details) => context.showUserPopover( - member, - globalPosition: details.globalPosition, - ), - child: ListTile( - leading: AvatarOrHash( - member.avatarUrl, - member.displayName, - ), - title: Text( - member.displayName, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - member.userId, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ) - .toList(), + subtitle: Text( + member.stateKey ?? "Unknown User", + overflow: TextOverflow.ellipsis, ), ), ), diff --git a/lib/widgets/chat_page/mention_overlay.dart b/lib/widgets/chat_page/mention_overlay.dart new file mode 100644 index 0000000..9858574 --- /dev/null +++ b/lib/widgets/chat_page/mention_overlay.dart @@ -0,0 +1,124 @@ +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/models/room.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/loading.dart"; + +class MentionOverlay extends ConsumerWidget { + final String? triggerCharacter; + final String query; + final Room room; + final void Function({required String id, required String name}) addTag; + const MentionOverlay( + this.room, { + required this.query, + required this.addTag, + required this.triggerCharacter, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final rooms = ref.watch(RoomsController.provider); + + return Padding( + padding: EdgeInsets.all(8), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: Container( + 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) + .split(":") + .first ?? + "Unknown User", + ), + ), + ) + .toList(), + ); + }, + ), + "#" => ListView( + children: + (query.isEmpty + ? rooms.values + : rooms.values.where( + (room) => (room.metadata?.name ?? "Unnamed Room") + .toLowerCase() + .contains(query.toLowerCase()), + )) + .map( + (room) => ListTile( + leading: AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Room", + fallback: Icon(Icons.numbers), + ), + title: Text(room.metadata?.name ?? "Unnamed Room"), + subtitle: room.metadata?.topic == null + ? null + : Text(room.metadata!.topic!, maxLines: 1), + onTap: () => addTag( + id: "[#${room.metadata?.name ?? "Unnamed Room"}](https://matrix.to/#/${room.metadata?.id})", + name: + (room.metadata?.canonicalAlias ?? + room.metadata?.id) + ?.substring(1) + .split(":") + .first ?? + "", + ), + ), + ) + .toList(), + ), + + _ => Loading(), + }, + ), + ), + ); + } +} diff --git a/lib/widgets/chat_page/message_wrapper.dart b/lib/widgets/chat_page/message_wrapper.dart new file mode 100644 index 0000000..da53be0 --- /dev/null +++ b/lib/widgets/chat_page/message_wrapper.dart @@ -0,0 +1,54 @@ +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; + +class MessageWrapper extends StatelessWidget { + final Message message; + final Widget child; + final MessageGroupStatus? groupStatus; + const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); + + @override + Widget build(BuildContext context) => ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: AnimatedContainer( + padding: message.metadata?["flashing"] == true + ? EdgeInsets.all(8) + : EdgeInsets.all(0), + color: message.metadata?["flashing"] == true + ? Theme.of(context).colorScheme.onSurface.withAlpha(50) + : Colors.transparent, + duration: Duration(milliseconds: 250), + child: Row( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + groupStatus?.isFirst != false + ? AvatarOrHash( + Uri.parse(message.metadata?["avatarUrl"] ?? ""), + height: 40, + message.metadata?["displayName"] ?? "", + ) + : SizedBox(width: 40), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + if (groupStatus?.isFirst != false) + Text( + message.metadata?["displayName"] ?? message.authorId, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + child, + ], + ), + ), + ], + ), + ), + ); +} diff --git a/lib/widgets/chat_page/composer/relation_preview.dart b/lib/widgets/chat_page/relation_preview.dart similarity index 61% rename from lib/widgets/chat_page/composer/relation_preview.dart rename to lib/widgets/chat_page/relation_preview.dart index c90b07b..7aa3ae8 100644 --- a/lib/widgets/chat_page/composer/relation_preview.dart +++ b/lib/widgets/chat_page/relation_preview.dart @@ -2,8 +2,7 @@ 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/chat_page/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; class RelationPreview extends ConsumerWidget { final Message? relatedMessage; @@ -11,9 +10,8 @@ class RelationPreview extends ConsumerWidget { final VoidCallback onDismiss; final bool shouldMention; final VoidCallback toggleShouldMention; - - const RelationPreview( - this.relatedMessage, { + const RelationPreview({ + required this.relatedMessage, required this.relationType, required this.onDismiss, required this.shouldMention, @@ -32,38 +30,31 @@ class RelationPreview extends ConsumerWidget { child: Row( spacing: 8, children: [ + SizedBox(width: 4), if (relationType == RelationType.edit) Text( "Editing message:", style: TextStyle(fontWeight: FontWeight.bold), ), - - MessageAvatar(relatedMessage!), - + AvatarOrHash( + Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""), + relatedMessage?.metadata?["displayName"]?.toString() ?? "", + height: 16, + ), + Text( + relatedMessage!.metadata?["displayName"] ?? + relatedMessage!.authorId, + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), Expanded( - child: Row( - spacing: 8, - children: [ - Flexible( - child: MessageDisplayname( - relatedMessage!, - style: theme.textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Text( - relatedMessage?.metadata?["body"] ?? - relatedMessage?.metadata?["eventType"] ?? - "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: theme.textTheme.labelMedium, - ), - ), - ], + child: Text( + relatedMessage?.metadata?["body"] ?? + relatedMessage?.metadata?["eventType"], + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium, + maxLines: 1, ), ), @@ -78,12 +69,11 @@ class RelationPreview extends ConsumerWidget { ), ), ), - IconButton( tooltip: "Cancel ${relationType == RelationType.edit ? "edit" : "reply"}", onPressed: onDismiss, - icon: const Icon(Icons.close), + icon: Icon(Icons.close), iconSize: 20, ), ], diff --git a/lib/widgets/chat_page/reply_widget.dart b/lib/widgets/chat_page/reply_widget.dart index b999be4..cd30acc 100644 --- a/lib/widgets/chat_page/reply_widget.dart +++ b/lib/widgets/chat_page/reply_widget.dart @@ -1,25 +1,27 @@ +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/controllers/selected_room_controller.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/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)?; class ReplyWidget extends ConsumerWidget { final Message message; final bool alwaysShow; + final Room room; final MessageGroupStatus? groupStatus; final OnTapReply onTapReply; const ReplyWidget( this.message, { + required this.room, required this.groupStatus, this.onTapReply, this.alwaysShow = false, @@ -27,75 +29,118 @@ class ReplyWidget extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - final room = ref.watch(SelectedRoomController.provider); - return message.replyToMessageId == null || room == null - ? SizedBox.shrink() - : Padding( - padding: EdgeInsets.only(bottom: 12), - child: Quoted( - ref - .watch( - EventController.provider( - GetEventRequest( - room: room, - eventId: message.replyToMessageId!, - ), + Widget build(BuildContext context, WidgetRef ref) => + message.replyToMessageId == null + ? SizedBox.shrink() + : Padding( + padding: EdgeInsets.only(bottom: 12), + child: Quoted( + ref + .watch( + EventController.provider( + GetEventRequest( + room: room, + eventId: message.replyToMessageId!, ), - ) - .betterWhen( - loading: () => Text("Fetching event..."), - data: (event) => event == null - ? SizedBox.shrink() - : ref - .watch( - MessageController.provider( - MessageConfig(room: room, event: event), - ), - ) - .betterWhen( - loading: () => Text("Parsing message..."), - data: (replyMessage) { - if (replyMessage == null) { - return SizedBox.shrink(); - } - - return InkWell( - onTap: () => onTapReply?.call(replyMessage), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - MessageAvatar(replyMessage), - Flexible( - child: MessageDisplayname( - replyMessage, - clickable: false, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - Flexible( - child: Text( - replyMessage.metadata!["body"], - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.labelMedium, - maxLines: 1, - ), - ), - ], - ), - ); - }, - ), ), - ), - ); - } + ) + .betterWhen( + loading: () => Text("Fetching event..."), + data: (event) => event == null + ? SizedBox.shrink() + : ref + .watch( + MessageController.provider( + MessageConfig(room: room, event: event), + ), + ) + .betterWhen( + loading: () => Text("Parsing message..."), + data: (replyMessage) { + if (replyMessage == null) { + 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, + ), + Flexible( + child: Text( + replyMessage + .metadata?["displayName"] ?? + replyMessage.authorId, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + replyText, + overflow: TextOverflow.ellipsis, + style: Theme.of( + context, + ).textTheme.labelMedium, + maxLines: 1, + ), + ), + ], + ), + ); + }, + ), + ), + ), + ); } diff --git a/lib/widgets/chat_page/room_appbar.dart b/lib/widgets/chat_page/room_appbar.dart index 62e282d..436bcb9 100644 --- a/lib/widgets/chat_page/room_appbar.dart +++ b/lib/widgets/chat_page/room_appbar.dart @@ -1,20 +1,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; +import "package:nexus/models/room.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/chat_page/expandable_image.dart"; import "package:nexus/widgets/chat_page/room_menu.dart"; -class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { +class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { final bool isDesktop; - final void Function(BuildContext context)? onOpenMemberList; + final Room room; + final void Function(BuildContext context) onOpenMemberList; final void Function(BuildContext context) onOpenDrawer; - const RoomAppbar({ + const RoomAppbar( + this.room, { required this.isDesktop, + required this.onOpenMemberList, required this.onOpenDrawer, - this.onOpenMemberList, super.key, }); @@ -22,57 +22,47 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { Size get preferredSize => AppBar().preferredSize; @override - Widget build(BuildContext context, WidgetRef ref) { - final room = ref.watch(SelectedRoomController.provider); - return Appbar( - leading: isDesktop - ? room == null - ? null - : ExpandableImage( - room.metadata?.avatar?.toString(), - child: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Rooms", - height: 24, - fallback: Icon(Icons.numbers), - ), - ) - : DrawerButton(onPressed: () => onOpenDrawer(context)), - scrolledUnderElevation: 0, - title: room == null - ? null - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - room.metadata?.name ?? "Unnamed Room", - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - if (room.metadata?.topic?.isNotEmpty == true) - Text( - room.metadata!.topic!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], + Widget build(BuildContext context) => Appbar( + leading: isDesktop + ? AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Rooms", + height: 24, + fallback: Icon(Icons.numbers), + ) + : DrawerButton(onPressed: () => onOpenDrawer(context)), + scrolledUnderElevation: 0, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + room.metadata?.name ?? "Unnamed Room", + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + if (room.metadata?.topic?.isNotEmpty == true) + Text( + room.metadata!.topic!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - actions: [ - IconButton( - onPressed: null, - icon: Icon(Icons.push_pin), - tooltip: "Open pinned messages", - ), - IconButton( - onPressed: () => onOpenMemberList?.call(context), - tooltip: "Open member list", - icon: Icon(Icons.people), - ), - if (room != null) RoomMenu(room), - ].toIList(), - ); - } + ), + ], + ), + actions: [ + IconButton( + onPressed: null, + icon: Icon(Icons.push_pin), + tooltip: "Open pinned messages", + ), + IconButton( + onPressed: () => onOpenMemberList(context), + tooltip: "Open member list", + icon: Icon(Icons.people), + ), + RoomMenu(room), + ].toIList(), + ); } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 7fb3f8f..839109f 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -1,34 +1,28 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:flutter/services.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flyer_chat_file_message/flyer_chat_file_message.dart"; import "package:flyer_chat_system_message/flyer_chat_system_message.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/power_level_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart"; -import "package:nexus/controllers/via_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; -import "package:nexus/models/configs/power_level_config.dart"; import "package:nexus/models/relation_type.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/emoji_picker_button.dart"; -import "package:nexus/widgets/chat_page/expandable_image_message.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/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/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/form_text_input.dart"; -import "package:nexus/main.dart"; +import "package:nexus/widgets/loading.dart"; +// import "package:dynamic_polls/dynamic_polls.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; @@ -42,138 +36,46 @@ class RoomChat extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final client = ref.watch(ClientController.provider.notifier); - final relatedMessage = useState(null); + final replyToMessage = useState(null); final memberListOpened = useState(showMembersByDefault); final relationType = useState(RelationType.reply); + final room = ref.watch(SelectedRoomController.provider); final userId = ref.watch(ClientStateController.provider)?.userId; - final roomId = ref.watch( - SelectedRoomController.provider.select((value) => value?.metadata?.id), - ); final theme = Theme.of(context); final danger = theme.colorScheme.error; - if (roomId == null || userId == null) { - return Scaffold( - appBar: RoomAppbar( - isDesktop: isDesktop, - onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), - onOpenMemberList: null, - ), - body: Center( - child: Text( - "Nothing to see here...", - style: theme.textTheme.headlineMedium, - ), + if (room == null || userId == null || room.metadata?.id == null) { + return Center( + child: Text( + "Nothing to see here...", + style: theme.textTheme.headlineMedium, ), ); } - final controllerProvider = RoomChatController.provider(roomId); + final controllerProvider = RoomChatController.provider(room.metadata!.id); final notifier = ref.watch(controllerProvider.notifier); - final composerNode = useFocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - relatedMessage.value = null; - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - ); - List getMessageOptions(Message message) { final isSentByMe = message.authorId == userId; return [ - if (ref.watch( - PowerLevelController.provider( - PowerLevelConfig(eventType: "m.reaction"), - ), - )) - PopupMenuItem( - child: Row( - children: [ - ...{ - ...ref.watch( - AccountDataController.provider.select( - (value) => IList( - value["m.recent_emoji"]?.content["recent_emoji"] ?? - [], - ).map((entry) => entry["emoji"]), - ), - ), - "👍", - "🤣", - "😭", - "🤔", - } - .toIList() - .sublist(0, 4) - .map( - (emoji) => IconButton( - onPressed: () async { - Navigator.of(context).pop(); - await notifier - .sendReaction(emoji, message) - .onError(showError); - }, - icon: Text(emoji), - ), - ), - EmojiPickerButton( - context: context, - onPressed: Navigator.of(context).pop, - onSelection: (emoji) => - notifier.sendReaction(emoji, message).onError(showError), - ), - ], - ), - ), - if (ref.watch( - PowerLevelController.provider( - PowerLevelConfig(eventType: "m.room.message"), - ), - )) - PopupMenuItem( - onTap: () { - relatedMessage.value = message; - relationType.value = RelationType.reply; - composerNode.requestFocus(); - }, - child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), - ), + PopupMenuItem( + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.reply; + }, + child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), + ), if (message is TextMessage && isSentByMe) PopupMenuItem( onTap: () { - relatedMessage.value = message; + replyToMessage.value = message; relationType.value = RelationType.edit; - composerNode.requestFocus(); }, child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")), ), - PopupMenuItem( - onTap: () async { - final room = ref.watch(SelectedRoomController.provider); - if (room == null) return; - - final vias = ref.watch(ViaController.provider(room)); - - await Clipboard.setData( - ClipboardData( - text: - "matrix:roomid/${room.metadata?.id.substring(1)}/e/${message.id}$vias)", - ), - ); - }, - child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), - ), - if (ref.watch( - PowerLevelController.provider( - PowerLevelConfig(eventType: "m.room.redaction"), - ), - )) + if (isSentByMe) // TODO: Or if user has permission to redact others' messages PopupMenuItem( onTap: () => showDialog( context: context, @@ -205,13 +107,11 @@ class RoomChat extends HookConsumerWidget { ), TextButton( onPressed: () async { + notifier.deleteMessage( + message, + reason: deleteReasonController.text, + ); Navigator.of(context).pop(); - await notifier - .deleteMessage( - message, - reason: deleteReasonController.text, - ) - .onError(showError); }, child: Text("Delete"), ), @@ -220,10 +120,7 @@ class RoomChat extends HookConsumerWidget { }, ), ), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Delete", style: TextStyle(color: danger)), - ), + child: ListTile(leading: Icon(Icons.delete), title: Text("Delete")), ), PopupMenuItem( onTap: () => showDialog( @@ -257,9 +154,10 @@ class RoomChat extends HookConsumerWidget { ), TextButton( onPressed: () { + if (room.metadata == null) return; client.reportEvent( ReportRequest( - roomId: roomId, + roomId: room.metadata!.id, eventId: message.id, reason: reasonController.text.isEmpty ? null @@ -292,6 +190,7 @@ class RoomChat extends HookConsumerWidget { return Scaffold( appBar: RoomAppbar( + room, isDesktop: isDesktop, onOpenDrawer: (_) => Scaffold.of(context).openDrawer(), onOpenMemberList: (thisContext) { @@ -334,47 +233,23 @@ class RoomChat extends HookConsumerWidget { children: getMessageOptions(message), ), builders: Builders( - loadMoreBuilder: (_) => SizedBox.shrink(), + loadMoreBuilder: (_) => Loading(), chatAnimatedListBuilder: (_, itemBuilder) => ChatAnimatedList( itemBuilder: itemBuilder, - onEndReached: - ref.watch( - SelectedRoomController.provider.select( - (room) => room?.hasMore == true, - ), - ) + onEndReached: room.hasMore ? notifier.loadOlder : null, - onStartReached: () async { - final room = ref.watch( - SelectedRoomController.provider, - ); - return room == null - ? null - : await client.markRead(room); - }, + onStartReached: () => client.markRead(room), bottomPadding: 72, ), composerBuilder: (_) => ChatBox( - node: composerNode, - onSend: - ( - text, { - required shouldMention, - required tags, - }) => notifier.send( - text, - tags: tags, - relationType: relationType.value, - shouldMention: shouldMention, - relation: relatedMessage.value, - ), relationType: relationType.value, - relatedMessage: relatedMessage.value, - onDismiss: () => relatedMessage.value = null, + relatedMessage: replyToMessage.value, + onDismiss: () => replyToMessage.value = null, + room: room, ), textMessageBuilder: @@ -385,6 +260,7 @@ class RoomChat extends HookConsumerWidget { required bool isSentByMe, MessageGroupStatus? groupStatus, }) => TextMessageWrapper( + room: room, message, content: message.text, groupStatus: groupStatus, @@ -402,6 +278,7 @@ class RoomChat extends HookConsumerWidget { MessageGroupStatus? groupStatus, }) => TextMessageWrapper( message, + room: room, content: message.text, groupStatus: groupStatus, onTapReply: notifier.scrollToMessage, @@ -433,6 +310,7 @@ class RoomChat extends HookConsumerWidget { ), child: FlyerChatFileMessage( topWidget: ReplyWidget( + room: room, message, onTapReply: notifier.scrollToMessage, groupStatus: groupStatus, @@ -470,7 +348,7 @@ class RoomChat extends HookConsumerWidget { ), ), ), - resolveUser: (_) async => null, + resolveUser: notifier.resolveUser, chatController: controller, ), ), @@ -480,11 +358,11 @@ class RoomChat extends HookConsumerWidget { ), if (memberListOpened.value == true && showMembersByDefault) - MemberList(), + MemberList(room), ], ), - endDrawer: showMembersByDefault ? null : MemberList(), + endDrawer: showMembersByDefault ? null : MemberList(room), ); } } diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart index 4405707..2687bc8 100644 --- a/lib/widgets/chat_page/room_menu.dart +++ b/lib/widgets/chat_page/room_menu.dart @@ -1,9 +1,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; -import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/via_controller.dart"; import "package:nexus/models/room.dart"; class RoomMenu extends ConsumerWidget { @@ -18,6 +16,13 @@ class RoomMenu extends ConsumerWidget { return PopupMenuButton( itemBuilder: (_) => [ + // PopupMenuItem( + // onTap: () async { + // final link = await room.matrixToInviteLink(); + // await Clipboard.setData(ClipboardData(text: link.toString())); + // }, + // child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), + // ), PopupMenuItem( onTap: () async { await client.markRead(room); @@ -28,18 +33,6 @@ class RoomMenu extends ConsumerWidget { title: Text("Mark as Read"), ), ), - PopupMenuItem( - onTap: () async { - final vias = ref.watch(ViaController.provider(room)); - - await Clipboard.setData( - ClipboardData( - text: "matrix:roomid/${room.metadata?.id.substring(1)}$vias)", - ), - ); - }, - child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), - ), PopupMenuItem( onTap: () => showDialog( context: context, diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index f79c38f..4642a58 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -1,15 +1,18 @@ 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/controllers/key_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; +import "package:nexus/helpers/extensions/join_room_with_snackbars.dart"; +import "package:nexus/pages/settings_page.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/chat_page/join_dialog.dart"; import "package:nexus/widgets/chat_page/room_menu.dart"; +import "package:nexus/widgets/form_text_input.dart"; class Sidebar extends HookConsumerWidget { - final bool isDesktop; - const Sidebar({required this.isDesktop, super.key}); + const Sidebar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -88,7 +91,53 @@ class Sidebar extends HookConsumerWidget { PopupMenuItem( onTap: () => showDialog( context: context, - builder: (_) => JoinDialog(ref), + builder: (alertContext) => HookBuilder( + builder: (_) { + final roomAlias = useTextEditingController(); + return AlertDialog( + title: Text("Join a Room"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter the room alias, ID, or a Matrix.to link.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: roomAlias, + title: "#room:server", + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(alertContext).pop(); + + final client = ref.watch( + ClientController.provider.notifier, + ); + if (context.mounted) { + client.joinRoomWithSnackBars( + context, + roomAlias.text, + ref, + ); + } + }, + child: Text("Join"), + ), + ], + ); + }, + ), ), child: ListTile( title: Text("Join an existing room (or space)"), @@ -96,7 +145,7 @@ class Sidebar extends HookConsumerWidget { ), ), PopupMenuItem( - onTap: null, + onTap: () {}, child: ListTile( title: Text("Create a new room"), leading: Icon(Icons.add), @@ -107,15 +156,17 @@ class Sidebar extends HookConsumerWidget { ), IconButton( tooltip: "Explore other rooms", - onPressed: null, + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog(title: Text("To-do")), + ), icon: Icon(Icons.explore), ), IconButton( tooltip: "Open settings", - onPressed: null, - // () => Navigator.of( - // context, - // ).push(MaterialPageRoute(builder: (_) => SettingsPage())), + onPressed: () => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => SettingsPage())), icon: Icon(Icons.settings), ), ], @@ -169,12 +220,9 @@ class Sidebar extends HookConsumerWidget { ), ) .toList(), - onDestinationSelected: (value) { - selectedRoomIdNotifier.set( - selectedSpace.children[value].metadata?.id, - ); - if (!isDesktop) Navigator.of(context).pop(); - }, + onDestinationSelected: (value) => selectedRoomIdNotifier.set( + selectedSpace.children[value].metadata?.id, + ), ), ), ), diff --git a/lib/widgets/chat_page/text_message_wrapper.dart b/lib/widgets/chat_page/text_message_wrapper.dart new file mode 100644 index 0000000..9734a34 --- /dev/null +++ b/lib/widgets/chat_page/text_message_wrapper.dart @@ -0,0 +1,114 @@ +import "package:flutter/material.dart"; +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/reply_widget.dart"; + +class TextMessageWrapper extends StatelessWidget { + final Message message; + final String? content; + final Room room; + final MessageGroupStatus? groupStatus; + final Future Function(Message oldMessage, Message newMessage) + updateMessage; + final bool isSentByMe; + final Widget? extra; + final OnTapReply onTapReply; + + const TextMessageWrapper( + this.message, { + this.content, + this.onTapReply, + required this.room, + required this.updateMessage, + required this.groupStatus, + required this.isSentByMe, + this.extra, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textMessage = message is TextMessage ? message as TextMessage : null; + + return MessageWrapper( + message, + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: isSentByMe + ? colorScheme.primaryContainer + : colorScheme.surfaceContainer, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReplyWidget( + message, + room: room, + groupStatus: groupStatus, + onTapReply: onTapReply, + ), + if (content != null) + Html( + textStyle: message.metadata?["big"] == true + ? TextStyle(fontSize: 32) + : null, + content! + .replaceAllMapped( + RegExp( + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: false, + ), + (m) { + // If it's already an tag, leave it unchanged + if (m.group(1) != null) { + return m.group(1)!; + } + + // Otherwise, wrap the bare URL + final url = m.group(2)!; + return "$url"; + }, + ) + .replaceAll("\n", "
"), + ), + if (textMessage?.editedAt != null) + Text("(edited)", style: theme.textTheme.labelSmall), + if (textMessage != null) + LinkPreview( + text: textMessage.text, + backgroundColor: isSentByMe + ? colorScheme.inversePrimary + : colorScheme.surfaceContainerLow, + outsidePadding: EdgeInsets.only(top: 4), + insidePadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + linkPreviewData: message.metadata?["linkPreviewData"], + onLinkPreviewDataFetched: (linkPreviewData) => updateMessage( + message, + message.copyWith( + metadata: { + ...(message.metadata ?? {}), + "linkPreviewData": linkPreviewData, + }, + ), + ), + ), + if (extra != null) extra!, + ], + ), + ), + ), + groupStatus, + ); + } +} diff --git a/lib/widgets/chat_page/user_popover.dart b/lib/widgets/chat_page/user_popover.dart deleted file mode 100644 index a9a4799..0000000 --- a/lib/widgets/chat_page/user_popover.dart +++ /dev/null @@ -1,214 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:intl/intl.dart"; -import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/power_level_controller.dart"; -import "package:nexus/controllers/profile_controller.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/models/configs/power_level_config.dart"; -import "package:nexus/models/membership.dart"; -import "package:nexus/models/membership_status.dart"; -import "package:nexus/models/requests/membership_action.dart"; -import "package:nexus/models/requests/set_membership_request.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/chat_page/expandable_image.dart"; -import "package:nexus/widgets/form_text_input.dart"; - -class UserPopover extends ConsumerWidget { - final Membership member; - const UserPopover(this.member, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final client = ref.watch(ClientController.provider.notifier); - final roomId = ref.watch( - SelectedRoomController.provider.select((room) => room?.metadata?.id), - ); - - void showMembershipDialog(MembershipAction action) => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final actionReasonController = useTextEditingController(); - return AlertDialog( - title: Text( - "${toBeginningOfSentenceCase(action.name)} ${member.userId}", - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Are you sure you want to ${action.name} ${member.userId}?", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: actionReasonController, - title: "Reason for ${action.name} (optional)", - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - client - .setMembership( - SetMembershipRequest( - userId: member.userId, - roomId: roomId!, - action: action, - reason: actionReasonController.text, - ), - ) - .onError(showError); - }, - child: Text(toBeginningOfSentenceCase(action.name)), - ), - ], - ); - }, - ), - ); - - return Column( - spacing: 16, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Wrap( - alignment: WrapAlignment.center, - spacing: 16, - runSpacing: 8, - children: [ - ExpandableImage( - member.avatarUrl?.toString(), - child: AvatarOrHash( - member.avatarUrl, - member.displayName, - height: 80, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - member.displayName, - style: textTheme.headlineSmall, - ), - SelectableText(member.userId, style: textTheme.titleSmall), - SizedBox(height: 4), - ref - .watch(ProfileController.provider(member.userId)) - .betterWhen( - loading: SizedBox.shrink, - data: (profile) => Wrap( - spacing: 4, - children: [ - for (final pronoun in profile.pronouns.where( - (pronoun) => pronoun.language == "en", - )) - Chip( - label: Text(pronoun.summary), - labelStyle: TextStyle( - color: theme.colorScheme.onPrimary, - ), - color: WidgetStatePropertyAll( - theme.colorScheme.primary, - ), - ), - if (profile.timezone != null) - Chip( - label: Text(profile.timezone!), - labelStyle: TextStyle( - color: theme.colorScheme.onPrimary, - ), - color: WidgetStatePropertyAll( - theme.colorScheme.primary, - ), - ), - ], - ), - ), - ], - ), - ], - ), - if (member.userId != - ref.watch(ClientStateController.provider)?.userId && - roomId != null) - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FilledButton.icon(onPressed: null, label: Text("Message")), - - if (ref.watch( - PowerLevelController.provider( - PowerLevelConfig( - eventType: "m.room.member", - action: MembershipAction.kick, - isStateEvent: true, - targetUser: member.userId, - ), - ), - ) && - member.status == MembershipStatus.join || - member.status == MembershipStatus.invite) - FilledButton.icon( - onPressed: () => showMembershipDialog(MembershipAction.kick), - label: Text("Kick"), - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.error, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onError, - ), - ), - ), - if (ref.watch( - PowerLevelController.provider( - PowerLevelConfig( - eventType: "m.room.member", - action: MembershipAction.ban, - isStateEvent: true, - targetUser: member.userId, - ), - ), - )) - ElevatedButton.icon( - onPressed: () => showMembershipDialog( - member.status == MembershipStatus.ban - ? MembershipAction.unban - : MembershipAction.ban, - ), - label: Text( - member.status == MembershipStatus.ban ? "Unban" : "Ban", - ), - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.errorContainer, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onErrorContainer, - ), - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart deleted file mode 100644 index 9c70c27..0000000 --- a/lib/widgets/chat_page/wrappers/message_wrapper.dart +++ /dev/null @@ -1,83 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; -import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; -import "package:nexus/widgets/chat_page/wrappers/reaction_row.dart"; -import "package:timeago/timeago.dart"; - -class MessageWrapper extends StatelessWidget { - final Message message; - final Widget child; - final MessageGroupStatus? groupStatus; - const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final error = message.metadata?["error"]; - - return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: AnimatedContainer( - padding: message.metadata?["flashing"] == true - ? EdgeInsets.all(8) - : EdgeInsets.all(0), - color: message.metadata?["flashing"] == true - ? Theme.of(context).colorScheme.onSurface.withAlpha(50) - : Colors.transparent, - duration: Duration(milliseconds: 250), - child: Row( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - groupStatus?.isFirst != false - ? MessageAvatar(message, height: 40) - : SizedBox(width: 40), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - if (groupStatus?.isFirst != false) - Row( - spacing: 4, - children: [ - Flexible( - child: MessageDisplayname( - message, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - if (message.deliveredAt != null && - groupStatus?.isFirst != false) - Tooltip( - message: message.deliveredAt!.toString(), - child: Text( - format(message.deliveredAt!), - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.grey, - ), - ), - ), - ], - ), - child, - if (error != null && error != "not sent") - Text( - error, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - ReactionRow(message), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/chat_page/wrappers/reaction_row.dart b/lib/widgets/chat_page/wrappers/reaction_row.dart deleted file mode 100644 index 5e8fe86..0000000 --- a/lib/widgets/chat_page/wrappers/reaction_row.dart +++ /dev/null @@ -1,116 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/cross_cache_controller.dart"; -import "package:nexus/controllers/room_chat_controller.dart"; -import "package:nexus/controllers/selected_room_controller.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; -import "package:nexus/main.dart"; - -class ReactionRow extends ConsumerWidget { - final Message message; - const ReactionRow(this.message, {super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final clientState = ref.watch(ClientStateController.provider); - - return Wrap( - spacing: 4, - runSpacing: 4, - children: clientState?.homeserverUrl == null || message.reactions == null - ? [] - : message.reactions! - .mapTo( - (reaction, reactors) => HookBuilder( - builder: (context) { - final enabled = useState(true); - final selected = reactors.contains(clientState!.userId); - return Tooltip( - message: reactors.join(", "), - child: ChoiceChip( - showCheckmark: false, - selected: selected, - label: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - Flexible( - child: reaction.startsWith("mxc://") - ? Image( - height: 20, - image: CachedNetworkImage( - headers: ref.headers, - Uri.parse(reaction) - .mxcToHttps( - clientState.homeserverUrl!, - ) - .toString(), - ref.watch( - CrossCacheController.provider, - ), - ), - ) - : Text( - reaction, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - reactors.length.toString(), - overflow: TextOverflow.ellipsis, - ), - ], - ), - onSelected: enabled.value - ? (value) async { - enabled.value = false; - try { - final roomId = ref.watch( - SelectedRoomController.provider.select( - (value) => value?.metadata?.id, - ), - ); - if (roomId == null || - clientState.userId == null) { - return; - } - - final controller = ref.watch( - RoomChatController.provider( - roomId, - ).notifier, - ); - - if (selected) { - await controller - .removeReaction( - reaction, - message, - clientState.userId!, - ) - .onError(showError); - } else { - await controller - .sendReaction(reaction, message) - .onError(showError); - } - } finally { - enabled.value = true; - } - } - : null, - ), - ); - }, - ), - ) - .toList(), - ); - } -} diff --git a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart deleted file mode 100644 index 8d7a625..0000000 --- a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart +++ /dev/null @@ -1,147 +0,0 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:flutter/material.dart"; -import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_link_previewer/flutter_link_previewer.dart"; -import "package:flutter_linkify/flutter_linkify.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/cross_cache_controller.dart"; -import "package:nexus/controllers/url_preview_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/launch_helper.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/reply_widget.dart"; - -class TextMessageWrapper extends ConsumerWidget { - final Message message; - final String? content; - final MessageGroupStatus? groupStatus; - final Future Function(Message oldMessage, Message newMessage) - updateMessage; - final bool isSentByMe; - final Widget? extra; - final OnTapReply onTapReply; - - const TextMessageWrapper( - this.message, { - this.content, - this.onTapReply, - required this.updateMessage, - required this.groupStatus, - required this.isSentByMe, - this.extra, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final textMessage = message is TextMessage ? message as TextMessage : null; - - final link = textMessage == null - ? null - : RegExp( - r'''https?://[^\s"'<>]+''', - ).allMatches(textMessage.text).firstOrNull?.group(0); - - return MessageWrapper( - message, - ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(8)), - child: Container( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), - decoration: BoxDecoration( - color: isSentByMe - ? (message.id.startsWith("~") - ? colorScheme.onPrimary - : colorScheme.primaryContainer) - : colorScheme.surfaceContainer, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ReplyWidget( - message, - groupStatus: groupStatus, - onTapReply: onTapReply, - ), - if (content != null) - message.metadata?["format"] == "org.matrix.custom.html" - ? Html( - textStyle: message.metadata?["big"] == true - ? TextStyle(fontSize: 32) - : null, - content!.replaceAllMapped( - RegExp( - "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", - caseSensitive: false, - dotAll: true, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ), - ) - : Linkify( - text: content!, - options: LinkifyOptions(humanize: false), - onOpen: (link) => ref - .watch(LaunchHelper.provider) - .launchUrl(Uri.parse(link.url)), - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - if (textMessage?.editedAt != null) - Text("(edited)", style: theme.textTheme.labelSmall), - if (link != null) - ref - .watch(UrlPreviewController.provider(link)) - .betterWhen( - loading: SizedBox.shrink, - data: (preview) => preview == null - ? SizedBox.shrink() - : LinkPreview( - onTap: (url) => ref - .watch(LaunchHelper.provider) - .launchUrl(Uri.parse(url)), - imageBuilder: (url) => Image( - image: CachedNetworkImage( - url, - ref.watch(CrossCacheController.provider), - headers: ref.headers, - ), - fit: BoxFit.cover, - errorBuilder: (_, _, _) => SizedBox.shrink(), - ), - text: link, - backgroundColor: isSentByMe - ? colorScheme.inversePrimary - : colorScheme.surfaceContainerLow, - outsidePadding: EdgeInsets.only(top: 4), - insidePadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - linkPreviewData: preview, - onLinkPreviewDataFetched: (_) => null, - ), - ), - if (extra != null) extra!, - ], - ), - ), - ), - groupStatus, - ); - } -} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index fee47c5..2e0c766 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 5485b95..f70fb6e 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar = @@ -28,4 +29,7 @@ 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 13ef2de..78dcf40 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ 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 deleted file mode 100644 index d3fa575..0000000 --- a/linux/nexus.federated.Nexus.desktop +++ /dev/null @@ -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; \ No newline at end of file diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix deleted file mode 100644 index 91ba95a..0000000 --- a/linux/nix/devshell.nix +++ /dev/null @@ -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"; - }; -} diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix deleted file mode 100644 index adaeb15..0000000 --- a/linux/nix/pkg/default.nix +++ /dev/null @@ -1,45 +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="; - emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; - }; - - 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 deleted file mode 100644 index 1bc92bf..0000000 --- a/linux/nix/pkg/gomuks.nix +++ /dev/null @@ -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 - ''; -}) diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index abf5dc5..58cd859 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -43,7 +43,6 @@ static void my_application_activate(GApplication* application) { } } #endif - gtk_widget_set_size_request(GTK_WIDGET(window), 250, -1); if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..faf938b --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import dynamic_system_colors +import file_picker +import file_selector_macos +import path_provider_foundation +import screen_retriever_macos +import shared_preferences_foundation +import url_launcher_macos +import window_manager +import window_size + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) + WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..0bc2ae2 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,72 @@ +PODS: + - dynamic_system_colors (0.0.2): + - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - screen_retriever_macos (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.5.0): + - FlutterMacOS + - window_size (0.0.2): + - FlutterMacOS + +DEPENDENCIES: + - dynamic_system_colors (from `Flutter/ephemeral/.symlinks/plugins/dynamic_system_colors/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) + +EXTERNAL SOURCES: + dynamic_system_colors: + :path: Flutter/ephemeral/.symlinks/plugins/dynamic_system_colors/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + window_size: + :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos + +SPEC CHECKSUMS: + dynamic_system_colors: 9481d54d1e04fb1917c1b0c40c74cc92351960ce + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + window_manager: b729e31d38fb04905235df9ea896128991cad99e + window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b57e7d2 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 5A553D24F3E8B1047C8C23C4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 60A1D24B0215DBE0E54A5A38 /* Pods_RunnerTests.framework */; }; + D2AEE4BE48ADE8D8F6E19971 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C51BFFA46A953A0BFF99675 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* nexus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = nexus.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4C51BFFA46A953A0BFF99675 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5131A033CA79F625AC209D23 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 60A1D24B0215DBE0E54A5A38 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6FA1A73337D513DE7F34546E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 80AEE9B45607797016E3ABA8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 9044CACCBFBE4B0D8BAA81A1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 923B07782679F767C1BC6A1B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + E9A585ECBC36621C75768A9C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5A553D24F3E8B1047C8C23C4 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2AEE4BE48ADE8D8F6E19971 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 4CA469C933619B2BCC919549 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* nexus.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 4CA469C933619B2BCC919549 /* Pods */ = { + isa = PBXGroup; + children = ( + 80AEE9B45607797016E3ABA8 /* Pods-Runner.debug.xcconfig */, + 5131A033CA79F625AC209D23 /* Pods-Runner.release.xcconfig */, + E9A585ECBC36621C75768A9C /* Pods-Runner.profile.xcconfig */, + 9044CACCBFBE4B0D8BAA81A1 /* Pods-RunnerTests.debug.xcconfig */, + 923B07782679F767C1BC6A1B /* Pods-RunnerTests.release.xcconfig */, + 6FA1A73337D513DE7F34546E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4C51BFFA46A953A0BFF99675 /* Pods_Runner.framework */, + 60A1D24B0215DBE0E54A5A38 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + FFFC45027507AEBCC5945DAB /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + CE90484977A49E445FE81AD4 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 4297205FC5E56A01D5CDD2FD /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* nexus.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4297205FC5E56A01D5CDD2FD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + CE90484977A49E445FE81AD4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FFFC45027507AEBCC5945DAB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9044CACCBFBE4B0D8BAA81A1 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 923B07782679F767C1BC6A1B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6FA1A73337D513DE7F34546E /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nexus.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nexus"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ff47eeb --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..b4c5133 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = nexus + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 nexus.federated. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/nix/android.nix b/nix/android.nix new file mode 100644 index 0000000..f373968 --- /dev/null +++ b/nix/android.nix @@ -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" ]; + +} diff --git a/pubspec.lock b/pubspec.lock index 984341b..da5de89 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer_buffer - sha256: "5fcd06b0715ebeee99f03e3f437b3412249969d8d12b191ea8a1d76e42a4e4a1" + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.1.11" analyzer_plugin: dependency: transitive description: @@ -348,21 +348,11 @@ packages: dynamic_system_colors: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "3b61760d5e0ac1229eefde5b61247947eede4110" - url: "https://github.com/hasali19/flutter_dynamic_system_colors" - source: git + name: dynamic_system_colors + sha256: "43794e658fa88cbdec9f397dd1afd2eb69b6c9717e99b93b16ba37c3aa3b3a8c" + url: "https://pub.dev" + source: hosted version: "1.8.0" - emoji_text_field: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997" - url: "https://github.com/Henry-Hiles/emoji_text_field" - source: git - version: "1.0.0" encrypt: dependency: transitive description: @@ -475,10 +465,11 @@ packages: flutter_chat_ui: dependency: "direct main" description: - name: flutter_chat_ui - sha256: cfbaac38f429beb33d9cc1ca920ae7ccbadbed282c99335d590d61306d3a3d0f - url: "https://pub.dev" - source: hosted + path: "packages/flutter_chat_ui" + ref: HEAD + resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627" + url: "https://github.com/Henry-Hiles/flutter_chat_ui" + source: git version: "2.11.1" flutter_hooks: dependency: "direct main" @@ -499,19 +490,12 @@ packages: flutter_link_previewer: dependency: "direct main" description: - name: flutter_link_previewer - sha256: "346f345064e65bc8bf739bccf19d6d6ca50f8183ffc52e452afa58c06ee2cbf7" - url: "https://pub.dev" - source: hosted + path: "packages/flutter_link_previewer" + ref: HEAD + resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627" + url: "https://github.com/Henry-Hiles/flutter_chat_ui" + source: git version: "4.2.0" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_lints: dependency: "direct dev" description: @@ -537,10 +521,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.1.0" flutter_svg: dependency: "direct main" description: @@ -659,10 +643,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "08527ec06aaef75e4b78694e045ef0cd8346594eaf9cc18b0f866398b07b93b1" + sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.1.0" html: dependency: transitive description: @@ -672,7 +656,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -839,14 +823,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" lints: dependency: transitive description: @@ -1075,26 +1051,26 @@ packages: dependency: transitive description: name: riverpod - sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.9" + version: "1.0.0-dev.8" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "64e8debf5b719a37d48b9785dd595d34133fdcd84b8fd07157a621c54ab2156f" + sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.0" rxdart: dependency: transitive description: @@ -1380,14 +1356,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" - timeago: - dependency: "direct main" - description: - name: timeago - sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e - url: "https://pub.dev" - source: hosted - version: "3.7.1" typed_data: dependency: transitive description: @@ -1580,6 +1548,15 @@ 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 dbed5c5..3c0198d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: nexus description: "Yet another Matrix client" -version: 0.1.0 +version: 1.0.0 publish_to: none flutter: @@ -21,8 +21,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_riverpod: ^3.3.1 - hooks_riverpod: ^3.3.1 + flutter_riverpod: ^3.0.3 + hooks_riverpod: ^3.0.3 intl: ^0.20.1 fast_immutable_collections: ^11.0.0 path_provider: ^2.1.3 @@ -31,17 +31,25 @@ dependencies: image_picker: ^1.1.2 file_picker: ^10.3.3 path: ^1.9.0 - dynamic_system_colors: - git: - url: https://github.com/hasali19/flutter_dynamic_system_colors + dynamic_system_colors: ^1.8.0 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 flyer_chat_file_message: ^2.3.1 - flutter_chat_ui: ^2.11.1 - flutter_link_previewer: ^4.2.0 + flutter_chat_ui: + git: + url: https://github.com/Henry-Hiles/flutter_chat_ui + path: packages/flutter_chat_ui + flutter_link_previewer: + git: + url: https://github.com/Henry-Hiles/flutter_chat_ui + path: packages/flutter_link_previewer color_hash: ^1.0.1 flutter_widget_from_html_core: ^0.17.0 flutter_svg: ^2.2.2 @@ -55,19 +63,13 @@ dependencies: hooks: ^1.0.0 code_assets: ^1.0.0 ffigen: ^20.1.1 - timeago: ^3.7.1 - http: ^1.6.0 - flutter_linkify: ^6.0.0 - emoji_text_field: - git: - url: https://github.com/Henry-Hiles/emoji_text_field dev_dependencies: build_runner: ^2.4.11 custom_lint: ^0.8.0 flutter_lints: ^6.0.0 freezed: ^3.2.3 - riverpod_lint: ^3.1.3 + riverpod_lint: ^3.0.3 flutter_launcher_icons: ^0.14.1 json_serializable: ^6.11.1 @@ -75,9 +77,8 @@ flutter_launcher_icons: ios: true android: true image_path: assets/icon.png - adaptive_icon_background: assets/background.png + adaptive_icon_background: "#000000" adaptive_icon_foreground: assets/foreground.png - adaptive_icon_monochrome: assets/monochrome.png remove_alpha_ios: true windows: generate: true \ No newline at end of file diff --git a/scripts/generate.dart b/scripts/generate.dart index 446a469..b240d98 100644 --- a/scripts/generate.dart +++ b/scripts/generate.dart @@ -3,7 +3,26 @@ import "package:ffigen/ffigen.dart"; import "package:path/path.dart"; void main(List 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..."); diff --git a/scripts/generate.sh b/scripts/generate.sh new file mode 100755 index 0000000..6076ab8 --- /dev/null +++ b/scripts/generate.sh @@ -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 \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bde1c28..55fb066 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( @@ -23,4 +24,6 @@ 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 7b6b425..9333a2f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ 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 3583d23..24405eb 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" diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index f8a91f7..e3c83c9 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ