diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index dc1e9c7..a3abae1 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 --release" + 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" env: KEYSTORE_PATH: ../../keystore.jks KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 0000000..eeb4868 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,48 @@ +name: "Build iOS App" + +on: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: + +jobs: + build-app: + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.47.4 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: gomuks/go.mod + + - name: Build app + run: | + flutter pub get + dart scripts/generate.dart + flutter pub run build_runner build + flutter build ios --release --no-codesign + + # Unsigned IPA: SideStore/AltStore re-sign this on-device with the + # user's own Apple ID at install time, so no certificate is needed here. + - name: Package unsigned IPA + run: | + mkdir Payload + cp -r build/ios/iphoneos/Runner.app Payload/ + zip -r Nexus.ipa Payload + + - name: Upload IPA artifact + uses: actions/upload-artifact@v6 + with: + name: Nexus.ipa + path: Nexus.ipa diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 8ed63cf..45d04d7 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,32 +1,47 @@ name: "Build MacOS App" on: - # TODO: Uncomment this once the MacOS workflow actually works - # push: - # branches: ["main"] - # tags: ["*"] - workflow_dispatch: + push: + branches: ["main"] + tags: ["*"] + workflow_dispatch: jobs: - build-app: - runs-on: macos-latest + build-app: + runs-on: macos-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - submodules: recursive + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: recursive - - name: Lix GHA Installer Action - uses: samueldr/lix-gha-installer-action@v2026-02-22 - with: - extra_nix_config: experimental-features = nix-command flakes flake-self-attrs + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.47.4 - - name: Build app - run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build macos --release" + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: gomuks/go.mod - - name: Upload installer artifact - uses: actions/upload-artifact@v6 - with: - name: App - path: build/macos/Build/Products/Release/Nexus.app \ No newline at end of file + - 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 + uses: actions/upload-artifact@v6 + with: + name: nexus.dmg + path: ${{ steps.create-dmg.outputs.dmg_path }} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d9c8dd5..4cb30cd 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,65 +1,64 @@ 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.44.4 + - 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 Go - uses: actions/setup-go@v6 - with: - go-version-file: gomuks/go.mod + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: >- + mingw-w64-x86_64-gcc - - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - install: >- - mingw-w64-x86_64-gcc + - name: Go build + run: | + cd gomuks/pkg/ffi + go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared - - name: Go build - run: | - cd gomuks/pkg/ffi - go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared + - name: Build with Flutter + run: | + flutter pub get + dart scripts/generate.dart + flutter pub run build_runner build + flutter build windows --release - - name: Build with Flutter - run: | - flutter pub get - dart scripts/generate.dart - flutter pub run build_runner build - flutter build windows --release + - name: Copy MinGW runtime DLLs + shell: msys2 {0} + run: | + cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/ + cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/ + cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/ - - name: Copy MinGW runtime DLLs - shell: msys2 {0} - run: | - cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/ - cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/ + - name: Install Inno Setup + run: choco install innosetup -y - - name: Install Inno Setup - run: choco install innosetup -y + - name: Build Inno Setup installer + run: iscc windows/installer.iss - - 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 + - name: Upload installer artifact + uses: actions/upload-artifact@v6 + with: + name: windows-installer + path: windows/dist/Nexus-Setup.exe diff --git a/.gitmodules b/.gitmodules index 145276a..06dd51a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "gomuks"] path = gomuks url = https://github.com/gomuks/gomuks - branch = main diff --git a/.metadata b/.metadata index 1be1841..f12453b 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "nixpkgs000000000000000000000000000000000" + revision: "6b182d2c7585eba26d4edce0f97630effd256c33" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: nixpkgs000000000000000000000000000000000 - base_revision: nixpkgs000000000000000000000000000000000 - - platform: macos - create_revision: nixpkgs000000000000000000000000000000000 - base_revision: nixpkgs000000000000000000000000000000000 + create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + - platform: ios + create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b83039c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "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 2ff533e..fac2ed8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "muks", "prefs", "unban", - "unredact" + "unifiedpush", + "unredact", + "webpush" ] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index fb69a56..d9c7c9d 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.watch(MyController.provider.notifier).helperMethod() +ref.read(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 57b42d8..1da8adb 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,17 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. ## Progress -- [ ] Platform Support +- [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] Android - - [ ] MacOS ([#50](https://git.federated.nexus/Nexus/nexus/issues/50)) - - [ ] iOS + - [x] MacOS + - [x] Unsigned .ipa + - [ ] App Store + - [x] iOS + - [x] Unsigned .ipa + - [ ] App Store - [ ] Web (may not be possible) - [x] Login (via OAuth) - [x] Rooms / Spaces @@ -50,10 +55,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] Mentions + - [x] Tags - [x] Users - [x] Rooms - - [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions) + - [x] Emojis - [ ] Custom emojis/stickers - [ ] GIFs using Gomuks' GIF proxies - [x] Receiving @@ -63,9 +68,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] URL Previews - [x] Replies - [x] Viewing - - [ ] Jump to original message - - [x] In loaded timeline - - [ ] Out of loaded timeline + - [x] Jump to original message - [x] Edits - [x] Attachments - [x] Unencrypted @@ -105,7 +108,8 @@ 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 -- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) +- [x] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35)) +- [x] Notifications page - [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195) - [ ] Invites - [x] Settings @@ -115,10 +119,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) +- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip) +- [Unsigned MacOS DMG](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/nexus.dmg.zip) - [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip) - 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 @@ -126,8 +133,8 @@ If you want to try out Nexus, grab one of the following artifacts from CI: #### Linux -- With Nix: Either use direnv and `direnv allow`, or `nix flake develop` -- Without Nix: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues. +- On NixOS: Either use direnv and `direnv allow`, or `nix flake develop` +- On other distros: Install Flutter, Go, Git, Libclang, Libass, MPV, and Glibc. Do not use any Snap packages, they cause various compilation issues. #### Windows @@ -187,6 +194,12 @@ 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/altstore-source.json b/altstore-source.json new file mode 100644 index 0000000..4471fb6 --- /dev/null +++ b/altstore-source.json @@ -0,0 +1,22 @@ +{ + "name": "Nexus", + "identifier": "nexus.federated.nexus.source", + "subtitle": "Nexus for iOS", + "description": "Nexus iOS builds for AltStore and SideStore.", + "iconURL": "https://git.federated.nexus/Nexus/nexus/raw/branch/main/assets/icon.png", + "website": "https://git.federated.nexus/Nexus/nexus", + "apps": [ + { + "name": "Nexus", + "bundleIdentifier": "nexus.federated.nexus", + "developerName": "Nexus", + "subtitle": "A Matrix client", + "localizedDescription": "Nexus for iOS. Unsigned builds, resigned at install by AltStore/SideStore.", + "iconURL": "https://git.federated.nexus/Nexus/nexus/raw/branch/main/assets/icon.png", + "category": "social", + "screenshots": [], + "versions": [] + } + ], + "news": [] +} diff --git a/analysis_options.yaml b/analysis_options.yaml index a8b1078..00df0c4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,12 +1,18 @@ analyzer: - errors: - invalid_annotation_target: ignore - avoid_print: ignore - exclude: - - "build/**" - - "**/*.g.dart" - - "**/*.freezed.dart" + errors: + invalid_annotation_target: ignore + avoid_print: ignore + exclude: + - "build/**" + - "**/*.g.dart" + - "**/*.freezed.dart" + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: - rules: - prefer_double_quotes: true \ No newline at end of file + rules: + prefer_double_quotes: true diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 2e7fb67..0000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,77 +0,0 @@ -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 new file mode 100644 index 0000000..9c0d96c --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,84 @@ +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 deleted file mode 100644 index 11bc662..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -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 new file mode 100644 index 0000000..441eb71 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,25 @@ +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 74b269f..5fadfe8 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-8.14.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-all.zip diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 7cffcd0..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -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 new file mode 100644 index 0000000..b59bc94 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +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/icon.svg b/assets/bundled/icon.svg similarity index 100% rename from assets/icon.svg rename to assets/bundled/icon.svg diff --git a/assets/twim/notif.webp b/assets/twim/notif.webp new file mode 100644 index 0000000..d8d9010 Binary files /dev/null and b/assets/twim/notif.webp differ diff --git a/assets/twim/oauth.webp b/assets/twim/oauth.webp deleted file mode 100644 index 36fbedf..0000000 Binary files a/assets/twim/oauth.webp and /dev/null differ diff --git a/assets/twim/settings.png b/assets/twim/settings.png deleted file mode 100644 index 3037be1..0000000 Binary files a/assets/twim/settings.png and /dev/null differ diff --git a/flake.lock b/flake.lock index 41cfb9d..efbcc03 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1782949081, - "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "lastModified": 1788450739, + "narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", "type": "github" }, "original": { @@ -42,15 +42,16 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1774860670, - "narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=", - "owner": "neobrain", + "lastModified": 1790043059, + "narHash": "sha256-7ID4HL4IrrlalCsxMASkcAr+v0VHTNFp6+sZ+dpvOV8=", + "owner": "Henry-Hiles", "repo": "nix2flatpak", - "rev": "61d68e21e3fbc2d57590051f48736bea271f4aba", + "rev": "290dd282e555cab988c021c2be5f2361e58c4d51", "type": "github" }, "original": { - "owner": "neobrain", + "owner": "Henry-Hiles", + "ref": "quad/nexus", "repo": "nix2flatpak", "type": "github" } @@ -73,11 +74,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1782614948, - "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", + "lastModified": 1788057806, + "narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", + "rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c", "type": "github" }, "original": { @@ -88,11 +89,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1784497964, - "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", + "lastModified": 1790185690, + "narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", + "rev": "4975466d324710c576dc11ad614684e6bd8cad8e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2b13a08..97d6068 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:neobrain/nix2flatpak"; + nix2flatpak.url = "github:Henry-Hiles/nix2flatpak/quad/nexus"; }; outputs = @@ -16,62 +16,12 @@ ... }@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 9444dd2..480938f 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit 9444dd293664c300c6e710bb6cddcd9c6cab365d +Subproject commit 480938f9e01e7eae2cf249657efae03df76599d6 diff --git a/hook/build.dart b/hook/build.dart index 984f8c6..9315e04 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -1,4 +1,5 @@ import "dart:io"; + import "package:collection/collection.dart"; import "package:hooks/hooks.dart"; import "package:code_assets/code_assets.dart"; @@ -13,13 +14,47 @@ Future main(List args) => build(args, (input, output) async { String libFileName; Map extraEnv = {}; + IOSSdk? iosSdk; + String? iosSdkPath; + String? iosTargetTriple; switch (targetOS) { case OS.linux: libFileName = "libgomuks.so"; break; + case OS.iOS: + // Go only supports buildmode=c-archive for ios, not c-shared, so the + // archive built below gets linked into a dylib via a separate clang + // invocation before being registered as a normal asset like macOS. + libFileName = "libgomuks.dylib"; + final iosConfig = codeConfig.iOS; + iosSdk = iosConfig.targetSdk; + final minVersion = iosConfig.targetVersion; + iosSdkPath = await getXCodeTool(sdkType: iosSdk.type); + + final archTriple = switch (targetArch) { + Architecture.arm64 => "arm64", + Architecture.x64 => "x86_64", + _ => throw UnsupportedError( + "Unsupported iOS architecture: $targetArch", + ), + }; + + iosTargetTriple = iosSdk == IOSSdk.iPhoneSimulator + ? "$archTriple-apple-ios$minVersion.0-simulator" + : "$archTriple-apple-ios$minVersion.0"; + extraEnv = { + "GOOS": "ios", + "CC": await getXCodeTool(sdkType: iosSdk.type, findTool: "clang"), + "CGO_CFLAGS": "-isysroot $iosSdkPath -target $iosTargetTriple", + "CGO_LDFLAGS": "-isysroot $iosSdkPath -target $iosTargetTriple", + }; + break; case OS.macOS: libFileName = "libgomuks.dylib"; - extraEnv = {"SDKROOT": await getXCodeSDK()}; + extraEnv = { + "SDKROOT": await getXCodeTool(), + "MACOSX_DEPLOYMENT_TARGET": codeConfig.macOS.targetVersion.toString(), + }; break; case OS.windows: libFileName = "libgomuks.dll"; @@ -86,15 +121,28 @@ Future main(List args) => build(args, (input, output) async { final tags = [ "sqlite_fts5", "goolm", - // goheif/dav1d is not supported on Android, would need to be fixed upstream - if (targetOS == OS.android) "noheic", + // goheif/dav1d is not supported on Android or iOS, would need to be fixed upstream + if (targetOS == OS.android || targetOS == OS.iOS) "noheic", ].join(","); + + final archiveFile = targetOS == OS.iOS + ? buildDir.resolve("${targetArch.name}/libgomuks.a") + : libFile; print( - "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${libFile.path}...", + "Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${archiveFile.path}...", ); final result = await Process.run( "go", - ["build", "-tags", tags, "-o", libFile.path, "-buildmode=c-shared"], + [ + "build", + "-trimpath", + "-ldflags=-s -w", + "-tags", + tags, + "-o", + archiveFile.path, + "-buildmode=${targetOS == OS.iOS ? "c-archive" : "c-shared"}", + ], workingDirectory: gomuksBuildDir.resolve("pkg/ffi/").toFilePath(), environment: { "CGO_ENABLED": "1", @@ -114,6 +162,35 @@ Future main(List args) => build(args, (input, output) async { "Failed to build Gomuks shared library\n${result.stderr}", ); } + + if (targetOS == OS.iOS) { + print("Linking $archiveFile into $libFile..."); + final linkResult = await Process.run("xcrun", [ + "--sdk", + iosSdk!.type, + "clang", + "-dynamiclib", + "-isysroot", + iosSdkPath!, + "-target", + iosTargetTriple!, + "-install_name", + "@rpath/libgomuks.dylib", + "-framework", + "Security", + "-framework", + "CoreFoundation", + "-framework", + "SystemConfiguration", + "-force_load", + archiveFile.path, + "-o", + libFile.path, + ]); + if (linkResult.exitCode != 0) { + throw Exception("Failed to link Gomuks dylib\n${linkResult.stderr}"); + } + } } final generatedFile = "src/third_party/gomuks.g.dart"; diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..b29d5cb --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - Flutter (1.0.0) + - media_kit_libs_ios_video (1.0.4): + - Flutter + - media_kit_video (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) + - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + media_kit_libs_ios_video: + :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" + media_kit_video: + :path: ".symlinks/plugins/media_kit_video/ios" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 + media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.17.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..065d08b --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,759 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2ADFEE6C745FFB2100CC9AFD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 88D55A5F1C7625122EBEE29D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 22DE4310DAFBE211CDF700B1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 363A89F6FA09BCCBDED440CE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 63516CF65A79A5DDDE64B0D1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AEB96D8976BD46D6860C7041 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B8B599FDB72E4713FB7B5A44 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FE5E4162842555A83F8A7FD7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8C0A36760DA8B873D8F6DDCE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2ADFEE6C745FFB2100CC9AFD /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 88D55A5F1C7625122EBEE29D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1CFFAE23B502BCCEB2B191A3 /* Pods */ = { + isa = PBXGroup; + children = ( + AEB96D8976BD46D6860C7041 /* Pods-Runner.debug.xcconfig */, + 363A89F6FA09BCCBDED440CE /* Pods-Runner.release.xcconfig */, + 22DE4310DAFBE211CDF700B1 /* Pods-Runner.profile.xcconfig */, + B8B599FDB72E4713FB7B5A44 /* Pods-RunnerTests.debug.xcconfig */, + FE5E4162842555A83F8A7FD7 /* Pods-RunnerTests.release.xcconfig */, + 63516CF65A79A5DDDE64B0D1 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 594EB1F65336CF9C7958F6FA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 82F9A3759658343C81A0EA4C /* Pods_Runner.framework */, + 8156A45F510382D558AED93A /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 1CFFAE23B502BCCEB2B191A3 /* Pods */, + 594EB1F65336CF9C7958F6FA /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EE82EC529EB6C0F1FA480659 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 8C0A36760DA8B873D8F6DDCE /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 0830C6758DA3ECA81FB12386 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1D165FDFB9D0D939B041966D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0830C6758DA3ECA81FB12386 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 1D165FDFB9D0D939B041966D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + EE82EC529EB6C0F1FA480659 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PK66NJM372; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B8B599FDB72E4713FB7B5A44 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FE5E4162842555A83F8A7FD7 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 63516CF65A79A5DDDE64B0D1 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PK66NJM372; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PK66NJM372; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c73d4ae --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..1682af0 Binary files /dev/null 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 new file mode 100644 index 0000000..51f4a9b Binary files /dev/null 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 new file mode 100644 index 0000000..97b3b3f Binary files /dev/null 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 new file mode 100644 index 0000000..17d971e Binary files /dev/null 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 new file mode 100644 index 0000000..ca74554 Binary files /dev/null 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 new file mode 100644 index 0000000..6129702 Binary files /dev/null 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 new file mode 100644 index 0000000..16c3d1e Binary files /dev/null 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 new file mode 100644 index 0000000..97b3b3f Binary files /dev/null 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 new file mode 100644 index 0000000..f515ebd Binary files /dev/null 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 new file mode 100644 index 0000000..2e49a13 Binary files /dev/null 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 new file mode 100644 index 0000000..66b9ef2 Binary files /dev/null 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 new file mode 100644 index 0000000..53af9ff Binary files /dev/null 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 new file mode 100644 index 0000000..455803d Binary files /dev/null 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 new file mode 100644 index 0000000..7a2fd0f Binary files /dev/null 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 new file mode 100644 index 0000000..2e49a13 Binary files /dev/null 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 new file mode 100644 index 0000000..c34ad9e Binary files /dev/null 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 new file mode 100644 index 0000000..5e21545 Binary files /dev/null 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 new file mode 100644 index 0000000..2140e17 Binary files /dev/null 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 new file mode 100644 index 0000000..a27d854 Binary files /dev/null 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 new file mode 100644 index 0000000..8b4eb75 Binary files /dev/null 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 new file mode 100644 index 0000000..21c424b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..7680b42 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,79 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Nexus + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + nexus + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + nexus.federated.nexus + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/controllers/attachment.dart b/lib/controllers/attachment.dart index 2a99ee5..d4a5e09 100644 --- a/lib/controllers/attachment.dart +++ b/lib/controllers/attachment.dart @@ -6,10 +6,8 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/message.dart"; import "package:path/path.dart"; -class AttachmentController extends Notifier<(String, MessageContent?)?> { - final String roomId; - AttachmentController(this.roomId); - +class AttachmentController(final String roomId) + extends Notifier<(String, MessageContent?)?> { @override Null build() => null; diff --git a/lib/controllers/auth_url.dart b/lib/controllers/auth_url.dart index 156eb81..963df86 100644 --- a/lib/controllers/auth_url.dart +++ b/lib/controllers/auth_url.dart @@ -4,10 +4,8 @@ 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 extends AsyncNotifier { - final Uri homeserver; - AuthUrlController(this.homeserver); - +class AuthUrlController(final Uri homeserver) + extends AsyncNotifier { @override Future build() async => ref .watch(ClientController.provider.notifier) diff --git a/lib/controllers/author.dart b/lib/controllers/author.dart index bbbe068..f9c9167 100644 --- a/lib/controllers/author.dart +++ b/lib/controllers/author.dart @@ -1,19 +1,17 @@ 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 extends AsyncNotifier { - final Event event; - AuthorController(this.event); - +class AuthorController(final Event event) + extends AsyncNotifier { @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 aa8c3ae..4d0c972 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -1,26 +1,28 @@ +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/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:flutter/cupertino.dart"; +import "package:intl/intl.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; -import "package:nexus/main.dart"; +import "package:nexus/models/capabilities.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"; @@ -29,7 +31,9 @@ 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"; @@ -38,8 +42,9 @@ 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"; @@ -47,135 +52,100 @@ import "package:path_provider/path_provider.dart"; class ClientController extends AsyncNotifier { @override Future build() async { - final Pointer root; - if (Platform.isAndroid) { - final dir = await getApplicationSupportDirectory(); - root = "${dir.path}/gomuks".toNativeUtf8().cast(); - } else { - root = nullptr.cast(); + debugPrint("Setting Gomuks env..."); + if (Platform.isAndroid || Platform.isIOS) { + final env = { + "GOMUKS_ROOT": (await getApplicationSupportDirectory()).path, + "GOMUKS_CACHE_HOME": (await getApplicationCacheDirectory()).path, + }; + for (final MapEntry(:key, :value) in env.entries) { + final keyPtr = key.toNativeUtf8().cast(); + final valuePtr = value.toNativeUtf8().cast(); + try { + GomuksSetEnv(keyPtr, valuePtr); + } finally { + calloc + ..free(keyPtr) + ..free(valuePtr); + } + } } - final handle = GomuksInit(root); + debugPrint("Initializing Gomuks..."); + final handle = await Isolate.run(() { + final bufferPointer = GomuksConfig( + matrix: .new( + initialDeviceDisplayName: + "Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}", + ), + ).toJson().toGomuksBufferPtr(); - 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); - } - } - }); + try { + return GomuksInit(bufferPointer.ref); + } finally { + calloc + ..free(bufferPointer.ref.base) + ..free(bufferPointer); + } + }); ref.onDispose(() => GomuksDestroy(handle)); - ref.onDispose(callable.close); - final errorCode = GomuksStart(handle, callable.nativeFunction); - - if (errorCode == 0) return handle; - throw Exception("GomuksStart returned error code $errorCode"); + return handle; } - Future _sendCommand( - String command, [ - Map data = const {}, - ]) async { + Future callGomuksMethod( + Map data, + FutureOr Function(int handle, GomuksBorrowedBuffer data) + callback, + ) async { final bufferPointer = data.toGomuksBufferPtr(); - final handle = await future; - final response = await Isolate.run( - () => GomuksSubmitCommand( - handle, - command.toNativeUtf8().cast(), - bufferPointer.ref, - ), + + 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), ); - calloc.free(bufferPointer); - - final json = response.buf.toJson(); - if (response.command.cast().toDartString() == "error") { - throw json; - } - - return json; + return ( + Event.fromJson(response["event"]), + RoomMetadata.fromJson(response["room"]), + ); } + dynamic _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); + } + }); + Future redactEvent(RedactEventRequest report) => _sendCommand("redact_event", report.toJson()); @@ -213,21 +183,17 @@ class ClientController extends AsyncNotifier { } } - Future joinRoom(JoinRoomRequest request) async { - final response = await _sendCommand("join_room", request.toJson()); - return response["room_id"]; - } + 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 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?; @@ -248,6 +214,12 @@ 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); @@ -259,8 +231,19 @@ class ClientController extends AsyncNotifier { Future paginate(PaginateRequest request) async => .fromJson(await _sendCommand("paginate", request.toJson())); - Future getProfile(String userId) async => - .fromJson(await _sendCommand("get_profile", {"user_id": userId})); + 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 reportEvent(ReportRequest request) => _sendCommand("report_event", request.toJson()); @@ -271,6 +254,15 @@ 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())); @@ -280,9 +272,11 @@ 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 || room.metadata == null) return; + + if (event == null) return; await _sendCommand("mark_read", { "room_id": room.metadata!.id, @@ -308,10 +302,14 @@ 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.host}", + "user_id": "@fake-user:${homeserver.authority}", }); 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 ac8f5cb..7748f97 100644 --- a/lib/controllers/client_id.dart +++ b/lib/controllers/client_id.dart @@ -1,10 +1,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; -class ClientIdController extends AsyncNotifier { - final Uri homeserver; - ClientIdController(this.homeserver); - +class ClientIdController(final Uri homeserver) extends AsyncNotifier { @override Future build() => ref .watch(ClientController.provider.notifier) diff --git a/lib/controllers/emoji.dart b/lib/controllers/emoji.dart deleted file mode 100644 index caea3de..0000000 --- a/lib/controllers/emoji.dart +++ /dev/null @@ -1,84 +0,0 @@ -import "dart:convert"; -import "package:emoji_text_field/models/emoji_category.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:http/http.dart"; -import "package:nexus/models/emoji.dart"; - -typedef EmojiTuple = (IMap, IMap>); - -class EmojiController extends AsyncNotifier { - @override - Future build() async { - final response = await get( - .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 b7b417a..7a1e5dc 100644 --- a/lib/controllers/event.dart +++ b/lib/controllers/event.dart @@ -5,10 +5,8 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_event.dart"; -class EventController extends AsyncNotifier { - final GetEventRequest request; - EventController(this.request); - +class EventController(final GetEventRequest request) + extends AsyncNotifier { @override Future build() async { final room = ref.watch( diff --git a/lib/controllers/gomuks_listener.dart b/lib/controllers/gomuks_listener.dart new file mode 100644 index 0000000..dd75cf3 --- /dev/null +++ b/lib/controllers/gomuks_listener.dart @@ -0,0 +1,124 @@ +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 new file mode 100644 index 0000000..da42cf9 --- /dev/null +++ b/lib/controllers/jump_to_event.dart @@ -0,0 +1,16 @@ +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 eff3ab2..77b4883 100644 --- a/lib/controllers/key.dart +++ b/lib/controllers/key.dart @@ -1,30 +1,28 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/shared_prefs.dart"; -class KeyController extends Notifier { - final String key; - KeyController(this.key); - +class KeyController(final String key) extends AsyncNotifier { static const String spaceKey = "space"; static const String roomKey = "room"; + static const String pushKeyKey = "pushKey"; @override - String? build() => - ref.watch(SharedPrefsController.provider).requireValue.getString(key); + Future build() => + ref.watch(SharedPrefsController.provider).getString(key); Future set(String? value) async { - final prefs = ref.watch(SharedPrefsController.provider).requireValue; - state = value; + final prefs = ref.watch(SharedPrefsController.provider); + state = .data(value); if (value == null) { - prefs.remove(key); + await prefs.remove(key); } else { - prefs.setString(key, value); + await prefs.setString(key, value); } } static final provider = - NotifierProvider.family( + AsyncNotifierProvider.family( KeyController.new, ); } diff --git a/lib/controllers/member_list_opened.dart b/lib/controllers/member_list_opened.dart index e3509f0..f0295d8 100644 --- a/lib/controllers/member_list_opened.dart +++ b/lib/controllers/member_list_opened.dart @@ -1,22 +1,20 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/shared_prefs.dart"; -class MemberListOpenedController extends Notifier { +class MemberListOpenedController extends AsyncNotifier { static const String key = "memberListOpened"; @override - bool build() => - ref.watch(SharedPrefsController.provider).requireValue.getBool(key) ?? - true; + Future build() async => + await ref.watch(SharedPrefsController.provider).getBool(key) ?? true; Future set(bool value) async { - final prefs = ref.watch(SharedPrefsController.provider).requireValue; - state = value; - - prefs.setBool(key, value); + state = .data(value); + await ref.watch(SharedPrefsController.provider).setBool(key, value); } - static final provider = NotifierProvider( - MemberListOpenedController.new, - ); + static final provider = + AsyncNotifierProvider( + MemberListOpenedController.new, + ); } diff --git a/lib/controllers/members.dart b/lib/controllers/members.dart index 8566b40..3c287ea 100644 --- a/lib/controllers/members.dart +++ b/lib/controllers/members.dart @@ -6,10 +6,8 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_room_state.dart"; -class MembersController extends AsyncNotifier> { - final String roomId; - MembersController(this.roomId); - +class MembersController(final String roomId) + extends AsyncNotifier> { @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 613f7b1..82905aa 100644 --- a/lib/controllers/members_by_status.dart +++ b/lib/controllers/members_by_status.dart @@ -5,10 +5,8 @@ import "package:nexus/models/configs/members_by_status.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; -class MembersByStatusController extends AsyncNotifier> { - final MembersByStatusConfig config; - MembersByStatusController(this.config); - +class MembersByStatusController(final MembersByStatusConfig config) + extends AsyncNotifier> { @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 6f41e9d..f903ec4 100644 --- a/lib/controllers/members_grouped.dart +++ b/lib/controllers/members_grouped.dart @@ -8,11 +8,8 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/event.dart"; -class MembersGroupedController +class MembersGroupedController(final MembersByStatusConfig config) 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 52dd8d9..ab37d07 100644 --- a/lib/controllers/multi_provider.dart +++ b/lib/controllers/multi_provider.dart @@ -1,14 +1,15 @@ import "dart:async"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -class MultiProviderController extends AsyncNotifier { - MultiProviderController(this.providers); - final IList providers; - +class MultiProviderController(final IList providers) + extends AsyncNotifier { @override - Future build() => - .wait(providers.map((provider) => ref.watch(provider.future))); + Future build() => .wait( + providers.map((provider) => ref.watch(provider.future)), + eagerError: true, + ); static final provider = AsyncNotifierProvider.family< diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart new file mode 100644 index 0000000..5ee3449 --- /dev/null +++ b/lib/controllers/notification.dart @@ -0,0 +1,162 @@ +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 new file mode 100644 index 0000000..da4312d --- /dev/null +++ b/lib/controllers/notifications.dart @@ -0,0 +1,53 @@ +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 914a301..323a275 100644 --- a/lib/controllers/pinned_events.dart +++ b/lib/controllers/pinned_events.dart @@ -4,10 +4,8 @@ import "package:nexus/controllers/event.dart"; import "package:nexus/controllers/pinned_ids.dart"; import "package:nexus/models/event.dart"; -class PinnedEventsController extends AsyncNotifier> { - final String roomId; - PinnedEventsController(this.roomId); - +class PinnedEventsController(final String roomId) + extends AsyncNotifier> { @override Future> build() async { final pinIds = ref.watch(PinnedIdsController.provider(roomId)); @@ -15,9 +13,8 @@ class PinnedEventsController extends AsyncNotifier> { 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 3d1e5da..01358a9 100644 --- a/lib/controllers/pinned_ids.dart +++ b/lib/controllers/pinned_ids.dart @@ -5,10 +5,7 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/pinned_events.dart"; -class PinnedIdsController extends Notifier> { - final String roomId; - PinnedIdsController(this.roomId); - +class PinnedIdsController(final String roomId) extends Notifier> { @override IList build() { final room = ref.watch( diff --git a/lib/controllers/portal.dart b/lib/controllers/portal.dart new file mode 100644 index 0000000..daae24a --- /dev/null +++ b/lib/controllers/portal.dart @@ -0,0 +1,22 @@ +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 2f0c72e..7111e38 100644 --- a/lib/controllers/power_level.dart +++ b/lib/controllers/power_level.dart @@ -6,10 +6,8 @@ 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 extends Notifier { - final PowerLevelConfig config; - PowerLevelController(this.config); - +class PowerLevelController(final PowerLevelConfig config) + extends Notifier { @override bool build() { if (config case EventPowerLevelConfig(:final eventType)) { diff --git a/lib/controllers/profile.dart b/lib/controllers/profile.dart index 58fa49d..7a1477c 100644 --- a/lib/controllers/profile.dart +++ b/lib/controllers/profile.dart @@ -2,13 +2,11 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/profile_response.dart"; -class ProfileController extends AsyncNotifier { - final String userId; - ProfileController(this.userId); - +class ProfileController(final String userId) + extends AsyncNotifier { @override Future build() { - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); return client.getProfile(userId); } diff --git a/lib/controllers/push_key.dart b/lib/controllers/push_key.dart new file mode 100644 index 0000000..e3f6b82 --- /dev/null +++ b/lib/controllers/push_key.dart @@ -0,0 +1,31 @@ +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 615a59d..9a92843 100644 --- a/lib/controllers/reactions.dart +++ b/lib/controllers/reactions.dart @@ -5,10 +5,8 @@ import "package:nexus/controllers/rooms.dart"; import "package:nexus/models/configs/reactions.dart"; import "package:nexus/models/content/reaction.dart"; -class ReactionsController extends AsyncNotifier>> { - final ReactionsConfig config; - ReactionsController(this.config); - +class ReactionsController(final ReactionsConfig config) + extends AsyncNotifier>> { @override Future>> build() async { final eventInfo = ref.watch( diff --git a/lib/controllers/recent_emoji.dart b/lib/controllers/recent_emoji.dart new file mode 100644 index 0000000..6cf57f7 --- /dev/null +++ b/lib/controllers/recent_emoji.dart @@ -0,0 +1,44 @@ +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 3e531bb..91b137a 100644 --- a/lib/controllers/room_chat.dart +++ b/lib/controllers/room_chat.dart @@ -1,5 +1,6 @@ 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"; @@ -9,20 +10,21 @@ 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 extends AsyncNotifier?> { - final String roomId; - RoomChatController(this.roomId); - +class RoomChatController(final (String roomId, String? contextualEvent) info) + extends AsyncNotifier { @override - Future?> build() async { - final client = ref.watch(ClientController.provider.notifier); - final room = ref.watch( + Future build() async { + final (roomId, eventId) = info; + final client = ref.read(ClientController.provider.notifier); + final room = ref.read( RoomsController.provider.select((rooms) => rooms[roomId]), ); @@ -30,21 +32,14 @@ class RoomChatController extends AsyncNotifier?> { if (!room.hasFetchedState) { final state = await client.getRoomState(.new(roomId: roomId)); - await ref.read(RoomsController.provider.notifier).addState(roomId, state); } - // While there are under 20 events, try to load more - // until there's no more or the conditions are met. - if (room.hasMore && room.timeline.length < 20) { - loadOlder(); - } - - return room.timeline + final timeline = room.timeline .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) .map((element) => element.value) .toIList() - .addAll(room.sticky) + .addAll(room.clientSticky) .map((entry) { final foundEvent = entry == null ? null : room.events[entry]; @@ -62,6 +57,26 @@ class RoomChatController extends AsyncNotifier?> { }) .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 @@ -69,49 +84,101 @@ class RoomChatController extends AsyncNotifier?> { .redactEvent( RedactEventRequest( eventId: event.eventId, - roomId: roomId, + roomId: info.$1, reason: reason, ), ); - Future loadOlder() async { - final timelineKeys = ref - .read(RoomsController.provider.select((value) => value[roomId])) - ?.timeline - .keys; - final response = await ref - .watch(ClientController.provider.notifier) - .paginate( - .new( - roomId: roomId, - maxTimelineId: timelineKeys?.isNotEmpty == true - ? timelineKeys?.reduce(min) - : null, + Future paginate(Direction direction) async { + if (state.isLoading) return; + + final chat = await future; + + if (direction == .forward + ? chat?.hasMoreForward == false + : chat?.hasMoreBackward == false) { + return; + } + + state = .loading(); + + final client = ref.read(ClientController.provider.notifier); + + if (chat?.historicalData == null) { + final timelineKeys = ref + .read(RoomsController.provider.select((value) => value[info.$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, ), - ); - - 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( @@ -125,8 +192,8 @@ class RoomChatController extends AsyncNotifier?> { if (relationType == .edit) { baseContent = relation?.content; } else { - final provider = AttachmentController.provider(roomId); - baseContent = ref.watch(provider)?.$2; + final provider = AttachmentController.provider(info.$1); + baseContent = ref.read(provider)?.$2; ref.invalidate(provider); } @@ -142,10 +209,10 @@ class RoomChatController extends AsyncNotifier?> { ); } - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); final event = await client.sendMessage( SendMessageRequest( - roomId: roomId, + roomId: info.$1, baseContent: baseContent, mentions: Mentions( userIds: [ @@ -164,12 +231,12 @@ class RoomChatController extends AsyncNotifier?> { ); ref - .watch(RoomsController.provider.notifier) + .read(RoomsController.provider.notifier) .update( .new({ - roomId: .new( + info.$1: .new( events: .new({event.rowId: event}), - sticky: .new({event.rowId}), + clientSticky: .new({event.rowId}), ), }), .new(), @@ -181,10 +248,10 @@ class RoomChatController extends AsyncNotifier?> { Event event, String userId, ) async { - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); final allReactionEvents = await client.getRelatedEvents( .new( - roomId: roomId, + roomId: info.$1, eventId: event.eventId, relationType: "m.annotation", ), @@ -205,16 +272,16 @@ class RoomChatController extends AsyncNotifier?> { if (reactionEvent != null) { await ref .watch(ClientController.provider.notifier) - .redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId)); + .redactEvent(.new(eventId: reactionEvent.eventId, roomId: info.$1)); } } Future sendReaction(String reaction, Event event) async { - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); await client.sendEvent( .new( - roomId: roomId, + roomId: info.$1, type: EventType.reaction.type, content: ReactionContent(key: reaction), synchronous: true, @@ -226,7 +293,7 @@ class RoomChatController extends AsyncNotifier?> { } static final provider = AsyncNotifierProvider.family - .autoDispose?, String>( + .autoDispose( RoomChatController.new, ); } diff --git a/lib/controllers/room_creators.dart b/lib/controllers/room_creators.dart index 7db72c2..09acdd7 100644 --- a/lib/controllers/room_creators.dart +++ b/lib/controllers/room_creators.dart @@ -4,10 +4,7 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/create.dart"; import "package:nexus/models/room.dart"; -class RoomCreatorsController extends Notifier> { - final Room room; - RoomCreatorsController(this.room); - +class RoomCreatorsController(final Room room) extends Notifier> { @override IList build() { final createRowId = room.state[EventType.create.type]?[""]; diff --git a/lib/controllers/room_summary.dart b/lib/controllers/room_summary.dart new file mode 100644 index 0000000..e5b3174 --- /dev/null +++ b/lib/controllers/room_summary.dart @@ -0,0 +1,16 @@ +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 d0c6eb9..ff4d6a2 100644 --- a/lib/controllers/rooms.dart +++ b/lib/controllers/rooms.dart @@ -1,4 +1,5 @@ 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"; @@ -46,10 +47,10 @@ class RoomsController extends Notifier> { roomId, existing?.copyWith( hasMore: incoming.hasMore, - sticky: - (incoming.sticky.isEmpty == true - ? existing.sticky - : existing.sticky.addAll(incoming.sticky)) + clientSticky: + (incoming.clientSticky.isEmpty == true + ? existing.clientSticky + : existing.clientSticky.addAll(incoming.clientSticky)) .removeWhere( (rowId) => incoming.timeline.values.contains(rowId), ), diff --git a/lib/controllers/settings.dart b/lib/controllers/settings.dart index 572d2e9..8c107e3 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 Settings.fromJson(json.decode(await file.readAsString())); + return .fromJson(json.decode(await file.readAsString())); } catch (_) { - return Settings(); + return .new(); } } Future set(Settings settings) async { - state = AsyncData(settings); + state = .data(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 37e6284..7fc1b3f 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -1,13 +1,19 @@ import "dart:io"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:material_ui/material_ui.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"; @@ -18,9 +24,6 @@ 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([ @@ -49,8 +52,7 @@ 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), @@ -76,8 +78,7 @@ 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), @@ -98,46 +99,141 @@ 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, _) => - 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), + builder: (context, ref, _) { + final specVersionsResponse = ref.watch( + SpecVersionsController.provider, + ); + return DialogListTile( + icon: Icon(icon), + title: title, + subtitle: Text(description), + initialValue: ref + .watch(AccountDataController.provider) + .invitePermissionConfig + .defaultAction, + options: + specVersionsResponse.maybeWhen( + data: (data) => data.unstableFeatures.msc4494, + orElse: () => true, + ) + ? DefaultInviteAction.values + : IList(DefaultInviteAction.values) + .remove(.denyPublic) + .toList(), + getName: (option) => switch (option) { + .allow => "Allow", + .deny => "Deny", + .denyPublic => "Deny public", + }, + onChanged: specVersionsResponse.maybeWhen( + data: (_) => + (value) => ref + .watch(ClientController.provider.notifier) + .setAccountData( + .new( + type: AccountData.invitePermissionConfigKey, + content: InvitePermissionConfig( + defaultAction: value, + ), + ), + ) + .onError(showError), + orElse: () => null, ), + ); + }, ), icon: Icons.person_off, ), @@ -156,15 +252,14 @@ class SettingsSectionsController final colorScheme = Theme.of(context).colorScheme; return M3EButton.icon( onPressed: () async { - Navigator.of( - context, - ).popUntil((route) => route.isFirst); - - await WidgetsBinding.instance.endOfFrame; - + await ref + .watch(UnifiedPushController.provider.notifier) + .deregister() + .onError(showError); await ref .watch(ClientController.provider.notifier) - .logout(); + .logout() + .onError(showError); }, label: Text(title), icon: Icon(icon), diff --git a/lib/controllers/shared_prefs.dart b/lib/controllers/shared_prefs.dart index 876fc47..9ecb34a 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 AsyncNotifier { +class SharedPrefsController extends Notifier { @override - Future build() async => .getInstance(); + SharedPreferencesAsync build() => SharedPreferencesAsync(); static final provider = - AsyncNotifierProvider( + NotifierProvider( SharedPrefsController.new, ); } diff --git a/lib/controllers/spaces.dart b/lib/controllers/spaces.dart index 60696da..7dc14b0 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:flutter/material.dart"; +import "package:material_ui/material_ui.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 new file mode 100644 index 0000000..d2316bd --- /dev/null +++ b/lib/controllers/spec_versions.dart @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..db99868 --- /dev/null +++ b/lib/controllers/unified_push.dart @@ -0,0 +1,178 @@ +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 new file mode 100644 index 0000000..8f7fd8e --- /dev/null +++ b/lib/controllers/unified_push_allowed.dart @@ -0,0 +1,33 @@ +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 1b17870..41c3ef4 100644 --- a/lib/controllers/url_preview.dart +++ b/lib/controllers/url_preview.dart @@ -3,10 +3,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/open_graph_data.dart"; -class UrlPreviewController extends AsyncNotifier { - final Uri url; - UrlPreviewController(this.url); - +class UrlPreviewController(final Uri url) + extends AsyncNotifier { @override Future build() async { if (url.host == "matrix.to") return null; diff --git a/lib/controllers/via.dart b/lib/controllers/via.dart index d9227ba..39db682 100644 --- a/lib/controllers/via.dart +++ b/lib/controllers/via.dart @@ -7,10 +7,7 @@ import "package:nexus/models/content/membership.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/room.dart"; -class ViaController extends Notifier { - final Room room; - ViaController(this.room); - +class ViaController(final Room room) extends Notifier { @override String build() { final servers = {}; diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart new file mode 100644 index 0000000..a95da6d --- /dev/null +++ b/lib/helpers/extensions/build_event_options.dart @@ -0,0 +1,266 @@ +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/get_xcode_sdk.dart b/lib/helpers/extensions/get_xcode_sdk.dart index 4c09246..58920c8 100644 --- a/lib/helpers/extensions/get_xcode_sdk.dart +++ b/lib/helpers/extensions/get_xcode_sdk.dart @@ -1,11 +1,14 @@ import "dart:io"; -Future getXCodeSDK() async { - final result = await Process.run("xcrun", ["--show-sdk-path"]); +Future getXCodeTool({String? sdkType, String? findTool}) async { + final result = await Process.run("xcrun", [ + if (sdkType != null) ...["--sdk", sdkType], + if (findTool != null) ...["-f", findTool] else "--show-sdk-path", + ]); if (result.exitCode != 0) { - throw Exception("Failed to get XCode SDK\n${result.stderr}"); + throw Exception("Failed to get ${sdkType ?? "XCode"} ${findTool ?? "SDK"}"); } - return result.stdout.trim(); + return result.stdout.toString().trim(); } diff --git a/lib/helpers/extensions/link_to_mention.dart b/lib/helpers/extensions/link_to_mention.dart index f4868d3..8595669 100644 --- a/lib/helpers/extensions/link_to_mention.dart +++ b/lib/helpers/extensions/link_to_mention.dart @@ -9,6 +9,7 @@ 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 bf48535..6beca04 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 2f24a3e..6ccba4c 100644 --- a/lib/helpers/extensions/show_about_dialog.dart +++ b/lib/helpers/extensions/show_about_dialog.dart @@ -1,11 +1,11 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 ShowContextMenu on BuildContext { +extension ShowAboutDialog on BuildContext { Future showAboutDialog(WidgetRef ref) async { final packageInfo = await PackageInfo.fromPlatform(); @@ -20,7 +20,7 @@ extension ShowContextMenu on BuildContext { Row( spacing: 12, children: [ - SvgPicture.asset("assets/icon.svg", width: 64), + SvgPicture.asset("assets/bundled/icon.svg", width: 64), Expanded( child: Column( crossAxisAlignment: .start, @@ -50,8 +50,8 @@ extension ShowContextMenu on BuildContext { M3ECardColumn( onTap: (index) => ref.watch(LaunchHelper.provider).launchUrl(switch (index) { - 0 => Uri.https("git.federated.nexus", "nexus/nexus"), - _ => Uri.https("liberapay.com", "QuadRadical"), + 0 => .https("git.federated.nexus", "nexus/nexus"), + _ => .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 c860115..ccef3b5 100644 --- a/lib/helpers/extensions/show_context_menu.dart +++ b/lib/helpers/extensions/show_context_menu.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; extension ShowContextMenu on BuildContext { void showContextMenu({ @@ -9,7 +9,7 @@ extension ShowContextMenu on BuildContext { showMenu( context: this, - constraints: .loose(Size.infinite), + constraints: .loose(.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 1ea3015..f42cd57 100644 --- a/lib/helpers/extensions/show_user_popover.dart +++ b/lib/helpers/extensions/show_user_popover.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 eaa7714..a21ca82 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:flutter/material.dart"; +import "package:material_ui/material_ui.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 new file mode 100644 index 0000000..3b1531e --- /dev/null +++ b/lib/helpers/hooks/chat_scroll.dart @@ -0,0 +1,203 @@ +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 575395f..89c7015 100644 --- a/lib/helpers/launch_helper.dart +++ b/lib/helpers/launch_helper.dart @@ -2,10 +2,7 @@ import "package:flutter/services.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:url_launcher/url_launcher.dart" as ul; -class LaunchHelper { - final Ref ref; - LaunchHelper(this.ref); - +class LaunchHelper(Ref 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 3adb10e..83a345a 100644 --- a/lib/helpers/mxc_image.dart +++ b/lib/helpers/mxc_image.dart @@ -1,14 +1,12 @@ 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 extends ImageProvider { - final WidgetRef ref; - final DownloadMediaRequest request; - const MxcImage(this.ref, this.request); - +class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) + extends ImageProvider { @override Future obtainKey(ImageConfiguration configuration) => Future.value(this); diff --git a/lib/main.dart b/lib/main.dart index 53cc57b..5586cbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,26 +1,31 @@ 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/shared_prefs.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/controllers/unified_push.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/font_licenses.dart"; -import "package:nexus/pages/chat.dart"; -import "package:nexus/pages/select_server.dart"; -import "package:nexus/pages/verify.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/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:window_manager/window_manager.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; final GlobalKey navigatorKey = GlobalKey(); +late final bool isInBackground; final class Logger extends ProviderObserver { @override @@ -37,14 +42,7 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue} } void showError(Object error, [StackTrace? stackTrace]) { - 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")) { + if (error.toString().contains("'_nextFrame != null': is not true.")) { return; } @@ -61,49 +59,59 @@ void showError(Object error, [StackTrace? stackTrace]) { } } -void main() async { +void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); - 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)); + LicenseRegistry.addLicense(() => .fromIterable(fontLicenses)); FlutterError.onError = (FlutterErrorDetails details) => showError(details.exception.toString(), details.stack); - 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(), - ), - ); + 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(), + ), + ); + } } -class App extends StatelessWidget { - const App({super.key}); - +class const App({super.key}) extends StatelessWidget { @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) @@ -112,7 +120,7 @@ class App extends StatelessWidget { data: (settings) => settings.useDynamicTheming ? lightDynamic : null, ) ?? - ColorScheme.fromSeed(seedColor: Colors.indigo)) + ThemeData.light().colorScheme) .theme, darkTheme: (ref @@ -122,10 +130,7 @@ class App extends StatelessWidget { data: (settings) => settings.useDynamicTheming ? darkDynamic : null, ) ?? - ColorScheme.fromSeed( - seedColor: Colors.indigo, - brightness: Brightness.dark, - )) + ThemeData.dark().colorScheme) .theme, themeMode: ref .watch(SettingsController.provider) @@ -137,36 +142,55 @@ class App extends StatelessWidget { ), child: Scaffold( body: Consumer( - builder: (_, ref, _) => ref - .watch( - MultiProviderController.provider( - IListConst([ - SharedPrefsController.provider, - ClientController.provider, - ]), - ), - ) - .betterWhen( - data: (_) => Consumer( - builder: (_, ref, _) { - final clientState = ref.watch( - ClientStateController.provider, - ); + 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); - 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(); - } - }, - ), + 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), + ), + ]), ), + 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 7df7459..955d8d1 100644 --- a/lib/models/account_data.dart +++ b/lib/models/account_data.dart @@ -1,62 +1,60 @@ 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 -abstract class AccountData with _$AccountData { - const AccountData._(); - static List? readRecentEmojiValue( - Map json, - String key, - ) => json[key]?["recent_emoji"]; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const AccountData({ + @JsonKey(name: AccountData.invitePermissionConfigKey) + final InvitePermissionConfig invitePermissionConfig = + const InvitePermissionConfig(), - static Map>? recentEmojiToJson( - IList recentEmoji, - ) => {"recent_emoji": recentEmoji.map((emoji) => emoji.toJson()).toList()}; + @JsonKey(name: AccountData.directKey) + final IMap> directMessages = const IMap.empty(), + @JsonKey(name: AccountData.recentEmojiKey) + final RecentEmojiData recentEmoji = const RecentEmojiData(), +}) with _$AccountData { static const invitePermissionConfigKey = "m.invite_permission_config"; static const directKey = "m.direct"; static const recentEmojiKey = "m.recent_emoji"; - 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; + Map toJson() => _$AccountDataToJson(this); factory AccountData.fromJson(Map json) => _$AccountDataFromJson(json); } -@freezed -abstract class InvitePermissionConfig with _$InvitePermissionConfig { - const factory InvitePermissionConfig({ - @JsonKey(unknownEnumValue: DefaultInviteAction.allow) - @Default(DefaultInviteAction.allow) - DefaultInviteAction defaultAction, - }) = _InvitePermissionConfig; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const RecentEmojiData({ + final IList recentEmoji = const IList.empty(), +}) with _$RecentEmojiData { + Map toJson() => _$RecentEmojiDataToJson(this); + + factory RecentEmojiData.fromJson(Map json) => + _$RecentEmojiDataFromJson(json); +} + +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const InvitePermissionConfig({ + @JsonKey(unknownEnumValue: DefaultInviteAction.allow) + final DefaultInviteAction defaultAction = DefaultInviteAction.allow, +}) with _$InvitePermissionConfig { + Map toJson() => _$InvitePermissionConfigToJson(this); factory InvitePermissionConfig.fromJson(Map json) => _$InvitePermissionConfigFromJson(json); } -@freezed -abstract class RecentEmoji with _$RecentEmoji { - const factory RecentEmoji({required String emoji, required int total}) = - _RecentEmoji; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const RecentEmoji({required final String emoji, required final int total}) + with _$RecentEmoji { + Map toJson() => _$RecentEmojiToJson(this); factory RecentEmoji.fromJson(Map json) => _$RecentEmojiFromJson(json); diff --git a/lib/models/capabilities.dart b/lib/models/capabilities.dart new file mode 100644 index 0000000..6830170 --- /dev/null +++ b/lib/models/capabilities.dart @@ -0,0 +1,25 @@ +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 1e15136..262203d 100644 --- a/lib/models/client_state.dart +++ b/lib/models/client_state.dart @@ -1,16 +1,19 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "client_state.freezed.dart"; part "client_state.g.dart"; -@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; +@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); 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 29fc471..6b4aec4 100644 --- a/lib/models/configs/members_by_status.dart +++ b/lib/models/configs/members_by_status.dart @@ -1,14 +1,16 @@ 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 -abstract class MembersByStatusConfig with _$MembersByStatusConfig { - const factory MembersByStatusConfig({ - required String roomId, - required MembershipStatus status, - }) = _MembersByStatusConfig; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const MembersByStatusConfig({ + required final String roomId, + required final MembershipStatus status, +}) with _$MembersByStatusConfig { + Map toJson() => _$MembersByStatusConfigToJson(this); factory MembersByStatusConfig.fromJson(Map json) => _$MembersByStatusConfigFromJson(json); diff --git a/lib/models/configs/power_level.dart b/lib/models/configs/power_level.dart index ed9c5e8..5722db7 100644 --- a/lib/models/configs/power_level.dart +++ b/lib/models/configs/power_level.dart @@ -1,6 +1,7 @@ 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 787b28c..b9de793 100644 --- a/lib/models/configs/reactions.dart +++ b/lib/models/configs/reactions.dart @@ -1,13 +1,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "reactions.freezed.dart"; part "reactions.g.dart"; -@freezed -abstract class ReactionsConfig with _$ReactionsConfig { - const factory ReactionsConfig({ - required String roomId, - required int eventRowId, - }) = _ReactionsConfig; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const ReactionsConfig({ + required final String roomId, + required final int eventRowId, +}) with _$ReactionsConfig { + Map toJson() => _$ReactionsConfigToJson(this); factory ReactionsConfig.fromJson(Map json) => _$ReactionsConfigFromJson(json); diff --git a/lib/models/configs/user.dart b/lib/models/configs/user.dart index 0331597..bbbc83d 100644 --- a/lib/models/configs/user.dart +++ b/lib/models/configs/user.dart @@ -1,11 +1,13 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "user.freezed.dart"; part "user.g.dart"; -@freezed -abstract class UserConfig with _$UserConfig { - const factory UserConfig({required String? roomId, required String userId}) = - _UserConfig; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const UserConfig({final String? roomId, required final String userId}) + with _$UserConfig { + Map toJson() => _$UserConfigToJson(this); factory UserConfig.fromJson(Map json) => _$UserConfigFromJson(json); diff --git a/lib/models/content/avatar.dart b/lib/models/content/avatar.dart index 66d4c47..25167e9 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 -abstract class AvatarContent extends Content with _$AvatarContent { +sealed 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 636be13..1a35d92 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 -abstract class CanonicalAliasContent extends Content +sealed class CanonicalAliasContent extends Content with _$CanonicalAliasContent { CanonicalAliasContent._(); factory CanonicalAliasContent({ diff --git a/lib/models/content/content.dart b/lib/models/content/content.dart index e7b1141..e28ccbf 100644 --- a/lib/models/content/content.dart +++ b/lib/models/content/content.dart @@ -19,9 +19,11 @@ import "package:nexus/models/content/history_visibility.dart"; class Content { final Error? parseError; - Content({this.parseError}); - factory Content.fromJson(Map json) => Content(); + const Content({this.parseError}); + + factory Content.fromJson(Map json) => const Content(); + Map toJson() => {}; static Map readValue(Map json, _) => @@ -34,13 +36,16 @@ class Content { ?.contentFromJson ?? Content.fromJson)(json); } catch (error) { - if (error is Error) return .new(parseError: error); + if (error is Error) return Content(parseError: error); rethrow; } } } -enum EventType { +enum EventType( + final String type, + final Content Function(Map json) contentFromJson, +) { encrypted("m.room.encrypted", EncryptedContent.fromJson), redaction("m.room.redaction", RedactionContent.fromJson), encryption("m.room.encryption", EncryptionContent.fromJson), @@ -61,8 +66,4 @@ 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 c534558..b937a94 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 -abstract class CreateContent extends Content with _$CreateContent { +sealed class CreateContent extends Content with _$CreateContent { CreateContent._(); factory CreateContent({ @JsonKey(name: "additional_creators") @@ -30,9 +30,10 @@ enum RoomType { space, } -@freezed -abstract class PreviousRoom with _$PreviousRoom { - const factory PreviousRoom({required String roomId}) = _PreviousRoom; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const PreviousRoom({required final String roomId}) with _$PreviousRoom { + Map toJson() => _$PreviousRoomToJson(this); factory PreviousRoom.fromJson(Map json) => _$PreviousRoomFromJson(json); diff --git a/lib/models/content/encrypted.dart b/lib/models/content/encrypted.dart index b33a440..8658a71 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 -abstract class EncryptedContent extends Content with _$EncryptedContent { +sealed 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 3380632..52c7b34 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 -abstract class EncryptionContent extends Content with _$EncryptionContent { +sealed 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 707805c..3cfc011 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 -abstract class HistoryVisibilityContent extends Content +sealed 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 1d14eee..3f1c17b 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 -abstract class JoinRulesContent extends Content with _$JoinRulesContent { +sealed class JoinRulesContent extends Content with _$JoinRulesContent { JoinRulesContent._(); factory JoinRulesContent({ required JoinRule joinRule, @@ -17,12 +17,13 @@ abstract class JoinRulesContent extends Content with _$JoinRulesContent { _$JoinRulesContentFromJson(json); } -@freezed -abstract class AllowCondition with _$AllowCondition { - const factory AllowCondition({ - String? roomId, - required AllowConditionType type, - }) = _AllowCondition; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const AllowCondition({ + final String? roomId, + required final AllowConditionType type, +}) with _$AllowCondition { + Map toJson() => _$AllowConditionToJson(this); factory AllowCondition.fromJson(Map json) => _$AllowConditionFromJson(json); diff --git a/lib/models/content/membership.dart b/lib/models/content/membership.dart index dbbd123..46be974 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 -abstract class MembershipContent extends Content with _$MembershipContent { +sealed 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 e7e8923..36bd725 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") -abstract class MessageContent extends Content with _$MessageContent { +sealed 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 205f6bb..c451d64 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 -abstract class NameContent extends Content with _$NameContent { +sealed 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 8aea838..9dac5de 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 -abstract class PinnedEventsContent extends Content with _$PinnedEventsContent { +sealed 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 3709c38..5ae6561 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 -abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { +sealed class PowerLevelsContent extends Content with _$PowerLevelsContent { PowerLevelsContent._(); factory PowerLevelsContent({ @Default(IMap.empty()) IMap events, @@ -24,12 +24,13 @@ abstract class PowerLevelsContent extends Content with _$PowerLevelsContent { _$PowerLevelsContentFromJson(json); } -@freezed -abstract class Notifications with _$Notifications { - const factory Notifications({ - @Default(50) int room, - @Default(IMapConst({})) IMap other, - }) = _Notifications; +@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); factory Notifications.fromJson(Map json) => _$NotificationsFromJson(json); diff --git a/lib/models/content/reaction.dart b/lib/models/content/reaction.dart index 0f81bc0..93c563d 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) -abstract class ReactionContent extends Content with _$ReactionContent { +sealed 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 e9c1a90..2ed6436 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 -abstract class RedactionContent extends Content with _$RedactionContent { +sealed 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 1e50988..3873bf3 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 -abstract class ServerACLContent extends Content with _$ServerACLContent { +sealed 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 89d9332..c6fa893 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 -abstract class StickerContent extends Content with _$StickerContent { +sealed 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 8fa5229..c8256e3 100644 --- a/lib/models/content/topic.dart +++ b/lib/models/content/topic.dart @@ -1,11 +1,12 @@ 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 -abstract class TopicContent extends Content with _$TopicContent { +sealed class TopicContent extends Content with _$TopicContent { TopicContent._(); factory TopicContent({ required String topic, @@ -16,24 +17,24 @@ abstract class TopicContent extends Content with _$TopicContent { _$TopicContentFromJson(json); } -@freezed -abstract class TopicContentBlock with _$TopicContentBlock { - factory TopicContentBlock({ - @Default(IList.empty()) - @JsonKey(name: "m.text") - IList representations, - }) = _TopicContentBlock; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const TopicContentBlock({ + final IList representations = const IList.empty(), +}) with _$TopicContentBlock { + Map toJson() => _$TopicContentBlockToJson(this); factory TopicContentBlock.fromJson(Map json) => _$TopicContentBlockFromJson(json); } -@freezed -abstract class TextualRepresentation with _$TextualRepresentation { - factory TextualRepresentation({ - required String body, - @Default("text/plain") String mimetype, - }) = _TextualRepresentation; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const TextualRepresentation({ + required final String body, + final String mimetype = "text/plain", +}) with _$TextualRepresentation { + Map toJson() => _$TextualRepresentationToJson(this); factory TextualRepresentation.fromJson(Map json) => _$TextualRepresentationFromJson(json); diff --git a/lib/models/direction.dart b/lib/models/direction.dart new file mode 100644 index 0000000..8c4947e --- /dev/null +++ b/lib/models/direction.dart @@ -0,0 +1,8 @@ +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 8e4eac6..75faf08 100644 --- a/lib/models/emoji.dart +++ b/lib/models/emoji.dart @@ -1,17 +1,25 @@ 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 -abstract class Emoji with _$Emoji { - const factory Emoji({ - required String emoji, - required String category, - required IList aliases, - required String description, - required IList tags, - }) = _Emoji; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable(createToJson: false) +class Emoji({ + @JsonKey( + fromJson: Emoji.widgetFromJson, + readValue: Emoji.readWidgetValueFromJson, + ) + required final Widget widget, + @JsonKey(name: "emoji") required final String value, + required final IList aliases, + required final String description, + required final IList tags, +}) with _$Emoji { + static Widget widgetFromJson(String emoji) => Text(emoji); + static String readWidgetValueFromJson(Map json, _) => + json["emoji"]; factory Emoji.fromJson(Map json) => _$EmojiFromJson(json); } diff --git a/lib/models/emoji_category.dart b/lib/models/emoji_category.dart new file mode 100644 index 0000000..cc39bc3 --- /dev/null +++ b/lib/models/emoji_category.dart @@ -0,0 +1,12 @@ +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 28bf0ae..dc3dd46 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -3,11 +3,39 @@ 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 -abstract class Event with _$Event { +@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); + static String typeJsonFromJson(Map json, _) => json["decrypted_type"] ?? json["type"]; @@ -25,32 +53,6 @@ abstract class Event with _$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)), @@ -72,37 +74,27 @@ abstract class Event with _$Event { ); } -@freezed -abstract class LocalContent with _$LocalContent { - const factory LocalContent({ - String? sanitizedHtml, - String? editSource, - bool? wasPlaintext, - bool? bigEmoji, - bool? hasMath, - bool? replyFallbackRemoved, - }) = _LocalContent; +@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); + @override factory LocalContent.fromJson(Map json) => _$LocalContentFromJson(json); } -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); +@Freezed(toJson: false, fromJson: false) +class const UnreadType(final int value) with _$UnreadType { + factory UnreadType.fromJson(int json) => UnreadType(json); + int toJson() => value; static const none = UnreadType(0); static const normal = UnreadType(1); @@ -110,9 +102,9 @@ class UnreadType { static const highlight = UnreadType(4); static const sound = UnreadType(8); - 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; + bool isNone() => value == 0; + bool isNormal() => (value & 1) != 0; + bool shouldNotify() => (value & 2) != 0; + bool isHighlighted() => (value & 4) != 0; + bool playsSound() => (value & 8) != 0; } diff --git a/lib/models/event_context.dart b/lib/models/event_context.dart new file mode 100644 index 0000000..6103c7d --- /dev/null +++ b/lib/models/event_context.dart @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..e6a8ce7 --- /dev/null +++ b/lib/models/gomuks_config.dart @@ -0,0 +1,55 @@ +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 903e23d..5504941 100644 --- a/lib/models/homeserver.dart +++ b/lib/models/homeserver.dart @@ -1,12 +1,11 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "homeserver.freezed.dart"; @freezed -abstract class Homeserver with _$Homeserver { - const factory Homeserver({ - required String name, - required String description, - required Uri url, - required String iconUrl, - }) = _Homeserver; -} +class const Homeserver({ + required final String name, + required final String description, + required final Uri url, + required final String iconUrl, +}) with _$Homeserver; diff --git a/lib/models/info/audio.dart b/lib/models/info/audio.dart index ccfcf7a..ac90282 100644 --- a/lib/models/info/audio.dart +++ b/lib/models/info/audio.dart @@ -1,16 +1,17 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/ms_duration.dart"; + part "audio.freezed.dart"; part "audio.g.dart"; -@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; +@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); factory AudioInfo.fromJson(Map json) => _$AudioInfoFromJson(json); diff --git a/lib/models/info/file.dart b/lib/models/info/file.dart index 1509c99..ce0aa84 100644 --- a/lib/models/info/file.dart +++ b/lib/models/info/file.dart @@ -1,14 +1,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "file.freezed.dart"; part "file.g.dart"; -@freezed -abstract class FileInfo with _$FileInfo { - /// Information for images, [size] is in bytes. - const factory FileInfo({ - @JsonKey(name: "mimetype") String? mimeType, - int? size, - }) = _FileInfo; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const FileInfo({ + @JsonKey(name: "mimetype") final String? mimeType, + final int? size, +}) with _$FileInfo { + Map toJson() => _$FileInfoToJson(this); factory FileInfo.fromJson(Map json) => _$FileInfoFromJson(json); diff --git a/lib/models/info/image.dart b/lib/models/info/image.dart index 9833016..266fb01 100644 --- a/lib/models/info/image.dart +++ b/lib/models/info/image.dart @@ -1,17 +1,18 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "image.freezed.dart"; part "image.g.dart"; -@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; +@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); factory ImageInfo.fromJson(Map json) => _$ImageInfoFromJson(json); diff --git a/lib/models/info/video.dart b/lib/models/info/video.dart index 6ff3547..31b1fb6 100644 --- a/lib/models/info/video.dart +++ b/lib/models/info/video.dart @@ -1,18 +1,19 @@ import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/ms_duration.dart"; + part "video.freezed.dart"; part "video.g.dart"; -@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; +@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); factory VideoInfo.fromJson(Map json) => _$VideoInfoFromJson(json); diff --git a/lib/models/join_rule.dart b/lib/models/join_rule.dart index 3fade23..44fb9bb 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: FieldRename.snake) +@JsonEnum(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 0cd250f..f9a814e 100644 --- a/lib/models/lazy_load_summary.dart +++ b/lib/models/lazy_load_summary.dart @@ -1,15 +1,17 @@ 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 -abstract class LazyLoadSummary with _$LazyLoadSummary { - const factory LazyLoadSummary({ - required IList? heroes, - required int? joinedMemberCount, - required int? invitedMemberCount, - }) = _LazyLoadSummary; +@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); factory LazyLoadSummary.fromJson(Map json) => _$LazyLoadSummaryFromJson(json); diff --git a/lib/models/membership_action.dart b/lib/models/membership_action.dart index d852164..f03ca99 100644 --- a/lib/models/membership_action.dart +++ b/lib/models/membership_action.dart @@ -1,4 +1 @@ -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 ba7a241..7eb5f72 100644 --- a/lib/models/membership_status.dart +++ b/lib/models/membership_status.dart @@ -1,4 +1 @@ -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 5bb3f3f..3074335 100644 --- a/lib/models/oauth_auth_code_response.dart +++ b/lib/models/oauth_auth_code_response.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "oauth_auth_code_response.freezed.dart"; part "oauth_auth_code_response.g.dart"; -@freezed -abstract class OAuthAuthCodeResponse with _$OAuthAuthCodeResponse { - const factory OAuthAuthCodeResponse({ - required String state, - required String codeVerifier, - required Uri url, - }) = _OAuthAuthCodeResponse; +@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); factory OAuthAuthCodeResponse.fromJson(Map json) => _$OAuthAuthCodeResponseFromJson(json); diff --git a/lib/models/open_graph_data.dart b/lib/models/open_graph_data.dart index d7e840d..a183ef7 100644 --- a/lib/models/open_graph_data.dart +++ b/lib/models/open_graph_data.dart @@ -1,16 +1,18 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "open_graph_data.freezed.dart"; part "open_graph_data.g.dart"; -@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; +@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); factory OpenGraphData.fromJson(Map json) => _$OpenGraphDataFromJson(json); diff --git a/lib/models/paginate.dart b/lib/models/paginate.dart index df0a0f6..dc6eede 100644 --- a/lib/models/paginate.dart +++ b/lib/models/paginate.dart @@ -1,16 +1,18 @@ 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 -abstract class Paginate with _$Paginate { - const factory Paginate({ - required IList events, - required IList relatedEvents, - required bool hasMore, - }) = _Paginate; +@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); factory Paginate.fromJson(Map json) => _$PaginateFromJson(json); diff --git a/lib/models/paginate_manual.dart b/lib/models/paginate_manual.dart new file mode 100644 index 0000000..9f45c1f --- /dev/null +++ b/lib/models/paginate_manual.dart @@ -0,0 +1,19 @@ +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 8b7b749..57f76c5 100644 --- a/lib/models/profile_response.dart +++ b/lib/models/profile_response.dart @@ -1,53 +1,55 @@ 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 -abstract class ProfileResponse with _$ProfileResponse { - const factory ProfileResponse({ - @JsonKey(fromJson: Profile.fromJson) required Profile profile, - required Bio? bio, - }) = _ProfileResponse; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const ProfileResponse({ + @JsonKey(fromJson: Profile.fromJson) required final Profile profile, + final Bio? bio, +}) with _$ProfileResponse { + Map toJson() => _$ProfileResponseToJson(this); factory ProfileResponse.fromJson(Map json) => _$ProfileResponseFromJson(json); } -@freezed -abstract class Bio with _$Bio { - const factory Bio({required String html, String? editSource}) = _Bio; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const Bio({required final String html, final String? editSource}) + with _$Bio { + Map toJson() => _$BioToJson(this); factory Bio.fromJson(Map json) => _$BioFromJson(json); } -@freezed -abstract class Profile with _$Profile { +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const Profile({ + final String? id, + final String? parseError, + final Uri? avatarUrl, + + @JsonKey(name: "displayname", fromJson: MembershipContent.displaynameFromJson) + final String? displayName, + + @JsonKey(readValue: Profile.readTimezone, name: "m.tz") + final String? timezone, + + @JsonKey(readValue: Profile.readPronouns, name: "m.pronouns") + final IList pronouns = const IList.empty(), +}) with _$Profile { + Map toJson() => _$ProfileToJson(this); + static Object? readPronouns(Map map, String key) => map[key] ?? map["io.fsky.nyx.pronouns"]; static Object? readTimezone(Map map, String key) => map[key] ?? map["us.cloke.msc4175.tz"]; - 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); @@ -60,10 +62,13 @@ abstract class Profile with _$Profile { } } -@freezed -abstract class Pronoun with _$Pronoun { - const factory Pronoun({required String language, required String summary}) = - _Pronoun; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const Pronoun({ + required final String language, + required final String summary, +}) with _$Pronoun { + Map toJson() => _$PronounToJson(this); factory Pronoun.fromJson(Map json) => _$PronounFromJson(json); diff --git a/lib/models/read_receipt.dart b/lib/models/read_receipt.dart index d533e2d..ab4edd8 100644 --- a/lib/models/read_receipt.dart +++ b/lib/models/read_receipt.dart @@ -1,17 +1,19 @@ 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 -abstract class ReadReceipt with _$ReadReceipt { - const factory ReadReceipt({ - String? roomId, - required String userId, - String? threadId, - required String eventId, - @EpochDateTimeConverter() required DateTime timestamp, - }) = _ReadReceipt; +@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); factory ReadReceipt.fromJson(Map json) => _$ReadReceiptFromJson(json); diff --git a/lib/models/requests/deregister_pusher.dart b/lib/models/requests/deregister_pusher.dart new file mode 100644 index 0000000..1416add --- /dev/null +++ b/lib/models/requests/deregister_pusher.dart @@ -0,0 +1,13 @@ +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 b5d5771..c9751f8 100644 --- a/lib/models/requests/download_media.dart +++ b/lib/models/requests/download_media.dart @@ -1,15 +1,17 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "download_media.freezed.dart"; part "download_media.g.dart"; -@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; +@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); factory DownloadMediaRequest.fromJson(Map json) => _$DownloadMediaRequestFromJson(json); diff --git a/lib/models/requests/get_event.dart b/lib/models/requests/get_event.dart index 2665a2a..b4692ee 100644 --- a/lib/models/requests/get_event.dart +++ b/lib/models/requests/get_event.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "get_event.freezed.dart"; part "get_event.g.dart"; -@freezed -abstract class GetEventRequest with _$GetEventRequest { - const factory GetEventRequest({ - required String roomId, - required String eventId, - @Default(false) bool unredact, - }) = _GetEventRequest; +@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); 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 new file mode 100644 index 0000000..68f23a0 --- /dev/null +++ b/lib/models/requests/get_event_context.dart @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..1512b6f --- /dev/null +++ b/lib/models/requests/get_mentions.dart @@ -0,0 +1,19 @@ +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 52d2716..08be426 100644 --- a/lib/models/requests/get_related_events.dart +++ b/lib/models/requests/get_related_events.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "get_related_events.freezed.dart"; part "get_related_events.g.dart"; -@freezed -abstract class GetRelatedEventsRequest with _$GetRelatedEventsRequest { - const factory GetRelatedEventsRequest({ - required String roomId, - required String eventId, - required String relationType, - }) = _GetRelatedEventsRequest; +@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); 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 d3f52f7..66504da 100644 --- a/lib/models/requests/get_room_state.dart +++ b/lib/models/requests/get_room_state.dart @@ -1,15 +1,17 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "get_room_state.freezed.dart"; part "get_room_state.g.dart"; -@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; +@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); factory GetRoomStateRequest.fromJson(Map json) => _$GetRoomStateRequestFromJson(json); diff --git a/lib/models/requests/join_room.dart b/lib/models/requests/join_room.dart index 8487298..350b762 100644 --- a/lib/models/requests/join_room.dart +++ b/lib/models/requests/join_room.dart @@ -1,14 +1,16 @@ 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 -abstract class JoinRoomRequest with _$JoinRoomRequest { - const factory JoinRoomRequest({ - required String roomIdOrAlias, - required IList via, - }) = _JoinRoomRequest; +@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); 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 9d55c1f..fb47358 100644 --- a/lib/models/requests/oauth/exchange_token.dart +++ b/lib/models/requests/oauth/exchange_token.dart @@ -1,16 +1,18 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "exchange_token.freezed.dart"; part "exchange_token.g.dart"; -@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; +@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); 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 5ca5b6f..1ff166a 100644 --- a/lib/models/requests/oauth/get_auth_url.dart +++ b/lib/models/requests/oauth/get_auth_url.dart @@ -2,25 +2,27 @@ 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 -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; +@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); factory OAuthGetAuthUrl.fromJson(Map json) => _$OAuthGetAuthUrlFromJson(json); } -abstract class Scope { +sealed 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 cf4f9e0..e5c1a70 100644 --- a/lib/models/requests/oauth/register_client.dart +++ b/lib/models/requests/oauth/register_client.dart @@ -1,26 +1,27 @@ 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 -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, +@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, - @Default(AuthMethod.none) - @JsonKey(name: "token_endpoint_auth_method") - AuthMethod? authMethod, - }) = _OAuthRegisterClientRequest; + @JsonKey(name: "token_endpoint_auth_method") + final AuthMethod? authMethod = AuthMethod.none, +}) with _$OAuthRegisterClientRequest { + Map toJson() => _$OAuthRegisterClientRequestToJson(this); factory OAuthRegisterClientRequest.fromJson(Map json) => _$OAuthRegisterClientRequestFromJson(json); diff --git a/lib/models/requests/paginate.dart b/lib/models/requests/paginate.dart index ddc62f3..b3903c6 100644 --- a/lib/models/requests/paginate.dart +++ b/lib/models/requests/paginate.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "paginate.freezed.dart"; part "paginate.g.dart"; -@freezed -abstract class PaginateRequest with _$PaginateRequest { - const factory PaginateRequest({ - required String roomId, - required int? maxTimelineId, - @Default(20) int limit, - }) = _PaginateRequest; +@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); factory PaginateRequest.fromJson(Map json) => _$PaginateRequestFromJson(json); diff --git a/lib/models/requests/paginate_manual.dart b/lib/models/requests/paginate_manual.dart new file mode 100644 index 0000000..9f8a1d3 --- /dev/null +++ b/lib/models/requests/paginate_manual.dart @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..dd34e62 --- /dev/null +++ b/lib/models/requests/register_pusher.dart @@ -0,0 +1,56 @@ +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 f87b1f1..ed05715 100644 --- a/lib/models/requests/report.dart +++ b/lib/models/requests/report.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "report.freezed.dart"; part "report.g.dart"; -@freezed -abstract class ReportRequest with _$ReportRequest { - const factory ReportRequest({ - required String roomId, - required String eventId, - String? reason, - }) = _ReportRequest; +@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); factory ReportRequest.fromJson(Map json) => _$ReportRequestFromJson(json); diff --git a/lib/models/requests/send_event.dart b/lib/models/requests/send_event.dart index 196c0b5..acad57f 100644 --- a/lib/models/requests/send_event.dart +++ b/lib/models/requests/send_event.dart @@ -1,19 +1,21 @@ 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 -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; +@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); factory SendEventRequest.fromJson(Map json) => _$SendEventRequestFromJson(json); diff --git a/lib/models/requests/send_message.dart b/lib/models/requests/send_message.dart index 951198e..9907439 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 -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; +@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); factory SendMessageRequest.fromJson(Map json) => _$SendMessageRequestFromJson(json); } -@freezed -abstract class Mentions with _$Mentions { - const factory Mentions({ - @Default(false) bool room, - @Default(IList.empty()) IList userIds, - }) = _Mentions; +@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); factory Mentions.fromJson(Map json) => _$MentionsFromJson(json); } -@Freezed(toJson: false) -abstract class Relation with _$Relation { - const Relation._(); - - const factory Relation({ - required String eventId, - required RelationType relationType, - }) = _Relation; - +@Freezed(toJson: false, fromJson: false) +@JsonSerializable(createToJson: false) +class const Relation({ + required final String eventId, + required final RelationType relationType, +}) with _$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 ffccdb4..95ef75c 100644 --- a/lib/models/requests/set_account_data.dart +++ b/lib/models/requests/set_account_data.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "set_account_data.freezed.dart"; part "set_account_data.g.dart"; -@freezed -abstract class SetAccountDataRequest with _$SetAccountDataRequest { - const factory SetAccountDataRequest({ - required String type, - required dynamic content, - String? roomId, - }) = _SetAccountDataRequest; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const SetAccountDataRequest({ + required final String type, + required final dynamic content, + final String? roomId, +}) with _$SetAccountDataRequest { + Map toJson() => _$SetAccountDataRequestToJson(this); factory SetAccountDataRequest.fromJson(Map json) => _$SetAccountDataRequestFromJson(json); diff --git a/lib/models/requests/set_membership.dart b/lib/models/requests/set_membership.dart index 4bbe8a3..9d88da2 100644 --- a/lib/models/requests/set_membership.dart +++ b/lib/models/requests/set_membership.dart @@ -1,18 +1,19 @@ 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 -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; +@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); factory SetMembershipRequest.fromJson(Map json) => _$SetMembershipRequestFromJson(json); diff --git a/lib/models/requests/set_state.dart b/lib/models/requests/set_state.dart index c92763d..d0e3e95 100644 --- a/lib/models/requests/set_state.dart +++ b/lib/models/requests/set_state.dart @@ -1,22 +1,23 @@ 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 -abstract class SetStateRequest with _$SetStateRequest { - const factory SetStateRequest({ - required String roomId, - required String type, - required String stateKey, - required Content content, +@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, - @JsonKey(name: "delay_ms", includeIfNull: false) - @MSDuration() - @Default(null) - Duration? delay, - }) = _SetStateRequest; + @JsonKey(name: "delay_ms", includeIfNull: false) + @MSDuration() + final Duration? delay, +}) with _$SetStateRequest { + Map toJson() => _$SetStateRequestToJson(this); factory SetStateRequest.fromJson(Map json) => _$SetStateRequestFromJson(json); diff --git a/lib/models/requests/upload_media.dart b/lib/models/requests/upload_media.dart index a640bea..d6e69ac 100644 --- a/lib/models/requests/upload_media.dart +++ b/lib/models/requests/upload_media.dart @@ -1,23 +1,26 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "upload_media.freezed.dart"; part "upload_media.g.dart"; -@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; +@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); factory UploadMediaRequest.fromJson(Map json) => _$UploadMediaRequestFromJson(json); diff --git a/lib/models/room.dart b/lib/models/room.dart index fb21a55..96b29b4 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -3,11 +3,44 @@ 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 -abstract class Room with _$Room { +@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 { static IMap timelineTupleJsonToIMap(List json) => IMap.fromEntries( json.map( @@ -26,32 +59,7 @@ abstract class Room with _$Room { }), ); - /// [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; + Map toJson() => _$RoomToJson(this); factory Room.fromJson(Map json) => _$RoomFromJson(json); } diff --git a/lib/models/room_chat.dart b/lib/models/room_chat.dart new file mode 100644 index 0000000..a038a88 --- /dev/null +++ b/lib/models/room_chat.dart @@ -0,0 +1,32 @@ +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 7c16cae..3aafd0e 100644 --- a/lib/models/room_metadata.dart +++ b/lib/models/room_metadata.dart @@ -1,29 +1,31 @@ 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 -abstract class RoomMetadata with _$RoomMetadata { - const factory RoomMetadata({ - @JsonKey(name: "room_id") required String id, +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const RoomMetadata({ + @JsonKey(name: "room_id") required final String id, - // required CreateEventContent creationContent, - // required TombstoneEventContent tombstoneEventContent, - String? name, - Uri? avatar, - String? dmUserId, - String? topic, - String? canonicalAlias, - LazyLoadSummary? lazyLoadSummary, - required bool hasMemberList, - @JsonKey(name: "preview_event_rowid") required int previewEventRowID, - @EpochDateTimeConverter() required DateTime sortingTimestamp, - required int unreadHighlights, - required int unreadNotifications, - required int unreadMessages, - }) = _RoomMetadata; + // 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); factory RoomMetadata.fromJson(Map json) => _$RoomMetadataFromJson(json); diff --git a/lib/models/room_summary.dart b/lib/models/room_summary.dart new file mode 100644 index 0000000..7c2d6d2 --- /dev/null +++ b/lib/models/room_summary.dart @@ -0,0 +1,25 @@ +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 6a49845..2ff541d 100644 --- a/lib/models/setting.dart +++ b/lib/models/setting.dart @@ -1,16 +1,13 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; -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, - }); -} +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, +}); diff --git a/lib/models/settings.dart b/lib/models/settings.dart index eddc2da..7f73375 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,15 +1,17 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:freezed_annotation/freezed_annotation.dart"; + part "settings.freezed.dart"; part "settings.g.dart"; -@freezed -abstract class Settings with _$Settings { - const factory Settings({ - @Default(ThemeMode.system) ThemeMode theme, - @Default(true) bool useDynamicTheming, - @Default(false) bool linuxMobileMode, - }) = _Settings; +@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); factory Settings.fromJson(Map json) => _$SettingsFromJson(json); diff --git a/lib/models/settings_category.dart b/lib/models/settings_category.dart index 88bfdd0..2dc7076 100644 --- a/lib/models/settings_category.dart +++ b/lib/models/settings_category.dart @@ -1,14 +1,13 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/setting.dart"; + part "settings_category.freezed.dart"; @freezed -abstract class SettingsCategory with _$SettingsCategory { - const factory SettingsCategory({ - required String title, - required IconData icon, - required IList settings, - }) = _SettingsCategory; -} +class const SettingsCategory({ + required final String title, + required final IconData icon, + required final IList settings, +}) with _$SettingsCategory; diff --git a/lib/models/space.dart b/lib/models/space.dart index 73fbbc6..97efe7a 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -3,16 +3,15 @@ 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 -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; -} +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; diff --git a/lib/models/space_edge.dart b/lib/models/space_edge.dart index 192af31..ea74130 100644 --- a/lib/models/space_edge.dart +++ b/lib/models/space_edge.dart @@ -1,13 +1,15 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "space_edge.freezed.dart"; part "space_edge.g.dart"; -@freezed -abstract class SpaceEdge with _$SpaceEdge { - const factory SpaceEdge({ - required String childId, - @Default(false) bool suggested, - }) = _SpaceEdge; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const SpaceEdge({ + required final String childId, + final bool suggested = false, +}) with _$SpaceEdge { + Map toJson() => _$SpaceEdgeToJson(this); factory SpaceEdge.fromJson(Map json) => _$SpaceEdgeFromJson(json); diff --git a/lib/models/spec_versions_response.dart b/lib/models/spec_versions_response.dart index 07a3ada..1f414d2 100644 --- a/lib/models/spec_versions_response.dart +++ b/lib/models/spec_versions_response.dart @@ -1,24 +1,27 @@ 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 -abstract class SpecVersionsResponse with _$SpecVersionsResponse { - const factory SpecVersionsResponse({ - required IList versions, - required UnstableFeatures unstableFeatures, - }) = _SpecVersionsResponse; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const SpecVersionsResponse({ + required final IList versions, + required final UnstableFeatures unstableFeatures, +}) with _$SpecVersionsResponse { + Map toJson() => _$SpecVersionsResponseToJson(this); factory SpecVersionsResponse.fromJson(Map json) => _$SpecVersionsResponseFromJson(json); } -@freezed -abstract class UnstableFeatures with _$UnstableFeatures { - const factory UnstableFeatures({ - @JsonKey(name: "uk.timedout.msc4494") @Default(false) bool msc4494, - }) = _UnstableFeatures; +@Freezed(toJson: false, fromJson: false) +@JsonSerializable() +class const UnstableFeatures({ + @JsonKey(name: "uk.timedout.msc4494") final bool msc4494 = false, +}) with _$UnstableFeatures { + Map toJson() => _$UnstableFeaturesToJson(this); factory UnstableFeatures.fromJson(Map json) => _$UnstableFeaturesFromJson(json); diff --git a/lib/models/subspace.dart b/lib/models/subspace.dart index 1a1879c..97d1de2 100644 --- a/lib/models/subspace.dart +++ b/lib/models/subspace.dart @@ -1,10 +1,11 @@ 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 -abstract class Subspace with _$Subspace { - const factory Subspace({required Room room, required IList children}) = - _Subspace; -} +class const Subspace({ + required final Room room, + required final IList children, +}) with _$Subspace; diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index b2699d6..a9c10bf 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -2,20 +2,21 @@ 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 -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; +@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); factory SyncData.fromJson(Map json) => _$SyncDataFromJson(json); diff --git a/lib/models/sync_status.dart b/lib/models/sync_status.dart index 7848fbe..57bd882 100644 --- a/lib/models/sync_status.dart +++ b/lib/models/sync_status.dart @@ -1,14 +1,16 @@ import "package:freezed_annotation/freezed_annotation.dart"; + part "sync_status.freezed.dart"; part "sync_status.g.dart"; -@freezed -abstract class SyncStatus with _$SyncStatus { - const factory SyncStatus({ - required SyncStatusType type, - String? error, - required int errorCount, - }) = _SyncStatus; +@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); factory SyncStatus.fromJson(Map json) => _$SyncStatusFromJson(json); diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index 9da03aa..33727ac 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,28 +1,20 @@ import "dart:io"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/settings.dart"; import "package:window_manager/window_manager.dart"; -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(), - }); - +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 { @override Size get preferredSize => const .fromHeight(kToolbarHeight); @@ -47,7 +39,7 @@ class Appbar extends ConsumerWidget implements PreferredSizeWidget { : (_) => windowManager.startDragging(), ), child: AppBar( - leading: InkWell(onTap: onTap, child: leading), + leading: leading == null ? null : 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 75c08b9..982d56e 100644 --- a/lib/widgets/avatar_or_hash.dart +++ b/lib/widgets/avatar_or_hash.dart @@ -1,21 +1,15 @@ import "package:color_hash/color_hash.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/mxc_image.dart"; -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, - }); - +final class const AvatarOrHash( + final Uri? avatar, + final String title, { + final Widget? fallback, + final double height = 24, + super.key, +}) extends ConsumerWidget { @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 ca3de8a..3d04a78 100644 --- a/lib/widgets/composer/composer.dart +++ b/lib/widgets/composer/composer.dart @@ -1,7 +1,9 @@ import "dart:io"; + import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:file_selector/file_selector.dart"; -import "package:flutter/material.dart"; +import "package:nexus/widgets/emoji_picker.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:fluttertagger/fluttertagger.dart"; @@ -12,33 +14,24 @@ 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/mention_overlay.dart"; +import "package:nexus/widgets/composer/overlays/tagger_overlay.dart"; import "package:nexus/widgets/composer/relation_preview.dart"; -import "package:nexus/widgets/emoji_picker_button.dart"; import "package:nexus/main.dart"; -class Composer extends HookConsumerWidget { - final String roomId; - final Event? relatedEvent; - final RelationType relationType; - final VoidCallback onDismiss; - final FocusNode? node; - final Future Function( +class const Composer( + final String roomId, { + required final Event? relatedEvent, + required final RelationType relationType, + required final VoidCallback onDismiss, + required final Future Function( String text, { required bool shouldMention, required IList tags, }) - onSend; - const Composer( - this.roomId, { - required this.relatedEvent, - required this.relationType, - required this.onDismiss, - required this.onSend, - this.node, - super.key, - }); - + onSend, + final FocusNode? node, + super.key, +}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); @@ -126,10 +119,19 @@ class Composer extends HookConsumerWidget { ), ) ? [ - EmojiPickerButton( - context: context, - onSelection: (_) => node?.requestFocus(), - controller: controller.value, + 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), ), PopupMenuButton( tooltip: "Add media", @@ -143,9 +145,8 @@ class Composer extends HookConsumerWidget { ), onTap: () async => ref .watch( - AttachmentController.provider( - roomId, - ).notifier, + AttachmentController.provider(roomId) + .notifier, ) .add( (await ref @@ -163,9 +164,8 @@ class Composer extends HookConsumerWidget { ), onTap: () async => ref .watch( - AttachmentController.provider( - roomId, - ).notifier, + AttachmentController.provider(roomId) + .notifier, ) .add( (await ref @@ -179,9 +179,8 @@ class Composer extends HookConsumerWidget { PopupMenuItem( onTap: () async => ref .watch( - AttachmentController.provider( - roomId, - ).notifier, + AttachmentController.provider(roomId) + .notifier, ) .add((await openFile())!) .onError(showError), @@ -196,9 +195,9 @@ class Composer extends HookConsumerWidget { Expanded( child: FlutterTagger( triggerStrategy: .eager, - overlay: MentionOverlay( - roomId, - query: query.value, + overlay: TaggerOverlay( + query.value, + roomId: roomId, triggerCharacter: triggerCharacter.value, addTag: ({required id, required name}) { controller.value.addTag(id: id, name: name); @@ -213,6 +212,7 @@ class Composer extends HookConsumerWidget { triggerCharacterAndStyles: { "@": style, "#": style, + ":": style, }, builder: (context, key) => Focus( onKeyEvent: (_, event) { @@ -234,7 +234,12 @@ class Composer extends HookConsumerWidget { child: TextField( maxLines: 12, minLines: 1, - autofocus: true, + autofocus: + (Platform.isLinux || + Platform.isMacOS || + Platform.isWindows) + ? true + : false, decoration: .new( hintText: "Your message here...", border: .none, diff --git a/lib/widgets/composer/mention_overlay.dart b/lib/widgets/composer/mention_overlay.dart deleted file mode 100644 index ca4f95a..0000000 --- a/lib/widgets/composer/mention_overlay.dart +++ /dev/null @@ -1,158 +0,0 @@ -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 new file mode 100644 index 0000000..0e762c0 --- /dev/null +++ b/lib/widgets/composer/overlays/emoji_overlay.dart @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..b5af177 --- /dev/null +++ b/lib/widgets/composer/overlays/room_overlay.dart @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..7d973dc --- /dev/null +++ b/lib/widgets/composer/overlays/tagger_overlay.dart @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..fe4f70f --- /dev/null +++ b/lib/widgets/composer/overlays/user_overlay.dart @@ -0,0 +1,64 @@ +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 c9cc271..fca9de6 100644 --- a/lib/widgets/composer/relation_preview.dart +++ b/lib/widgets/composer/relation_preview.dart @@ -1,25 +1,17 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 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, - }); - +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 { @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 2b0f9bd..c44ae11 100644 --- a/lib/widgets/divider_text.dart +++ b/lib/widgets/divider_text.dart @@ -1,11 +1,8 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:nexus/widgets/divider_widget.dart"; -class DividerText extends StatelessWidget { - final String text; - - const DividerText(this.text, {super.key}); - +final class const DividerText(final String text, {super.key}) + extends StatelessWidget { @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 6f13bd4..5e4972c 100644 --- a/lib/widgets/divider_widget.dart +++ b/lib/widgets/divider_widget.dart @@ -1,9 +1,7 @@ -import "package:flutter/material.dart"; - -class DividerWidget extends StatelessWidget { - final Widget widget; - const DividerWidget(this.widget, {super.key}); +import "package:material_ui/material_ui.dart"; +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 new file mode 100644 index 0000000..a0c691a --- /dev/null +++ b/lib/widgets/emoji_picker.dart @@ -0,0 +1,53 @@ +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 deleted file mode 100644 index 2ac906a..0000000 --- a/lib/widgets/emoji_picker_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:emoji_text_field/emoji_text_field.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/emoji.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 9b62200..ddc4c98 100644 --- a/lib/widgets/error_dialog.dart +++ b/lib/widgets/error_dialog.dart @@ -1,13 +1,13 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/misc.dart"; -class ErrorDialog extends ConsumerWidget { - final Object error; - final StackTrace? stackTrace; - final ProviderOrFamily? provider; - const ErrorDialog(this.error, this.stackTrace, {this.provider, super.key}); - +final class const ErrorDialog( + final Object error, + final StackTrace? stackTrace, { + final ProviderOrFamily? provider, + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return AlertDialog( diff --git a/lib/widgets/event_preview.dart b/lib/widgets/event_preview.dart index 7a40a75..30dfbfe 100644 --- a/lib/widgets/event_preview.dart +++ b/lib/widgets/event_preview.dart @@ -1,14 +1,12 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 EventPreview extends StatelessWidget { - final Event event; - const EventPreview(this.event, {super.key}); - +class const EventPreview(final Event event, {super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) => IgnorePointer( child: Padding( diff --git a/lib/widgets/expandable_image.dart b/lib/widgets/expandable_image.dart index ee4f900..0639417 100644 --- a/lib/widgets/expandable_image.dart +++ b/lib/widgets/expandable_image.dart @@ -1,15 +1,15 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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"; -class ExpandableImage extends ConsumerWidget { - final Widget child; - final DownloadMediaRequest? request; - const ExpandableImage(this.request, {required this.child, super.key}); - +final class const ExpandableImage( + final DownloadMediaRequest? request, { + required final Widget child, + super.key, +}) extends ConsumerWidget { @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 afdad89..4c0fdde 100644 --- a/lib/widgets/file_card.dart +++ b/lib/widgets/file_card.dart @@ -1,13 +1,13 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:nexus/helpers/extensions/size_to_string.dart"; import "package:nexus/models/info/file.dart"; -class FileCard extends StatelessWidget { - final Uri uri; - final FileInfo? info; - final String? filename; - const FileCard(this.uri, this.info, {this.filename, super.key}); - +final class const FileCard( + final Uri uri, + final FileInfo? info, { + final String? filename, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( width: 320, diff --git a/lib/widgets/highlight_wrapper.dart b/lib/widgets/highlight_wrapper.dart index c7e568e..b10e3ff 100644 --- a/lib/widgets/highlight_wrapper.dart +++ b/lib/widgets/highlight_wrapper.dart @@ -1,10 +1,10 @@ -import "package:flutter/material.dart"; - -class HighlightWrapper extends StatelessWidget { - final Widget child; - final bool isHighlighted; - const HighlightWrapper(this.child, {this.isHighlighted = false, super.key}); +import "package:material_ui/material_ui.dart"; +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 a5c3dee..abd9be2 100644 --- a/lib/widgets/html/code_block.dart +++ b/lib/widgets/html/code_block.dart @@ -1,12 +1,13 @@ import "dart:math"; -import "package:flutter/material.dart"; - -class CodeBlock extends StatelessWidget { - final String code; - final String lang; - const CodeBlock(this.code, {required this.lang, super.key}); +import "package:flutter/services.dart"; +import "package:material_ui/material_ui.dart"; +class const CodeBlock( + final String code, { + required final String lang, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -28,7 +29,7 @@ class CodeBlock extends StatelessWidget { ), ), TextButton.icon( - onPressed: () {}, + onPressed: () => Clipboard.setData(.new(text: code)), icon: Icon(Icons.copy), label: Text("Copy"), ), diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 5437285..85f8054 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:flutter/material.dart"; +import "package:material_ui/material_ui.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,130 +11,140 @@ import "package:nexus/widgets/html/spoiler_text.dart"; import "package:nexus/widgets/html/code_block.dart"; import "package:nexus/widgets/html/quoted.dart"; -class Html extends ConsumerWidget { - final String html; - final String? roomId; - final TextStyle? textStyle; - const Html(this.html, {this.roomId, this.textStyle, super.key}); - +class const Html( + final String html, { + final String? roomId, + final TextStyle? textStyle, + super.key, +}) extends ConsumerWidget { @override - 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(); - } + 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(); + } - if (element.attributes.keys.contains("data-mx-spoiler")) { - return InlineCustomWidget(child: SpoilerText(text: element.text)); - } + if (element.attributes.keys.contains("data-mx-spoiler")) { + return InlineCustomWidget(child: SpoilerText(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), + "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(), ), - 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 f6105e3..2387a49 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -1,46 +1,60 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 MentionChip extends ConsumerWidget { - final String? roomId; - final String content; - const MentionChip(this.content, this.roomId, {super.key}); - +class const MentionChip(final String content, final String? roomId, {super.key}) + extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final mention = content.mention; - final membership = mention?.startsWith("@") == true - ? ref - .watch( - UserController.provider(.new(roomId: roomId, userId: mention!)), - ) - .whenOrNull(data: (data) => data) - : null; + 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, + }; return mention == null ? SizedBox.shrink() : InkWell( onTap: () { - if (membership != null) { + if (data case MembershipContent membership) { context.showUserPopover(membership, mention, roomId: roomId); + } else if (data case RoomSummary summary) { + // TODO: Handle summary } }, - 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, + 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, ), ), ); diff --git a/lib/widgets/html/quoted.dart b/lib/widgets/html/quoted.dart index e582b06..3157263 100644 --- a/lib/widgets/html/quoted.dart +++ b/lib/widgets/html/quoted.dart @@ -1,9 +1,6 @@ -import "package:flutter/material.dart"; - -class Quoted extends StatelessWidget { - final Widget child; - const Quoted(this.child, {super.key}); +import "package:material_ui/material_ui.dart"; +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 a7a457b..92a5538 100644 --- a/lib/widgets/html/spoiler_text.dart +++ b/lib/widgets/html/spoiler_text.dart @@ -1,11 +1,7 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -class SpoilerText extends HookWidget { - final String text; - - const SpoilerText({super.key, required this.text}); - +class const SpoilerText(final String text, {super.key}) extends HookWidget { @override Widget build(BuildContext context) { final revealed = useState(false); diff --git a/lib/widgets/join_dialog.dart b/lib/widgets/join_dialog.dart index 50b36f2..e818c7d 100644 --- a/lib/widgets/join_dialog.dart +++ b/lib/widgets/join_dialog.dart @@ -1,5 +1,5 @@ import "package:collection/collection.dart"; -import "package:flutter/material.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/client.dart"; @@ -7,10 +7,7 @@ import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/spaces.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart"; -class JoinDialog extends HookWidget { - final WidgetRef ref; - const JoinDialog(this.ref, {super.key}); - +class const JoinDialog(final WidgetRef ref, {super.key}) extends HookWidget { @override Widget build(BuildContext context) { final roomAlias = useTextEditingController(); @@ -53,9 +50,8 @@ class JoinDialog extends HookWidget { .new( roomIdOrAlias: roomIdOrAlias, via: .new( - Uri.tryParse( - roomAlias.text.replaceAll("/#", ""), - )?.queryParametersAll["via"] ?? + Uri.tryParse(roomAlias.text.replaceAll("/#", "")) + ?.queryParametersAll["via"] ?? [], ), ), @@ -76,9 +72,8 @@ class JoinDialog extends HookWidget { await ref .watch( - KeyController.provider( - KeyController.spaceKey, - ).notifier, + KeyController.provider(KeyController.spaceKey) + .notifier, ) .set( space?.id ?? @@ -100,9 +95,8 @@ class JoinDialog extends HookWidget { if (space == null) { await ref .watch( - KeyController.provider( - KeyController.roomKey, - ).notifier, + KeyController.provider(KeyController.roomKey) + .notifier, ) .set(id); } @@ -115,9 +109,9 @@ class JoinDialog 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 93c2554..709d558 100644 --- a/lib/widgets/lazy_loading/message_avatar.dart +++ b/lib/widgets/lazy_loading/message_avatar.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 MessageAvatar extends ConsumerWidget { - final Event event; - final double height; - const MessageAvatar(this.event, {this.height = 24, super.key}); - +class const MessageAvatar( + final Event event, { + final double height = 24, + super.key, +}) extends ConsumerWidget { @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 bd5733c..9203f69 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/author.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; @@ -6,17 +6,12 @@ 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 MessageDisplayname extends ConsumerWidget { - final Event event; - final TextStyle? style; - final bool clickable; - const MessageDisplayname( - this.event, { - this.clickable = true, - this.style, - super.key, - }); - +class const MessageDisplayname( + final Event event, { + final TextStyle? style, + final bool clickable = true, + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( AuthorController.provider(event), @@ -40,14 +35,11 @@ class MessageDisplayname extends ConsumerWidget { 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 653248c..77ba7a4 100644 --- a/lib/widgets/linkified_text.dart +++ b/lib/widgets/linkified_text.dart @@ -1,14 +1,14 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_linkify/flutter_linkify.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/helpers/launch_helper.dart"; -class LinkifiedText extends ConsumerWidget { - final String text; - final int? maxLines; - final TextStyle? style; - const LinkifiedText(this.text, {this.maxLines, this.style, super.key}); - +final class const LinkifiedText( + final String text, { + final int? maxLines, + final TextStyle? style, + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => Linkify( text: text, diff --git a/lib/widgets/loading.dart b/lib/widgets/loading.dart index fc84563..180cd1b 100644 --- a/lib/widgets/loading.dart +++ b/lib/widgets/loading.dart @@ -1,9 +1,6 @@ -import "package:flutter/material.dart"; - -class Loading extends StatelessWidget { - final double? height; - const Loading({this.height, super.key}); +import "package:material_ui/material_ui.dart"; +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 92bb179..9ddafa3 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:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_buttons/m3e_buttons.dart"; @@ -16,10 +16,8 @@ import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/user_bottom_sheet.dart"; -class MemberList extends HookConsumerWidget { - final String roomId; - const MemberList(this.roomId, {super.key}); - +class const MemberList(final String roomId, {super.key}) + extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final statusIndex = useState(0); @@ -96,9 +94,9 @@ class MemberList extends HookConsumerWidget { ), 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 914a252..b04c209 100644 --- a/lib/widgets/message_image.dart +++ b/lib/widgets/message_image.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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"; -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}); - +final class const MessageImage( + final Uri url, { + final i.ImageInfo? info, + required final bool encrypted, + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final request = DownloadMediaRequest(mxc: url, encrypted: encrypted); diff --git a/lib/pages/chat.dart b/lib/widgets/pages/chat.dart similarity index 56% rename from lib/pages/chat.dart rename to lib/widgets/pages/chat.dart index 316c9af..186dfa2 100644 --- a/lib/pages/chat.dart +++ b/lib/widgets/pages/chat.dart @@ -1,25 +1,23 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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.dart"; +import "package:nexus/widgets/room_chat/room_chat.dart"; import "package:nexus/widgets/loading.dart"; -class ChatPage extends ConsumerWidget { - const ChatPage({super.key}); - +class const ChatPage({super.key}) extends HookConsumerWidget { @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)); - - ref.read(EmojiController.provider); + final roomId = ref + .watch(KeyController.provider(KeyController.roomKey)) + .requireValue; return SafeArea( child: Scaffold( @@ -29,10 +27,19 @@ class ChatPage extends ConsumerWidget { children: [ if (isDesktop) Sidebar(isDesktop: isDesktop), Expanded( - child: RoomChat( - roomId: roomId, - isDesktop: isDesktop, - showMembersByDefault: showMembersByDefault, + 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, + ); + }, ), ), ], diff --git a/lib/widgets/pages/notifications.dart b/lib/widgets/pages/notifications.dart new file mode 100644 index 0000000..2ad8930 --- /dev/null +++ b/lib/widgets/pages/notifications.dart @@ -0,0 +1,215 @@ +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/pages/select_server.dart b/lib/widgets/pages/select_server.dart similarity index 77% rename from lib/pages/select_server.dart rename to lib/widgets/pages/select_server.dart index 853e5fc..34f1a50 100644 --- a/lib/pages/select_server.dart +++ b/lib/widgets/pages/select_server.dart @@ -1,5 +1,5 @@ import "package:app_links/app_links.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; @@ -9,13 +9,11 @@ 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/pages/settings.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/divider_text.dart"; -class SelectServerPage extends HookConsumerWidget { - const SelectServerPage({super.key}); - +class const SelectServerPage({super.key}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); @@ -26,6 +24,17 @@ class SelectServerPage 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 { @@ -33,26 +42,14 @@ class SelectServerPage extends HookConsumerWidget { newHomeserver = Uri.https(newHomeserver!.path); } - final newUrl = newHomeserver == null - ? null - : await ref - .read(ClientController.provider.notifier) - .discoverHomeserver(newHomeserver); + final newUrl = await ref + .read(ClientController.provider.notifier) + .discoverHomeserver(newHomeserver!); if (context.mounted) { - 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 { + Future tryLogin(Uri url) async { final codeResponse = await ref.watch( - AuthUrlController.provider(newUrl).future, + AuthUrlController.provider(url).future, ); await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url); @@ -70,7 +67,7 @@ class SelectServerPage extends HookConsumerWidget { .watch(ClientController.provider.notifier) .exchangeToken( .new( - homeserverUrl: newUrl, + homeserverUrl: url, codeVerifier: codeResponse.codeVerifier, redirectUri: .new( scheme: "nexus.federated.nexus", @@ -78,20 +75,42 @@ class SelectServerPage extends HookConsumerWidget { ), code: code, clientId: await ref.watch( - ClientIdController.provider(newUrl).future, + ClientIdController.provider(url).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; } @@ -116,7 +135,7 @@ class SelectServerPage extends HookConsumerWidget { children: [ Row( children: [ - SvgPicture.asset("assets/icon.svg", width: 128), + SvgPicture.asset("assets/bundled/icon.svg", width: 128), SizedBox(width: 12), Expanded( child: Column( @@ -163,23 +182,20 @@ class SelectServerPage 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/pages/settings.dart b/lib/widgets/pages/settings.dart similarity index 81% rename from lib/pages/settings.dart rename to lib/widgets/pages/settings.dart index fa4d8da..50eb7f0 100644 --- a/lib/pages/settings.dart +++ b/lib/widgets/pages/settings.dart @@ -1,6 +1,6 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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,14 +8,12 @@ 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/pages/settings_category.dart"; +import "package:nexus/widgets/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 SettingsPage extends ConsumerWidget { - const SettingsPage({super.key}); - +class const SettingsPage({super.key}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder( builder: (_, constraints) => HookBuilder( @@ -142,12 +140,12 @@ class SettingsPage 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( @@ -172,30 +170,34 @@ class SettingsPage extends ConsumerWidget { ) : Row( children: [ - 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, + 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, + ), ), VerticalDivider(), Expanded( diff --git a/lib/pages/settings_category.dart b/lib/widgets/pages/settings_category.dart similarity index 91% rename from lib/pages/settings_category.dart rename to lib/widgets/pages/settings_category.dart index b21d048..8e94c56 100644 --- a/lib/pages/settings_category.dart +++ b/lib/widgets/pages/settings_category.dart @@ -1,7 +1,7 @@ import "dart:async"; import "package:collection/collection.dart"; -import "package:flutter/material.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/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 SettingsCategoryPage extends HookConsumerWidget { - final int index; - final int? initialHighlight; - const SettingsCategoryPage(this.index, {this.initialHighlight, super.key}); - +class const SettingsCategoryPage( + final int index, { + final int? initialHighlight, + super.key, +}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final highlight = useState(initialHighlight); diff --git a/lib/pages/verify.dart b/lib/widgets/pages/verify.dart similarity index 93% rename from lib/pages/verify.dart rename to lib/widgets/pages/verify.dart index 4eefa6c..a4f95ec 100644 --- a/lib/pages/verify.dart +++ b/lib/widgets/pages/verify.dart @@ -1,14 +1,12 @@ -import "package:flutter/material.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/client.dart"; -import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/helpers/required_validator_helper.dart"; -class VerifyPage extends HookConsumerWidget { - const VerifyPage({super.key}); - +class const VerifyPage({super.key}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final passphraseController = useTextEditingController(); diff --git a/lib/widgets/pinned_events_drawer.dart b/lib/widgets/pinned_events_drawer.dart index 7cb9fe3..73d9a79 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:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/pinned_events.dart"; import "package:nexus/models/event.dart"; @@ -7,17 +7,12 @@ import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/renderers/event.dart"; -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, - }); - +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 { @override Widget build(BuildContext context, WidgetRef ref) { final pinsProvider = ref.watch(PinnedEventsController.provider(roomId)); @@ -71,11 +66,13 @@ class PinnedEventsDrawer extends HookConsumerWidget { }, child: Padding( padding: .symmetric(vertical: 4), - child: EventRenderer( - event, - maxLines: 2, - isGrouped: false, - getEventOptions: getEventOptions, + child: IgnorePointer( + child: EventRenderer( + event, + maxLines: 2, + isGrouped: false, + getEventOptions: getEventOptions, + ), ), ), ); diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart index 22a91f7..bd50a8b 100644 --- a/lib/widgets/players/audio.dart +++ b/lib/widgets/players/audio.dart @@ -1,18 +1,18 @@ import "dart:async"; -import "package:flutter/material.dart"; + +import "package:material_ui/material_ui.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 AudioPlayer extends HookConsumerWidget { - final Uri uri; - final AudioInfo? info; - final bool encrypted; - - const AudioPlayer(this.uri, this.info, {this.encrypted = false, super.key}); - +class const AudioPlayer( + final Uri uri, + final AudioInfo? info, { + final bool encrypted = false, + super.key, +}) extends HookConsumerWidget { @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 457cdc4..024c30e 100644 --- a/lib/widgets/players/video.dart +++ b/lib/widgets/players/video.dart @@ -1,5 +1,6 @@ import "dart:async"; -import "package:flutter/material.dart"; + +import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/info/video.dart"; @@ -7,12 +8,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 VideoPlayer extends HookConsumerWidget { - final VideoInfo? info; - final Uri uri; - final bool encrypted; - const VideoPlayer(this.uri, this.info, {this.encrypted = false, super.key}); - +class const VideoPlayer( + final Uri uri, + final VideoInfo? info, { + final bool encrypted = false, + super.key, +}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final player = useMemoized( diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart index b2fa5ca..195aa4f 100644 --- a/lib/widgets/reaction_row.dart +++ b/lib/widgets/reaction_row.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state.dart"; @@ -10,10 +10,7 @@ import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/main.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -class ReactionRow extends ConsumerWidget { - final Event event; - const ReactionRow(this.event, {super.key}); - +class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final clientState = ref.watch(ClientStateController.provider); @@ -67,9 +64,10 @@ class ReactionRow extends ConsumerWidget { enabled.value = false; try { final controller = ref.watch( - RoomChatController.provider( + RoomChatController.provider(( event.roomId, - ).notifier, + null, + )).notifier, ); if (selected) { diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index a9c703a..697161f 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -1,5 +1,6 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; +import "package:flutter/gestures.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/helpers/extensions/show_context_menu.dart"; @@ -24,24 +25,17 @@ 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 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, - }); - +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 { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); @@ -159,7 +153,7 @@ class EventRenderer extends HookConsumerWidget { final contextMenuCallback = getEventOptions == null ? null - : (details) => context.showContextMenu( + : (PositionedGestureDetails details) => context.showContextMenu( globalPosition: details.globalPosition, children: getEventOptions!(event).toList(), ); @@ -202,10 +196,18 @@ class EventRenderer extends HookConsumerWidget { onSecondaryTapUp: contextMenuCallback, onLongPressStart: contextMenuCallback, child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 8, - ).copyWith(top: isGrouped ? 0 : 8), - child: child, + 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), + ], + ), ), ), ), @@ -213,10 +215,7 @@ class EventRenderer extends HookConsumerWidget { ), ...[ - if (event.content is! MessageContent && - event.content is! StickerContent && - event.content is! EncryptedContent) - ReactionRow(event), + if (child is! MessageRenderer) 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 6dfae53..eb5b29b 100644 --- a/lib/widgets/renderers/generic_event.dart +++ b/lib/widgets/renderers/generic_event.dart @@ -1,18 +1,19 @@ -import "package:flutter/material.dart"; - -class GenericEventRenderer extends StatelessWidget { - final IconData icon; - final List children; - const GenericEventRenderer(this.icon, this.children, {super.key}); +import "package:material_ui/material_ui.dart"; +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)), - Expanded(child: Wrap(spacing: 4, children: children)), + Flexible(child: Wrap(spacing: 4, children: children)), ], ), ); diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index b2835b4..5ce5d68 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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,10 +7,8 @@ import "package:nexus/models/event.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -class MembershipRenderer extends StatelessWidget { - final Event event; - const MembershipRenderer(this.event, {super.key}); - +class const MembershipRenderer(final Event event, {super.key}) + extends StatelessWidget { @override Widget build(BuildContext context) { assert( diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index c684085..a0bbf99 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -1,5 +1,6 @@ import "package:collection/collection.dart"; -import "package:flutter/material.dart"; +import "package:flutter/rendering.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:linkify/linkify.dart"; import "package:nexus/controllers/client_state.dart"; @@ -15,48 +16,136 @@ 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 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, - }); - +class const MessageRenderer( + final Event event, { + final VoidCallback? onTapReply, + final bool textOnly = false, + final bool isGrouped = false, + final int? maxLines, + super.key, +}) extends ConsumerWidget { @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, @@ -77,7 +166,7 @@ class MessageRenderer extends ConsumerWidget { spacing: 4, children: [ Flexible(child: MessageDisplayname(event)), - Flexible(flex: 0, child: timestamp), + Flexible(flex: 0, child: Timestamp(event.timestamp)), ], ), Card( @@ -130,172 +219,13 @@ class MessageRenderer extends ConsumerWidget { ), ), ), - 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"), - }, + RendererBinding.instance.mouseTracker.mouseIsConnected + ? SelectableRegion( + selectionControls: materialTextSelectionControls, + child: rendered, + ) + : rendered, if (!textOnly) ReactionRow(event), ], ), diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 69eafaa..305c490 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -1,4 +1,4 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/widgets/appbar.dart"; @@ -7,21 +7,14 @@ import "package:nexus/widgets/expandable_image.dart"; import "package:nexus/widgets/linkified_text.dart"; import "package:nexus/widgets/room_menu.dart"; -class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { - final bool isDesktop; - final void Function(BuildContext context)? onOpenMemberList; - final void Function()? onOpenPinnedMessagesList; - final void Function() onOpenDrawer; - final String? roomId; - const RoomAppbar({ - required this.roomId, - required this.isDesktop, - required this.onOpenDrawer, - this.onOpenMemberList, - this.onOpenPinnedMessagesList, - super.key, - }); - +final class const RoomAppbar({ + required final String? roomId, + required final bool isDesktop, + required final void Function() onOpenDrawer, + final void Function(BuildContext context)? onOpenMemberList, + final void Function()? onOpenPinnedMessagesList, + super.key, +}) extends ConsumerWidget implements PreferredSizeWidget { @override Size get preferredSize => AppBar().preferredSize; @@ -67,9 +60,9 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { room.metadata?.name ?? "Unnamed Room", overflow: .ellipsis, maxLines: 3, - style: Theme.of( - context, - ).textTheme.headlineSmall, + style: Theme.of(context) + .textTheme + .headlineSmall, ), ), ], @@ -79,9 +72,9 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget { 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 deleted file mode 100644 index cb1e695..0000000 --- a/lib/widgets/room_chat.dart +++ /dev/null @@ -1,564 +0,0 @@ -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 listController = useRef(ListController()); - final scrollController = useScrollController(); - final controllerData = ref.watch(controllerProvider); - - final topEventBeforeLoad = useState(null); - - Future jumpToId(String eventId) async { - final index = controllerData.value?.indexWhere( - (element) => element.eventId == eventId, - ); - if (index == null) return; - - listController.value.animateToItem( - index: index, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => .new(milliseconds: 700), - curve: (_) => Curves.easeInOut, - ); - highlightedEvent.value = eventId; - await Future.delayed(.new(seconds: 1), () { - if (highlightedEvent.value == eventId) { - highlightedEvent.value = null; - } - }); - } - - Future loadOlder() async { - if (controllerData case AsyncData(:final value?)) { - topEventBeforeLoad.value = value.firstOrNull?.eventId; - await notifier.loadOlder(); - } - } - - useEffect(() { - ref - .read(controllerProvider.future) - .then( - (_) => WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent - .000001, - ); - } - }), - ); - - return null; - }, [scrollController.hasClients]); - - useEffect(() { - if (controllerData case AsyncData( - :final value?, - ) when scrollController.hasClients) { - if (topEventBeforeLoad.value != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - final index = value.indexWhere( - (event) => event.eventId == topEventBeforeLoad.value, - ); - if (index != -1) { - listController.value.jumpToItem( - index: index, - scrollController: scrollController, - alignment: 0, - ); - } - } - topEventBeforeLoad.value = null; - }); - } else if (scrollController.position.atEdge && - scrollController.position.pixels != 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.maxScrollExtent, - ); - } - }); - } - } - - return null; - }, [controllerData]); - - useEffect(() { - Future listener() async { - if (!scrollController.hasClients || !scrollController.position.atEdge) { - return; - } - - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - if (room == null) return; - - if (scrollController.position.pixels == 0) { - if (room.hasMore) { - await loadOlder(); - } - } else { - await client.markRead(room); - } - } - - scrollController.addListener(listener); - return () => scrollController.removeListener(listener); - }, [roomId, controllerData]); - - final composerNode = useFocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && event.logicalKey == .escape) { - relatedEvent.value = null; - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - ); - - IList getEventOptions(Event event) { - final danger = theme.colorScheme.error; - final isSentByMe = event.sender == userId; - - return [ - if (ref.watch( - PowerLevelController.provider( - .new(eventType: .reaction, roomId: roomId), - ), - )) - PopupMenuItem( - enabled: false, - child: IconTheme( - data: theme.iconTheme, - child: Row( - children: [ - ...{ - ...ref.watch( - AccountDataController.provider.select( - (value) => 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?) => CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: .symmetric(vertical: 36), - child: Center( - child: ElevatedButton( - onPressed: controllerData is AsyncData - ? loadOlder - : null, - child: Text("Load More"), - ), - ), - ), - ), - - SuperSliverList.builder( - listController: listController.value, - itemCount: value.length, - itemBuilder: (_, index) { - final event = value[index]; - final previousEvent = value.getOrNull( - index - 1, - ); - return HighlightWrapper( - EventRenderer( - event, - onTapReply: () => - jumpToId(event.replyTo!), - getEventOptions: getEventOptions, - isGrouped: - previousEvent?.content - is MessageContent && - previousEvent?.redactedBy == null && - previousEvent?.relationType != - "m.replace" && - "${event.sender}${event.pmp?.id}" == - "${previousEvent?.sender}${previousEvent?.pmp?.id}", - ), - isHighlighted: - highlightedEvent.value == event.eventId, - ); - }, - ), - - SliverPadding( - padding: .only(bottom: composerSize.value), - ), - ], - ), - AsyncData() => nothing, - AsyncLoading() => Loading(), - AsyncError(:final error, :final stackTrace) => - ErrorDialog(error, stackTrace), - }, - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: MeasureSize( - onChange: (size) => composerSize.value = size.height, - child: Composer( - roomId, - node: composerNode, - onSend: - (text, {required shouldMention, required tags}) => - notifier - .send( - text, - tags: tags, - relationType: relationType.value, - shouldMention: shouldMention, - relation: relatedEvent.value, - ) - .onError(showError), - relationType: relationType.value, - relatedEvent: relatedEvent.value, - onDismiss: () => relatedEvent.value = null, - ), - ), - ), - ], - ), - ), - - if (memberListOpened == true && showMembersByDefault) - MemberList(roomId), - ], - ), - ), - ), - ); - } -} diff --git a/lib/widgets/room_chat/chat_timeline.dart b/lib/widgets/room_chat/chat_timeline.dart new file mode 100644 index 0000000..2cab4ca --- /dev/null +++ b/lib/widgets/room_chat/chat_timeline.dart @@ -0,0 +1,80 @@ +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 new file mode 100644 index 0000000..709d22d --- /dev/null +++ b/lib/widgets/room_chat/room_chat.dart @@ -0,0 +1,231 @@ +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 95936df..7488ada 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:flutter/material.dart"; +import "package:material_ui/material_ui.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"; -class RoomMenu extends ConsumerWidget { - final Room? room; - final IList children; - const RoomMenu(this.room, {this.children = const IList.empty(), super.key}); - +final class const RoomMenu( + final Room? room, { + final IList children = const IList.empty(), + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final danger = Theme.of(context).colorScheme.error; - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); return PopupMenuButton( itemBuilder: (_) => [ @@ -35,8 +35,7 @@ class RoomMenu extends ConsumerWidget { 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 f0a236c..e66e335 100644 --- a/lib/widgets/settings/dialog_list_tile.dart +++ b/lib/widgets/settings/dialog_list_tile.dart @@ -1,28 +1,18 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/widgets/settings/radio_dialog.dart"; -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, - }); - +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 { @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 7695b9d..6696139 100644 --- a/lib/widgets/settings/radio_dialog.dart +++ b/lib/widgets/settings/radio_dialog.dart @@ -1,21 +1,14 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -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, - }); - +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 { @override Widget build(BuildContext context) { final mutValue = useState(null); diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 1bd6cf5..cc6d441 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -1,33 +1,35 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/spaces.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/pages/settings.dart"; +import "package:nexus/widgets/pages/notifications.dart"; +import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_widget.dart"; import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/room_menu.dart"; -class Sidebar extends HookConsumerWidget { - final bool isDesktop; - const Sidebar({required this.isDesktop, super.key}); +// Needed for navigation_rail_m3e (#65). +import "package:flutter/material.dart" as old_mat; +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); + final selectedSpaceId = ref.watch(selectedSpaceProvider).requireValue; final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedRoomController = KeyController.provider( KeyController.roomKey, ); - final selectedRoomId = ref.watch(selectedRoomController); + final selectedRoomId = ref.watch(selectedRoomController).requireValue; final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); final spaces = ref.watch(SpacesController.provider); @@ -74,126 +76,149 @@ class Sidebar extends HookConsumerWidget { shape: Border(), child: Row( children: [ - Theme( - data: Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemCollapsedHeight: 48, - itemVerticalGap: 0, + MaterialUiCompatibilityBridge( + child: Builder( + builder: (context) => old_mat.Theme( + data: old_mat.Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemCollapsedHeight: 48, + itemVerticalGap: 0, + ), + ], ), - ], - ), - child: Container( - color: NavigationRailTokensAdapter(context).containerColor, - padding: EdgeInsets.only(top: 16), - child: NavigationRailM3E( - type: .alwaysCollapse, - labelBehavior: .alwaysHide, - scrollable: true, - onDestinationSelected: (value) { - selectedSpaceIdNotifier.set(spaces[value].id); - selectedRoomIdNotifier.set( - spaces[value].children.firstOrNull?.metadata?.id, - ); - }, - sections: [ - .new( - destinations: spaces - .map( - (space) => NavigationRailM3EDestination( - badgeCount: switch (space.children - .addAll( - space.subSpaces - .map((element) => element.children) - .flattened, - ) - .fold( - 0, - (previousValue, room) => - previousValue + - (room.metadata?.unreadNotifications ?? 0), - )) { - 0 => - space.children - .addAll( - space.subSpaces - .map( - (element) => element.children, - ) - .flattened, - ) - .any( - (room) => - room.metadata?.unreadMessages != - 0, - ) - ? 0 - : null, - int badgeCount => badgeCount, - }, - short: true, - icon: AvatarOrHash( - height: 28, - space.room?.metadata?.avatar, - fallback: space.icon == null - ? null - : Icon(space.icon), - space.title, - ), - label: space.title, - ), - ) - .toList(), - ), - ], - selectedIndex: selectedIndex, - trailingAtBottom: true, - trailing: Padding( - padding: .symmetric(vertical: 16), - child: Column( - spacing: 8, - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (_) => JoinDialog(ref), - ), - child: ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - PopupMenuItem( - onTap: null, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: Icon(Icons.add), - ), - IconButton( - tooltip: "Explore other rooms", - onPressed: null, - icon: Icon(Icons.explore), - ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: Icon(Icons.settings), + 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(), + ), + ); + }, + icon: Icon(Icons.notifications), + ), + IconButton( + tooltip: "Open settings", + onPressed: () => showDialog( + context: context, + builder: (_) => SettingsPage(), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), ), ), ), ), ), + Expanded( child: Scaffold( backgroundColor: Colors.transparent, @@ -219,67 +244,75 @@ class Sidebar extends HookConsumerWidget { ), ], ), - body: Theme( - data: Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, + body: MaterialUiCompatibilityBridge( + child: Builder( + builder: (context) => old_mat.Theme( + data: old_mat.Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemExpandedHeight: 48, + iconLabelGap: 16, + ), + ], ), - ], - ), - child: NavigationRailM3E( - expandedWidth: double.infinity, - scrollable: true, - background: Colors.transparent, - type: .alwaysExpand, - selectedIndex: 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, ), ), - 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(); - }, + for (final subSpace in selectedSpace.subSpaces) + .new( + header: DividerWidget( + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + if (subSpace.room.metadata?.avatar != null) + AvatarOrHash( + subSpace.room.metadata?.avatar, + subSpace.room.metadata?.name ?? + "Unnamed Room", + height: 16, + ), + Flexible( + child: Text( + subSpace.room.metadata?.name ?? + "Unnamed Space", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + destinations: roomsToDestinations( + subSpace.children, + ), + ), + ], + onDestinationSelected: (value) { + final children = selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ); + selectedRoomIdNotifier.set( + children[value].metadata?.id, // + ); + if (!isDesktop) Navigator.of(context).pop(); + }, + ), + ), ), ), ), diff --git a/lib/widgets/timestamp.dart b/lib/widgets/timestamp.dart new file mode 100644 index 0000000..bcdabd1 --- /dev/null +++ b/lib/widgets/timestamp.dart @@ -0,0 +1,17 @@ +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 3a20e19..5eda427 100644 --- a/lib/widgets/url_preview.dart +++ b/lib/widgets/url_preview.dart @@ -1,14 +1,11 @@ -import "package:flutter/material.dart"; +import "package:material_ui/material_ui.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 UrlPreview extends ConsumerWidget { - final Uri link; - const UrlPreview(this.link, {super.key}); - +class const UrlPreview(final Uri link, {super.key}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => ConstrainedBox( constraints: .loose(.fromWidth(400)), @@ -21,9 +18,9 @@ class UrlPreview 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( @@ -34,9 +31,15 @@ class UrlPreview extends ConsumerWidget { Text( preview.title!, style: Theme.of(context).textTheme.titleLarge, + maxLines: 3, + overflow: .ellipsis, ), if (preview.description != null) ...[ - Text(preview.description!), + Text( + preview.description!, + maxLines: 20, + overflow: .ellipsis, + ), SizedBox(height: 4), ], if (preview.imageUrl != null) diff --git a/lib/widgets/user_bottom_sheet.dart b/lib/widgets/user_bottom_sheet.dart index 15a24a0..9fbdf35 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:flutter/material.dart"; +import "package:material_ui/material_ui.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"; -class UserBottomSheet extends ConsumerWidget { - final MembershipContent member; - final String userId; - final String? roomId; - const UserBottomSheet(this.member, this.userId, {this.roomId, super.key}); - +final class const UserBottomSheet( + final MembershipContent member, + final String userId, { + final String? roomId, + super.key, +}) extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final textTheme = theme.textTheme; - final client = ref.watch(ClientController.provider.notifier); + final client = ref.read(ClientController.provider.notifier); void showMembershipDialog(MembershipAction action) => showDialog( context: context, @@ -200,36 +200,35 @@ class UserBottomSheet extends ConsumerWidget { ), if (ref.watch( - PowerLevelController.provider( - .membershipAction( - action: .kick, - roomId: roomId!, - targetUser: userId, - ), - ), - ) && - member.status == .join || - member.status == .invite) + PowerLevelController.provider( + .membershipAction( + action: .kick, + roomId: roomId!, + targetUser: userId, + ), + ), + )) Padding( padding: .only(top: 4), child: Row( mainAxisSize: MainAxisSize.max, spacing: 8, children: [ - M3EButton.icon( - onPressed: () => showMembershipDialog(.kick), - shape: .square, - icon: Icon(Icons.sports_martial_arts), - label: Text("Kick"), - decoration: .new( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.error, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onError, + if (member.status == .join || member.status == .invite) + M3EButton.icon( + onPressed: () => showMembershipDialog(.kick), + shape: .square, + icon: Icon(Icons.sports_martial_arts), + label: Text("Kick"), + decoration: .new( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.error, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onError, + ), ), ), - ), M3EButton.icon( onPressed: () => showMembershipDialog(.ban), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 603ea6a..70d9ba2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -37,6 +38,9 @@ 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 7d8f046..1d53ad7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ 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 new file mode 100644 index 0000000..663ab9b --- /dev/null +++ b/linux/nexus.federated.nexus.UnifiedPush.service @@ -0,0 +1,3 @@ +[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 d61734e..64b4264 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 %u +Exec=nexus Icon=nexus Terminal=false Type=Application Categories=Chat;Network;InstantMessaging; -MimeType=x-scheme-handler/nexus.federated.nexus; \ No newline at end of file +DBusActivatable=true \ No newline at end of file diff --git a/linux/nexus.federated.nexus.service b/linux/nexus.federated.nexus.service new file mode 100644 index 0000000..1757c2c --- /dev/null +++ b/linux/nexus.federated.nexus.service @@ -0,0 +1,3 @@ +[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 new file mode 100644 index 0000000..10f495f --- /dev/null +++ b/linux/nexus.federated.nexus.uri.desktop @@ -0,0 +1,8 @@ +[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 new file mode 100644 index 0000000..756eb0b --- /dev/null +++ b/linux/nix/default.nix @@ -0,0 +1,59 @@ +{ 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 812082c..8bc8641 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 = "36.0.1"; + platformToolsVersion = "37.0.1"; buildToolsVersions = [ "35.0.0" "36.0.0" @@ -23,7 +23,7 @@ pkgs.mkShell { [ go git - jdk17 + libGL (flutter.override { extraPkgConfigPackages = [ @@ -40,13 +40,11 @@ pkgs.mkShell { ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk"; ANDROID_SDK_ROOT = ANDROID_HOME; - JAVA_HOME = pkgs.jdk17; + JAVA_HOME = pkgs.jdk21; - TOOLS = "${ANDROID_HOME}/build-tools/${"36.0.0"}"; - GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${TOOLS}/aapt2"; + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/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 new file mode 100644 index 0000000..a8abd6c --- /dev/null +++ b/linux/nix/module.nix @@ -0,0 +1,45 @@ +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 c026606..3e55dfe 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -32,11 +32,15 @@ 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/icon.svg $out/share/icons/hicolor/scalable/apps/nexus.svg - install -Dm755 linux/nexus.federated.nexus.desktop -t $out/share/applications + 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 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 ffa9d7c..64e0faa 100644 --- a/linux/nix/pkg/gomuks.nix +++ b/linux/nix/pkg/gomuks.nix @@ -17,7 +17,7 @@ buildGoModule ( src = "${src}/gomuks"; - vendorHash = "sha256-C03ss88QAnmZu+XxoHhc1M/261xFl8hR/pzLbU37Qe8="; + vendorHash = "sha256-rLLDbNrYf5HqG3Y6zihRi1v+GqGeNAtZUL+8QHdCz7w="; buildPhase = '' runHook preBuild diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 62e5450..bf6e5ef 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -1,6 +1,11 @@ #include "my_application.h" +#include + #include +#include +#include + #ifdef GDK_WINDOWING_X11 #include #endif @@ -9,144 +14,384 @@ 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) -// 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 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); } -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); +static void notification_event_action( + GSimpleAction* action, + GVariant* parameter, + gpointer user_data) { + MyApplication* self = + MY_APPLICATION(user_data); - GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); - if (windows) { - gtk_window_present(GTK_WINDOW(windows->data)); + 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)); 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); + 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)); - 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)); - // 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)); + gdk_rgba_parse( + &background_color, + "#000000"); - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + fl_view_set_background_color( + view, + &background_color); - gtk_widget_grab_focus(GTK_WIDGET(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)); } -// 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); +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); 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; } -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); +static void my_application_startup( + GApplication* application) { + g_message( + "GApplication application-id: %s", + g_application_get_application_id( + application)); - // Perform any actions required at application startup. + g_message( + "GApplication is remote: %d", + g_application_get_is_remote( + application)); - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); + 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); } -// 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_shutdown( + GApplication* application) { + G_APPLICATION_CLASS( + my_application_parent_class) + ->shutdown(application); } -// 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_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); } -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) {} +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); +} 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)); -} + 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 diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f18d831..8b6bbb6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ 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 @@ -15,12 +16,14 @@ 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")) @@ -28,5 +31,6 @@ 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 new file mode 100644 index 0000000..65543e2 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..08cb773 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - FlutterMacOS (1.0.0) + - media_kit_libs_macos_video (1.0.4): + - FlutterMacOS + - media_kit_video (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) + - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + media_kit_libs_macos_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos + media_kit_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 + media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.17.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f87523c..fa18d44 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,9 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 34AEA7295333B7F48CD3BE4C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + BAF79690465DBAC1A1E19D28 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +63,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 18EDDEF1FB58EC56ABDA1BEB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* nexus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nexus.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* nexus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = nexus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +80,16 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5099E6191AE87E21D356D9C8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 6D9F8F8BBED102075ECF1DF9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8F5ECEC7BF9DD35CB3E7196E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9DDE815F827B87E2C55DF94C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + CBD1D9203BEE9731F981EEF8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 34AEA7295333B7F48CD3BE4C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +105,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + BAF79690465DBAC1A1E19D28 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +140,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + FC780AF47288088B9004ACE2 /* Pods */, ); sourceTree = ""; }; @@ -151,6 +167,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -175,10 +192,26 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 70FD05041ACDA7E801BB9823 /* Pods_Runner.framework */, + D76F30AE9CCFCA04C354BD4C /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; + FC780AF47288088B9004ACE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 6D9F8F8BBED102075ECF1DF9 /* Pods-Runner.debug.xcconfig */, + 9DDE815F827B87E2C55DF94C /* Pods-Runner.release.xcconfig */, + CBD1D9203BEE9731F981EEF8 /* Pods-Runner.profile.xcconfig */, + 18EDDEF1FB58EC56ABDA1BEB /* Pods-RunnerTests.debug.xcconfig */, + 8F5ECEC7BF9DD35CB3E7196E /* Pods-RunnerTests.release.xcconfig */, + 5099E6191AE87E21D356D9C8 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -186,6 +219,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + C413A8A75594D2165C9D56BD /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +238,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 446C82D5FE82BD05F78EDA6A /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + C4D158541D7059C07D3CA9B3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -216,6 +252,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* nexus.app */; productType = "com.apple.product-type.application"; @@ -260,6 +299,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -329,6 +371,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 446C82D5FE82BD05F78EDA6A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C413A8A75594D2165C9D56BD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C4D158541D7059C07D3CA9B3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +483,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 18EDDEF1FB58EC56ABDA1BEB /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +498,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8F5ECEC7BF9DD35CB3E7196E /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +513,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5099E6191AE87E21D356D9C8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -461,7 +567,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -543,7 +649,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -593,7 +699,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -700,6 +806,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ff47eeb..746b383 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + + diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f..96d3fee 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "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" - } -} + "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 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 82b6f9d..1682af0 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 13b35eb..c2e488b 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 0a3f5fa..e2bf256 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 bdb5722..983d7fa 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 f083318..c8f0676 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 326c0e7..468be1f 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 2f1632c..a77207d 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 c989ed4..51e0bd6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c + sha256: fdcd9f70f9eb80df3bc5ed0fa67280df2595fd481948df8a9ab082e6a40ad04b url: "https://pub.dev" source: hosted - version: "99.0.0" + version: "108.0.0" analysis_server_plugin: dependency: transitive description: name: analysis_server_plugin - sha256: "3960b28ee740004df39f85d5ebfc91785f7a90e51fd7c9a185e86a36b2f581b4" + sha256: "3460f040e1f77647fcd82db9bd3bb117de04d04a3d67c39ad361794331147a4e" url: "https://pub.dev" source: hosted - version: "0.3.14" + version: "0.3.23" analyzer: dependency: transitive description: name: analyzer - sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" + sha256: a51c769bff3b6dfbe9d60199b8606d808290702a296bef0c26a4ca391d414e46 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "14.4.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" + sha256: f890a2e708e38fbf4238ad2f9fd3d506538acdddb0cd8eb2c3e8c7bd17784b57 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.4" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "0057a98d64d7bb872b0c87dff6e73d2c2d80c77156e7a03f127a26f8aa240649" + sha256: "9a3518eb84e8e26665874d9867534961d855493e9c5eb0eb55cbd46edd995529" url: "https://pub.dev" source: hosted - version: "0.14.8" + version: "0.14.17" app_links: dependency: "direct main" description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + sha256: "6c5bcd986e06b94e3c40244af471750840a3d2341d1f9763a1100a14add517b4" url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.3.0" args: dependency: transitive description: @@ -89,6 +89,14 @@ 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: @@ -109,34 +117,34 @@ packages: dependency: transitive description: name: build - sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8" url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.11" build_config: dependency: transitive description: name: build_config - sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" build_daemon: dependency: transitive description: name: build_daemon - sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + sha256: e1d40ef3f7934986d5da2271b1ba07794921ce263e44d622fb6c406d76589e33 url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.1.6" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + sha256: "894c243f6bc32015fec466ce30a6925bd537a77a426ee5bf481120477eb3de67" url: "https://pub.dev" source: hosted - version: "2.15.1" + version: "2.16.1" built_collection: dependency: transitive description: @@ -149,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 url: "https://pub.dev" source: hosted - version: "8.12.6" + version: "8.13.0" button_m3e: dependency: transitive description: @@ -169,14 +177,6 @@ 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,14 +185,6 @@ 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: @@ -205,18 +197,18 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" code_assets: dependency: "direct main" description: name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + sha256: "828110d598123b5ea96c00c9f3c72105bf79f8ee36c20a39b26209ade421ec57" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.0" collection: dependency: "direct main" description: @@ -241,22 +233,14 @@ 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: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 url: "https://pub.dev" source: hosted - version: "0.3.5+4" + version: "0.3.5+5" crypto: dependency: transitive description: @@ -273,47 +257,54 @@ 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: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 + sha256: "82ade9fc4273f29ed673e33166944465225b4f7fc5d4aaef48605cc751c18fc1" url: "https://pub.dev" source: hosted - version: "3.1.8" + version: "3.1.13" dbus: dependency: transitive description: name: dbus - sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 url: "https://pub.dev" source: hosted - version: "0.7.14" + version: "0.7.15" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + sha256: "869b3bce0100eb519768ecd71c0111f0b5ea679f5f62e1f59c49d80067e730c5" url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "2.1.0" dynamic_polls: dependency: "direct main" description: name: dynamic_polls - sha256: "72ff19cdf041ad8dcfa76adaebb216d005f40b278d955e6e0c7bcb769215fabe" + sha256: "58cd1fafbbaba6b485a61692aa3bd33ded6f0f61734543b6c754511c362d8ae1" url: "https://pub.dev" source: hosted - version: "0.0.7" - emoji_text_field: - dependency: "direct main" + version: "0.0.8" + encrypt: + dependency: transitive description: - path: "." - ref: HEAD - resolved-ref: "5f7baaf8a6f059ec3ab8ff0f5d02339b00bf6997" - url: "https://github.com/Henry-Hiles/emoji_text_field" - source: git - version: "1.0.0" + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" equatable: dependency: transitive description: @@ -366,10 +357,10 @@ packages: dependency: "direct main" description: name: ffigen - sha256: b7803707faeec4ce3c1b0c2274906504b796e3b70ad573577e72333bd1c9b3ba + sha256: "31b2ca630cede89babbbf31688d20b735967c51bf60572b06ec16718e5a7f1ec" url: "https://pub.dev" source: hosted - version: "20.1.1" + version: "22.0.0" file: dependency: transitive description: @@ -390,34 +381,34 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b" + sha256: d670cd0ce77a2e785b18d8b4d0a8d6a222d6a813ec9b7ddf2790a1b4fb6fa92c url: "https://pub.dev" source: hosted - version: "0.5.2+8" + version: "0.5.2+11" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca + sha256: "97269e5307a0ab813b1fa2430bada0a96e0afb74848417f8676f64ba5de0051c" url: "https://pub.dev" source: hosted - version: "0.5.3+5" + version: "0.5.3+6" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4 url: "https://pub.dev" source: hosted - version: "0.9.5" + version: "0.9.5+1" file_selector_platform_interface: dependency: transitive description: @@ -438,10 +429,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec url: "https://pub.dev" source: hosted - version: "0.9.3+5" + version: "0.9.3+6" fixnum: dependency: transitive description: @@ -495,6 +486,46 @@ 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 @@ -512,10 +543,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" + sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.3" flutter_svg: dependency: "direct main" description: @@ -538,10 +569,10 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html_core - sha256: "7ff010b116f6abc16429923e616fbc727f3f65ef4cee12ffdb280aeecbc21e7f" + sha256: "530f2e3cc57be1a00f8bfae1b0000064e8a6df0cf1fb4d48a80f85afde9b39b7" url: "https://pub.dev" source: hosted - version: "0.17.2" + version: "0.17.4" fluttertagger: dependency: "direct main" description: @@ -554,10 +585,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "8599ba37236328ff6f97269ecce875d251a367eb2929c9bbf9b811b2ce56c74e" + sha256: "3af98c92d7d4dd182ea1393193e69a840fcbaee1f66f1158bd20504e5f3f6f4f" url: "https://pub.dev" source: hosted - version: "3.2.6-dev.1" + version: "4.0.2" freezed_annotation: dependency: "direct main" description: @@ -566,30 +597,22 @@ 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: "69e4412dd70e25a4991623c10bf72e3b12106f2cb4353a2d167353947597f3aa" + sha256: "597cd8f6ef8b01c747a6d95ff7b0310066f6c82ddf5565ca371f047f316fe43e" url: "https://pub.dev" source: hosted - version: "0.0.9" + version: "0.2.0" glob: dependency: transitive description: name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" graphs: dependency: transitive description: @@ -610,26 +633,26 @@ packages: dependency: "direct main" description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - sha256: dcaf59f0f41489ff08ce61438ada564d67f40cfa37cc7c8589da78fb600c2edc + sha256: d58e1e14aa112ca9c56d54effb7d0d717b2d3f96dad3cdd1fce16f8740450700 url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.3" html: dependency: transitive description: name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" url: "https://pub.dev" source: hosted - version: "0.15.6" + version: "0.15.7" http: dependency: "direct main" description: @@ -666,10 +689,10 @@ packages: dependency: transitive description: name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + sha256: a1e7f4951e538a568e14b856702afc9ae1d2f4b202daced8d22c1b9cd211ce89 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.10.1" image_picker: dependency: "direct main" description: @@ -682,10 +705,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + sha256: "0a55d645a670d6ae11efa948f955ae904afa472f1344fc15a7ddacc20c1e7219" url: "https://pub.dev" source: hosted - version: "0.8.13+19" + version: "0.8.13+23" image_picker_for_web: dependency: transitive description: @@ -698,10 +721,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: "852a7762962f3bb3ad54ef1d342669e41557f466033a676baa8876b367cf4050" url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+8" image_picker_linux: dependency: transitive description: @@ -738,18 +761,26 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.1.0" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -762,10 +793,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 + sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d url: "https://pub.dev" source: hosted - version: "6.14.0" + version: "6.14.1" leak_tracker: dependency: transitive description: @@ -807,6 +838,14 @@ 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: @@ -819,18 +858,18 @@ packages: dependency: "direct main" description: name: m3e_buttons - sha256: af1b19bd5ec7327d20b5a57c6c91e1ed30ee15a4dbade781c0cdbc72e6b04b5e + sha256: dca780e813563754576194ed18e3f6a33422ed136c8d4ccfeca20909edddd728 url: "https://pub.dev" source: hosted - version: "0.0.4" + version: "1.0.3" m3e_card_list: dependency: "direct main" description: name: m3e_card_list - sha256: d4aba0123cccda40ac80789befa8d355e1dc16aa7dcee910157690b0546d78d6 + sha256: ed1cc150f051b8812ffd735915ae59fed23517ffeeed04c4b2b8ef6257cc18b7 url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "1.0.0" m3e_design: dependency: transitive description: @@ -843,10 +882,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -855,6 +894,22 @@ 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: @@ -931,18 +986,18 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" motor: dependency: transitive description: @@ -951,39 +1006,39 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "90eaad8059ebc2004a63810a4696ea9acdb02473e47ed3b6b3bafb100ac77e31" + url: "https://pub.dev" + source: hosted + version: "0.19.5" navigation_rail_m3e: dependency: "direct main" description: path: "packages/navigation_rail_m3e" ref: HEAD - resolved-ref: "667b0bc8526fd53296778903b6ef3f22424f3aa4" + resolved-ref: da860a413d5cf78b9c9c967215250bcdbfe5191d 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: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 url: "https://pub.dev" source: hosted - version: "9.4.1" + version: "9.6.0" package_config: dependency: transitive description: name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.0" package_info_plus: dependency: "direct main" description: @@ -1076,10 +1131,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.2.0" plugin_platform_interface: dependency: transitive description: @@ -1088,38 +1143,54 @@ 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: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.5.3" posix: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e url: "https://pub.dev" source: hosted - version: "6.5.0" + 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: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: c38b81cbf34450b67e0265d73433569d12e34782e30ed769c9cc99c9d5f2e796 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" quiver: dependency: transitive description: @@ -1132,34 +1203,34 @@ packages: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "1.1.1" riverpod: dependency: transitive description: name: riverpod - sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" + sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.3" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "3e275138862ccc22ed61444a1f9a840f753094c367f28f4123f50289cd204d68" + sha256: bb13cb3985815840cc103eda1027158358c5fdc00f308fc6f76574d0594107af url: "https://pub.dev" source: hosted - version: "1.0.0-dev.10" + version: "1.0.0-dev.12" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "166f29492228dc471b6fe294092560ccd5545ed15c9fc155622455b58b2451a9" + sha256: "215cb3cd54a97e09bc3512535fa4e8bedd3bf2a06dc2ae015ac8e0050fc71bd1" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.9" rxdart: dependency: transitive description: @@ -1228,18 +1299,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399" url: "https://pub.dev" source: hosted - version: "2.4.27" + version: "2.4.28" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.7" shared_preferences_linux: dependency: transitive description: @@ -1280,22 +1351,6 @@ 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: @@ -1313,34 +1368,18 @@ packages: dependency: transitive description: name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + sha256: "11b6047da8e4eb6c643ccac3d0fda3f9c9e11651695f7da24a85ade8aff15a44" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" source_helper: dependency: transitive description: name: source_helper - sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" + sha256: "5e6f216fdf6376c9f3852381ae037499797a3385377d388b011dac98d303c67c" url: "https://pub.dev" source: hosted - version: "1.3.12" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" + version: "1.3.13" source_span: dependency: transitive description: @@ -1353,10 +1392,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.2" state_notifier: dependency: transitive description: @@ -1377,10 +1416,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1401,10 +1440,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + sha256: "397b146f97613b83d84bdccb439bc8d0f3aebb6c1d14a9641e6f24201c490b2d" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.2" term_glyph: dependency: transitive description: @@ -1413,30 +1452,14 @@ 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: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" - test_core: - dependency: transitive - description: - name: test_core - sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" - url: "https://pub.dev" - source: hosted - version: "0.6.17" + version: "0.7.12" timeago: dependency: "direct main" description: @@ -1445,6 +1468,14 @@ 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: @@ -1453,22 +1484,56 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - universal_html: - dependency: transitive + unifiedpush: + dependency: "direct main" description: - name: universal_html - sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 + name: unifiedpush + sha256: "8ed9767f750a1dc6159a77e2171641d0cb825dc87682d1ce1b8618689b79f58e" url: "https://pub.dev" source: hosted - version: "2.3.0" - universal_io: + version: "6.2.0" + unifiedpush_android: dependency: transitive description: - name: universal_io - sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + name: unifiedpush_android + sha256: a902e505e91c8e28b54b379bb27f255f66df409274a97af24d6719f12f0d13dc url: "https://pub.dev" source: hosted - version: "2.3.1" + 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: + dependency: transitive + description: + name: unifiedpush_storage_interface + sha256: b8d423a4695efc616aa21d8ab48fb5ef99d6288c68b56282b8faac1579ceabd9 + 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" universal_platform: dependency: transitive description: @@ -1497,34 +1562,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" url: "https://pub.dev" source: hosted - version: "6.3.32" + version: "6.3.33" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.4.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.6" url_launcher_platform_interface: dependency: transitive description: @@ -1545,10 +1610,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" uuid: dependency: transitive description: @@ -1561,10 +1626,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + sha256: "9d0e3b9cb16542ad660daee871e726a10d13a93b7b5391677c3160e8f5e83935" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" vector_graphics_codec: dependency: transitive description: @@ -1577,42 +1642,42 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" + sha256: "4dca4feb77dc3ec7f6e27e49c53241eb8217f55e4f9b12599a27f8903bca5682" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "92b9910f66ed1057fd4da7b040ae7c74cafacf885bdc81be496928d5049b032d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.3" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "15.3.0" wakelock_plus: dependency: transitive description: name: wakelock_plus - sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53" + sha256: "22b3e7e937721de70e63c85e7139f4ac781dc22863b9262431a53ac030eb074b" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.8.0" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + sha256: "764c25504562abc8ac3406f5d175f126b276e6ba916c7c2675690b73368ca384" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.7.0" watcher: dependency: transitive description: @@ -1645,22 +1710,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive + webcrypto: + dependency: "direct dev" description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + name: webcrypto + sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "0.6.0" + webpush_encryption: + dependency: transitive + description: + name: webpush_encryption + sha256: "5b83272b91acda6ae515fcd980c94f06bf413702282497c5a68f5dfc64fed27f" + url: "https://pub.dev" + source: hosted + version: "1.0.1" win32: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" window_manager: dependency: "direct main" description: @@ -1669,6 +1742,15 @@ 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: @@ -1681,18 +1763,18 @@ packages: dependency: transitive description: name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" url: "https://pub.dev" source: hosted - version: "6.6.1" + version: "7.0.1" yaml: dependency: transitive description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" yaml_edit: dependency: transitive description: @@ -1702,5 +1784,5 @@ packages: source: hosted version: "2.2.4" sdks: - dart: ">=3.12.2 <4.0.0" - flutter: ">=3.44.0" + dart: "3.13.3" + flutter: ">=3.47.0" diff --git a/pubspec.yaml b/pubspec.yaml index 72748b1..2535e0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,98 +1,119 @@ name: nexus -description: "Yet another Matrix client" +description: "A simple and user-friendly Matrix client" version: 0.1.0 publish_to: none flutter: - fonts: - - family: fallback-emoji - fonts: - - asset: assets/fonts/NotoColorEmoji.ttf - - family: fallback-sans - fonts: - - asset: assets/fonts/Roboto.ttf - assets: - - assets/ - uses-material-design: true + fonts: + - family: fallback-emoji + fonts: + - asset: assets/fonts/NotoColorEmoji.ttf + - family: fallback-sans + fonts: + - asset: assets/fonts/Roboto.ttf + assets: + - assets/bundled/ + uses-material-design: true environment: - sdk: "^3.12.2" + sdk: 3.13.3 dependency_overrides: - path_provider_android: 2.2.23 # Pinned to avoid JNI - linkify: - git: - url: https://github.com/appelladev/linkify - ref: fix/consecutive-periods-loose-url + dynamic_color: 2.1.0 + unifiedpush_linux: + git: + url: https://codeberg.org/Henry-Hiles/flutter-connector + ref: quad/feat/should-write-service-option + path: unifiedpush_linux + unifiedpush_platform_interface: + git: + url: https://codeberg.org/Henry-Hiles/flutter-connector + ref: quad/feat/should-write-service-option + path: unifiedpush_platform_interface + path_provider_android: 2.2.23 # Pinned to avoid JNI + linkify: + git: + url: https://github.com/appelladev/linkify + ref: fix/consecutive-periods-loose-url dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - flutter_riverpod: 3.3.2 - hooks_riverpod: 3.3.2 - intl: 0.20.2 - fast_immutable_collections: 11.2.0 - path_provider: 2.1.6 - url_launcher: 6.3.2 - freezed_annotation: 3.1.0 - image_picker: ^1.2.3 - path: 1.9.1 - dynamic_color: 1.8.1 - collection: 1.19.1 - window_manager: 0.5.2 - color_hash: 1.0.1 - flutter_widget_from_html_core: 0.17.2 - flutter_svg: 2.3.0 - json_annotation: 4.12.0 - shared_preferences: 2.5.5 - fluttertagger: 2.3.2 - dynamic_polls: 0.0.7 - flutter_hooks: 0.21.3+1 - ffi: 2.2.0 - hooks: 2.0.2 - code_assets: 1.2.1 - ffigen: 20.1.1 - timeago: 3.7.1 - http: 1.6.0 - flutter_linkify: 6.0.0 - linkify: 5.0.0 - emoji_text_field: - git: - url: https://github.com/Henry-Hiles/emoji_text_field - flutter_blurhash: 0.9.1 - super_sliver_list: 0.4.1 - media_kit: 1.2.6 - media_kit_video: 2.0.1 - media_kit_libs_video: 1.0.7 - measure_size: 5.0.2 - m3e_buttons: 0.0.4 - navigation_rail_m3e: - git: - url: https://github.com/Henry-Hiles/material_3_expressive - path: packages/navigation_rail_m3e - m3e_card_list: 0.1.0 - xdg_directories: 1.1.0 - package_info_plus: 10.2.1 - app_links: 7.2.1 - file_selector: ^1.1.0 - + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_riverpod: 3.4.3 + hooks_riverpod: 3.4.3 + intl: 0.20.3 + fast_immutable_collections: 11.2.0 + path_provider: 2.1.6 + url_launcher: 6.3.2 + freezed_annotation: 3.1.0 + image_picker: 1.2.3 + path: 1.9.1 + dynamic_color: 2.1.0 + collection: 1.19.1 + window_manager: 0.5.2 + color_hash: 1.0.1 + flutter_widget_from_html_core: 0.17.4 + flutter_svg: 2.3.0 + json_annotation: 4.12.0 + shared_preferences: 2.5.5 + fluttertagger: 2.3.2 + dynamic_polls: 0.0.8 + flutter_hooks: 0.21.3+1 + ffi: 2.2.0 + hooks: 2.2.0 + code_assets: 2.1.0 + ffigen: 22.0.0 + timeago: 3.7.1 + http: 1.6.0 + flutter_linkify: 6.0.0 + linkify: 5.0.0 + flutter_blurhash: 0.9.1 + super_sliver_list: 0.4.1 + media_kit: 1.2.6 + media_kit_video: 2.0.1 + media_kit_libs_video: 1.0.7 + measure_size: 5.0.2 + m3e_buttons: 1.0.3 + navigation_rail_m3e: + git: + url: https://github.com/Henry-Hiles/material_3_expressive + path: packages/navigation_rail_m3e + m3e_card_list: 1.0.0 + xdg_directories: 1.1.0 + package_info_plus: 10.2.1 + app_links: 7.2.1 + file_selector: 1.1.0 + material_ui: 1.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 + dev_dependencies: - build_runner: ^2.15.1 - flutter_lints: 6.0.0 - freezed: 3.2.6-dev.1 - riverpod_lint: 3.1.4 - flutter_launcher_icons: 0.14.4 - json_serializable: 6.14.0 + build_runner: 2.16.1 + flutter_lints: 6.0.0 + freezed: 4.0.2 + riverpod_lint: 3.1.9 + flutter_launcher_icons: 0.14.4 + json_serializable: 6.14.1 + # Transitive dep but needed for commands + webcrypto: 0.6.0 flutter_launcher_icons: - ios: true - android: true - image_path: assets/icon.png - adaptive_icon_background: assets/background.png - adaptive_icon_foreground: assets/foreground.png - adaptive_icon_monochrome: assets/monochrome.png - remove_alpha_ios: true - windows: - generate: true \ No newline at end of file + image_path: assets/icon.png + ios: true + image_path_ios: assets/mobile.png + android: true + adaptive_icon_background: assets/background.png + adaptive_icon_foreground: assets/foreground.png + adaptive_icon_monochrome: assets/monochrome.png + windows: + generate: true + macos: + generate: true + image_path: assets/mobile.png diff --git a/scripts/generate.dart b/scripts/generate.dart index c381230..d8a8cb1 100644 --- a/scripts/generate.dart +++ b/scripts/generate.dart @@ -1,4 +1,5 @@ import "dart:io"; + import "package:ffigen/ffigen.dart"; import "package:path/path.dart"; import "package:nexus/helpers/extensions/get_xcode_sdk.dart"; @@ -6,21 +7,23 @@ 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: Output( - dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), + output: .new( + dart: .new( + path: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), + ), ), - headers: Headers( + visitors: [.new(func: (node) => node.isIncluded = true)], + input: .new( entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri], compilerOptions: [ "--no-warnings", - if (Platform.isMacOS) "-I${await getXCodeSDK()}/usr/include", + if (Platform.isMacOS) "-I${await getXCodeTool()}/usr/include", ], ), - functions: Functions.includeAll, ).generate( libclangDylib: libclangPath == null ? null @@ -37,5 +40,5 @@ void main(List args) async { ), ), ); - print("Done!"); + print("FFI bindings generated!"); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0694802..5d2b53e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -30,6 +31,8 @@ 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 238ef93..c74b2f5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,10 +10,12 @@ 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)