diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a3abae1..dc1e9c7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,7 +27,7 @@ jobs: 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 --target-platform android-arm64" + 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 }} diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index eeb4868..5f16f9f 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: 3.47.4 + flutter-version: 3.44.4 - name: Set up Go uses: actions/setup-go@v6 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 45d04d7..4005dc5 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -19,29 +19,22 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: 3.47.4 + flutter-version: 3.44.4 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: gomuks/go.mod - - name: Build App + - name: Build app run: | flutter pub get dart scripts/generate.dart flutter pub run build_runner build flutter build macos --release - - 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 + - name: Upload installer artifact uses: actions/upload-artifact@v6 with: - name: nexus.dmg - path: ${{ steps.create-dmg.outputs.dmg_path }} + name: Nexus.App + path: build/macos/Build/Products/Release/Nexus.app diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4cb30cd..d9c8dd5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,64 +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 Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.44.4 - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - install: >- - mingw-w64-x86_64-gcc + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: gomuks/go.mod - - name: Go build - run: | - cd gomuks/pkg/ffi - go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: >- + mingw-w64-x86_64-gcc - - name: Build with Flutter - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build - flutter build windows --release + - name: Go build + run: | + cd gomuks/pkg/ffi + go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared - - 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: Build with Flutter + run: | + flutter pub get + dart scripts/generate.dart + flutter pub run build_runner build + flutter build windows --release - - name: Install Inno Setup - run: choco install innosetup -y + - 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: Build Inno Setup installer - run: iscc windows/installer.iss + - name: Install Inno Setup + run: choco install innosetup -y - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: windows-installer - path: windows/dist/Nexus-Setup.exe + - 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 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 06dd51a..145276a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "gomuks"] path = gomuks url = https://github.com/gomuks/gomuks + branch = main 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 fac2ed8..2ff533e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,8 +11,6 @@ "muks", "prefs", "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 1da8adb..368bb90 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,10 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Platform Support - [x] Linux - - [x] Windows - - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus). + - [x] Windows - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus). - [x] Android - - [x] MacOS - - [x] Unsigned .ipa - - [ ] App Store - - [x] iOS - - [x] Unsigned .ipa - - [ ] App Store + - [x] MacOS - Unsigned .App only + - [x] iOS - Unsigned .ipa only - [ ] Web (may not be possible) - [x] Login (via OAuth) - [x] Rooms / Spaces @@ -55,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 @@ -68,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 @@ -108,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 @@ -119,13 +115,13 @@ 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) +- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.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) +- [Unsigned MacOS App](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/Nexus.App.zip) - [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip) - Flatpak - [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip) - [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip) -- [NixOS Module](linux/nix/module.nix) ## Build it yourself @@ -194,12 +190,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 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/build.gradle b/android/build.gradle new file mode 100644 index 0000000..11bc662 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,26 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + + afterEvaluate {project -> + 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/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/notif.webp b/assets/twim/notif.webp deleted file mode 100644 index d8d9010..0000000 Binary files a/assets/twim/notif.webp and /dev/null differ 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/flake.lock b/flake.lock index efbcc03..b743efe 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1788450739, - "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", + "lastModified": 1785627969, + "narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", + "rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a", "type": "github" }, "original": { @@ -42,16 +42,15 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1790043059, - "narHash": "sha256-7ID4HL4IrrlalCsxMASkcAr+v0VHTNFp6+sZ+dpvOV8=", - "owner": "Henry-Hiles", + "lastModified": 1774860670, + "narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=", + "owner": "neobrain", "repo": "nix2flatpak", - "rev": "290dd282e555cab988c021c2be5f2361e58c4d51", + "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": 1785031560, + "narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", + "rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c", "type": "github" }, "original": { @@ -89,11 +88,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1790185690, - "narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=", + "lastModified": 1785967620, + "narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4975466d324710c576dc11ad614684e6bd8cad8e", + "rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436", "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 480938f..9444dd2 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit 480938f9e01e7eae2cf249657efae03df76599d6 +Subproject commit 9444dd293664c300c6e710bb6cddcd9c6cab365d diff --git a/hook/build.dart b/hook/build.dart index 9315e04..66ae366 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"; @@ -51,10 +50,7 @@ Future main(List args) => build(args, (input, output) async { break; case OS.macOS: libFileName = "libgomuks.dylib"; - extraEnv = { - "SDKROOT": await getXCodeTool(), - "MACOSX_DEPLOYMENT_TARGET": codeConfig.macOS.targetVersion.toString(), - }; + extraEnv = {"SDKROOT": await getXCodeTool()}; break; case OS.windows: libFileName = "libgomuks.dll"; @@ -135,8 +131,6 @@ Future main(List args) => build(args, (input, output) async { "go", [ "build", - "-trimpath", - "-ldflags=-s -w", "-tags", tags, "-o", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c73d4ae..c30b367 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -7,8 +7,7 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate - return super.application(application, didFinishLaunchingWithOptions: launchOptions) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 1682af0..0d531c4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 51f4a9b..da4acee 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 97b3b3f..a3cfb1d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 17d971e..adbdcd5 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index ca74554..fee4302 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 6129702..4d21624 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 16c3d1e..3e7a859 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 97b3b3f..a3cfb1d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index f515ebd..518d397 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 2e49a13..25f2b47 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index 66b9ef2..8f79bb9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index 53af9ff..c48dec6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index 455803d..99d44e8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index 7a2fd0f..6f987f0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 2e49a13..25f2b47 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index c34ad9e..fcf969a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index 5e21545..16dea29 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 2140e17..3366fb5 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index a27d854..81c9cb8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8b4eb75..efda04b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 21c424b..3774574 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index d4a5e09..2a99ee5 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; 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 4d0c972..e31e4f3 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -1,28 +1,26 @@ -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"; @@ -31,9 +29,7 @@ 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"; @@ -42,9 +38,8 @@ 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,99 +47,134 @@ import "package:path_provider/path_provider.dart"; class ClientController extends AsyncNotifier { @override Future build() async { - debugPrint("Setting Gomuks env..."); + final Pointer root; 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 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); + + if (errorCode == 0) return handle; + throw Exception("GomuksStart returned error code $errorCode"); } - 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.toJson(); - - 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 ( - Event.fromJson(response["event"]), - RoomMetadata.fromJson(response["room"]), - ); - } - - dynamic _sendCommand( + 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()); @@ -183,17 +213,21 @@ class ClientController extends AsyncNotifier { } } - Future joinRoom(JoinRoomRequest request) async => - (await _sendCommand("join_room", request.toJson()))["room_id"]; - - Future getRoomSummary(JoinRoomRequest request) async => - .fromJson(await _sendCommand("get_room_summary", request.toJson())); + Future joinRoom(JoinRoomRequest request) async { + final response = await _sendCommand("join_room", request.toJson()); + return response["room_id"]; + } 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?; @@ -214,12 +248,6 @@ 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); @@ -231,19 +259,8 @@ class ClientController extends AsyncNotifier { 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 ProfileResponse(profile: .new(id: userId)); - } - } + Future getProfile(String userId) async => + .fromJson(await _sendCommand("get_profile", {"user_id": userId})); Future reportEvent(ReportRequest request) => _sendCommand("report_event", request.toJson()); @@ -254,15 +271,6 @@ class ClientController extends AsyncNotifier { 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())); @@ -272,11 +280,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, @@ -302,14 +308,10 @@ class ClientController extends AsyncNotifier { Future getSpecVersions() async => .fromJson(await _sendCommand("get_versions")); - Future getCapabilities() async => Capabilities.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..caea3de --- /dev/null +++ b/lib/controllers/emoji.dart @@ -0,0 +1,84 @@ +import "dart:convert"; +import "package:emoji_text_field/models/emoji_category.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:http/http.dart"; +import "package:nexus/models/emoji.dart"; + +typedef EmojiTuple = (IMap, IMap>); + +class EmojiController extends AsyncNotifier { + @override + Future build() async { + final response = await get( + .https("github.com", "github/gemoji/raw/refs/heads/master/db/emoji.json"), + ); + + if (response.statusCode != 200) { + throw Exception("Failed to load emoji data"); + } + + final data = json.decode(response.body); + + final entries = (data as List) + .cast>() + .map(Emoji.fromJson) + .toIList(); + + final categoryMap = entries.fold>>( + .new(), + (acc, entry) => acc.update( + entry.category, + (list) => list.add(entry.emoji), + ifAbsent: () => .new([entry.emoji]), + ), + ); + + final keywordMap = entries.fold>>( + .new(), + (acc, entry) => acc.add( + entry.emoji, + .new([...entry.tags, ...entry.aliases, entry.description]), + ), + ); + + final customCategories = IMap.fromEntries( + categoryMap.entries.map( + (entry) => MapEntry( + entry.key, + EmojiCategory( + name: entry.key, + icon: switch (entry.key) { + "Smileys & Emotion" => Icons.emoji_emotions, + "People & Body" => Icons.emoji_people, + "Animals & Nature" => Icons.emoji_nature, + "Food & Drink" => Icons.emoji_food_beverage, + "Travel & Places" => Icons.travel_explore, + "Activities" => Icons.sports_soccer, + "Objects" => Icons.emoji_objects, + "Symbols" => Icons.emoji_symbols, + "Flags" => Icons.emoji_flags, + _ => Icons.category, + }, + emojis: entry.value.toList(growable: false), + ), + ), + ), + ); + + final customKeywords = IMap( + .fromEntries( + keywordMap.entries.map( + (e) => .new(e.key, e.value.toList(growable: false)), + ), + ), + ); + + return (customCategories, customKeywords); + } + + static final provider = AsyncNotifierProvider( + EmojiController.new, + ); +} diff --git a/lib/controllers/event.dart b/lib/controllers/event.dart index 7a1e5dc..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( diff --git a/lib/controllers/gomuks_listener.dart b/lib/controllers/gomuks_listener.dart deleted file mode 100644 index dd75cf3..0000000 --- a/lib/controllers/gomuks_listener.dart +++ /dev/null @@ -1,124 +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.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(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/jump_to_event.dart b/lib/controllers/jump_to_event.dart deleted file mode 100644 index da42cf9..0000000 --- a/lib/controllers/jump_to_event.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; - -class JumpToEventController(String? _) extends Notifier { - @override - String? build() => null; - - void set(String? eventId) => state = eventId; - - @override - bool updateShouldNotify(_, _) => true; - - static final provider = NotifierProvider.family - .autoDispose( - JumpToEventController.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 index f0295d8..e3509f0 100644 --- a/lib/controllers/member_list_opened.dart +++ b/lib/controllers/member_list_opened.dart @@ -1,20 +1,22 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/shared_prefs.dart"; -class MemberListOpenedController extends AsyncNotifier { +class MemberListOpenedController extends Notifier { static const String key = "memberListOpened"; @override - Future build() async => - await ref.watch(SharedPrefsController.provider).getBool(key) ?? true; + bool build() => + ref.watch(SharedPrefsController.provider).requireValue.getBool(key) ?? + true; Future set(bool value) async { - state = .data(value); - await ref.watch(SharedPrefsController.provider).setBool(key, value); + final prefs = ref.watch(SharedPrefsController.provider).requireValue; + state = value; + + prefs.setBool(key, value); } - static final provider = - AsyncNotifierProvider( - MemberListOpenedController.new, - ); + static final provider = NotifierProvider( + MemberListOpenedController.new, + ); } diff --git a/lib/controllers/members.dart b/lib/controllers/members.dart index 3c287ea..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( 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 5ee3449..0000000 --- a/lib/controllers/notification.dart +++ /dev/null @@ -1,162 +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/pages/notifications.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 case final eventId?) { - if (navigatorKey.currentContext case final context?) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsPage( - highlightedEventId: eventId, - defaultToAllNotifications: true, - ), - ), - ); - } - } - }, - ); - } - - if (Platform.isLinux) { - const notificationChannel = MethodChannel("nexus/notifications"); - - notificationChannel.setMethodCallHandler((call) async { - if (call.method != "notificationClicked") { - return; - } - - final eventId = call.arguments as String; - - if (navigatorKey.currentContext case final context?) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsPage( - highlightedEventId: eventId, - 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, - icon: icon == null ? null : XdgNotificationIconFile(icon.path), - defaultAction: "app.event", - defaultActionTarget: payload, - ); - } 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 da4312d..0000000 --- a/lib/controllers/notifications.dart +++ /dev/null @@ -1,53 +0,0 @@ -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/models/event.dart"; - -typedef NotificationsRequest = (UnreadType? unreadType, String? roomId); - -class NotificationsController([final NotificationsRequest? request]) - extends AsyncNotifier> { - @override - Future> build() async { - final client = ref.read(ClientController.provider.notifier); - - final (unreadType, roomId) = request ?? (null, null); - - return await client.getMentions( - .new( - maxTimestamp: .now(), - unreadType: unreadType ?? .highlight, - roomId: roomId, - ), - ); - } - - Future loadOlder() async { - final currentNotifications = await future; - state = .loading(); - state = await .guard(() async { - final lastTs = currentNotifications.lastOrNull?.timestamp; - if (lastTs == null) return const .empty(); - - final client = ref.read(ClientController.provider.notifier); - final (unreadType, roomId) = request ?? (null, null); - - final newNotifications = await client.getMentions( - .new( - maxTimestamp: lastTs, - unreadType: unreadType ?? .highlight, - roomId: roomId, - ), - ); - - return currentNotifications.addAll(newNotifications); - }); - } - - static final provider = AsyncNotifierProvider.family - .autoDispose< - NotificationsController, - IList, - NotificationsRequest? - >(NotificationsController.new); -} diff --git a/lib/controllers/pinned_events.dart b/lib/controllers/pinned_events.dart index 323a275..914a301 100644 --- a/lib/controllers/pinned_events.dart +++ b/lib/controllers/pinned_events.dart @@ -4,8 +4,10 @@ import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/pinned_ids.dart"; import "package:nexus/models/event.dart"; -class PinnedEventsController(final String roomId) - extends AsyncNotifier> { +class PinnedEventsController extends AsyncNotifier> { + final String roomId; + PinnedEventsController(this.roomId); + @override Future> build() async { final pinIds = ref.watch(PinnedIdsController.provider(roomId)); @@ -13,8 +15,9 @@ class PinnedEventsController(final String roomId) return (await Future.wait( pinIds.map( (eventId) => ref.watch( - EventController.provider(.new(eventId: eventId, roomId: roomId)) - .future, + EventController.provider( + .new(eventId: eventId, roomId: roomId), + ).future, ), ), )).nonNulls.toIList(); 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..58fa49d 100644 --- a/lib/controllers/profile.dart +++ b/lib/controllers/profile.dart @@ -2,11 +2,13 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/profile_response.dart"; -class ProfileController(final String userId) - extends AsyncNotifier { +class ProfileController extends AsyncNotifier { + final String userId; + ProfileController(this.userId); + @override Future build() { - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); return client.getProfile(userId); } 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 6cf57f7..0000000 --- a/lib/controllers/recent_emoji.dart +++ /dev/null @@ -1,44 +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/account_data.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/models/account_data.dart"; - -class RecentEmojiController extends Notifier> { - @override - IList build() => ref.watch( - AccountDataController.provider.select( - (value) => value.recentEmoji.recentEmoji, - ), - ); - - 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 91b137a..75ce7d8 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,36 +9,45 @@ 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"; -class RoomChatController(final (String roomId, String? contextualEvent) info) - extends AsyncNotifier { +class RoomChatController extends AsyncNotifier?> { + final String roomId; + Future? _loadingOlder; + RoomChatController(this.roomId); + @override - Future build() async { - final (roomId, eventId) = info; - final client = ref.read(ClientController.provider.notifier); - final room = ref.read( + Future?> build() async { + final client = ref.watch(ClientController.provider.notifier); + final initialRoom = ref.watch( RoomsController.provider.select((rooms) => rooms[roomId]), ); - if (room == null) return null; + if (initialRoom == null) return null; + Room room = initialRoom; if (!room.hasFetchedState) { final state = await client.getRoomState(.new(roomId: roomId)); + await ref.read(RoomsController.provider.notifier).addState(roomId, state); + room = ref.read(RoomsController.provider)[roomId] ?? room; } - final timeline = room.timeline + // Load one more page when the initial timeline is short. + if (room.hasMore && room.timeline.length < 20) { + await loadOlder(); + room = ref.read(RoomsController.provider)[roomId] ?? room; + } + + return room.timeline .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) .map((element) => element.value) .toIList() - .addAll(room.clientSticky) + .addAll(room.sticky) .map((entry) { final foundEvent = entry == null ? null : room.events[entry]; @@ -57,26 +65,6 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) }) .nonNulls .toIList(); - - if (info.$2 == null || timeline.map((e) => e.eventId).contains(info.$2)) { - ref.watch(RoomsController.provider.select((rooms) => rooms[roomId])); - - return .new( - timeline: timeline, - hasMoreBackward: room.hasMore, - hasMoreForward: false, - ); - } else { - final context = await client.getEventContext( - .new(roomId: roomId, eventId: info.$2!), - ); - return .new( - timeline: context.before.add(context.event).addAll(context.after), - hasMoreBackward: true, - hasMoreForward: true, - historicalData: .new(start: context.start, end: context.end), - ); - } } Future deleteMessage(Event event, {String? reason}) => ref @@ -84,101 +72,53 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) .redactEvent( RedactEventRequest( eventId: event.eventId, - roomId: info.$1, + roomId: roomId, reason: reason, ), ); - Future paginate(Direction direction) async { - if (state.isLoading) return; + Future loadOlder() => _loadingOlder ??= _loadOlder().whenComplete(() { + _loadingOlder = null; + }); - final chat = await future; + Future _loadOlder() async { + final room = ref.read(RoomsController.provider)[roomId]; + if (room == null || !room.hasMore) return false; - 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.$1])) - ?.timeline - .keys; - final response = await client.paginate( - .new( - roomId: info.$1, - maxTimelineId: timelineKeys?.isNotEmpty == true - ? timelineKeys?.reduce(min) - : null, - ), - ); - - if (response.events.isEmpty) { - state = .data(state.value); - } - - ref - .read(RoomsController.provider.notifier) - .update( - IMap({ - info.$1: 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, - ), - ), - }), - .new(), - ); - } else { - final paginationResponse = await client.paginateManual( - .new( - roomId: info.$1, - direction: direction, - since: direction == .forward - ? chat!.historicalData!.end - : chat!.historicalData!.start, - ), - ); - - state = .data( - .new( - timeline: direction == .forward - ? chat.timeline.addAll(paginationResponse.events) - : paginationResponse.events.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, + final timelineKeys = room.timeline.keys; + final response = await ref + .watch(ClientController.provider.notifier) + .paginate( + .new( + roomId: roomId, + maxTimelineId: timelineKeys.isNotEmpty + ? timelineKeys.reduce(min) + : null, ), - ), - ); - } + ); + + ref + .watch(RoomsController.provider.notifier) + .update( + IMap({ + 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, + ), + ), + }), + .new(), + ); + + return response.hasMore; } Future send( @@ -192,8 +132,8 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) if (relationType == .edit) { baseContent = relation?.content; } else { - final provider = AttachmentController.provider(info.$1); - baseContent = ref.read(provider)?.$2; + final provider = AttachmentController.provider(roomId); + baseContent = ref.watch(provider)?.$2; ref.invalidate(provider); } @@ -209,10 +149,10 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) ); } - final client = ref.read(ClientController.provider.notifier); + final client = ref.watch(ClientController.provider.notifier); final event = await client.sendMessage( SendMessageRequest( - roomId: info.$1, + roomId: roomId, baseContent: baseContent, mentions: Mentions( userIds: [ @@ -231,12 +171,12 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) ); ref - .read(RoomsController.provider.notifier) + .watch(RoomsController.provider.notifier) .update( .new({ - info.$1: .new( + roomId: .new( events: .new({event.rowId: event}), - clientSticky: .new({event.rowId}), + sticky: .new({event.rowId}), ), }), .new(), @@ -248,10 +188,10 @@ class RoomChatController(final (String roomId, String? contextualEvent) 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.$1, + roomId: roomId, eventId: event.eventId, relationType: "m.annotation", ), @@ -272,16 +212,16 @@ class RoomChatController(final (String roomId, String? contextualEvent) info) if (reactionEvent != null) { await ref .watch(ClientController.provider.notifier) - .redactEvent(.new(eventId: reactionEvent.eventId, roomId: info.$1)); + .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.$1, + roomId: roomId, type: EventType.reaction.type, content: ReactionContent(key: reaction), synchronous: true, @@ -293,7 +233,7 @@ class RoomChatController(final (String roomId, String? contextualEvent) 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 ff4d6a2..d0c6eb9 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -1,5 +1,4 @@ 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"; @@ -47,10 +46,10 @@ class RoomsController extends Notifier> { roomId, existing?.copyWith( hasMore: incoming.hasMore, - clientSticky: - (incoming.clientSticky.isEmpty == true - ? existing.clientSticky - : existing.clientSticky.addAll(incoming.clientSticky)) + sticky: + (incoming.sticky.isEmpty == true + ? existing.sticky + : existing.sticky.addAll(incoming.sticky)) .removeWhere( (rowId) => incoming.timeline.values.contains(rowId), ), 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 7fc1b3f..37e6284 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -1,19 +1,13 @@ 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/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"; @@ -24,6 +18,9 @@ class SettingsSectionsController @override Future>> build() async { final settings = await ref.watch(SettingsController.provider.future); + final specVersionsResponse = await ref + .watch(ClientController.provider.notifier) + .getSpecVersions(); return .new({ "General": .new([ @@ -52,7 +49,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), @@ -78,7 +76,8 @@ class SettingsSectionsController settings: .new([ .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), @@ -99,141 +98,46 @@ 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).", + 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, + builder: (context, ref, _) => + DialogListTile( + icon: Icon(icon), + title: title, + subtitle: Text(description), + initialValue: ref + .watch(AccountDataController.provider) + .invitePermissionConfig + .defaultAction, + options: specVersionsResponse.unstableFeatures.msc4494 + ? DefaultInviteAction.values + : IList( + DefaultInviteAction.values, + ).remove(.denyPublic).toList(), + getName: (option) => switch (option) { + .allow => "Allow", + .deny => "Deny", + .denyPublic => "Deny public", + }, + onChanged: (value) => ref + .watch(ClientController.provider.notifier) + .setAccountData( + .new( + type: AccountData.invitePermissionConfigKey, + content: InvitePermissionConfig( + defaultAction: value, + ), + ), + ) + .onError(showError), ), - ); - }, ), icon: Icons.person_off, ), @@ -252,14 +156,15 @@ class SettingsSectionsController final colorScheme = Theme.of(context).colorScheme; return M3EButton.icon( onPressed: () async { - await ref - .watch(UnifiedPushController.provider.notifier) - .deregister() - .onError(showError); + Navigator.of( + context, + ).popUntil((route) => route.isFirst); + + await WidgetsBinding.instance.endOfFrame; + await ref .watch(ClientController.provider.notifier) - .logout() - .onError(showError); + .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 7dc14b0..60696da 100644 --- a/lib/controllers/spaces.dart +++ b/lib/controllers/spaces.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_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/rooms.dart"; 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 db99868..0000000 --- a/lib/controllers/unified_push.dart +++ /dev/null @@ -1,178 +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 { - @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 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); - }, - 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 (event, roomMetadata) = await ref - .read(ClientController.provider.notifier) - .handlePush(json.decode(String.fromCharCodes(message.content))); - - if (event.unreadType?.shouldNotify() != 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.eventId, - body: switch (event.content) { - MessageContent(:final body?) || - StickerContent(:final body) => body, - _ => null, - }, - ); - - if (isInBackground) exit(0); - }, - onUnregistered: deregister, - ); - - ref.listen( - ClientStateController.provider.select((value) => value?.deviceId), - (_, _) => register(), - ); - - if (registered) { - // Needs to be registered every startup - await register(); - } - - return registered; - } - - Future register() async { - state = .loading(); - try { - final deviceId = ref.read( - ClientStateController.provider.select((value) => value?.deviceId), - ); - if (deviceId == null) return; - - 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."); - } - - await UnifiedPush.register( - instance: deviceId, - vapid: capabilities.webpush?.vapid, - ); - } 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 8f7fd8e..0000000 --- a/lib/controllers/unified_push_allowed.dart +++ /dev/null @@ -1,33 +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()) { - 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..1b17870 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -3,8 +3,10 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/open_graph_data.dart"; -class UrlPreviewController(final Uri url) - extends AsyncNotifier { +class UrlPreviewController extends AsyncNotifier { + final Uri url; + UrlPreviewController(this.url); + @override Future build() async { if (url.host == "matrix.to") return null; 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 a95da6d..0000000 --- a/lib/helpers/extensions/build_event_options.dart +++ /dev/null @@ -1,266 +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/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(Event, RelationType) onRelation, - }) { - final theme = Theme.of(context); - final danger = theme.colorScheme.error; - - final notifier = ref.read( - RoomChatController.provider((roomId, null)).notifier, - ); - final client = ref.read(ClientController.provider.notifier); - - final isPinned = ref - .watch(PinnedIdsController.provider(roomId)) - .contains(eventId); - - Future sendReaction(String emoji) async { - await notifier.sendReaction(emoji, this).onError(showError); - - await ref - .read(RecentEmojiController.provider.notifier) - .add(emoji) - .onError(showError); - } - - 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( - children: [ - for (final emoji in { - ...ref - .watch(RecentEmojiController.provider) - .map((entry) => entry.emoji), - "👍", - "🤣", - "😭", - "🤔", - }.take(4)) - IconButton( - icon: Text(emoji), - onPressed: () { - Navigator.of(context).pop(); - sendReaction(emoji); - }, - ), - 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(); - sendReaction(emoji); - }, - ), - ); - }, - ), - ], - ), - ), - ), - - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .message, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () => onRelation(this, .reply), - child: const ListTile( - leading: Icon(Icons.reply), - title: Text("Reply"), - ), - ), - - if (content is MessageContent && sender == userId) - PopupMenuItem( - onTap: () => onRelation(this, .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$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), - ), - )) - PopupMenuItem( - onTap: () => showReasonDialog( - title: "Delete Message", - description: - "Are you sure you want to delete this message? " - "This cannot be reversed.", - action: "Delete", - onConfirm: (reason) => notifier.deleteMessage(this, reason: reason), - ), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Delete", 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) => client.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/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart index 8595669..f4868d3 100644 --- a/lib/helpers/extensions/link_to_mention.dart +++ b/lib/helpers/extensions/link_to_mention.dart @@ -9,7 +9,6 @@ extension LinkToMention on String { /// /// Returns the decoded identifier (e.g. "#room:matrix.org") /// or null if this is not a Matrix link. - /// TODO: Needs to be reworked to handle event links. Might be worth just rewriting, I don't like this code. String? get mention { final trimmed = trim(); diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index 6beca04..bf48535 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -1,4 +1,4 @@ -import "package:material_ui/material_ui.dart"; +import "package:flutter/material.dart"; extension SchemeToTheme on ColorScheme { ThemeData get theme { diff --git a/lib/helpers/extensions/show_about_dialog.dart b/lib/helpers/extensions/show_about_dialog.dart index 6ccba4c..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, @@ -50,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/hooks/chat_scroll.dart b/lib/helpers/hooks/chat_scroll.dart deleted file mode 100644 index 3b1531eb..0000000 --- a/lib/helpers/hooks/chat_scroll.dart +++ /dev/null @@ -1,203 +0,0 @@ -import "dart:async"; - -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/event.dart"; -import "package:nexus/models/room_chat.dart"; - -final class ChatScroll({ - required final IList historyItems, - required final IList liveItems, - required final GlobalKey centerKey, - required final ScrollController scrollController, - required final bool atBottom, - required final Future Function(String id) jumpToId, - required final Future Function() jumpToBottom, - required final GlobalKey Function(String eventId) keyFor, -}) { - factory use({ - required AsyncValue controllerData, - required Future Function(Direction direction) paginate, - required Future Function() markRead, - required ValueNotifier contextualEvent, - }) { - final anchorId = useState(null); - - final itemKeys = useMemoized(() => {}, []); - GlobalKey keyFor(String eventId) => - itemKeys.putIfAbsent(eventId, GlobalKey.new); - - final scrollController = useScrollController(); - final centerKey = useMemoized(GlobalKey.new); - - final atBottom = useState(true); - - final pendingAnchorTarget = useState(null); - final anchorMountedCompleter = useRef?>(null); - - useEffect(() { - if (anchorId.value == null) { - if (controllerData case AsyncData(:final value?) - when value.timeline.isNotEmpty) { - final hasContextualEvent = value.timeline.any( - (event) => event.eventId == contextualEvent.value, - ); - - anchorId.value = hasContextualEvent - ? contextualEvent.value - : value.timeline.last.eventId; - } - } - - return null; - }, [controllerData, contextualEvent.value]); - - useEffect(() { - final target = pendingAnchorTarget.value; - if (target == null) return null; - - final found = - controllerData.value?.timeline.any( - (event) => event.eventId == target, - ) ?? - false; - - if (found || controllerData is AsyncError) { - if (found) { - anchorId.value = target; - - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = keyFor(target).currentContext; - if (context != null && context.mounted) { - anchorMountedCompleter.value?.complete(context); - anchorMountedCompleter.value = null; - } - }); - } else { - anchorMountedCompleter.value?.completeError( - StateError("Failed to load context for $target"), - ); - anchorMountedCompleter.value = null; - } - pendingAnchorTarget.value = null; - } - - return null; - }, [controllerData, pendingAnchorTarget.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.indexWhere((item) => item.eventId == 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]); - - useEffect( - () { - const loadThreshold = 500.0; - const readThreshold = 50.0; - - Future checkPosition() async { - if (!scrollController.hasClients) return; - - final position = scrollController.position; - - final isAtBottom = position.extentBefore <= readThreshold; - 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 <= readThreshold) { - await markRead(); - } - } - - scrollController.addListener(checkPosition); - - WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition()); - - return () => scrollController.removeListener(checkPosition); - }, - [ - scrollController, - controllerData, - paginate, - markRead, - contextualEvent.value, - ], - ); - - return .new( - historyItems: split.history, - liveItems: split.live, - centerKey: centerKey, - scrollController: scrollController, - atBottom: atBottom.value, - jumpToId: (String itemId) async { - if (!scrollController.hasClients) return; - - final existing = keyFor(itemId).currentContext; - if (existing != null && existing.mounted) { - // Already mounted, just scroll - await Scrollable.ensureVisible( - existing, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeInOut, - ); - } else { - final completer = Completer(); - anchorMountedCompleter.value = completer; - pendingAnchorTarget.value = itemId; - contextualEvent.value = itemId; - - final context = await completer.future; - if (!context.mounted) return; - - await Scrollable.ensureVisible(context, alignment: 10); - if (!context.mounted) return; - await Scrollable.ensureVisible( - context, - alignment: 0.5, - duration: const .new(milliseconds: 700), - curve: Curves.easeOutCirc, - ); - } - }, - 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, - ); - }, - keyFor: keyFor, - ); - } -} 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 5586cbf..aabfe47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,31 +1,26 @@ 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: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/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 @@ -42,7 +37,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; } @@ -59,59 +61,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) @@ -142,55 +134,36 @@ class const App({super.key}) extends StatelessWidget { ), child: Scaffold( body: Consumer( - builder: (_, ref, _) => switch (ref.watch( - MultiProviderController.provider( - .new([ - GomuksListenerController.provider, - NotificationController.provider, - UnifiedPushController.provider, - MemberListOpenedController.provider, - KeyController.provider(KeyController.roomKey), - KeyController.provider(KeyController.spaceKey), - ]), - ), - )) { - AsyncData(value: _) || AsyncLoading(value: _?) => Consumer( - builder: (_, ref, _) { - final clientState = ref.watch(ClientStateController.provider); + builder: (_, ref, _) => ref + .watch( + MultiProviderController.provider( + IListConst([ + SharedPrefsController.provider, + ClientController.provider, + ]), + ), + ) + .betterWhen( + data: (_) => Consumer( + builder: (_, ref, _) { + final clientState = ref.watch( + ClientStateController.provider, + ); - if (clientState == null || !clientState.isInitialized) { - return Loading(); - } + 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), - ), - ]), + if (!clientState.isLoggedIn) { + return SelectServerPage(); + } else if (!clientState.isVerified) { + return VerifyPage(); + } else { + return ChatPage(); + } + }, + ), ), - body: Loading(), - ), - AsyncError(:final error, :final stackTrace) => ErrorDialog( - error, - stackTrace, - ), - }, ), ), ), diff --git a/lib/models/account_data.dart b/lib/models/account_data.dart index 955d8d1..7df7459 100644 --- a/lib/models/account_data.dart +++ b/lib/models/account_data.dart @@ -1,60 +1,62 @@ 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(), +@freezed +abstract class AccountData with _$AccountData { + const AccountData._(); + static List? readRecentEmojiValue( + Map json, + String key, + ) => json[key]?["recent_emoji"]; - @JsonKey(name: AccountData.directKey) - final IMap> directMessages = const IMap.empty(), + static Map>? recentEmojiToJson( + IList recentEmoji, + ) => {"recent_emoji": recentEmoji.map((emoji) => emoji.toJson()).toList()}; - @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); + const factory AccountData({ + @JsonKey(name: AccountData.invitePermissionConfigKey) + @Default(InvitePermissionConfig()) + InvitePermissionConfig invitePermissionConfig, + + @JsonKey(name: AccountData.directKey) + @Default(IMap.empty()) + IMap> directMessages, + + @JsonKey( + name: AccountData.recentEmojiKey, + readValue: AccountData.readRecentEmojiValue, + toJson: AccountData.recentEmojiToJson, + ) + @Default(IList.empty()) + IList recentEmoji, + }) = _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); +@freezed +abstract class InvitePermissionConfig with _$InvitePermissionConfig { + const factory InvitePermissionConfig({ + @JsonKey(unknownEnumValue: DefaultInviteAction.allow) + @Default(DefaultInviteAction.allow) + DefaultInviteAction defaultAction, + }) = _InvitePermissionConfig; 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); +@freezed +abstract class RecentEmoji with _$RecentEmoji { + const factory RecentEmoji({required String emoji, required int total}) = + _RecentEmoji; factory RecentEmoji.fromJson(Map json) => _$RecentEmojiFromJson(json); 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 index 75faf08..8e4eac6 100644 --- a/lib/models/emoji.dart +++ b/lib/models/emoji.dart @@ -1,25 +1,17 @@ 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); +@freezed +abstract class Emoji with _$Emoji { + const factory Emoji({ + required String emoji, + required String category, + required IList aliases, + required String description, + required IList tags, + }) = _Emoji; - 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 dc3dd46..28bf0ae 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -3,39 +3,11 @@ 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"; - 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"]; @@ -53,6 +25,32 @@ class const Event({ } } + 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)), @@ -74,27 +72,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,9 +110,9 @@ class const UnreadType(final int value) with _$UnreadType { static const highlight = UnreadType(4); static const sound = UnreadType(8); - bool isNone() => value == 0; - bool isNormal() => (value & 1) != 0; - bool shouldNotify() => (value & 2) != 0; - bool isHighlighted() => (value & 4) != 0; - bool playsSound() => (value & 8) != 0; + 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/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 f9a814e..0cd250f 100644 --- a/lib/models/lazy_load_summary.dart +++ b/lib/models/lazy_load_summary.dart @@ -1,17 +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({ - required final IList? heroes, - required final int? joinedMemberCount, - 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/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_response.dart b/lib/models/profile_response.dart index 57f76c5..8b7b749 100644 --- a/lib/models/profile_response.dart +++ b/lib/models/profile_response.dart @@ -1,55 +1,53 @@ 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); +@freezed +abstract class ProfileResponse with _$ProfileResponse { + const factory ProfileResponse({ + @JsonKey(fromJson: Profile.fromJson) required Profile profile, + required Bio? bio, + }) = _ProfileResponse; 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); +@freezed +abstract class Bio with _$Bio { + const factory Bio({required String html, String? editSource}) = _Bio; 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); - +@freezed +abstract class Profile with _$Profile { 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"]; + const factory Profile({ + 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: "m.pronouns") + IList pronouns, + }) = _Profile; + factory Profile.fromJson(Map json) => _$ProfileFromJson(json); @@ -62,13 +60,10 @@ class const Profile({ } } -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const Pronoun({ - required final String language, - required final String summary, -}) with _$Pronoun { - Map toJson() => _$PronounToJson(this); +@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/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 index 95ef75c..ffccdb4 100644 --- a/lib/models/requests/set_account_data.dart +++ b/lib/models/requests/set_account_data.dart @@ -1,16 +1,14 @@ 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); +@freezed +abstract class SetAccountDataRequest with _$SetAccountDataRequest { + const factory SetAccountDataRequest({ + required String type, + required dynamic content, + String? roomId, + }) = _SetAccountDataRequest; factory SetAccountDataRequest.fromJson(Map json) => _$SetAccountDataRequestFromJson(json); diff --git a/lib/models/requests/set_membership.dart b/lib/models/requests/set_membership.dart index 9d88da2..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, - @JsonKey(name: "action") 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 96b29b4..fb21a55 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -3,44 +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 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( @@ -59,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 a038a88..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"; -import "package:nexus/models/event.dart"; - -part "room_chat.freezed.dart"; -part "room_chat.g.dart"; - -@Freezed(toJson: false, fromJson: false) -@JsonSerializable() -class const RoomChat({ - 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 3aafd0e..7c16cae 100644 --- a/lib/models/room_metadata.dart +++ b/lib/models/room_metadata.dart @@ -1,31 +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, +@freezed +abstract class RoomMetadata with _$RoomMetadata { + const factory RoomMetadata({ + @JsonKey(name: "room_id") required String id, - // CreateEventContent creationContent, - // TombstoneEventContent tombstoneEventContent, - 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); + // 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 7c2d6d2..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") required 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 7f73375..eddc2da 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,17 +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, -}) 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 index 1f414d2..07a3ada 100644 --- a/lib/models/spec_versions_response.dart +++ b/lib/models/spec_versions_response.dart @@ -1,27 +1,24 @@ 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); +@freezed +abstract class SpecVersionsResponse with _$SpecVersionsResponse { + const factory SpecVersionsResponse({ + required IList versions, + required UnstableFeatures unstableFeatures, + }) = _SpecVersionsResponse; 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); +@freezed +abstract class UnstableFeatures with _$UnstableFeatures { + const factory UnstableFeatures({ + @JsonKey(name: "uk.timedout.msc4494") @Default(false) bool msc4494, + }) = _UnstableFeatures; 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..b2699d6 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -2,21 +2,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.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 56% rename from lib/widgets/pages/chat.dart rename to lib/pages/chat.dart index 186dfa2..316c9af 100644 --- a/lib/widgets/pages/chat.dart +++ b/lib/pages/chat.dart @@ -1,23 +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/jump_to_event.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( @@ -27,19 +29,10 @@ class const ChatPage({super.key}) extends HookConsumerWidget { children: [ if (isDesktop) Sidebar(isDesktop: isDesktop), Expanded( - child: Consumer( - builder: (context, ref, _) { - final initialHighlight = ref.watch( - JumpToEventController.provider(roomId), - ); - return RoomChat( - key: ValueKey((roomId, initialHighlight)), - roomId: roomId, - isDesktop: isDesktop, - initialHighlightedEvent: initialHighlight, - showMembersByDefault: showMembersByDefault, - ); - }, + child: RoomChat( + roomId: roomId, + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, ), ), ], diff --git a/lib/widgets/pages/select_server.dart b/lib/pages/select_server.dart similarity index 77% rename from lib/widgets/pages/select_server.dart rename to lib/pages/select_server.dart index 34f1a50..853e5fc 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); @@ -67,7 +70,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { .watch(ClientController.provider.notifier) .exchangeToken( .new( - homeserverUrl: url, + homeserverUrl: newUrl, codeVerifier: codeResponse.codeVerifier, redirectUri: .new( scheme: "nexus.federated.nexus", @@ -75,42 +78,20 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { ), code: code, clientId: await ref.watch( - ClientIdController.provider(url).future, + ClientIdController.provider(newUrl).future, ), ), - ); + ) + .onError(showError); } - } catch (error, stackTrace) { - showError(error, stackTrace); - rethrow; } finally { isLoading.value = false; } }); } - - 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; } @@ -135,7 +116,7 @@ class const SelectServerPage({super.key}) extends HookConsumerWidget { children: [ Row( children: [ - SvgPicture.asset("assets/bundled/icon.svg", width: 128), + SvgPicture.asset("assets/icon.svg", width: 128), SizedBox(width: 12), Expanded( child: Column( @@ -182,20 +163,23 @@ 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", ), .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.", + 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", ), 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 93% rename from lib/widgets/pages/verify.dart rename to lib/pages/verify.dart index a4f95ec..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(); diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 33727ac..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), diff --git a/lib/widgets/avatar_or_hash.dart b/lib/widgets/avatar_or_hash.dart index 982d56e..75c08b9 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -1,15 +1,21 @@ 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 height = 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( diff --git a/lib/widgets/composer/composer.dart b/lib/widgets/composer/composer.dart index 3d04a78..8b7fec4 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -1,9 +1,7 @@ import "dart:io"; - import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:file_selector/file_selector.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"; @@ -14,24 +12,33 @@ 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/widgets/emoji_picker_button.dart"; import "package:nexus/main.dart"; -class const Composer( - final String roomId, { - required final Event? 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); @@ -119,19 +126,10 @@ class const Composer( ), ) ? [ - IconButton( - onPressed: () => showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => EmojiPicker( - onSelection: (value) { - Navigator.of(context).pop(); - controller.value.text += value; - node?.requestFocus(); - }, - ), - ), - icon: Icon(Icons.emoji_emotions), + EmojiPickerButton( + context: context, + onSelection: (_) => node?.requestFocus(), + controller: controller.value, ), PopupMenuButton( tooltip: "Add media", @@ -145,8 +143,9 @@ class const Composer( ), onTap: () async => ref .watch( - AttachmentController.provider(roomId) - .notifier, + AttachmentController.provider( + roomId, + ).notifier, ) .add( (await ref @@ -164,8 +163,9 @@ class const Composer( ), onTap: () async => ref .watch( - AttachmentController.provider(roomId) - .notifier, + AttachmentController.provider( + roomId, + ).notifier, ) .add( (await ref @@ -179,8 +179,9 @@ class const Composer( PopupMenuItem( onTap: () async => ref .watch( - AttachmentController.provider(roomId) - .notifier, + AttachmentController.provider( + roomId, + ).notifier, ) .add((await openFile())!) .onError(showError), @@ -195,9 +196,9 @@ class const Composer( 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.value.addTag(id: id, name: name); @@ -212,7 +213,6 @@ class const Composer( triggerCharacterAndStyles: { "@": style, "#": style, - ":": style, }, builder: (context, key) => Focus( onKeyEvent: (_, event) { 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 a0c691a..0000000 --- a/lib/widgets/emoji_picker.dart +++ /dev/null @@ -1,53 +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) async { - await onSelection(emoji); - 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/emoji_picker_button.dart b/lib/widgets/emoji_picker_button.dart new file mode 100644 index 0000000..2ac906a --- /dev/null +++ b/lib/widgets/emoji_picker_button.dart @@ -0,0 +1,52 @@ +import "package:emoji_text_field/emoji_text_field.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/emoji.dart"; + +class EmojiPickerButton extends HookConsumerWidget { + final TextEditingController? controller; + final void Function(String emoji)? onSelection; + final VoidCallback? onPressed; + final BuildContext context; + const EmojiPickerButton({ + this.controller, + this.onPressed, + this.onSelection, + required this.context, + super.key, + }); + + @override + Widget build(_, WidgetRef ref) => IconButton( + onPressed: () async { + onPressed?.call(); + final controller = this.controller ?? .new(); + + final emojis = await ref.watch(EmojiController.provider.future); + if (context.mounted) { + showModalBottomSheet( + context: context, + builder: (context) => EmojiKeyboardView( + config: .new( + showRecentTab: false, + customCategories: emojis.$1.unlock, + customKeywords: emojis.$2.unlock, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + height: 600, + ), + textController: controller + ..addListener(() { + // Without this, there will sometimes be a debugLocked is not true error sometimes + // It might be preferable to use a microtask instead of a `Future.delayed`. + Future.delayed(.zero, () { + if (context.mounted) Navigator.of(context).pop(); + }); + onSelection?.call(controller.text); + }), + ), + ); + } + }, + icon: Icon(Icons.emoji_emotions), + ); +} 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 30dfbfe..7a40a75 100644 --- a/lib/widgets/event_preview.dart +++ b/lib/widgets/event_preview.dart @@ -1,12 +1,14 @@ -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( 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 abd9be2..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); @@ -29,7 +28,7 @@ class const CodeBlock( ), ), TextButton.icon( - onPressed: () => Clipboard.setData(.new(text: code)), + onPressed: () {}, icon: Icon(Icons.copy), label: Text("Copy"), ), diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 85f8054..5437285 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.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_riverpod/flutter_riverpod.dart"; import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; @@ -11,140 +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-", ""), - ) - : null, + 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), - ), + "blockquote" => Quoted( + Html(element.innerHtml, textStyle: textStyle, roomId: roomId), + ), - "a" => - element.attributes["href"]?.mention == null - ? null - : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!, roomId), - ), + "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(), + "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, + // 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)), - ), + _ => 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 2387a49..f6105e3 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -1,60 +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/room_summary.dart"; import "package:nexus/controllers/user.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"; -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 mention = content.mention; - 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), - - _ => null, - }; + 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: () { - if (data case MembershipContent membership) { + if (membership != null) { context.showUserPopover(membership, mention, roomId: roomId); - } else if (data case RoomSummary summary) { - // TODO: Handle summary } }, - child: Text( - switch (data) { - RoomSummary summary => - (summary.name == null ? null : "#${summary.name}") ?? - summary.canonicalAlias ?? - summary.roomId, - 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/join_dialog.dart b/lib/widgets/join_dialog.dart index e818c7d..50b36f2 100644 --- a/lib/widgets/join_dialog.dart +++ b/lib/widgets/join_dialog.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:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client.dart"; @@ -7,7 +7,10 @@ import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/spaces.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; -class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { +class JoinDialog extends HookWidget { + final WidgetRef ref; + const JoinDialog(this.ref, {super.key}); + @override Widget build(BuildContext context) { final roomAlias = useTextEditingController(); @@ -50,8 +53,9 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { .new( roomIdOrAlias: roomIdOrAlias, via: .new( - Uri.tryParse(roomAlias.text.replaceAll("/#", "")) - ?.queryParametersAll["via"] ?? + Uri.tryParse( + roomAlias.text.replaceAll("/#", ""), + )?.queryParametersAll["via"] ?? [], ), ), @@ -72,8 +76,9 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { await ref .watch( - KeyController.provider(KeyController.spaceKey) - .notifier, + KeyController.provider( + KeyController.spaceKey, + ).notifier, ) .set( space?.id ?? @@ -95,8 +100,9 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { if (space == null) { await ref .watch( - KeyController.provider(KeyController.roomKey) - .notifier, + KeyController.provider( + KeyController.roomKey, + ).notifier, ) .set(id); } @@ -109,9 +115,9 @@ class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { if (context.mounted) { scaffoldMessenger.showSnackBar( .new( - backgroundColor: Theme.of(context) - .colorScheme - .errorContainer, + backgroundColor: Theme.of( + context, + ).colorScheme.errorContainer, content: Text( error.toString(), style: .new( diff --git a/lib/widgets/lazy_loading/message_avatar.dart b/lib/widgets/lazy_loading/message_avatar.dart index 709d558..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), diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index 9203f69..bd5733c 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.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,12 +6,17 @@ import "package:nexus/helpers/extensions/show_user_popover.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; import "package:nexus/models/event.dart"; -class const MessageDisplayname( - final Event event, { - final TextStyle? style, - final bool clickable = 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), @@ -35,11 +40,14 @@ class const MessageDisplayname( maxLines: 1, overflow: .ellipsis, ), + if (event.pmp != null) Text( "(via ${event.sender})", - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: event.sender.colorHash, fontWeight: .bold), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: event.sender.colorHash, + fontWeight: .bold, + ), maxLines: 1, overflow: .ellipsis, ), 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 180cd1b..fc84563 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -1,6 +1,9 @@ -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? height}) extends StatelessWidget { @override Widget build(BuildContext context) => Center( child: Padding( diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 9ddafa3..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"; @@ -16,8 +16,10 @@ import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/user_bottom_sheet.dart"; -class const MemberList(final String roomId, {super.key}) - extends HookConsumerWidget { +class MemberList extends HookConsumerWidget { + final String roomId; + const MemberList(this.roomId, {super.key}); + @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) => 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/pages/notifications.dart b/lib/widgets/pages/notifications.dart deleted file mode 100644 index 2ad8930..0000000 --- a/lib/widgets/pages/notifications.dart +++ /dev/null @@ -1,215 +0,0 @@ -import "dart:async"; - -import "package:collection/collection.dart"; -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/jump_to_event.dart"; -import "package:nexus/controllers/key.dart"; -import "package:nexus/controllers/notifications.dart"; -import "package:nexus/controllers/spaces.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/widgets/appbar.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 NotificationsPage({ - final String? highlightedEventId, - final bool defaultToAllNotifications = false, - super.key, -}) extends HookConsumerWidget { - @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(highlightedEventId); - 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( - (element) => element.eventId == 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 Scaffold( - appBar: Appbar(title: Text("Notifications")), - body: 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 event = value[index]; - final isHighlighted = - event.eventId == highlightedId.value; - - return Padding( - padding: .only(top: 8), - child: HighlightWrapper( - InkWell( - onTap: () async { - final spaces = ref.read( - SpacesController.provider, - ); - final space = spaces.firstWhereOrNull( - (space) => - space.children.any( - (room) => - room.metadata?.id == - event.roomId, - ) || - space.subSpaces.any( - (subSpace) => - subSpace.children.any( - (room) => - room.metadata?.id == - event.roomId, - ), - ), - ); - if (space == null) return; - - await ref - .read( - KeyController.provider( - KeyController.spaceKey, - ).notifier, - ) - .set(space.id); - await ref - .read( - KeyController.provider( - KeyController.roomKey, - ).notifier, - ) - .set(event.roomId); - ref - .watch( - JumpToEventController.provider( - event.roomId, - ).notifier, - ) - .set(event.eventId); - - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - child: IgnorePointer( - child: EventRenderer(event), - ), - ), - 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 73d9a79..7cb9fe3 100644 --- a/lib/widgets/pinned_events_drawer.dart +++ b/lib/widgets/pinned_events_drawer.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:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/pinned_events.dart"; import "package:nexus/models/event.dart"; @@ -7,12 +7,17 @@ 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 Future Function(String eventId) jumpToId, - 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)); @@ -66,13 +71,11 @@ final class const PinnedEventsDrawer( }, child: Padding( padding: .symmetric(vertical: 4), - child: IgnorePointer( - child: EventRenderer( - event, - maxLines: 2, - isGrouped: false, - 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..22a91f7 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/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 uri; + final AudioInfo? info; + final bool encrypted; + + const AudioPlayer(this.uri, this.info, {this.encrypted = false, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final player = useMemoized( diff --git a/lib/widgets/players/video.dart b/lib/widgets/players/video.dart index 024c30e..457cdc4 100644 --- a/lib/widgets/players/video.dart +++ b/lib/widgets/players/video.dart @@ -1,6 +1,5 @@ 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"; @@ -8,12 +7,12 @@ import "package:flutter_hooks/flutter_hooks.dart"; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; -class const VideoPlayer( - final Uri uri, - final VideoInfo? info, { - final bool encrypted = false, - super.key, -}) extends HookConsumerWidget { +class VideoPlayer extends HookConsumerWidget { + final VideoInfo? info; + final Uri uri; + final bool encrypted; + const VideoPlayer(this.uri, this.info, {this.encrypted = false, super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final player = useMemoized( diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart index 195aa4f..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(( + RoomChatController.provider( event.roomId, - null, - )).notifier, + ).notifier, ); if (selected) { diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 697161f..a9c703a 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -1,6 +1,5 @@ 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/helpers/extensions/show_context_menu.dart"; @@ -25,17 +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 Event event, { - final bool textOnly = false, - final bool isGrouped = false, - final int? maxLines, - final VoidCallback? onTapReply, - 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); @@ -153,7 +159,7 @@ class const EventRenderer( final contextMenuCallback = getEventOptions == null ? null - : (PositionedGestureDetails details) => context.showContextMenu( + : (details) => context.showContextMenu( globalPosition: details.globalPosition, children: getEventOptions!(event).toList(), ); @@ -196,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, ), ), ), @@ -215,7 +213,10 @@ class const EventRenderer( ), ...[ - if (child is! MessageRenderer) ReactionRow(event), + if (event.content is! MessageContent && + event.content is! StickerContent && + event.content is! EncryptedContent) + ReactionRow(event), if (event.sendError != null && event.sendError != "not sent") Padding( diff --git a/lib/widgets/renderers/generic_event.dart b/lib/widgets/renderers/generic_event.dart index eb5b29b..6dfae53 100644 --- a/lib/widgets/renderers/generic_event.dart +++ b/lib/widgets/renderers/generic_event.dart @@ -1,19 +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, { - 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: Wrap(spacing: 4, children: children)), + Expanded(child: Wrap(spacing: 4, children: children)), ], ), ); diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index 5ce5d68..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,8 +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, {super.key}) - extends StatelessWidget { +class MembershipRenderer extends StatelessWidget { + final Event event; + const MembershipRenderer(this.event, {super.key}); + @override Widget build(BuildContext context) { assert( diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index a0bbf99..c684085 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -1,6 +1,5 @@ import "package:collection/collection.dart"; -import "package:flutter/rendering.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"; @@ -16,136 +15,48 @@ import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/linkified_text.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 timestamp = Tooltip( + message: event.timestamp.toString(), + child: Text( + format(event.timestamp), + maxLines: 1, + overflow: .ellipsis, + style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey), + ), + ); + final textStyle = TextStyle( fontSize: event.localContent?.bigEmoji == true ? 32 : null, fontStyle: event.content is EmoteMessageContent ? .italic : null, ); - 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) => - 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, :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), - ], - ], - ), - MessageContent(: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"), - }; - return Row( crossAxisAlignment: .start, mainAxisSize: .min, @@ -166,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( @@ -219,13 +130,172 @@ class const MessageRenderer( ), ), ), + 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)!; + } - RendererBinding.instance.mouseTracker.mouseIsConnected - ? SelectableRegion( - selectionControls: materialTextSelectionControls, - child: rendered, - ) - : rendered, + // 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, + :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), + ], + ], + ), + MessageContent(: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"), + }, if (!textOnly) ReactionRow(event), ], ), diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 305c490..69eafaa 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -1,4 +1,4 @@ -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"; @@ -7,14 +7,21 @@ import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/linkified_text.dart"; import "package:nexus/widgets/room_menu.dart"; -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 { +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, + }); + @override Size get preferredSize => AppBar().preferredSize; @@ -60,9 +67,9 @@ final class const RoomAppbar({ room.metadata?.name ?? "Unnamed Room", overflow: .ellipsis, maxLines: 3, - style: Theme.of(context) - .textTheme - .headlineSmall, + style: Theme.of( + context, + ).textTheme.headlineSmall, ), ), ], @@ -72,9 +79,9 @@ final class const RoomAppbar({ room.metadata!.topic!, style: Theme.of(context).textTheme.bodyLarge ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart new file mode 100644 index 0000000..f50db27 --- /dev/null +++ b/lib/widgets/room_chat.dart @@ -0,0 +1,602 @@ +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/member_list_opened.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/emoji_picker_button.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 userId = ref.watch(ClientStateController.provider)?.userId; + final memberListOpened = ref.watch(MemberListOpenedController.provider); + 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 scrollController = useScrollController(); + final controllerData = ref.watch(controllerProvider); + final historyListController = useRef(ListController()); + final liveListController = useRef(ListController()); + final timelineCenterKey = useMemoized(GlobalKey.new); + final timelineBoundary = useRef(null); + final hasMore = ref.watch( + RoomsController.provider.select( + (rooms) => rooms[roomId]?.hasMore ?? false, + ), + ); + + Future jumpToId(String eventId) async { + final events = controllerData.value; + if (events == null || !scrollController.hasClients) return; + + final sourceIndex = events.indexWhere( + (event) => event.eventId == eventId, + ); + final boundaryIndex = events.indexWhere( + (event) => event.eventId == timelineBoundary.value, + ); + if (sourceIndex < 0 || boundaryIndex < 0) return; + + final listController = sourceIndex <= boundaryIndex + ? historyListController.value + : liveListController.value; + if (!listController.isAttached) return; + listController.animateToItem( + index: sourceIndex <= boundaryIndex + ? boundaryIndex - sourceIndex + : sourceIndex - boundaryIndex - 1, + 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; + } + }); + } + + 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) => value.recentEmoji + .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), + ), + ), + EmojiPickerButton( + context: context, + onPressed: Navigator.of(context).pop, + onSelection: (emoji) => + notifier.sendReaction(emoji, event).onError(showError), + ), + ], + ), + ), + ), + 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) { + ref + .watch(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) { + AsyncData(:final value?) || + AsyncLoading(:final value?) => Builder( + builder: (_) { + if (value.isEmpty) return nothing; + var boundaryIndex = value.indexWhere( + (event) => + event.eventId == timelineBoundary.value, + ); + if (boundaryIndex < 0) { + boundaryIndex = value.length - 1; + timelineBoundary.value = + value[boundaryIndex].eventId; + } + + final historyLength = boundaryIndex + 1; + final liveLength = value.length - historyLength; + + int? eventIndexForKey(Key key) { + if (key is! ValueKey) return null; + final index = value.indexWhere( + (event) => event.eventId == key.value, + ); + return index < 0 ? null : index; + } + + int? historyChildIndex(Key key) { + if (key == const ValueKey("history-loader")) { + return historyLength; + } + final eventIndex = eventIndexForKey(key); + return eventIndex == null || + eventIndex >= historyLength + ? null + : historyLength - eventIndex - 1; + } + + int? liveChildIndex(Key key) { + final eventIndex = eventIndexForKey(key); + return eventIndex == null || + eventIndex < historyLength + ? null + : eventIndex - historyLength; + } + + Widget eventRow( + Event event, + Event? previousEvent, + ) => 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}", + ), + key: ValueKey(event.eventId), + isHighlighted: + highlightedEvent.value == event.eventId, + ); + + return CustomScrollView( + reverse: true, + center: timelineCenterKey, + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: SizedBox(height: composerSize.value), + ), + SuperSliverList.builder( + listController: liveListController.value, + itemCount: liveLength, + findChildIndexCallback: liveChildIndex, + itemBuilder: (_, index) => eventRow( + value[historyLength + index], + value.getOrNull( + historyLength + index - 1, + ), + ), + ), + SuperSliverList.builder( + key: timelineCenterKey, + listController: historyListController.value, + itemCount: historyLength + 1, + findChildIndexCallback: historyChildIndex, + itemBuilder: (_, index) { + if (index == historyLength) { + return _HistoryLoader( + key: const ValueKey("history-loader"), + hasMore: hasMore, + isLoading: + controllerData is AsyncLoading, + onLoad: notifier.loadOlder, + ); + } + final event = + value[historyLength - index - 1]; + return eventRow( + event, + value.getOrNull( + historyLength - index - 2, + ), + ); + }, + ), + ], + ); + }, + ), + 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 == true && showMembersByDefault) + MemberList(roomId), + ], + ), + ), + ), + ); + } +} + +class _HistoryLoader extends HookWidget { + final bool hasMore; + final bool isLoading; + final Future Function() onLoad; + + const _HistoryLoader({ + required this.hasMore, + required this.isLoading, + required this.onLoad, + super.key, + }); + + @override + Widget build(BuildContext context) { + useEffect(() { + if (hasMore) { + Future.microtask(onLoad); + } + return null; + }, const []); + + return Padding( + padding: .symmetric(vertical: 36), + child: Center( + child: ElevatedButton( + onPressed: hasMore && !isLoading ? onLoad : null, + child: isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(hasMore ? "Load More" : "Start of timeline"), + ), + ), + ); + } +} diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart deleted file mode 100644 index 2cab4ca..0000000 --- a/lib/widgets/room_chat/chat_timeline.dart +++ /dev/null @@ -1,80 +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/content/message.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 ChatScroll scroll, - required final Future Function(String) jumpToId, - required final IList Function(Event) getEventOptions, - required final String? highlightedEvent, - required final double composerHeight, - super.key, -}) extends StatelessWidget { - bool isGrouped(Event event, Event? previousEvent) => - previousEvent?.content is MessageContent && - previousEvent?.redactedBy == null && - previousEvent?.relationType != "m.replace" && - event.sender == previousEvent?.sender && - event.pmp?.id == previousEvent?.pmp?.id; - - Widget eventRow( - Event event, - Event? previousEvent, { - required Future Function(String) jumpToId, - required IList Function(Event) getEventOptions, - required String? highlightedEvent, - required Key key, - }) => HighlightWrapper( - EventRenderer( - event, - onTapReply: () => jumpToId(event.replyTo!), - getEventOptions: getEventOptions, - isGrouped: isGrouped(event, previousEvent), - ), - key: key, - isHighlighted: highlightedEvent == event.eventId, - ); - - @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.liveItems.length, - itemBuilder: (_, index) => eventRow( - scroll.liveItems[index], - index > 0 - ? scroll.liveItems.getOrNull(index - 1) - : scroll.historyItems.firstOrNull, - jumpToId: jumpToId, - getEventOptions: getEventOptions, - highlightedEvent: highlightedEvent, - key: scroll.keyFor(scroll.liveItems[index].eventId), - ), - ), - - SuperSliverList.builder( - key: scroll.centerKey, - itemCount: scroll.historyItems.length, - itemBuilder: (_, index) => eventRow( - scroll.historyItems[index], - scroll.historyItems.getOrNull(index + 1), - jumpToId: jumpToId, - getEventOptions: getEventOptions, - highlightedEvent: highlightedEvent, - key: scroll.keyFor(scroll.historyItems[index].eventId), - ), - ), - ], - ); -} diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart deleted file mode 100644 index 709d22d..0000000 --- a/lib/widgets/room_chat/room_chat.dart +++ /dev/null @@ -1,231 +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/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"; - -final class const RoomChat({ - required final String? roomId, - required final bool isDesktop, - required final bool showMembersByDefault, - final String? initialHighlightedEvent, - super.key, -}) extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final relatedEvent = useState(null); - final relationType = useState(RelationType.reply); - final contextualEvent = useState(initialHighlightedEvent); - 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 controllerProvider = RoomChatController.provider(( - roomId, - 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 composerNode = useFocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && event.logicalKey == .escape) { - relatedEvent.value = null; - return .handled; - } - - return .ignored; - }, - ); - - Future jumpToId(String eventId) async { - highlightedEvent.value = eventId; - - await scroll.jumpToId(eventId); - await Future.delayed(.new(milliseconds: 700), () { - if (highlightedEvent.value == eventId) highlightedEvent.value = null; - }); - } - - useEffect(() { - if (initialHighlightedEvent == null) return null; - - void check() { - if (!context.mounted) return; - - if (scroll.scrollController.hasClients) { - jumpToId(initialHighlightedEvent!); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) => check()); - } - } - - check(); - - return null; - }, [initialHighlightedEvent]); - - IList getEventOptions(Event event) => - event.buildEventOptions( - context: context, - ref: ref, - roomId: roomId, - userId: userId, - onRelation: (event, type) { - relatedEvent.value = event; - relationType.value = type; - composerNode.requestFocus(); - }, - ); - - 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) { - 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: ChatTimeline( - scroll: scroll, - jumpToId: jumpToId, - getEventOptions: getEventOptions, - highlightedEvent: highlightedEvent.value, - composerHeight: composerSize.value, - ), - ), - ), - 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, - relation: relatedEvent.value, - ) - .onError(showError), - relationType: relationType.value, - relatedEvent: relatedEvent.value, - onDismiss: () => relatedEvent.value = null, - ), - ), - ), - ], - ), - ), - - if (memberListOpened == true && showMembersByDefault) - MemberList(roomId), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/room_menu.dart b/lib/widgets/room_menu.dart index 7488ada..95936df 100644 --- a/lib/widgets/room_menu.dart +++ b/lib/widgets/room_menu.dart @@ -1,20 +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"; -final class const RoomMenu( - 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: (_) => [ @@ -35,7 +35,8 @@ final class const RoomMenu( await Clipboard.setData( .new( - text: "matrix:roomid/${room!.metadata?.id.substring(1)}$vias", + text: + "matrix:roomid/${room!.metadata?.id.substring(1)}$vias)", ), ); }, 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 cc6d441..1bd6cf5 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -1,35 +1,33 @@ 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: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/widgets/pages/notifications.dart"; -import "package:nexus/widgets/pages/settings.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"; -// Needed for navigation_rail_m3e (#65). -import "package:flutter/material.dart" as old_mat; +class Sidebar extends HookConsumerWidget { + final bool isDesktop; + const Sidebar({required this.isDesktop, super.key}); -class const Sidebar({required final bool isDesktop, super.key}) - extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedSpaceProvider = KeyController.provider( KeyController.spaceKey, ); - final selectedSpaceId = ref.watch(selectedSpaceProvider).requireValue; + final selectedSpaceId = ref.watch(selectedSpaceProvider); final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedRoomController = KeyController.provider( KeyController.roomKey, ); - final selectedRoomId = ref.watch(selectedRoomController).requireValue; + final selectedRoomId = ref.watch(selectedRoomController); final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); final spaces = ref.watch(SpacesController.provider); @@ -76,149 +74,126 @@ class const Sidebar({required final bool isDesktop, super.key}) shape: Border(), child: Row( children: [ - MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemCollapsedHeight: 48, - itemVerticalGap: 0, - ), - ], + Theme( + data: Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemCollapsedHeight: 48, + itemVerticalGap: 0, ), - 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 notifications", - onPressed: () { - if (!isDesktop) Navigator.of(context).pop(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => NotificationsPage(), - ), - ); + ], + ), + 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, }, - icon: Icon(Icons.notifications), + short: true, + icon: AvatarOrHash( + height: 28, + space.room?.metadata?.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + ), + label: space.title, ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( + ) + .toList(), + ), + ], + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: .symmetric(vertical: 16), + child: Column( + spacing: 8, + children: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => showDialog( context: context, - builder: (_) => SettingsPage(), + 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.settings), ), ], + 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, @@ -244,75 +219,67 @@ class const Sidebar({required final bool isDesktop, super.key}) ), ], ), - body: MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, - ), - ], + 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, - ), - ), - ], + ], + ), + 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(); - }, - ), - ), + ), + 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..3a20e19 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 Uri link; + const UrlPreview(this.link, {super.key}); + @override Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox( constraints: .loose(.fromWidth(400)), @@ -18,9 +21,9 @@ class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { onTap: () => ref.watch(LaunchHelper.provider).launchUrl(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 +34,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 9fbdf35..15a24a0 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, @@ -200,35 +200,36 @@ final class const UserBottomSheet( ), if (ref.watch( - PowerLevelController.provider( - .membershipAction( - action: .kick, - roomId: roomId!, - targetUser: userId, - ), - ), - )) + PowerLevelController.provider( + .membershipAction( + action: .kick, + roomId: roomId!, + targetUser: userId, + ), + ), + ) && + member.status == .join || + member.status == .invite) 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(.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), 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 756eb0b..0000000 --- a/linux/nix/default.nix +++ /dev/null @@ -1,59 +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; - }; - 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" - ]; - - 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..812082c 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" @@ -23,7 +23,7 @@ pkgs.mkShell { [ go git - + jdk17 libGL (flutter.override { extraPkgConfigPackages = [ @@ -40,11 +40,13 @@ pkgs.mkShell { 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"; + TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; } // lib.optionalAttrs pkgs.stdenv.isLinux { + CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ]; LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; }; } 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 3e55dfe..c026606 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -32,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-As1X1/IfCbSD5ful2kAxgBita95KxmInRN4/6QmVaTI="; }; 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 ''; diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix index 64e0faa..ffa9d7c 100644 --- a/linux/nix/pkg/gomuks.nix +++ b/linux/nix/pkg/gomuks.nix @@ -17,7 +17,7 @@ buildGoModule ( src = "${src}/gomuks"; - vendorHash = "sha256-rLLDbNrYf5HqG3Y6zihRi1v+GqGeNAtZUL+8QHdCz7w="; + vendorHash = "sha256-C03ss88QAnmZu+XxoHhc1M/261xFl8hR/pzLbU37Qe8="; buildPhase = '' runHook preBuild 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/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 index 65543e2..ff5ddb3 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '13.0' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index fa18d44..67876ad 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -567,7 +567,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 +649,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 +699,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; 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 51e0bd6..9be2c20 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,34 +109,34 @@ 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: "94eaf6708fe64408c632ef2689ca3777b112f9421306ccf4f8c84d7c5c9f83f8" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: e1d40ef3f7934986d5da2271b1ba07794921ce263e44d622fb6c406d76589e33 + sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6" url: "https://pub.dev" source: hosted - version: "4.1.6" + version: "4.1.5" 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: @@ -157,10 +149,10 @@ packages: dependency: transitive description: name: built_value - sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 + sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" url: "https://pub.dev" source: hosted - version: "8.13.0" + version: "8.12.7" 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: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" url: "https://pub.dev" source: hosted - version: "0.7.15" + version: "0.7.13" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "869b3bce0100eb519768ecd71c0111f0b5ea679f5f62e1f59c49d80067e730c5" + sha256: "4aeb76de323e8eb660bab5557d826ad7ebbf1434a598f0c6aa8a11a9696a6757" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "1.9.0" 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: "56fc0be88937409839a59c133dfdee4165e80da324c9e39f59013060249ffb44" url: "https://pub.dev" source: hosted - version: "22.0.0" + version: "21.0.0" file: dependency: transitive description: @@ -381,34 +390,34 @@ packages: dependency: transitive description: name: file_selector_android - sha256: d670cd0ce77a2e785b18d8b4d0a8d6a222d6a813ec9b7ddf2790a1b4fb6fa92c + sha256: "1d45e9910f68c16eb0c74f0b10097ad81aed516ea28054c027137e8f7d75e840" url: "https://pub.dev" source: hosted - version: "0.5.2+11" + version: "0.5.2+9" 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: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.9.1" 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: @@ -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: "20b29df165d4d866ffd0d4808090210740690707705f44fc61c9b275bec3af36" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "0.0.5" 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: fff8b05f9f8c4a0da552397b03823b4fe351e98ec8aa4ed46e3ab9a7f6655c83 - url: "https://pub.dev" - source: hosted - version: "1.1.1+2" - material_ui: - dependency: "direct main" - description: - name: material_ui - sha256: "134ab2f0843545c4e661e4ba2753fe1bcf0753cb690d5a9ee4870638a40411e9" - url: "https://pub.dev" - source: hosted - version: "1.4.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: @@ -1010,35 +955,43 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "90eaad8059ebc2004a63810a4696ea9acdb02473e47ed3b6b3bafb100ac77e31" + sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72 url: "https://pub.dev" source: hosted - version: "0.19.5" + version: "0.19.2" 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: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.6.0" + version: "9.5.0" 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: @@ -1131,10 +1084,10 @@ packages: 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,22 +1096,14 @@ 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: @@ -1167,30 +1112,22 @@ packages: 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" 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 +1140,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 +1236,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 +1288,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,10 +1321,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "11b6047da8e4eb6c643ccac3d0fda3f9c9e11651695f7da24a85ade8aff15a44" + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.2.4" source_helper: dependency: transitive description: @@ -1380,6 +1333,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.13" + 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 +1361,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 +1385,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 +1409,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "397b146f97613b83d84bdccb439bc8d0f3aebb6c1d14a9641e6f24201c490b2d" + sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423" url: "https://pub.dev" source: hosted - version: "3.4.2" + version: "3.4.1+1" term_glyph: dependency: transitive description: @@ -1452,14 +1421,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 +1453,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 +1461,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 +1505,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 +1553,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: @@ -1650,34 +1593,34 @@ packages: 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: "7253bca0fcf40d8413ddfcf4d2a1fa0a82475e79be25a4f2c564b695c9351486" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.7.0" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "764c25504562abc8ac3406f5d175f126b276e6ba916c7c2675690b73368ca384" + sha256: "0618d1799f0b28bcf98255b4ee8313e6fc4d38589dc4ee5fe5840d57d1aff6da" url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.6.0" watcher: dependency: transitive description: @@ -1710,22 +1653,14 @@ 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: @@ -1742,15 +1677,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.2" - xdg_desktop_portal: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "9ead341e908da2972293b2edc345f2b0a6423d78" - url: "https://github.com/Henry-Hiles/xdg_desktop_portal.dart" - source: git - version: "0.1.14" xdg_directories: dependency: "direct main" description: @@ -1771,10 +1697,10 @@ packages: 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 +1710,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 2535e0b..adf1ba1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nexus -description: "A simple and user-friendly Matrix client" +description: "Yet another Matrix client" version: 0.1.0 publish_to: none @@ -12,24 +12,13 @@ flutter: fonts: - asset: assets/fonts/Roboto.ttf assets: - - assets/bundled/ + - 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: @@ -41,79 +30,69 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_riverpod: 3.4.3 - hooks_riverpod: 3.4.3 - intl: 0.20.3 + 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 + image_picker: ^1.2.3 path: 1.9.1 - dynamic_color: 2.1.0 + dynamic_color: 1.9.0 collection: 1.19.1 window_manager: 0.5.2 color_hash: 1.0.1 - flutter_widget_from_html_core: 0.17.4 + 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.8 + dynamic_polls: 0.0.7 flutter_hooks: 0.21.3+1 ffi: 2.2.0 - hooks: 2.2.0 - code_assets: 2.1.0 - ffigen: 22.0.0 + hooks: 2.0.2 + code_assets: 1.2.1 + ffigen: 21.0.0 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: 1.0.3 + m3e_buttons: 0.0.5 navigation_rail_m3e: git: url: https://github.com/Henry-Hiles/material_3_expressive path: packages/navigation_rail_m3e - m3e_card_list: 1.0.0 + 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 - material_ui: 1.4.0 - unifiedpush: 6.2.0 - unifiedpush_storage_shared_preferences: 1.0.0 - flutter_local_notifications: 22.3.1 - material_emoji_picker: 1.1.1+2 - xdg_desktop_portal: - git: - url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart + file_selector: ^1.1.0 dev_dependencies: - build_runner: 2.16.1 + build_runner: ^2.15.1 flutter_lints: 6.0.0 - freezed: 4.0.2 - riverpod_lint: 3.1.9 + freezed: 3.2.6-dev.1 + riverpod_lint: 3.1.4 flutter_launcher_icons: 0.14.4 json_serializable: 6.14.1 - # Transitive dep but needed for commands - webcrypto: 0.6.0 flutter_launcher_icons: - image_path: assets/icon.png ios: true - image_path_ios: assets/mobile.png 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 - macos: - generate: true - image_path: assets/mobile.png diff --git a/scripts/generate.dart b/scripts/generate.dart index d8a8cb1..4dc7329 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,23 +6,21 @@ 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", ], ), + functions: Functions.includeAll, ).generate( libclangDylib: libclangPath == null ? null @@ -40,5 +37,5 @@ void main(List args) async { ), ), ); - 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)