diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b9b6207..dc1e9c7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,39 +1,39 @@ name: "Build APK" on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: jobs: - build-apk: - runs-on: ubuntu-latest + build-apk: + runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive + 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: 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: 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: Build app - run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --target-platform android-arm64 --dart-define=GIT_COMMIT=${{ github.sha }}" - 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 + - 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 index c255c8d..d280bc6 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -1,73 +1,37 @@ name: "Build Flatpaks" on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: + 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 }} + 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 + 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: 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: 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 - - publish: - needs: build-flatpak - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Install flatpak tooling - run: sudo apt-get update && sudo apt-get install -y flatpak ostree - - - name: Download bundles - uses: actions/download-artifact@v7 - with: - pattern: flatpak-* - path: bundles - - - name: Build OSTree repo - run: | - ostree init --repo=repo --mode=archive-z2 - for bundle in bundles/*/nexus.federated.nexus.flatpak; do - flatpak build-import-bundle repo "$bundle" - done - flatpak build-update-repo --prune repo - - - uses: actions/upload-pages-artifact@v4 - with: - path: repo - - - name: Deploy to Pages - id: deployment - uses: actions/deploy-pages@v4 + - 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/ios.yml b/.github/workflows/ios.yml deleted file mode 100644 index 28344ea..0000000 --- a/.github/workflows/ios.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "Build iOS App" - -on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: - -jobs: - build-app: - runs-on: macos-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.4 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod - - - name: Build app - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build - flutter build ios --no-codesign --dart-define=GIT_COMMIT=${{ github.sha }} - - # Unsigned IPA: SideStore/AltStore re-sign this on-device with the - # user's own Apple ID at install time, so no certificate is needed here. - - name: Package unsigned IPA - run: | - mkdir Payload - cp -r build/ios/iphoneos/Runner.app Payload/ - zip -r Nexus.ipa Payload - - - name: Upload IPA artifact - uses: actions/upload-artifact@v6 - with: - name: Nexus.ipa - path: Nexus.ipa diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index b56f16f..0000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Build MacOS App" - -on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: - -jobs: - build-app: - runs-on: macos-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.4 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod - - - name: Build App - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build - flutter build macos --dart-define=GIT_COMMIT=${{ github.sha }} - - - name: Create DMG - id: create-dmg - uses: L-Super/create-dmg-actions@28511e988b13ca34096d439159a66d468b8e724f - with: - dmg_name: nexus - src_dir: build/macos/Build/Products/Release/Nexus.app - - - name: Upload DMG - uses: actions/upload-artifact@v6 - with: - name: nexus.dmg - path: ${{ steps.create-dmg.outputs.dmg_path }} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index db339b9..d9c8dd5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,74 +1,65 @@ name: "Build EXE" on: - push: - branches: ["main"] - tags: ["*"] - workflow_dispatch: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: jobs: - build-exe: - runs-on: windows-latest + build-exe: + runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.47.4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.4 - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: gomuks/go.mod - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: CLANG64 - install: >- - mingw-w64-x86_64-gcc + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: >- + mingw-w64-x86_64-gcc - - name: Go build - run: | - cd gomuks/pkg/ffi - go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared + - name: Go build + run: | + cd gomuks/pkg/ffi + go build -tags goolm,sqlite_fts5 -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 build windows --dart-define=GIT_COMMIT=${{ github.sha }} + - name: Build with Flutter + run: | + flutter pub get + dart scripts/generate.dart + flutter pub run build_runner build + flutter build windows --release - - name: Copy MinGW runtime DLLs - shell: msys2 {0} - run: | - cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/ + - name: Copy MinGW runtime DLLs + shell: msys2 {0} + run: | + cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/ + cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/ + cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/ - - name: Copy MSVC runtime DLLs - shell: powershell - run: | - $release = "build/windows/x64/runner/Release" + - name: Install Inno Setup + run: choco install innosetup -y - Copy-Item C:\Windows\System32\MSVCP140.dll $release -Force - Copy-Item C:\Windows\System32\VCRUNTIME140.dll $release -Force - Copy-Item C:\Windows\System32\VCRUNTIME140_1.dll $release -Force + - name: Build Inno Setup installer + run: iscc windows/installer.iss - - name: Install Inno Setup - run: choco install innosetup -y - - - name: Build Inno Setup installer - run: iscc windows/installer.iss - - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: windows-installer - path: windows/dist/Nexus-Setup.exe + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: windows-installer + path: windows/dist/Nexus-Setup.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore index 912ced3..2bec583 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,4 @@ key.properties password.txt # Nix -/result -# Widget Preview related -.widget_preview/ +/result \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 06dd51a..dc5e02e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "gomuks"] path = gomuks - url = https://github.com/gomuks/gomuks + url = https://github.com/henry-hiles/gomuks + branch = main diff --git a/.metadata b/.metadata index f12453b..1be1841 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "6b182d2c7585eba26d4edce0f97630effd256c33" + revision: "nixpkgs000000000000000000000000000000000" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 - base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 - - platform: ios - create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 - base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + create_revision: nixpkgs000000000000000000000000000000000 + base_revision: nixpkgs000000000000000000000000000000000 + - platform: macos + create_revision: nixpkgs000000000000000000000000000000000 + base_revision: nixpkgs000000000000000000000000000000000 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index b83039c..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "configurations": [ - { - "name": "Flutter", - "type": "dart", - "request": "launch", - "program": "lib/main.dart" - }, - { - "name": "Flutter (Headless)", - "type": "dart", - "request": "launch", - "program": "lib/main.dart", - "env": { "FLUTTER_HEADLESS": "1" } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index a5cac8a..2ff533e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,10 +10,7 @@ "msgtype", "muks", "prefs", - "recents", "unban", - "unifiedpush", - "unredact", - "webpush" + "unredact" ] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d9c7c9d..fb69a56 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -21,7 +21,7 @@ See [Effective Dart: Style](https://dart.dev/effective-dart/style) for general r Controllers live in `lib/controllers/` and provide a source that exposes data and logic via Riverpod providers, allowing other parts of the code to watch state changes with ref.watch (`ref.watch(MyController.provider)`), access the current value with ref.read (`ref.read(MyController.provider)`), and run helper methods on those classes using the notifier: ```dart -ref.read(MyController.provider.notifier).helperMethod() +ref.watch(MyController.provider.notifier).helperMethod() ``` We use an object oriented style for controllers, where `provider` is a static member on the controller class. E.g. diff --git a/README.md b/README.md index da4a6cb..d191d6e 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,12 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. ## Progress -- [x] Platform Support +- [ ] Platform Support - [x] Linux - - [x] Notifications through UnifiedPush - [x] Windows - - [ ] Background notifications - [x] Android - - [x] Notifications through UnifiedPush - - [x] Notifications through FCM - - [x] MacOS - - [x] Unsigned .ipa - - [ ] App Store - - [ ] Background notifications - - [x] iOS - - [x] Unsigned .ipa - - [ ] App Store - - [ ] Background notifications + - [ ] MacOS ([#50](https://git.federated.nexus/Nexus/nexus/issues/50)) + - [ ] iOS - [ ] Web (may not be possible) - [x] Login (via OAuth) - [x] Rooms / Spaces @@ -60,10 +50,10 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Per message profiles - [x] Attachments - [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391) - - [x] Tags + - [x] Mentions - [x] Users - [x] Rooms - - [x] Emojis + - [ ] 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 @@ -73,7 +63,9 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] URL Previews - [x] Replies - [x] Viewing - - [x] Jump to original message + - [ ] Jump to original message + - [x] In loaded timeline + - [ ] Out of loaded timeline - [x] Edits - [x] Attachments - [x] Unencrypted @@ -86,10 +78,12 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Users - [x] Clickable - [x] Rooms - - [x] Clickable + - [ ] Clickable - [x] Matrix URIs - [x] Matrix.to links - [x] Events + - [ ] Render more nicely + - [ ] Clickable - [x] Custom emojis/stickers - [x] History loading - [x] Editing @@ -111,8 +105,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Member list - [x] Sort by power level - [ ] Colors based off of power level -- [x] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) -- [x] Notifications page +- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites - [x] Settings @@ -122,48 +115,10 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. If you want to try out Nexus, grab one of the following artifacts from CI: - [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip) -- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip) -- [Unsigned MacOS DMG](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/nexus.dmg.zip) - [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip) - -Or, use one of the below methods: - -### Flatpak - -Make sure Flatpak is set up on your system, then run: - -```bash -flatpak install --user https://nexus.federated.nexus/flatpak/nexus.flatpakref -``` - -This adds the Nexus remote and installs the app for your user only, so no root access is needed (x86_64 and aarch64 are supported). To install system-wide instead, use `--system` in place of `--user`. Updates arrive through `flatpak update` or your software center. - -### NixOS Module - -Add Nexus as a flake input, following `nixpkgs` for less fetches: - -```nix -nexus = { - url = "git+https://git.federated.nexus/Nexus/nexus"; - inputs.nixpkgs.follows = "nixpkgs"; -}; -``` - -Then import the module and enable it in your NixOS configuration: - -```nix -{ inputs, ... }: -{ - imports = [ inputs.nexus.nixosModules.default ]; - - programs.nexus = { - enable = true; - enableNotifications = true; # optional: push notifications via UnifiedPush - }; -} -``` - -This installs Nexus system-wide. With `enableNotifications`, it also installs KUnifiedPush and registers its systemd and D-Bus services so notifications work. The `package` option lets you override the package if you need to. +- 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) ## Build it yourself @@ -171,17 +126,16 @@ This installs Nexus system-wide. With `enableNotifications`, it also installs KU #### Linux -- On NixOS: Either use direnv and `direnv allow`, or `nix develop` -- On other distros: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues. +- 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. #### Windows You will need: - Flutter +- Android SDK + NDK - Git -- Libass -- MPV - Go - Visual Studio 2022 (Desktop development with C++) - [MSYS2/MinGW-w64 GCC](https://www.msys2.org/) (for CGO) @@ -232,12 +186,6 @@ dart scripts/generate.dart > export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" > ``` -Build webcrypto: - -```sh -flutter pub run webcrypto:setup -``` - Build generated files, and watch for new changes: ```sh @@ -254,7 +202,7 @@ Development instructions can be found in [DEVELOPMENT.md](./DEVELOPMENT.md). ## Community -Join the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. +Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client. # Credits diff --git a/altstore-source.json b/altstore-source.json deleted file mode 100644 index 4471fb6..0000000 --- a/altstore-source.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "Nexus", - "identifier": "nexus.federated.nexus.source", - "subtitle": "Nexus for iOS", - "description": "Nexus iOS builds for AltStore and SideStore.", - "iconURL": "https://git.federated.nexus/Nexus/nexus/raw/branch/main/assets/icon.png", - "website": "https://git.federated.nexus/Nexus/nexus", - "apps": [ - { - "name": "Nexus", - "bundleIdentifier": "nexus.federated.nexus", - "developerName": "Nexus", - "subtitle": "A Matrix client", - "localizedDescription": "Nexus for iOS. Unsigned builds, resigned at install by AltStore/SideStore.", - "iconURL": "https://git.federated.nexus/Nexus/nexus/raw/branch/main/assets/icon.png", - "category": "social", - "screenshots": [], - "versions": [] - } - ], - "news": [] -} diff --git a/analysis_options.yaml b/analysis_options.yaml index 00df0c4..a8b1078 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,18 +1,12 @@ analyzer: - errors: - invalid_annotation_target: ignore - avoid_print: ignore - exclude: - - "build/**" - - "**/*.g.dart" - - "**/*.freezed.dart" - - android/** - - ios/** - - web/** - - windows/** - - macos/** - - linux/** + errors: + invalid_annotation_target: ignore + avoid_print: ignore + exclude: + - "build/**" + - "**/*.g.dart" + - "**/*.freezed.dart" include: package:flutter_lints/flutter.yaml linter: - rules: - prefer_double_quotes: true + rules: + prefer_double_quotes: true \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..2e7fb67 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,77 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "nexus.federated.nexus" + ndkVersion = flutter.ndkVersion + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + defaultConfig { + applicationId = "nexus.federated.nexus" + minSdk = 29 + targetSdkVersion flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + } + + signingConfigs { + release { + keyAlias "key" + def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH") + storeFile storePath ? file(storePath) : null + keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") + storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD") + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + + debug { + applicationIdSuffix = ".debug" + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 9c0d96c..0000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,84 +0,0 @@ -import java.util.Properties - -plugins { - id("com.android.application") - id("dev.flutter.flutter-gradle-plugin") -} - -val keystoreProperties = Properties() -val keystorePropertiesFile = rootProject.file("key.properties") - -if (keystorePropertiesFile.exists()) { - keystorePropertiesFile.inputStream().use { - keystoreProperties.load(it) - } -} - -android { - namespace = "nexus.federated.nexus" - compileSdk = 36 - ndkVersion = flutter.ndkVersion - - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - defaultConfig { - applicationId = "nexus.federated.nexus" - - minSdk = 29 - targetSdk = flutter.targetSdkVersion - - versionCode = flutter.versionCode - versionName = flutter.versionName - - multiDexEnabled = true - } - - signingConfigs { - create("release") { - keyAlias = "key" - - val storePath = - keystoreProperties["path"]?.toString() - ?: System.getenv("KEYSTORE_PATH") - - storeFile = storePath?.let { file(it) } - - keyPassword = - keystoreProperties["password"]?.toString() - ?: System.getenv("KEYSTORE_PASSWORD") - - storePassword = - keystoreProperties["password"]?.toString() - ?: System.getenv("KEYSTORE_PASSWORD") - } - } - - buildTypes { - release { - signingConfig = signingConfigs.getByName("release") - } - - debug { - applicationIdSuffix = ".debug" - } - } -} - -dependencies { - implementation("org.unifiedpush.android:embedded-fcm-distributor:3.1.0") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") -} - -kotlin { - compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21 - } -} - -flutter { - source = "../.." -} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d4dbebe..a0a0bcc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,9 +6,7 @@ - + if (project.hasProperty("android")) { + android { + compileSdkVersion 36 + buildToolsVersion '33.0.1' + } + } + } + + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index 441eb71..0000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} - -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 5fadfe8..74b269f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..7cffcd0 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.13.0" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false +} + +include ":app" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index b59bc94..0000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "9.3.1" apply false - id("org.jetbrains.kotlin.android") version "2.4.10" apply false -} - -include(":app") \ No newline at end of file diff --git a/assets/fonts/AtkinsonHyperlegibleMono.ttf b/assets/fonts/AtkinsonHyperlegibleMono.ttf deleted file mode 100644 index ac9c7ee..0000000 Binary files a/assets/fonts/AtkinsonHyperlegibleMono.ttf and /dev/null differ diff --git a/assets/fonts/Inter.ttf b/assets/fonts/Inter.ttf deleted file mode 100644 index 4a1af5a..0000000 Binary files a/assets/fonts/Inter.ttf and /dev/null differ diff --git a/assets/fonts/NotoColorEmoji.ttf b/assets/fonts/NotoColorEmoji.ttf new file mode 100644 index 0000000..b652015 Binary files /dev/null and b/assets/fonts/NotoColorEmoji.ttf differ diff --git a/assets/fonts/Roboto.ttf b/assets/fonts/Roboto.ttf new file mode 100644 index 0000000..01656a3 Binary files /dev/null and b/assets/fonts/Roboto.ttf differ diff --git a/assets/fonts/Twemoji.ttf b/assets/fonts/Twemoji.ttf deleted file mode 100644 index e389432..0000000 Binary files a/assets/fonts/Twemoji.ttf and /dev/null differ diff --git a/assets/bundled/icon.svg b/assets/icon.svg similarity index 100% rename from assets/bundled/icon.svg rename to assets/icon.svg diff --git a/assets/twim/oauth.webp b/assets/twim/oauth.webp new file mode 100644 index 0000000..36fbedf Binary files /dev/null and b/assets/twim/oauth.webp differ diff --git a/assets/twim/settings.png b/assets/twim/settings.png new file mode 100644 index 0000000..3037be1 Binary files /dev/null and b/assets/twim/settings.png differ diff --git a/assets/twim/summary.png b/assets/twim/summary.png deleted file mode 100644 index 70d9a50..0000000 Binary files a/assets/twim/summary.png and /dev/null differ diff --git a/flake.lock b/flake.lock index a48d846..41cfb9d 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1788450739, - "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", "type": "github" }, "original": { @@ -42,16 +42,15 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1790870263, - "narHash": "sha256-ZBblCzxsUBBCZJRJR0knnaOt2+Xl5JGfI+YkGPf2SI4=", - "owner": "Henry-Hiles", + "lastModified": 1774860670, + "narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=", + "owner": "neobrain", "repo": "nix2flatpak", - "rev": "2ef0e3cde2bdce0870f79a7568a72c2681ad20b5", + "rev": "61d68e21e3fbc2d57590051f48736bea271f4aba", "type": "github" }, "original": { - "owner": "Henry-Hiles", - "ref": "quad/nexus", + "owner": "neobrain", "repo": "nix2flatpak", "type": "github" } @@ -74,11 +73,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1788057806, - "narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=", + "lastModified": 1782614948, + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", "type": "github" }, "original": { @@ -89,11 +88,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1790689126, - "narHash": "sha256-ilerN1WLSvF+HMjziC/Wv99J5y02maDH+6hZPwsORKg=", + "lastModified": 1784497964, + "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b4fd65b198c599cbe814fcb9f42d25d021595ec9", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 97d6068..2b13a08 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ self.submodules = true; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; - nix2flatpak.url = "github:Henry-Hiles/nix2flatpak/quad/nexus"; + nix2flatpak.url = "github:neobrain/nix2flatpak"; }; outputs = @@ -16,12 +16,62 @@ ... }@inputs: flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ ./linux/nix ]; systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; + + perSystem = + { + lib, + pkgs, + system, + ... + }: + + { + _module.args.pkgs = import nixpkgs { + inherit system; + config = { + android_sdk.accept_license = true; + allowUnfree = true; + }; + }; + + packages = + let + default = pkgs.callPackage ./linux/nix/pkg { + src = self; + }; + in + { + inherit default; + + flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak { + appName = "Nexus"; + developer = "QuadRadical"; + appId = "nexus.federated.nexus"; + package = default; + runtime = "org.gnome.Platform/49"; + permissions = { + share = [ "network" ]; + sockets = [ + "pulseaudio" + "fallback-x11" + "wayland" + ]; + devices = [ "dri" ]; + }; + }; + + gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix { + src = self; + }; + }; + + devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { }; + }; }; } diff --git a/gomuks b/gomuks index 38e5033..c06eb1c 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit 38e5033c3a91db0cbea6cbd9f0feef9ff6441038 +Subproject commit c06eb1cbda2d49ff199a351e2d55df6ff7ab602b diff --git a/hook/build.dart b/hook/build.dart index 9315e04..625201d 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -1,5 +1,4 @@ import "dart:io"; - import "package:collection/collection.dart"; import "package:hooks/hooks.dart"; import "package:code_assets/code_assets.dart"; @@ -14,46 +13,22 @@ Future main(List args) => build(args, (input, output) async { String libFileName; Map extraEnv = {}; - IOSSdk? iosSdk; - String? iosSdkPath; - String? iosTargetTriple; switch (targetOS) { case OS.linux: libFileName = "libgomuks.so"; break; - case OS.iOS: - // Go only supports buildmode=c-archive for ios, not c-shared, so the - // archive built below gets linked into a dylib via a separate clang - // invocation before being registered as a normal asset like macOS. - libFileName = "libgomuks.dylib"; - final iosConfig = codeConfig.iOS; - iosSdk = iosConfig.targetSdk; - final minVersion = iosConfig.targetVersion; - iosSdkPath = await getXCodeTool(sdkType: iosSdk.type); - - final archTriple = switch (targetArch) { - Architecture.arm64 => "arm64", - Architecture.x64 => "x86_64", - _ => throw UnsupportedError( - "Unsupported iOS architecture: $targetArch", - ), - }; - - iosTargetTriple = iosSdk == IOSSdk.iPhoneSimulator - ? "$archTriple-apple-ios$minVersion.0-simulator" - : "$archTriple-apple-ios$minVersion.0"; - extraEnv = { - "GOOS": "ios", - "CC": await getXCodeTool(sdkType: iosSdk.type, findTool: "clang"), - "CGO_CFLAGS": "-isysroot $iosSdkPath -target $iosTargetTriple", - "CGO_LDFLAGS": "-isysroot $iosSdkPath -target $iosTargetTriple", - }; - break; case OS.macOS: libFileName = "libgomuks.dylib"; extraEnv = { - "SDKROOT": await getXCodeTool(), - "MACOSX_DEPLOYMENT_TARGET": codeConfig.macOS.targetVersion.toString(), + "SDKROOT": await getXCodeSDK(), + "CGO_ENABLED": "1", + "GOARCH": switch (targetArch) { + Architecture.arm64 => "arm64", + Architecture.x64 => "amd64", + _ => throw UnsupportedError( + "Unsupported macOS architecture: $targetArch", + ), + }, }; break; case OS.windows: @@ -121,28 +96,15 @@ Future main(List args) => build(args, (input, output) async { final tags = [ "sqlite_fts5", "goolm", - // goheif/dav1d is not supported on Android or iOS, would need to be fixed upstream - if (targetOS == OS.android || targetOS == OS.iOS) "noheic", + // goheif/dav1d is not supported on Android, would need to be fixed upstream + if (targetOS == OS.android) "noheic", ].join(","); - - final archiveFile = targetOS == OS.iOS - ? buildDir.resolve("${targetArch.name}/libgomuks.a") - : libFile; print( - "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${archiveFile.path}...", + "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${libFile.path}...", ); final result = await Process.run( "go", - [ - "build", - "-trimpath", - "-ldflags=-s -w", - "-tags", - tags, - "-o", - archiveFile.path, - "-buildmode=${targetOS == OS.iOS ? "c-archive" : "c-shared"}", - ], + ["build", "-tags", tags, "-o", libFile.path, "-buildmode=c-shared"], workingDirectory: gomuksBuildDir.resolve("pkg/ffi/").toFilePath(), environment: { "CGO_ENABLED": "1", @@ -162,35 +124,6 @@ Future main(List args) => build(args, (input, output) async { "Failed to build Gomuks shared library\n${result.stderr}", ); } - - if (targetOS == OS.iOS) { - print("Linking $archiveFile into $libFile..."); - final linkResult = await Process.run("xcrun", [ - "--sdk", - iosSdk!.type, - "clang", - "-dynamiclib", - "-isysroot", - iosSdkPath!, - "-target", - iosTargetTriple!, - "-install_name", - "@rpath/libgomuks.dylib", - "-framework", - "Security", - "-framework", - "CoreFoundation", - "-framework", - "SystemConfiguration", - "-force_load", - archiveFile.path, - "-o", - libFile.path, - ]); - if (linkResult.exitCode != 0) { - throw Exception("Failed to link Gomuks dylib\n${linkResult.stderr}"); - } - } } final generatedFile = "src/third_party/gomuks.g.dart"; diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 391a902..0000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6..0000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bf..0000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index 620e46e..0000000 --- a/ios/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# 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', '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 Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_ios_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_ios_build_settings(target) - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index b29d5cb..0000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - Flutter (1.0.0) - - media_kit_libs_ios_video (1.0.4): - - Flutter - - media_kit_video (0.0.1): - - Flutter - -DEPENDENCIES: - - Flutter (from `Flutter`) - - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - media_kit_libs_ios_video: - :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" - media_kit_video: - :path: ".symlinks/plugins/media_kit_video/ios" - -SPEC CHECKSUMS: - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 - media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 - -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e - -COCOAPODS: 1.17.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 065d08b..0000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,759 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2ADFEE6C745FFB2100CC9AFD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 88D55A5F1C7625122EBEE29D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 22DE4310DAFBE211CDF700B1 /* 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 = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 363A89F6FA09BCCBDED440CE /* 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 = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 63516CF65A79A5DDDE64B0D1 /* 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 = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AEB96D8976BD46D6860C7041 /* 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 = ""; }; - B8B599FDB72E4713FB7B5A44 /* 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 = ""; }; - FE5E4162842555A83F8A7FD7 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 8C0A36760DA8B873D8F6DDCE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 2ADFEE6C745FFB2100CC9AFD /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 88D55A5F1C7625122EBEE29D /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1CFFAE23B502BCCEB2B191A3 /* Pods */ = { - isa = PBXGroup; - children = ( - AEB96D8976BD46D6860C7041 /* Pods-Runner.debug.xcconfig */, - 363A89F6FA09BCCBDED440CE /* Pods-Runner.release.xcconfig */, - 22DE4310DAFBE211CDF700B1 /* Pods-Runner.profile.xcconfig */, - B8B599FDB72E4713FB7B5A44 /* Pods-RunnerTests.debug.xcconfig */, - FE5E4162842555A83F8A7FD7 /* Pods-RunnerTests.release.xcconfig */, - 63516CF65A79A5DDDE64B0D1 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 594EB1F65336CF9C7958F6FA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */, - 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - 1CFFAE23B502BCCEB2B191A3 /* Pods */, - 594EB1F65336CF9C7958F6FA /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - EE82EC529EB6C0F1FA480659 /* [CP] Check Pods Manifest.lock */, - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - 8C0A36760DA8B873D8F6DDCE /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 0830C6758DA3ECA81FB12386 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 1D165FDFB9D0D939B041966D /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, - ); - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 0830C6758DA3ECA81FB12386 /* [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; - }; - 1D165FDFB9D0D939B041966D /* [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; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - EE82EC529EB6C0F1FA480659 /* [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 */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - 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_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PK66NJM372; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B8B599FDB72E4713FB7B5A44 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = FE5E4162842555A83F8A7FD7 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - 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)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 63516CF65A79A5DDDE64B0D1 /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - 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)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - 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_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - 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_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - 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_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PK66NJM372; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PK66NJM372; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index c3fedb2..0000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc1..0000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index c73d4ae..0000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d0d98aa..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1 +0,0 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file 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 deleted file mode 100644 index 1682af0..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null 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 deleted file mode 100644 index 51f4a9b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null 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 deleted file mode 100644 index 97b3b3f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null 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 deleted file mode 100644 index 17d971e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null 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 deleted file mode 100644 index ca74554..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null 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 deleted file mode 100644 index 6129702..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null 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 deleted file mode 100644 index 16c3d1e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null 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 deleted file mode 100644 index 97b3b3f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null 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 deleted file mode 100644 index f515ebd..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null 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 deleted file mode 100644 index 2e49a13..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null 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 deleted file mode 100644 index 66b9ef2..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and /dev/null 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 deleted file mode 100644 index 53af9ff..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and /dev/null 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 deleted file mode 100644 index 455803d..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and /dev/null 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 deleted file mode 100644 index 7a2fd0f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and /dev/null 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 deleted file mode 100644 index 2e49a13..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null 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 deleted file mode 100644 index c34ad9e..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null 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 deleted file mode 100644 index 5e21545..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and /dev/null 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 deleted file mode 100644 index 2140e17..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and /dev/null 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 deleted file mode 100644 index a27d854..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null 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 deleted file mode 100644 index 8b4eb75..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null 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 deleted file mode 100644 index 21c424b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index 7680b42..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,79 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nexus - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - nexus - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - nexus.federated.nexus - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift deleted file mode 100644 index b9ce8ea..0000000 --- a/ios/Runner/SceneDelegate.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Flutter -import UIKit - -class SceneDelegate: FlutterSceneDelegate { - -} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -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/lib/controllers/account_data.dart b/lib/controllers/account_data.dart index c595565..0926e08 100644 --- a/lib/controllers/account_data.dart +++ b/lib/controllers/account_data.dart @@ -1,22 +1,16 @@ -import "dart:convert"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/models/account_data.dart"; -class AccountDataController extends Notifier { +class AccountDataController extends Notifier> { @override - AccountData build() => .new(); + IMap build() => .new(); - void update(IMap> newAccountData) => - state = .fromJson({ - ...json.decode(json.encode(state.toJson())), - ...newAccountData - .map((key, value) => MapEntry(key, value["content"])) - .unlock, - }); + void update(IMap newData) => + state = .new({...state.unlock, ...newData.unlock}); - static final provider = NotifierProvider( - AccountDataController.new, - ); + static final provider = + NotifierProvider>( + AccountDataController.new, + ); } diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index d4a5e09..e33493e 100644 --- a/lib/controllers/attachment.dart +++ b/lib/controllers/attachment.dart @@ -6,8 +6,10 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/message.dart"; import "package:path/path.dart"; -class AttachmentController(final String roomId) - extends Notifier<(String, MessageContent?)?> { +class AttachmentController extends Notifier<(String, MessageContent?)?> { + final String roomId; + AttachmentController(this.roomId); + @override Null build() => null; @@ -15,16 +17,18 @@ class AttachmentController(final String roomId) final filename = basename(file.path); state = (filename, null); - final isEncrypted = ref.read( - RoomsController.provider.select( - (value) => - value[roomId]?.state[EventType.encryption.type]?.isNotEmpty == true, - ), + final room = ref.watch( + RoomsController.provider.select((value) => value[roomId]), ); final content = await ref .watch(ClientController.provider.notifier) - .uploadMedia(.new(path: file.path, encrypt: isEncrypted)); + .uploadMedia( + .new( + path: file.path, + encrypt: room?.state[EventType.encryption.type]?.isNotEmpty == true, + ), + ); state = (filename, content); } diff --git a/lib/controllers/auth_url.dart b/lib/controllers/auth_url.dart index 963df86..156eb81 100644 --- a/lib/controllers/auth_url.dart +++ b/lib/controllers/auth_url.dart @@ -4,8 +4,10 @@ import "package:nexus/controllers/client_id.dart"; import "package:nexus/models/oauth_auth_code_response.dart"; import "package:nexus/models/requests/oauth/get_auth_url.dart"; -class AuthUrlController(final Uri homeserver) - extends AsyncNotifier { +class AuthUrlController extends AsyncNotifier { + final Uri homeserver; + AuthUrlController(this.homeserver); + @override Future build() async => ref .watch(ClientController.provider.notifier) diff --git a/lib/controllers/author.dart b/lib/controllers/author.dart index f9c9167..bbbe068 100644 --- a/lib/controllers/author.dart +++ b/lib/controllers/author.dart @@ -1,17 +1,19 @@ import "dart:async"; - import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/user.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; -class AuthorController(final Event event) - extends AsyncNotifier { +class AuthorController extends AsyncNotifier { + final Event event; + AuthorController(this.event); + @override Future build() async { final member = await ref.watch( - UserController.provider(.new(roomId: event.roomId, userId: event.sender)) - .future, + UserController.provider( + .new(roomId: event.roomId, userId: event.sender), + ).future, ); return .new( diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index 7d9604b..2cb1d66 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -1,50 +1,42 @@ -import "dart:async"; import "dart:ffi"; import "dart:io"; import "dart:isolate"; import "dart:math"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:ffi/ffi.dart"; -import "package:flutter/cupertino.dart"; -import "package:intl/intl.dart"; +import "package:flutter/foundation.dart"; +import "package:nexus/controllers/account_data.dart"; +import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/init_complete.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/space_edges.dart"; +import "package:nexus/controllers/sync_status.dart"; +import "package:nexus/controllers/top_level_spaces.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/models/capabilities.dart"; +import "package:nexus/main.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/models/event_context.dart"; -import "package:nexus/models/gomuks_config.dart"; import "package:nexus/models/oauth_auth_code_response.dart"; -import "package:nexus/models/open_graph_data.dart"; import "package:nexus/models/paginate.dart"; -import "package:nexus/models/paginate_manual.dart"; -import "package:nexus/models/requests/deregister_pusher.dart"; import "package:nexus/models/requests/download_media.dart"; import "package:nexus/models/requests/get_event.dart"; -import "package:nexus/models/requests/get_event_context.dart"; -import "package:nexus/models/requests/get_mentions.dart"; import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/join_room.dart"; -import "package:nexus/models/profile_response.dart"; +import "package:nexus/models/profile.dart"; import "package:nexus/models/requests/oauth/exchange_token.dart"; import "package:nexus/models/requests/oauth/get_auth_url.dart"; import "package:nexus/models/requests/oauth/register_client.dart"; import "package:nexus/models/requests/paginate.dart"; -import "package:nexus/models/requests/paginate_manual.dart"; import "package:nexus/models/requests/redact_event.dart"; -import "package:nexus/models/requests/register_pusher.dart"; import "package:nexus/models/requests/report.dart"; import "package:nexus/models/requests/send_event.dart"; import "package:nexus/models/requests/send_message.dart"; -import "package:nexus/models/requests/set_account_data.dart"; import "package:nexus/models/requests/set_membership.dart"; import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/upload_media.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/models/room_metadata.dart"; -import "package:nexus/models/room_summary.dart"; -import "package:nexus/models/spec_versions_response.dart"; +import "package:nexus/models/sync_data.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:path_provider/path_provider.dart"; @@ -52,101 +44,134 @@ import "package:path_provider/path_provider.dart"; class ClientController extends AsyncNotifier { @override Future build() async { - debugPrint("Setting Gomuks env..."); - if (Platform.isAndroid || Platform.isIOS) { - final env = { - "GOMUKS_ROOT": (await getApplicationSupportDirectory()).path, - "GOMUKS_CACHE_HOME": (await getApplicationCacheDirectory()).path, - }; - for (final MapEntry(:key, :value) in env.entries) { - final keyPtr = key.toNativeUtf8().cast(); - final valuePtr = value.toNativeUtf8().cast(); - try { - GomuksSetEnv(keyPtr, valuePtr); - } finally { - calloc - ..free(keyPtr) - ..free(valuePtr); - } - } + final Pointer root; + if (Platform.isAndroid) { + final dir = await getApplicationSupportDirectory(); + root = "${dir.path}/gomuks".toNativeUtf8().cast(); + } else { + root = nullptr.cast(); } - debugPrint("Initializing Gomuks..."); - final handle = await Isolate.run(() { - final bufferPointer = GomuksConfig( - matrix: .new( - initialDeviceDisplayName: - "Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}", - ), - ).toJson().toGomuksBufferPtr(); + final handle = GomuksInit(root); - try { - return GomuksInit(bufferPointer.ref); - } finally { - calloc - ..free(bufferPointer.ref.base) - ..free(bufferPointer); - } - }); + final callable = + NativeCallable< + Void Function(Pointer, Int64, GomuksOwnedBuffer) + >.listener(( + Pointer command, + int requestId, + GomuksOwnedBuffer data, + ) { + try { + final muksEventType = command.cast().toDartString(); + debugPrint("Handling $muksEventType..."); + final decodedMuksEvent = data.toJson(); + + switch (muksEventType) { + case "client_state": + ref + .watch(ClientStateController.provider.notifier) + .set(.fromJson(decodedMuksEvent)); + break; + case "sync_status": + ref + .watch(SyncStatusController.provider.notifier) + .set(.fromJson(decodedMuksEvent)); + break; + case "init_complete": + ref.watch(InitCompleteController.provider.notifier).complete(); + break; + case "send_complete": + final event = Event.fromJson(decodedMuksEvent["event"]); + ref + .watch(RoomsController.provider.notifier) + .update( + .new({ + event.roomId: .new(events: .new({event.rowId: event})), + }), + .new(), + ); + + break; + case "sync_complete": + final syncData = SyncData.fromJson(decodedMuksEvent); + final roomProvider = RoomsController.provider; + final accountDataProvider = AccountDataController.provider; + + if (syncData.clearState) { + ref.invalidate(roomProvider); + ref.invalidate(accountDataProvider); + } + + ref + .watch(roomProvider.notifier) + .update(syncData.rooms, syncData.leftRooms); + ref + .watch(accountDataProvider.notifier) + .update(syncData.accountData); + + if (syncData.topLevelSpaces != null) { + ref + .watch(TopLevelSpacesController.provider.notifier) + .set(syncData.topLevelSpaces!); + } + + if (syncData.spaceEdges != null) { + ref + .watch(SpaceEdgesController.provider.notifier) + .set(syncData.spaceEdges!); + } + + // ref + // .watch(SyncStatusController.provider.notifier) + // .set(SyncStatus.fromJson(decodedMuksEvent)); + break; + default: + debugPrint("Unhandled event: $muksEventType"); + } + debugPrint("Finished handling $muksEventType..."); + } catch (error, stackTrace) { + if (kDebugMode) { + debugPrintStack(stackTrace: stackTrace, label: error.toString()); + rethrow; + } else { + showError(error, stackTrace); + } + } + }); ref.onDispose(() => GomuksDestroy(handle)); + ref.onDispose(callable.close); - return handle; - } + final errorCode = GomuksStart(handle, callable.nativeFunction); - Future callGomuksMethod( - Map data, - FutureOr Function(int handle, GomuksBorrowedBuffer data) - callback, - ) async { - final bufferPointer = data.toGomuksBufferPtr(); - - try { - final handle = await future; - - final response = await Isolate.run( - () => callback(handle, bufferPointer.ref), - ); - - final json = response.buf.toJsonAndFree(); - - if (response.command.cast().toDartString() == "error") { - throw json; - } - - return json; - } finally { - calloc - ..free(bufferPointer.ref.base) - ..free(bufferPointer); - } - } - - Future<(Event, RoomMetadata)?> handlePush(Map data) async { - final response = await callGomuksMethod( - data, - (handle, data) async => GomuksHandlePush(handle, data), - ); - - return response == null - ? null - : ( - Event.fromJson(response["event"]), - RoomMetadata.fromJson(response["room"]), - ); + if (errorCode == 0) return handle; + throw Exception("GomuksStart returned error code $errorCode"); } Future _sendCommand( String command, [ Map data = const {}, - ]) => callGomuksMethod(data, (handle, data) { - final commandPointer = command.toNativeUtf8().cast(); - try { - return GomuksSubmitCommand(handle, commandPointer, data); - } finally { - calloc.free(commandPointer); + ]) async { + final bufferPointer = data.toGomuksBufferPtr(); + final handle = await future; + final response = await Isolate.run( + () => GomuksSubmitCommand( + handle, + command.toNativeUtf8().cast(), + bufferPointer.ref, + ), + ); + + calloc.free(bufferPointer); + + final json = response.buf.toJson(); + if (response.command.cast().toDartString() == "error") { + throw json; } - }); + + return json; + } Future redactEvent(RedactEventRequest report) => _sendCommand("redact_event", report.toJson()); @@ -158,7 +183,7 @@ class ClientController extends AsyncNotifier { final json = request.toJson(); final content = request.content.toJson(); - return .fromJson( + return Event.fromJson( await _sendCommand("send_event", { ...json, "content": { @@ -185,17 +210,26 @@ class ClientController extends AsyncNotifier { } } - Future joinRoom(JoinRoomRequest request) async => - (await _sendCommand("join_room", request.toJson()))["room_id"]; + Future joinRoom(JoinRoomRequest request) async { + final response = await _sendCommand("join_room", request.toJson()); + return response["room_id"]; + } - Future getRoomSummary(JoinRoomRequest request) async => - .fromJson(await _sendCommand("get_room_summary", request.toJson())); + Future getAccessToken() async { + final response = await _sendCommand("get_account_info", {}); + return response?["access_token"]; + } Future leaveRoom(Room room) async { if (room.metadata == null) return; await _sendCommand("leave_room", {"room_id": room.metadata!.id}); } + // (await _sendCommand("get_event_context", { + // "room_id": request.roomId, + // "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc", + // })); + Future> getRoomState(GetRoomStateRequest request) async { Future getState(GetRoomStateRequest request) async => (await _sendCommand("get_room_state", request.toJson())) as List?; @@ -216,35 +250,17 @@ class ClientController extends AsyncNotifier { return .new(response?.map((event) => .fromJson(event))); } - Future> getMentions(GetMentionsRequest request) async => .new( - // TODO: Handle `related_events` - ((await _sendCommand("get_mentions", request.toJson()))["events"] as List) - .map((event) => .fromJson(event)), - ); - Future getEvent(GetEventRequest request) async { final json = await _sendCommand("get_event", request.toJson()); return json == null ? null : .fromJson(json); } - Future getUrlPreview(Uri url) async => - .fromJson(await _sendCommand("get_url_preview", {"url": url.toString()})); - Future paginate(PaginateRequest request) async => .fromJson(await _sendCommand("paginate", request.toJson())); - Future paginateManual(PaginateManualRequest request) async => - .fromJson(await _sendCommand("paginate_manual", request.toJson())); - - Future getEventContext(GetEventContextRequest request) async => - .fromJson(await _sendCommand("get_event_context", request.toJson())); - - Future getProfile(String userId) async { - try { - return .fromJson(await _sendCommand("get_profile", {"user_id": userId})); - } catch (_) { - return .new(profile: .new(id: userId)); - } + Future getProfile(String userId) async { + final json = await _sendCommand("get_profile", {"user_id": userId}); + return .fromJsonWithCatch({...json, "id": userId}); } Future reportEvent(ReportRequest request) => @@ -253,18 +269,6 @@ class ClientController extends AsyncNotifier { Future setMembership(SetMembershipRequest request) => _sendCommand("set_membership", request.toJson()); - Future setAccountData(SetAccountDataRequest request) => - _sendCommand("set_account_data", request.toJson()); - - Future registerPusher(RegisterPusherRequest request) => - _sendCommand("register_homeserver_push", request.toJson()); - - Future deregisterPusher(DeregisterPusherRequest request) => - _sendCommand("register_homeserver_push", { - ...request.toJson(), - "kind": null, - }); - Future uploadMedia(UploadMediaRequest request) async => .fromJson(await _sendCommand("upload_media", request.toJson())); @@ -274,11 +278,9 @@ class ClientController extends AsyncNotifier { Future logout() => _sendCommand("logout"); Future markRead(Room room) async { - if (room.timeline.isEmpty || room.metadata == null) return; final eventRowId = room.timeline[room.timeline.keys.reduce(max)]; final event = eventRowId == null ? null : room.events[eventRowId]; - - if (event == null) return; + if (event == null || room.metadata == null) return; await _sendCommand("mark_read", { "room_id": room.metadata!.id, @@ -301,16 +303,10 @@ class ClientController extends AsyncNotifier { Future exchangeToken(OAuthExchangeTokenRequest request) async => await _sendCommand("oauth_exchange_token", request.toJson()); - Future getSpecVersions() async => - .fromJson(await _sendCommand("get_versions")); - - Future getCapabilities() async => - .fromJson((await _sendCommand("get_capabilities"))["capabilities"]); - Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { - "user_id": "@fake-user:${homeserver.authority}", + "user_id": "@fake-user:${homeserver.host}", }); return Uri.parse(response["m.homeserver"]?["base_url"]); } catch (error) { diff --git a/lib/controllers/client_id.dart b/lib/controllers/client_id.dart index 7748f97..ac8f5cb 100644 --- a/lib/controllers/client_id.dart +++ b/lib/controllers/client_id.dart @@ -1,7 +1,10 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; -class ClientIdController(final Uri homeserver) extends AsyncNotifier { +class ClientIdController extends AsyncNotifier { + final Uri homeserver; + ClientIdController(this.homeserver); + @override Future build() => ref .watch(ClientController.provider.notifier) diff --git a/lib/controllers/emoji.dart b/lib/controllers/emoji.dart new file mode 100644 index 0000000..f1a5c79 --- /dev/null +++ b/lib/controllers/emoji.dart @@ -0,0 +1,121 @@ +import "dart:convert"; +import "package:collection/collection.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:http/http.dart"; +import "package:nexus/models/gemoji.dart"; + +class EmoteSection { + final String category; + final String label; + final IconData icon; + final List items; + final bool isRecent; + + const EmoteSection({ + required this.category, + required this.label, + required this.icon, + required this.items, + this.isRecent = false, + }); +} + +class EmoteItem { + final String id; + final String display; + final List keywords; + final int useCounter; + final Image? customImage; + + const EmoteItem({ + required this.id, + required this.display, + required this.keywords, + required this.useCounter, + this.customImage, + }); +} + +class EmojiController extends AsyncNotifier> { + @override + Future> build() async { + final response = await get( + .https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"), + ); + + if (response.statusCode != 200) { + throw Exception("Failed to load emoji data"); + } + + return compute(parseEmojiJson, response.body); + } + + static IList parseEmojiJson(String body) { + final data = json.decode(body) as List; + final allEntries = data + .cast>() + .map(Gemoji.fromJson) + .toIList(); + + final groupedByCategory = groupBy(allEntries, (g) => g.category); + + final result = groupedByCategory.entries.fold>( + .new(), + (resultList, entry) => resultList.add( + EmoteSection( + category: entry.key, + label: entry.key, + icon: getIconForCategory(entry.key), + items: entry.value + .map( + (gemoji) => EmoteItem( + id: gemoji.emoji, + display: gemoji.emoji, + keywords: [ + ...gemoji.tags, + ...gemoji.aliases, + ...gemoji.description.split(" "), + ], + useCounter: 0, + ), + ) + .toList(), + isRecent: false, + ), + ), + ); + + return result; + } + + static final provider = + AsyncNotifierProvider>( + EmojiController.new, + ); + + static IconData getIconForCategory(String category) { + switch (category.toLowerCase()) { + case "smileys & emotion": + return Icons.sentiment_very_satisfied; + case "people & body": + return Icons.face; + case "food & drink": + return Icons.restaurant; + case "travel & places": + return Icons.flag; + case "activities": + return Icons.sports_soccer; + case "objects": + return Icons.lightbulb; + case "symbols": + return Icons.circle; + case "flags": + return Icons.flag; + default: + return Icons.emoji_emotions_outlined; + } + } +} diff --git a/lib/controllers/event.dart b/lib/controllers/event.dart index 5c6513b..b7b417a 100644 --- a/lib/controllers/event.dart +++ b/lib/controllers/event.dart @@ -5,8 +5,10 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_event.dart"; -class EventController(final GetEventRequest request) - extends AsyncNotifier { +class EventController extends AsyncNotifier { + final GetEventRequest request; + EventController(this.request); + @override Future build() async { final room = ref.watch( @@ -16,28 +18,11 @@ class EventController(final GetEventRequest request) (event) => event.eventId == request.eventId, ); - if (event != null) return event; - - final fetchedEvent = await ref - .watch(ClientController.provider.notifier) - .getEvent(request) - .onError((_, _) => null); - - if (fetchedEvent != null) { - ref - .watch(RoomsController.provider.notifier) - .update( - .new({ - request.roomId: .new( - events: .new({fetchedEvent.rowId: fetchedEvent}), - ), - }), - ); - - return fetchedEvent; - } - - return null; + return event ?? + await ref + .watch(ClientController.provider.notifier) + .getEvent(request) + .onError((_, _) => null); } static final provider = AsyncNotifierProvider.family diff --git a/lib/controllers/gomuks_listener.dart b/lib/controllers/gomuks_listener.dart deleted file mode 100644 index 2a43676..0000000 --- a/lib/controllers/gomuks_listener.dart +++ /dev/null @@ -1,123 +0,0 @@ -import "dart:async"; -import "dart:ffi"; - -import "package:flutter/foundation.dart"; -import "package:nexus/controllers/account_data.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/init_complete.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/space_edges.dart"; -import "package:nexus/controllers/sync_status.dart"; -import "package:nexus/controllers/top_level_spaces.dart"; -import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/main.dart"; -import "package:ffi/ffi.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/sync_data.dart"; -import "package:nexus/src/third_party/gomuks.g.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class GomuksListenerController extends AsyncNotifier { - @override - Future build() async { - debugPrint("Starting gomuks..."); - final handle = await ref.watch(ClientController.provider.future); - - final callable = - NativeCallable< - Void Function(Pointer, Int64, GomuksOwnedBuffer) - >.listener(( - Pointer command, - int requestId, - GomuksOwnedBuffer data, - ) { - try { - final muksEventType = command.cast().toDartString(); - debugPrint("Handling $muksEventType..."); - final decodedMuksEvent = data.toJsonAndFree(); - - switch (muksEventType) { - case "client_state": - ref - .watch(ClientStateController.provider.notifier) - .set(.fromJson(decodedMuksEvent)); - break; - case "sync_status": - ref - .watch(SyncStatusController.provider.notifier) - .set(.fromJson(decodedMuksEvent)); - break; - case "init_complete": - ref.watch(InitCompleteController.provider.notifier).complete(); - break; - case "send_complete": - final event = Event.fromJson(decodedMuksEvent["event"]); - ref - .watch(RoomsController.provider.notifier) - .update( - .new({ - event.roomId: .new(events: .new({event.rowId: event})), - }), - ); - - break; - case "sync_complete": - final syncData = SyncData.fromJson(decodedMuksEvent); - final roomProvider = RoomsController.provider; - final accountDataProvider = AccountDataController.provider; - - if (syncData.clearState) { - ref.invalidate(roomProvider); - ref.invalidate(accountDataProvider); - } - - ref - .watch(roomProvider.notifier) - .update(syncData.rooms, syncData.leftRooms); - ref - .watch(accountDataProvider.notifier) - .update(syncData.accountData); - - if (syncData.topLevelSpaces != null) { - ref - .watch(TopLevelSpacesController.provider.notifier) - .set(syncData.topLevelSpaces!); - } - - if (syncData.spaceEdges != null) { - ref - .watch(SpaceEdgesController.provider.notifier) - .set(syncData.spaceEdges!); - } - - // ref - // .watch(SyncStatusController.provider.notifier) - // .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - default: - debugPrint("Unhandled event: $muksEventType"); - } - debugPrint("Finished handling $muksEventType..."); - } catch (error, stackTrace) { - if (kDebugMode) { - debugPrintStack(stackTrace: stackTrace, label: error.toString()); - rethrow; - } else { - showError(error, stackTrace); - } - } - }); - - ref.onDispose(callable.close); - - final errorCode = GomuksStart(handle, callable.nativeFunction); - if (errorCode != 0) { - throw Exception("GomuksStart returned error code $errorCode"); - } - } - - static final provider = AsyncNotifierProvider( - GomuksListenerController.new, - ); -} diff --git a/lib/controllers/header.dart b/lib/controllers/header.dart new file mode 100644 index 0000000..3e173ad --- /dev/null +++ b/lib/controllers/header.dart @@ -0,0 +1,20 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/client_state.dart"; + +class HeaderController extends AsyncNotifier> { + @override + Future> build() async { + if (ref.watch(ClientStateController.provider)?.isLoggedIn != true) { + return {}; + } + final client = ref.watch(ClientController.provider.notifier); + final accessToken = await client.getAccessToken(); + return {"authorization": "Bearer $accessToken"}; + } + + static final provider = + AsyncNotifierProvider>( + HeaderController.new, + ); +} diff --git a/lib/controllers/jump_request.dart b/lib/controllers/jump_request.dart deleted file mode 100644 index fc21ab2..0000000 --- a/lib/controllers/jump_request.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; - -typedef JumpRequest = ({String roomId, int rowId}); - -class JumpRequestController extends Notifier { - @override - JumpRequest? build() => null; - - void request(String roomId, int rowId) => - state = (roomId: roomId, rowId: rowId); - - void consume() => state = null; - - static final provider = NotifierProvider( - JumpRequestController.new, - ); -} diff --git a/lib/controllers/key.dart b/lib/controllers/key.dart index 77b4883..eff3ab2 100644 --- a/lib/controllers/key.dart +++ b/lib/controllers/key.dart @@ -1,28 +1,30 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/shared_prefs.dart"; -class KeyController(final String key) extends AsyncNotifier { +class KeyController extends Notifier { + final String key; + KeyController(this.key); + static const String spaceKey = "space"; static const String roomKey = "room"; - static const String pushKeyKey = "pushKey"; @override - Future build() => - ref.watch(SharedPrefsController.provider).getString(key); + String? build() => + ref.watch(SharedPrefsController.provider).requireValue.getString(key); Future set(String? value) async { - final prefs = ref.watch(SharedPrefsController.provider); - state = .data(value); + final prefs = ref.watch(SharedPrefsController.provider).requireValue; + state = value; if (value == null) { - await prefs.remove(key); + prefs.remove(key); } else { - await prefs.setString(key, value); + prefs.setString(key, value); } } static final provider = - AsyncNotifierProvider.family( + NotifierProvider.family( KeyController.new, ); } diff --git a/lib/controllers/member_list_opened.dart b/lib/controllers/member_list_opened.dart deleted file mode 100644 index f0295d8..0000000 --- a/lib/controllers/member_list_opened.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/shared_prefs.dart"; - -class MemberListOpenedController extends AsyncNotifier { - static const String key = "memberListOpened"; - - @override - Future build() async => - await ref.watch(SharedPrefsController.provider).getBool(key) ?? true; - - Future set(bool value) async { - state = .data(value); - await ref.watch(SharedPrefsController.provider).setBool(key, value); - } - - static final provider = - AsyncNotifierProvider( - MemberListOpenedController.new, - ); -} diff --git a/lib/controllers/members.dart b/lib/controllers/members.dart index d8172b1..8566b40 100644 --- a/lib/controllers/members.dart +++ b/lib/controllers/members.dart @@ -6,8 +6,10 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_room_state.dart"; -class MembersController(final String roomId) - extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { + final String roomId; + MembersController(this.roomId); + @override Future> build() async { final room = ref.watch( @@ -27,7 +29,7 @@ class MembersController(final String roomId) ), ); - ref + await ref .read(RoomsController.provider.notifier) .addState(roomId, fetchedState, isMembers: true); } diff --git a/lib/controllers/members_by_status.dart b/lib/controllers/members_by_status.dart index 82905aa..613f7b1 100644 --- a/lib/controllers/members_by_status.dart +++ b/lib/controllers/members_by_status.dart @@ -5,8 +5,10 @@ import "package:nexus/models/configs/members_by_status.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; -class MembersByStatusController(final MembersByStatusConfig config) - extends AsyncNotifier> { +class MembersByStatusController extends AsyncNotifier> { + final MembersByStatusConfig config; + MembersByStatusController(this.config); + @override Future> build() => ref.watch( MembersController.provider(config.roomId).selectAsync( diff --git a/lib/controllers/members_grouped.dart b/lib/controllers/members_grouped.dart index f903ec4..6f41e9d 100644 --- a/lib/controllers/members_grouped.dart +++ b/lib/controllers/members_grouped.dart @@ -8,8 +8,11 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/event.dart"; -class MembersGroupedController(final MembersByStatusConfig config) +class MembersGroupedController extends AsyncNotifier>>> { + final MembersByStatusConfig config; + MembersGroupedController(this.config); + @override Future>>> build() async { final room = ref.watch( diff --git a/lib/controllers/multi_provider.dart b/lib/controllers/multi_provider.dart index ab37d07..52dd8d9 100644 --- a/lib/controllers/multi_provider.dart +++ b/lib/controllers/multi_provider.dart @@ -1,15 +1,14 @@ import "dart:async"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -class MultiProviderController(final IList providers) - extends AsyncNotifier { +class MultiProviderController extends AsyncNotifier { + MultiProviderController(this.providers); + final IList providers; + @override - Future build() => .wait( - providers.map((provider) => ref.watch(provider.future)), - eagerError: true, - ); + Future build() => + .wait(providers.map((provider) => ref.watch(provider.future))); static final provider = AsyncNotifierProvider.family< diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart deleted file mode 100644 index fe631d4..0000000 --- a/lib/controllers/notification.dart +++ /dev/null @@ -1,173 +0,0 @@ -import "dart:async"; -import "dart:io"; - -import "package:flutter/services.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/portal.dart"; -import "package:nexus/main.dart"; -import "package:flutter_local_notifications/flutter_local_notifications.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/widgets/notifications_list.dart"; -import "package:xdg_desktop_portal/xdg_desktop_portal.dart"; - -class NotificationController - extends AsyncNotifier { - @override - Future build() async { - final notifications = FlutterLocalNotificationsPlugin(); - - if (!Platform.isLinux) { - final darwin = DarwinInitializationSettings(); - - await notifications.initialize( - settings: .new( - windows: .new( - appName: "Nexus", - appUserModelId: "nexus.federated.nexus", - guid: "dde78daf-130f-4e46-a80a-e31deeab45d7", - ), - android: .new("ic_launcher_foreground"), - iOS: darwin, - macOS: darwin, - ), - onDidReceiveNotificationResponse: (details) { - if (details.payload == null) return; - - if (int.tryParse(details.payload!) case final rowId?) { - if (navigatorKey.currentContext case final context?) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsList( - highlightedEvent: rowId, - defaultToAllNotifications: true, - ), - ), - ); - } - } - }, - ); - } - - if (Platform.isLinux) { - const notificationChannel = MethodChannel("nexus/notifications"); - - notificationChannel.setMethodCallHandler((call) async { - if (call.method != "notificationClicked") { - return; - } - - if (int.tryParse(call.arguments) case int rowId) { - if (navigatorKey.currentContext case final context?) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsList( - highlightedEvent: rowId, - defaultToAllNotifications: true, - ), - ), - ); - } - } - }); - - ref.onDispose(() => notificationChannel.setMethodCallHandler(null)); - } - - return notifications; - } - - Future requestPermissions() async { - if (Platform.isLinux) { - return true; - } - - final controller = await future; - - if (Platform.isIOS) { - return await controller - .resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin - >() - ?.requestPermissions(alert: true, badge: true, sound: true) ?? - true; - } else if (Platform.isMacOS) { - return await controller - .resolvePlatformSpecificImplementation< - MacOSFlutterLocalNotificationsPlugin - >() - ?.requestPermissions(alert: true, badge: true, sound: true) ?? - true; - } else if (Platform.isAndroid) { - return await controller - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin - >() - ?.requestNotificationsPermission() ?? - true; - } - - return true; - } - - Future send({ - required int id, - required String title, - String? body, - File? icon, - String? payload, - }) async { - debugPrint("Sending notification for $id"); - - if (Platform.isLinux) { - final portal = await ref.watch(PortalController.provider.future); - - await portal.notification.addNotification( - id.toString(), - title: title, - body: body, - defaultAction: "app.event", - defaultActionTarget: payload, - category: XdgNotificationCategory.imReceived, - priority: .normal, - buttons: [ - // TODO: Impl when notif daemons actualy support imReplyWithText - // .new( - // label: "Reply", - // action: "app.reply", - // purpose: XdgNotificationButtonPurpose.imReplyWithText, - // ), - ], - ); - } else { - final notificationDetails = NotificationDetails( - android: .new( - "messages", - "Messages", - largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path), - ), - // TODO: See if icons can be added to iOS, macOS, and Windows - // notifications (#68) - iOS: .new(), - macOS: .new(), - windows: .new(), - ); - - final controller = await future; - - await controller.show( - id: id, - title: title, - body: body, - notificationDetails: notificationDetails, - payload: payload, - ); - } - } - - static final provider = - AsyncNotifierProvider< - NotificationController, - FlutterLocalNotificationsPlugin - >(NotificationController.new); -} diff --git a/lib/controllers/notifications.dart b/lib/controllers/notifications.dart deleted file mode 100644 index cd10dff..0000000 --- a/lib/controllers/notifications.dart +++ /dev/null @@ -1,93 +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.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/models/event.dart"; - -typedef NotificationsRequest = (UnreadType? unreadType, String? roomId); - -class NotificationsController(final NotificationsRequest request) - extends AsyncNotifier> { - void storeEvents(Iterable events) => ref - .read(RoomsController.provider.notifier) - .update( - .new( - events - .groupListsBy((event) => event.roomId) - .map( - (roomId, roomEvents) => .new( - roomId, - .new( - events: IMap.fromIterable( - roomEvents, - keyMapper: (event) => event.rowId, - ), - ), - ), - ), - ), - ); - - @override - Future> build() async { - final client = ref.read(ClientController.provider.notifier); - - final (unreadType, roomId) = request; - - final mentions = await client.getMentions( - .new( - maxTimestamp: .now(), - unreadType: unreadType ?? .highlight, - roomId: roomId, - ), - ); - - storeEvents(mentions); - - return .new(mentions.map((event) => (event.rowId, event.roomId))); - } - - Future loadOlder() async { - final currentNotifications = await future; - state = .loading(); - state = await .guard(() async { - final lastNotification = currentNotifications.lastOrNull; - final lastTs = lastNotification == null - ? null - : ref.watch( - RoomsController.provider.select( - (rooms) => rooms[lastNotification.$2] - ?.events[lastNotification.$1] - ?.timestamp, - ), - ); - - if (lastTs == null) return currentNotifications; - - final client = ref.read(ClientController.provider.notifier); - final (unreadType, roomId) = request; - - final newNotifications = await client.getMentions( - .new( - maxTimestamp: lastTs, - unreadType: unreadType ?? .highlight, - roomId: roomId, - ), - ); - - storeEvents(newNotifications); - - return currentNotifications.addAll( - newNotifications.map((event) => (event.rowId, event.roomId)), - ); - }); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose< - NotificationsController, - IList<(int, String)>, - NotificationsRequest - >(NotificationsController.new); -} diff --git a/lib/controllers/pinned_events.dart b/lib/controllers/pinned_events.dart index d0d7bb5..914a301 100644 --- a/lib/controllers/pinned_events.dart +++ b/lib/controllers/pinned_events.dart @@ -2,25 +2,29 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/pinned_ids.dart"; +import "package:nexus/models/event.dart"; + +class PinnedEventsController extends AsyncNotifier> { + final String roomId; + PinnedEventsController(this.roomId); -class PinnedEventsController(final String roomId) - extends AsyncNotifier> { @override - Future> build() async { + Future> build() async { final pinIds = ref.watch(PinnedIdsController.provider(roomId)); return (await Future.wait( pinIds.map( (eventId) => ref.watch( - EventController.provider(.new(eventId: eventId, roomId: roomId)) - .selectAsync((data) => data?.rowId), + EventController.provider( + .new(eventId: eventId, roomId: roomId), + ).future, ), ), )).nonNulls.toIList(); } static final provider = AsyncNotifierProvider.family - .autoDispose, String>( + .autoDispose, String>( PinnedEventsController.new, ); } diff --git a/lib/controllers/pinned_ids.dart b/lib/controllers/pinned_ids.dart index 01358a9..3d1e5da 100644 --- a/lib/controllers/pinned_ids.dart +++ b/lib/controllers/pinned_ids.dart @@ -5,7 +5,10 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/pinned_events.dart"; -class PinnedIdsController(final String roomId) extends Notifier> { +class PinnedIdsController extends Notifier> { + final String roomId; + PinnedIdsController(this.roomId); + @override IList build() { final room = ref.watch( diff --git a/lib/controllers/portal.dart b/lib/controllers/portal.dart deleted file mode 100644 index daae24a..0000000 --- a/lib/controllers/portal.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "dart:io"; - -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:xdg_desktop_portal/xdg_desktop_portal.dart"; - -class PortalController extends AsyncNotifier { - @override - Future build() async { - final portal = XdgDesktopPortalClient(); - if (!await File("/.flatpak-info").exists()) { - await portal.registerApplication("nexus.federated.nexus"); - } - - ref.onDispose(portal.close); - return portal; - } - - static final provider = - AsyncNotifierProvider( - PortalController.new, - ); -} diff --git a/lib/controllers/power_level.dart b/lib/controllers/power_level.dart index 7111e38..2f0c72e 100644 --- a/lib/controllers/power_level.dart +++ b/lib/controllers/power_level.dart @@ -6,8 +6,10 @@ import "package:nexus/models/configs/power_level.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/power_levels.dart"; -class PowerLevelController(final PowerLevelConfig config) - extends Notifier { +class PowerLevelController extends Notifier { + final PowerLevelConfig config; + PowerLevelController(this.config); + @override bool build() { if (config case EventPowerLevelConfig(:final eventType)) { diff --git a/lib/controllers/profile.dart b/lib/controllers/profile.dart index 7a1477c..32525a3 100644 --- a/lib/controllers/profile.dart +++ b/lib/controllers/profile.dart @@ -1,17 +1,17 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; -import "package:nexus/models/profile_response.dart"; +import "package:nexus/models/profile.dart"; + +class ProfileController extends AsyncNotifier { + final String userId; + ProfileController(this.userId); -class ProfileController(final String userId) - extends AsyncNotifier { @override - Future build() { - final client = ref.read(ClientController.provider.notifier); + Future build() { + final client = ref.watch(ClientController.provider.notifier); return client.getProfile(userId); } static final provider = AsyncNotifierProvider.family - .autoDispose( - ProfileController.new, - ); + .autoDispose(ProfileController.new); } diff --git a/lib/controllers/push_key.dart b/lib/controllers/push_key.dart deleted file mode 100644 index e3f6b82..0000000 --- a/lib/controllers/push_key.dart +++ /dev/null @@ -1,31 +0,0 @@ -import "dart:convert"; - -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/key.dart"; - -class PushKeyController(final String instance) extends AsyncNotifier { - @override - Future build() async => json.decode( - await ref.watch(KeyController.provider(KeyController.pushKeyKey).future) ?? - "{}", - )[instance]; - - Future set(String? value) async { - final provider = KeyController.provider(KeyController.pushKeyKey); - final notifier = ref.watch(provider.notifier); - final prefs = IMap(json.decode(await ref.watch(provider.future) ?? "{}")); - - state = .data(value); - notifier.set( - json.encode( - prefs.add(instance, value).where((_, value) => value != null).unlock, - ), - ); - } - - static final provider = - AsyncNotifierProvider.family( - PushKeyController.new, - ); -} diff --git a/lib/controllers/reactions.dart b/lib/controllers/reactions.dart index 9a92843..615a59d 100644 --- a/lib/controllers/reactions.dart +++ b/lib/controllers/reactions.dart @@ -5,8 +5,10 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/configs/reactions.dart"; import "package:nexus/models/content/reaction.dart"; -class ReactionsController(final ReactionsConfig config) - extends AsyncNotifier>> { +class ReactionsController extends AsyncNotifier>> { + final ReactionsConfig config; + ReactionsController(this.config); + @override Future>> build() async { final eventInfo = ref.watch( diff --git a/lib/controllers/recent_emoji.dart b/lib/controllers/recent_emoji.dart deleted file mode 100644 index d814a79..0000000 --- a/lib/controllers/recent_emoji.dart +++ /dev/null @@ -1,57 +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:material_emoji_picker/material_emoji_picker.dart"; -import "package:nexus/controllers/account_data.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/models/account_data.dart"; - -class RecentEmojiController extends Notifier> { - @override - IList build() { - final Set emoji = ref - .watch(EmojiController.provider) - .requireValue - .map((element) => element.emojis) - .flattened - .map((e) => e.value) - .toSet(); - - final IList recents = ref.watch( - AccountDataController.provider.select( - (value) => value.recentEmoji.recentEmoji, - ), - ); - - return .new(recents.where((recent) => emoji.contains(recent.emoji))); - } - - Future add(String emoji) => ref - .watch(ClientController.provider.notifier) - .setAccountData( - .new( - type: AccountData.recentEmojiKey, - content: RecentEmojiData( - recentEmoji: .new([ - .new( - emoji: emoji, - total: - (state - .firstWhereOrNull( - (element) => element.emoji == emoji, - ) - ?.total ?? - 0) + - 1, - ), - ...state.whereNot((element) => element.emoji == emoji), - ]), - ), - ), - ); - - static final provider = - NotifierProvider.autoDispose>( - RecentEmojiController.new, - ); -} diff --git a/lib/controllers/room_chat.dart b/lib/controllers/room_chat.dart index 1576401..558817c 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -1,6 +1,5 @@ import "dart:async"; import "dart:math"; - import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -10,81 +9,59 @@ import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/reaction.dart"; -import "package:nexus/models/direction.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/models/room_chat.dart"; -typedef ChatParams = ({String roomId, int? contextualEvent}); +class RoomChatController extends AsyncNotifier?> { + final String roomId; + RoomChatController(this.roomId); -class RoomChatController(final ChatParams info) - extends AsyncNotifier { @override - Future build() async { - final client = ref.read(ClientController.provider.notifier); - final room = ref.read( - RoomsController.provider.select((rooms) => rooms[info.roomId]), + Future?> build() async { + final client = ref.watch(ClientController.provider.notifier); + final room = ref.watch( + RoomsController.provider.select((rooms) => rooms[roomId]), ); if (room == null) return null; if (!room.hasFetchedState) { - final state = await client.getRoomState(.new(roomId: info.roomId)); - ref.read(RoomsController.provider.notifier).addState(info.roomId, state); + final state = await client.getRoomState(.new(roomId: roomId)); + + await ref.read(RoomsController.provider.notifier).addState(roomId, state); } - final timeline = room.timeline + // While there are under 20 events, try to load more + // until there's no more or the conditions are met. + if (room.hasMore && room.timeline.length < 20) { + loadOlder(); + } + + return room.timeline .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) - .map((e) => e.value) + .map((element) => element.value) .toIList() - .addAll(room.clientSticky) + .addAll(room.sticky) + .map((entry) { + final foundEvent = entry == null ? null : room.events[entry]; + + final editedEvent = + foundEvent == null || foundEvent.lastEditRowId == 0 + ? null + : room.events[foundEvent.lastEditRowId]; + + return editedEvent == null + ? foundEvent + : foundEvent?.copyWith( + content: editedEvent.content, + localContent: editedEvent.localContent, + ); + }) .nonNulls .toIList(); - if (info.contextualEvent == null || - timeline.contains(info.contextualEvent)) { - ref.watch( - RoomsController.provider.select( - (rooms) => - (rooms[info.roomId]?.timeline, rooms[info.roomId]?.clientSticky), - ), - ); - - return .new( - timeline: timeline, - hasMoreBackward: room.hasMore, - hasMoreForward: false, - ); - } else { - final context = await client.getEventContext( - .new( - roomId: info.roomId, - eventId: room.events[info.contextualEvent!]!.eventId, - ), - ); - - final events = context.before.add(context.event).addAll(context.after); - ref - .read(RoomsController.provider.notifier) - .update( - .new({ - info.roomId: Room( - events: IMap.fromIterable( - events, - keyMapper: (event) => event.rowId, - ), - ), - }), - ); - return .new( - timeline: .new(events.map((element) => element.rowId)), - hasMoreBackward: true, - hasMoreForward: true, - historicalData: .new(start: context.start, end: context.end), - ); - } } Future deleteMessage(Event event, {String? reason}) => ref @@ -92,136 +69,49 @@ class RoomChatController(final ChatParams info) .redactEvent( RedactEventRequest( eventId: event.eventId, - roomId: info.roomId, + roomId: roomId, reason: reason, ), ); - void hideClientSticky(int eventRowId) => ref - .read(RoomsController.provider.notifier) - .update( - .new({ - info.roomId: Room( - resetClientSticky: true, - clientSticky: - ref - .read( - RoomsController.provider.select( - (rooms) => rooms[info.roomId]?.clientSticky, - ), - ) - ?.remove(eventRowId) ?? - .new(), + Future loadOlder() async { + final timelineKeys = ref + .read(RoomsController.provider.select((value) => value[roomId])) + ?.timeline + .keys; + final response = await ref + .watch(ClientController.provider.notifier) + .paginate( + .new( + roomId: roomId, + maxTimelineId: timelineKeys?.isNotEmpty == true + ? timelineKeys?.reduce(min) + : null, ), - }), - ); + ); - Future paginate(Direction direction) async { - if (state.isLoading) return; - - final chat = await future; - - if (direction == .forward - ? chat?.hasMoreForward == false - : chat?.hasMoreBackward == false) { - return; - } - - state = .loading(); - - final client = ref.read(ClientController.provider.notifier); - - if (chat?.historicalData == null) { - final timelineKeys = ref - .read(RoomsController.provider.select((value) => value[info.roomId])) - ?.timeline - .keys; - final response = await client.paginate( - .new( - roomId: info.roomId, - maxTimelineId: timelineKeys?.isNotEmpty == true - ? timelineKeys?.reduce(min) - : null, - ), - ); - - if (response.events.isEmpty) { - state = .data(state.value); - } - - ref - .read(RoomsController.provider.notifier) - .update( - IMap({ - info.roomId: Room( - events: IMap.fromIterable( - response.events.addAll(response.relatedEvents), - keyMapper: (event) => event.rowId, - valueMapper: (event) => event, - ), - hasMore: response.hasMore, - timeline: IMap.fromIterable( - response.events, - keyMapper: (event) => event.timelineRowId, - valueMapper: (event) => event.rowId, - ), + ref + .watch(RoomsController.provider.notifier) + .update( + IMap({ + roomId: Room( + events: IMap.fromIterable( + response.events.addAll(response.relatedEvents), + keyMapper: (event) => event.rowId, + valueMapper: (event) => event, ), - }), - ); - } else { - final paginationResponse = await client.paginateManual( - .new( - roomId: info.roomId, - direction: direction, - since: direction == .forward - ? chat!.historicalData!.end - : chat!.historicalData!.start, - ), - ); - - ref - .read(RoomsController.provider.notifier) - .update( - .new({ - info.roomId: Room( - events: IMap.fromIterable( - paginationResponse.events, - keyMapper: (event) => event.rowId, - ), + hasMore: response.hasMore, + timeline: IMap.fromIterable( + response.events, + keyMapper: (event) => event.timelineRowId, + valueMapper: (event) => event.rowId, ), - }), - ); + ), + }), + .new(), + ); - final eventRowIds = paginationResponse.events - .map((element) => element.rowId) - .toIList(); - - state = .data( - .new( - timeline: direction == .forward - ? chat.timeline.addAll(eventRowIds) - : eventRowIds.addAll(chat.timeline), - hasMoreForward: - direction == .forward && paginationResponse.nextBatch == null - ? false - : chat.hasMoreForward, - hasMoreBackward: - direction == .backward && paginationResponse.nextBatch == null - ? false - : chat.hasMoreBackward, - historicalData: chat.historicalData?.copyWith( - start: - (direction == .backward - ? paginationResponse.nextBatch - : null) ?? - chat.historicalData!.start, - end: - (direction == .forward ? paginationResponse.nextBatch : null) ?? - chat.historicalData!.end, - ), - ), - ); - } + return response.hasMore; } Future send( @@ -229,23 +119,17 @@ class RoomChatController(final ChatParams info) bool shouldMention = true, required IList tags, required RelationType relationType, - int? relatedEventRowId, + Event? relation, }) async { Content? baseContent; - final relatedEvent = relatedEventRowId == null - ? null - : ref.watch( - RoomsController.provider.select( - (rooms) => rooms[info.roomId]?.events[relatedEventRowId], - ), - ); - - if (relationType == .edit && relatedEventRowId != null) { - baseContent = relatedEvent?.content; + if (relationType == .edit) { + baseContent = relation?.content; } else { - final provider = AttachmentController.provider(info.roomId); - baseContent = ref.read(provider)?.$2; - ref.invalidate(provider); + final attachment = ref.watch(AttachmentController.provider(roomId)); + if (attachment != null && attachment.$2 == null) return; + + baseContent = attachment?.$2; + ref.invalidate(AttachmentController.provider(roomId)); } var taggedMessage = text; @@ -260,36 +144,37 @@ class RoomChatController(final ChatParams info) ); } - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); final event = await client.sendMessage( SendMessageRequest( - roomId: info.roomId, + roomId: roomId, baseContent: baseContent, mentions: Mentions( userIds: [ if (shouldMention == true && - relatedEvent != null && + relation != null && relationType == RelationType.reply) - relatedEvent.sender, + relation.sender, ].toIList(), room: taggedMessage.contains("@room"), ), text: taggedMessage, - relation: relatedEvent == null + relation: relation == null ? null - : .new(eventId: relatedEvent.eventId, relationType: relationType), + : .new(eventId: relation.eventId, relationType: relationType), ), ); ref - .read(RoomsController.provider.notifier) + .watch(RoomsController.provider.notifier) .update( .new({ - info.roomId: .new( + roomId: .new( events: .new({event.rowId: event}), - clientSticky: .new({event.rowId}), + sticky: .new({event.rowId}), ), }), + .new(), ); } @@ -298,10 +183,10 @@ class RoomChatController(final ChatParams info) Event event, String userId, ) async { - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); final allReactionEvents = await client.getRelatedEvents( .new( - roomId: info.roomId, + roomId: roomId, eventId: event.eventId, relationType: "m.annotation", ), @@ -322,18 +207,16 @@ class RoomChatController(final ChatParams info) if (reactionEvent != null) { await ref .watch(ClientController.provider.notifier) - .redactEvent( - .new(eventId: reactionEvent.eventId, roomId: info.roomId), - ); + .redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId)); } } Future sendReaction(String reaction, Event event) async { - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); await client.sendEvent( .new( - roomId: info.roomId, + roomId: roomId, type: EventType.reaction.type, content: ReactionContent(key: reaction), synchronous: true, @@ -345,7 +228,7 @@ class RoomChatController(final ChatParams info) } static final provider = AsyncNotifierProvider.family - .autoDispose( + .autoDispose?, String>( RoomChatController.new, ); } diff --git a/lib/controllers/room_creators.dart b/lib/controllers/room_creators.dart index 09acdd7..7db72c2 100644 --- a/lib/controllers/room_creators.dart +++ b/lib/controllers/room_creators.dart @@ -4,7 +4,10 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/create.dart"; import "package:nexus/models/room.dart"; -class RoomCreatorsController(final Room room) extends Notifier> { +class RoomCreatorsController extends Notifier> { + final Room room; + RoomCreatorsController(this.room); + @override IList build() { final createRowId = room.state[EventType.create.type]?[""]; diff --git a/lib/controllers/room_summary.dart b/lib/controllers/room_summary.dart deleted file mode 100644 index e5b3174..0000000 --- a/lib/controllers/room_summary.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/models/requests/join_room.dart"; -import "package:nexus/models/room_summary.dart"; - -class RoomSummaryController(final JoinRoomRequest request) - extends AsyncNotifier { - @override - Future build() => - ref.read(ClientController.provider.notifier).getRoomSummary(request); - - static final provider = AsyncNotifierProvider.family - .autoDispose( - RoomSummaryController.new, - ); -} diff --git a/lib/controllers/rooms.dart b/lib/controllers/rooms.dart index 7b86f2a..d0c6eb9 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -1,4 +1,4 @@ -import "package:collection/collection.dart"; +import "dart:isolate"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/models/event.dart"; @@ -8,35 +8,35 @@ class RoomsController extends Notifier> { @override IMap build() => .new(); - void addState( + Future addState( String roomId, IList state, { bool isMembers = false, - }) => update( + }) async => update( .new({ roomId: Room( events: .fromEntries(state.map((event) => .new(event.rowId, event))), hasFetchedState: true, hasFetchedMembers: isMembers, - state: .new( - state - .groupListsBy((event) => event.type) - .map( - (type, group) => .new( - type, - .fromEntries(group.map((e) => .new(e.stateKey!, e.rowId))), - ), + state: await Isolate.run(() { + final newState = state.fold>>( + .new(), + (previousValue, stateEvent) => previousValue.add( + stateEvent.type, + (previousValue[stateEvent.type] ?? .new()).add( + stateEvent.stateKey!, + stateEvent.rowId, ), - ), + ), + ); + return newState; + }), ), }), .new(), ); - void update( - IMap rooms, [ - ISet leftRooms = const ISet.empty(), - ]) { + void update(IMap rooms, ISet leftRooms) { final merged = rooms.entries.fold(state, (acc, entry) { final roomId = entry.key; final incoming = entry.value; @@ -46,16 +46,13 @@ class RoomsController extends Notifier> { roomId, existing?.copyWith( hasMore: incoming.hasMore, - clientSticky: incoming.resetClientSticky - ? incoming.clientSticky - : (incoming.clientSticky.isEmpty == true - ? existing.clientSticky - : existing.clientSticky.addAll( - incoming.clientSticky, - )) - .removeWhere( - (rowId) => incoming.timeline.values.contains(rowId), - ), + sticky: + (incoming.sticky.isEmpty == true + ? existing.sticky + : existing.sticky.addAll(incoming.sticky)) + .removeWhere( + (rowId) => incoming.timeline.values.contains(rowId), + ), metadata: incoming.metadata ?? existing.metadata, events: incoming.events.isEmpty ? existing.events @@ -67,7 +64,6 @@ class RoomsController extends Notifier> { (previousValue[event.key] ?? .new()).addAll(event.value), ), ), - resetClientSticky: false, reset: false, hasFetchedMembers: incoming.hasFetchedMembers || existing.hasFetchedMembers, diff --git a/lib/controllers/settings.dart b/lib/controllers/settings.dart index 8c107e3..572d2e9 100644 --- a/lib/controllers/settings.dart +++ b/lib/controllers/settings.dart @@ -9,14 +9,14 @@ class SettingsController extends AsyncNotifier { final file = await ref.watch(SettingsFileController.provider.future); try { - return .fromJson(json.decode(await file.readAsString())); + return Settings.fromJson(json.decode(await file.readAsString())); } catch (_) { - return .new(); + return Settings(); } } Future set(Settings settings) async { - state = .data(settings); + state = AsyncData(settings); final file = await ref.watch(SettingsFileController.provider.future); await file.writeAsString(json.encode(settings.toJson())); } diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index 073019a..7c9796b 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -1,21 +1,12 @@ import "dart:io"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:intl/intl.dart"; import "package:m3e_buttons/m3e_buttons.dart"; -import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/init_complete.dart"; -import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/settings.dart"; -import "package:nexus/controllers/unified_push.dart"; -import "package:nexus/controllers/spec_versions.dart"; -import "package:nexus/controllers/unified_push_allowed.dart"; -import "package:nexus/models/account_data.dart"; import "package:nexus/models/settings_category.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/settings/dialog_list_tile.dart"; @@ -53,7 +44,8 @@ class SettingsSectionsController .new( title: "Use Dynamic Theme", icon: Icons.palette, - description: "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", + description: + "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.", builder: (title, description, icon) => SwitchListTile( title: Text(title), subtitle: Text(description), @@ -77,41 +69,10 @@ class SettingsSectionsController title: "Behavior", icon: Icons.psychology, settings: .new([ - .new( - title: "All Rooms In Home", - description: "Show all rooms in home, instead of just ones that aren't in spaces", - icon: Icons.home, - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.showAllInHome, - onChanged: (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(showAllInHome: value)) - .onError(showError), - ), - ), - if (!Platform.isAndroid && !Platform.isIOS) - .new( - title: "Show Minimize Button", - description: - "Enable this to add a minimize button to the appbar", - icon: Icons.minimize, - builder: (title, description, icon) => SwitchListTile( - title: Text(title), - subtitle: Text(description), - secondary: Icon(icon), - value: settings.showMinimizeButton, - onChanged: (value) => ref - .watch(SettingsController.provider.notifier) - .set(settings.copyWith(showMinimizeButton: value)) - .onError(showError), - ), - ), .new( title: "Linux Mobile Mode", - description: "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", + description: + "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.", icon: Icons.construction, builder: (title, description, icon) => SwitchListTile( title: Text(title), @@ -132,146 +93,6 @@ class SettingsSectionsController if (ref.watch(ClientStateController.provider)?.isLoggedIn == true) "Account": .new([ .new(title: "Profile", icon: Icons.person, settings: .new([])), - .new( - title: "Notifications", - icon: Icons.notifications, - settings: .new([ - .new( - title: "Push notifications", - description: "Enable push notifications using Web Push", - builder: (title, description, icon) => HookConsumer( - builder: (context, ref, _) { - final loading = useState(false); - final pusherRegistered = ref.watch( - UnifiedPushController.provider, - ); - final unifiedPushAllowed = ref.watch( - UnifiedPushAllowedController.provider, - ); - - return SwitchListTile( - title: Text(title), - subtitle: Text( - unifiedPushAllowed.maybeWhen( - data: (data) => data, - orElse: () => null, - ) ?? - description, - ), - secondary: Icon(icon), - value: pusherRegistered.maybeWhen( - data: (value) => value, - orElse: () => false, - ), - onChanged: loading.value - ? null - : unifiedPushAllowed.maybeWhen( - data: (data) => data == null, - orElse: () => false, - ) - ? pusherRegistered.maybeWhen( - data: (_) => (value) async { - try { - loading.value = true; - if (value) { - if (await ref - .watch( - NotificationController - .provider - .notifier, - ) - .requestPermissions()) { - await ref - .watch( - UnifiedPushController - .provider - .notifier, - ) - .register(); - } else { - // TODO: Handle not granted - } - } else { - await ref - .watch( - UnifiedPushController - .provider - .notifier, - ) - .deregister(); - } - } catch (error, stackTrace) { - showError(error, stackTrace); - } finally { - loading.value = false; - } - }, - orElse: () => null, - ) - : null, - ); - }, - ), - icon: Icons.notification_add, - ), - ]), - ), - .new( - title: "Safety", - icon: Icons.gpp_good, - settings: .new([ - .new( - title: "Invite Blocking", - description: "Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).", - builder: (title, description, icon) => Consumer( - builder: (context, ref, _) { - final specVersionsResponse = ref.watch( - SpecVersionsController.provider, - ); - return DialogListTile( - icon: Icon(icon), - title: title, - subtitle: Text(description), - initialValue: ref - .watch(AccountDataController.provider) - .invitePermissionConfig - .defaultAction, - options: - specVersionsResponse.maybeWhen( - data: (data) => data.unstableFeatures.msc4494, - orElse: () => true, - ) - ? DefaultInviteAction.values - : IList(DefaultInviteAction.values) - .remove(.denyPublic) - .toList(), - getName: (option) => switch (option) { - .allow => "Allow", - .deny => "Deny", - .denyPublic => "Deny public", - }, - onChanged: specVersionsResponse.maybeWhen( - data: (_) => - (value) => ref - .watch(ClientController.provider.notifier) - .setAccountData( - .new( - type: AccountData.invitePermissionConfigKey, - content: InvitePermissionConfig( - defaultAction: value, - ), - ), - ) - .onError(showError), - orElse: () => null, - ), - ); - }, - ), - icon: Icons.person_off, - ), - ]), - ), .new( title: "Other", icon: Icons.key, @@ -285,21 +106,15 @@ class SettingsSectionsController final colorScheme = Theme.of(context).colorScheme; return M3EButton.icon( onPressed: () async { - final container = ProviderScope.containerOf(context); + Navigator.of( + context, + ).popUntil((route) => route.isFirst); - final route = ModalRoute.of(context)!; - Navigator.of(context).popUntil((r) => r.isFirst); - await route.completed; + await WidgetsBinding.instance.endOfFrame; - await container - .read(UnifiedPushController.provider.notifier) - .deregister() - .onError(showError); - container.invalidate(InitCompleteController.provider); - await container - .read(ClientController.provider.notifier) - .logout() - .onError(showError); + await ref + .watch(ClientController.provider.notifier) + .logout(); }, label: Text(title), icon: Icon(icon), diff --git a/lib/controllers/shared_prefs.dart b/lib/controllers/shared_prefs.dart index 9ecb34a..876fc47 100644 --- a/lib/controllers/shared_prefs.dart +++ b/lib/controllers/shared_prefs.dart @@ -1,12 +1,12 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:shared_preferences/shared_preferences.dart"; -class SharedPrefsController extends Notifier { +class SharedPrefsController extends AsyncNotifier { @override - SharedPreferencesAsync build() => SharedPreferencesAsync(); + Future build() async => .getInstance(); static final provider = - NotifierProvider( + AsyncNotifierProvider( SharedPrefsController.new, ); } diff --git a/lib/controllers/spaces.dart b/lib/controllers/spaces.dart index e86cfe1..24113ad 100644 --- a/lib/controllers/spaces.dart +++ b/lib/controllers/spaces.dart @@ -1,19 +1,18 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/settings.dart"; import "package:nexus/controllers/top_level_spaces.dart"; import "package:nexus/controllers/space_edges.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/space.dart"; import "package:nexus/models/subspace.dart"; -class SpacesController extends AsyncNotifier> { +class SpacesController extends Notifier> { @override - Future> build() async { + IList build() { final rooms = ref.watch(RoomsController.provider); final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); final spaceEdges = ref.watch(SpaceEdgesController.provider); @@ -78,21 +77,21 @@ class SpacesController extends AsyncNotifier> { final spaces = topLevelSpaceIds.map(buildSpace).toIList(); - final usedRooms = { + final usedRoomIds = { for (final space in spaces) ...[ - ...space.children, - ...space.subSpaces.expand((s) => s.children), + ...space.children.map((r) => r.metadata?.id), + ...space.subSpaces.expand((s) => s.children.map((r) => r.metadata?.id)), ], }.nonNulls.toISet(); - final directMessages = accountData.directMessages.values.flattened; + final directMessages = IMap( + accountData["m.direct"]?.content ?? {}, + ).values.expand((e) => e).toISet(); final otherRooms = rooms.entries .where( (e) => - !usedRooms - .map((element) => element.metadata?.id) - .contains(e.key) && + !usedRoomIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) && !childrenById.containsKey(e.key), ) @@ -100,15 +99,6 @@ class SpacesController extends AsyncNotifier> { .toIList(); final homeRooms = otherRooms - .addAll( - await ref.watch( - SettingsController.provider.selectAsync( - (data) => data.showAllInHome, - ), - ) - ? usedRooms - : .empty(), - ) .where((r) => !directMessages.contains(r.metadata?.id)) .toIList(); @@ -155,7 +145,7 @@ class SpacesController extends AsyncNotifier> { .toIList(); } - static final provider = AsyncNotifierProvider>( + static final provider = NotifierProvider>( SpacesController.new, ); } diff --git a/lib/controllers/spec_versions.dart b/lib/controllers/spec_versions.dart deleted file mode 100644 index d2316bd..0000000 --- a/lib/controllers/spec_versions.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/models/spec_versions_response.dart"; - -class SpecVersionsController extends AsyncNotifier { - @override - Future build() => - ref.read(ClientController.provider.notifier).getSpecVersions(); - - static final provider = - AsyncNotifierProvider.autoDispose< - SpecVersionsController, - SpecVersionsResponse - >(SpecVersionsController.new); -} diff --git a/lib/controllers/unified_push.dart b/lib/controllers/unified_push.dart deleted file mode 100644 index 97849fe..0000000 --- a/lib/controllers/unified_push.dart +++ /dev/null @@ -1,222 +0,0 @@ -import "dart:async"; -import "dart:convert"; -import "dart:io"; - -import "package:flutter/foundation.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:intl/intl.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/notification.dart"; -import "package:nexus/controllers/push_key.dart"; -import "package:nexus/main.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/sticker.dart"; -import "package:unifiedpush/unifiedpush.dart"; -import "package:unifiedpush_storage_shared_preferences/storage.dart"; -import "package:window_manager/window_manager.dart"; - -class UnifiedPushController extends AsyncNotifier { - Completer? _endpoint; - - @override - Future build() async { - if (!Platform.isLinux && !Platform.isAndroid) return false; - - final registered = await UnifiedPush.initialize( - linuxOptions: .new( - dbusName: "nexus.federated.nexus.UnifiedPush", - storage: UnifiedPushStorageSharedPreferences(), - background: isInBackground, - shouldWriteService: false, - ), - onNewEndpoint: (endpoint, instance) async { - final task = Future(() async { - final pushKey = endpoint.pubKeySet!.pubKey; - await ref - .read(PushKeyController.provider(instance).notifier) - .set(pushKey); - - await ref - .read(ClientController.provider.notifier) - .registerPusher( - .new( - appDisplayName: "Nexus", - appId: "nexus.federated.nexus", - data: .webPush( - url: .parse(endpoint.url), - auth: endpoint.pubKeySet!.auth, - ), - deviceDisplayName: - "Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}", - kind: .webPush, - lang: "en", - pushKey: pushKey, - ), - ); - - state = .data(true); - }); - - if (_endpoint case final completer? when !completer.isCompleted) { - completer.complete(task); - } else { - await task; - } - }, - onRegistrationFailed: (reason, instance) { - if (_endpoint case final completer? when !completer.isCompleted) { - completer.completeError(reason); - } else { - throw reason; - } - }, - onMessage: (message, instance) async { - debugPrint("UP message received for $instance"); - if (message.decrypted == false) { - throw Exception( - "Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.", - ); - } - final pushResponse = await ref - .read(ClientController.provider.notifier) - .handlePush(json.decode(String.fromCharCodes(message.content))); - - if (pushResponse == null) return; - - final (event, roomMetadata) = pushResponse; - - if (event.unreadType?.has(.notify) != true || - (!isInBackground && - await windowManager.isFocused().onError((_, _) => true) && - await ref.read( - KeyController.provider(KeyController.roomKey).future, - ) == - event.roomId)) { - if (isInBackground) exit(0); - return; - } - - final icon = roomMetadata.avatar == null - ? null - : await ref - .read(ClientController.provider.notifier) - .downloadMedia( - .new(mxc: roomMetadata.avatar!, isAvatar: true), - ); - - await ref - .read(NotificationController.provider.notifier) - .send( - id: event.eventId.hashCode & 0x7fffffff, - title: roomMetadata.name ?? "New Event", - icon: icon, - payload: event.rowId.toString(), - body: switch (event.content) { - MessageContent(:final body?) || - StickerContent(:final body) => body, - _ => null, - }, - ); - - if (isInBackground) exit(0); - }, - onUnregistered: deregister, - ); - - if (!registered) return false; - - try { - return await register(); - } catch (error, stackTrace) { - showError(error, stackTrace); - return false; - } - } - - Future register() async { - state = .loading(); - try { - final deviceIdCompleter = Completer(); - final subscription = ref.listen( - ClientStateController.provider.select((value) => value?.deviceId), - (_, next) { - if (next != null && !deviceIdCompleter.isCompleted) { - deviceIdCompleter.complete(next); - } - }, - fireImmediately: true, - ); - final deviceId = await deviceIdCompleter.future.whenComplete( - subscription.close, - ); - - await Future(() async { - final capabilities = await ref - .read(ClientController.provider.notifier) - .getCapabilities(); - - if (capabilities.webpush?.vapid == null) { - throw UnsupportedError( - "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.", - ); - } - - if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor()) { - throw Exception("No UnifiedPush distributors found."); - } - - final endpoint = _endpoint = Completer(); - - try { - await Future.wait([ - UnifiedPush.register( - instance: deviceId, - vapid: capabilities.webpush?.vapid, - ), - endpoint.future, - ], eagerError: true); - } finally { - if (_endpoint == endpoint) _endpoint = null; - } - }).timeout( - .new(seconds: 10), - onTimeout: () => throw Exception("UnifiedPush registration timed out."), - ); - - state = .data(true); - return true; - } catch (_) { - state = .data(false); - rethrow; - } - } - - Future deregister([String? instance]) async { - final clientState = ref.read(ClientStateController.provider); - - final keyProvider = PushKeyController.provider( - instance ?? clientState!.deviceId!, - ); - final key = await ref.read(keyProvider.future); - - if (key != null) { - await ref - .read(ClientController.provider.notifier) - .deregisterPusher(.new(appId: "nexus.federated.nexus", pushKey: key)); - await ref.read(keyProvider.notifier).set(null); - } else { - debugPrint( - "No matching pushKey found. Skipping deregistration from homeserver.", - ); - } - - await UnifiedPush.unregister(instance ?? clientState!.deviceId!); - state = .data(false); - } - - static final provider = AsyncNotifierProvider( - UnifiedPushController.new, - ); -} diff --git a/lib/controllers/unified_push_allowed.dart b/lib/controllers/unified_push_allowed.dart deleted file mode 100644 index 604a060..0000000 --- a/lib/controllers/unified_push_allowed.dart +++ /dev/null @@ -1,37 +0,0 @@ -import "dart:io"; - -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:unifiedpush/unifiedpush.dart"; - -class UnifiedPushAllowedController extends AsyncNotifier { - @override - Future build() async { - if (!await UnifiedPush.tryUseCurrentOrDefaultDistributor().timeout( - .new(seconds: 10), - onTimeout: () => - throw Exception("UnifiedPush distributor check timed out."), - )) { - return "No valid distributors found. ${Platform.isLinux - ? "Try installing KUnifiedPush" - : Platform.isAndroid - ? "Try installing Google Play Services or NTFY" - : "Your platform is not currently supported by UnifiedPush"}."; - } - - final capabilities = await ref - .watch(ClientController.provider.notifier) - .getCapabilities(); - - if (capabilities.webpush?.vapid == null) { - return "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus."; - } - - return null; - } - - static final provider = - AsyncNotifierProvider.autoDispose( - UnifiedPushAllowedController.new, - ); -} diff --git a/lib/controllers/url_preview.dart b/lib/controllers/url_preview.dart index 41c3ef4..2a81a3c 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -1,26 +1,50 @@ -import "package:flutter/widgets.dart"; +import "dart:convert"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client.dart"; +import "package:http/http.dart"; +import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/header.dart"; import "package:nexus/models/open_graph_data.dart"; -class UrlPreviewController(final Uri url) - extends AsyncNotifier { +class UrlPreviewController extends AsyncNotifier { + final String link; + UrlPreviewController(this.link); + @override Future build() async { - if (url.host == "matrix.to") return null; - try { - return await ref - .watch(ClientController.provider.notifier) - .getUrlPreview(url); - } catch (error, stackTrace) { - debugPrintStack(label: error.toString(), stackTrace: stackTrace); + final homeserver = ref.watch( + ClientStateController.provider.select((value) => value?.homeserverUrl), + ); + + if (homeserver != null && !link.contains("matrix.to")) { + { + final response = await get( + .parse(homeserver) + .resolve("/_matrix/client/v1/media/preview_url") + .replace(queryParameters: {"url": link}), + headers: await ref.watch(HeaderController.provider.future), + ); + + if (response.statusCode == 200) { + final decodedValue = json.decode(response.body); + if (decodedValue is! Map) return null; + + return .fromJson( + decodedValue, + ).copyWith(imageUrl: decodedValue["og:image"]); + } + } + } + } catch (_) { return null; } + return null; } static final provider = - AsyncNotifierProvider.family( - UrlPreviewController.new, - ); + AsyncNotifierProvider.family< + UrlPreviewController, + OpenGraphData?, + String + >(UrlPreviewController.new); } diff --git a/lib/controllers/user.dart b/lib/controllers/user.dart index c69ebd0..b366c88 100644 --- a/lib/controllers/user.dart +++ b/lib/controllers/user.dart @@ -27,15 +27,14 @@ class UserController extends AsyncNotifier { return content; } - final profileResponse = await ref.watch( + final profile = await ref.watch( ProfileController.provider(config.userId).future, ); return .new( status: .leave, - avatarUrl: profileResponse.profile.avatarUrl, - displayName: - profileResponse.profile.displayName ?? config.userId.localpart, + avatarUrl: profile.avatarUrl, + displayName: profile.displayName ?? config.userId.localpart, ); } diff --git a/lib/controllers/via.dart b/lib/controllers/via.dart index 39db682..d9227ba 100644 --- a/lib/controllers/via.dart +++ b/lib/controllers/via.dart @@ -7,7 +7,10 @@ import "package:nexus/models/content/membership.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/room.dart"; -class ViaController(final Room room) extends Notifier { +class ViaController extends Notifier { + final Room room; + ViaController(this.room); + @override String build() { final servers = {}; diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart deleted file mode 100644 index fdf3bdf..0000000 --- a/lib/helpers/extensions/build_event_options.dart +++ /dev/null @@ -1,283 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; -import "package:flutter/services.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/pinned_ids.dart"; -import "package:nexus/controllers/power_level.dart"; -import "package:nexus/controllers/recent_emoji.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/helpers/extensions/event_id_is_fake.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/emoji_picker.dart"; -import "package:nexus/main.dart"; - -extension BuildEventOptions on Event { - IList buildEventOptions({ - required BuildContext context, - required WidgetRef ref, - required String roomId, - required String userId, - required void Function(int, RelationType) onRelation, - }) { - final theme = Theme.of(context); - final danger = theme.colorScheme.error; - - final notifier = RoomChatController.provider(( - roomId: roomId, - contextualEvent: null, - )).notifier; - - final isPinned = ref - .watch(PinnedIdsController.provider(roomId)) - .contains(eventId); - - void showReasonDialog({ - required String title, - required String description, - required String action, - required Future Function(String reason) onConfirm, - }) { - showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final reasonController = useTextEditingController(); - - return AlertDialog( - title: Text(title), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text(description), - const SizedBox(height: 12), - TextField( - controller: reasonController, - textCapitalization: .sentences, - decoration: .new(labelText: "Reason (optional)"), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () { - final reason = reasonController.text; - Navigator.of(context).pop(); - - onConfirm(reason).onError(showError); - }, - child: Text(action), - ), - ], - ); - }, - ), - ); - } - - return .new([ - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .reaction, roomId: roomId), - ), - )) - PopupMenuItem( - enabled: false, - child: IconTheme( - data: theme.iconTheme, - child: Row( - mainAxisAlignment: .center, - children: [ - for (final emoji in { - ...ref - .watch(RecentEmojiController.provider) - .map((entry) => entry.emoji), - "👍", - "🤣", - "😭", - "🤔", - }.take(3)) - IconButton( - icon: Text(emoji), - onPressed: () async { - Navigator.of(context).pop(); - await ref - .watch(notifier) - .sendReaction(emoji, this) - .onError(showError); - - await ref - .read(RecentEmojiController.provider.notifier) - .add(emoji) - .onError(showError); - }, - ), - IconButton( - icon: const Icon(Icons.emoji_emotions), - onPressed: () { - Navigator.of(context).pop(); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => EmojiPicker( - allowFreeText: true, - onSelection: (emoji) { - Navigator.of(context).pop(); - ref - .watch(notifier) - .sendReaction(emoji, this) - .onError(showError); - }, - ), - ); - }, - ), - ], - ), - ), - ), - - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .message, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () => onRelation(rowId, .reply), - child: const ListTile( - leading: Icon(Icons.reply), - title: Text("Reply"), - ), - ), - - if (content is MessageContent && sender == userId) - PopupMenuItem( - onTap: () => onRelation(rowId, .edit), - child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")), - ), - - if (ref.watch( - PowerLevelController.provider( - .state(eventType: .pinnedEvents, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () async { - try { - final pins = ref.read( - PinnedIdsController.provider(roomId).notifier, - ); - - if (isPinned) { - await pins.removePin(eventId); - } else { - await pins.addPin(eventId); - } - } catch (error, stackTrace) { - showError(error, stackTrace); - } - }, - child: ListTile( - leading: const Icon(Icons.push_pin), - title: Text(isPinned ? "Unpin Event" : "Pin Event"), - ), - ), - - PopupMenuItem( - onTap: () async { - final room = ref.read( - RoomsController.provider.select((rooms) => rooms[roomId]), - ); - if (room == null) return; - - final vias = ref.read(ViaController.provider(room)); - - await Clipboard.setData( - ClipboardData( - text: - "matrix:roomid/${room.metadata?.id.substring(1)}/e/${eventId.substring(1)}$vias", - ), - ); - }, - child: const ListTile( - leading: Icon(Icons.link), - title: Text("Copy Link"), - ), - ), - - if (content case MessageContent(:final body?)) - PopupMenuItem( - onTap: () => Clipboard.setData(ClipboardData(text: body)), - child: const ListTile( - leading: Icon(Icons.copy), - title: Text("Copy Text"), - ), - ), - - if (ref.watch( - PowerLevelController.provider( - .redaction(targetUser: sender, roomId: roomId), - ), - ) && - !eventId.isFake) - PopupMenuItem( - onTap: () => showReasonDialog( - title: "Delete Message", - description: - "Are you sure you want to delete this message? " - "This cannot be reversed.", - action: "Delete", - onConfirm: (reason) => - ref.watch(notifier).deleteMessage(this, reason: reason), - ), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Delete", style: .new(color: danger)), - ), - ), - - if (eventId.isFake) - PopupMenuItem( - onTap: () => ref.watch(notifier).hideClientSticky(rowId), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Hide", style: .new(color: danger)), - ), - ), - - PopupMenuItem( - onTap: () => showReasonDialog( - title: "Report", - description: - "Report this this to your server administrators, " - "who can take action like banning this server or room.", - action: "Report", - onConfirm: (reason) => ref - .read(ClientController.provider.notifier) - .reportEvent( - .new( - roomId: roomId, - eventId: eventId, - reason: reason.isEmpty ? null : reason, - ), - ), - ), - child: ListTile( - leading: Icon(Icons.report, color: danger), - title: Text("Report", style: .new(color: danger)), - ), - ), - ]); - } -} diff --git a/lib/helpers/extensions/event_id_is_fake.dart b/lib/helpers/extensions/event_id_is_fake.dart deleted file mode 100644 index 2e660af..0000000 --- a/lib/helpers/extensions/event_id_is_fake.dart +++ /dev/null @@ -1,3 +0,0 @@ -extension EventIdIsFake on String { - bool get isFake => startsWith("~") || startsWith("\$gomuks"); -} diff --git a/lib/helpers/extensions/focus_room.dart b/lib/helpers/extensions/focus_room.dart deleted file mode 100644 index 33111ea..0000000 --- a/lib/helpers/extensions/focus_room.dart +++ /dev/null @@ -1,46 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter_riverpod/experimental/mutation.dart"; -import "package:nexus/controllers/jump_request.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/spaces.dart"; -import "package:nexus/models/space.dart"; - -extension FocusRoom on MutationTarget { - Future focusRoom(String roomId, [int? eventRowId]) async { - final spaces = await container.read(SpacesController.provider.future); - - if (spaces.firstWhereOrNull((space) => space.id == roomId) case Space _?) { - await container - .read(KeyController.provider(KeyController.spaceKey).notifier) - .set(roomId); - return true; - } - - final parent = spaces.firstWhereOrNull( - (space) => - space.children.any((room) => room.metadata?.id == roomId) || - space.subSpaces.any( - (sub) => - sub.room.metadata?.id == roomId || - sub.children.any((room) => room.metadata?.id == roomId), - ), - ); - if (parent == null) return false; - - await container - .read(KeyController.provider(KeyController.spaceKey).notifier) - .set(parent.id); - - if (eventRowId != null) { - container - .read(JumpRequestController.provider.notifier) - .request(roomId, eventRowId); - } - - await container - .read(KeyController.provider(KeyController.roomKey).notifier) - .set(roomId); - - return true; - } -} diff --git a/lib/helpers/extensions/get_all_child_rooms.dart b/lib/helpers/extensions/get_all_child_rooms.dart deleted file mode 100644 index 7a17eec..0000000 --- a/lib/helpers/extensions/get_all_child_rooms.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space.dart"; - -extension GetAllChildRooms on Space { - IList get allChildRooms => - children.addAll(subSpaces.expand((s) => s.children)); -} diff --git a/lib/helpers/extensions/get_filename.dart b/lib/helpers/extensions/get_filename.dart deleted file mode 100644 index c0c20e4..0000000 --- a/lib/helpers/extensions/get_filename.dart +++ /dev/null @@ -1,11 +0,0 @@ -import "package:nexus/models/content/message.dart"; - -extension GetFilename on MessageContent { - String? get filename => switch (this) { - ImageMessageContent(:final filename) || - VideoMessageContent(:final filename) || - AudioMessageContent(:final filename) || - FileMessageContent(:final filename) => filename, - _ => null, - }; -} diff --git a/lib/helpers/extensions/get_headers.dart b/lib/helpers/extensions/get_headers.dart new file mode 100644 index 0000000..0fcec48 --- /dev/null +++ b/lib/helpers/extensions/get_headers.dart @@ -0,0 +1,7 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/header.dart"; + +extension GetHeaders on WidgetRef { + Map get headers => + watch(HeaderController.provider).requireValue; +} diff --git a/lib/helpers/extensions/get_link_info.dart b/lib/helpers/extensions/get_link_info.dart deleted file mode 100644 index 28e999b..0000000 --- a/lib/helpers/extensions/get_link_info.dart +++ /dev/null @@ -1,70 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; - -/// A parsed Matrix link. -/// -/// [identifier] is the room ID, room alias or user ID (e.g. "#room:matrix.org"), -/// [eventId] is set for event permalinks (e.g. "$abc123"), and [via] holds any -/// `via` servers from the link. -typedef LinkInfo = ({String identifier, String? eventId, IList via}); - -extension GetLinkInfo on String { - /// Parses this string as a Matrix link. - /// - /// Supports: - /// - https://matrix.to/#/{id}[/{eventId}][?via=...] - /// - matrix:roomid/{id}[/e/{eventId}][?via=...] - /// - matrix:r/{alias}[/e/{eventId}][?via=...] - /// - matrix:u/{userId} - /// - /// Returns null if this is not a Matrix link. - LinkInfo? get linkInfo { - final uri = Uri.tryParse(trim()); - if (uri == null) return null; - - if ((uri.scheme == "http" || uri.scheme == "https") && - uri.host == "matrix.to") { - // matrix.to keeps everything, including the query, in the fragment, - // so parse the fragment as its own URI. - final inner = Uri.tryParse(uri.fragment); - final segments = inner?.pathSegments.where((s) => s.isNotEmpty).toList(); - if (inner == null || segments == null || segments.isEmpty) return null; - - return ( - identifier: segments.first, - eventId: segments.length > 1 && segments[1].startsWith(r"$") - ? segments[1] - : null, - via: .new(inner.queryParametersAll["via"] ?? []), - ); - } - - if (uri.scheme == "matrix") { - final segments = uri.pathSegments; - if (segments.length < 2 || segments[1].isEmpty) return null; - - final sigil = switch (segments[0].toLowerCase()) { - "r" => "#", - "roomid" => "!", - "u" => "@", - _ => null, - }; - if (sigil == null) return null; - - // Event segment is "e/{eventId}" (without the "$" sigil) and is - // only valid on room links. - final hasEvent = - sigil != "@" && - segments.length >= 4 && - segments[2].toLowerCase() == "e" && - segments[3].isNotEmpty; - - return ( - identifier: "$sigil${segments[1]}", - eventId: hasEvent ? "\$${segments[3]}" : null, - via: .new(uri.queryParametersAll["via"] ?? []), - ); - } - - return null; - } -} diff --git a/lib/helpers/extensions/get_xcode_sdk.dart b/lib/helpers/extensions/get_xcode_sdk.dart index 58920c8..4c09246 100644 --- a/lib/helpers/extensions/get_xcode_sdk.dart +++ b/lib/helpers/extensions/get_xcode_sdk.dart @@ -1,14 +1,11 @@ import "dart:io"; -Future getXCodeTool({String? sdkType, String? findTool}) async { - final result = await Process.run("xcrun", [ - if (sdkType != null) ...["--sdk", sdkType], - if (findTool != null) ...["-f", findTool] else "--show-sdk-path", - ]); +Future getXCodeSDK() async { + final result = await Process.run("xcrun", ["--show-sdk-path"]); if (result.exitCode != 0) { - throw Exception("Failed to get ${sdkType ?? "XCode"} ${findTool ?? "SDK"}"); + throw Exception("Failed to get XCode SDK\n${result.stderr}"); } - return result.stdout.toString().trim(); + return result.stdout.trim(); } diff --git a/lib/helpers/extensions/gomuks_buffer.dart b/lib/helpers/extensions/gomuks_buffer.dart index 10a273a..cc16b46 100644 --- a/lib/helpers/extensions/gomuks_buffer.dart +++ b/lib/helpers/extensions/gomuks_buffer.dart @@ -1,21 +1,20 @@ import "dart:convert"; import "dart:ffi"; import "dart:typed_data"; - import "package:ffi/ffi.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; extension GomuksOwnedBufferToX on GomuksOwnedBuffer { - Uint8List toBytesAndFree() { + Uint8List toBytes() { try { if (base == nullptr || length <= 0) return .new(0); return .fromList(base.asTypedList(length)); } finally { - GomuksFreeBuffer(this); + calloc.free(base); } } - dynamic toJsonAndFree() => jsonDecode(utf8.decode(toBytesAndFree())); + dynamic toJson() => jsonDecode(utf8.decode(toBytes())); } extension JsonToGomuksBuffer on Map { diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart new file mode 100644 index 0000000..f4868d3 --- /dev/null +++ b/lib/helpers/extensions/link_to_mention.dart @@ -0,0 +1,45 @@ +extension LinkToMention on String { + /// Extracts a Matrix identifier from this string. + /// + /// Supports: + /// - https://matrix.to/#/... + /// - matrix:roomid/... + /// - matrix:r/... + /// - matrix:u/... + /// + /// Returns the decoded identifier (e.g. "#room:matrix.org") + /// or null if this is not a Matrix link. + String? get mention { + final trimmed = trim(); + + final matrixTo = RegExp( + r"^https?://matrix\.to/#/(.[^/?#]+)", + caseSensitive: false, + ); + + final matrixToMatch = matrixTo.firstMatch(trimmed); + if (matrixToMatch != null) { + return Uri.decodeComponent(matrixToMatch.group(1)!); + } + + if (trimmed.toLowerCase().startsWith("matrix:")) { + try { + final uri = Uri.parse(trimmed); + + if (uri.pathSegments.isNotEmpty) { + final identifier = uri.pathSegments.last; + if (identifier.isNotEmpty) { + return "${switch (uri.pathSegments.firstOrNull) { + "r" => "#", + "roomid" => "!", + "u" => "@", + _ => "", + }}${Uri.decodeComponent(identifier)}"; + } + } + } catch (_) {} + } + + return null; + } +} diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index ac50a09..bf48535 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -1,10 +1,9 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; extension SchemeToTheme on ColorScheme { ThemeData get theme { final textTheme = ThemeData( - fontFamily: "bundled-sans", - fontFamilyFallback: ["bundled-emoji"], + fontFamilyFallback: ["sans", "emoji", "fallback-sans", "fallback-emoji"], brightness: brightness, ).textTheme; return .from(colorScheme: this).copyWith( @@ -28,7 +27,6 @@ extension SchemeToTheme on ColorScheme { borderRadius: .circular(8), ), ), - iconButtonTheme: .new(variant: StyleVariant.material3Expressive), textTheme: textTheme, inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), diff --git a/lib/helpers/extensions/show_about_dialog.dart b/lib/helpers/extensions/show_about_dialog.dart index 4729e99..2f24a3e 100644 --- a/lib/helpers/extensions/show_about_dialog.dart +++ b/lib/helpers/extensions/show_about_dialog.dart @@ -1,11 +1,11 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_card_list/m3e_card_list.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:package_info_plus/package_info_plus.dart"; -extension ShowAboutDialog on BuildContext { +extension ShowContextMenu on BuildContext { Future showAboutDialog(WidgetRef ref) async { final packageInfo = await PackageInfo.fromPlatform(); @@ -20,7 +20,7 @@ extension ShowAboutDialog on BuildContext { Row( spacing: 12, children: [ - SvgPicture.asset("assets/bundled/icon.svg", width: 64), + SvgPicture.asset("assets/icon.svg", width: 64), Expanded( child: Column( crossAxisAlignment: .start, @@ -33,9 +33,7 @@ extension ShowAboutDialog on BuildContext { "Nexus", style: Theme.of(context).textTheme.headlineMedium, ), - Text( - "(${packageInfo.version}+${const String.fromEnvironment("GIT_COMMIT", defaultValue: "dev")})", - ), + Text("(${packageInfo.version})"), ], ), @@ -52,8 +50,8 @@ extension ShowAboutDialog on BuildContext { M3ECardColumn( onTap: (index) => ref.watch(LaunchHelper.provider).launchUrl(switch (index) { - 0 => .https("git.federated.nexus", "nexus/nexus"), - _ => .https("liberapay.com", "QuadRadical"), + 0 => Uri.https("git.federated.nexus", "nexus/nexus"), + _ => Uri.https("liberapay.com", "QuadRadical"), }), children: [ ListTile( diff --git a/lib/helpers/extensions/show_context_menu.dart b/lib/helpers/extensions/show_context_menu.dart index ccef3b5..c860115 100644 --- a/lib/helpers/extensions/show_context_menu.dart +++ b/lib/helpers/extensions/show_context_menu.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; extension ShowContextMenu on BuildContext { void showContextMenu({ @@ -9,7 +9,7 @@ extension ShowContextMenu on BuildContext { showMenu( context: this, - constraints: .loose(.infinite), + constraints: .loose(Size.infinite), position: .fromLTRB( globalPosition.dx, globalPosition.dy, diff --git a/lib/helpers/extensions/show_user_popover.dart b/lib/helpers/extensions/show_user_popover.dart index f42cd57..1ea3015 100644 --- a/lib/helpers/extensions/show_user_popover.dart +++ b/lib/helpers/extensions/show_user_popover.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/widgets/user_bottom_sheet.dart"; diff --git a/lib/helpers/extensions/string_to_color.dart b/lib/helpers/extensions/string_to_color.dart index a21ca82..eaa7714 100644 --- a/lib/helpers/extensions/string_to_color.dart +++ b/lib/helpers/extensions/string_to_color.dart @@ -1,5 +1,5 @@ import "package:color_hash/color_hash.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; extension ToColor on String { Color get colorHash => ColorHash(this, lightness: .5, saturation: .7).color; diff --git a/lib/helpers/font_licenses.dart b/lib/helpers/font_licenses.dart index 4bd65d9..41567c2 100644 --- a/lib/helpers/font_licenses.dart +++ b/lib/helpers/font_licenses.dart @@ -1,6 +1,11 @@ import "package:flutter/foundation.dart"; -const sil = """This Font Software is licensed under the SIL Open Font License, Version 1.1. +const fontLicenses = [ + LicenseEntryWithLineBreaks( + ["Noto Color Emoji"], + """Copyright 2021 Google Inc. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: https://openfontlicense.org @@ -90,426 +95,102 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE."""; - -const List fontLicenses = [ - .new( - ["Twitter Color Emoji"], - """Twitter Color Emoji is built from Twemoji (https://github.com/jdecked/twemoji) -using the Noto Emoji as fallback. -Its parts are under different licenses: - -1) Emoji artwork: Copyright Twitter, Inc and other contributors. -Attribution 4.0 International - -======================================================================= - -Creative Commons Corporation ("Creative Commons") is not a law firm and -does not provide legal services or legal advice. Distribution of -Creative Commons public licenses does not create a lawyer-client or -other relationship. Creative Commons makes its licenses and related -information available on an "as-is" basis. Creative Commons gives no -warranties regarding its licenses, any material licensed under their -terms and conditions, or any related information. Creative Commons -disclaims all liability for damages resulting from their use to the -fullest extent possible. - -Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and -conditions that creators and other rights holders may use to share -original works of authorship and other material subject to copyright -and certain other rights specified in the public license below. The -following considerations are for informational purposes only, are not -exhaustive, and do not form part of our licenses. - - Considerations for licensors: Our public licenses are - intended for use by those authorized to give the public - permission to use material in ways otherwise restricted by - copyright and certain other rights. Our licenses are - irrevocable. Licensors should read and understand the terms - and conditions of the license they choose before applying it. - Licensors should also secure all rights necessary before - applying our licenses so that the public can reuse the - material as expected. Licensors should clearly mark any - material not subject to the license. This includes other CC- - licensed material, or material used under an exception or - limitation to copyright. More considerations for licensors: - wiki.creativecommons.org/Considerations_for_licensors - - Considerations for the public: By using one of our public - licenses, a licensor grants the public permission to use the - licensed material under specified terms and conditions. If - the licensor's permission is not necessary for any reason--for - example, because of any applicable exception or limitation to - copyright--then that use is not regulated by the license. Our - licenses grant only permissions under copyright and certain - other rights that a licensor has authority to grant. Use of - the licensed material may still be restricted for other - reasons, including because others have copyright or other - rights in the material. A licensor may make special requests, - such as asking that all changes be marked or described. - Although not required by our licenses, you are encouraged to - respect those requests where reasonable. More_considerations - for the public: - wiki.creativecommons.org/Considerations_for_licensees - -======================================================================= - -Creative Commons Attribution 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree -to be bound by the terms and conditions of this Creative Commons -Attribution 4.0 International Public License ("Public License"). To the -extent this Public License may be interpreted as a contract, You are -granted the Licensed Rights in consideration of Your acceptance of -these terms and conditions, and the Licensor grants You such rights in -consideration of benefits the Licensor receives from making the -Licensed Material available under these terms and conditions. - - -Section 1 -- Definitions. - - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - d. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - e. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - f. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - g. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - h. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - i. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - j. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - k. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - - -Section 2 -- Scope. - - a. License grant. - - 1. Subject to the terms and conditions of this Public License, - the Licensor hereby grants You a worldwide, royalty-free, - non-sublicensable, non-exclusive, irrevocable license to - exercise the Licensed Rights in the Licensed Material to: - - a. reproduce and Share the Licensed Material, in whole or - in part; and - - b. produce, reproduce, and Share Adapted Material. - - 2. Exceptions and Limitations. For the avoidance of doubt, where - Exceptions and Limitations apply to Your use, this Public - License does not apply, and You do not need to comply with - its terms and conditions. - - 3. Term. The term of this Public License is specified in Section - 6(a). - - 4. Media and formats; technical modifications allowed. The - Licensor authorizes You to exercise the Licensed Rights in - all media and formats whether now known or hereafter created, - and to make technical modifications necessary to do so. The - Licensor waives and/or agrees not to assert any right or - authority to forbid You from making technical modifications - necessary to exercise the Licensed Rights, including - technical modifications necessary to circumvent Effective - Technological Measures. For purposes of this Public License, - simply making modifications authorized by this Section 2(a) - (4) never produces Adapted Material. - - 5. Downstream recipients. - - a. Offer from the Licensor -- Licensed Material. Every - recipient of the Licensed Material automatically - receives an offer from the Licensor to exercise the - Licensed Rights under the terms and conditions of this - Public License. - - b. No downstream restrictions. You may not offer or impose - any additional or different terms or conditions on, or - apply any Effective Technological Measures to, the - Licensed Material if doing so restricts exercise of the - Licensed Rights by any recipient of the Licensed - Material. - - 6. No endorsement. Nothing in this Public License constitutes or - may be construed as permission to assert or imply that You - are, or that Your use of the Licensed Material is, connected - with, or sponsored, endorsed, or granted official status by, - the Licensor or others designated to receive attribution as - provided in Section 3(a)(1)(A)(i). - - b. Other rights. - - 1. Moral rights, such as the right of integrity, are not - licensed under this Public License, nor are publicity, - privacy, and/or other similar personality rights; however, to - the extent possible, the Licensor waives and/or agrees not to - assert any such rights held by the Licensor to the limited - extent necessary to allow You to exercise the Licensed - Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this - Public License. - - 3. To the extent possible, the Licensor waives any right to - collect royalties from You for the exercise of the Licensed - Rights, whether directly or through a collecting society - under any voluntary or waivable statutory or compulsory - licensing scheme. In all other cases the Licensor expressly - reserves any right to collect such royalties. - - -Section 3 -- License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the -following conditions. - - a. Attribution. - - 1. If You Share the Licensed Material (including in modified - form), You must: - - a. retain the following if it is supplied by the Licensor - with the Licensed Material: - - i. identification of the creator(s) of the Licensed - Material and any others designated to receive - attribution, in any reasonable manner requested by - the Licensor (including by pseudonym if - designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of - warranties; - - v. a URI or hyperlink to the Licensed Material to the - extent reasonably practicable; - - b. indicate if You modified the Licensed Material and - retain an indication of any previous modifications; and - - c. indicate the Licensed Material is licensed under this - Public License, and include the text of, or the URI or - hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any - reasonable manner based on the medium, means, and context in - which You Share the Licensed Material. For example, it may be - reasonable to satisfy the conditions by providing a URI or - hyperlink to a resource that includes the required - information. - - 3. If requested by the Licensor, You must remove any of the - information required by Section 3(a)(1)(A) to the extent - reasonably practicable. - - 4. If You Share Adapted Material You produce, the Adapter's - License You apply must not prevent recipients of the Adapted - Material from complying with this Public License. - - -Section 4 -- Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that -apply to Your use of the Licensed Material: - - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database; - - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material; and - - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not -replace Your obligations under this Public License where the Licensed -Rights include other Copyright and Similar Rights. - - -Section 5 -- Disclaimer of Warranties and Limitation of Liability. - - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - - -Section 6 -- Term and Termination. - - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. - - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided - it is cured within 30 days of Your discovery of the - violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any - right the Licensor may have to seek remedies for Your violations - of this Public License. - - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. - - -Section 7 -- Other Terms and Conditions. - - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. - - -Section 8 -- Interpretation. - - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. - - -======================================================================= - -Creative Commons is not a party to its public licenses. -Notwithstanding, Creative Commons may elect to apply one of its public -licenses to material it publishes and in those instances will be -considered the "Licensor." Except for the limited purpose of indicating -that material is shared under a Creative Commons public license or as -otherwise permitted by the Creative Commons policies published at -creativecommons.org/policies, Creative Commons does not authorize the -use of the trademark "Creative Commons" or any other trademark or logo -of Creative Commons without its prior written consent including, -without limitation, in connection with any unauthorized modifications -to any of its public licenses or any other arrangements, -understandings, or agreements concerning use of licensed material. For -the avoidance of doubt, this paragraph does not form part of the public -licenses. - -Creative Commons may be contacted at creativecommons.org. - -2) Font files and font tooling (Noto Emoji): SIL Open Font License, Version 1.1. -Copyright 2021 Google Inc. All Rights Reserved -$sil -""", +OTHER DEALINGS IN THE FONT SOFTWARE.""", ), - .new( - ["Inter"], - """Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) -Modified for use in this application by removing selected Unicode character mappings. + LicenseEntryWithLineBreaks( + ["Roboto"], + """Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) -$sil""", - ), - .new( - ["Atkinson Hyperlegible Mono"], - """Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono) +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org -$sil""", + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE.""", ), ]; diff --git a/lib/helpers/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart deleted file mode 100644 index 7845576..0000000 --- a/lib/helpers/hooks/chat_scroll.dart +++ /dev/null @@ -1,207 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/models/direction.dart"; -import "package:nexus/models/room_chat.dart"; - -typedef JumpToEvent = Future Function(int rowId); - -final class ChatScroll({ - required final IList historyRows, - required final IList liveRows, - required final GlobalKey centerKey, - required final ScrollController scrollController, - required final bool atBottom, - required final JumpToEvent jumpToEvent, - required final Future Function() jumpToBottom, - required final GlobalKey Function(int eventRowId) keyFor, -}) { - factory use({ - required AsyncValue controllerData, - required Future Function(Direction direction) paginate, - required Future Function() markRead, - required ValueNotifier contextualEvent, - }) { - final anchorId = useState(null); - final atBottom = useState(true); - final pendingJump = useState(null); - final scrollController = useScrollController(); - final centerKey = useMemoized(GlobalKey.new); - - final itemKeys = useMemoized(() => {}, []); - GlobalKey keyFor(int eventRowId) => - itemKeys.putIfAbsent(eventRowId, GlobalKey.new); - - useEffect(() { - if (anchorId.value == null) { - if (controllerData case AsyncData(:final value?) - when value.timeline.isNotEmpty) { - anchorId.value = value.timeline.contains(contextualEvent.value) - ? contextualEvent.value - : value.timeline.last; - } - } - - return null; - }, [controllerData, contextualEvent.value]); - - useEffect(() { - final rowId = pendingJump.value; - if (rowId == null || anchorId.value != rowId) return null; - pendingJump.value = null; - - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!scrollController.hasClients) return; - - final position = scrollController.position; - scrollController.jumpTo( - (-position.viewportDimension) - .clamp(position.minScrollExtent, position.maxScrollExtent) - .toDouble(), - ); - await WidgetsBinding.instance.endOfFrame; - - final context = keyFor(rowId).currentContext; - if (context?.mounted == true) { - await Scrollable.ensureVisible( - context!, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - } - }); - - return null; - }, [anchorId.value, pendingJump.value]); - - final ({IList history, IList live}) split = useMemoized(() { - final items = controllerData.value?.timeline; - final anchor = anchorId.value; - - if (items == null || anchor == null) { - return (history: const .empty(), live: const .empty()); - } - - final anchorIndex = items.indexOf(anchor); - - if (anchorIndex == -1) return (history: const .empty(), live: items); - - return ( - history: items.take(anchorIndex).toIList().reversed.toIList(), - live: items.skip(anchorIndex).toIList(), - ); - }, [controllerData, anchorId.value]); - - final lastRowId = split.live.lastOrNull; - final previousLastRowId = useRef(null); - - useEffect(() { - final previous = previousLastRowId.value; - previousLastRowId.value = lastRowId; - - if (previous == null || lastRowId == null || previous == lastRowId) { - return null; - } - if (!atBottom.value || contextualEvent.value != null) return null; - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!scrollController.hasClients) return; - - scrollController.jumpTo(scrollController.position.minScrollExtent); - }); - - return null; - }, [lastRowId]); - - useEffect( - () { - const loadThreshold = 500.0; - - Future checkPosition() async { - if (!scrollController.hasClients) return; - - final position = scrollController.position; - - final isAtBottom = position.extentBefore <= 0; - if (isAtBottom != atBottom.value) atBottom.value = isAtBottom; - - if (position.extentAfter <= loadThreshold) { - await paginate(.backward); - } else if (contextualEvent.value != null && - position.extentBefore <= loadThreshold) { - await paginate(.forward); - } else if (position.extentBefore <= 0) { - await markRead(); - } - - if (controllerData.isLoading == false && - position.extentBefore <= 0 && - controllerData.value?.hasMoreForward == false && - contextualEvent.value != null) { - anchorId.value = null; - contextualEvent.value = null; - } - } - - scrollController.addListener(checkPosition); - WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition()); - - return () => scrollController.removeListener(checkPosition); - }, - [ - scrollController, - controllerData, - paginate, - markRead, - contextualEvent.value, - ], - ); - - return .new( - historyRows: split.history, - liveRows: split.live, - centerKey: centerKey, - scrollController: scrollController, - atBottom: atBottom.value, - keyFor: keyFor, - jumpToEvent: (int rowId) async { - if (!scrollController.hasClients) return; - - final context = keyFor(rowId).currentContext; - if (context != null) { - await Scrollable.ensureVisible( - context, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - return; - } - - pendingJump.value = rowId; - if (controllerData.value?.timeline.contains(rowId) ?? false) { - anchorId.value = rowId; - } else { - anchorId.value = null; - contextualEvent.value = rowId; - } - }, - jumpToBottom: () async { - if (contextualEvent.value != null) { - anchorId.value = null; - contextualEvent.value = null; - } - - if (!scrollController.hasClients) return; - - await scrollController.animateTo( - scrollController.position.minScrollExtent, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - }, - ); - } -} diff --git a/lib/helpers/launch_helper.dart b/lib/helpers/launch_helper.dart index 89c7015..575395f 100644 --- a/lib/helpers/launch_helper.dart +++ b/lib/helpers/launch_helper.dart @@ -2,7 +2,10 @@ import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:url_launcher/url_launcher.dart" as ul; -class LaunchHelper(Ref ref) { +class LaunchHelper { + final Ref ref; + LaunchHelper(this.ref); + Future launchUrl(Uri url, {bool useWebview = false}) async { try { return await ul.launchUrl( diff --git a/lib/helpers/mxc_image.dart b/lib/helpers/mxc_image.dart index 83a345a..3adb10e 100644 --- a/lib/helpers/mxc_image.dart +++ b/lib/helpers/mxc_image.dart @@ -1,12 +1,14 @@ import "dart:ui"; - import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/requests/download_media.dart"; -class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) - extends ImageProvider { +class MxcImage extends ImageProvider { + final WidgetRef ref; + final DownloadMediaRequest request; + const MxcImage(this.ref, this.request); + @override Future obtainKey(ImageConfiguration configuration) => Future.value(this); diff --git a/lib/main.dart b/lib/main.dart index 3501d5c..f50d22d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,27 @@ import "dart:io"; - import "package:dynamic_color/dynamic_color.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/foundation.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:material_emoji_picker/material_emoji_picker.dart"; import "package:media_kit/media_kit.dart"; +import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/gomuks_listener.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/member_list_opened.dart"; +import "package:nexus/controllers/header.dart"; import "package:nexus/controllers/multi_provider.dart"; -import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/settings.dart"; -import "package:nexus/controllers/unified_push.dart"; +import "package:nexus/controllers/shared_prefs.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/font_licenses.dart"; -import "package:nexus/widgets/pages/chat.dart"; -import "package:nexus/widgets/pages/select_server.dart"; -import "package:nexus/widgets/pages/settings.dart"; -import "package:nexus/widgets/pages/verify.dart"; -import "package:nexus/widgets/appbar.dart"; +import "package:nexus/pages/chat.dart"; +import "package:nexus/pages/select_server.dart"; +import "package:nexus/pages/verify.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; final GlobalKey navigatorKey = GlobalKey(); -late final bool isInBackground; final class Logger extends ProviderObserver { @override @@ -43,7 +38,14 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue} } void showError(Object error, [StackTrace? stackTrace]) { - if (error.toString().contains("'_nextFrame != null': is not true.")) { + if (error.toString().contains("DioException") || + error.toString().contains( + "setState() or markNeedsBuild() called during build.", + ) || + error.toString().contains("Invalid source") || + error.toString().contains("UTF-16") || + error.toString().contains("HTTP request failed") || + error.toString().contains("Invalid image data")) { return; } @@ -60,59 +62,49 @@ void showError(Object error, [StackTrace? stackTrace]) { } } -void main(List args) async { +void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); - LicenseRegistry.addLicense(() => .fromIterable(fontLicenses)); + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + await windowManager.ensureInitialized(); + await windowManager.waitUntilReadyToShow( + WindowOptions( + titleBarStyle: TitleBarStyle.hidden, + windowButtonVisibility: false, + ), + ); + await windowManager.setMinimumSize(Size.square(500)); + } + + LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses)); FlutterError.onError = (FlutterErrorDetails details) => showError(details.exception.toString(), details.stack); - isInBackground = - Platform.environment["FLUTTER_HEADLESS"] != null || - args.contains("--unifiedpush-bg"); - - if (isInBackground) { - await ProviderContainer() - .read(UnifiedPushController.provider.future) - .timeout(Duration(seconds: 5)); - - await Future.delayed(Duration(seconds: 10)); - exit(0); - } else { - if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { - await windowManager.ensureInitialized(); - await windowManager.waitUntilReadyToShow( - WindowOptions( - titleBarStyle: TitleBarStyle.hidden, - windowButtonVisibility: false, - ), - ); - await windowManager.setMinimumSize(Size.square(500)); - } - - runApp( - ProviderScope( - retry: (_, _) => null, - observers: [ - // Change false to true if you want debug information on provider reloads - // ignore: dead_code - if (false && kDebugMode) Logger(), - ], - child: App(), - ), - ); - } + runApp( + ProviderScope( + retry: (_, _) => null, + observers: [ + // Change false to true if you want debug information on provider reloads + // ignore: dead_code + if (false && kDebugMode) Logger(), + ], + child: const App(), + ), + ); } -class const App({super.key}) extends StatelessWidget { +class App extends StatelessWidget { + const App({super.key}); + @override Widget build(BuildContext context) => DynamicColorBuilder( builder: (lightDynamic, darkDynamic) => Consumer( builder: (context, ref, child) => MaterialApp( navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, + // Use indigo to work around bugs in theme generation theme: (ref .watch(SettingsController.provider) @@ -121,7 +113,7 @@ class const App({super.key}) extends StatelessWidget { data: (settings) => settings.useDynamicTheming ? lightDynamic : null, ) ?? - ThemeData.light().colorScheme) + ColorScheme.fromSeed(seedColor: Colors.indigo)) .theme, darkTheme: (ref @@ -131,7 +123,10 @@ class const App({super.key}) extends StatelessWidget { data: (settings) => settings.useDynamicTheming ? darkDynamic : null, ) ?? - ThemeData.dark().colorScheme) + ColorScheme.fromSeed( + seedColor: Colors.indigo, + brightness: Brightness.dark, + )) .theme, themeMode: ref .watch(SettingsController.provider) @@ -141,61 +136,39 @@ class const App({super.key}) extends StatelessWidget { ), home: child, ), - child: DefaultTextHeightBehavior( - textHeightBehavior: .new(leadingDistribution: .even), - child: Scaffold( - body: Consumer( - builder: (context, ref, _) => switch (ref.watch( - MultiProviderController.provider( - .new([ - GomuksListenerController.provider, - NotificationController.provider, - UnifiedPushController.provider, - MemberListOpenedController.provider, - KeyController.provider(KeyController.roomKey), - KeyController.provider(KeyController.spaceKey), - EmojiController.provider, - ]), - ), - )) { - AsyncData(value: _) || AsyncLoading(value: _?) => Consumer( - builder: (_, ref, _) { - final clientState = ref.watch(ClientStateController.provider); - - if (clientState == null || !clientState.isInitialized) { - return Loading(); - } - - if (!clientState.isLoggedIn) { - return SelectServerPage(); - } else if (!clientState.isVerified) { - return VerifyPage(); - } else { - return ChatPage(); - } - }, - ), - - AsyncLoading _ => Scaffold( - appBar: Appbar( - actions: .new([ - IconButton( - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: Icon(Icons.settings), - ), + child: Scaffold( + body: Consumer( + builder: (_, ref, _) => ref + .watch( + MultiProviderController.provider( + IListConst([ + SharedPrefsController.provider, + ClientController.provider, + HeaderController.provider, ]), ), - body: Loading(), + ) + .betterWhen( + data: (_) => Consumer( + builder: (_, ref, _) { + final clientState = ref.watch( + ClientStateController.provider, + ); + + if (clientState == null || !clientState.isInitialized) { + return Loading(); + } + + if (!clientState.isLoggedIn) { + return SelectServerPage(); + } else if (!clientState.isVerified) { + return VerifyPage(); + } else { + return ChatPage(); + } + }, + ), ), - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, - ), ), ), ), diff --git a/lib/models/account_data.dart b/lib/models/account_data.dart index 955d8d1..a325ffe 100644 --- a/lib/models/account_data.dart +++ b/lib/models/account_data.dart @@ -1,69 +1,16 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "account_data.freezed.dart"; part "account_data.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const AccountData({ - @JsonKey(name: AccountData.invitePermissionConfigKey) - final InvitePermissionConfig invitePermissionConfig = - const InvitePermissionConfig(), - - @JsonKey(name: AccountData.directKey) - final IMap> directMessages = const IMap.empty(), - - @JsonKey(name: AccountData.recentEmojiKey) - final RecentEmojiData recentEmoji = const RecentEmojiData(), -}) with _$AccountData { - static const invitePermissionConfigKey = "m.invite_permission_config"; - static const directKey = "m.direct"; - static const recentEmojiKey = "m.recent_emoji"; - - Map toJson() => _$AccountDataToJson(this); +@freezed +abstract class AccountData with _$AccountData { + const factory AccountData({ + required String userId, + required String? roomId, + required String type, + required dynamic content, + }) = _AccountData; factory AccountData.fromJson(Map json) => _$AccountDataFromJson(json); } - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RecentEmojiData({ - final IList recentEmoji = const IList.empty(), -}) with _$RecentEmojiData { - Map toJson() => _$RecentEmojiDataToJson(this); - - factory RecentEmojiData.fromJson(Map json) => - _$RecentEmojiDataFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const InvitePermissionConfig({ - @JsonKey(unknownEnumValue: DefaultInviteAction.allow) - final DefaultInviteAction defaultAction = DefaultInviteAction.allow, -}) with _$InvitePermissionConfig { - Map toJson() => _$InvitePermissionConfigToJson(this); - - factory InvitePermissionConfig.fromJson(Map json) => - _$InvitePermissionConfigFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RecentEmoji({required final String emoji, required final int total}) - with _$RecentEmoji { - Map toJson() => _$RecentEmojiToJson(this); - - factory RecentEmoji.fromJson(Map json) => - _$RecentEmojiFromJson(json); -} - -@JsonEnum(fieldRename: .snake) -enum DefaultInviteAction { - allow, - deny, - @JsonValue("uk.timedout.msc4494.deny_public") - denyPublic, -} diff --git a/lib/models/capabilities.dart b/lib/models/capabilities.dart deleted file mode 100644 index 6830170..0000000 --- a/lib/models/capabilities.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "capabilities.freezed.dart"; -part "capabilities.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Capabilities({ - @JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush, -}) with _$Capabilities { - Map toJson() => _$CapabilitiesToJson(this); - - factory Capabilities.fromJson(Map json) => - _$CapabilitiesFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const WebPush({required final bool enabled, final String? vapid}) - with _$WebPush { - Map toJson() => _$WebPushToJson(this); - - factory WebPush.fromJson(Map json) => - _$WebPushFromJson(json); -} diff --git a/lib/models/client_state.dart b/lib/models/client_state.dart index 262203d..1e15136 100644 --- a/lib/models/client_state.dart +++ b/lib/models/client_state.dart @@ -1,19 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "client_state.freezed.dart"; part "client_state.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ClientState({ - required final bool isInitialized, - required final bool isLoggedIn, - required final bool isVerified, - required final String? userId, - required final String? deviceId, - required final String? homeserverUrl, -}) with _$ClientState { - Map toJson() => _$ClientStateToJson(this); +@freezed +abstract class ClientState with _$ClientState { + const factory ClientState({ + required bool isInitialized, + required bool isLoggedIn, + required bool isVerified, + required String? userId, + required String? homeserverUrl, + }) = _ClientState; factory ClientState.fromJson(Map json) => _$ClientStateFromJson(json); diff --git a/lib/models/configs/members_by_status.dart b/lib/models/configs/members_by_status.dart index 6b4aec4..29fc471 100644 --- a/lib/models/configs/members_by_status.dart +++ b/lib/models/configs/members_by_status.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/membership_status.dart"; - part "members_by_status.freezed.dart"; part "members_by_status.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const MembersByStatusConfig({ - required final String roomId, - required final MembershipStatus status, -}) with _$MembersByStatusConfig { - Map toJson() => _$MembersByStatusConfigToJson(this); +@freezed +abstract class MembersByStatusConfig with _$MembersByStatusConfig { + const factory MembersByStatusConfig({ + required String roomId, + required MembershipStatus status, + }) = _MembersByStatusConfig; factory MembersByStatusConfig.fromJson(Map json) => _$MembersByStatusConfigFromJson(json); diff --git a/lib/models/configs/power_level.dart b/lib/models/configs/power_level.dart index 5722db7..ed9c5e8 100644 --- a/lib/models/configs/power_level.dart +++ b/lib/models/configs/power_level.dart @@ -1,7 +1,6 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/membership_action.dart"; - part "power_level.freezed.dart"; @freezed diff --git a/lib/models/configs/reactions.dart b/lib/models/configs/reactions.dart index b9de793..787b28c 100644 --- a/lib/models/configs/reactions.dart +++ b/lib/models/configs/reactions.dart @@ -1,15 +1,13 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "reactions.freezed.dart"; part "reactions.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ReactionsConfig({ - required final String roomId, - required final int eventRowId, -}) with _$ReactionsConfig { - Map toJson() => _$ReactionsConfigToJson(this); +@freezed +abstract class ReactionsConfig with _$ReactionsConfig { + const factory ReactionsConfig({ + required String roomId, + required int eventRowId, + }) = _ReactionsConfig; factory ReactionsConfig.fromJson(Map json) => _$ReactionsConfigFromJson(json); diff --git a/lib/models/configs/user.dart b/lib/models/configs/user.dart index bbbc83d..0331597 100644 --- a/lib/models/configs/user.dart +++ b/lib/models/configs/user.dart @@ -1,13 +1,11 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "user.freezed.dart"; part "user.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const UserConfig({final String? roomId, required final String userId}) - with _$UserConfig { - Map toJson() => _$UserConfigToJson(this); +@freezed +abstract class UserConfig with _$UserConfig { + const factory UserConfig({required String? roomId, required String userId}) = + _UserConfig; factory UserConfig.fromJson(Map json) => _$UserConfigFromJson(json); diff --git a/lib/models/content/avatar.dart b/lib/models/content/avatar.dart index 25167e9..66d4c47 100644 --- a/lib/models/content/avatar.dart +++ b/lib/models/content/avatar.dart @@ -5,7 +5,7 @@ part "avatar.freezed.dart"; part "avatar.g.dart"; @freezed -sealed class AvatarContent extends Content with _$AvatarContent { +abstract class AvatarContent extends Content with _$AvatarContent { AvatarContent._(); factory AvatarContent({ImageInfo? info, Uri? url}) = _AvatarContent; diff --git a/lib/models/content/canonical_alias.dart b/lib/models/content/canonical_alias.dart index 1a35d92..636be13 100644 --- a/lib/models/content/canonical_alias.dart +++ b/lib/models/content/canonical_alias.dart @@ -5,7 +5,7 @@ part "canonical_alias.freezed.dart"; part "canonical_alias.g.dart"; @freezed -sealed class CanonicalAliasContent extends Content +abstract class CanonicalAliasContent extends Content with _$CanonicalAliasContent { CanonicalAliasContent._(); factory CanonicalAliasContent({ diff --git a/lib/models/content/content.dart b/lib/models/content/content.dart index e28ccbf..e7b1141 100644 --- a/lib/models/content/content.dart +++ b/lib/models/content/content.dart @@ -19,11 +19,9 @@ import "package:nexus/models/content/history_visibility.dart"; class Content { final Error? parseError; + Content({this.parseError}); - const Content({this.parseError}); - - factory Content.fromJson(Map json) => const Content(); - + factory Content.fromJson(Map json) => Content(); Map toJson() => {}; static Map readValue(Map json, _) => @@ -36,16 +34,13 @@ class Content { ?.contentFromJson ?? Content.fromJson)(json); } catch (error) { - if (error is Error) return Content(parseError: error); + if (error is Error) return .new(parseError: error); rethrow; } } } -enum EventType( - final String type, - final Content Function(Map json) contentFromJson, -) { +enum EventType { encrypted("m.room.encrypted", EncryptedContent.fromJson), redaction("m.room.redaction", RedactionContent.fromJson), encryption("m.room.encryption", EncryptionContent.fromJson), @@ -66,4 +61,8 @@ enum EventType( reaction("m.reaction", ReactionContent.fromJson), pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson), message("m.room.message", MessageContent.fromJson); + + final String type; + final Content Function(Map json) contentFromJson; + const EventType(this.type, this.contentFromJson); } diff --git a/lib/models/content/create.dart b/lib/models/content/create.dart index b937a94..c534558 100644 --- a/lib/models/content/create.dart +++ b/lib/models/content/create.dart @@ -5,7 +5,7 @@ part "create.freezed.dart"; part "create.g.dart"; @freezed -sealed class CreateContent extends Content with _$CreateContent { +abstract class CreateContent extends Content with _$CreateContent { CreateContent._(); factory CreateContent({ @JsonKey(name: "additional_creators") @@ -30,10 +30,9 @@ enum RoomType { space, } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const PreviousRoom({required final String roomId}) with _$PreviousRoom { - Map toJson() => _$PreviousRoomToJson(this); +@freezed +abstract class PreviousRoom with _$PreviousRoom { + const factory PreviousRoom({required String roomId}) = _PreviousRoom; factory PreviousRoom.fromJson(Map json) => _$PreviousRoomFromJson(json); diff --git a/lib/models/content/encrypted.dart b/lib/models/content/encrypted.dart index 8658a71..b33a440 100644 --- a/lib/models/content/encrypted.dart +++ b/lib/models/content/encrypted.dart @@ -4,7 +4,7 @@ part "encrypted.freezed.dart"; part "encrypted.g.dart"; @freezed -sealed class EncryptedContent extends Content with _$EncryptedContent { +abstract class EncryptedContent extends Content with _$EncryptedContent { EncryptedContent._(); factory EncryptedContent() = _EncryptedContent; diff --git a/lib/models/content/encryption.dart b/lib/models/content/encryption.dart index 52c7b34..3380632 100644 --- a/lib/models/content/encryption.dart +++ b/lib/models/content/encryption.dart @@ -4,7 +4,7 @@ part "encryption.freezed.dart"; part "encryption.g.dart"; @freezed -sealed class EncryptionContent extends Content with _$EncryptionContent { +abstract class EncryptionContent extends Content with _$EncryptionContent { EncryptionContent._(); factory EncryptionContent({ required String algorithm, diff --git a/lib/models/content/history_visibility.dart b/lib/models/content/history_visibility.dart index 3cfc011..707805c 100644 --- a/lib/models/content/history_visibility.dart +++ b/lib/models/content/history_visibility.dart @@ -4,7 +4,7 @@ part "history_visibility.freezed.dart"; part "history_visibility.g.dart"; @freezed -sealed class HistoryVisibilityContent extends Content +abstract class HistoryVisibilityContent extends Content with _$HistoryVisibilityContent { HistoryVisibilityContent._(); factory HistoryVisibilityContent({ diff --git a/lib/models/content/join_rules.dart b/lib/models/content/join_rules.dart index 3f1c17b..1d14eee 100644 --- a/lib/models/content/join_rules.dart +++ b/lib/models/content/join_rules.dart @@ -6,7 +6,7 @@ part "join_rules.freezed.dart"; part "join_rules.g.dart"; @freezed -sealed class JoinRulesContent extends Content with _$JoinRulesContent { +abstract class JoinRulesContent extends Content with _$JoinRulesContent { JoinRulesContent._(); factory JoinRulesContent({ required JoinRule joinRule, @@ -17,13 +17,12 @@ sealed class JoinRulesContent extends Content with _$JoinRulesContent { _$JoinRulesContentFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const AllowCondition({ - final String? roomId, - required final AllowConditionType type, -}) with _$AllowCondition { - Map toJson() => _$AllowConditionToJson(this); +@freezed +abstract class AllowCondition with _$AllowCondition { + const factory AllowCondition({ + String? roomId, + required AllowConditionType type, + }) = _AllowCondition; factory AllowCondition.fromJson(Map json) => _$AllowConditionFromJson(json); diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart index 46be974..dbbd123 100644 --- a/lib/models/content/membership.dart +++ b/lib/models/content/membership.dart @@ -5,7 +5,7 @@ part "membership.freezed.dart"; part "membership.g.dart"; @freezed -sealed class MembershipContent extends Content with _$MembershipContent { +abstract class MembershipContent extends Content with _$MembershipContent { MembershipContent._(); static String? displaynameFromJson(String? displayName) => diff --git a/lib/models/content/message.dart b/lib/models/content/message.dart index 36bd725..e7e8923 100644 --- a/lib/models/content/message.dart +++ b/lib/models/content/message.dart @@ -10,7 +10,7 @@ part "message.g.dart"; typedef EncryptedFile = Map; @Freezed(unionKey: "msgtype", fallbackUnion: "default") -sealed class MessageContent extends Content with _$MessageContent { +abstract class MessageContent extends Content with _$MessageContent { MessageContent._(); static String? mediaUrlFromJson(Map json, String key) => json[key] ?? json["file"]?[key]; diff --git a/lib/models/content/name.dart b/lib/models/content/name.dart index c451d64..205f6bb 100644 --- a/lib/models/content/name.dart +++ b/lib/models/content/name.dart @@ -4,7 +4,7 @@ part "name.freezed.dart"; part "name.g.dart"; @freezed -sealed class NameContent extends Content with _$NameContent { +abstract class NameContent extends Content with _$NameContent { NameContent._(); factory NameContent({required String name}) = _NameContent; diff --git a/lib/models/content/pinned_events.dart b/lib/models/content/pinned_events.dart index 9dac5de..8aea838 100644 --- a/lib/models/content/pinned_events.dart +++ b/lib/models/content/pinned_events.dart @@ -5,7 +5,7 @@ part "pinned_events.freezed.dart"; part "pinned_events.g.dart"; @freezed -sealed class PinnedEventsContent extends Content with _$PinnedEventsContent { +abstract class PinnedEventsContent extends Content with _$PinnedEventsContent { PinnedEventsContent._(); factory PinnedEventsContent({ @Default(IList.empty()) @JsonKey(name: "pinned") IList pinnedEvents, diff --git a/lib/models/content/power_levels.dart b/lib/models/content/power_levels.dart index 5ae6561..3709c38 100644 --- a/lib/models/content/power_levels.dart +++ b/lib/models/content/power_levels.dart @@ -5,7 +5,7 @@ part "power_levels.freezed.dart"; part "power_levels.g.dart"; @freezed -sealed class PowerLevelsContent extends Content with _$PowerLevelsContent { +abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { PowerLevelsContent._(); factory PowerLevelsContent({ @Default(IMap.empty()) IMap events, @@ -24,13 +24,12 @@ sealed class PowerLevelsContent extends Content with _$PowerLevelsContent { _$PowerLevelsContentFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Notifications({ - final int room = 50, - final IMap other = const IMap.empty(), -}) with _$Notifications { - Map toJson() => _$NotificationsToJson(this); +@freezed +abstract class Notifications with _$Notifications { + const factory Notifications({ + @Default(50) int room, + @Default(IMapConst({})) IMap other, + }) = _Notifications; factory Notifications.fromJson(Map json) => _$NotificationsFromJson(json); diff --git a/lib/models/content/reaction.dart b/lib/models/content/reaction.dart index 93c563d..0f81bc0 100644 --- a/lib/models/content/reaction.dart +++ b/lib/models/content/reaction.dart @@ -4,7 +4,7 @@ part "reaction.freezed.dart"; part "reaction.g.dart"; @Freezed(toJson: false) -sealed class ReactionContent extends Content with _$ReactionContent { +abstract class ReactionContent extends Content with _$ReactionContent { ReactionContent._(); static String? keyJsonFromJson(Map json, String key) => json["m.relates_to"]?["key"]; diff --git a/lib/models/content/redaction.dart b/lib/models/content/redaction.dart index 2ed6436..e9c1a90 100644 --- a/lib/models/content/redaction.dart +++ b/lib/models/content/redaction.dart @@ -4,7 +4,7 @@ part "redaction.freezed.dart"; part "redaction.g.dart"; @freezed -sealed class RedactionContent extends Content with _$RedactionContent { +abstract class RedactionContent extends Content with _$RedactionContent { RedactionContent._(); factory RedactionContent({String? reason, String? redacts}) = _RedactionContent; diff --git a/lib/models/content/server_acl.dart b/lib/models/content/server_acl.dart index 3873bf3..1e50988 100644 --- a/lib/models/content/server_acl.dart +++ b/lib/models/content/server_acl.dart @@ -5,7 +5,7 @@ part "server_acl.freezed.dart"; part "server_acl.g.dart"; @freezed -sealed class ServerACLContent extends Content with _$ServerACLContent { +abstract class ServerACLContent extends Content with _$ServerACLContent { ServerACLContent._(); factory ServerACLContent({ @Default(IList.empty()) IList allow, diff --git a/lib/models/content/sticker.dart b/lib/models/content/sticker.dart index c6fa893..89d9332 100644 --- a/lib/models/content/sticker.dart +++ b/lib/models/content/sticker.dart @@ -5,7 +5,7 @@ part "sticker.freezed.dart"; part "sticker.g.dart"; @freezed -sealed class StickerContent extends Content with _$StickerContent { +abstract class StickerContent extends Content with _$StickerContent { StickerContent._(); factory StickerContent({ required String body, diff --git a/lib/models/content/topic.dart b/lib/models/content/topic.dart index c8256e3..8fa5229 100644 --- a/lib/models/content/topic.dart +++ b/lib/models/content/topic.dart @@ -1,12 +1,11 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; - part "topic.freezed.dart"; part "topic.g.dart"; @freezed -sealed class TopicContent extends Content with _$TopicContent { +abstract class TopicContent extends Content with _$TopicContent { TopicContent._(); factory TopicContent({ required String topic, @@ -17,24 +16,24 @@ sealed class TopicContent extends Content with _$TopicContent { _$TopicContentFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const TopicContentBlock({ - final IList representations = const IList.empty(), -}) with _$TopicContentBlock { - Map toJson() => _$TopicContentBlockToJson(this); +@freezed +abstract class TopicContentBlock with _$TopicContentBlock { + factory TopicContentBlock({ + @Default(IList.empty()) + @JsonKey(name: "m.text") + IList representations, + }) = _TopicContentBlock; factory TopicContentBlock.fromJson(Map json) => _$TopicContentBlockFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const TextualRepresentation({ - required final String body, - final String mimetype = "text/plain", -}) with _$TextualRepresentation { - Map toJson() => _$TextualRepresentationToJson(this); +@freezed +abstract class TextualRepresentation with _$TextualRepresentation { + factory TextualRepresentation({ + required String body, + @Default("text/plain") String mimetype, + }) = _TextualRepresentation; factory TextualRepresentation.fromJson(Map json) => _$TextualRepresentationFromJson(json); diff --git a/lib/models/direction.dart b/lib/models/direction.dart deleted file mode 100644 index 8c4947e..0000000 --- a/lib/models/direction.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -enum Direction { - @JsonValue("f") - forward, - @JsonValue("b") - backward, -} diff --git a/lib/models/emoji.dart b/lib/models/emoji.dart deleted file mode 100644 index 75faf08..0000000 --- a/lib/models/emoji.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:flutter/widgets.dart"; -part "emoji.freezed.dart"; -part "emoji.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable(createToJson: false) -class Emoji({ - @JsonKey( - fromJson: Emoji.widgetFromJson, - readValue: Emoji.readWidgetValueFromJson, - ) - required final Widget widget, - @JsonKey(name: "emoji") required final String value, - required final IList aliases, - required final String description, - required final IList tags, -}) with _$Emoji { - static Widget widgetFromJson(String emoji) => Text(emoji); - - static String readWidgetValueFromJson(Map json, _) => - json["emoji"]; - factory Emoji.fromJson(Map json) => _$EmojiFromJson(json); -} diff --git a/lib/models/emoji_category.dart b/lib/models/emoji_category.dart deleted file mode 100644 index cc39bc3..0000000 --- a/lib/models/emoji_category.dart +++ /dev/null @@ -1,12 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/emoji.dart"; -import "package:flutter/widgets.dart"; -part "emoji_category.freezed.dart"; - -@Freezed(toJson: false, fromJson: false) -class EmojiCategory({ - required final Widget icon, - required final String name, - required final IList emojis, -}) with _$EmojiCategory; diff --git a/lib/models/event.dart b/lib/models/event.dart index de065fe..c54dbc5 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -2,40 +2,12 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/epoch_date_time_converter.dart"; -import "package:nexus/models/profile_response.dart"; - +import "package:nexus/models/profile.dart"; part "event.freezed.dart"; part "event.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Event({ - @JsonKey(name: "rowid") required final int rowId, - @JsonKey(name: "timeline_rowid") required final int timelineRowId, - final String? stateKey, - required final String roomId, - required final String eventId, - required final String sender, - @JsonKey(readValue: Event.typeJsonFromJson) required final String type, - @EpochDateTimeConverter() required final DateTime timestamp, - final IMap unsigned = const IMap.empty(), - final LocalContent? localContent, - final String? transactionId, - final String? redactedBy, - final String? relatesTo, - final String? relationType, - final String? replyTo, - final String? decryptionError, - final String? sendError, - final IMap reactions = const IMap.empty(), - @JsonKey(name: "last_edit_rowid") final int lastEditRowId = 0, - final UnreadType? unreadType, - final Profile? pmp, - required final Content content, - required final Content? previousContent, -}) with _$Event { - Map toJson() => _$EventToJson(this); - +@freezed +abstract class Event with _$Event { static String typeJsonFromJson(Map json, _) => json["decrypted_type"] ?? json["type"]; @@ -45,17 +17,37 @@ class const Event({ return content["m.new_content"] ?? content; } - static String? replyToFromJson(Map json) { - try { - return json["m.relates_to"]?["m.in_reply_to"]?["event_id"]; - } catch (_) { - return null; - } - } + const factory Event({ + @JsonKey(name: "rowid") required int rowId, + @JsonKey(name: "timeline_rowid") required int timelineRowId, + required String roomId, + required String eventId, + required String sender, + @JsonKey(readValue: Event.typeJsonFromJson) required String type, + String? stateKey, + @EpochDateTimeConverter() required DateTime timestamp, + @Default(IMap.empty()) IMap unsigned, + LocalContent? localContent, + String? transactionId, + String? redactedBy, + String? relatesTo, + String? relationType, + String? replyTo, + String? decryptionError, + String? sendError, + @Default(IMap.empty()) IMap reactions, + @JsonKey(name: "last_edit_rowid") @Default(0) int lastEditRowId, + @UnreadTypeConverter() UnreadType? unreadType, + Profile? pmp, + required Content content, + required Content? previousContent, + }) = _Event; factory Event.fromJson(Map json) => _$EventFromJson(json).copyWith( - replyTo: replyToFromJson(getContentFromJson(json)), + replyTo: getContentFromJson( + json, + )["m.relates_to"]?["m.in_reply_to"]?["event_id"], pmp: json["content"]?["com.beeper.per_message_profile"] == null ? null : Profile.fromJsonWithCatch( @@ -74,27 +66,37 @@ class const Event({ ); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const LocalContent({ - final String? sanitizedHtml, - final String? editSource, - final bool? wasPlaintext, - final bool? bigEmoji, - final bool? hasMath, - final bool? replyFallbackRemoved, -}) with _$LocalContent { - Map toJson() => _$LocalContentToJson(this); +@freezed +abstract class LocalContent with _$LocalContent { + const factory LocalContent({ + String? sanitizedHtml, + String? editSource, + bool? wasPlaintext, + bool? bigEmoji, + bool? hasMath, + bool? replyFallbackRemoved, + }) = _LocalContent; - @override factory LocalContent.fromJson(Map json) => _$LocalContentFromJson(json); } -@Freezed(toJson: false, fromJson: false) -class const UnreadType(final int value) with _$UnreadType { - factory UnreadType.fromJson(int json) => UnreadType(json); - int toJson() => value; +class UnreadTypeConverter implements JsonConverter { + const UnreadTypeConverter(); + + @override + UnreadType? fromJson(int? json) => json == null ? null : UnreadType(json); + + @override + int? toJson(UnreadType? object) => object?.value; +} + +// I think this is correct but I'm not sure, its some type of bitmask. +@immutable +class UnreadType { + final int value; + + const UnreadType(this.value); static const none = UnreadType(0); static const normal = UnreadType(1); @@ -102,6 +104,9 @@ class const UnreadType(final int value) with _$UnreadType { static const highlight = UnreadType(4); static const sound = UnreadType(8); - bool has(UnreadType flag) => value & flag.value != 0; - UnreadType operator |(UnreadType other) => UnreadType(value | other.value); + bool get isNone => value == 0; + bool get isNormal => (value & 1) != 0; + bool get shouldNotify => (value & 2) != 0; + bool get isHighlighted => (value & 4) != 0; + bool get playsSound => (value & 8) != 0; } diff --git a/lib/models/event_context.dart b/lib/models/event_context.dart deleted file mode 100644 index 6103c7d..0000000 --- a/lib/models/event_context.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; - -part "event_context.freezed.dart"; -part "event_context.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const EventContext({ - required final Event event, - required final IList before, - required final IList after, - required final String start, - required final String end, - final IList relatedEvents = const IList.empty(), -}) with _$EventContext { - Map toJson() => _$EventContextToJson(this); - - factory EventContext.fromJson(Map json) => - _$EventContextFromJson(json); -} diff --git a/lib/models/gemoji.dart b/lib/models/gemoji.dart new file mode 100644 index 0000000..084a1ca --- /dev/null +++ b/lib/models/gemoji.dart @@ -0,0 +1,17 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +part "gemoji.freezed.dart"; +part "gemoji.g.dart"; + +@freezed +abstract class Gemoji with _$Gemoji { + const factory Gemoji({ + required String emoji, + required String category, + required IList aliases, + required String description, + required IList tags, + }) = _Gemoji; + + factory Gemoji.fromJson(Map json) => _$GemojiFromJson(json); +} diff --git a/lib/models/gomuks_config.dart b/lib/models/gomuks_config.dart deleted file mode 100644 index e6a8ce7..0000000 --- a/lib/models/gomuks_config.dart +++ /dev/null @@ -1,55 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "gomuks_config.freezed.dart"; -part "gomuks_config.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const GomuksConfig({ - final MatrixConfig? matrix, - final PushConfig? push, - final MediaConfig? media, -}) with _$GomuksConfig { - Map toJson() => _$GomuksConfigToJson(this); - - factory GomuksConfig.fromJson(Map json) => - _$GomuksConfigFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const MatrixConfig({ - @JsonKey(name: "disable_http2") final bool disableHttp2 = false, - @JsonKey(name: "set_presence") final String? setPresence, - @JsonKey(name: "initial_device_display_name") - required final String initialDeviceDisplayName, -}) with _$MatrixConfig { - Map toJson() => _$MatrixConfigToJson(this); - - factory MatrixConfig.fromJson(Map json) => - _$MatrixConfigFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const PushConfig({ - @JsonKey(name: "fcm_gateway") required final String fcmGateway, - @JsonKey(name: "vapid_private_key") required final String vapidPrivateKey, - @JsonKey(name: "vapid_public_key") required final String vapidPublicKey, -}) with _$PushConfig { - Map toJson() => _$PushConfigToJson(this); - - factory PushConfig.fromJson(Map json) => - _$PushConfigFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const MediaConfig({ - @JsonKey(name: "thumbnail_size") required final int thumbnailSize, -}) with _$MediaConfig { - Map toJson() => _$MediaConfigToJson(this); - - factory MediaConfig.fromJson(Map json) => - _$MediaConfigFromJson(json); -} diff --git a/lib/models/homeserver.dart b/lib/models/homeserver.dart index 5504941..903e23d 100644 --- a/lib/models/homeserver.dart +++ b/lib/models/homeserver.dart @@ -1,11 +1,12 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "homeserver.freezed.dart"; @freezed -class const Homeserver({ - required final String name, - required final String description, - required final Uri url, - required final String iconUrl, -}) with _$Homeserver; +abstract class Homeserver with _$Homeserver { + const factory Homeserver({ + required String name, + required String description, + required Uri url, + required String iconUrl, + }) = _Homeserver; +} diff --git a/lib/models/info/audio.dart b/lib/models/info/audio.dart index ac90282..ccfcf7a 100644 --- a/lib/models/info/audio.dart +++ b/lib/models/info/audio.dart @@ -1,17 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/ms_duration.dart"; - part "audio.freezed.dart"; part "audio.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const AudioInfo({ - @MSDuration() final Duration? duration, - @JsonKey(name: "mimetype") final String? mimeType, - final int? size, -}) with _$AudioInfo { - Map toJson() => _$AudioInfoToJson(this); +@freezed +abstract class AudioInfo with _$AudioInfo { + /// Information for images, [size] is in bytes. + const factory AudioInfo({ + @MSDuration() Duration? duration, + @JsonKey(name: "mimetype") String? mimeType, + int? size, + }) = _AudioInfo; factory AudioInfo.fromJson(Map json) => _$AudioInfoFromJson(json); diff --git a/lib/models/info/file.dart b/lib/models/info/file.dart index ce0aa84..1509c99 100644 --- a/lib/models/info/file.dart +++ b/lib/models/info/file.dart @@ -1,15 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "file.freezed.dart"; part "file.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const FileInfo({ - @JsonKey(name: "mimetype") final String? mimeType, - final int? size, -}) with _$FileInfo { - Map toJson() => _$FileInfoToJson(this); +@freezed +abstract class FileInfo with _$FileInfo { + /// Information for images, [size] is in bytes. + const factory FileInfo({ + @JsonKey(name: "mimetype") String? mimeType, + int? size, + }) = _FileInfo; factory FileInfo.fromJson(Map json) => _$FileInfoFromJson(json); diff --git a/lib/models/info/image.dart b/lib/models/info/image.dart index 266fb01..9833016 100644 --- a/lib/models/info/image.dart +++ b/lib/models/info/image.dart @@ -1,18 +1,17 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "image.freezed.dart"; part "image.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ImageInfo({ - @JsonKey(name: "h") final double? height, - @JsonKey(name: "w") final double? width, - @JsonKey(name: "mimetype") final String? mimeType, - @JsonKey(name: "xyz.amorgan.blurhash") final String? blurHash, - final int? size, -}) with _$ImageInfo { - Map toJson() => _$ImageInfoToJson(this); +@freezed +abstract class ImageInfo with _$ImageInfo { + /// Information for images, [size] is in bytes. + const factory ImageInfo({ + @JsonKey(name: "h") double? height, + @JsonKey(name: "w") double? width, + @JsonKey(name: "mimetype") String? mimeType, + @JsonKey(name: "xyz.amorgan.blurhash") String? blurHash, + int? size, + }) = _ImageInfo; factory ImageInfo.fromJson(Map json) => _$ImageInfoFromJson(json); diff --git a/lib/models/info/video.dart b/lib/models/info/video.dart index 31b1fb6..6ff3547 100644 --- a/lib/models/info/video.dart +++ b/lib/models/info/video.dart @@ -1,19 +1,18 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/ms_duration.dart"; - part "video.freezed.dart"; part "video.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const VideoInfo({ - @JsonKey(name: "h") final int? height, - @JsonKey(name: "w") final int? width, - @JsonKey(name: "mimetype") final String? mimeType, - @MSDuration() final Duration? duration, - final int? size, -}) with _$VideoInfo { - Map toJson() => _$VideoInfoToJson(this); +@freezed +abstract class VideoInfo with _$VideoInfo { + /// Information for images, [size] is in bytes. + const factory VideoInfo({ + @JsonKey(name: "h") int? height, + @JsonKey(name: "w") int? width, + @JsonKey(name: "mimetype") String? mimeType, + @MSDuration() Duration? duration, + int? size, + }) = _VideoInfo; factory VideoInfo.fromJson(Map json) => _$VideoInfoFromJson(json); diff --git a/lib/models/join_rule.dart b/lib/models/join_rule.dart index 44fb9bb..3fade23 100644 --- a/lib/models/join_rule.dart +++ b/lib/models/join_rule.dart @@ -1,4 +1,4 @@ import "package:freezed_annotation/freezed_annotation.dart"; -@JsonEnum(fieldRename: .snake) +@JsonEnum(fieldRename: FieldRename.snake) enum JoinRule { public, knock, invite, private, restricted, knockRestricted } diff --git a/lib/models/lazy_load_summary.dart b/lib/models/lazy_load_summary.dart index fba1c98..0cd250f 100644 --- a/lib/models/lazy_load_summary.dart +++ b/lib/models/lazy_load_summary.dart @@ -1,18 +1,15 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "lazy_load_summary.freezed.dart"; part "lazy_load_summary.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const LazyLoadSummary({ - @JsonKey(name: "m.heroes") required final IList? heroes, - @JsonKey(name: "m.joined_member_count") required final int? joinedMemberCount, - @JsonKey(name: "m.invited_member_count") - required final int? invitedMemberCount, -}) with _$LazyLoadSummary { - Map toJson() => _$LazyLoadSummaryToJson(this); +@freezed +abstract class LazyLoadSummary with _$LazyLoadSummary { + const factory LazyLoadSummary({ + required IList? heroes, + required int? joinedMemberCount, + required int? invitedMemberCount, + }) = _LazyLoadSummary; factory LazyLoadSummary.fromJson(Map json) => _$LazyLoadSummaryFromJson(json); diff --git a/lib/models/membership_action.dart b/lib/models/membership_action.dart index f03ca99..d852164 100644 --- a/lib/models/membership_action.dart +++ b/lib/models/membership_action.dart @@ -1 +1,4 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +@JsonEnum() enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/models/membership_status.dart b/lib/models/membership_status.dart index 7eb5f72..ba7a241 100644 --- a/lib/models/membership_status.dart +++ b/lib/models/membership_status.dart @@ -1 +1,4 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +@JsonEnum() enum MembershipStatus { leave, invite, ban, join, knock } diff --git a/lib/models/nav_page.dart b/lib/models/nav_page.dart deleted file mode 100644 index 2443c53..0000000 --- a/lib/models/nav_page.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:flutter/widgets.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; - -abstract class const NavPage({super.key}) extends Widget { - String get title; - IconData get icon; - Future? badgeBuilder(WidgetRef ref) => null; -} diff --git a/lib/models/oauth_auth_code_response.dart b/lib/models/oauth_auth_code_response.dart index 3074335..5bb3f3f 100644 --- a/lib/models/oauth_auth_code_response.dart +++ b/lib/models/oauth_auth_code_response.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "oauth_auth_code_response.freezed.dart"; part "oauth_auth_code_response.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const OAuthAuthCodeResponse({ - required final String state, - required final String codeVerifier, - required final Uri url, -}) with _$OAuthAuthCodeResponse { - Map toJson() => _$OAuthAuthCodeResponseToJson(this); +@freezed +abstract class OAuthAuthCodeResponse with _$OAuthAuthCodeResponse { + const factory OAuthAuthCodeResponse({ + required String state, + required String codeVerifier, + required Uri url, + }) = _OAuthAuthCodeResponse; factory OAuthAuthCodeResponse.fromJson(Map json) => _$OAuthAuthCodeResponseFromJson(json); diff --git a/lib/models/open_graph_data.dart b/lib/models/open_graph_data.dart index a183ef7..d7e840d 100644 --- a/lib/models/open_graph_data.dart +++ b/lib/models/open_graph_data.dart @@ -1,18 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "open_graph_data.freezed.dart"; part "open_graph_data.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const OpenGraphData({ - @JsonKey(name: "og:title") required final String? title, - @JsonKey(name: "og:description") required final String? description, - @JsonKey(name: "og:image") required final Uri? imageUrl, - @JsonKey(name: "og:image:width") required final double? width, - @JsonKey(name: "og:image:height") required final double? height, -}) with _$OpenGraphData { - Map toJson() => _$OpenGraphDataToJson(this); +@freezed +abstract class OpenGraphData with _$OpenGraphData { + const factory OpenGraphData({ + @JsonKey(name: "og:title") required String? title, + @JsonKey(name: "og:description") required String? description, + @JsonKey(name: "og:image") required Uri? imageUrl, + @JsonKey(name: "og:image:width") required double? width, + @JsonKey(name: "og:image:height") required double? height, + }) = _OpenGraphData; factory OpenGraphData.fromJson(Map json) => _$OpenGraphDataFromJson(json); diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart index dc6eede..df0a0f6 100644 --- a/lib/models/paginate.dart +++ b/lib/models/paginate.dart @@ -1,18 +1,16 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/event.dart"; - part "paginate.freezed.dart"; part "paginate.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Paginate({ - required final IList events, - required final IList relatedEvents, - required final bool hasMore, -}) with _$Paginate { - Map toJson() => _$PaginateToJson(this); +@freezed +abstract class Paginate with _$Paginate { + const factory Paginate({ + required IList events, + required IList relatedEvents, + required bool hasMore, + }) = _Paginate; factory Paginate.fromJson(Map json) => _$PaginateFromJson(json); diff --git a/lib/models/paginate_manual.dart b/lib/models/paginate_manual.dart deleted file mode 100644 index 9f45c1f..0000000 --- a/lib/models/paginate_manual.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/event.dart"; - -part "paginate_manual.freezed.dart"; -part "paginate_manual.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const PaginateManual({ - required final IList events, - final IList relatedEvents = const IList.empty(), - required final String? nextBatch, -}) with _$PaginateManual { - Map toJson() => _$PaginateManualToJson(this); - - factory PaginateManual.fromJson(Map json) => - _$PaginateManualFromJson(json); -} diff --git a/lib/models/profile.dart b/lib/models/profile.dart new file mode 100644 index 0000000..6ae1e94 --- /dev/null +++ b/lib/models/profile.dart @@ -0,0 +1,52 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/content/membership.dart"; +part "profile.freezed.dart"; +part "profile.g.dart"; + +@freezed +abstract class Profile with _$Profile { + static Object? readPronouns(Map map, _) => + map["m.pronouns"] ?? map["io.fsky.nyx.pronouns"]; + + static Object? readTimezone(Map map, _) => + map["m.tz"] ?? map["us.cloke.msc4175.tz"]; + + const factory Profile({ + required String id, + String? parseError, + Uri? avatarUrl, + + @JsonKey( + name: "displayname", + fromJson: MembershipContent.displaynameFromJson, + ) + String? displayName, + + @JsonKey(readValue: Profile.readTimezone, name: "m.tz") String? timezone, + + @Default(IList.empty()) + @JsonKey(readValue: Profile.readPronouns, name: "io.fsky.nyx.pronouns") + IList pronouns, + }) = _Profile; + + factory Profile.fromJson(Map json) => + _$ProfileFromJson(json); + + factory Profile.fromJsonWithCatch(Map json) { + try { + return Profile.fromJson(json); + } catch (error) { + return Profile(id: json["id"], parseError: error.toString()); + } + } +} + +@freezed +abstract class Pronoun with _$Pronoun { + const factory Pronoun({required String language, required String summary}) = + _Pronoun; + + factory Pronoun.fromJson(Map json) => + _$PronounFromJson(json); +} diff --git a/lib/models/profile_response.dart b/lib/models/profile_response.dart deleted file mode 100644 index 57f76c5..0000000 --- a/lib/models/profile_response.dart +++ /dev/null @@ -1,75 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/membership.dart"; - -part "profile_response.freezed.dart"; -part "profile_response.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ProfileResponse({ - @JsonKey(fromJson: Profile.fromJson) required final Profile profile, - final Bio? bio, -}) with _$ProfileResponse { - Map toJson() => _$ProfileResponseToJson(this); - - factory ProfileResponse.fromJson(Map json) => - _$ProfileResponseFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Bio({required final String html, final String? editSource}) - with _$Bio { - Map toJson() => _$BioToJson(this); - - factory Bio.fromJson(Map json) => _$BioFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Profile({ - final String? id, - final String? parseError, - final Uri? avatarUrl, - - @JsonKey(name: "displayname", fromJson: MembershipContent.displaynameFromJson) - final String? displayName, - - @JsonKey(readValue: Profile.readTimezone, name: "m.tz") - final String? timezone, - - @JsonKey(readValue: Profile.readPronouns, name: "m.pronouns") - final IList pronouns = const IList.empty(), -}) with _$Profile { - Map toJson() => _$ProfileToJson(this); - - static Object? readPronouns(Map map, String key) => - map[key] ?? map["io.fsky.nyx.pronouns"]; - - static Object? readTimezone(Map map, String key) => - map[key] ?? map["us.cloke.msc4175.tz"]; - - factory Profile.fromJson(Map json) => - _$ProfileFromJson(json); - - factory Profile.fromJsonWithCatch(Map json) { - try { - return Profile.fromJson(json); - } catch (error) { - return Profile(parseError: error.toString()); - } - } -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Pronoun({ - required final String language, - required final String summary, -}) with _$Pronoun { - Map toJson() => _$PronounToJson(this); - - factory Pronoun.fromJson(Map json) => - _$PronounFromJson(json); -} diff --git a/lib/models/read_receipt.dart b/lib/models/read_receipt.dart index ab4edd8..d533e2d 100644 --- a/lib/models/read_receipt.dart +++ b/lib/models/read_receipt.dart @@ -1,19 +1,17 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/epoch_date_time_converter.dart"; - part "read_receipt.freezed.dart"; part "read_receipt.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ReadReceipt({ - final String? roomId, - required final String userId, - final String? threadId, - required final String eventId, - @EpochDateTimeConverter() required final DateTime timestamp, -}) with _$ReadReceipt { - Map toJson() => _$ReadReceiptToJson(this); +@freezed +abstract class ReadReceipt with _$ReadReceipt { + const factory ReadReceipt({ + String? roomId, + required String userId, + String? threadId, + required String eventId, + @EpochDateTimeConverter() required DateTime timestamp, + }) = _ReadReceipt; factory ReadReceipt.fromJson(Map json) => _$ReadReceiptFromJson(json); diff --git a/lib/models/requests/deregister_pusher.dart b/lib/models/requests/deregister_pusher.dart deleted file mode 100644 index 1416add..0000000 --- a/lib/models/requests/deregister_pusher.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "deregister_pusher.freezed.dart"; -part "deregister_pusher.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const DeregisterPusherRequest({ - required final String appId, - @JsonKey(name: "pushkey") required final String pushKey, -}) with _$DeregisterPusherRequest { - Map toJson() => _$DeregisterPusherRequestToJson(this); -} diff --git a/lib/models/requests/download_media.dart b/lib/models/requests/download_media.dart index c9751f8..b5d5771 100644 --- a/lib/models/requests/download_media.dart +++ b/lib/models/requests/download_media.dart @@ -1,17 +1,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "download_media.freezed.dart"; part "download_media.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const DownloadMediaRequest({ - required final Uri mxc, - final bool encrypted = false, - final bool isAvatar = false, - final bool thumbnailAvatar = false, -}) with _$DownloadMediaRequest { - Map toJson() => _$DownloadMediaRequestToJson(this); +@freezed +abstract class DownloadMediaRequest with _$DownloadMediaRequest { + const factory DownloadMediaRequest({ + required Uri mxc, + @Default(false) bool encrypted, + @Default(false) bool isAvatar, + @Default(false) bool thumbnailAvatar, + }) = _DownloadMediaRequest; factory DownloadMediaRequest.fromJson(Map json) => _$DownloadMediaRequestFromJson(json); diff --git a/lib/models/requests/get_event.dart b/lib/models/requests/get_event.dart index b4692ee..2665a2a 100644 --- a/lib/models/requests/get_event.dart +++ b/lib/models/requests/get_event.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "get_event.freezed.dart"; part "get_event.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const GetEventRequest({ - required final String roomId, - required final String eventId, - final bool unredact = false, -}) with _$GetEventRequest { - Map toJson() => _$GetEventRequestToJson(this); +@freezed +abstract class GetEventRequest with _$GetEventRequest { + const factory GetEventRequest({ + required String roomId, + required String eventId, + @Default(false) bool unredact, + }) = _GetEventRequest; factory GetEventRequest.fromJson(Map json) => _$GetEventRequestFromJson(json); diff --git a/lib/models/requests/get_event_context.dart b/lib/models/requests/get_event_context.dart deleted file mode 100644 index 68f23a0..0000000 --- a/lib/models/requests/get_event_context.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "get_event_context.freezed.dart"; -part "get_event_context.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const GetEventContextRequest({ - required final String roomId, - required final String eventId, - final int limit = 20, -}) with _$GetEventContextRequest { - Map toJson() => _$GetEventContextRequestToJson(this); - - factory GetEventContextRequest.fromJson(Map json) => - _$GetEventContextRequestFromJson(json); -} diff --git a/lib/models/requests/get_mentions.dart b/lib/models/requests/get_mentions.dart deleted file mode 100644 index 1512b6f..0000000 --- a/lib/models/requests/get_mentions.dart +++ /dev/null @@ -1,19 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/epoch_date_time_converter.dart"; -import "package:nexus/models/event.dart"; -part "get_mentions.freezed.dart"; -part "get_mentions.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class GetMentionsRequest({ - @EpochDateTimeConverter() required final DateTime maxTimestamp, - @JsonKey(name: "type") required final UnreadType unreadType, - final int limit = 20, - final String? roomId, -}) with _$GetMentionsRequest { - Map toJson() => _$GetMentionsRequestToJson(this); - - factory GetMentionsRequest.fromJson(Map json) => - _$GetMentionsRequestFromJson(json); -} diff --git a/lib/models/requests/get_related_events.dart b/lib/models/requests/get_related_events.dart index 08be426..52d2716 100644 --- a/lib/models/requests/get_related_events.dart +++ b/lib/models/requests/get_related_events.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "get_related_events.freezed.dart"; part "get_related_events.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const GetRelatedEventsRequest({ - required final String roomId, - required final String eventId, - required final String relationType, -}) with _$GetRelatedEventsRequest { - Map toJson() => _$GetRelatedEventsRequestToJson(this); +@freezed +abstract class GetRelatedEventsRequest with _$GetRelatedEventsRequest { + const factory GetRelatedEventsRequest({ + required String roomId, + required String eventId, + required String relationType, + }) = _GetRelatedEventsRequest; factory GetRelatedEventsRequest.fromJson(Map json) => _$GetRelatedEventsRequestFromJson(json); diff --git a/lib/models/requests/get_room_state.dart b/lib/models/requests/get_room_state.dart index 66504da..d3f52f7 100644 --- a/lib/models/requests/get_room_state.dart +++ b/lib/models/requests/get_room_state.dart @@ -1,17 +1,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "get_room_state.freezed.dart"; part "get_room_state.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const GetRoomStateRequest({ - required final String roomId, - final bool refetch = false, - final bool fetchMembers = false, - final bool includeMembers = false, -}) with _$GetRoomStateRequest { - Map toJson() => _$GetRoomStateRequestToJson(this); +@freezed +abstract class GetRoomStateRequest with _$GetRoomStateRequest { + const factory GetRoomStateRequest({ + required String roomId, + @Default(false) bool refetch, + @Default(false) bool fetchMembers, + @Default(false) bool includeMembers, + }) = _GetRoomStateRequest; factory GetRoomStateRequest.fromJson(Map json) => _$GetRoomStateRequestFromJson(json); diff --git a/lib/models/requests/join_room.dart b/lib/models/requests/join_room.dart index 350b762..8487298 100644 --- a/lib/models/requests/join_room.dart +++ b/lib/models/requests/join_room.dart @@ -1,16 +1,14 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "join_room.freezed.dart"; part "join_room.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const JoinRoomRequest({ - required final String roomIdOrAlias, - final IList via = const IList.empty(), -}) with _$JoinRoomRequest { - Map toJson() => _$JoinRoomRequestToJson(this); +@freezed +abstract class JoinRoomRequest with _$JoinRoomRequest { + const factory JoinRoomRequest({ + required String roomIdOrAlias, + required IList via, + }) = _JoinRoomRequest; factory JoinRoomRequest.fromJson(Map json) => _$JoinRoomRequestFromJson(json); diff --git a/lib/models/requests/oauth/exchange_token.dart b/lib/models/requests/oauth/exchange_token.dart index fb47358..9d55c1f 100644 --- a/lib/models/requests/oauth/exchange_token.dart +++ b/lib/models/requests/oauth/exchange_token.dart @@ -1,18 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "exchange_token.freezed.dart"; part "exchange_token.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const OAuthExchangeTokenRequest({ - required final Uri homeserverUrl, - required final String codeVerifier, - required final Uri redirectUri, - required final String code, - required final String clientId, -}) with _$OAuthExchangeTokenRequest { - Map toJson() => _$OAuthExchangeTokenRequestToJson(this); +@freezed +abstract class OAuthExchangeTokenRequest with _$OAuthExchangeTokenRequest { + const factory OAuthExchangeTokenRequest({ + required Uri homeserverUrl, + required String codeVerifier, + required Uri redirectUri, + required String code, + required String clientId, + }) = _OAuthExchangeTokenRequest; factory OAuthExchangeTokenRequest.fromJson(Map json) => _$OAuthExchangeTokenRequestFromJson(json); diff --git a/lib/models/requests/oauth/get_auth_url.dart b/lib/models/requests/oauth/get_auth_url.dart index 1ff166a..5ca5b6f 100644 --- a/lib/models/requests/oauth/get_auth_url.dart +++ b/lib/models/requests/oauth/get_auth_url.dart @@ -2,27 +2,25 @@ import "dart:math"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "get_auth_url.freezed.dart"; part "get_auth_url.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const OAuthGetAuthUrl({ - required final ResponseMode responseMode, - required final Uri homeserverUrl, - required final Uri redirectUri, - required final IList scopes, - required final String clientId, - final String? userIdHint, -}) with _$OAuthGetAuthUrl { - Map toJson() => _$OAuthGetAuthUrlToJson(this); +@freezed +abstract class OAuthGetAuthUrl with _$OAuthGetAuthUrl { + const factory OAuthGetAuthUrl({ + required ResponseMode responseMode, + required Uri homeserverUrl, + required Uri redirectUri, + required IList scopes, + required String clientId, + String? userIdHint, + }) = _OAuthGetAuthUrl; factory OAuthGetAuthUrl.fromJson(Map json) => _$OAuthGetAuthUrlFromJson(json); } -sealed class Scope { +abstract class Scope { static final openid = "openid"; static final email = "email"; static final clientApi = "urn:matrix:client:api:*"; diff --git a/lib/models/requests/oauth/register_client.dart b/lib/models/requests/oauth/register_client.dart index e5c1a70..cf4f9e0 100644 --- a/lib/models/requests/oauth/register_client.dart +++ b/lib/models/requests/oauth/register_client.dart @@ -1,27 +1,26 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "register_client.freezed.dart"; part "register_client.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const OAuthRegisterClientRequest({ - required final Uri homeserverUrl, - final ApplicationType applicationType = ApplicationType.web, - final String? clientName, - required final Uri clientUri, - final Uri? logoUri, - final Uri? policyUri, - final Uri? tosUri, - final IList? grantTypes, - final IList? redirectUris, - final IList? responseTypes, +@freezed +abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest { + const factory OAuthRegisterClientRequest({ + required Uri homeserverUrl, + @Default(ApplicationType.web) ApplicationType applicationType, + String? clientName, + required Uri clientUri, + Uri? logoUri, + Uri? policyUri, + Uri? tosUri, + IList? grantTypes, + IList? redirectUris, + IList? responseTypes, - @JsonKey(name: "token_endpoint_auth_method") - final AuthMethod? authMethod = AuthMethod.none, -}) with _$OAuthRegisterClientRequest { - Map toJson() => _$OAuthRegisterClientRequestToJson(this); + @Default(AuthMethod.none) + @JsonKey(name: "token_endpoint_auth_method") + AuthMethod? authMethod, + }) = _OAuthRegisterClientRequest; factory OAuthRegisterClientRequest.fromJson(Map json) => _$OAuthRegisterClientRequestFromJson(json); diff --git a/lib/models/requests/paginate.dart b/lib/models/requests/paginate.dart index b3903c6..ddc62f3 100644 --- a/lib/models/requests/paginate.dart +++ b/lib/models/requests/paginate.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "paginate.freezed.dart"; part "paginate.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const PaginateRequest({ - required final String roomId, - required final int? maxTimelineId, - final int limit = 20, -}) with _$PaginateRequest { - Map toJson() => _$PaginateRequestToJson(this); +@freezed +abstract class PaginateRequest with _$PaginateRequest { + const factory PaginateRequest({ + required String roomId, + required int? maxTimelineId, + @Default(20) int limit, + }) = _PaginateRequest; factory PaginateRequest.fromJson(Map json) => _$PaginateRequestFromJson(json); diff --git a/lib/models/requests/paginate_manual.dart b/lib/models/requests/paginate_manual.dart deleted file mode 100644 index 9f8a1d3..0000000 --- a/lib/models/requests/paginate_manual.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/direction.dart"; - -part "paginate_manual.freezed.dart"; -part "paginate_manual.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const PaginateManualRequest({ - required final String roomId, - // Root event ID of a thread to paginate - final String? threadRoot, - // Can be null for starting pagination of a thread - final String? since, - required final Direction direction, - final int limit = 20, -}) with _$PaginateManualRequest { - Map toJson() => _$PaginateManualRequestToJson(this); - - factory PaginateManualRequest.fromJson(Map json) => - _$PaginateManualRequestFromJson(json); -} diff --git a/lib/models/requests/register_pusher.dart b/lib/models/requests/register_pusher.dart deleted file mode 100644 index dd34e62..0000000 --- a/lib/models/requests/register_pusher.dart +++ /dev/null @@ -1,56 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "register_pusher.freezed.dart"; -part "register_pusher.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RegisterPusherRequest({ - required final String appDisplayName, - required final String appId, - final bool append = false, - required final PusherData data, - required final String deviceDisplayName, - required final PusherKind kind, - required final String lang, - - /// TODO: What does this do? - final String? profileTag, - - @JsonKey(name: "pushkey") required final String pushKey, -}) with _$RegisterPusherRequest { - Map toJson() => _$RegisterPusherRequestToJson(this); -} - -@freezed -sealed class PusherData with _$PusherData { - const factory PusherData.http({ - required Uri url, - @Default(PushFormat.eventIdOnly) PushFormat format, - }) = HttpPusherData; - - const factory PusherData.webPush({ - required Uri url, - @Default(PushFormat.eventIdOnly) PushFormat format, - - /// `data.auth`: RFC8291 authentication secret. - required String auth, - }) = WebPushPusherData; - - factory PusherData.fromJson(Map json) => - _$PusherDataFromJson(json); -} - -@JsonEnum(fieldRename: .snake) -enum PushFormat { - @JsonValue(null) - all, - eventIdOnly, -} - -enum PusherKind { - http, - email, - @JsonValue("org.matrix.msc4174.webpush") - webPush, -} diff --git a/lib/models/requests/report.dart b/lib/models/requests/report.dart index ed05715..f87b1f1 100644 --- a/lib/models/requests/report.dart +++ b/lib/models/requests/report.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "report.freezed.dart"; part "report.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const ReportRequest({ - required final String roomId, - required final String eventId, - final String? reason, -}) with _$ReportRequest { - Map toJson() => _$ReportRequestToJson(this); +@freezed +abstract class ReportRequest with _$ReportRequest { + const factory ReportRequest({ + required String roomId, + required String eventId, + String? reason, + }) = _ReportRequest; factory ReportRequest.fromJson(Map json) => _$ReportRequestFromJson(json); diff --git a/lib/models/requests/send_event.dart b/lib/models/requests/send_event.dart index acad57f..196c0b5 100644 --- a/lib/models/requests/send_event.dart +++ b/lib/models/requests/send_event.dart @@ -1,21 +1,19 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; - part "send_event.freezed.dart"; part "send_event.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SendEventRequest({ - required final String roomId, - required final String type, - required final Content content, - final String? relatesTo, - final String? relationType, - final bool synchronous = false, - final bool disableEncryption = false, -}) with _$SendEventRequest { - Map toJson() => _$SendEventRequestToJson(this); +@freezed +abstract class SendEventRequest with _$SendEventRequest { + const factory SendEventRequest({ + required String roomId, + required String type, + required Content content, + String? relatesTo, + String? relationType, + @Default(false) bool synchronous, + @Default(false) bool disableEncryption, + }) = _SendEventRequest; factory SendEventRequest.fromJson(Map json) => _$SendEventRequestFromJson(json); diff --git a/lib/models/requests/send_message.dart b/lib/models/requests/send_message.dart index 9907439..951198e 100644 --- a/lib/models/requests/send_message.dart +++ b/lib/models/requests/send_message.dart @@ -2,43 +2,43 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/relation_type.dart"; - part "send_message.freezed.dart"; part "send_message.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SendMessageRequest({ - required final String roomId, - required final String text, - final Content? baseContent, - @JsonKey(name: "mentions") final Mentions mentions = const Mentions(), - @JsonKey(name: "relates_to") final Relation? relation, -}) with _$SendMessageRequest { - Map toJson() => _$SendMessageRequestToJson(this); +@freezed +abstract class SendMessageRequest with _$SendMessageRequest { + const factory SendMessageRequest({ + required String roomId, + required String text, + Content? baseContent, + @Default(Mentions()) @JsonKey(name: "mentions") Mentions mentions, + @JsonKey(name: "relates_to") Relation? relation, + }) = _SendMessageRequest; factory SendMessageRequest.fromJson(Map json) => _$SendMessageRequestFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Mentions({ - final bool room = false, - final IList userIds = const IList.empty(), -}) with _$Mentions { - Map toJson() => _$MentionsToJson(this); +@freezed +abstract class Mentions with _$Mentions { + const factory Mentions({ + @Default(false) bool room, + @Default(IList.empty()) IList userIds, + }) = _Mentions; factory Mentions.fromJson(Map json) => _$MentionsFromJson(json); } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable(createToJson: false) -class const Relation({ - required final String eventId, - required final RelationType relationType, -}) with _$Relation { +@Freezed(toJson: false) +abstract class Relation with _$Relation { + const Relation._(); + + const factory Relation({ + required String eventId, + required RelationType relationType, + }) = _Relation; + Map toJson() { switch (relationType) { case RelationType.reply: diff --git a/lib/models/requests/set_account_data.dart b/lib/models/requests/set_account_data.dart deleted file mode 100644 index 95ef75c..0000000 --- a/lib/models/requests/set_account_data.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; - -part "set_account_data.freezed.dart"; -part "set_account_data.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SetAccountDataRequest({ - required final String type, - required final dynamic content, - final String? roomId, -}) with _$SetAccountDataRequest { - Map toJson() => _$SetAccountDataRequestToJson(this); - - factory SetAccountDataRequest.fromJson(Map json) => - _$SetAccountDataRequestFromJson(json); -} diff --git a/lib/models/requests/set_membership.dart b/lib/models/requests/set_membership.dart index 9b8a03f..4bbe8a3 100644 --- a/lib/models/requests/set_membership.dart +++ b/lib/models/requests/set_membership.dart @@ -1,19 +1,18 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/membership_action.dart"; - part "set_membership.freezed.dart"; part "set_membership.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SetMembershipRequest({ - required final String userId, - required final String roomId, - final String? reason, - required final MembershipAction action, - @JsonKey(name: "msc4293_redact_events") final bool redact = false, -}) with _$SetMembershipRequest { - Map toJson() => _$SetMembershipRequestToJson(this); +@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/requests/set_state.dart b/lib/models/requests/set_state.dart index d0e3e95..c92763d 100644 --- a/lib/models/requests/set_state.dart +++ b/lib/models/requests/set_state.dart @@ -1,23 +1,22 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/ms_duration.dart"; - part "set_state.freezed.dart"; part "set_state.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SetStateRequest({ - required final String roomId, - required final String type, - required final String stateKey, - required final Content content, +@freezed +abstract class SetStateRequest with _$SetStateRequest { + const factory SetStateRequest({ + required String roomId, + required String type, + required String stateKey, + required Content content, - @JsonKey(name: "delay_ms", includeIfNull: false) - @MSDuration() - final Duration? delay, -}) with _$SetStateRequest { - Map toJson() => _$SetStateRequestToJson(this); + @JsonKey(name: "delay_ms", includeIfNull: false) + @MSDuration() + @Default(null) + Duration? delay, + }) = _SetStateRequest; factory SetStateRequest.fromJson(Map json) => _$SetStateRequestFromJson(json); diff --git a/lib/models/requests/upload_media.dart b/lib/models/requests/upload_media.dart index d6e69ac..a640bea 100644 --- a/lib/models/requests/upload_media.dart +++ b/lib/models/requests/upload_media.dart @@ -1,26 +1,23 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "upload_media.freezed.dart"; part "upload_media.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const UploadMediaRequest({ - required final String path, - required final bool encrypt, - final String? filename, - @JsonKey(name: "voice_message") final bool isVoiceMessage = false, - final bool forceFile = false, - final - // Below params only work if encodeTo is set - String? - encodeTo, - final int? resizeWidth, - final int? resizeHeight, - final int? resizePercent, - final int quality = 80, -}) with _$UploadMediaRequest { - Map toJson() => _$UploadMediaRequestToJson(this); +@freezed +abstract class UploadMediaRequest with _$UploadMediaRequest { + const factory UploadMediaRequest({ + required String path, + required bool encrypt, + String? filename, + @Default(false) @JsonKey(name: "voice_message") bool isVoiceMessage, + @Default(false) bool forceFile, + + // Below params only work if encodeTo is set + String? encodeTo, + int? resizeWidth, + int? resizeHeight, + int? resizePercent, + @Default(80) int quality, + }) = _UploadMediaRequest; factory UploadMediaRequest.fromJson(Map json) => _$UploadMediaRequestFromJson(json); diff --git a/lib/models/room.dart b/lib/models/room.dart index 24b0c5e..fb21a55 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -3,46 +3,11 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/room_metadata.dart"; - part "room.freezed.dart"; part "room.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Room({ - @JsonKey(name: "meta") final RoomMetadata? metadata, - - /// [timeline] is an IMap of timelineRowId to eventRowId - @JsonKey(fromJson: Room.timelineTupleJsonToIMap) - final IMap timeline = const IMap.empty(), - - /// [clientSticky] is an ISet of eventRowId - @JsonKey(includeFromJson: false, includeToJson: false) - final ISet clientSticky = const ISet.empty(), - - /// [events] is an IMap of eventRowId to event - @JsonKey(fromJson: Room.eventsJsonToIMap) - final IMap events = const IMap.empty(), - - final bool reset = false, - @JsonKey(includeFromJson: false, includeToJson: false) - final bool resetClientSticky = false, - - @JsonKey(includeFromJson: false, includeToJson: false) - final bool hasFetchedState = false, - - @JsonKey(includeFromJson: false, includeToJson: false) - final bool hasFetchedMembers = false, - - final IMap> state = const IMap.empty(), - - final IMap> receipts = const IMap.empty(), - final bool dismissNotifications = false, - final bool hasMore = true, - - // IMap accountData, - // IList notifications, -}) with _$Room { +@freezed +abstract class Room with _$Room { static IMap timelineTupleJsonToIMap(List json) => IMap.fromEntries( json.map( @@ -61,7 +26,32 @@ class const Room({ }), ); - Map toJson() => _$RoomToJson(this); + /// [timeline] is an IMap of timelineRowId to eventRowId + /// [events] is an IMap of eventRowId to event + /// [sticky] is an ISet of eventRowId + const factory Room({ + @JsonKey(name: "meta") RoomMetadata? metadata, + @Default(IMap.empty()) + @JsonKey(fromJson: Room.timelineTupleJsonToIMap) + IMap timeline, + @Default(ISet.empty()) ISet sticky, + + @Default(IMap.empty()) + @JsonKey(fromJson: Room.eventsJsonToIMap) + IMap events, + + @Default(false) bool reset, + @Default(false) bool hasFetchedState, + @Default(false) bool hasFetchedMembers, + @Default(IMap.empty()) IMap> state, + + @Default(IMap.empty()) IMap> receipts, + @Default(false) bool dismissNotifications, + @Default(true) bool hasMore, + + // required IMap accountData, + // required IList notifications, + }) = _Room; factory Room.fromJson(Map json) => _$RoomFromJson(json); } diff --git a/lib/models/room_chat.dart b/lib/models/room_chat.dart deleted file mode 100644 index e784222..0000000 --- a/lib/models/room_chat.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; - -part "room_chat.freezed.dart"; -part "room_chat.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RoomChat({ - /// List of eventRowId - required final IList timeline, - required final bool hasMoreForward, - required final bool hasMoreBackward, - final HistoricalData? historicalData, -}) with _$RoomChat { - Map toJson() => _$RoomChatToJson(this); - - factory RoomChat.fromJson(Map json) => - _$RoomChatFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const HistoricalData({ - required final String start, - required final String end, -}) with _$HistoricalData { - Map toJson() => _$HistoricalDataToJson(this); - - factory HistoricalData.fromJson(Map json) => - _$HistoricalDataFromJson(json); -} diff --git a/lib/models/room_metadata.dart b/lib/models/room_metadata.dart index 822fcda..7c16cae 100644 --- a/lib/models/room_metadata.dart +++ b/lib/models/room_metadata.dart @@ -1,28 +1,29 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/epoch_date_time_converter.dart"; import "package:nexus/models/lazy_load_summary.dart"; - part "room_metadata.freezed.dart"; part "room_metadata.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RoomMetadata({ - @JsonKey(name: "room_id") required final String id, - final String? name, - final Uri? avatar, - final String? dmUserId, - final String? topic, - final String? canonicalAlias, - final LazyLoadSummary? lazyLoadSummary, - required final bool hasMemberList, - @JsonKey(name: "preview_event_rowid") required final int previewEventRowID, - @EpochDateTimeConverter() required final DateTime sortingTimestamp, - required final int unreadHighlights, - required final int unreadNotifications, - required final int unreadMessages, -}) with _$RoomMetadata { - Map toJson() => _$RoomMetadataToJson(this); +@freezed +abstract class RoomMetadata with _$RoomMetadata { + const factory RoomMetadata({ + @JsonKey(name: "room_id") required String id, + + // required CreateEventContent creationContent, + // required TombstoneEventContent tombstoneEventContent, + String? name, + Uri? avatar, + String? dmUserId, + String? topic, + String? canonicalAlias, + LazyLoadSummary? lazyLoadSummary, + required bool hasMemberList, + @JsonKey(name: "preview_event_rowid") required int previewEventRowID, + @EpochDateTimeConverter() required DateTime sortingTimestamp, + required int unreadHighlights, + required int unreadNotifications, + required int unreadMessages, + }) = _RoomMetadata; factory RoomMetadata.fromJson(Map json) => _$RoomMetadataFromJson(json); diff --git a/lib/models/room_summary.dart b/lib/models/room_summary.dart deleted file mode 100644 index d0a6bee..0000000 --- a/lib/models/room_summary.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:freezed_annotation/freezed_annotation.dart"; -import "package:nexus/models/content/create.dart"; -import "package:nexus/models/join_rule.dart"; - -part "room_summary.freezed.dart"; -part "room_summary.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RoomSummary({ - required final String roomId, - @JsonKey(name: "num_joined_members") final int? joinedMembers, - final JoinRule? joinRule, - final String? name, - final Uri? avatarUrl, - final String? canonicalAlias, - final String? topic, - final String? roomVersion, - @JsonKey(unknownEnumValue: RoomType.room) final RoomType? roomType, -}) with _$RoomSummary { - Map toJson() => _$RoomSummaryToJson(this); - - factory RoomSummary.fromJson(Map json) => - _$RoomSummaryFromJson(json); -} diff --git a/lib/models/setting.dart b/lib/models/setting.dart index 2ff541d..6a49845 100644 --- a/lib/models/setting.dart +++ b/lib/models/setting.dart @@ -1,13 +1,16 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; -class Setting({ - required final String title, - required final String description, - required final IconData icon, - required final Widget Function( - String title, - String description, - IconData icon, - ) - builder, -}); +class Setting { + final String title; + final String description; + final IconData icon; + final Widget Function(String title, String description, IconData icon) + builder; + + Setting({ + required this.title, + required this.description, + required this.builder, + required this.icon, + }); +} diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 97cd6ca..eddc2da 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,19 +1,15 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:freezed_annotation/freezed_annotation.dart"; - part "settings.freezed.dart"; part "settings.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Settings({ - final ThemeMode theme = ThemeMode.system, - final bool useDynamicTheming = true, - final bool linuxMobileMode = false, - final bool showAllInHome = false, - final bool showMinimizeButton = false, -}) with _$Settings { - Map toJson() => _$SettingsToJson(this); +@freezed +abstract class Settings with _$Settings { + const factory Settings({ + @Default(ThemeMode.system) ThemeMode theme, + @Default(true) bool useDynamicTheming, + @Default(false) bool linuxMobileMode, + }) = _Settings; factory Settings.fromJson(Map json) => _$SettingsFromJson(json); diff --git a/lib/models/settings_category.dart b/lib/models/settings_category.dart index 2dc7076..88bfdd0 100644 --- a/lib/models/settings_category.dart +++ b/lib/models/settings_category.dart @@ -1,13 +1,14 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/setting.dart"; - part "settings_category.freezed.dart"; @freezed -class const SettingsCategory({ - required final String title, - required final IconData icon, - required final IList settings, -}) with _$SettingsCategory; +abstract class SettingsCategory with _$SettingsCategory { + const factory SettingsCategory({ + required String title, + required IconData icon, + required IList settings, + }) = _SettingsCategory; +} diff --git a/lib/models/space.dart b/lib/models/space.dart index 97efe7a..73fbbc6 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -3,15 +3,16 @@ import "package:flutter/widgets.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/subspace.dart"; - part "space.freezed.dart"; @freezed -class const Space({ - required final String id, - required final String title, - final IconData? icon, - final Room? room, - required final IList children, - required final IList subSpaces, -}) with _$Space; +abstract class Space with _$Space { + const factory Space({ + required String id, + required String title, + IconData? icon, + Room? room, + required IList children, + required IList subSpaces, + }) = _Space; +} diff --git a/lib/models/space_edge.dart b/lib/models/space_edge.dart index ea74130..192af31 100644 --- a/lib/models/space_edge.dart +++ b/lib/models/space_edge.dart @@ -1,15 +1,13 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "space_edge.freezed.dart"; part "space_edge.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SpaceEdge({ - required final String childId, - final bool suggested = false, -}) with _$SpaceEdge { - Map toJson() => _$SpaceEdgeToJson(this); +@freezed +abstract class SpaceEdge with _$SpaceEdge { + const factory SpaceEdge({ + required String childId, + @Default(false) bool suggested, + }) = _SpaceEdge; factory SpaceEdge.fromJson(Map json) => _$SpaceEdgeFromJson(json); diff --git a/lib/models/spec_versions_response.dart b/lib/models/spec_versions_response.dart deleted file mode 100644 index 1f414d2..0000000 --- a/lib/models/spec_versions_response.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:freezed_annotation/freezed_annotation.dart"; - -part "spec_versions_response.freezed.dart"; -part "spec_versions_response.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SpecVersionsResponse({ - required final IList versions, - required final UnstableFeatures unstableFeatures, -}) with _$SpecVersionsResponse { - Map toJson() => _$SpecVersionsResponseToJson(this); - - factory SpecVersionsResponse.fromJson(Map json) => - _$SpecVersionsResponseFromJson(json); -} - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const UnstableFeatures({ - @JsonKey(name: "uk.timedout.msc4494") final bool msc4494 = false, -}) with _$UnstableFeatures { - Map toJson() => _$UnstableFeaturesToJson(this); - - factory UnstableFeatures.fromJson(Map json) => - _$UnstableFeaturesFromJson(json); -} diff --git a/lib/models/subspace.dart b/lib/models/subspace.dart index 97d1de2..1a1879c 100644 --- a/lib/models/subspace.dart +++ b/lib/models/subspace.dart @@ -1,11 +1,10 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room.dart"; - part "subspace.freezed.dart"; @freezed -class const Subspace({ - required final Room room, - required final IList children, -}) with _$Subspace; +abstract class Subspace with _$Subspace { + const factory Subspace({required Room room, required IList children}) = + _Subspace; +} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index a9c10bf..0f98bb2 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -1,22 +1,22 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/account_data.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/space_edge.dart"; - part "sync_data.freezed.dart"; part "sync_data.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SyncData({ - final bool clearState = false, - final IMap> accountData = const IMap.empty(), - final IMap rooms = const IMap.empty(), - final ISet leftRooms = const ISet.empty(), - final IMap>? spaceEdges, - final IList? topLevelSpaces, -}) with _$SyncData { - Map toJson() => _$SyncDataToJson(this); +@freezed +abstract class SyncData with _$SyncData { + const factory SyncData({ + @Default(false) bool clearState, + @Default(IMap.empty()) IMap accountData, + @Default(IMap.empty()) IMap rooms, + @Default(ISet.empty()) ISet leftRooms, + // required IList invitedRooms, + IMap>? spaceEdges, + IList? topLevelSpaces, + }) = _SyncData; factory SyncData.fromJson(Map json) => _$SyncDataFromJson(json); diff --git a/lib/models/sync_status.dart b/lib/models/sync_status.dart index 57bd882..7848fbe 100644 --- a/lib/models/sync_status.dart +++ b/lib/models/sync_status.dart @@ -1,16 +1,14 @@ import "package:freezed_annotation/freezed_annotation.dart"; - part "sync_status.freezed.dart"; part "sync_status.g.dart"; -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const SyncStatus({ - required final SyncStatusType type, - required final String? error, - required final int errorCount, -}) with _$SyncStatus { - Map toJson() => _$SyncStatusToJson(this); +@freezed +abstract class SyncStatus with _$SyncStatus { + const factory SyncStatus({ + required SyncStatusType type, + String? error, + required int errorCount, + }) = _SyncStatus; factory SyncStatus.fromJson(Map json) => _$SyncStatusFromJson(json); diff --git a/lib/widgets/pages/chat.dart b/lib/pages/chat.dart similarity index 72% rename from lib/widgets/pages/chat.dart rename to lib/pages/chat.dart index 74aea14..316c9af 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/pages/chat.dart @@ -1,22 +1,25 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/emoji.dart"; import "package:nexus/controllers/init_complete.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/sidebar.dart"; -import "package:nexus/widgets/room_chat/room_chat.dart"; +import "package:nexus/widgets/room_chat.dart"; import "package:nexus/widgets/loading.dart"; -class const ChatPage({super.key}) extends HookConsumerWidget { +class ChatPage extends ConsumerWidget { + const ChatPage({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth > 650; final showMembersByDefault = constraints.maxWidth > 1000; final initComplete = ref.watch(InitCompleteController.provider); - final roomId = ref - .watch(KeyController.provider(KeyController.roomKey)) - .requireValue; + final roomId = ref.watch(KeyController.provider(KeyController.roomKey)); + + ref.read(EmojiController.provider); return SafeArea( child: Scaffold( @@ -24,10 +27,9 @@ class const ChatPage({super.key}) extends HookConsumerWidget { body: initComplete ? Row( children: [ - if (isDesktop) Sidebar(), + if (isDesktop) Sidebar(isDesktop: isDesktop), Expanded( child: RoomChat( - key: ValueKey(roomId), roomId: roomId, isDesktop: isDesktop, showMembersByDefault: showMembersByDefault, @@ -41,7 +43,9 @@ class const ChatPage({super.key}) extends HookConsumerWidget { children: [Loading(), Text("Syncing...")], ), ), - drawer: isDesktop || !initComplete ? null : Sidebar(), + drawer: isDesktop || !initComplete + ? null + : Sidebar(isDesktop: isDesktop), ), ); }, diff --git a/lib/widgets/pages/select_server.dart b/lib/pages/select_server.dart similarity index 64% rename from lib/widgets/pages/select_server.dart rename to lib/pages/select_server.dart index c690dc1..f7cefb8 100644 --- a/lib/widgets/pages/select_server.dart +++ b/lib/pages/select_server.dart @@ -1,5 +1,5 @@ import "package:app_links/app_links.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; @@ -9,11 +9,13 @@ import "package:nexus/controllers/client_id.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/main.dart"; import "package:nexus/models/homeserver.dart"; -import "package:nexus/widgets/pages/settings.dart"; +import "package:nexus/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/divider_text.dart"; -class const SelectServerPage({super.key}) extends HookConsumerWidget { +class SelectServerPage extends HookConsumerWidget { + const SelectServerPage({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); @@ -24,17 +26,6 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { final homeserverUrl = useTextEditingController(); Future setHomeserver(Uri? newHomeserver) async { - if (newHomeserver == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Failed to parse homeserver URL. Are you sure you typed it correctly?", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - } isLoading.value = true; try { @@ -42,14 +33,26 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { newHomeserver = Uri.https(newHomeserver!.path); } - final newUrl = await ref - .read(ClientController.provider.notifier) - .discoverHomeserver(newHomeserver!); + final newUrl = newHomeserver == null + ? null + : await ref + .read(ClientController.provider.notifier) + .discoverHomeserver(newHomeserver); if (context.mounted) { - Future tryLogin(Uri url) async { + if (newUrl == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Homeserver verification failed. Is your homeserver down?", + style: .new(color: theme.colorScheme.onErrorContainer), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + } else { final codeResponse = await ref.watch( - AuthUrlController.provider(url).future, + AuthUrlController.provider(newUrl).future, ); await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); @@ -57,60 +60,33 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { AppLinks().uriLinkStream.listen((encodedUri) async { final state = encodedUri.queryParameters["state"]; if (state != codeResponse.state) return; - isLoading.value = true; - try { - final code = encodedUri.queryParameters["code"]; + final code = encodedUri.queryParameters["code"]; - if (code != null) { - await ref - .watch(ClientController.provider.notifier) - .exchangeToken( - .new( - homeserverUrl: url, - codeVerifier: codeResponse.codeVerifier, - redirectUri: .new( - scheme: "nexus.federated.nexus", - path: "/", - ), - code: code, - clientId: await ref.watch( - ClientIdController.provider(url).future, - ), + if (code != null) { + await ref + .watch(ClientController.provider.notifier) + .exchangeToken( + .new( + homeserverUrl: newUrl, + codeVerifier: codeResponse.codeVerifier, + redirectUri: .new( + scheme: "nexus.federated.nexus", + path: "/", ), - ); - } - } catch (error, stackTrace) { - showError(error, stackTrace); - rethrow; - } finally { - isLoading.value = false; + code: code, + clientId: await ref.watch( + ClientIdController.provider(newUrl).future, + ), + ), + ) + .onError(showError); } }); } - - if (newUrl == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Homeserver verification failed.", - style: .new(color: theme.colorScheme.onErrorContainer), - ), - action: SnackBarAction( - onPressed: () => tryLogin(newHomeserver!).onError(showError), - label: "Attempt log in anyways", - textColor: theme.colorScheme.onErrorContainer, - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - } else { - await tryLogin(newUrl); - } } } catch (error, stackTrace) { showError(error, stackTrace); - rethrow; } finally { isLoading.value = false; } @@ -134,9 +110,9 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { padding: .symmetric(vertical: 8, horizontal: 12), children: [ Row( - spacing: 12, children: [ - SvgPicture.asset("assets/bundled/icon.svg", width: 128), + SvgPicture.asset("assets/icon.svg", width: 128), + SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: .start, @@ -144,7 +120,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { Text("Nexus", style: theme.textTheme.displayMedium), Text( "A Simple Matrix Client", - style: theme.textTheme.titleLarge, + style: theme.textTheme.headlineMedium, overflow: .ellipsis, ), ], @@ -161,7 +137,6 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { child: TextField( textInputAction: .done, autofocus: true, - keyboardType: .url, onSubmitted: (text) => setHomeserver(.tryParse(text)), controller: homeserverUrl, decoration: .new( @@ -183,24 +158,26 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { ...([ .new( name: "Matrix.org", - description: "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", url: .https("matrix.org"), iconUrl: "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", ), .new( name: "Federated Nexus", - description: "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", url: .https("federated.nexus"), iconUrl: "https://federated.nexus/images/icon.png", ), - // TODO: Add back unredacted when they have MAS, find where new favicon is - // .new( - // name: "Unredacted", - // description: "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", - // url: .https("unredacted.org", "services/si/matrix"), - // iconUrl: "https://unredacted.org/favicon.ico", - // ), + .new( + name: "Unredacted", + description: + "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", + url: .https("unredacted.org", "services/si/matrix"), + iconUrl: "https://unredacted.org/favicon.ico", + ), ].map( (homeserver) => Card( child: ListTile( diff --git a/lib/widgets/pages/settings.dart b/lib/pages/settings.dart similarity index 81% rename from lib/widgets/pages/settings.dart rename to lib/pages/settings.dart index 50eb7f0..fa4d8da 100644 --- a/lib/widgets/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,6 +1,6 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_card_list/m3e_card_list.dart"; @@ -8,12 +8,14 @@ import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/controllers/settings_sections.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/show_about_dialog.dart"; -import "package:nexus/widgets/pages/settings_category.dart"; +import "package:nexus/pages/settings_category.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; -class const SettingsPage({super.key}) extends ConsumerWidget { +class SettingsPage extends ConsumerWidget { + const SettingsPage({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( builder: (_, constraints) => HookBuilder( @@ -140,12 +142,12 @@ class const SettingsPage({super.key}) extends ConsumerWidget { vertical: 8, ), margin: .symmetric(horizontal: 12), - color: Theme.of(context) - .colorScheme - .primaryContainer, + color: Theme.of( + context, + ).colorScheme.primaryContainer, itemCount: categories.length, - onTap: (index) => Navigator.of(context) - .push( + onTap: (index) => + Navigator.of(context).push( MaterialPageRoute( builder: (context) => SettingsCategoryPage( @@ -170,34 +172,30 @@ class const SettingsPage({super.key}) extends ConsumerWidget { ) : Row( children: [ - MaterialUiCompatibilityBridge( - child: NavigationRailM3E( - type: .alwaysExpand, - trailing: searchBar, - scrollable: true, - sections: sections - .mapTo( - ( - categoryGroup, - categories, - ) => NavigationRailM3ESection( - header: DividerText(categoryGroup), - destinations: categories - .map( - (category) => - NavigationRailM3EDestination( - icon: Icon(category.icon), - label: category.title, - ), - ) - .toList(), - ), - ) - .toList(), - selectedIndex: selected.value, - onDestinationSelected: (value) => - selected.value = value, - ), + NavigationRailM3E( + type: .alwaysExpand, + trailing: searchBar, + scrollable: true, + sections: sections + .mapTo( + (categoryGroup, categories) => + NavigationRailM3ESection( + header: DividerText(categoryGroup), + destinations: categories + .map( + (category) => + NavigationRailM3EDestination( + icon: Icon(category.icon), + label: category.title, + ), + ) + .toList(), + ), + ) + .toList(), + selectedIndex: selected.value, + onDestinationSelected: (value) => + selected.value = value, ), VerticalDivider(), Expanded( diff --git a/lib/widgets/pages/settings_category.dart b/lib/pages/settings_category.dart similarity index 91% rename from lib/widgets/pages/settings_category.dart rename to lib/pages/settings_category.dart index 8e94c56..b21d048 100644 --- a/lib/widgets/pages/settings_category.dart +++ b/lib/pages/settings_category.dart @@ -1,7 +1,7 @@ import "dart:async"; import "package:collection/collection.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/settings_sections.dart"; @@ -9,11 +9,11 @@ import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/widgets/highlight_wrapper.dart"; import "package:super_sliver_list/super_sliver_list.dart"; -class const SettingsCategoryPage( - final int index, { - final int? initialHighlight, - super.key, -}) extends HookConsumerWidget { +class SettingsCategoryPage extends HookConsumerWidget { + final int index; + final int? initialHighlight; + const SettingsCategoryPage(this.index, {this.initialHighlight, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final highlight = useState(initialHighlight); diff --git a/lib/widgets/pages/verify.dart b/lib/pages/verify.dart similarity index 90% rename from lib/widgets/pages/verify.dart rename to lib/pages/verify.dart index 44d276f..4eefa6c 100644 --- a/lib/widgets/pages/verify.dart +++ b/lib/pages/verify.dart @@ -1,12 +1,14 @@ -import "package:material_ui/material_ui.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.dart"; -import "package:nexus/widgets/pages/settings.dart"; +import "package:nexus/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/helpers/required_validator_helper.dart"; -class const VerifyPage({super.key}) extends HookConsumerWidget { +class VerifyPage extends HookConsumerWidget { + const VerifyPage({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final passphraseController = useTextEditingController(); @@ -41,8 +43,6 @@ class const VerifyPage({super.key}) extends HookConsumerWidget { ]), ), body: AlertDialog( - insetPadding: .symmetric(vertical: 12, horizontal: 32), - scrollable: true, title: Text("Verify"), content: Form( key: formKey.value, diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index a6489a2..9da03aa 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,20 +1,28 @@ import "dart:io"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/settings.dart"; import "package:window_manager/window_manager.dart"; -final class const Appbar({ - final Widget? leading, - final Widget? title, - final Color? backgroundColor, - final double? scrolledUnderElevation, - final IList actions = const .empty(), - final VoidCallback? onTap, - super.key, -}) extends ConsumerWidget implements PreferredSizeWidget { +class Appbar extends ConsumerWidget implements PreferredSizeWidget { + final Widget? leading; + final Widget? title; + final Color? backgroundColor; + final double? scrolledUnderElevation; + final IList actions; + final VoidCallback? onTap; + + const Appbar({ + super.key, + this.title, + this.onTap, + this.backgroundColor, + this.scrolledUnderElevation, + this.leading, + this.actions = const .empty(), + }); + @override Size get preferredSize => const .fromHeight(kToolbarHeight); @@ -39,7 +47,7 @@ final class const Appbar({ : (_) => windowManager.startDragging(), ), child: AppBar( - leading: leading == null ? null : InkWell(onTap: onTap, child: leading), + leading: InkWell(onTap: onTap, child: leading), backgroundColor: backgroundColor, scrolledUnderElevation: scrolledUnderElevation, actionsPadding: const .symmetric(horizontal: 8), @@ -51,17 +59,6 @@ final class const Appbar({ actions: [ ...actions, if (!(Platform.isAndroid || Platform.isIOS)) ...[ - if (ref - .watch(SettingsController.provider) - .whenOrNull( - data: (settings) => settings.showMinimizeButton, - ) ?? - false) - IconButton( - tooltip: "Minimize window", - onPressed: windowManager.minimize, - icon: const Icon(Icons.minimize), - ), if (!Platform.isLinux) IconButton( tooltip: "Maximize window", diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index bdaf657..75c08b9 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -1,30 +1,37 @@ import "package:color_hash/color_hash.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/mxc_image.dart"; -final class const AvatarOrHash( - final Uri? avatar, - final String title, { - final Widget? fallback, - final double dimension = 24, - super.key, -}) extends ConsumerWidget { +class AvatarOrHash extends ConsumerWidget { + final Uri? avatar; + final String title; + final Widget? fallback; + final double height; + const AvatarOrHash( + this.avatar, + this.title, { + this.fallback, + this.height = 24, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final box = ColoredBox( color: ColorHash(title).color, - child: Center(child: Icon(Icons.person, size: dimension / 2)), + child: Center(child: Icon(Icons.person, size: height / 2)), ); return SizedBox( - width: dimension, - height: dimension, + width: height, + height: height, child: Center( child: ClipRRect( - borderRadius: .all(.circular((dimension - 8) / 2.5)), - child: SizedBox.square( - dimension: dimension, + borderRadius: .all(.circular((height - 8) / 2.5)), + child: SizedBox( + width: height, + height: height, child: avatar == null ? fallback ?? box : Image( diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart index cc3a49a..281db0d 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -1,10 +1,7 @@ import "dart:io"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:file_selector/file_selector.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/widgets/emoji_picker.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:fluttertagger/fluttertagger.dart"; @@ -13,95 +10,64 @@ import "package:nexus/controllers/attachment.dart"; import "package:nexus/controllers/image_picker.dart"; import "package:nexus/controllers/power_level.dart"; import "package:nexus/models/content/message.dart"; +import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/composer/overlays/tagger_overlay.dart"; +import "package:nexus/widgets/composer/mention_overlay.dart"; import "package:nexus/widgets/composer/relation_preview.dart"; import "package:nexus/main.dart"; +import "package:nexus/widgets/emote_picker.dart"; -class const Composer( - final String roomId, { - required final int? relatedEvent, - required final RelationType relationType, - required final VoidCallback onDismiss, - required final Future Function( +class Composer extends HookConsumerWidget { + final String roomId; + final Event? relatedEvent; + final RelationType relationType; + final VoidCallback onDismiss; + final FocusNode? node; + final Future Function( String text, { required bool shouldMention, required IList tags, }) - onSend, - final FocusNode? node, - super.key, -}) extends HookConsumerWidget { + onSend; + const Composer( + this.roomId, { + required this.relatedEvent, + 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 = useMemoized(FlutterTaggerController.new); + final controller = useRef(FlutterTaggerController()); final triggerCharacter = useState(""); final shouldMention = useState(true); final query = useState(""); - final relatedEvent = this.relatedEvent == null - ? null - : ref.watch( - RoomsController.provider.select((rooms) { - final event = rooms[roomId]?.events[this.relatedEvent!]; - if (event == null) return null; - final edited = event.lastEditRowId == 0 - ? null - : rooms[roomId]?.events[event.lastEditRowId]; - - return edited == null - ? event - : event.copyWith( - content: edited.content, - localContent: edited.localContent, - ); - }), - ); - - final textBefore = useState(null); - - useEffect(() { - if (relationType == .edit && relatedEvent != null) { - textBefore.value ??= controller.text; - - final text = - relatedEvent.localContent?.editSource ?? - switch (relatedEvent.content) { - TextMessageContent(:final body) => body, - _ => "", - }; - - controller.value = .new( - text: text, - selection: .collapsed(offset: text.length), - ); - } else if (textBefore.value case final draft?) { - controller.value = .new( - text: draft, - selection: .collapsed(offset: draft.length), - ); - textBefore.value = null; - } - - return null; - }, [relationType, relatedEvent == null]); + if (relationType == .edit && controller.value.text.isEmpty) { + controller.value.text = + relatedEvent?.localContent?.editSource ?? + switch (relatedEvent?.content) { + TextMessageContent(:final body) => body, + _ => "", + }; + } final attachment = ref.watch(AttachmentController.provider(roomId)); void send() { - if (controller.value.text.isEmpty && attachment == null || - attachment != null && attachment.$2 == null) { - return; - } + if (controller.value.text.isEmpty && attachment == null) return; onSend( - controller.formattedText, + controller.value.formattedText, shouldMention: shouldMention.value, - tags: .new(controller.tags), + tags: .new(controller.value.tags), ); onDismiss(); - controller.text = ""; + controller.value.text = ""; } final style = TextStyle( @@ -146,7 +112,9 @@ class const Composer( ), Container( color: theme.colorScheme.surfaceContainerHighest, + padding: .symmetric(horizontal: 8), child: Row( + spacing: 8, mainAxisAlignment: .center, children: ref.watch( @@ -156,18 +124,34 @@ class const Composer( ) ? [ IconButton( - onPressed: () => showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => EmojiPicker( - onSelection: (value) { - Navigator.of(context).pop(); - controller.text += value; + icon: Icon(Icons.emoji_emotions), + tooltip: "Insert emoji", + onPressed: () { + showEmotePickerBottomSheet( + context: context, + roomId: roomId, + onSelect: (emojiId) { + final textController = controller.value; + final selection = textController.selection; + final newText = textController.text + .replaceRange( + selection.start, + selection.end, + emojiId, + ); + + textController.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed( + offset: + selection.start + emojiId.length, + ), + ); + node?.requestFocus(); }, - ), - ), - icon: Icon(Icons.emoji_emotions), + ); + }, ), PopupMenuButton( tooltip: "Add media", @@ -180,9 +164,10 @@ class const Composer( leading: Icon(Icons.add_a_photo), ), onTap: () async => ref - .read( - AttachmentController.provider(roomId) - .notifier, + .watch( + AttachmentController.provider( + roomId, + ).notifier, ) .add( (await ref @@ -199,9 +184,10 @@ class const Composer( leading: Icon(Icons.add_photo_alternate), ), onTap: () async => ref - .read( - AttachmentController.provider(roomId) - .notifier, + .watch( + AttachmentController.provider( + roomId, + ).notifier, ) .add( (await ref @@ -215,8 +201,9 @@ class const Composer( PopupMenuItem( onTap: () async => ref .watch( - AttachmentController.provider(roomId) - .notifier, + AttachmentController.provider( + roomId, + ).notifier, ) .add((await openFile())!) .onError(showError), @@ -228,20 +215,19 @@ class const Composer( ], icon: Icon(Icons.add), ), - SizedBox(width: 8), Expanded( child: FlutterTagger( triggerStrategy: .eager, - overlay: TaggerOverlay( - query.value, - roomId: roomId, + overlay: MentionOverlay( + roomId, + query: query.value, triggerCharacter: triggerCharacter.value, addTag: ({required id, required name}) { - controller.addTag(id: id, name: name); + controller.value.addTag(id: id, name: name); node?.requestFocus(); }, ), - controller: controller, + controller: controller.value, onSearch: (newQuery, newTriggerCharacter) { triggerCharacter.value = newTriggerCharacter; query.value = newQuery; @@ -249,7 +235,6 @@ class const Composer( triggerCharacterAndStyles: { "@": style, "#": style, - ":": style, }, builder: (context, key) => Focus( onKeyEvent: (_, event) { @@ -271,16 +256,12 @@ class const Composer( child: TextField( maxLines: 12, minLines: 1, - textCapitalization: .sentences, - autofocus: - (Platform.isLinux || - Platform.isMacOS || - Platform.isWindows), + autofocus: true, decoration: .new( hintText: "Your message here...", border: .none, ), - controller: controller, + controller: controller.value, key: key, focusNode: node, ), diff --git a/lib/widgets/composer/mention_overlay.dart b/lib/widgets/composer/mention_overlay.dart new file mode 100644 index 0000000..ca4f95a --- /dev/null +++ b/lib/widgets/composer/mention_overlay.dart @@ -0,0 +1,158 @@ +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/members_by_status.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/via.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_localpart.dart"; +import "package:nexus/models/content/membership.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 String roomId; + final void Function({required String id, required String name}) addTag; + const MentionOverlay( + this.roomId, { + 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: .all(8), + child: ClipRRect( + borderRadius: .all(.circular(12)), + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + padding: .all(8), + child: switch (triggerCharacter) { + "@" => + ref + .watch( + MembersByStatusController.provider( + .new(roomId: roomId, status: .join), + ), + ) + .betterWhen( + data: (members) => ListView( + children: + (query.isEmpty + ? members + : members.where( + (member) => + member.stateKey + ?.toLowerCase() + .contains( + query.toLowerCase(), + ) == + true || + switch (member.content) { + MembershipContent( + :final displayName, + ) => + displayName + ?.toLowerCase() + .contains( + query.toLowerCase(), + ) == + true, + _ => false, + }, + )) + .map( + (member) => switch (member.content) { + MembershipContent( + :final displayName, + :final avatarUrl, + ) => + Material( + color: Colors.transparent, + child: ListTile( + leading: AvatarOrHash( + avatarUrl, + displayName ?? + member.stateKey!.localpart, + ), + title: Text( + displayName ?? + member.stateKey!.localpart, + ), + subtitle: Text(member.stateKey!), + onTap: () => addTag( + id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})", + name: member.stateKey!.localpart, + ), + ), + ), + _ => SizedBox.shrink(), + }, + ) + .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 ?? + "Unknown Room"; + return Material( + color: Colors.transparent, + child: 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/composer/overlays/emoji_overlay.dart b/lib/widgets/composer/overlays/emoji_overlay.dart deleted file mode 100644 index 0e762c0..0000000 --- a/lib/widgets/composer/overlays/emoji_overlay.dart +++ /dev/null @@ -1,47 +0,0 @@ -import "package:collection/collection.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_emoji_picker/material_emoji_picker.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; - -class const EmojiOverlay( - final String query, { - required final String roomId, - required final void Function({required String id, required String name}) - addTag, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) => ref - .watch(EmojiController.provider) - .betterWhen( - data: (emojis) => ListView( - children: emojis - .map((element) => element.emojis) - .flattened - .where( - (emoji) => - emoji.aliases.join().contains(query) || - emoji.description.contains(query) || - emoji.tags.join().contains(query), - ) - .map( - (emoji) => ListTile( - leading: SizedBox.square( - dimension: 28, - child: FittedBox(child: emoji.widget), - ), - title: Text(emoji.aliases.first), - subtitle: Text(emoji.description), - onTap: () => addTag( - id: Uri.tryParse(emoji.value)?.scheme == "mxc" - ? "" // TODO: Handle custom emotes - : emoji.value, - name: emoji.aliases.first, - ), - ), - ) - .toList(), - ), - ); -} diff --git a/lib/widgets/composer/overlays/room_overlay.dart b/lib/widgets/composer/overlays/room_overlay.dart deleted file mode 100644 index b5af177..0000000 --- a/lib/widgets/composer/overlays/room_overlay.dart +++ /dev/null @@ -1,58 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; - -class const RoomOverlay( - final String query, { - required final void Function({required String id, required String name}) - addTag, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final rooms = ref.watch(RoomsController.provider); - return 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 ?? - "Unknown Room"; - 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(), - ); - } -} diff --git a/lib/widgets/composer/overlays/tagger_overlay.dart b/lib/widgets/composer/overlays/tagger_overlay.dart deleted file mode 100644 index 7d973dc..0000000 --- a/lib/widgets/composer/overlays/tagger_overlay.dart +++ /dev/null @@ -1,39 +0,0 @@ -import "package:material_ui/material_ui.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/widgets/composer/overlays/room_overlay.dart"; -import "package:nexus/widgets/composer/overlays/user_overlay.dart"; -import "package:nexus/widgets/composer/overlays/emoji_overlay.dart"; -import "package:nexus/widgets/loading.dart"; - -class const TaggerOverlay( - final String query, { - required final String roomId, - required final void Function({required String id, required String name}) - addTag, - required final String? triggerCharacter, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - return Padding( - padding: .all(8), - child: ClipRRect( - borderRadius: .all(.circular(12)), - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - padding: .all(8), - child: Material( - color: Colors.transparent, - child: switch (triggerCharacter) { - "@" => UserOverlay(query, roomId: roomId, addTag: addTag), - "#" => RoomOverlay(query, addTag: addTag), - ":" => EmojiOverlay(query, roomId: roomId, addTag: addTag), - - _ => Loading(), - }, - ), - ), - ), - ); - } -} diff --git a/lib/widgets/composer/overlays/user_overlay.dart b/lib/widgets/composer/overlays/user_overlay.dart deleted file mode 100644 index fe4f70f..0000000 --- a/lib/widgets/composer/overlays/user_overlay.dart +++ /dev/null @@ -1,64 +0,0 @@ -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/members_by_status.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; - -class const UserOverlay( - final String query, { - required final String roomId, - required final void Function({required String id, required String name}) - addTag, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) => ref - .watch( - MembersByStatusController.provider(.new(roomId: roomId, status: .join)), - ) - .betterWhen( - data: (members) => ListView( - children: - (query.isEmpty - ? members - : members.where( - (member) => - member.stateKey?.toLowerCase().contains( - query.toLowerCase(), - ) == - true || - switch (member.content) { - MembershipContent(:final displayName) => - displayName?.toLowerCase().contains( - query.toLowerCase(), - ) == - true, - _ => false, - }, - )) - .map( - (member) => switch (member.content) { - MembershipContent(:final displayName, :final avatarUrl) => - ListTile( - leading: AvatarOrHash( - avatarUrl, - displayName ?? member.stateKey!.localpart, - ), - title: Text( - displayName ?? member.stateKey!.localpart, - ), - subtitle: Text(member.stateKey!), - onTap: () => addTag( - id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})", - name: member.stateKey!.localpart, - ), - ), - _ => SizedBox.shrink(), - }, - ) - .toList(), - ), - ); -} diff --git a/lib/widgets/composer/relation_preview.dart b/lib/widgets/composer/relation_preview.dart index fca9de6..c9cc271 100644 --- a/lib/widgets/composer/relation_preview.dart +++ b/lib/widgets/composer/relation_preview.dart @@ -1,17 +1,25 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/event_preview.dart"; -class const RelationPreview( - final Event? relatedEvent, { - required final RelationType relationType, - required final VoidCallback onDismiss, - required final bool shouldMention, - required final VoidCallback toggleShouldMention, - super.key, -}) extends ConsumerWidget { +class RelationPreview extends ConsumerWidget { + final Event? relatedEvent; + final RelationType relationType; + final VoidCallback onDismiss; + final bool shouldMention; + final VoidCallback toggleShouldMention; + + const RelationPreview( + this.relatedEvent, { + required this.relationType, + required this.onDismiss, + required this.shouldMention, + required this.toggleShouldMention, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { if (relatedEvent == null) return SizedBox.shrink(); diff --git a/lib/widgets/divider_text.dart b/lib/widgets/divider_text.dart index c44ae11..2b0f9bd 100644 --- a/lib/widgets/divider_text.dart +++ b/lib/widgets/divider_text.dart @@ -1,8 +1,11 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:nexus/widgets/divider_widget.dart"; -final class const DividerText(final String text, {super.key}) - extends StatelessWidget { +class DividerText extends StatelessWidget { + final String text; + + const DividerText(this.text, {super.key}); + @override Widget build(BuildContext context) => DividerWidget(Text(text, style: Theme.of(context).textTheme.labelLarge)); diff --git a/lib/widgets/divider_widget.dart b/lib/widgets/divider_widget.dart index 5e4972c..6f13bd4 100644 --- a/lib/widgets/divider_widget.dart +++ b/lib/widgets/divider_widget.dart @@ -1,7 +1,9 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class DividerWidget extends StatelessWidget { + final Widget widget; + const DividerWidget(this.widget, {super.key}); -final class const DividerWidget(final Widget widget, {super.key}) - extends StatelessWidget { @override Widget build(BuildContext context) => LayoutBuilder( builder: (_, constraints) => Row( diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart deleted file mode 100644 index 453213a..0000000 --- a/lib/widgets/emoji_picker.dart +++ /dev/null @@ -1,56 +0,0 @@ -import "dart:async"; - -import "package:collection/collection.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_emoji_picker/material_emoji_picker.dart" as upstream; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/recent_emoji.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/main.dart"; - -class const EmojiPicker({ - required final FutureOr Function(String value) onSelection, - final bool allowFreeText = false, - super.key, -}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final recentEmoji = ref.watch(RecentEmojiController.provider); - - return ref - .watch(upstream.EmojiController.provider) - .betterWhen( - data: (categories) => upstream.EmojiPicker( - onSelection: (emoji, isFreeText) async { - await onSelection(emoji); - - if (!isFreeText) { - await ref - .watch(RecentEmojiController.provider.notifier) - .add(emoji) - .onError(showError); - } - }, - allowFreeText: allowFreeText, - prependCategories: .new([ - .new( - icon: Icon(Icons.history), - name: "Recent", - emojis: .new( - recentEmoji - .map( - (recent) => categories - .map((element) => element.emojis) - .flattened - .firstWhereOrNull( - (element) => element.value == recent.emoji, - ), - ) - .nonNulls, - ), - ), - ]), - ), - ); - } -} diff --git a/lib/widgets/emote_picker.dart b/lib/widgets/emote_picker.dart new file mode 100644 index 0000000..881526c --- /dev/null +++ b/lib/widgets/emote_picker.dart @@ -0,0 +1,399 @@ +import "dart:math" as math; + +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter/rendering.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/emoji.dart"; + +class EmotePicker extends HookConsumerWidget { + final String? roomId; + final void Function(String emoteId) onSelect; + final VoidCallback? onClose; + + const EmotePicker({ + super.key, + this.roomId, + this.onClose, + required this.onSelect, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, + minHeight: math.min(MediaQuery.of(context).size.height * 0.6, 400), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Divider(height: 1), + // Content + Expanded( + child: ListView( + children: [ + EmoteCategoryList( + roomId: roomId, + onSelect: onSelect, + fetchSections: (rid, query) => + fetchEmojiSections(ref, rid, query), + placeholderText: "emojis & emotes", + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class EmoteCategoryList extends HookWidget { + final String? roomId; + final void Function(String) onSelect; + final Future> Function(String? roomId, String query) + fetchSections; + final String placeholderText; + + const EmoteCategoryList({ + super.key, + required this.roomId, + required this.onSelect, + required this.fetchSections, + required this.placeholderText, + }); + + @override + Widget build(BuildContext context) { + final searchController = useTextEditingController(text: ""); + final searchFocus = useFocusNode(); + final searchText = useState(""); + + useEffect(() { + void listener() { + searchText.value = searchController.text; + } + + searchController.addListener(listener); + return () => searchController.removeListener(listener); + }, [searchController]); + + final isSearching = searchText.value.isNotEmpty; + final sectionsFuture = useMemoized( + () => fetchSections(roomId, searchText.value), + [searchText.value], + ); + final sectionsSnapshot = useFuture(sectionsFuture); + final sections = sectionsSnapshot.data ?? [].toIList(); + + final scrollController = useScrollController(); + + final sectionKeysMap = useState({}); + + final activeCategoryId = useState(null); + + useEffect(() { + if (sections.isEmpty) return null; + + bool active = true; + void listener() { + if (!active || !scrollController.hasClients) return; + + final scrollPixels = scrollController.offset; + String? current; + + for (final section in sections) { + final key = sectionKeysMap.value.putIfAbsent( + section.category, + () => GlobalKey(), + ); + if (key.currentContext?.findRenderObject() == null) continue; + + final ro = key.currentContext!.findRenderObject()!; + if (!ro.attached) continue; + + try { + final vp = RenderAbstractViewport.of(ro); + final reveal = vp.getOffsetToReveal(ro, 0.0).offset; + + if (reveal <= scrollPixels + 80) { + current = section.category; + } else { + break; + } + } catch (_) { + continue; + } + } + + if (current != null && current != activeCategoryId.value) { + activeCategoryId.value = current; + } + } + + scrollController.addListener(listener); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (active) listener(); + }); + + return () { + active = false; + scrollController.removeListener(listener); + }; + }, [sections, scrollController]); + + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: TextField( + controller: searchController, + focusNode: searchFocus, + decoration: InputDecoration( + hintText: "Search $placeholderText...", + prefixIcon: Icon(Icons.search), + suffixIcon: isSearching + ? IconButton( + icon: Icon(Icons.clear), + onPressed: () { + searchController.clear(); + searchFocus.requestFocus(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + Expanded( + child: Row( + children: [ + if (!isSearching) ...[ + SizedBox( + width: 100, + child: ListView.builder( + itemCount: sections.length, + itemBuilder: (context, index) { + final section = sections[index]; + final isActive = + activeCategoryId.value == section.category; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + activeCategoryId.value = section.category; + final key = sectionKeysMap.value[section.category]; + if (key?.currentContext != null) { + Scrollable.ensureVisible( + key!.currentContext!, + alignment: 0.0, + duration: const Duration(milliseconds: 250), + ); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + width: 2, + color: isActive + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + ), + ), + color: isActive + ? Theme.of(context).colorScheme.primary + .withValues(alpha: 0.08) + : Colors.transparent, + ), + child: Text( + section.isRecent ? "Recent" : section.label, + style: TextStyle( + fontSize: 13, + fontWeight: isActive ? FontWeight.w600 : null, + color: isActive + ? Theme.of(context).colorScheme.primary + : Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + }, + ), + ), + const VerticalDivider(width: 1), + ], + Expanded( + child: + sectionsSnapshot.connectionState == ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : sections.isEmpty + ? Center(child: Text("No $placeholderText found")) + : CustomScrollView( + controller: scrollController, + slivers: [ + for (final section in sections) ...[ + SliverToBoxAdapter( + key: sectionKeysMap.value.putIfAbsent( + section.category, + () => GlobalKey(), + ), + child: Container( + padding: const EdgeInsets.only( + left: 12, + top: 12, + bottom: 8, + ), + child: Text( + section.isRecent + ? "Recently Used" + : section.label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 44, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + delegate: SliverChildBuilderDelegate(( + context, + index, + ) { + final item = section.items[index]; + return GestureDetector( + onTap: () => onSelect(item.id), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 6, + ), + ), + child: Text( + item.display, + style: const TextStyle(fontSize: 24), + ), + ), + ); + }, childCount: section.items.length), + ), + ), + ], + const SliverPadding( + padding: EdgeInsets.only(bottom: 24), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +Future> fetchEmojiSections( + WidgetRef ref, + String? roomId, + String query, +) async { + final emotes = await ref.watch(EmojiController.provider.future); + final q = query.toLowerCase(); + + if (q.isEmpty) { + return emotes; + } + + return emotes + .map((cat) { + final filtered = cat.items + .where( + (e) => + e.keywords.any((k) => k.contains(q)) || e.id.contains(query), + ) + .toList(); + return filtered.isNotEmpty + ? EmoteSection( + category: cat.category, + label: cat.label, + icon: cat.icon, + items: filtered, + isRecent: cat.isRecent, + ) + : null; + }) + .whereType() + .toIList(); +} + +Future showEmotePickerBottomSheet({ + required BuildContext context, + String? roomId, + required void Function(String) onSelect, +}) async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + useSafeArea: true, + enableDrag: true, + isDismissible: true, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.75, + maxWidth: 600, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) => EmotePicker(roomId: roomId, onSelect: onSelect), + ); +} diff --git a/lib/widgets/error_dialog.dart b/lib/widgets/error_dialog.dart index ddc4c98..9b62200 100644 --- a/lib/widgets/error_dialog.dart +++ b/lib/widgets/error_dialog.dart @@ -1,13 +1,13 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/misc.dart"; -final class const ErrorDialog( - final Object error, - final StackTrace? stackTrace, { - final ProviderOrFamily? provider, - super.key, -}) extends ConsumerWidget { +class ErrorDialog extends ConsumerWidget { + final Object error; + final StackTrace? stackTrace; + final ProviderOrFamily? provider; + const ErrorDialog(this.error, this.stackTrace, {this.provider, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { return AlertDialog( diff --git a/lib/widgets/event_preview.dart b/lib/widgets/event_preview.dart index 1b88fa8..7a40a75 100644 --- a/lib/widgets/event_preview.dart +++ b/lib/widgets/event_preview.dart @@ -1,35 +1,32 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/event.dart"; -class const EventPreview(final Event event, {super.key}) - extends StatelessWidget { +class EventPreview extends StatelessWidget { + final Event event; + const EventPreview(this.event, {super.key}); + @override Widget build(BuildContext context) => IgnorePointer( child: Padding( padding: .symmetric(vertical: 4), child: Row( mainAxisSize: .min, - spacing: 8, + spacing: 12, children: [ - if (event.content is MessageContent) MessageAvatar(event, height: 36), + if (event.content is MessageContent) MessageAvatar(event), Flexible( - child: Column( - mainAxisAlignment: .center, - crossAxisAlignment: .start, + child: Wrap( + crossAxisAlignment: .center, + spacing: 8, + runSpacing: 2, children: [ - if (event.content is MessageContent) - MessageDisplayname(event, shouldWrap: false), - EventRenderer( - event.rowId, - roomId: event.roomId, - textOnly: true, - maxLines: 1, - ), + if (event.content is MessageContent) MessageDisplayname(event), + EventRenderer(event, textOnly: true, maxLines: 1), ], ), ), diff --git a/lib/widgets/expandable_image.dart b/lib/widgets/expandable_image.dart index 0639417..ee4f900 100644 --- a/lib/widgets/expandable_image.dart +++ b/lib/widgets/expandable_image.dart @@ -1,15 +1,15 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_buttons/m3e_buttons.dart"; import "package:nexus/helpers/mxc_image.dart"; import "package:nexus/models/requests/download_media.dart"; import "package:nexus/widgets/error_dialog.dart"; -final class const ExpandableImage( - final DownloadMediaRequest? request, { - required final Widget child, - super.key, -}) extends ConsumerWidget { +class ExpandableImage extends ConsumerWidget { + final Widget child; + final DownloadMediaRequest? request; + const ExpandableImage(this.request, {required this.child, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => InkWell( onTap: request == null diff --git a/lib/widgets/file_card.dart b/lib/widgets/file_card.dart index 4c0fdde..afdad89 100644 --- a/lib/widgets/file_card.dart +++ b/lib/widgets/file_card.dart @@ -1,13 +1,13 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:nexus/helpers/extensions/size_to_string.dart"; import "package:nexus/models/info/file.dart"; -final class const FileCard( - final Uri uri, - final FileInfo? info, { - final String? filename, - super.key, -}) extends StatelessWidget { +class FileCard extends StatelessWidget { + final Uri uri; + final FileInfo? info; + final String? filename; + const FileCard(this.uri, this.info, {this.filename, super.key}); + @override Widget build(BuildContext context) => SizedBox( width: 320, diff --git a/lib/widgets/highlight_wrapper.dart b/lib/widgets/highlight_wrapper.dart index b10e3ff..c7e568e 100644 --- a/lib/widgets/highlight_wrapper.dart +++ b/lib/widgets/highlight_wrapper.dart @@ -1,10 +1,10 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class HighlightWrapper extends StatelessWidget { + final Widget child; + final bool isHighlighted; + const HighlightWrapper(this.child, {this.isHighlighted = false, super.key}); -final class const HighlightWrapper( - final Widget child, { - final bool isHighlighted = false, - super.key, -}) extends StatelessWidget { @override Widget build(BuildContext context) => ClipRRect( borderRadius: .all(.circular(12)), diff --git a/lib/widgets/html/code_block.dart b/lib/widgets/html/code_block.dart index 1881bc6..a5c3dee 100644 --- a/lib/widgets/html/code_block.dart +++ b/lib/widgets/html/code_block.dart @@ -1,13 +1,12 @@ import "dart:math"; -import "package:flutter/services.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class CodeBlock extends StatelessWidget { + final String code; + final String lang; + const CodeBlock(this.code, {required this.lang, super.key}); -class const CodeBlock( - final String code, { - required final String lang, - super.key, -}) extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -25,11 +24,11 @@ class const CodeBlock( padding: .symmetric(horizontal: 8), child: Text( lang.substring(0, min(lang.length, 15)), - style: .new(fontFamily: "bundled-mono"), + style: .new(fontFamily: "monospace"), ), ), TextButton.icon( - onPressed: () => Clipboard.setData(.new(text: code)), + onPressed: () {}, icon: Icon(Icons.copy), label: Text("Copy"), ), @@ -44,7 +43,7 @@ class const CodeBlock( code, minLines: 1, maxLines: 99, - style: .new(fontFamily: "bundled-mono"), + style: .new(fontFamily: "monospace"), ), ), ), diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 7c99471..5437285 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.dart @@ -1,8 +1,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.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/helpers/extensions/get_link_info.dart"; +import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/mxc_image.dart"; import "package:nexus/widgets/expandable_image.dart"; @@ -11,154 +11,130 @@ import "package:nexus/widgets/html/spoiler_text.dart"; import "package:nexus/widgets/html/code_block.dart"; import "package:nexus/widgets/html/quoted.dart"; -class const Html( - final String html, { - final String? roomId, - final TextStyle? textStyle, - super.key, -}) extends ConsumerWidget { +class Html extends ConsumerWidget { + final String html; + final String? roomId; + final TextStyle? textStyle; + const Html(this.html, {this.roomId, this.textStyle, super.key}); + @override - Widget - build(BuildContext context, WidgetRef ref) - // needed until https://github.com/daohoangson/flutter_widget_from_html/issues/1618 is resolved - => MaterialUiCompatibilityBridge( - child: HtmlWidget( - html, - buildAsync: false, - textStyle: textStyle, - customWidgetBuilder: (element) { - if (element.attributes.keys.contains("data-mx-profile-fallback")) { - return SizedBox.shrink(); - } + Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( + html, + buildAsync: false, + 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(element.text)); - } + 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 width = int.tryParse(element.attributes["width"] ?? ""); - final src = Uri.tryParse(element.attributes["src"] ?? ""); + final height = + int.tryParse(element.attributes["height"] ?? "") ?? + (element.attributes.keys.contains("data-mx-emoticon") ? 32 : null) ?? + 300; + final width = int.tryParse(element.attributes["width"] ?? ""); + final src = Uri.tryParse(element.attributes["src"] ?? ""); - return switch (element.localName) { - "code" => - element.parent?.localName == "pre" - ? CodeBlock( - element.text, - lang: element.className.replaceAll("language-", ""), - ) - : InlineCustomWidget( - child: ClipRRect( - borderRadius: .all(.circular(4)), - child: Container( - padding: .all(8), - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, - child: SelectableText( - element.text, - style: .new(fontFamily: "bundled-mono"), - ), - ), - ), - ), + return switch (element.localName) { + "code" => + element.parent?.localName == "pre" + ? CodeBlock( + element.text, + lang: element.className.replaceAll("language-", ""), + ) + : null, - "blockquote" => Quoted( - Html(element.innerHtml, textStyle: textStyle, roomId: roomId), - ), - - "a" => - element.attributes["href"]?.linkInfo == null - ? null - : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!, roomId), - ), - - "img" => - src == null - ? SizedBox.shrink() - : InlineCustomWidget( - alignment: PlaceholderAlignment.middle, - child: ExpandableImage( - .new(mxc: src), - child: Image( - image: MxcImage(ref, .new(mxc: src)), - errorBuilder: (_, error, _) => Text( - "Image Failed to Load", - style: .new( - color: Theme.of(context).colorScheme.error, - ), - ), - height: height.toDouble(), - width: width?.toDouble(), - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null - ? child - : CircularProgressIndicator(), - ), - ), - ), - - // Allowed elements list - ("del" || - "h1" || - "h2" || - "h3" || - "h4" || - "h5" || - "h6" || - "p" || - "ul" || - "ol" || - "sup" || - "sub" || - "li" || - "b" || - "i" || - "u" || - "strong" || - "em" || - "s" || - "code" || - "hr" || - "br" || - "div" || - "table" || - "thead" || - "tbody" || - "tr" || - "th" || - "td" || - "caption" || - "pre" || - "span" || - "details" || - "summary") => - null, - - _ => SizedBox.shrink(), - }; - }, - customStylesBuilder: (element) => { - "width": "auto", - ...Map.fromEntries( - element.attributes - .mapTo?>( - (key, value) => switch (key) { - "data-mx-color" => .new("color", value), - "data-mx-bg-color" => .new("background-color", value), - _ => null, - }, - ) - .nonNulls, + "blockquote" => Quoted( + Html(element.innerHtml, textStyle: textStyle, roomId: roomId), ), - }, - onTapUrl: (url) => - ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), - ), + + "a" => + element.attributes["href"]?.mention == null + ? null + : InlineCustomWidget( + child: MentionChip(element.attributes["href"]!, roomId), + ), + + "img" => + src == null + ? SizedBox.shrink() + : InlineCustomWidget( + alignment: PlaceholderAlignment.middle, + child: ExpandableImage( + .new(mxc: src), + child: Image( + image: MxcImage(ref, .new(mxc: src)), + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: .new(color: Theme.of(context).colorScheme.error), + ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), + ), + ), + ), + + // Allowed elements list + ("del" || + "h1" || + "h2" || + "h3" || + "h4" || + "h5" || + "h6" || + "p" || + "ul" || + "ol" || + "sup" || + "sub" || + "li" || + "b" || + "i" || + "u" || + "strong" || + "em" || + "s" || + "code" || + "hr" || + "br" || + "div" || + "table" || + "thead" || + "tbody" || + "tr" || + "th" || + "td" || + "caption" || + "pre" || + "span" || + "details" || + "summary") => + null, + + _ => SizedBox.shrink(), + }; + }, + customStylesBuilder: (element) => { + "width": "auto", + ...Map.fromEntries( + element.attributes + .mapTo?>( + (key, value) => switch (key) { + "data-mx-color" => .new("color", value), + "data-mx-bg-color" => .new("background-color", value), + _ => null, + }, + ) + .nonNulls, + ), + }, + onTapUrl: (url) => ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), ); } diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart index 29e2310..f6105e3 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -1,95 +1,46 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/event.dart"; -import "package:nexus/controllers/room_summary.dart"; import "package:nexus/controllers/user.dart"; -import "package:nexus/helpers/extensions/focus_room.dart"; -import "package:nexus/helpers/extensions/get_link_info.dart"; +import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/models/content/membership.dart"; -import "package:nexus/models/room_summary.dart"; -import "package:nexus/widgets/room_summary_dialog.dart"; -class const MentionChip(final String content, final String? roomId, {super.key}) - extends ConsumerWidget { +class MentionChip extends ConsumerWidget { + final String? roomId; + final String content; + const MentionChip(this.content, this.roomId, {super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { - final linkInfo = content.linkInfo; - final mention = linkInfo?.identifier; - final data = switch (mention?.characters.firstOrNull) { - "@" => - ref - .watch( - UserController.provider(.new(roomId: roomId, userId: mention!)), - ) - .whenOrNull(data: (data) => data), - - "#" || "!" => - ref - .watch( - RoomSummaryController.provider(.new(roomIdOrAlias: mention!)), - ) - .whenOrNull(data: (data) => data) ?? - RoomSummary(roomId: mention), - - _ => null, - }; + final mention = content.mention; + final membership = mention?.startsWith("@") == true + ? ref + .watch( + UserController.provider(.new(roomId: roomId, userId: mention!)), + ) + .whenOrNull(data: (data) => data) + : null; return mention == null ? SizedBox.shrink() : InkWell( - onTap: () async { - if (data case MembershipContent membership) { + onTap: () { + if (membership != null) { context.showUserPopover(membership, mention, roomId: roomId); - } else if (data case RoomSummary summary) { - // TODO: This is an extra call that isn't truly needed, get rid of it - final eventRowId = linkInfo?.eventId == null - ? null - : await ref - .read( - EventController.provider( - .new( - roomId: summary.roomId, - eventId: linkInfo!.eventId!, - ), - ).selectAsync((data) => data?.rowId), - ) - .onError((_, _) => null); - - if (!await ref.focusRoom(summary.roomId, eventRowId) && - context.mounted) { - showDialog( - context: context, - builder: (context) => Consumer( - builder: (context, ref, _) => RoomSummaryDialog( - ref - .watch( - RoomSummaryController.provider( - .new(roomIdOrAlias: mention), - ), - ) - .whenOrNull(data: (data) => data) ?? - summary, - via: linkInfo?.via, - ), - ), - ); - } } }, - child: Text( - switch (data) { - RoomSummary summary => - "${(summary.name == null ? null : "#${summary.name}") ?? summary.canonicalAlias ?? summary.roomId}${linkInfo?.eventId == null ? "" : " > ${linkInfo?.eventId}"}", - MembershipContent membership => - membership.displayName == null - ? mention - : "@${membership.displayName}", - _ => mention, - }, - style: .new( - fontWeight: .bold, - color: Theme.of(context).colorScheme.primary, + child: IgnorePointer( + child: Chip( + label: Text( + (membership?.displayName == null + ? null + : "@${membership!.displayName}") ?? + mention, + style: .new( + fontWeight: .bold, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + backgroundColor: Theme.of(context).colorScheme.primary, ), ), ); diff --git a/lib/widgets/html/quoted.dart b/lib/widgets/html/quoted.dart index 3157263..e582b06 100644 --- a/lib/widgets/html/quoted.dart +++ b/lib/widgets/html/quoted.dart @@ -1,6 +1,9 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class Quoted extends StatelessWidget { + final Widget child; + const Quoted(this.child, {super.key}); -class const Quoted(final Widget child, {super.key}) extends StatelessWidget { @override Widget build(BuildContext context) => Container( decoration: BoxDecoration( diff --git a/lib/widgets/html/spoiler_text.dart b/lib/widgets/html/spoiler_text.dart index 92a5538..a7a457b 100644 --- a/lib/widgets/html/spoiler_text.dart +++ b/lib/widgets/html/spoiler_text.dart @@ -1,7 +1,11 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -class const SpoilerText(final String text, {super.key}) extends HookWidget { +class SpoilerText extends HookWidget { + final String text; + + const SpoilerText({super.key, required this.text}); + @override Widget build(BuildContext context) { final revealed = useState(false); diff --git a/lib/widgets/invites_list.dart b/lib/widgets/invites_list.dart deleted file mode 100644 index 35dd26f..0000000 --- a/lib/widgets/invites_list.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/models/nav_page.dart"; - -class const InvitesList({super.key}) - extends StatelessWidget - implements NavPage { - @override - String get title => "Invites"; - - @override - IconData get icon => Icons.inbox; - - // TODO: Show notification badge here - @override - Future? badgeBuilder(WidgetRef ref) => null; - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/widgets/join_dialog.dart b/lib/widgets/join_dialog.dart index b559023..50b36f2 100644 --- a/lib/widgets/join_dialog.dart +++ b/lib/widgets/join_dialog.dart @@ -1,40 +1,19 @@ -import "package:material_ui/material_ui.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/room_summary.dart"; -import "package:nexus/helpers/extensions/get_link_info.dart"; -import "package:nexus/widgets/room_summary_dialog.dart"; +import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/key.dart"; +import "package:nexus/controllers/spaces.dart"; +import "package:nexus/helpers/extensions/link_to_mention.dart"; + +class JoinDialog extends HookWidget { + final WidgetRef ref; + const JoinDialog(this.ref, {super.key}); -class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { @override Widget build(BuildContext context) { final roomAlias = useTextEditingController(); - - Future onJoin() async { - Navigator.of(context).pop(); - - final linkInfo = roomAlias.text.linkInfo; - final via = linkInfo?.via ?? const .empty(); - final roomIdOrAlias = linkInfo?.identifier ?? roomAlias.text; - - showDialog( - context: context, - builder: (context) => Consumer( - builder: (context, ref, _) => switch (ref.watch( - RoomSummaryController.provider( - .new(roomIdOrAlias: roomIdOrAlias, via: via), - ), - )) { - AsyncData(:final value) => RoomSummaryDialog(value, via: via), - AsyncError _ || AsyncLoading _ => RoomSummaryDialog( - .new(roomId: roomIdOrAlias), - via: via, - ), - }, - ), - ); - } - return AlertDialog( title: Text("Join a Room"), content: Column( @@ -45,14 +24,114 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { SizedBox(height: 12), TextField( controller: roomAlias, - decoration: .new(hintText: "#room:server.name"), - onEditingComplete: onJoin, + decoration: .new(hintText: "#room:server"), ), ], ), actions: [ TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - TextButton(onPressed: onJoin, child: Text("Join")), + 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( + .new( + content: Text("Joining room $roomIdOrAlias."), + duration: Duration(days: 999), + ), + ); + + try { + final id = await ref + .watch(ClientController.provider.notifier) + .joinRoom( + .new( + roomIdOrAlias: roomIdOrAlias, + via: .new( + Uri.tryParse( + roomAlias.text.replaceAll("/#", ""), + )?.queryParametersAll["via"] ?? + [], + ), + ), + ); + + snackbar.close(); + + scaffoldMessenger.showSnackBar( + .new( + content: Text("Room $roomIdOrAlias successfully joined."), + action: .new( + 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, + ) || + space.subSpaces.any( + (child) => + child.room.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( + .new( + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, + content: Text( + error.toString(), + style: .new( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ); + } + } + } + }, + child: Text("Join"), + ), ], ); } diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart index 50b612a..93c2554 100644 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ b/lib/widgets/lazy_loading/message_avatar.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/author.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; @@ -6,11 +6,11 @@ import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -class const MessageAvatar( - final Event event, { - final double height = 24, - super.key, -}) extends ConsumerWidget { +class MessageAvatar extends ConsumerWidget { + final Event event; + final double height; + const MessageAvatar(this.event, {this.height = 24, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( AuthorController.provider(event), @@ -21,9 +21,9 @@ class const MessageAvatar( child: AvatarOrHash( value.avatarUrl, value.displayName ?? event.sender.localpart, - dimension: height, + height: height, ), ), - _ => AvatarOrHash(null, event.sender.localpart, dimension: height), + _ => AvatarOrHash(null, event.sender.localpart, height: height), }; } diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index 593e755..bd5733c 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -1,61 +1,62 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/author.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; import "package:nexus/models/event.dart"; -import "package:nexus/models/content/membership.dart"; -class const MessageDisplayname( - final Event event, { - final TextStyle? style, - final bool clickable = true, - final bool shouldWrap = true, - super.key, -}) extends ConsumerWidget { +class MessageDisplayname extends ConsumerWidget { + final Event event; + final TextStyle? style; + final bool clickable; + const MessageDisplayname( + this.event, { + this.clickable = true, + this.style, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( AuthorController.provider(event), )) { - AsyncData(:final MembershipContent? value) || - AsyncLoading(:final value) || - AsyncError(:final value) => InkWell( - onTap: clickable && value != null + AsyncData(:final value) || AsyncLoading(:final value?) => InkWell( + onTap: clickable ? () => context.showUserPopover( value, event.sender, roomId: event.roomId, ) : null, - child: switch ([ - Text( - value?.displayName ?? event.sender.localpart, - style: - style ?? .new(color: event.sender.colorHash, fontWeight: .bold), - maxLines: 1, - overflow: .ellipsis, - ), - if (event.pmp != null) + child: Wrap( + spacing: 4, + crossAxisAlignment: .center, + children: [ Text( - "(via ${event.sender})", - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: event.sender.colorHash, fontWeight: .bold), + value.displayName ?? event.sender.localpart, + style: + style ?? .new(color: event.sender.colorHash, fontWeight: .bold), maxLines: 1, overflow: .ellipsis, ), - ]) { - final children when shouldWrap => Wrap( - spacing: 4, - crossAxisAlignment: .center, - children: children, - ), - final children => Row( - spacing: 4, - mainAxisSize: .min, - children: [for (final child in children) Flexible(child: child)], - ), - }, + + if (event.pmp != null) + Text( + "(via ${event.sender})", + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: event.sender.colorHash, + fontWeight: .bold, + ), + maxLines: 1, + overflow: .ellipsis, + ), + ], + ), + ), + _ => Text( + event.sender.localpart, + style: .new(color: event.sender.colorHash, fontWeight: .bold), ), }; } diff --git a/lib/widgets/linkified_text.dart b/lib/widgets/linkified_text.dart index 77ba7a4..653248c 100644 --- a/lib/widgets/linkified_text.dart +++ b/lib/widgets/linkified_text.dart @@ -1,14 +1,14 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_linkify/flutter_linkify.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/launch_helper.dart"; -final class const LinkifiedText( - final String text, { - final int? maxLines, - final TextStyle? style, - super.key, -}) extends ConsumerWidget { +class LinkifiedText extends ConsumerWidget { + final String text; + final int? maxLines; + final TextStyle? style; + const LinkifiedText(this.text, {this.maxLines, this.style, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => Linkify( text: text, diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart index e92860e..fc84563 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -1,15 +1,14 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class Loading extends StatelessWidget { + final double? height; + const Loading({this.height, super.key}); -class const Loading({super.key, final double? dimension}) - extends StatelessWidget { @override Widget build(BuildContext context) => Center( child: Padding( padding: .all(16), - child: SizedBox.square( - dimension: dimension, - child: CircularProgressIndicator(), - ), + child: SizedBox(height: height, child: CircularProgressIndicator()), ), ); } diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index ac0cb2a..92bb179 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -1,5 +1,5 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_buttons/m3e_buttons.dart"; @@ -7,7 +7,6 @@ import "package:m3e_card_list/m3e_card_list.dart"; import "package:nexus/controllers/members_by_status.dart"; import "package:nexus/controllers/members_grouped.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; -import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_status.dart"; @@ -15,9 +14,12 @@ import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; +import "package:nexus/widgets/user_bottom_sheet.dart"; + +class MemberList extends HookConsumerWidget { + final String roomId; + const MemberList(this.roomId, {super.key}); -class const MemberList(final String roomId, {super.key}) - extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final statusIndex = useState(0); @@ -94,9 +96,9 @@ class const MemberList(final String roomId, {super.key}) ), SliverM3ECardList( padding: .all(4), - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, margin: .symmetric(horizontal: 12, vertical: 4), itemCount: members.length, itemBuilder: (context, index) => @@ -123,16 +125,13 @@ class const MemberList(final String roomId, {super.key}) members[index].stateKey!, overflow: .ellipsis, ), - leading: Transform.translate( - offset: .new(0, 2), - child: AvatarOrHash( - avatarUrl, - dimension: 40, - displayName ?? - members[index] - .stateKey! - .localpart, - ), + leading: AvatarOrHash( + avatarUrl, + height: 36, + displayName ?? + members[index] + .stateKey! + .localpart, ), ), _ => throw Exception( @@ -143,10 +142,20 @@ class const MemberList(final String roomId, {super.key}) final member = members[index]; if (member.content case MembershipContent content) { - context.showUserPopover( - content, - member.stateKey!, - roomId: roomId, + showModalBottomSheet( + constraints: .loose( + .new( + 500, + (context.size?.height ?? 1000) - 80, + ), + ), + isScrollControlled: true, + context: context, + builder: (context) => UserBottomSheet( + content, + member.stateKey!, + roomId: roomId, + ), ); } }, diff --git a/lib/widgets/message_image.dart b/lib/widgets/message_image.dart index b04c209..914a252 100644 --- a/lib/widgets/message_image.dart +++ b/lib/widgets/message_image.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_blurhash/flutter_blurhash.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/mxc_image.dart"; @@ -7,12 +7,12 @@ import "package:nexus/models/requests/download_media.dart"; import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/loading.dart"; -final class const MessageImage( - final Uri url, { - final i.ImageInfo? info, - required final bool encrypted, - super.key, -}) extends ConsumerWidget { +class MessageImage extends ConsumerWidget { + final Uri url; + final i.ImageInfo? info; + final bool encrypted; + const MessageImage(this.url, {this.info, required this.encrypted, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final request = DownloadMediaRequest(mxc: url, encrypted: encrypted); diff --git a/lib/widgets/notifications_list.dart b/lib/widgets/notifications_list.dart deleted file mode 100644 index fde1bd5..0000000 --- a/lib/widgets/notifications_list.dart +++ /dev/null @@ -1,176 +0,0 @@ -import "dart:async"; - -import "package:m3e_buttons/m3e_buttons.dart"; -import "package:material_ui/material_ui.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/notifications.dart"; -import "package:nexus/helpers/extensions/focus_room.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/nav_page.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:nexus/widgets/loading.dart"; -import "package:nexus/widgets/renderers/event.dart"; -import "package:super_sliver_list/super_sliver_list.dart"; - -class const NotificationsList({ - final int? highlightedEvent, - final bool defaultToAllNotifications = false, - super.key, -}) extends HookConsumerWidget implements NavPage { - @override - String get title => "Notifications"; - - @override - IconData get icon => Icons.notifications; - - // TODO: Show notification badge here - @override - Future? badgeBuilder(WidgetRef ref) => null; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final options = { - M3EToggleButtonGroupAction( - checkedLabel: Text("Mentions"), - icon: Icon(Icons.alternate_email), - ): .highlight, - M3EToggleButtonGroupAction( - checkedLabel: Text("All Notifications"), - icon: Icon(Icons.notifications), - ): .notify, - }; - final unreadTypeIndex = useState(defaultToAllNotifications ? 1 : 0); - - final highlightedId = useState(highlightedEvent); - final listController = useRef(ListController()); - final scrollController = useScrollController(); - - final provider = NotificationsController.provider(( - options.values.toList()[unreadTypeIndex.value], - null, - )); - final notifications = ref.watch(provider); - final notifier = ref.watch(provider.notifier); - - useEffect(() { - if (highlightedId.value == null) return null; - Timer? timer; - - void listener() => - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!listController.value.isAttached) return; - - final notifications = await ref.watch(provider.future); - - final index = notifications.indexWhere( - (tuple) => tuple.$1 == highlightedId.value!, - ); - - if (index == -1) return; - - listController.value.animateToItem( - index: index, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => .new(milliseconds: 700), - curve: (_) => Curves.easeInOut, - ); - timer = Timer(.new(seconds: 1), () { - highlightedId.value = null; - }); - listController.value.removeListener(listener); - }); - - listController.value.addListener(listener); - return timer?.cancel; - }, []); - - useEffect(() { - Future listener() async { - if (!scrollController.hasClients || notifications.isLoading) return; - - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent) { - await notifier.loadOlder(); - } - } - - scrollController.addListener(listener); - return () => scrollController.removeListener(listener); - }, [scrollController, notifications]); - - return Stack( - children: [ - Column( - children: [ - if (notifications is AsyncLoading && notifications.value != null) - const LinearProgressIndicator(minHeight: 2), - Expanded( - child: switch (notifications) { - AsyncData(:final value) || AsyncLoading(:final value?) => - value.isEmpty - ? Center( - child: Text( - "No notifications yet", - style: Theme.of(context).textTheme.headlineMedium, - ), - ) - : SuperListView.builder( - listController: listController.value, - controller: scrollController, - itemCount: value.length, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 12, - ), - reverse: true, - itemBuilder: (context, index) { - final (rowId, roomId) = value[index]; - final isHighlighted = rowId == highlightedId.value; - - return Padding( - padding: .only(top: 8), - child: HighlightWrapper( - InkWell( - onTap: () async { - final navigator = Navigator.of(context); - if (navigator.canPop()) navigator.pop(); - - await ref.focusRoom(roomId, rowId); - }, - child: IgnorePointer( - child: EventRenderer(rowId, roomId: roomId), - ), - ), - isHighlighted: isHighlighted, - ), - ); - }, - ), - AsyncLoading() => const Loading(), - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, - ), - ], - ), - Align( - alignment: .topRight, - child: Padding( - padding: .all(16), - child: M3EToggleButtonGroup( - selectedIndex: unreadTypeIndex.value, - onSelectedIndexChanged: (index) => - unreadTypeIndex.value = index ?? unreadTypeIndex.value, - actions: options.keys.toList(), - ), - ), - ), - ], - ); - } -} diff --git a/lib/widgets/pinned_events_drawer.dart b/lib/widgets/pinned_events_drawer.dart index cff6ad6..7cb9fe3 100644 --- a/lib/widgets/pinned_events_drawer.dart +++ b/lib/widgets/pinned_events_drawer.dart @@ -1,19 +1,23 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/pinned_events.dart"; -import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; -final class const PinnedEventsDrawer( - final String roomId, { - required final IList Function(Event event) getEventOptions, - required final JumpToEvent jumpToEvent, - super.key, -}) extends HookConsumerWidget { +class PinnedEventsDrawer extends HookConsumerWidget { + final String roomId; + final IList Function(Event event) getEventOptions; + final Future Function(String eventId) jumpToId; + const PinnedEventsDrawer( + this.roomId, { + required this.getEventOptions, + required this.jumpToId, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final pinsProvider = ref.watch(PinnedEventsController.provider(roomId)); @@ -57,23 +61,21 @@ final class const PinnedEventsDrawer( reverse: true, itemCount: value.length, itemBuilder: (context, index) { - final rowId = value.reversed[index]; + final event = value.reversed[index]; return InkWell( borderRadius: .circular(12), onTap: () { Navigator.of(context).pop(); - jumpToEvent(rowId); + jumpToId(event.eventId); }, child: Padding( padding: .symmetric(vertical: 4), - child: IgnorePointer( - child: EventRenderer( - rowId, - roomId: roomId, - maxLines: 2, - getEventOptions: getEventOptions, - ), + child: EventRenderer( + event, + maxLines: 2, + isGrouped: false, + getEventOptions: getEventOptions, ), ), ); diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart index bd50a8b..0c96579 100644 --- a/lib/widgets/players/audio.dart +++ b/lib/widgets/players/audio.dart @@ -1,18 +1,18 @@ import "dart:async"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:media_kit/media_kit.dart"; -import "package:nexus/controllers/client.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/models/info/audio.dart"; -class const AudioPlayer( - final Uri uri, - final AudioInfo? info, { - final bool encrypted = false, - super.key, -}) extends HookConsumerWidget { +class AudioPlayer extends HookConsumerWidget { + final Uri url; + final AudioInfo? info; + + const AudioPlayer(this.url, this.info, {super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final player = useMemoized( @@ -24,12 +24,11 @@ class const AudioPlayer( final duration = useState(Duration.zero); useEffect(() { - player.platform?.state = player.platform!.state.copyWith(buffering: true); scheduleMicrotask(() async { - final video = await ref - .watch(ClientController.provider.notifier) - .downloadMedia(.new(mxc: uri, encrypted: encrypted)); - await player.open(Media(video.path), play: false); + await player.open( + Media(url.toString(), httpHeaders: ref.headers), + play: false, + ); player.stream.playing.listen((value) { playing.value = value; @@ -68,18 +67,12 @@ class const AudioPlayer( padding: .only(left: 8, right: 16), child: Row( children: [ - if (player.state.buffering) - SizedBox.square( - dimension: 24, - child: CircularProgressIndicator(padding: .all(4)), - ) - else - IconButton( - onPressed: player.playOrPause, - icon: Icon( - playing.value ? Icons.pause_circle : Icons.play_circle, - ), + IconButton( + onPressed: player.playOrPause, + icon: Icon( + playing.value ? Icons.pause_circle : Icons.play_circle, ), + ), SizedBox(width: 8), Text( format(position.value), diff --git a/lib/widgets/players/video.dart b/lib/widgets/players/video.dart index 024c30e..8083860 100644 --- a/lib/widgets/players/video.dart +++ b/lib/widgets/players/video.dart @@ -1,19 +1,18 @@ import "dart:async"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; import "package:nexus/models/info/video.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; + +class VideoPlayer extends HookConsumerWidget { + final VideoInfo? info; + final Uri url; + const VideoPlayer(this.url, this.info, {super.key}); -class const VideoPlayer( - final Uri uri, - final VideoInfo? info, { - final bool encrypted = false, - super.key, -}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final player = useMemoized( @@ -22,13 +21,12 @@ class const VideoPlayer( final controller = useMemoized(() => VideoController(player)); useEffect(() { - player.platform?.state = player.platform!.state.copyWith(buffering: true); - scheduleMicrotask(() async { - final video = await ref - .watch(ClientController.provider.notifier) - .downloadMedia(.new(mxc: uri, encrypted: encrypted)); - await player.open(Media(video.path), play: false); - }); + scheduleMicrotask( + () => player.open( + Media(url.toString(), httpHeaders: ref.headers), + play: false, + ), + ); return player.dispose; }, []); diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart index edef81c..b2fa5ca 100644 --- a/lib/widgets/reaction_row.dart +++ b/lib/widgets/reaction_row.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state.dart"; @@ -10,7 +10,10 @@ import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/main.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { +class ReactionRow extends ConsumerWidget { + final Event event; + const ReactionRow(this.event, {super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final clientState = ref.watch(ClientStateController.provider); @@ -64,10 +67,9 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { enabled.value = false; try { final controller = ref.watch( - RoomChatController.provider(( - roomId: event.roomId, - contextualEvent: null, - )).notifier, + RoomChatController.provider( + event.roomId, + ).notifier, ); if (selected) { diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index af4cef4..bcca961 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -1,13 +1,8 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/gestures.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/event.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/rooms.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; -import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/content/avatar.dart"; import "package:nexus/models/content/canonical_alias.dart"; import "package:nexus/models/content/content.dart"; @@ -29,18 +24,24 @@ import "package:nexus/widgets/renderers/message.dart"; import "package:nexus/widgets/reaction_row.dart"; import "package:nexus/widgets/renderers/membership.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -import "package:nexus/widgets/timestamp.dart"; -class const EventRenderer( - final int eventRowId, { - required final String roomId, - final int? previousEventRowId, - final bool textOnly = false, - final int? maxLines, - final JumpToEvent? jumpToEvent, - final IList Function(Event event)? getEventOptions, - super.key, -}) extends HookConsumerWidget { +class EventRenderer extends HookConsumerWidget { + final Event event; + final bool textOnly; + final bool isGrouped; + final int? maxLines; + final VoidCallback? onTapReply; + final IList Function(Event event)? getEventOptions; + const EventRenderer( + this.event, { + this.onTapReply, + this.textOnly = false, + this.isGrouped = false, + this.maxLines, + this.getEventOptions, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); @@ -49,38 +50,6 @@ class const EventRenderer( final focusNode = useFocusNode(); useListenable(focusNode); - final (event, previousEvent) = ref.watch( - RoomsController.provider.select((rooms) { - final event = rooms[roomId]?.events[eventRowId]; - if (event == null) return (null, null); - - final edited = event.lastEditRowId == 0 - ? null - : rooms[roomId]?.events[event.lastEditRowId]; - - return ( - edited == null - ? event - : event.copyWith( - content: edited.content, - localContent: edited.localContent, - ), - previousEventRowId == null - ? null - : rooms[roomId]?.events[previousEventRowId!], - ); - }), - ); - - if (event == null) return SizedBox.shrink(); - - final isGrouped = - previousEvent?.content is MessageContent && - previousEvent?.redactedBy == null && - previousEvent?.relationType != "m.replace" && - event.sender == previousEvent?.sender && - event.pmp?.id == previousEvent?.pmp?.id; - final child = event.redactedBy != null || event.relationType == "m.replace" ? null : switch (event.content) { @@ -97,18 +66,7 @@ class const EventRenderer( EncryptedContent() || StickerContent() => MessageRenderer( event, - onTapReply: jumpToEvent == null - ? null - : () async { - final replyEvent = await ref.read( - EventController.provider( - .new(roomId: roomId, eventId: event.replyTo!), - ).future, - ); - if (replyEvent != null) { - await jumpToEvent!(replyEvent.rowId); - } - }, + onTapReply: onTapReply, isGrouped: isGrouped, maxLines: maxLines, textOnly: textOnly, @@ -116,181 +74,92 @@ class const EventRenderer( MembershipContent content => switch (event.previousContent) { MembershipContent(:final status) => - status == content.status - ? null - : MembershipRenderer(event, maxLines: maxLines), - _ => MembershipRenderer(event, maxLines: maxLines), + status == content.status ? null : MembershipRenderer(event), + _ => MembershipRenderer(event), }, - AvatarContent() => GenericEventRenderer( - Icons.interests, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room avatar"), - ], - ), + AvatarContent() => GenericEventRenderer(Icons.interests, [ + MessageDisplayname(event), + Text("changed the room avatar"), + ]), - CreateContent() => GenericEventRenderer( - Icons.add, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "created the room"), - ], - ), + CreateContent() => GenericEventRenderer(Icons.add, [ + MessageDisplayname(event), + Text("created the room"), + ]), - PowerLevelsContent() => GenericEventRenderer( - Icons.power, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room's power levels"), - ], - ), + PowerLevelsContent() => GenericEventRenderer(Icons.power, [ + MessageDisplayname(event), + Text("changed the room's power levels"), + ]), - JoinRulesContent() => GenericEventRenderer( - Icons.rule, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "changed the room's join rules"), - ], - ), + JoinRulesContent() => GenericEventRenderer(Icons.rule, [ + MessageDisplayname(event), + Text("changed the room's join rules"), + ]), - TopicContent() => GenericEventRenderer( - Icons.description, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "updated the room topic"), - ], - ), + TopicContent() => GenericEventRenderer(Icons.description, [ + MessageDisplayname(event), + Text("updated the room topic"), + ]), HistoryVisibilityContent(:final historyVisibility) => - GenericEventRenderer(Icons.history, maxLines: maxLines, [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan( - text: - "changed the room's history visibility to ${switch (historyVisibility) { - .invited => "since invited", - .joined => "since joined", - .shared => "all history visible (shared)", - .worldReadable => "all history visible (world readable)", - }}", + GenericEventRenderer(Icons.history, [ + MessageDisplayname(event), + Text( + "changed the room's history visibility to ${switch (historyVisibility) { + .invited => "since invited", + .joined => "since joined", + .shared => "all history visible (shared)", + .worldReadable => "all history visible (world readable)", + }}", ), ]), - PinnedEventsContent() => GenericEventRenderer( - Icons.push_pin, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "pinned/unpinned some events"), - ], - ), + PinnedEventsContent() => GenericEventRenderer(Icons.push_pin, [ + MessageDisplayname(event), + Text("pinned/unpinned some events"), + ]), - ServerACLContent() => GenericEventRenderer( - Icons.list, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan(text: "updated the server ban list"), - ], - ), + ServerACLContent() => GenericEventRenderer(Icons.list, [ + MessageDisplayname(event), + Text("updated the server ban list"), + ]), CanonicalAliasContent(:final alias, :final altAliases) => - GenericEventRenderer(Icons.numbers, maxLines: maxLines, [ - WidgetSpan( - alignment: .middle, - child: MessageDisplayname( - event, - shouldWrap: maxLines == null, - ), - ), - TextSpan( - text: switch ([ - if (event.previousContent case CanonicalAliasContent( - alias: final prevAlias, - altAliases: final prevAltAliases, - )) ...[ - if (prevAlias != alias) - if (alias == null) - "removed the room's canonical alias" - else - "changed the room's canonical alias to $alias", + GenericEventRenderer(Icons.numbers, [ + MessageDisplayname(event), + Text(switch ([ + if (event.previousContent case CanonicalAliasContent( + alias: final prevAlias, + altAliases: final prevAltAliases, + )) ...[ + if (prevAlias != alias) + if (alias == null) + "removed the room's canonical alias" + else + "changed the room's canonical alias to $alias", - if (prevAltAliases - .remove(alias ?? "") - .remove(prevAlias ?? "") != - altAliases - .remove(alias ?? "") - .remove(prevAlias ?? "")) - "changed the room's aliases", - ] else ...[ - if (alias != null) "set the room's canonical alias", - if (altAliases.isNotEmpty) "set the room's aliases", - ], - ]) { - [] => "did something related to room aliases", - List prev => prev.join(" and "), - }, - ), + if (prevAltAliases + .remove(alias ?? "") + .remove(prevAlias ?? "") != + altAliases.remove(alias ?? "").remove(prevAlias ?? "")) + "changed the room's aliases", + ] else ...[ + if (alias != null) "set the room's canonical alias", + if (altAliases.isNotEmpty) "set the room's aliases", + ], + ]) { + [] => "did something related to room aliases", + List prev => prev.join(" and "), + }), ]), _ => null, }; final contextMenuCallback = getEventOptions == null ? null - : (PositionedGestureDetails details) => context.showContextMenu( + : (details) => context.showContextMenu( globalPosition: details.globalPosition, children: getEventOptions!(event).toList(), ); @@ -298,7 +167,7 @@ class const EventRenderer( return Column( crossAxisAlignment: .start, children: [ - if (child != null) + if (child != null) ...[ if (textOnly) child else ...[ @@ -333,18 +202,10 @@ class const EventRenderer( onSecondaryTapUp: contextMenuCallback, onLongPressStart: contextMenuCallback, child: Padding( - padding: EdgeInsets.symmetric(horizontal: 8) - .copyWith(top: isGrouped ? 0 : 8), - child: Wrap( - verticalDirection: .up, - spacing: 6, - crossAxisAlignment: .center, - children: [ - child, - if (child is! MessageRenderer) - Timestamp(event.timestamp), - ], - ), + padding: EdgeInsets.symmetric( + horizontal: 8, + ).copyWith(top: isGrouped ? 0 : 8), + child: child, ), ), ), @@ -352,44 +213,18 @@ class const EventRenderer( ), ...[ - if (child is! MessageRenderer) ReactionRow(event), + if (event.content is! MessageContent) ReactionRow(event), if (event.sendError != null && event.sendError != "not sent") - Padding( - padding: .only(bottom: 4), - child: Row( - spacing: 4, - children: [ - Text( - event.sendError!, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - InkWell( - child: Text( - "Hide", - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.primary, - decoration: .underline, - decorationColor: theme.colorScheme.primary, - ), - ), - onTap: () => ref - .read( - RoomChatController.provider(( - roomId: roomId, - contextualEvent: null, - )).notifier, - ) - .hideClientSticky(event.rowId), - ), - ], + Text( + event.sendError!, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.error, ), ), ].map((child) => Padding(padding: .only(left: 4), child: child)), - ] - else if (textOnly) + ], + ] else if (textOnly) Text("Unknown event type", style: errorStyle), ], ); diff --git a/lib/widgets/renderers/generic_event.dart b/lib/widgets/renderers/generic_event.dart index 5ff9dbc..6dfae53 100644 --- a/lib/widgets/renderers/generic_event.dart +++ b/lib/widgets/renderers/generic_event.dart @@ -1,33 +1,18 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; + +class GenericEventRenderer extends StatelessWidget { + final IconData icon; + final List children; + const GenericEventRenderer(this.icon, this.children, {super.key}); -class const GenericEventRenderer( - final IconData icon, - final List children, { - final int? maxLines, - super.key, -}) extends StatelessWidget { @override Widget build(BuildContext context) => Padding( padding: .symmetric(vertical: 4), child: Row( spacing: 8, - mainAxisSize: .min, children: [ Padding(padding: .symmetric(horizontal: 4), child: Icon(icon)), - Flexible( - child: Text.rich( - TextSpan( - children: [ - for (final (index, child) in children.indexed) ...[ - if (index > 0) TextSpan(text: " "), - child, - ], - ], - ), - maxLines: maxLines ?? 999, - overflow: .ellipsis, - ), - ), + Expanded(child: Wrap(spacing: 4, children: children)), ], ), ); diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index c57200c..b2835b4 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; @@ -7,11 +7,10 @@ import "package:nexus/models/event.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -class const MembershipRenderer( - final Event event, { - final int? maxLines, - super.key, -}) extends StatelessWidget { +class MembershipRenderer extends StatelessWidget { + final Event event; + const MembershipRenderer(this.event, {super.key}); + @override Widget build(BuildContext context) { assert( @@ -20,45 +19,34 @@ class const MembershipRenderer( ); return switch (event.content) { - MembershipContent content => GenericEventRenderer( - Icons.people, - maxLines: maxLines, - [ - WidgetSpan( - alignment: .middle, - child: InkWell( - onTap: () => context.showUserPopover( - content, - event.stateKey!, - roomId: event.roomId, - ), - child: Text( - overflow: .ellipsis, - content.displayName ?? event.stateKey!.localpart, - maxLines: 1, - style: .new(color: event.sender.colorHash, fontWeight: .bold), - ), - ), + MembershipContent content => GenericEventRenderer(Icons.people, [ + InkWell( + onTap: () => context.showUserPopover( + content, + event.stateKey!, + roomId: event.roomId, ), - TextSpan( - text: - "${switch (content.status) { - .invite => "was invited to", - .join => "joined", - .leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - .ban => "was banned from", - .knock => "asked to join", - }} the room${event.sender == event.stateKey ? "" : " by"}", + child: Text( + overflow: .ellipsis, + content.displayName ?? event.stateKey!.localpart, + maxLines: 1, + style: .new(color: event.sender.colorHash, fontWeight: .bold), ), - if (event.sender != event.stateKey) - WidgetSpan( - alignment: .middle, - child: MessageDisplayname(event, shouldWrap: maxLines == null), - ), - if (content.reason != null) - TextSpan(text: "for \"${content.reason}\""), - ], - ), + ), + Text( + overflow: .ellipsis, + maxLines: 1, + "${switch (content.status) { + .invite => "was invited to", + .join => "joined", + .leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), + .ban => "was banned from", + .knock => "asked to join", + }} the room${event.sender == event.stateKey ? "" : " by "}", + ), + if (event.sender != event.stateKey) MessageDisplayname(event), + if (content.reason != null) Text("for \"${content.reason}\""), + ]), _ => SizedBox.shrink(), }; } diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 3404bc5..6d660af 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -1,10 +1,9 @@ import "package:collection/collection.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:linkify/linkify.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/event.dart"; -import "package:nexus/helpers/extensions/get_filename.dart"; import "package:nexus/models/content/encrypted.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/sticker.dart"; @@ -14,143 +13,54 @@ import "package:nexus/widgets/html/html.dart"; import "package:nexus/widgets/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/linkified_text.dart"; -import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/message_image.dart"; import "package:nexus/widgets/reaction_row.dart"; -import "package:nexus/widgets/timestamp.dart"; import "package:nexus/widgets/url_preview.dart"; +import "package:timeago/timeago.dart"; import "package:nexus/widgets/event_preview.dart"; import "package:nexus/widgets/players/video.dart"; import "package:nexus/widgets/players/audio.dart"; -class const MessageRenderer( - final Event event, { - final VoidCallback? onTapReply, - final bool textOnly = false, - final bool isGrouped = false, - final int? maxLines, - super.key, -}) extends ConsumerWidget { +class MessageRenderer extends ConsumerWidget { + final Event event; + final bool textOnly; + final bool isGrouped; + final int? maxLines; + final VoidCallback? onTapReply; + const MessageRenderer( + this.event, { + this.onTapReply, + this.textOnly = false, + this.isGrouped = false, + this.maxLines, + super.key, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final errorStyle = TextStyle(color: colorScheme.error); - final textStyle = TextStyle( - fontSize: event.localContent?.bigEmoji == true && maxLines != 1 - ? 32 - : null, - fontStyle: event.content is EmoteMessageContent ? .italic : null, + final timestamp = Tooltip( + message: event.timestamp.toString(), + child: Text( + format(event.timestamp), + maxLines: 1, + overflow: .ellipsis, + style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey), + ), ); - final rendered = switch (event.content) { - EncryptedContent() => Text("Unable to decrypt event", style: errorStyle), - StickerContent(:final body, :final url, :final info) => - textOnly - ? Text(body, maxLines: maxLines, overflow: .ellipsis) - : ConstrainedBox( - constraints: .loose(.square(200)), - child: MessageImage(url, info: info, encrypted: false), - ), - // TODO: Handle locations - // LocationMessageContent(:final body , :final geoUri) => - MessageContent content => switch (content) { - TextMessageContent(:final body, :final formattedBody, :final format) || - NoticeMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - EmoteMessageContent(:final body, :final formattedBody, :final format) || - ImageMessageContent(:final body, :final formattedBody, :final format) || - VideoMessageContent(:final body, :final formattedBody, :final format) || - AudioMessageContent(:final body, :final formattedBody, :final format) || - FileMessageContent( - :final body, - :final formattedBody, - :final format, - ) => Column( - crossAxisAlignment: .start, - children: [ - format == .html && !textOnly - ? Html( - roomId: event.roomId, - textStyle: textStyle, - formattedBody!.replaceAllMapped( - RegExp( - r"(]*>.*?<\/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"; - }, - ), - ) - : LinkifiedText( - body.isEmpty ? content.filename ?? "" : body, - style: textStyle, - maxLines: maxLines, - ), - - if (!textOnly) ...[ - if (event.content - case ImageMessageContent(:final url) || - FileMessageContent(:final url) || - VideoMessageContent(:final url) || - AudioMessageContent(:final url)) - ConstrainedBox( - constraints: .loose(.square(500)), - child: switch (event.content) { - VideoMessageContent(:final info, :final file) => - VideoPlayer(url, info, encrypted: file != null), - AudioMessageContent(:final info, :final file) => - AudioPlayer(url, info, encrypted: file != null), - FileMessageContent(:final info, :final filename) => - FileCard(url, info, filename: filename), - ImageMessageContent(:final info, :final file) => - MessageImage(url, info: info, encrypted: file != null), - _ => SizedBox.shrink(), - }, - ), - - if (event.lastEditRowId != 0) - Text("(edited)", style: theme.textTheme.labelSmall), - - if (linkify(body) - .firstWhereOrNull((element) => element is UrlElement) - case final UrlElement link?) - if (Uri.tryParse(link.url) case final Uri url?) UrlPreview(url), - ], - ], - ), - LocationMessageContent(:final String? body) || - UnknownMessageContent(:final body) => - body == null - ? Text("This message is redacted", style: errorStyle) - : Wrap( - spacing: 8, - children: [ - Text("Unknown message type:", style: errorStyle), - Text(body), - ], - ), - }, - _ => throw Exception("This is impossible"), - }; + final textStyle = TextStyle( + fontSize: event.localContent?.bigEmoji == true ? 32 : null, + fontStyle: event.content is EmoteMessageContent ? .italic : null, + ); return Row( crossAxisAlignment: .start, mainAxisSize: .min, - spacing: 6, + spacing: 8, children: [ if (!textOnly) if (isGrouped) @@ -159,7 +69,7 @@ class const MessageRenderer( MessageAvatar(event, height: 40), Flexible( child: Column( - spacing: 2, + spacing: 4, crossAxisAlignment: .start, children: [ if (!isGrouped && !textOnly) @@ -167,7 +77,7 @@ class const MessageRenderer( spacing: 4, children: [ Flexible(child: MessageDisplayname(event)), - Flexible(flex: 0, child: Timestamp(event.timestamp)), + Flexible(flex: 0, child: timestamp), ], ), Card( @@ -199,32 +109,180 @@ class const MessageRenderer( onTap: onTapReply, child: Padding( padding: .symmetric(vertical: 8, horizontal: 12), - child: SizedBox( - height: 48, - child: switch (ref.watch( - EventController.provider( - .new( - roomId: event.roomId, - eventId: event.replyTo!, - ), + child: switch (ref.watch( + EventController.provider( + .new( + roomId: event.roomId, + eventId: event.replyTo!, ), - )) { - AsyncData(:final value?) || - AsyncLoading( - :final value?, - ) => EventPreview(value), - AsyncError _ => Text( - "An error occurred while fetching the reply", - style: errorStyle, - ), - _ => Loading(dimension: 12), - }, - ), + ), + )) { + AsyncData(:final value?) || + AsyncLoading( + :final value?, + ) => EventPreview(value), + AsyncError _ => Text( + "An error occurred while fetching the reply", + style: errorStyle, + ), + _ => Text("Fetching event..."), + }, ), ), ), - rendered, - if (!textOnly) ReactionRow(event), + switch (event.content) { + EncryptedContent() => Text( + "Unable to decrypt event", + style: errorStyle, + ), + StickerContent(:final body, :final url, :final info) => + textOnly + ? Text( + body, + maxLines: maxLines, + overflow: .ellipsis, + ) + : ConstrainedBox( + constraints: .loose(.square(200)), + child: MessageImage( + url, + info: info, + encrypted: false, + ), + ), + // TODO: Handle locations + // LocationMessageContent(:final body , :final geoUri) => + TextMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + NoticeMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + EmoteMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + ImageMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + VideoMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + AudioMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + FileMessageContent( + :final body, + :final formattedBody, + :final format, + ) => Column( + crossAxisAlignment: .start, + children: [ + format == .html && !textOnly + ? Html( + roomId: event.roomId, + textStyle: textStyle, + formattedBody!.replaceAllMapped( + RegExp( + r"(]*>.*?<\/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"; + }, + ), + ) + : LinkifiedText( + body, + style: textStyle, + maxLines: maxLines, + ), + + if (!textOnly) ...[ + if (event.content + case ImageMessageContent(:final url) || + FileMessageContent(:final url) || + VideoMessageContent(:final url) || + AudioMessageContent(:final url)) + ConstrainedBox( + constraints: .loose(.square(500)), + child: switch (event.content) { + VideoMessageContent(:final info) => + VideoPlayer(url, info), + AudioMessageContent(:final info) => + AudioPlayer(url, info), + FileMessageContent( + :final info, + :final filename, + ) => + FileCard(url, info, filename: filename), + ImageMessageContent( + :final info, + :final file, + ) => + MessageImage( + url, + info: info, + encrypted: file != null, + ), + _ => SizedBox.shrink(), + }, + ), + + if (event.lastEditRowId != 0) + Text( + "(edited)", + style: theme.textTheme.labelSmall, + ), + + if (linkify(body).firstWhereOrNull( + (element) => element is UrlElement, + ) + case final UrlElement link?) + UrlPreview(link.url), + + ReactionRow(event), + ], + ], + ), + MessageContent(:final body) => + body == null + ? Text( + "This message is redacted", + style: errorStyle, + ) + : Row( + spacing: 8, + mainAxisSize: .min, + children: [ + Text( + "Unknown message type:", + style: errorStyle, + ), + Text(body), + ], + ), + _ => throw Exception("This is impossible"), + }, ], ), ), diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 3b29129..93eca85 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -1,19 +1,27 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/room_menu_button.dart"; -import "package:nexus/widgets/room_summary_dialog.dart"; +import "package:nexus/widgets/expandable_image.dart"; +import "package:nexus/widgets/linkified_text.dart"; +import "package:nexus/widgets/room_menu.dart"; + +class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { + final bool isDesktop; + final void Function(BuildContext context)? onOpenMemberList; + final void Function()? onOpenPinnedMessagesList; + final void Function() onOpenDrawer; + final String? roomId; + const RoomAppbar({ + required this.roomId, + required this.isDesktop, + required this.onOpenDrawer, + this.onOpenMemberList, + this.onOpenPinnedMessagesList, + super.key, + }); -final class const RoomAppbar({ - required final String? roomId, - required final bool isDesktop, - required final void Function() onOpenDrawer, - final void Function(BuildContext context)? onOpenMemberList, - final void Function()? onOpenPinnedMessagesList, - super.key, -}) extends ConsumerWidget implements PreferredSizeWidget { @override Size get preferredSize => AppBar().preferredSize; @@ -24,19 +32,57 @@ final class const RoomAppbar({ : ref.watch(RoomsController.provider.select((value) => value[roomId!])); return Appbar( - onTap: room?.metadata == null + onTap: room == null ? null : () => showDialog( context: context, - builder: (context) => RoomSummaryDialog( - .new( - roomId: room!.metadata!.id, - joinedMembers: - room.metadata!.lazyLoadSummary?.joinedMemberCount, - avatarUrl: room.metadata?.avatar, - canonicalAlias: room.metadata?.canonicalAlias, - name: room.metadata?.name, - topic: room.metadata?.topic, + builder: (context) => Dialog( + constraints: .loose(.fromWidth(400)), + child: Padding( + padding: .all(24), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + spacing: 8, + children: [ + Row( + spacing: 12, + mainAxisSize: .min, + children: [ + if (room.metadata?.avatar != null) + ExpandableImage( + room.metadata?.avatar == null + ? null + : .new(mxc: room.metadata!.avatar!), + child: AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Room", + height: 64, + fallback: Icon(Icons.numbers), + ), + ), + Expanded( + child: Text( + room.metadata?.name ?? "Unnamed Room", + overflow: .ellipsis, + maxLines: 3, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ], + ), + if (room.metadata?.topic?.isNotEmpty == true) + LinkifiedText( + room.metadata!.topic!, + style: Theme.of(context).textTheme.bodyLarge + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ), ), @@ -46,7 +92,7 @@ final class const RoomAppbar({ : AvatarOrHash( room.metadata?.avatar, room.metadata?.name ?? "Unnamed Room", - dimension: 24, + height: 24, fallback: Icon(Icons.numbers), ) : DrawerButton(onPressed: onOpenDrawer), @@ -72,23 +118,21 @@ final class const RoomAppbar({ ), ], ), - actions: .new( - room == null - ? [] - : [ - IconButton( - onPressed: onOpenPinnedMessagesList?.call, - icon: Icon(Icons.push_pin), - tooltip: "Open pinned messages", - ), - IconButton( - onPressed: () => onOpenMemberList?.call(context), - tooltip: "Open member list", - icon: Icon(Icons.people), - ), - RoomMenuButton(room), - ], - ), + actions: room == null + ? .new() + : .new([ + IconButton( + onPressed: onOpenPinnedMessagesList?.call, + icon: Icon(Icons.push_pin), + tooltip: "Open pinned messages", + ), + IconButton( + onPressed: () => onOpenMemberList?.call(context), + tooltip: "Open member list", + icon: Icon(Icons.people), + ), + RoomMenu(room), + ]), ); } } diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart new file mode 100644 index 0000000..f679cf6 --- /dev/null +++ b/lib/widgets/room_chat.dart @@ -0,0 +1,576 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:measure_size/measure_size.dart"; +import "package:nexus/controllers/account_data.dart"; +import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/client_state.dart"; +import "package:nexus/controllers/pinned_ids.dart"; +import "package:nexus/controllers/power_level.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/room_chat.dart"; +import "package:nexus/controllers/via.dart"; +import "package:nexus/models/content/message.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/composer/composer.dart"; +import "package:nexus/widgets/emote_picker.dart"; +import "package:nexus/widgets/pinned_events_drawer.dart"; +import "package:nexus/widgets/renderers/event.dart"; +import "package:nexus/widgets/member_list.dart"; +import "package:nexus/widgets/room_appbar.dart"; +import "package:nexus/widgets/highlight_wrapper.dart"; +import "package:nexus/widgets/error_dialog.dart"; +import "package:nexus/main.dart"; +import "package:nexus/widgets/loading.dart"; +import "package:super_sliver_list/super_sliver_list.dart"; + +class RoomChat extends HookConsumerWidget { + final bool isDesktop; + final bool showMembersByDefault; + final String? roomId; + const RoomChat({ + required this.roomId, + required this.isDesktop, + required this.showMembersByDefault, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final relatedEvent = useState(null); + final relationType = useState(RelationType.reply); + final highlightedEvent = useState(null); + + final composerSize = useState(64); + + final memberListOpened = useState(showMembersByDefault); + + final userId = ref.watch(ClientStateController.provider)?.userId; + final theme = Theme.of(context); + + final nothing = Center( + child: Text( + "Nothing to see here...", + style: theme.textTheme.headlineMedium, + ), + ); + if (userId == null || this.roomId == null) { + return Scaffold( + appBar: RoomAppbar( + roomId: this.roomId, + isDesktop: isDesktop, + onOpenDrawer: () => Scaffold.of(context).openDrawer(), + ), + body: nothing, + ); + } + + final roomId = this.roomId!; + + final controllerProvider = RoomChatController.provider(roomId); + final notifier = ref.watch(controllerProvider.notifier); + + final client = ref.watch(ClientController.provider.notifier); + + final listController = useRef(ListController()); + final scrollController = useScrollController(); + final controllerData = ref.watch(controllerProvider); + + final topEventBeforeLoad = useState(null); + + Future jumpToId(String eventId) async { + final index = controllerData.value?.indexWhere( + (element) => element.eventId == eventId, + ); + if (index == null) return; + + listController.value.animateToItem( + index: index, + scrollController: scrollController, + alignment: 0.5, + duration: (_) => .new(milliseconds: 700), + curve: (_) => Curves.easeInOut, + ); + highlightedEvent.value = eventId; + await Future.delayed(.new(seconds: 1), () { + if (highlightedEvent.value == eventId) { + highlightedEvent.value = null; + } + }); + } + + Future loadOlder() async { + if (controllerData case AsyncData(:final value?)) { + topEventBeforeLoad.value = value.firstOrNull?.eventId; + await notifier.loadOlder(); + } + } + + useEffect(() { + ref + .read(controllerProvider.future) + .then( + (_) => WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent - .000001, + ); + } + }), + ); + + return null; + }, [scrollController.hasClients]); + + useEffect(() { + if (controllerData case AsyncData( + :final value?, + ) when scrollController.hasClients) { + if (topEventBeforeLoad.value != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + final index = value.indexWhere( + (event) => event.eventId == topEventBeforeLoad.value, + ); + if (index != -1) { + listController.value.jumpToItem( + index: index, + scrollController: scrollController, + alignment: 0, + ); + } + } + topEventBeforeLoad.value = null; + }); + } else if (scrollController.position.atEdge && + scrollController.position.pixels != 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent, + ); + } + }); + } + } + + return null; + }, [controllerData]); + + useEffect(() { + Future listener() async { + if (!scrollController.hasClients || !scrollController.position.atEdge) { + return; + } + + final room = ref.watch( + RoomsController.provider.select((value) => value[roomId]), + ); + if (room == null) return; + + if (scrollController.position.pixels == 0) { + if (room.hasMore) { + await loadOlder(); + } + } else { + await client.markRead(room); + } + } + + scrollController.addListener(listener); + return () => scrollController.removeListener(listener); + }, [roomId, controllerData]); + + final composerNode = useFocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && event.logicalKey == .escape) { + relatedEvent.value = null; + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + ); + + IList getEventOptions(Event event) { + final danger = theme.colorScheme.error; + final isSentByMe = event.sender == userId; + + return [ + if (ref.watch( + PowerLevelController.provider( + .new(eventType: .reaction, roomId: roomId), + ), + )) + PopupMenuItem( + enabled: false, + child: IconTheme( + data: theme.iconTheme, + child: Row( + children: [ + ...{ + ...ref.watch( + AccountDataController.provider.select( + (value) => IList( + value["m.recent_emoji"] + ?.content["recent_emoji"] ?? + [], + ).map((entry) => entry["emoji"]).toIList(), + ), + ), + "👍", + "🤣", + "😭", + "🤔", + } + .toIList() + .sublist(0, 4) + .map( + (emoji) => IconButton( + onPressed: () async { + Navigator.of(context).pop(); + await notifier + .sendReaction(emoji, event) + .onError(showError); + }, + icon: Text(emoji), + ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); // Close popup menu + showEmotePickerBottomSheet( + context: context, + roomId: roomId, + onSelect: (emojiId) async { + await notifier + .sendReaction(emojiId, event) + .onError(showError); + }, + ); + }, + child: Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.emoji_emotions), + ), + ), + ], + ), + ), + ), + if (ref.watch( + PowerLevelController.provider( + .new(eventType: .message, roomId: roomId), + ), + )) + PopupMenuItem( + onTap: () { + relatedEvent.value = event; + relationType.value = .reply; + composerNode.requestFocus(); + }, + child: ListTile(leading: Icon(Icons.reply), title: Text("Reply")), + ), + if (event.content is MessageContent && isSentByMe) + PopupMenuItem( + onTap: () { + relatedEvent.value = event; + relationType.value = .edit; + composerNode.requestFocus(); + }, + child: ListTile(leading: Icon(Icons.edit), title: Text("Edit")), + ), + if (ref.watch( + PowerLevelController.provider( + .state(eventType: .pinnedEvents, roomId: roomId), + ), + )) + switch (ref + .watch(PinnedIdsController.provider(roomId)) + .contains(event.eventId)) { + bool isPinned => PopupMenuItem( + onTap: () async { + try { + final notifier = ref.read( + PinnedIdsController.provider(roomId).notifier, + ); + if (isPinned) { + await notifier.removePin(event.eventId); + } else { + await notifier.addPin(event.eventId); + } + } catch (error, stackTrace) { + showError(error, stackTrace); + } + }, + child: ListTile( + leading: Icon(Icons.push_pin), + title: Text(isPinned == true ? "Unpin Event" : "Pin Event"), + ), + ), + }, + PopupMenuItem( + onTap: () async { + final room = ref.watch( + RoomsController.provider.select((value) => value[roomId]), + ); + if (room == null) return; + + final vias = ref.watch(ViaController.provider(room)); + + await Clipboard.setData( + ClipboardData( + text: + "matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias)", + ), + ); + }, + child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), + ), + if (ref.watch( + PowerLevelController.provider( + .redaction(targetUser: event.sender, roomId: roomId), + ), + )) + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final deleteReasonController = useTextEditingController(); + return AlertDialog( + title: Text("Delete Message"), + content: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Are you sure you want to delete this message? This can not be reversed.", + ), + SizedBox(height: 12), + TextField( + controller: deleteReasonController, + textCapitalization: .sentences, + decoration: .new( + labelText: "Reason for deletion (optional)", + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + await notifier + .deleteMessage( + event, + reason: deleteReasonController.text, + ) + .onError(showError); + }, + child: Text("Delete"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.delete, color: danger), + title: Text("Delete", style: .new(color: danger)), + ), + ), + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (_) { + final reasonController = useTextEditingController(); + return AlertDialog( + title: Text("Report"), + content: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Report this event to your server administrators, who can take action like banning this server or room.", + ), + + SizedBox(height: 12), + TextField( + controller: reasonController, + textCapitalization: .sentences, + decoration: .new( + labelText: "Reason for report (optional)", + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () { + client.reportEvent( + .new( + roomId: roomId, + eventId: event.eventId, + reason: reasonController.text.isEmpty + ? null + : reasonController.text, + ), + ); + Navigator.of(context).pop(); + }, + child: Text("Report"), + ), + ], + ); + }, + ), + ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: .new(color: danger)), + ), + ), + ].toIList(); + } + + return Scaffold( + endDrawer: PinnedEventsDrawer( + roomId, + getEventOptions: getEventOptions, + jumpToId: jumpToId, + ), + body: Builder( + builder: (middleContext) => Scaffold( + endDrawer: showMembersByDefault ? null : MemberList(roomId), + appBar: RoomAppbar( + roomId: roomId, + isDesktop: isDesktop, + onOpenDrawer: Scaffold.of(context).openDrawer, + onOpenMemberList: (thisContext) { + memberListOpened.value = !memberListOpened.value; + Scaffold.of(thisContext).openEndDrawer(); + }, + onOpenPinnedMessagesList: () { + Scaffold.of(middleContext).openEndDrawer(); + }, + ), + body: Row( + children: [ + Expanded( + child: Stack( + children: [ + Positioned.fill( + child: Padding( + padding: .symmetric(horizontal: 4), + child: switch (controllerData) { + AsyncData(:final value?) || + AsyncLoading(:final value?) => CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: .symmetric(vertical: 36), + child: Center( + child: ElevatedButton( + onPressed: controllerData is AsyncData + ? loadOlder + : null, + child: Text("Load More"), + ), + ), + ), + ), + + SuperSliverList.builder( + listController: listController.value, + itemCount: value.length, + itemBuilder: (_, index) { + final event = value[index]; + final previousEvent = value.getOrNull( + index - 1, + ); + return HighlightWrapper( + EventRenderer( + event, + onTapReply: () => + jumpToId(event.replyTo!), + getEventOptions: getEventOptions, + isGrouped: + previousEvent?.content + is MessageContent && + previousEvent?.redactedBy == null && + previousEvent?.relationType != + "m.replace" && + "${event.sender}${event.pmp?.id}" == + "${previousEvent?.sender}${previousEvent?.pmp?.id}", + ), + isHighlighted: + highlightedEvent.value == event.eventId, + ); + }, + ), + + SliverPadding( + padding: .only(bottom: composerSize.value), + ), + ], + ), + AsyncData() => nothing, + AsyncLoading() => Loading(), + AsyncError(:final error, :final stackTrace) => + ErrorDialog(error, stackTrace), + }, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: MeasureSize( + onChange: (size) => composerSize.value = size.height, + child: Composer( + roomId, + node: composerNode, + onSend: + (text, {required shouldMention, required tags}) => + notifier + .send( + text, + tags: tags, + relationType: relationType.value, + shouldMention: shouldMention, + relation: relatedEvent.value, + ) + .onError(showError), + relationType: relationType.value, + relatedEvent: relatedEvent.value, + onDismiss: () => relatedEvent.value = null, + ), + ), + ), + ], + ), + ), + + if (memberListOpened.value == true && showMembersByDefault) + MemberList(roomId), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart deleted file mode 100644 index 254c782..0000000 --- a/lib/widgets/room_chat/chat_timeline.dart +++ /dev/null @@ -1,65 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/helpers/hooks/chat_scroll.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/renderers/event.dart"; -import "package:nexus/widgets/highlight_wrapper.dart"; -import "package:super_sliver_list/super_sliver_list.dart"; - -class const ChatTimeline({ - required final String roomId, - required final ChatScroll scroll, - required final JumpToEvent jumpToEvent, - required final IList Function(Event) getEventOptions, - required final int? highlightedEvent, - required final double composerHeight, - super.key, -}) extends StatelessWidget { - Widget eventRow( - int eventRowId, - int? previousEventRowId, { - required Key key, - }) => HighlightWrapper( - EventRenderer( - eventRowId, - previousEventRowId: previousEventRowId, - roomId: roomId, - jumpToEvent: jumpToEvent, - getEventOptions: getEventOptions, - ), - key: key, - isHighlighted: highlightedEvent == eventRowId, - ); - - @override - Widget build(BuildContext context) => CustomScrollView( - reverse: true, - center: scroll.centerKey, - keyboardDismissBehavior: .onDrag, - controller: scroll.scrollController, - slivers: [ - SliverToBoxAdapter(child: SizedBox(height: composerHeight)), - - SuperSliverList.builder( - itemCount: scroll.liveRows.length, - itemBuilder: (_, index) => eventRow( - scroll.liveRows[index], - index > 0 - ? scroll.liveRows.getOrNull(index - 1) - : scroll.historyRows.firstOrNull, - key: scroll.keyFor(scroll.liveRows[index]), - ), - ), - - SuperSliverList.builder( - key: scroll.centerKey, - itemCount: scroll.historyRows.length, - itemBuilder: (_, index) => eventRow( - scroll.historyRows[index], - scroll.historyRows.getOrNull(index + 1), - key: scroll.keyFor(scroll.historyRows[index]), - ), - ), - ], - ); -} diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart deleted file mode 100644 index a49e6f9..0000000 --- a/lib/widgets/room_chat/room_chat.dart +++ /dev/null @@ -1,282 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; -import "package:flutter/services.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:measure_size/measure_size.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/controllers/member_list_opened.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/helpers/hooks/chat_scroll.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/composer/composer.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/loading.dart"; -import "package:nexus/widgets/pinned_events_drawer.dart"; -import "package:nexus/widgets/member_list.dart"; -import "package:nexus/widgets/room_appbar.dart"; -import "package:nexus/main.dart"; -import "package:nexus/widgets/room_chat/chat_timeline.dart"; -import "package:nexus/helpers/extensions/build_event_options.dart"; -import "package:nexus/controllers/jump_request.dart"; - -final class const RoomChat({ - required final String? roomId, - required final bool isDesktop, - required final bool showMembersByDefault, - super.key, -}) extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final relatedEvent = useState(null); - final relationType = useState(RelationType.reply); - final pendingJump = ref.read(JumpRequestController.provider); - final contextualEvent = useState( - pendingJump != null && pendingJump.roomId == this.roomId - ? pendingJump.rowId - : null, - ); - final highlightedEvent = useState(null); - - final composerSize = useState(64); - - final userId = ref.watch(ClientStateController.provider)?.userId; - final memberListOpened = ref - .watch(MemberListOpenedController.provider) - .requireValue; - final theme = Theme.of(context); - - final nothing = Center( - child: Text( - "Nothing to see here...", - style: theme.textTheme.headlineMedium, - ), - ); - if (userId == null || this.roomId == null) { - return Scaffold( - appBar: RoomAppbar( - roomId: this.roomId, - isDesktop: isDesktop, - onOpenDrawer: Scaffold.of(context).openDrawer, - ), - body: nothing, - ); - } - - final roomId = this.roomId!; - - final jumpRowId = ref.watch( - JumpRequestController.provider.select( - (request) => request?.roomId == roomId ? request?.rowId : null, - ), - ); - - final controllerProvider = RoomChatController.provider(( - roomId: roomId, - contextualEvent: contextualEvent.value, - )); - final notifier = ref.watch(controllerProvider.notifier); - - final client = ref.read(ClientController.provider.notifier); - - final controllerData = ref.watch(controllerProvider); - - final scroll = ChatScroll.use( - controllerData: controllerData, - paginate: notifier.paginate, - contextualEvent: contextualEvent, - markRead: () async { - final room = ref.read( - RoomsController.provider.select((rooms) => rooms[roomId]), - ); - - if (room != null) { - await client.markRead(room); - } - }, - ); - - final lastComposerHeight = useRef(composerSize.value); - - useEffect(() { - final delta = composerSize.value - lastComposerHeight.value; - lastComposerHeight.value = composerSize.value; - - if (delta != 0) { - final wasAtBottom = scroll.atBottom; - - WidgetsBinding.instance.addPostFrameCallback((_) { - final controller = scroll.scrollController; - if (!controller.hasClients) return; - - final position = controller.position; - controller.jumpTo( - wasAtBottom ? position.minScrollExtent : position.pixels - delta, - ); - }); - } - - return null; - }, [composerSize.value]); - - final composerNode = useFocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && event.logicalKey == .escape) { - relatedEvent.value = null; - return .handled; - } - - return .ignored; - }, - ); - - Future jumpToEvent(int rowId) async { - highlightedEvent.value = rowId; - - await scroll.jumpToEvent(rowId); - await Future.delayed(.new(milliseconds: 700), () { - if (highlightedEvent.value == rowId) { - highlightedEvent.value = null; - } - }); - } - - useEffect(() { - if (jumpRowId == null) return null; - - void check() { - if (!context.mounted) return; - - if (scroll.scrollController.hasClients) { - jumpToEvent(jumpRowId); - Future.microtask( - () => ref.read(JumpRequestController.provider.notifier).consume(), - ); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) => check()); - } - } - - check(); - - return null; - }, [jumpRowId]); - - IList getEventOptions(Event event) => - event.buildEventOptions( - context: context, - ref: ref, - roomId: roomId, - userId: userId, - onRelation: (rowId, type) { - relatedEvent.value = rowId; - relationType.value = type; - composerNode.requestFocus(); - }, - ); - - return Scaffold( - endDrawer: PinnedEventsDrawer( - roomId, - getEventOptions: getEventOptions, - jumpToEvent: jumpToEvent, - ), - body: Builder( - builder: (middleContext) => Scaffold( - endDrawer: showMembersByDefault ? null : MemberList(roomId), - appBar: RoomAppbar( - roomId: roomId, - isDesktop: isDesktop, - onOpenDrawer: Scaffold.of(context).openDrawer, - onOpenMemberList: (thisContext) { - ref - .read(MemberListOpenedController.provider.notifier) - .set(!memberListOpened); - Scaffold.of(thisContext).openEndDrawer(); - }, - onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer, - ), - body: Row( - children: [ - Expanded( - child: Stack( - children: [ - Positioned.fill( - child: Padding( - padding: .symmetric(horizontal: 4), - child: switch (controllerData) { - AsyncLoading(value: _?) || - AsyncData(value: _) => ChatTimeline( - scroll: scroll, - roomId: roomId, - jumpToEvent: jumpToEvent, - getEventOptions: getEventOptions, - highlightedEvent: highlightedEvent.value, - composerHeight: composerSize.value, - ), - AsyncLoading _ => Loading(), - AsyncError(:final error, :final stackTrace) => - ErrorDialog(error, stackTrace), - }, - ), - ), - Positioned( - right: 16, - bottom: composerSize.value, - child: IgnorePointer( - ignoring: scroll.atBottom, - child: AnimatedOpacity( - opacity: scroll.atBottom ? 0 : 1, - duration: const Duration(milliseconds: 200), - child: FloatingActionButton.small( - onPressed: scroll.jumpToBottom, - child: const Icon(Icons.keyboard_arrow_down), - ), - ), - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: MeasureSize( - onChange: (size) => composerSize.value = size.height, - child: Composer( - roomId, - node: composerNode, - onSend: - (text, {required shouldMention, required tags}) => - notifier - .send( - text, - tags: tags, - relationType: relationType.value, - shouldMention: shouldMention, - relatedEventRowId: relatedEvent.value, - ) - .onError(showError), - relationType: relationType.value, - relatedEvent: relatedEvent.value, - onDismiss: () { - relatedEvent.value = null; - composerNode.requestFocus(); - }, - ), - ), - ), - ], - ), - ), - - if (memberListOpened == true && showMembersByDefault) - MemberList(roomId), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/room_list.dart b/lib/widgets/room_list.dart deleted file mode 100644 index b4f07f5..0000000 --- a/lib/widgets/room_list.dart +++ /dev/null @@ -1,318 +0,0 @@ -import "dart:math"; - -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; -import "package:nexus/models/nav_page.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/divider_widget.dart"; -import "package:nexus/widgets/error_dialog.dart"; -import "package:nexus/widgets/pages/settings.dart"; -import "package:nexus/widgets/join_dialog.dart"; -import "package:nexus/widgets/room_menu_button.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/spaces.dart"; -import "package:nexus/models/room.dart"; -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; - -// Needed for navigation_rail_m3e (#65). -import "package:flutter/material.dart" as old_mat; - -List roomDestinations( - IList rooms, { - required bool isDm, -}) => [ - for (final room in rooms) - .new( - label: room.metadata?.name ?? "Unnamed Room", - badgeCount: switch (room.metadata?.unreadNotifications) { - 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, - int unread => unread, - }, - icon: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - fallback: isDm ? null : const Icon(Icons.numbers), - ), - ), -]; - -class const RoomList({super.key}) extends ConsumerWidget implements NavPage { - @override - String get title => "Rooms"; - - @override - IconData get icon => Icons.list; - - // TODO: Show unread badge here and on sidebar hamburger - @override - Future? badgeBuilder(WidgetRef ref) => null; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final spacesAsync = ref.watch(SpacesController.provider); - if (spacesAsync.value == null) { - return switch (spacesAsync) { - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - _ => const Center(child: CircularProgressIndicator()), - }; - } - - return const Row( - children: [ - SpacesRail(), - Expanded(child: RoomsPane()), - ], - ); - } -} - -class const SpacesRail({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final spaces = ref.watch( - SpacesController.provider.select((async) => async.requireValue), - ); - final selectedSpaceId = ref.watch( - KeyController.provider(KeyController.spaceKey) - .select((async) => async.requireValue), - ); - - return MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemCollapsedHeight: 48, - itemVerticalGap: 0, - ), - ], - ), - child: Container( - color: NavigationRailTokensAdapter(context).containerColor, - padding: .only(top: 16), - child: NavigationRailM3E( - type: .alwaysCollapse, - labelBehavior: .alwaysHide, - scrollable: true, - onDestinationSelected: (value) { - ref - .read( - KeyController.provider(KeyController.spaceKey).notifier, - ) - .set(spaces[value].id); - ref - .read( - KeyController.provider(KeyController.roomKey).notifier, - ) - .set(spaces[value].children.firstOrNull?.metadata?.id); - }, - sections: [ - .new( - destinations: spaces.map((space) { - final notifications = space.allChildRooms.fold( - 0, - (sum, room) => - sum + (room.metadata?.unreadNotifications ?? 0), - ); - - return NavigationRailM3EDestination( - badgeCount: notifications > 0 - ? notifications - : space.allChildRooms.any( - (room) => room.metadata?.unreadMessages != 0, - ) - ? 0 - : null, - short: true, - icon: AvatarOrHash( - dimension: 28, - space.room?.metadata?.avatar, - fallback: space.icon == null ? null : Icon(space.icon), - space.title, - ), - label: space.title, - ); - }).toList(), - ), - ], - selectedIndex: max( - 0, - spaces.indexWhere((space) => space.id == selectedSpaceId), - ), - trailingAtBottom: true, - trailing: Padding( - padding: .only(top: 8), - child: Column( - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (_) => JoinDialog(ref), - ), - child: const ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - const PopupMenuItem( - onTap: null, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: const Icon(Icons.add), - ), - const IconButton( - tooltip: "Explore other rooms", - onPressed: null, - icon: Icon(Icons.explore), - ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: const Icon(Icons.settings), - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} - -class const RoomsPane({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedSpaceId = ref.watch( - KeyController.provider(KeyController.spaceKey) - .select((async) => async.requireValue), - ); - final selectedRoomId = ref.watch( - KeyController.provider(KeyController.roomKey) - .select((async) => async.requireValue), - ); - final selectedSpace = ref.watch( - SpacesController.provider.select((async) { - final spaces = async.requireValue; - return spaces.firstWhereOrNull( - (space) => space.id == selectedSpaceId, - ) ?? - spaces.first; - }), - ); - final isDm = selectedSpace.id == "dms"; - - return Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: AvatarOrHash( - selectedSpace.room?.metadata?.avatar, - fallback: selectedSpace.icon == null - ? null - : Icon(selectedSpace.icon), - selectedSpace.title, - ), - title: Text(selectedSpace.title, overflow: .ellipsis), - backgroundColor: Colors.transparent, - actions: [ - RoomMenuButton( - selectedSpace.room, - children: selectedSpace.allChildRooms, - ), - ], - ), - body: MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, - ), - ], - ), - child: NavigationRailM3E( - expandedWidth: double.infinity, - scrollable: true, - background: Colors.transparent, - type: .alwaysExpand, - selectedIndex: max( - 0, - selectedSpace.allChildRooms.indexWhere( - (room) => room.metadata?.id == selectedRoomId, - ), - ), - sections: [ - .new( - header: selectedSpace.room == null - ? null - : const DividerWidget(Text("Rooms")), - destinations: roomDestinations( - selectedSpace.children, - isDm: isDm, - ), - ), - for (final subSpace in selectedSpace.subSpaces) - .new( - header: DividerWidget( - Row( - mainAxisSize: .min, - spacing: 8, - children: [ - if (subSpace.room.metadata?.avatar != null) - AvatarOrHash( - subSpace.room.metadata?.avatar, - subSpace.room.metadata?.name ?? "Unnamed Room", - dimension: 16, - ), - Flexible( - child: Text( - subSpace.room.metadata?.name ?? "Unnamed Space", - maxLines: 1, - overflow: .ellipsis, - ), - ), - ], - ), - ), - destinations: roomDestinations( - subSpace.children, - isDm: isDm, - ), - ), - ], - onDestinationSelected: (value) { - ref - .read( - KeyController.provider(KeyController.roomKey).notifier, - ) - .set(selectedSpace.allChildRooms[value].metadata?.id); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } - }, - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/room_menu_button.dart b/lib/widgets/room_menu.dart similarity index 50% rename from lib/widgets/room_menu_button.dart rename to lib/widgets/room_menu.dart index 7e6516d..95936df 100644 --- a/lib/widgets/room_menu_button.dart +++ b/lib/widgets/room_menu.dart @@ -1,22 +1,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/via.dart"; import "package:nexus/models/room.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:nexus/main.dart"; -final class const RoomMenuButton( - final Room? room, { - final IList children = const IList.empty(), - super.key, -}) extends ConsumerWidget { +class RoomMenu extends ConsumerWidget { + final Room? room; + final IList children; + const RoomMenu(this.room, {this.children = const IList.empty(), super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final danger = Theme.of(context).colorScheme.error; - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); return PopupMenuButton( itemBuilder: (_) => [ @@ -37,7 +35,8 @@ final class const RoomMenuButton( await Clipboard.setData( .new( - text: "matrix:roomid/${room!.metadata?.id.substring(1)}$vias", + text: + "matrix:roomid/${room!.metadata?.id.substring(1)}$vias)", ), ); }, @@ -46,63 +45,6 @@ final class const RoomMenuButton( title: Text("Copy Link"), ), ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final userIdController = useTextEditingController(); - final text = useValueListenable(userIdController).text.trim(); - final valid = RegExp(r"^@[^:\s]+:[^\s]+$").hasMatch(text); - - void submit() { - if (!valid) return; - Navigator.of(context).pop(); - client - .setMembership( - .new( - userId: text, - roomId: room!.metadata!.id, - action: .invite, - ), - ) - .onError(showError); - } - - return AlertDialog( - title: Text("Invite User"), - content: TextField( - controller: userIdController, - autofocus: true, - keyboardType: .emailAddress, - onSubmitted: (_) => submit(), - decoration: .new( - labelText: "User ID", - hintText: "@user:example.com", - errorText: text.isEmpty || valid - ? null - : "Enter a valid user ID", - ), - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: valid ? submit : null, - child: Text("Invite"), - ), - ], - ); - }, - ), - ), - child: ListTile( - leading: Icon(Icons.person_add), - title: Text("Invite"), - ), - ), PopupMenuItem( onTap: () => showDialog( context: context, @@ -140,6 +82,54 @@ final class const RoomMenuButton( ), ), ], + + // PopupMenuItem( + // onTap: () => showDialog( + // context: context, + // builder: (context) => HookBuilder( + // builder: (_) { + // final reasonController = useTextEditingController(); + // return AlertDialog( + // title: Text("Report"), + // content: Column( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // "Report this room to your server administrators, who can take action like banning this room.", + // ), + + // SizedBox(height: 12), + // FormTextInput( + // required: false, + // capitalize: true, + // controller: reasonController, + // title: "Reason for report (optional)", + // ), + // ], + // ), + // actions: [ + // TextButton( + // onPressed: Navigator.of(context).pop, + // child: Text("Cancel"), + // ), + // TextButton( + // onPressed: () { + // room.client.reportRoom(room.id, reasonController.text); + // Navigator.of(context).pop(); + // }, + // child: Text("Report"), + // ), + // ], + // ); + // }, + // ), + // ), + // child: ListTile( + // leading: Icon(Icons.report, color: danger), + // title: Text("Report", style: TextStyle(color: danger)), + // ), + // ), ], ); } diff --git a/lib/widgets/room_summary_dialog.dart b/lib/widgets/room_summary_dialog.dart deleted file mode 100644 index 496e72f..0000000 --- a/lib/widgets/room_summary_dialog.dart +++ /dev/null @@ -1,122 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/helpers/extensions/focus_room.dart"; -import "package:nexus/widgets/avatar_or_hash.dart"; -import "package:nexus/widgets/expandable_image.dart"; -import "package:nexus/widgets/linkified_text.dart"; -import "package:nexus/models/room_summary.dart"; - -class const RoomSummaryDialog( - final RoomSummary summary, { - final IList? via, - super.key, -}) extends StatelessWidget { - @override - Widget build(BuildContext context) => AlertDialog( - constraints: .loose(.fromWidth(400)), - scrollable: true, - contentPadding: EdgeInsets.all(24).copyWith(top: 8), - title: Row( - spacing: 12, - mainAxisSize: .min, - children: [ - if (summary.avatarUrl != null) - ExpandableImage( - summary.avatarUrl == null ? null : .new(mxc: summary.avatarUrl!), - child: AvatarOrHash( - summary.avatarUrl, - "", - dimension: 64, - fallback: Icon(Icons.numbers), - ), - ), - Expanded( - child: Text( - summary.name ?? summary.canonicalAlias ?? "Unnamed Room", - overflow: .ellipsis, - maxLines: 3, - ), - ), - ], - ), - content: Column( - children: [ - ListTile( - title: Text( - "${summary.joinedMembers ?? "Unknown number of"} members", - ), - leading: Icon(Icons.people), - ), - ListTile( - title: SelectableText(summary.canonicalAlias ?? summary.roomId), - leading: Icon(Icons.numbers), - ), - - if (summary.topic != null) - ListTile( - titleAlignment: ListTileTitleAlignment.titleHeight, - title: LinkifiedText( - summary.topic!, - style: Theme.of(context).textTheme.bodyLarge, - ), - leading: Icon(Icons.info), - ), - ], - ), - - actions: [ - TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")), - if (via != null) - TextButton( - onPressed: () async { - // Capture everything before the dialog closes. - final container = ProviderScope.containerOf(context); - final scaffoldMessenger = ScaffoldMessenger.of(context); - final colors = Theme.of(context).colorScheme; - Navigator.of(context).pop(); - - final roomIdOrAlias = summary.canonicalAlias ?? summary.roomId; - - final snackbar = scaffoldMessenger.showSnackBar( - .new( - content: Text("Joining room $roomIdOrAlias."), - duration: Duration(days: 999), - ), - ); - - try { - final id = await container - .read(ClientController.provider.notifier) - .joinRoom(.new(roomIdOrAlias: summary.roomId, via: via!)); - - snackbar.close(); - - scaffoldMessenger.showSnackBar( - .new( - content: Text("Room $roomIdOrAlias successfully joined."), - action: .new( - label: "Open", - onPressed: () => container.focusRoom(id), - ), - ), - ); - } catch (error) { - snackbar.close(); - scaffoldMessenger.showSnackBar( - .new( - backgroundColor: colors.errorContainer, - content: Text( - error.toString(), - style: .new(color: colors.onErrorContainer), - ), - ), - ); - } - }, - child: Text("Join"), - ), - ], - ); -} diff --git a/lib/widgets/settings/dialog_list_tile.dart b/lib/widgets/settings/dialog_list_tile.dart index e66e335..f0a236c 100644 --- a/lib/widgets/settings/dialog_list_tile.dart +++ b/lib/widgets/settings/dialog_list_tile.dart @@ -1,18 +1,28 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/widgets/settings/radio_dialog.dart"; -final class const DialogListTile({ - required final T? initialValue, - required final Icon icon, - required final String title, - required final List options, - required final void Function(T value)? onChanged, - required final String Function(T option) getName, - final Widget? subtitle, - final bool required = true, - super.key, -}) extends ConsumerWidget { +class DialogListTile extends ConsumerWidget { + final T? initialValue; + final String title; + final Widget? subtitle; + final List options; + final bool required; + final Icon icon; + final void Function(T value)? onChanged; + final String Function(T option) getName; + const DialogListTile({ + super.key, + required this.icon, + required this.title, + required this.initialValue, + required this.options, + required this.onChanged, + required this.getName, + this.subtitle, + this.required = true, + }); + @override Widget build(BuildContext context, WidgetRef ref) => FormField( validator: (value) => diff --git a/lib/widgets/settings/radio_dialog.dart b/lib/widgets/settings/radio_dialog.dart index 6696139..7695b9d 100644 --- a/lib/widgets/settings/radio_dialog.dart +++ b/lib/widgets/settings/radio_dialog.dart @@ -1,14 +1,21 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -final class const RadioDialog({ - required final T? value, - required final String title, - required final List options, - required final void Function(T value)? onChanged, - required final String Function(T option) getName, - super.key, -}) extends HookWidget { +class RadioDialog extends HookWidget { + final T? value; + final String title; + final List options; + final void Function(T value)? onChanged; + final String Function(T option) getName; + const RadioDialog({ + super.key, + required this.title, + required this.value, + required this.options, + required this.onChanged, + required this.getName, + }); + @override Widget build(BuildContext context) { final mutValue = useState(null); diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 2d1c728..1bd6cf5 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -1,34 +1,290 @@ -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:material_ui/material_ui.dart"; -import "package:nexus/models/nav_page.dart"; -import "package:nexus/widgets/invites_list.dart"; -import "package:nexus/widgets/notifications_list.dart"; -import "package:nexus/widgets/room_list.dart"; +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:navigation_rail_m3e/navigation_rail_m3e.dart"; +import "package:nexus/controllers/key.dart"; +import "package:nexus/controllers/spaces.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/divider_widget.dart"; +import "package:nexus/widgets/join_dialog.dart"; +import "package:nexus/widgets/room_menu.dart"; + +class Sidebar extends HookConsumerWidget { + final bool isDesktop; + const Sidebar({required this.isDesktop, super.key}); -class const Sidebar({super.key}) extends HookWidget { @override - Widget build(BuildContext context) { - final selectedIndex = useState(0); - const pages = [RoomList(), NotificationsList(), InvitesList()]; + Widget build(BuildContext context, WidgetRef ref) { + final selectedSpaceProvider = KeyController.provider( + KeyController.spaceKey, + ); + final selectedSpaceId = ref.watch(selectedSpaceProvider); + final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); + + final selectedRoomController = KeyController.provider( + KeyController.roomKey, + ); + final selectedRoomId = ref.watch(selectedRoomController); + final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); + + final spaces = ref.watch(SpacesController.provider); + final indexOfSelected = spaces.indexWhere( + (space) => space.id == selectedSpaceId, + ); + final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected; + + final selectedSpace = + spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ?? + spaces.first; + + final indexOfSelectedRoom = selectedSpace.children + .addAll( + selectedSpace.subSpaces.map((element) => element.children).flattened, + ) + .indexWhere((room) => room.metadata?.id == selectedRoomId); + final selectedRoomIndex = indexOfSelectedRoom == -1 + ? null + : indexOfSelectedRoom; + + List roomsToDestinations(IList rooms) => + rooms + .map( + (room) => NavigationRailM3EDestination( + label: room.metadata?.name ?? "Unnamed Room", + badgeCount: switch (room.metadata?.unreadNotifications) { + 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, + int unread => unread, + }, + icon: AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Room", + fallback: selectedSpaceId == "dms" + ? null + : Icon(Icons.numbers), + ), + ), + ) + .toList(); return Drawer( width: 330, shape: Border(), - child: Scaffold( - backgroundColor: Colors.transparent, - body: pages[selectedIndex.value], - bottomNavigationBar: NavigationBar( - selectedIndex: selectedIndex.value, - destinations: pages - .map( - (element) => NavigationDestination( - icon: Icon(element.icon), - label: element.title, + child: Row( + children: [ + Theme( + data: Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemCollapsedHeight: 48, + itemVerticalGap: 0, ), - ) - .toList(), - onDestinationSelected: (value) => selectedIndex.value = value, - ), + ], + ), + child: Container( + color: NavigationRailTokensAdapter(context).containerColor, + padding: EdgeInsets.only(top: 16), + child: NavigationRailM3E( + type: .alwaysCollapse, + labelBehavior: .alwaysHide, + scrollable: true, + onDestinationSelected: (value) { + selectedSpaceIdNotifier.set(spaces[value].id); + selectedRoomIdNotifier.set( + spaces[value].children.firstOrNull?.metadata?.id, + ); + }, + sections: [ + .new( + destinations: spaces + .map( + (space) => NavigationRailM3EDestination( + badgeCount: switch (space.children + .addAll( + space.subSpaces + .map((element) => element.children) + .flattened, + ) + .fold( + 0, + (previousValue, room) => + previousValue + + (room.metadata?.unreadNotifications ?? 0), + )) { + 0 => + space.children + .addAll( + space.subSpaces + .map( + (element) => element.children, + ) + .flattened, + ) + .any( + (room) => + room.metadata?.unreadMessages != + 0, + ) + ? 0 + : null, + int badgeCount => badgeCount, + }, + short: true, + icon: AvatarOrHash( + height: 28, + space.room?.metadata?.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + ), + label: space.title, + ), + ) + .toList(), + ), + ], + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: .symmetric(vertical: 16), + child: Column( + spacing: 8, + children: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (_) => JoinDialog(ref), + ), + child: ListTile( + title: Text("Join an existing room (or space)"), + leading: Icon(Icons.numbers), + ), + ), + PopupMenuItem( + onTap: null, + child: ListTile( + title: Text("Create a new room"), + leading: Icon(Icons.add), + ), + ), + ], + icon: Icon(Icons.add), + ), + IconButton( + tooltip: "Explore other rooms", + onPressed: null, + icon: Icon(Icons.explore), + ), + IconButton( + tooltip: "Open settings", + onPressed: () => showDialog( + context: context, + builder: (_) => SettingsPage(), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ), + ), + ), + Expanded( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: AvatarOrHash( + selectedSpace.room?.metadata?.avatar, + fallback: selectedSpace.icon == null + ? null + : Icon(selectedSpace.icon), + + selectedSpace.title, + ), + title: Text(selectedSpace.title, overflow: .ellipsis), + backgroundColor: Colors.transparent, + actions: [ + RoomMenu( + selectedSpace.room, + children: selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ), + ), + ], + ), + body: Theme( + data: Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemExpandedHeight: 48, + iconLabelGap: 16, + ), + ], + ), + child: NavigationRailM3E( + expandedWidth: double.infinity, + scrollable: true, + background: Colors.transparent, + type: .alwaysExpand, + selectedIndex: selectedRoomIndex ?? 0, + sections: [ + .new( + header: selectedSpace.room == null + ? null + : DividerWidget(Text("Rooms")), + destinations: roomsToDestinations(selectedSpace.children), + ), + for (final subSpace in selectedSpace.subSpaces) + .new( + header: DividerWidget( + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + if (subSpace.room.metadata?.avatar != null) + AvatarOrHash( + subSpace.room.metadata?.avatar, + subSpace.room.metadata?.name ?? + "Unnamed Room", + height: 16, + ), + Flexible( + child: Text( + subSpace.room.metadata?.name ?? + "Unnamed Space", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + destinations: roomsToDestinations(subSpace.children), + ), + ], + onDestinationSelected: (value) { + final children = selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ); + selectedRoomIdNotifier.set( + children[value].metadata?.id, // + ); + if (!isDesktop) Navigator.of(context).pop(); + }, + ), + ), + ), + ), + ], ), ); } diff --git a/lib/widgets/timestamp.dart b/lib/widgets/timestamp.dart deleted file mode 100644 index bcdabd1..0000000 --- a/lib/widgets/timestamp.dart +++ /dev/null @@ -1,17 +0,0 @@ -import "package:flutter/material.dart"; -import "package:timeago/timeago.dart"; - -class const Timestamp(final DateTime timestamp, {super.key}) - extends StatelessWidget { - @override - Widget build(BuildContext context) => Tooltip( - message: timestamp.toString(), - child: Text( - format(timestamp), - maxLines: 1, - overflow: .ellipsis, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: Colors.grey), - ), - ); -} diff --git a/lib/widgets/url_preview.dart b/lib/widgets/url_preview.dart index 5eda427..5831300 100644 --- a/lib/widgets/url_preview.dart +++ b/lib/widgets/url_preview.dart @@ -1,11 +1,14 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/url_preview.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/mxc_image.dart"; -class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { +class UrlPreview extends ConsumerWidget { + final String link; + const UrlPreview(this.link, {super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox( constraints: .loose(.fromWidth(400)), @@ -15,12 +18,13 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { data: (preview) => preview == null ? SizedBox.shrink() : InkWell( - onTap: () => ref.watch(LaunchHelper.provider).launchUrl(link), + onTap: () => + ref.watch(LaunchHelper.provider).launchUrl(.parse(link)), child: Card( margin: .symmetric(vertical: 4), - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, child: Padding( padding: .all(16), child: Column( @@ -31,15 +35,9 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { Text( preview.title!, style: Theme.of(context).textTheme.titleLarge, - maxLines: 3, - overflow: .ellipsis, ), if (preview.description != null) ...[ - Text( - preview.description!, - maxLines: 20, - overflow: .ellipsis, - ), + Text(preview.description!), SizedBox(height: 4), ], if (preview.imageUrl != null) diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart index d7cc55b..6f9f154 100644 --- a/lib/widgets/user_bottom_sheet.dart +++ b/lib/widgets/user_bottom_sheet.dart @@ -1,5 +1,5 @@ import "package:collection/collection.dart"; -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:intl/intl.dart"; @@ -16,17 +16,17 @@ import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/expandable_image.dart"; -final class const UserBottomSheet( - final MembershipContent member, - final String userId, { - final String? roomId, - super.key, -}) extends ConsumerWidget { +class UserBottomSheet extends ConsumerWidget { + final MembershipContent member; + final String userId; + final String? roomId; + const UserBottomSheet(this.member, this.userId, {this.roomId, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final textTheme = theme.textTheme; - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); void showMembershipDialog(MembershipAction action) => showDialog( context: context, @@ -77,180 +77,178 @@ final class const UserBottomSheet( ), ); - return SingleChildScrollView( - child: Padding( - padding: .all(42), - child: Column( - spacing: 4, - mainAxisSize: .min, - crossAxisAlignment: .center, - children: [ - Row( - mainAxisAlignment: .end, - children: [ - M3EButton( - onPressed: Navigator.of(context).pop, - child: Icon(Icons.close), - ), - ], - ), - SizedBox(height: 18), - - ExpandableImage( - member.avatarUrl == null ? null : .new(mxc: member.avatarUrl!), - child: AvatarOrHash( - member.avatarUrl, - member.displayName ?? userId.localpart, - dimension: 200, + return Padding( + padding: .all(42), + child: Column( + spacing: 4, + mainAxisSize: .min, + crossAxisAlignment: .center, + children: [ + Row( + mainAxisAlignment: .end, + children: [ + M3EButton( + onPressed: Navigator.of(context).pop, + child: Icon(Icons.close), ), - ), + ], + ), + SizedBox(height: 18), - SizedBox(height: 8), - - SelectableText( + ExpandableImage( + member.avatarUrl == null ? null : .new(mxc: member.avatarUrl!), + child: AvatarOrHash( + member.avatarUrl, member.displayName ?? userId.localpart, - style: textTheme.headlineLarge, - textAlign: .center, - ), - SelectableText( - userId, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + height: 200, ), + ), - ref - .watch(ProfileController.provider(userId)) - .betterWhen( - loading: () => Text(""), - data: (profileResponse) => Column( - children: [ - if (profileResponse.profile.timezone == null && - profileResponse.profile.pronouns.isEmpty) - Text(""), - Wrap( - crossAxisAlignment: .center, - alignment: .center, - spacing: 4, - runSpacing: 4, - children: [ - ...profileResponse.profile.pronouns - .where( - // TODO: Check system language (l10n) - (pronoun) => pronoun.language == "en", - ) - .mapIndexed( - (index, pronoun) => [ - if (index != 0) - Icon( - Icons.circle, - size: 4, - color: theme.colorScheme.onSurfaceVariant, - ), - Text( - pronoun.summary, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + SizedBox(height: 8), + + SelectableText( + member.displayName ?? userId.localpart, + style: textTheme.headlineLarge, + textAlign: .center, + ), + SelectableText( + userId, + textAlign: .center, + style: textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + + ref + .watch(ProfileController.provider(userId)) + .betterWhen( + loading: () => Text(""), + data: (profile) => Column( + children: [ + if (profile.timezone == null && profile.pronouns.isEmpty) + Text(""), + Wrap( + crossAxisAlignment: .center, + alignment: .center, + spacing: 4, + runSpacing: 4, + children: [ + ...profile.pronouns + .where( + // TODO: Check system language (l10n) + (pronoun) => pronoun.language == "en", + ) + .mapIndexed( + (index, pronoun) => [ + if (index != 0) + Icon( + Icons.circle, + size: 4, + color: theme.colorScheme.onSurfaceVariant, + ), + Text( + pronoun.summary, + textAlign: .center, + style: textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, ), - ], - ) - .flattened, - - if (profileResponse.profile.timezone != null) ...[ - if (profileResponse.profile.pronouns.isNotEmpty) - SizedBox( - height: 16, - child: VerticalDivider( - thickness: 1.5, - width: 4, - color: theme.colorScheme.onSurfaceVariant, ), - ), - Text( - profileResponse.profile.timezone!, - textAlign: .center, - style: textTheme.titleSmall?.copyWith( + ], + ) + .flattened, + + if (profile.timezone != null) ...[ + if (profile.pronouns.isNotEmpty) + SizedBox( + height: 16, + child: VerticalDivider( + thickness: 1.5, + width: 4, color: theme.colorScheme.onSurfaceVariant, ), ), - ], + Text( + profile.timezone!, + textAlign: .center, + style: textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), ], - ), - ], - ), - ), - - SizedBox(height: 8), - if (userId != ref.watch(ClientStateController.provider)?.userId && - roomId != null) ...[ - Row( - children: [ - Expanded( - child: M3EButton.icon( - onPressed: null, - shape: .square, - style: .tonal, - icon: Icon(Icons.message), - label: Text("Message"), + ], ), - ), - ], + ], + ), ), - if (ref.watch( - PowerLevelController.provider( - .membershipAction( - action: .kick, - roomId: roomId!, - targetUser: userId, + SizedBox(height: 8), + if (userId != ref.watch(ClientStateController.provider)?.userId && + roomId != null) ...[ + Row( + children: [ + Expanded( + child: M3EButton.icon( + onPressed: null, + shape: .square, + style: .tonal, + icon: Icon(Icons.message), + label: Text("Message"), ), ), - )) - Padding( - padding: .only(top: 4), - child: Row( - mainAxisSize: MainAxisSize.max, - spacing: 8, - children: [ - if (member.status == .join || member.status == .invite) - M3EButton.icon( - onPressed: () => showMembershipDialog(.kick), - shape: .square, - icon: Icon(Icons.sports_martial_arts), - label: Text("Kick"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.error, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onError, - ), - ), - ), + ], + ), - M3EButton.icon( - onPressed: () => showMembershipDialog(.ban), - shape: .square, - icon: Icon(Icons.gavel), - label: Text("Ban"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.errorContainer, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onErrorContainer, - ), + if (ref.watch( + PowerLevelController.provider( + .membershipAction( + action: .kick, + roomId: roomId!, + targetUser: userId, ), ), - ].map((e) => Expanded(child: e)).toList(), - ), + ) && + member.status == .join || + member.status == .invite) + Padding( + padding: .only(top: 4), + child: Row( + mainAxisSize: MainAxisSize.max, + spacing: 8, + children: [ + M3EButton.icon( + onPressed: () => showMembershipDialog(.kick), + shape: .square, + icon: Icon(Icons.sports_martial_arts), + label: Text("Kick"), + decoration: .new( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.error, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onError, + ), + ), + ), + + M3EButton.icon( + onPressed: () => showMembershipDialog(.ban), + shape: .square, + icon: Icon(Icons.gavel), + label: Text("Ban"), + decoration: .new( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.errorContainer, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onErrorContainer, + ), + ), + ), + ].map((e) => Expanded(child: e)).toList(), ), - ], + ), ], - ), + ], ), ); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 70d9ba2..603ea6a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,7 +13,6 @@ #include #include #include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -38,9 +37,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); - g_autoptr(FlPluginRegistrar) webcrypto_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin"); - webcrypto_plugin_register_with_registrar(webcrypto_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1d53ad7..7d8f046 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever_linux url_launcher_linux - webcrypto window_manager ) diff --git a/linux/nexus.federated.nexus.UnifiedPush.service b/linux/nexus.federated.nexus.UnifiedPush.service deleted file mode 100644 index 663ab9b..0000000 --- a/linux/nexus.federated.nexus.UnifiedPush.service +++ /dev/null @@ -1,3 +0,0 @@ -[D-BUS Service] -Name=nexus.federated.nexus.UnifiedPush -Exec=/usr/bin/env FLUTTER_HEADLESS=1 nexus diff --git a/linux/nexus.federated.nexus.desktop b/linux/nexus.federated.nexus.desktop index 64b4264..d61734e 100644 --- a/linux/nexus.federated.nexus.desktop +++ b/linux/nexus.federated.nexus.desktop @@ -2,9 +2,9 @@ Name=Nexus GenericName=Matrix Client Comment=A simple and user-friendly Matrix client -Exec=nexus +Exec=nexus %u Icon=nexus Terminal=false Type=Application Categories=Chat;Network;InstantMessaging; -DBusActivatable=true \ No newline at end of file +MimeType=x-scheme-handler/nexus.federated.nexus; \ No newline at end of file diff --git a/linux/nexus.federated.nexus.service b/linux/nexus.federated.nexus.service deleted file mode 100644 index 1757c2c..0000000 --- a/linux/nexus.federated.nexus.service +++ /dev/null @@ -1,3 +0,0 @@ -[D-BUS Service] -Name=nexus.federated.nexus -Exec=/usr/bin/env nexus diff --git a/linux/nexus.federated.nexus.uri.desktop b/linux/nexus.federated.nexus.uri.desktop deleted file mode 100644 index 10f495f..0000000 --- a/linux/nexus.federated.nexus.uri.desktop +++ /dev/null @@ -1,8 +0,0 @@ -[Desktop Entry] -Name=Nexus URI Handler -Exec=nexus %u -Icon=nexus -Terminal=false -Type=Application -NoDisplay=true -MimeType=x-scheme-handler/nexus.federated.nexus; \ No newline at end of file diff --git a/linux/nix/default.nix b/linux/nix/default.nix deleted file mode 100644 index 3c0a301..0000000 --- a/linux/nix/default.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ self, inputs, ... }: { - - perSystem = - { - pkgs, - system, - lib, - ... - }: - { - _module.args.pkgs = import inputs.nixpkgs { - inherit system; - config = { - android_sdk.accept_license = true; - allowUnfree = true; - }; - }; - - packages = - let - default = pkgs.callPackage ./pkg { - src = self; - commit = self.shortRev or self.dirtyShortRev; - }; - in - { - inherit default; - - flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak { - appName = "Nexus"; - developer = "QuadRadical"; - appId = "nexus.federated.nexus"; - package = default; - runtime = "org.gnome.Platform/51"; - permissions = { - share = [ "network" ]; - sockets = [ - "pulseaudio" - "fallback-x11" - "wayland" - ]; - - talk-names = [ - "org.unifiedpush.Distributor.*" - "org.freedesktop.Notifications" - ]; - devices = [ "dri" ]; - }; - }; - - gomuks = pkgs.callPackage ./pkg/gomuks.nix { - src = self; - }; - }; - - devShells.default = pkgs.callPackage ./devshell.nix { }; - }; - - flake.nixosModules.default = import ./module.nix self; -} diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix index 8bc8641..ae77467 100644 --- a/linux/nix/devshell.nix +++ b/linux/nix/devshell.nix @@ -2,7 +2,7 @@ let android = pkgs.androidenv.composeAndroidPackages { toolsVersion = "26.1.1"; - platformToolsVersion = "37.0.1"; + platformToolsVersion = "36.0.1"; buildToolsVersions = [ "35.0.0" "36.0.0" @@ -18,33 +18,31 @@ let }; in pkgs.mkShell { - packages = - with pkgs; - [ - go - git - - libGL - (flutter.override { - extraPkgConfigPackages = [ - mpv-unwrapped - libass - ]; - }) - android.platform-tools - ] - ++ lib.optional pkgs.stdenv.isLinux wayland; + packages = with pkgs; [ + go + git + jdk17 + libGL + wayland + (flutter.override { + extraPkgConfigPackages = [ + mpv-unwrapped + libass + ]; + }) + 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.jdk21; + JAVA_HOME = pkgs.jdk17; - GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2"; - } - // lib.optionalAttrs pkgs.stdenv.isLinux { - LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; + TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; }; } diff --git a/linux/nix/module.nix b/linux/nix/module.nix deleted file mode 100644 index a8abd6c..0000000 --- a/linux/nix/module.nix +++ /dev/null @@ -1,45 +0,0 @@ -self: -{ - pkgs, - lib, - config, - ... -}: - -let - inherit (lib) - mkIf - mkMerge - mkEnableOption - mkPackageOption - ; - - cfg = config.programs.nexus; -in -{ - options.programs.nexus = { - enable = mkEnableOption "Nexus, a simple and user-friendly Matrix client"; - package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "default" { }; - - enableNotifications = mkEnableOption "notifications support via UnifiedPush"; - }; - - config = mkIf cfg.enable (mkMerge [ - { - environment.systemPackages = [ cfg.package ]; - } - (mkIf cfg.enableNotifications { - environment.systemPackages = [ - pkgs.kdePackages.kunifiedpush - ]; - - systemd.packages = [ - pkgs.kdePackages.kunifiedpush - ]; - - services.dbus.packages = [ - cfg.package - ]; - }) - ]); -} diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix index 7574d45..4e00959 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -5,9 +5,7 @@ libass, libclang, flutter, - stdenv, src, - commit ? "dev", }: flutter.buildFlutterApplication { @@ -21,10 +19,6 @@ flutter.buildFlutterApplication { packageRun build_runner build ''; - flutterBuildFlags = [ - "--dart-define=GIT_COMMIT=${commit}" - ]; - buildInputs = [ mpv-unwrapped libass @@ -38,15 +32,11 @@ flutter.buildFlutterApplication { emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; linkify = "sha256-TpMD6+0zyY6i9l+6d8ErnVufmepCv362rCtnbOht/z4="; navigation_rail_m3e = "sha256-+2awDTQnK58gGRY1nuHckG/jjxarsYSRu9ovR4i4TEc="; - unifiedpush_linux = "sha256-aIF/8oabSuuZo7K6qr4r7UBmf57qgoGAXyJ40fdntuQ="; - unifiedpush_platform_interface = "sha256-aIF/8oabSuuZo7K6qr4r7UBmf57qgoGAXyJ40fdntuQ="; - xdg_desktop_portal = "sha256-8GgiXHGv+wy6hJgj/rZxbymMitNVmdDLAnedZm9STSc="; }; postInstall = '' - install -D assets/bundled/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg - install -Dm755 linux/*.desktop -t $out/share/applications - install -Dm644 linux/*.service -t $out/share/dbus-1/services + 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 ''; @@ -54,11 +44,7 @@ flutter.buildFlutterApplication { meta = { description = "A simple and user-friendly Matrix client"; mainProgram = "nexus"; - platforms = lib.platforms.unix; - - # https://github.com/NixOS/nixpkgs/blob/c59305bab2065cfecc4944690d9eedbb56f3a9fa/pkgs/development/compilers/flutter/build-support/build-flutter-application.nix#L15 - broken = stdenv.hostPlatform.isDarwin; - + platforms = lib.platforms.linux; maintainers = with lib.maintainers; [ quadradical ]; }; } diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix index d84450c..40fe29c 100644 --- a/linux/nix/pkg/gomuks.nix +++ b/linux/nix/pkg/gomuks.nix @@ -1,38 +1,31 @@ { src, - stdenv, buildGoModule, }: -buildGoModule ( - finalAttrs: - let - filename = "libgomuks${stdenv.hostPlatform.extensions.sharedLibrary}"; - in - { - pname = "gomuks-ffi"; - version = "submodule"; +buildGoModule (finalAttrs: { + pname = "gomuks-ffi"; + version = "submodule"; - doCheck = false; + doCheck = false; - src = "${src}/gomuks"; + src = "${src}/gomuks"; - vendorHash = "sha256-lKiVzlFk0yffT6uUiSqQo/CiyTOfgKqiD+MEMoBvmE8="; + vendorHash = "sha256-4QU51WGiNKYm7z/ajnas2qQaMK5BgyHwPfmNpQvW0qg="; - buildPhase = '' - runHook preBuild + buildPhase = '' + runHook preBuild - go build -buildmode=c-shared -o ${filename} -tags goolm,noheic,sqlite_fts5 ./pkg/ffi + go build -buildmode=c-shared -o libgomuks.so -tags goolm,noheic,sqlite_fts5 ./pkg/ffi - runHook postBuild - ''; + runHook postBuild + ''; - installPhase = '' - runHook preInstall + installPhase = '' + runHook preInstall - install -Dm0644 ${filename} -t $out/lib + install -Dm0644 libgomuks.so -t $out/lib - runHook postInstall - ''; - } -) + runHook postInstall + ''; +}) diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index bf6e5ef..62e5450 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -1,11 +1,6 @@ #include "my_application.h" -#include - #include -#include -#include - #ifdef GDK_WINDOWING_X11 #include #endif @@ -14,384 +9,144 @@ struct _MyApplication { GtkApplication parent_instance; - char** dart_entrypoint_arguments; - - FlEngine* flutter_engine; - FlMethodChannel* notification_channel; - - gchar* pending_notification_event_id; - gboolean flutter_ready; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) -static void my_application_activate(GApplication* application); - -static void send_notification_event_to_flutter( - MyApplication* self, - const gchar* event_id) { - if (self->notification_channel == nullptr || - !self->flutter_ready) { - g_free(self->pending_notification_event_id); - self->pending_notification_event_id = g_strdup(event_id); - return; - } - - g_autoptr(FlValue) args = - fl_value_new_string(event_id); - - fl_method_channel_invoke_method( - self->notification_channel, - "notificationClicked", - args, - nullptr, - nullptr, - nullptr); +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); } -static void notification_event_action( - GSimpleAction* action, - GVariant* parameter, - gpointer user_data) { - MyApplication* self = - MY_APPLICATION(user_data); +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); - if (parameter == nullptr) { - g_warning( - "Notification event action invoked without a target"); - return; - } - - if (!g_variant_is_of_type( - parameter, - G_VARIANT_TYPE_STRING)) { - g_warning( - "Notification event action received unexpected parameter type: %s", - g_variant_get_type_string(parameter)); - return; - } - - const gchar* event_id = - g_variant_get_string(parameter, nullptr); - - g_message( - "Notification event activated: %s", - event_id); - - send_notification_event_to_flutter( - self, - event_id); - - GList* windows = - gtk_application_get_windows( - GTK_APPLICATION(self)); - - if (windows != nullptr) { - gtk_window_present( - GTK_WINDOW(windows->data)); - } else { - my_application_activate( - G_APPLICATION(self)); - } -} - -static void first_frame_cb( - MyApplication* self, - FlView* view) { - self->flutter_ready = TRUE; - - gtk_widget_show( - gtk_widget_get_toplevel( - GTK_WIDGET(view))); - - if (self->pending_notification_event_id != nullptr) { - gchar* event_id = - self->pending_notification_event_id; - - self->pending_notification_event_id = nullptr; - - send_notification_event_to_flutter( - self, - event_id); - - g_free(event_id); - } -} - -static void my_application_activate( - GApplication* application) { - MyApplication* self = - MY_APPLICATION(application); - - GList* windows = - gtk_application_get_windows( - GTK_APPLICATION(application)); - - if (windows != nullptr) { - gtk_window_present( - GTK_WINDOW(windows->data)); + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); return; } GtkWindow* window = - GTK_WINDOW( - gtk_application_window_new( - GTK_APPLICATION(application))); + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). gboolean use_header_bar = TRUE; - #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = - gtk_window_get_screen(window); - + GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = - gdk_x11_screen_get_window_manager_name(screen); - + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif - - gtk_widget_set_size_request( - GTK_WIDGET(window), - 250, - -1); - + 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)); - - gtk_header_bar_set_title( - header_bar, - "nexus"); - - gtk_header_bar_set_show_close_button( - header_bar, - TRUE); - - gtk_window_set_titlebar( - window, - GTK_WIDGET(header_bar)); + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "nexus"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title( - window, - "nexus"); + gtk_window_set_title(window, "nexus"); } - gtk_window_set_default_size( - window, - 1280, - 720); + gtk_window_set_default_size(window, 1280, 720); - g_autoptr(FlDartProject) project = - fl_dart_project_new(); - - fl_dart_project_set_dart_entrypoint_arguments( - project, - self->dart_entrypoint_arguments); - - FlView* view = - fl_view_new(project); - - self->flutter_engine = - fl_view_get_engine(view); - - g_autoptr(FlStandardMethodCodec) codec = - fl_standard_method_codec_new(); - - self->notification_channel = - fl_method_channel_new( - fl_engine_get_binary_messenger( - self->flutter_engine), - "nexus/notifications", - FL_METHOD_CODEC(codec)); + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + FlView* view = fl_view_new(project); GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - gdk_rgba_parse( - &background_color, - "#000000"); + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); - fl_view_set_background_color( - view, - &background_color); + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - gtk_widget_show( - GTK_WIDGET(view)); - - if (std::getenv("FLUTTER_HEADLESS")) { - gtk_widget_hide( - GTK_WIDGET(window)); - } - - gtk_container_add( - GTK_CONTAINER(window), - GTK_WIDGET(view)); - - g_signal_connect_swapped( - view, - "first-frame", - G_CALLBACK(first_frame_cb), - self); - - gtk_widget_realize( - GTK_WIDGET(view)); - - fl_register_plugins( - FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus( - GTK_WIDGET(view)); + gtk_widget_grab_focus(GTK_WIDGET(view)); } -static gboolean my_application_local_command_line( - GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = - MY_APPLICATION(application); - - self->dart_entrypoint_arguments = - g_strdupv(*arguments + 1); +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - - if (!g_application_register( - application, - nullptr, - &error)) { - g_warning( - "Failed to register: %s", - error->message); - - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); - *exit_status = 0; return FALSE; } -static void my_application_startup( - GApplication* application) { - g_message( - "GApplication application-id: %s", - g_application_get_application_id( - application)); +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); - g_message( - "GApplication is remote: %d", - g_application_get_is_remote( - application)); + // Perform any actions required at application startup. - g_message( - "GApplication is registered: %d", - g_application_get_is_registered( - application)); - - g_message( - "prgname: %s", - g_get_prgname()); - - G_APPLICATION_CLASS( - my_application_parent_class) - ->startup(application); + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } -static void my_application_shutdown( - GApplication* application) { - G_APPLICATION_CLASS( - my_application_parent_class) - ->shutdown(application); +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); } -static void my_application_dispose( - GObject* object) { - MyApplication* self = - MY_APPLICATION(object); - - g_clear_pointer( - &self->dart_entrypoint_arguments, - g_strfreev); - - g_clear_pointer( - &self->pending_notification_event_id, - g_free); - - g_clear_object( - &self->notification_channel); - - self->flutter_engine = nullptr; - - G_OBJECT_CLASS( - my_application_parent_class) - ->dispose(object); +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init( - MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = - my_application_activate; - - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - - G_APPLICATION_CLASS(klass)->startup = - my_application_startup; - - G_APPLICATION_CLASS(klass)->shutdown = - my_application_shutdown; - - G_OBJECT_CLASS(klass)->dispose = - my_application_dispose; +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init( - MyApplication* self) { - self->dart_entrypoint_arguments = nullptr; - self->flutter_engine = nullptr; - self->notification_channel = nullptr; - self->pending_notification_event_id = nullptr; - self->flutter_ready = FALSE; - - GSimpleAction* action = - g_simple_action_new( - "event", - G_VARIANT_TYPE_STRING); - - g_signal_connect( - action, - "activate", - G_CALLBACK(notification_event_action), - self); - - g_action_map_add_action( - G_ACTION_MAP(self), - G_ACTION(action)); - - g_object_unref(action); -} +static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. g_set_prgname(APPLICATION_ID); - return MY_APPLICATION( - g_object_new( - my_application_get_type(), - "application-id", - APPLICATION_ID, - "flags", - G_APPLICATION_HANDLES_COMMAND_LINE | - G_APPLICATION_HANDLES_OPEN, - nullptr)); -} \ No newline at end of file + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, + nullptr)); +} diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b..c2efd0b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1 @@ -#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 index 5caa9d1..c2efd0b 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1 @@ -#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 index 8b6bbb6..f18d831 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import app_links import dynamic_color import file_selector_macos -import flutter_local_notifications import media_kit_libs_macos_video import media_kit_video import package_info_plus @@ -16,14 +15,12 @@ import screen_retriever_macos import shared_preferences_foundation import url_launcher_macos import wakelock_plus -import webcrypto import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) @@ -31,6 +28,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) - WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index 65543e2..0000000 --- a/macos/Podfile +++ /dev/null @@ -1,42 +0,0 @@ -platform :osx, '13.0' - -# 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 deleted file mode 100644 index 08cb773..0000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - FlutterMacOS (1.0.0) - - media_kit_libs_macos_video (1.0.4): - - FlutterMacOS - - media_kit_video (0.0.1): - - FlutterMacOS - -DEPENDENCIES: - - FlutterMacOS (from `Flutter/ephemeral`) - - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - -EXTERNAL SOURCES: - FlutterMacOS: - :path: Flutter/ephemeral - media_kit_libs_macos_video: - :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos - media_kit_video: - :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos - -SPEC CHECKSUMS: - FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 - media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 - -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 - -COCOAPODS: 1.17.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index fa18d44..f87523c 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,9 +27,6 @@ 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 */; }; - 34AEA7295333B7F48CD3BE4C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - BAF79690465DBAC1A1E19D28 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -63,12 +60,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 18EDDEF1FB58EC56ABDA1BEB /* 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 = ""; }; 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; }; + 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 = ""; }; @@ -80,16 +76,8 @@ 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 = ""; }; - 5099E6191AE87E21D356D9C8 /* 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 = ""; }; - 6D9F8F8BBED102075ECF1DF9 /* 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 = ""; }; - 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 8F5ECEC7BF9DD35CB3E7196E /* 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 = ""; }; - 9DDE815F827B87E2C55DF94C /* 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 = ""; }; - CBD1D9203BEE9731F981EEF8 /* 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 = ""; }; - D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,7 +85,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 34AEA7295333B7F48CD3BE4C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,8 +92,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - BAF79690465DBAC1A1E19D28 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -140,7 +125,6 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - FC780AF47288088B9004ACE2 /* Pods */, ); sourceTree = ""; }; @@ -167,7 +151,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -192,26 +175,10 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */, - D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; - FC780AF47288088B9004ACE2 /* Pods */ = { - isa = PBXGroup; - children = ( - 6D9F8F8BBED102075ECF1DF9 /* Pods-Runner.debug.xcconfig */, - 9DDE815F827B87E2C55DF94C /* Pods-Runner.release.xcconfig */, - CBD1D9203BEE9731F981EEF8 /* Pods-Runner.profile.xcconfig */, - 18EDDEF1FB58EC56ABDA1BEB /* Pods-RunnerTests.debug.xcconfig */, - 8F5ECEC7BF9DD35CB3E7196E /* Pods-RunnerTests.release.xcconfig */, - 5099E6191AE87E21D356D9C8 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -219,7 +186,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - C413A8A75594D2165C9D56BD /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -238,13 +204,11 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 446C82D5FE82BD05F78EDA6A /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - C4D158541D7059C07D3CA9B3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -252,9 +216,6 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* nexus.app */; productType = "com.apple.product-type.application"; @@ -299,9 +260,6 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, - ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -371,67 +329,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 446C82D5FE82BD05F78EDA6A /* [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; - }; - C413A8A75594D2165C9D56BD /* [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; - }; - C4D158541D7059C07D3CA9B3 /* [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; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -483,7 +380,6 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 18EDDEF1FB58EC56ABDA1BEB /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -498,7 +394,6 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8F5ECEC7BF9DD35CB3E7196E /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -513,7 +408,6 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5099E6191AE87E21D356D9C8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -567,7 +461,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -649,7 +543,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -699,7 +593,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -806,20 +700,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 746b383..ff47eeb 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,24 +5,6 @@ - - - - - - - - - - - - diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 96d3fee..a2ec33f 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "info": { - "version": 1, - "author": "xcode" + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" }, - "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" - } - ] -} \ No newline at end of file + { + "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 index 1682af0..82b6f9d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png 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 index c2e488b..13b35eb 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png 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 index e2bf256..0a3f5fa 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png 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 index 983d7fa..bdb5722 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png 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 index c8f0676..f083318 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png 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 index 468be1f..326c0e7 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png 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 index a77207d..2f1632c 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index a0e58e4..c989ed4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: fdcd9f70f9eb80df3bc5ed0fa67280df2595fd481948df8a9ab082e6a40ad04b + sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c url: "https://pub.dev" source: hosted - version: "108.0.0" + version: "99.0.0" analysis_server_plugin: dependency: transitive description: name: analysis_server_plugin - sha256: "3460f040e1f77647fcd82db9bd3bb117de04d04a3d67c39ad361794331147a4e" + sha256: "3960b28ee740004df39f85d5ebfc91785f7a90e51fd7c9a185e86a36b2f581b4" url: "https://pub.dev" source: hosted - version: "0.3.23" + version: "0.3.14" analyzer: dependency: transitive description: name: analyzer - sha256: a51c769bff3b6dfbe9d60199b8606d808290702a296bef0c26a4ca391d414e46 + sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" url: "https://pub.dev" source: hosted - version: "14.4.0" + version: "12.1.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: f890a2e708e38fbf4238ad2f9fd3d506538acdddb0cd8eb2c3e8c7bd17784b57 + sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.3" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9a3518eb84e8e26665874d9867534961d855493e9c5eb0eb55cbd46edd995529" + sha256: "0057a98d64d7bb872b0c87dff6e73d2c2d80c77156e7a03f127a26f8aa240649" url: "https://pub.dev" source: hosted - version: "0.14.17" + version: "0.14.8" app_links: dependency: "direct main" description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: archive - sha256: "6c5bcd986e06b94e3c40244af471750840a3d2341d1f9763a1100a14add517b4" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.0.9" args: dependency: transitive description: @@ -89,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.dev" - source: hosted - version: "1.6.5" async: dependency: transitive description: @@ -117,50 +109,50 @@ packages: dependency: transitive description: name: build - sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8" + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.11" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: e1d40ef3f7934986d5da2271b1ba07794921ce263e44d622fb6c406d76589e33 + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 url: "https://pub.dev" source: hosted - version: "4.1.6" + version: "4.1.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "894c243f6bc32015fec466ce30a6925bd537a77a426ee5bf481120477eb3de67" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.16.1" + version: "2.15.1" built_collection: dependency: transitive description: name: built_collection - sha256: "44922ecebd8e3d0cef743dadd3b54df4e87e2a4c52a29465a1872aa9e192caa8" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.1.1" built_value: dependency: transitive description: name: built_value - sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" url: "https://pub.dev" source: hosted - version: "8.13.0" + version: "8.12.6" button_m3e: dependency: transitive description: @@ -177,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -197,18 +205,18 @@ packages: dependency: transitive description: name: clock - sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.2" code_assets: dependency: "direct main" description: name: code_assets - sha256: "828110d598123b5ea96c00c9f3c72105bf79f8ee36c20a39b26209ade421ec57" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.2.1" collection: dependency: "direct main" description: @@ -233,14 +241,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" cross_file: dependency: transitive description: name: cross_file - sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "0.3.5+5" + version: "0.3.5+4" crypto: dependency: transitive description: @@ -257,54 +273,47 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - cupertino_ui: - dependency: transitive - description: - name: cupertino_ui - sha256: c23816577a1280cff409e989541767887011f8f1d8c1863a794ed308af46f2a9 - url: "https://pub.dev" - source: hosted - version: "1.1.1" dart_style: dependency: transitive description: name: dart_style - sha256: "82ade9fc4273f29ed673e33166944465225b4f7fc5d4aaef48605cc751c18fc1" + sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 url: "https://pub.dev" source: hosted - version: "3.1.13" + version: "3.1.8" dbus: dependency: transitive description: name: dbus - sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.15" + version: "0.7.14" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "869b3bce0100eb519768ecd71c0111f0b5ea679f5f62e1f59c49d80067e730c5" + sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.8.1" dynamic_polls: dependency: "direct main" description: name: dynamic_polls - sha256: "58cd1fafbbaba6b485a61692aa3bd33ded6f0f61734543b6c754511c362d8ae1" + sha256: "72ff19cdf041ad8dcfa76adaebb216d005f40b278d955e6e0c7bcb769215fabe" url: "https://pub.dev" source: hosted - version: "0.0.8" - encrypt: - dependency: transitive + version: "0.0.7" + emoji_text_field: + dependency: "direct main" description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.dev" - source: hosted - version: "5.0.3" + path: "." + ref: HEAD + resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997" + url: "https://github.com/Henry-Hiles/emoji_text_field" + source: git + version: "1.0.0" equatable: dependency: transitive description: @@ -357,10 +366,10 @@ packages: dependency: "direct main" description: name: ffigen - sha256: "31b2ca630cede89babbbf31688d20b735967c51bf60572b06ec16718e5a7f1ec" + sha256: b7803707faeec4ce3c1b0c2274906504b796e3b70ad573577e72333bd1c9b3ba url: "https://pub.dev" source: hosted - version: "22.0.0" + version: "20.1.1" file: dependency: transitive description: @@ -381,34 +390,34 @@ packages: dependency: transitive description: name: file_selector_android - sha256: d670cd0ce77a2e785b18d8b4d0a8d6a222d6a813ec9b7ddf2790a1b4fb6fa92c + sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b" url: "https://pub.dev" source: hosted - version: "0.5.2+11" + version: "0.5.2+8" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: "97269e5307a0ab813b1fa2430bada0a96e0afb74848417f8676f64ba5de0051c" + sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca url: "https://pub.dev" source: hosted - version: "0.5.3+6" + version: "0.5.3+5" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" url: "https://pub.dev" source: hosted - version: "0.9.4+1" + version: "0.9.4" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4 + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" url: "https://pub.dev" source: hosted - version: "0.9.5+1" + version: "0.9.5" file_selector_platform_interface: dependency: transitive description: @@ -429,10 +438,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" url: "https://pub.dev" source: hosted - version: "0.9.3+6" + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -486,46 +495,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "357be7ff996dc272e2037c69c1bdd5d1bd21172219d55f8bd2e11aeea602e6d3" - url: "https://pub.dev" - source: hosted - version: "22.3.1" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b" - url: "https://pub.dev" - source: hosted - version: "8.0.1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd" - url: "https://pub.dev" - source: hosted - version: "12.2.0" - flutter_local_notifications_web: - dependency: transitive - description: - name: flutter_local_notifications_web - sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945" - url: "https://pub.dev" - source: hosted - version: "3.1.1" flutter_localizations: dependency: "direct main" description: flutter @@ -543,10 +512,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3" + sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" url: "https://pub.dev" source: hosted - version: "3.4.3" + version: "3.3.2" flutter_svg: dependency: "direct main" description: @@ -569,10 +538,10 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - sha256: "530f2e3cc57be1a00f8bfae1b0000064e8a6df0cf1fb4d48a80f85afde9b39b7" + sha256: "7ff010b116f6abc16429923e616fbc727f3f65ef4cee12ffdb280aeecbc21e7f" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "0.17.2" fluttertagger: dependency: "direct main" description: @@ -585,10 +554,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "3af98c92d7d4dd182ea1393193e69a840fcbaee1f66f1158bd20504e5f3f6f4f" + sha256: "8599ba37236328ff6f97269ecce875d251a367eb2929c9bbf9b811b2ce56c74e" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "3.2.6-dev.1" freezed_annotation: dependency: "direct main" description: @@ -597,22 +566,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" get_x_storage: dependency: transitive description: name: get_x_storage - sha256: "597cd8f6ef8b01c747a6d95ff7b0310066f6c82ddf5565ca371f047f316fe43e" + sha256: "69e4412dd70e25a4991623c10bf72e3b12106f2cb4353a2d167353947597f3aa" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.0.9" glob: dependency: transitive description: name: glob - sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.3" graphs: dependency: transitive description: @@ -633,26 +610,26 @@ packages: dependency: "direct main" description: name: hooks - sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.0.2" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - sha256: d58e1e14aa112ca9c56d54effb7d0d717b2d3f96dad3cdd1fce16f8740450700 + sha256: dcaf59f0f41489ff08ce61438ada564d67f40cfa37cc7c8589da78fb600c2edc url: "https://pub.dev" source: hosted - version: "3.4.3" + version: "3.3.2" html: dependency: transitive description: name: html - sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.7" + version: "0.15.6" http: dependency: "direct main" description: @@ -689,10 +666,10 @@ packages: dependency: transitive description: name: image - sha256: a1e7f4951e538a568e14b856702afc9ae1d2f4b202daced8d22c1b9cd211ce89 + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.8.0" image_picker: dependency: "direct main" description: @@ -705,10 +682,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "0a55d645a670d6ae11efa948f955ae904afa472f1344fc15a7ddacc20c1e7219" + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" url: "https://pub.dev" source: hosted - version: "0.8.13+23" + version: "0.8.13+19" image_picker_for_web: dependency: transitive description: @@ -721,10 +698,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "852a7762962f3bb3ad54ef1d342669e41557f466033a676baa8876b367cf4050" + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 url: "https://pub.dev" source: hosted - version: "0.8.13+8" + version: "0.8.13+6" image_picker_linux: dependency: transitive description: @@ -761,26 +738,18 @@ packages: dependency: "direct main" description: name: intl - sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.20.3" + version: "0.20.2" io: dependency: transitive description: name: io - sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.1.0" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" + version: "1.0.5" json_annotation: dependency: "direct main" description: @@ -793,10 +762,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d + sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 url: "https://pub.dev" source: hosted - version: "6.14.1" + version: "6.14.0" leak_tracker: dependency: transitive description: @@ -838,14 +807,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - listen: - dependency: transitive - description: - name: listen - sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" - url: "https://pub.dev" - source: hosted - version: "1.0.1" logging: dependency: transitive description: @@ -858,18 +819,18 @@ packages: dependency: "direct main" description: name: m3e_buttons - sha256: dca780e813563754576194ed18e3f6a33422ed136c8d4ccfeca20909edddd728 + sha256: af1b19bd5ec7327d20b5a57c6c91e1ed30ee15a4dbade781c0cdbc72e6b04b5e url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "0.0.4" m3e_card_list: dependency: "direct main" description: name: m3e_card_list - sha256: ed1cc150f051b8812ffd735915ae59fed23517ffeeed04c4b2b8ef6257cc18b7 + sha256: d4aba0123cccda40ac80789befa8d355e1dc16aa7dcee910157690b0546d78d6 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "0.1.0" m3e_design: dependency: transitive description: @@ -882,10 +843,10 @@ packages: dependency: transitive description: name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.20" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -894,22 +855,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" - material_emoji_picker: - dependency: "direct main" - description: - name: material_emoji_picker - sha256: "738e70a8d2f592be05dd9889037cc6098a63e0d8c48723125848865ab305f138" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - material_ui: - dependency: "direct main" - description: - name: material_ui - sha256: "32d98f11cf5942a095a8c3e01b03e4de3b17941437713dcf36b1088fb62e23d2" - url: "https://pub.dev" - source: hosted - version: "1.5.0" measure_size: dependency: "direct main" description: @@ -986,18 +931,18 @@ packages: dependency: transitive description: name: meta - sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" mime: dependency: transitive description: name: mime - sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.0.0" motor: dependency: transitive description: @@ -1006,39 +951,39 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "90eaad8059ebc2004a63810a4696ea9acdb02473e47ed3b6b3bafb100ac77e31" - url: "https://pub.dev" - source: hosted - version: "0.19.5" navigation_rail_m3e: dependency: "direct main" description: path: "packages/navigation_rail_m3e" ref: HEAD - resolved-ref: da860a413d5cf78b9c9c967215250bcdbfe5191d + resolved-ref: "667b0bc8526fd53296778903b6ef3f22424f3aa4" url: "https://github.com/Henry-Hiles/material_3_expressive" source: git version: "0.3.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" objective_c: dependency: transitive description: name: objective_c - sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.6.0" + version: "9.4.1" package_config: dependency: transitive description: name: package_config - sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -1123,18 +1068,18 @@ packages: dependency: transitive description: name: petitparser - sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.0.2" platform: dependency: transitive description: name: platform - sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1143,54 +1088,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.dev" - source: hosted - version: "3.9.1" pool: dependency: transitive description: name: pool - sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.2" posix: dependency: transitive description: name: posix - sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.5.2" - process: - dependency: transitive - description: - name: process - sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" - url: "https://pub.dev" - source: hosted - version: "5.0.6" + version: "6.5.0" pub_semver: dependency: transitive description: name: pub_semver - sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c38b81cbf34450b67e0265d73433569d12e34782e30ed769c9cc99c9d5f2e796 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "1.5.0" quiver: dependency: transitive description: @@ -1203,34 +1132,34 @@ packages: dependency: transitive description: name: record_use - sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "0.6.0" riverpod: dependency: transitive description: name: riverpod - sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a" + sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" url: "https://pub.dev" source: hosted - version: "3.4.3" + version: "3.3.2" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: bb13cb3985815840cc103eda1027158358c5fdc00f308fc6f76574d0594107af + sha256: "3e275138862ccc22ed61444a1f9a840f753094c367f28f4123f50289cd204d68" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.12" + version: "1.0.0-dev.10" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "215cb3cd54a97e09bc3512535fa4e8bedd3bf2a06dc2ae015ac8e0050fc71bd1" + sha256: "166f29492228dc471b6fe294092560ccd5545ed15c9fc155622455b58b2451a9" url: "https://pub.dev" source: hosted - version: "3.1.9" + version: "3.1.4" rxdart: dependency: transitive description: @@ -1299,18 +1228,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399" + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" url: "https://pub.dev" source: hosted - version: "2.4.28" + version: "2.4.27" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -1351,6 +1280,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1368,18 +1313,34 @@ packages: dependency: transitive description: name: source_gen - sha256: "11b6047da8e4eb6c643ccac3d0fda3f9c9e11651695f7da24a85ade8aff15a44" + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.2.3" source_helper: dependency: transitive description: name: source_helper - sha256: "5e6f216fdf6376c9f3852381ae037499797a3385377d388b011dac98d303c67c" + sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" url: "https://pub.dev" source: hosted - version: "1.3.13" + version: "1.3.12" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -1392,10 +1353,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.2" + version: "1.12.1" state_notifier: dependency: transitive description: @@ -1416,10 +1377,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -1440,10 +1401,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "397b146f97613b83d84bdccb439bc8d0f3aebb6c1d14a9641e6f24201c490b2d" + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" url: "https://pub.dev" source: hosted - version: "3.4.2" + version: "3.4.1" term_glyph: dependency: transitive description: @@ -1452,14 +1413,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + url: "https://pub.dev" + source: hosted + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.11" + test_core: + dependency: transitive + description: + name: test_core + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + url: "https://pub.dev" + source: hosted + version: "0.6.17" timeago: dependency: "direct main" description: @@ -1468,14 +1445,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.1" - timezone: - dependency: transitive - description: - name: timezone - sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb" - url: "https://pub.dev" - source: hosted - version: "0.11.1" typed_data: dependency: transitive description: @@ -1484,56 +1453,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - unifiedpush: - dependency: "direct main" - description: - name: unifiedpush - sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - unifiedpush_android: + universal_html: dependency: transitive description: - name: unifiedpush_android - sha256: a902e505e91c8e28b54b379bb27f255f66df409274a97af24d6719f12f0d13dc + name: universal_html + sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 url: "https://pub.dev" source: hosted - version: "3.5.0" - unifiedpush_linux: - dependency: "direct overridden" - description: - path: unifiedpush_linux - ref: "quad/feat/should-write-service-option" - resolved-ref: "7589b07ef4280820e4adea165d3d86b219f6022c" - url: "https://codeberg.org/Henry-Hiles/flutter-connector" - source: git - version: "1.0.1" - unifiedpush_platform_interface: - dependency: "direct overridden" - description: - path: unifiedpush_platform_interface - ref: "quad/feat/should-write-service-option" - resolved-ref: "7589b07ef4280820e4adea165d3d86b219f6022c" - url: "https://codeberg.org/Henry-Hiles/flutter-connector" - source: git - version: "4.0.0" - unifiedpush_storage_interface: + version: "2.3.0" + universal_io: dependency: transitive description: - name: unifiedpush_storage_interface - sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9 + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 url: "https://pub.dev" source: hosted - version: "1.0.0" - unifiedpush_storage_shared_preferences: - dependency: "direct main" - description: - name: unifiedpush_storage_shared_preferences - sha256: eda9c52bac0058f81e0d9c65e33eedc12c5901d2e68ad47fdf68175ddf00a3b4 - url: "https://pub.dev" - source: hosted - version: "1.0.0" + version: "2.3.1" universal_platform: dependency: transitive description: @@ -1562,34 +1497,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.33" + version: "6.3.32" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.4.2" + version: "6.4.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" url: "https://pub.dev" source: hosted - version: "3.2.6" + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -1610,10 +1545,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" uuid: dependency: transitive description: @@ -1626,10 +1561,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "9d0e3b9cb16542ad660daee871e726a10d13a93b7b5391677c3160e8f5e83935" + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: @@ -1642,42 +1577,42 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "4dca4feb77dc3ec7f6e27e49c53241eb8217f55e4f9b12599a27f8903bca5682" + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.6" vector_math: dependency: transitive description: name: vector_math - sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.3.0" + version: "15.2.0" wakelock_plus: dependency: transitive description: name: wakelock_plus - sha256: "22b3e7e937721de70e63c85e7139f4ac781dc22863b9262431a53ac030eb074b" + sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.6.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "764c25504562abc8ac3406f5d175f126b276e6ba916c7c2675690b73368ca384" + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.5.1" watcher: dependency: transitive description: @@ -1710,30 +1645,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - webcrypto: - dependency: "direct dev" - description: - name: webcrypto - sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1" - url: "https://pub.dev" - source: hosted - version: "0.6.0" - webpush_encryption: + webkit_inspection_protocol: dependency: transitive description: - name: webpush_encryption - sha256: "5b83272b91acda6ae515fcd980c94f06bf413702282497c5a68f5dfc64fed27f" + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.2.1" win32: dependency: transitive description: name: win32 - sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.3.0" window_manager: dependency: "direct main" description: @@ -1742,15 +1669,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.2" - xdg_desktop_portal: - dependency: "direct main" - description: - path: "." - ref: "quad/nexus" - resolved-ref: "964249996e536b3c5b10c2654d1dc69044693431" - url: "https://github.com/Henry-Hiles/xdg_desktop_portal.dart" - source: git - version: "0.1.14" xdg_directories: dependency: "direct main" description: @@ -1763,18 +1681,18 @@ packages: dependency: transitive description: name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.6.1" yaml: dependency: transitive description: name: yaml - sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.3" yaml_edit: dependency: transitive description: @@ -1784,5 +1702,5 @@ packages: source: hosted version: "2.2.4" sdks: - dart: "3.13.3" - flutter: ">=3.47.0" + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7f99ba3..72748b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,123 +1,98 @@ name: nexus -description: "A simple and user-friendly Matrix client" +description: "Yet another Matrix client" version: 0.1.0 publish_to: none flutter: - fonts: - - family: bundled-emoji - fonts: - - asset: assets/fonts/Twemoji.ttf - - family: bundled-sans - fonts: - - asset: assets/fonts/Inter.ttf - - family: bundled-mono - fonts: - - asset: assets/fonts/AtkinsonHyperlegibleMono.ttf - assets: - - assets/bundled/ - uses-material-design: true + fonts: + - family: fallback-emoji + fonts: + - asset: assets/fonts/NotoColorEmoji.ttf + - family: fallback-sans + fonts: + - asset: assets/fonts/Roboto.ttf + assets: + - assets/ + uses-material-design: true environment: - sdk: 3.13.3 + sdk: "^3.12.2" dependency_overrides: - dynamic_color: 2.1.0 - unifiedpush_linux: - git: - url: https://codeberg.org/Henry-Hiles/flutter-connector - ref: quad/feat/should-write-service-option - path: unifiedpush_linux - unifiedpush_platform_interface: - git: - url: https://codeberg.org/Henry-Hiles/flutter-connector - ref: quad/feat/should-write-service-option - path: unifiedpush_platform_interface - path_provider_android: 2.2.23 # Pinned to avoid JNI - linkify: - git: - url: https://github.com/appelladev/linkify - ref: fix/consecutive-periods-loose-url + path_provider_android: 2.2.23 # Pinned to avoid JNI + linkify: + git: + url: https://github.com/appelladev/linkify + ref: fix/consecutive-periods-loose-url dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - flutter_riverpod: 3.4.3 - hooks_riverpod: 3.4.3 - intl: 0.20.3 - fast_immutable_collections: 11.2.0 - path_provider: 2.1.6 - url_launcher: 6.3.2 - freezed_annotation: 3.1.0 - image_picker: 1.2.3 - path: 1.9.1 - dynamic_color: 2.1.0 - collection: 1.19.1 - window_manager: 0.5.2 - color_hash: 1.0.1 - flutter_widget_from_html_core: 0.17.4 - flutter_svg: 2.3.0 - json_annotation: 4.12.0 - shared_preferences: 2.5.5 - fluttertagger: 2.3.2 - dynamic_polls: 0.0.8 - flutter_hooks: 0.21.3+1 - ffi: 2.2.0 - hooks: 2.2.0 - code_assets: 2.1.0 - ffigen: 22.0.0 - timeago: 3.7.1 - http: 1.6.0 - flutter_linkify: 6.0.0 - linkify: 5.0.0 - flutter_blurhash: 0.9.1 - super_sliver_list: 0.4.1 - media_kit: 1.2.6 - media_kit_video: 2.0.1 - media_kit_libs_video: 1.0.7 - measure_size: 5.0.2 - m3e_buttons: 1.0.3 - navigation_rail_m3e: - git: - url: https://github.com/Henry-Hiles/material_3_expressive - path: packages/navigation_rail_m3e - m3e_card_list: 1.0.0 - xdg_directories: 1.1.0 - package_info_plus: 10.2.1 - app_links: 7.2.1 - file_selector: 1.1.0 - material_ui: 1.5.0 - unifiedpush: 6.2.0 - unifiedpush_storage_shared_preferences: 1.0.0 - flutter_local_notifications: 22.3.1 - material_emoji_picker: 1.2.0 - xdg_desktop_portal: - git: - url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart - ref: quad/nexus - + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_riverpod: 3.3.2 + hooks_riverpod: 3.3.2 + intl: 0.20.2 + fast_immutable_collections: 11.2.0 + path_provider: 2.1.6 + url_launcher: 6.3.2 + freezed_annotation: 3.1.0 + image_picker: ^1.2.3 + path: 1.9.1 + dynamic_color: 1.8.1 + collection: 1.19.1 + window_manager: 0.5.2 + color_hash: 1.0.1 + flutter_widget_from_html_core: 0.17.2 + flutter_svg: 2.3.0 + json_annotation: 4.12.0 + shared_preferences: 2.5.5 + fluttertagger: 2.3.2 + dynamic_polls: 0.0.7 + flutter_hooks: 0.21.3+1 + ffi: 2.2.0 + hooks: 2.0.2 + code_assets: 1.2.1 + ffigen: 20.1.1 + timeago: 3.7.1 + http: 1.6.0 + flutter_linkify: 6.0.0 + linkify: 5.0.0 + emoji_text_field: + git: + url: https://github.com/Henry-Hiles/emoji_text_field + flutter_blurhash: 0.9.1 + super_sliver_list: 0.4.1 + media_kit: 1.2.6 + media_kit_video: 2.0.1 + media_kit_libs_video: 1.0.7 + measure_size: 5.0.2 + m3e_buttons: 0.0.4 + navigation_rail_m3e: + git: + url: https://github.com/Henry-Hiles/material_3_expressive + path: packages/navigation_rail_m3e + m3e_card_list: 0.1.0 + xdg_directories: 1.1.0 + package_info_plus: 10.2.1 + app_links: 7.2.1 + file_selector: ^1.1.0 + dev_dependencies: - build_runner: 2.16.1 - flutter_lints: 6.0.0 - freezed: 4.0.2 - riverpod_lint: 3.1.9 - flutter_launcher_icons: 0.14.4 - json_serializable: 6.14.1 - # Transitive dep but needed for commands - webcrypto: 0.6.0 + build_runner: ^2.15.1 + flutter_lints: 6.0.0 + freezed: 3.2.6-dev.1 + riverpod_lint: 3.1.4 + flutter_launcher_icons: 0.14.4 + json_serializable: 6.14.0 flutter_launcher_icons: - image_path: assets/icon.png - ios: true - image_path_ios: assets/mobile.png - android: true - adaptive_icon_background: assets/background.png - adaptive_icon_foreground: assets/foreground.png - adaptive_icon_monochrome: assets/monochrome.png - windows: - generate: true - macos: - generate: true - image_path: assets/mobile.png + ios: true + android: true + image_path: assets/icon.png + adaptive_icon_background: assets/background.png + 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 d8a8cb1..0b0cf7c 100644 --- a/scripts/generate.dart +++ b/scripts/generate.dart @@ -1,5 +1,4 @@ import "dart:io"; - import "package:ffigen/ffigen.dart"; import "package:path/path.dart"; import "package:nexus/helpers/extensions/get_xcode_sdk.dart"; @@ -7,38 +6,25 @@ import "package:nexus/helpers/extensions/get_xcode_sdk.dart"; void main(List args) async { final repoDir = Directory.fromUri(Platform.script.resolve("../gomuks")); - print("Generating FFI bindings..."); + print("Generating FFI Bindings..."); final libclangPath = Platform.environment["LIBCLANG_PATH"]; FfiGenerator( - output: .new( - dart: .new( - path: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), - ), + output: Output( + dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), ), - visitors: [.new(func: (node) => node.isIncluded = true)], - input: .new( + headers: Headers( entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri], compilerOptions: [ "--no-warnings", - if (Platform.isMacOS) "-I${await getXCodeTool()}/usr/include", + if (Platform.isMacOS) "-I${await getXCodeSDK()}/usr/include", ], ), + functions: Functions.includeAll, ).generate( libclangDylib: libclangPath == null ? null - : Uri.file( - join( - libclangPath, - "libclang.${(Platform.isLinux || Platform.isAndroid) - ? "so" - : Platform.isMacOS - ? "dylib" - : Platform.isWindows - ? "dll" - : throw UnsupportedError("Unsupported Platform")}", - ), - ), + : Uri.file(join(libclangPath, "libclang.so")), ); - print("FFI bindings generated!"); + print("Done!"); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5d2b53e..0694802 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,7 +13,6 @@ #include #include #include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -31,8 +30,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); - WebcryptoPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WebcryptoPlugin")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c74b2f5..238ef93 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,12 +10,10 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever_windows url_launcher_windows - webcrypto window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES)