Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e300f4efc9 |
|||
|
bab699658f |
|||
|
1c1f585362 |
|||
|
c90a1c41b0 |
|||
|
9f070fcdba |
|||
|
d712bb3193 |
|||
|
3217c17ea0 |
|||
|
031fbe326c |
|||
|
0149aa2f84 |
|||
|
a1efdb0f46 |
|||
| 389b65ae61 | |||
|
3429a3ed98 |
|||
|
7ab92f76bf |
|||
|
aecfac9533 |
|||
|
c7b89b8e56 |
|||
|
721727d48c |
|||
|
004c3bcd0d |
|||
|
943cd85e22 |
|||
|
09904536e7 |
|||
|
856072e0b0 |
|||
|
110caff170 |
|||
|
eeb2e2f54b |
|||
|
529a2e35c4 |
|||
|
6ad4657d76 |
|||
|
38fd19ad1a |
|||
|
0636366359 |
|||
|
efe9262a97 |
|||
|
2763a0fdf5 |
|||
|
a5da44fdea |
|||
|
ccca02a0bc |
|||
|
721349ca21 |
|||
|
5d994bf2bb |
|||
|
a90bf00f18 |
|||
|
f2efbbeb9f |
|||
|
ed761d6071 |
|||
|
8f35ea2435 |
|||
|
023e75e063 |
|||
|
4d0a4605d6 |
|||
|
faa7292001 |
|||
|
027033ad9e |
|||
|
ece3d16318 |
|||
|
5738239da2 |
|||
|
a2791a6717 |
|||
|
f910d87a23 |
|||
|
f4142859ba |
|||
|
775c77194b |
|||
|
41806be9bf |
|||
|
6e47594a91 |
|||
|
386b3ad1cf |
|||
|
61f2aa7f2a |
|||
|
0c660a98a7 |
|||
|
09534a6775 |
|||
|
0cd815dc79 |
|||
|
800ed0636d |
|||
|
e4cf2a837c |
|||
|
11dca9bf14 |
|||
|
436b8642b3 |
|||
|
269d97be8f |
|||
|
43f70f35a3 |
|||
|
92b148f5b6 |
|||
|
00a42b5f1a |
|||
|
700b4e720f |
|||
|
2a11bcb7e8 |
|||
|
fea39e4191 |
|||
|
deaf564c36 |
|||
|
c0d14ac30e |
|||
|
1a6a5c36ea |
|||
|
35ef82accd |
|||
|
f40dd22277 |
|||
|
1cfea5a819 |
|||
|
b3023d8147 |
|||
|
e8fb3a8523 |
|||
|
0d1ace3fd0 |
|||
|
3d690c26d8 |
|||
|
3fee789c90 |
|||
|
d7c64fea80 |
|||
|
87e7e093f7 |
|||
|
8b7e29ae33 |
|||
|
673c5bd894 |
|||
|
650c62fb99 |
|||
|
4f3b77f9f3 |
|||
|
85bb653980 |
|||
|
a9d2ccbcd1 |
|||
|
73c4a7d07f |
|||
|
57f9498aa3 |
|||
|
cfab803fa0 |
|||
|
f23a35384d |
|||
|
7f7fb451e6 |
|||
|
5d62e66cd9 |
|||
|
009ba504e3 |
|||
|
30b922d390 |
|||
|
63183e0824 |
|||
|
163947fc90 |
|||
|
11457470a0 |
|||
|
dca0b7dd7d |
|||
|
e5dc270f83 |
|||
|
810a0b102b |
|||
|
ec1603b828 |
|||
| 5acdbb5be0 | |||
|
e21c9e8c22 |
|||
|
ea687471bc |
|||
|
8c440447f2 |
|||
|
8ebec15324 |
|||
|
cd8d551e26 |
|||
|
118e5a5826 |
|||
|
6ca9b2a948 |
|||
|
23e01c7a0d |
|||
|
9e7fcdbef7 |
|||
|
9507d69458 |
|||
|
13d90df226 |
|||
|
e925f9657c |
|||
|
b8882d6e48 |
|||
| ff9bf3bec4 | |||
|
6e42902322 |
|||
|
c105a6035c |
|||
|
3d7ef9b32f |
|||
|
51ef66174a |
|||
|
1efb68ebe0 |
|||
|
0a667fda0b |
|||
|
da352cd035 |
|||
|
0201f71e5d |
|||
|
4314c88d5c |
|||
|
88cdcf9863 |
|||
|
e2f25749e0 |
|||
|
47a3d9d92f |
|||
|
a680c5cd0c |
|||
|
0887421f3e |
|||
|
a090d88a23 |
|||
|
32693fb3b5 |
|||
|
4e1f1b64bc |
|||
|
20f1c784f6 |
|||
|
9b79304edc |
|||
|
b797b8de9c |
|||
|
56bb75866c |
|||
|
3baea40669 |
|||
|
2e72ecd559 |
|||
| 077044ec19 | |||
|
01b13b4764 |
|||
|
050e42cc6f |
|||
|
67cee201b6 |
|||
|
2d7b067c93 |
|||
|
a9993013f5 |
|||
|
9c488e8981 |
|||
|
5c16f42d20 |
|||
|
092fc616e6 |
|||
|
6cbf78be6b |
|||
|
68f4bb0c03 |
|||
|
9e24f4cb31 |
|||
|
43d2e94d62 |
|||
|
62fe37c6ae |
|||
|
b9dfbd3ca2 |
|||
|
620440e2a1 |
2
.github/workflows/android.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }}
|
KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }}
|
||||||
|
|
||||||
- name: Build app
|
- 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:
|
env:
|
||||||
KEYSTORE_PATH: ../../keystore.jks
|
KEYSTORE_PATH: ../../keystore.jks
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
|
|
||||||
2
.github/workflows/ios.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.44.4
|
flutter-version: 3.47.4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
|
|
|
||||||
17
.github/workflows/macos.yml
vendored
|
|
@ -19,22 +19,29 @@ jobs:
|
||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.44.4
|
flutter-version: 3.47.4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: gomuks/go.mod
|
go-version-file: gomuks/go.mod
|
||||||
|
|
||||||
- name: Build app
|
- name: Build App
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
flutter pub get
|
||||||
dart scripts/generate.dart
|
dart scripts/generate.dart
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
flutter build macos --release
|
flutter build macos --release
|
||||||
|
|
||||||
- name: Upload installer artifact
|
- 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
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Nexus.App
|
name: nexus.dmg
|
||||||
path: build/macos/Build/Products/Release/Nexus.app
|
path: ${{ steps.create-dmg.outputs.dmg_path }}
|
||||||
|
|
|
||||||
3
.github/workflows/windows.yml
vendored
|
|
@ -19,8 +19,7 @@ jobs:
|
||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.44.4
|
flutter-version: 3.47.4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
1
.gitmodules
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
[submodule "gomuks"]
|
[submodule "gomuks"]
|
||||||
path = gomuks
|
path = gomuks
|
||||||
url = https://github.com/gomuks/gomuks
|
url = https://github.com/gomuks/gomuks
|
||||||
branch = main
|
|
||||||
|
|
|
||||||
17
.vscode/launch.json
vendored
Normal file
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.vscode/settings.json
vendored
|
|
@ -11,6 +11,8 @@
|
||||||
"muks",
|
"muks",
|
||||||
"prefs",
|
"prefs",
|
||||||
"unban",
|
"unban",
|
||||||
"unredact"
|
"unifiedpush",
|
||||||
|
"unredact",
|
||||||
|
"webpush"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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
|
```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.
|
We use an object oriented style for controllers, where `provider` is a static member on the controller class. E.g.
|
||||||
|
|
|
||||||
32
README.md
|
|
@ -17,10 +17,15 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
|
|
||||||
- [x] Platform Support
|
- [x] Platform Support
|
||||||
- [x] Linux
|
- [x] Linux
|
||||||
- [x] Windows - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus).
|
- [x] Windows
|
||||||
|
- Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus).
|
||||||
- [x] Android
|
- [x] Android
|
||||||
- [x] MacOS - Unsigned .App only
|
- [x] MacOS
|
||||||
- [x] iOS - Unsigned .ipa only
|
- [x] Unsigned .ipa
|
||||||
|
- [ ] App Store
|
||||||
|
- [x] iOS
|
||||||
|
- [x] Unsigned .ipa
|
||||||
|
- [ ] App Store
|
||||||
- [ ] Web (may not be possible)
|
- [ ] Web (may not be possible)
|
||||||
- [x] Login (via OAuth)
|
- [x] Login (via OAuth)
|
||||||
- [x] Rooms / Spaces
|
- [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] Per message profiles
|
||||||
- [x] Attachments
|
- [x] Attachments
|
||||||
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
|
- [ ] Commands with [MSC4391](https://github.com/matrix-org/matrix-spec-proposals/pull/4391)
|
||||||
- [x] Mentions
|
- [x] Tags
|
||||||
- [x] Users
|
- [x] Users
|
||||||
- [x] Rooms
|
- [x] Rooms
|
||||||
- [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions)
|
- [x] Emojis
|
||||||
- [ ] Custom emojis/stickers
|
- [ ] Custom emojis/stickers
|
||||||
- [ ] GIFs using Gomuks' GIF proxies
|
- [ ] GIFs using Gomuks' GIF proxies
|
||||||
- [x] Receiving
|
- [x] Receiving
|
||||||
|
|
@ -63,9 +68,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] URL Previews
|
- [x] URL Previews
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [x] Viewing
|
- [x] Viewing
|
||||||
- [ ] Jump to original message
|
- [x] Jump to original message
|
||||||
- [x] In loaded timeline
|
|
||||||
- [ ] Out of loaded timeline
|
|
||||||
- [x] Edits
|
- [x] Edits
|
||||||
- [x] Attachments
|
- [x] Attachments
|
||||||
- [x] Unencrypted
|
- [x] Unencrypted
|
||||||
|
|
@ -105,7 +108,8 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] Member list
|
- [x] Member list
|
||||||
- [x] Sort by power level
|
- [x] Sort by power level
|
||||||
- [ ] Colors based off of 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)
|
- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
||||||
- [ ] Invites
|
- [ ] Invites
|
||||||
- [x] Settings
|
- [x] Settings
|
||||||
|
|
@ -115,13 +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:
|
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)
|
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
||||||
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
|
||||||
- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip)
|
- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip)
|
||||||
- [Unsigned MacOS App](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/Nexus.App.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)
|
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
||||||
- Flatpak
|
- Flatpak
|
||||||
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
- [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)
|
- [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
|
## Build it yourself
|
||||||
|
|
||||||
|
|
@ -190,6 +194,12 @@ dart scripts/generate.dart
|
||||||
> export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"
|
> 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:
|
Build generated files, and watch for new changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,12 @@ analyzer:
|
||||||
- "build/**"
|
- "build/**"
|
||||||
- "**/*.g.dart"
|
- "**/*.g.dart"
|
||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
- web/**
|
||||||
|
- windows/**
|
||||||
|
- macos/**
|
||||||
|
- linux/**
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
|
|
||||||
|
|
@ -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 = "../.."
|
|
||||||
}
|
|
||||||
84
android/app/build.gradle.kts
Normal file
|
|
@ -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 = "../.."
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
25
android/build.gradle.kts
Normal file
|
|
@ -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<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
26
android/settings.gradle.kts
Normal file
|
|
@ -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")
|
||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
assets/twim/notif.webp
Normal file
|
After Width: | Height: | Size: 15 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 130 KiB |
29
flake.lock
generated
|
|
@ -5,11 +5,11 @@
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785627969,
|
"lastModified": 1788450739,
|
||||||
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
|
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
|
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -42,15 +42,16 @@
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774860670,
|
"lastModified": 1790043059,
|
||||||
"narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=",
|
"narHash": "sha256-7ID4HL4IrrlalCsxMASkcAr+v0VHTNFp6+sZ+dpvOV8=",
|
||||||
"owner": "neobrain",
|
"owner": "Henry-Hiles",
|
||||||
"repo": "nix2flatpak",
|
"repo": "nix2flatpak",
|
||||||
"rev": "61d68e21e3fbc2d57590051f48736bea271f4aba",
|
"rev": "290dd282e555cab988c021c2be5f2361e58c4d51",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "neobrain",
|
"owner": "Henry-Hiles",
|
||||||
|
"ref": "quad/nexus",
|
||||||
"repo": "nix2flatpak",
|
"repo": "nix2flatpak",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
@ -73,11 +74,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"nixpkgs-lib": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785031560,
|
"lastModified": 1788057806,
|
||||||
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
|
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs.lib",
|
||||||
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
|
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -88,11 +89,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785967620,
|
"lastModified": 1790185690,
|
||||||
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
|
"narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
|
"rev": "4975466d324710c576dc11ad614684e6bd8cad8e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
54
flake.nix
|
|
@ -5,7 +5,7 @@
|
||||||
self.submodules = true;
|
self.submodules = true;
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
nix2flatpak.url = "github:neobrain/nix2flatpak";
|
nix2flatpak.url = "github:Henry-Hiles/nix2flatpak/quad/nexus";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -16,62 +16,12 @@
|
||||||
...
|
...
|
||||||
}@inputs:
|
}@inputs:
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
imports = [ ./linux/nix ];
|
||||||
systems = [
|
systems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-linux"
|
"aarch64-linux"
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
"x86_64-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 { };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
gomuks
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9444dd293664c300c6e710bb6cddcd9c6cab365d
|
Subproject commit 480938f9e01e7eae2cf249657efae03df76599d6
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
import "package:hooks/hooks.dart";
|
import "package:hooks/hooks.dart";
|
||||||
import "package:code_assets/code_assets.dart";
|
import "package:code_assets/code_assets.dart";
|
||||||
|
|
@ -50,7 +51,10 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
break;
|
break;
|
||||||
case OS.macOS:
|
case OS.macOS:
|
||||||
libFileName = "libgomuks.dylib";
|
libFileName = "libgomuks.dylib";
|
||||||
extraEnv = {"SDKROOT": await getXCodeTool()};
|
extraEnv = {
|
||||||
|
"SDKROOT": await getXCodeTool(),
|
||||||
|
"MACOSX_DEPLOYMENT_TARGET": codeConfig.macOS.targetVersion.toString(),
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case OS.windows:
|
case OS.windows:
|
||||||
libFileName = "libgomuks.dll";
|
libFileName = "libgomuks.dll";
|
||||||
|
|
@ -131,6 +135,8 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
"go",
|
"go",
|
||||||
[
|
[
|
||||||
"build",
|
"build",
|
||||||
|
"-trimpath",
|
||||||
|
"-ldflags=-s -w",
|
||||||
"-tags",
|
"-tags",
|
||||||
tags,
|
tags,
|
||||||
"-o",
|
"-o",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import UIKit
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.5 KiB |
|
|
@ -6,10 +6,8 @@ import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:path/path.dart";
|
import "package:path/path.dart";
|
||||||
|
|
||||||
class AttachmentController extends Notifier<(String, MessageContent?)?> {
|
class AttachmentController(final String roomId)
|
||||||
final String roomId;
|
extends Notifier<(String, MessageContent?)?> {
|
||||||
AttachmentController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ import "package:nexus/controllers/client_id.dart";
|
||||||
import "package:nexus/models/oauth_auth_code_response.dart";
|
import "package:nexus/models/oauth_auth_code_response.dart";
|
||||||
import "package:nexus/models/requests/oauth/get_auth_url.dart";
|
import "package:nexus/models/requests/oauth/get_auth_url.dart";
|
||||||
|
|
||||||
class AuthUrlController extends AsyncNotifier<OAuthAuthCodeResponse> {
|
class AuthUrlController(final Uri homeserver)
|
||||||
final Uri homeserver;
|
extends AsyncNotifier<OAuthAuthCodeResponse> {
|
||||||
AuthUrlController(this.homeserver);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OAuthAuthCodeResponse> build() async => ref
|
Future<OAuthAuthCodeResponse> build() async => ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/user.dart";
|
import "package:nexus/controllers/user.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
class AuthorController extends AsyncNotifier<MembershipContent> {
|
class AuthorController(final Event event)
|
||||||
final Event event;
|
extends AsyncNotifier<MembershipContent> {
|
||||||
AuthorController(this.event);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MembershipContent> build() async {
|
Future<MembershipContent> build() async {
|
||||||
final member = await ref.watch(
|
final member = await ref.watch(
|
||||||
UserController.provider(
|
UserController.provider(.new(roomId: event.roomId, userId: event.sender))
|
||||||
.new(roomId: event.roomId, userId: event.sender),
|
.future,
|
||||||
).future,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return .new(
|
return .new(
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
|
import "dart:async";
|
||||||
import "dart:ffi";
|
import "dart:ffi";
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
import "dart:math";
|
import "dart:math";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:ffi/ffi.dart";
|
import "package:ffi/ffi.dart";
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/cupertino.dart";
|
||||||
import "package:nexus/controllers/account_data.dart";
|
import "package:intl/intl.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/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/content/message.dart";
|
||||||
import "package:nexus/models/event.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/oauth_auth_code_response.dart";
|
||||||
import "package:nexus/models/open_graph_data.dart";
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
import "package:nexus/models/paginate.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/download_media.dart";
|
||||||
import "package:nexus/models/requests/get_event.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_related_events.dart";
|
||||||
import "package:nexus/models/requests/get_room_state.dart";
|
import "package:nexus/models/requests/get_room_state.dart";
|
||||||
import "package:nexus/models/requests/join_room.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/get_auth_url.dart";
|
||||||
import "package:nexus/models/requests/oauth/register_client.dart";
|
import "package:nexus/models/requests/oauth/register_client.dart";
|
||||||
import "package:nexus/models/requests/paginate.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/redact_event.dart";
|
||||||
|
import "package:nexus/models/requests/register_pusher.dart";
|
||||||
import "package:nexus/models/requests/report.dart";
|
import "package:nexus/models/requests/report.dart";
|
||||||
import "package:nexus/models/requests/send_event.dart";
|
import "package:nexus/models/requests/send_event.dart";
|
||||||
import "package:nexus/models/requests/send_message.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/set_state.dart";
|
||||||
import "package:nexus/models/requests/upload_media.dart";
|
import "package:nexus/models/requests/upload_media.dart";
|
||||||
import "package:nexus/models/room.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/spec_versions_response.dart";
|
||||||
import "package:nexus/models/sync_data.dart";
|
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:path_provider/path_provider.dart";
|
import "package:path_provider/path_provider.dart";
|
||||||
|
|
@ -47,134 +52,99 @@ import "package:path_provider/path_provider.dart";
|
||||||
class ClientController extends AsyncNotifier<int> {
|
class ClientController extends AsyncNotifier<int> {
|
||||||
@override
|
@override
|
||||||
Future<int> build() async {
|
Future<int> build() async {
|
||||||
final Pointer<Char> root;
|
debugPrint("Setting Gomuks env...");
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
final dir = await getApplicationSupportDirectory();
|
final env = {
|
||||||
root = "${dir.path}/gomuks".toNativeUtf8().cast();
|
"GOMUKS_ROOT": (await getApplicationSupportDirectory()).path,
|
||||||
} else {
|
"GOMUKS_CACHE_HOME": (await getApplicationCacheDirectory()).path,
|
||||||
root = nullptr.cast();
|
};
|
||||||
}
|
for (final MapEntry(:key, :value) in env.entries) {
|
||||||
|
final keyPtr = key.toNativeUtf8().cast<Char>();
|
||||||
final handle = GomuksInit(root);
|
final valuePtr = value.toNativeUtf8().cast<Char>();
|
||||||
|
|
||||||
final callable =
|
|
||||||
NativeCallable<
|
|
||||||
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
|
|
||||||
>.listener((
|
|
||||||
Pointer<Char> command,
|
|
||||||
int requestId,
|
|
||||||
GomuksOwnedBuffer data,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final muksEventType = command.cast<Utf8>().toDartString();
|
GomuksSetEnv(keyPtr, valuePtr);
|
||||||
debugPrint("Handling $muksEventType...");
|
} finally {
|
||||||
final decodedMuksEvent = data.toJson();
|
calloc
|
||||||
|
..free(keyPtr)
|
||||||
switch (muksEventType) {
|
..free(valuePtr);
|
||||||
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
|
debugPrint("Initializing Gomuks...");
|
||||||
.watch(roomProvider.notifier)
|
final handle = await Isolate.run(() {
|
||||||
.update(syncData.rooms, syncData.leftRooms);
|
final bufferPointer = GomuksConfig(
|
||||||
ref
|
matrix: .new(
|
||||||
.watch(accountDataProvider.notifier)
|
initialDeviceDisplayName:
|
||||||
.update(syncData.accountData);
|
"Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}",
|
||||||
|
),
|
||||||
|
).toJson().toGomuksBufferPtr();
|
||||||
|
|
||||||
if (syncData.topLevelSpaces != null) {
|
try {
|
||||||
ref
|
return GomuksInit(bufferPointer.ref);
|
||||||
.watch(TopLevelSpacesController.provider.notifier)
|
} finally {
|
||||||
.set(syncData.topLevelSpaces!);
|
calloc
|
||||||
}
|
..free(bufferPointer.ref.base)
|
||||||
|
..free(bufferPointer);
|
||||||
if (syncData.spaceEdges != null) {
|
|
||||||
ref
|
|
||||||
.watch(SpaceEdgesController.provider.notifier)
|
|
||||||
.set(syncData.spaceEdges!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref
|
|
||||||
// .watch(SyncStatusController.provider.notifier)
|
|
||||||
// .set(SyncStatus.fromJson(decodedMuksEvent));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
debugPrint("Unhandled event: $muksEventType");
|
|
||||||
}
|
|
||||||
debugPrint("Finished handling $muksEventType...");
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
|
||||||
rethrow;
|
|
||||||
} else {
|
|
||||||
showError(error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.onDispose(() => GomuksDestroy(handle));
|
ref.onDispose(() => GomuksDestroy(handle));
|
||||||
ref.onDispose(callable.close);
|
|
||||||
|
|
||||||
final errorCode = GomuksStart(handle, callable.nativeFunction);
|
return handle;
|
||||||
|
|
||||||
if (errorCode == 0) return handle;
|
|
||||||
throw Exception("GomuksStart returned error code $errorCode");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _sendCommand(
|
Future<dynamic> callGomuksMethod(
|
||||||
String command, [
|
Map<String, dynamic> data,
|
||||||
Map<String, dynamic> data = const {},
|
FutureOr<GomuksResponse> Function(int handle, GomuksBorrowedBuffer data)
|
||||||
]) async {
|
callback,
|
||||||
|
) async {
|
||||||
final bufferPointer = data.toGomuksBufferPtr();
|
final bufferPointer = data.toGomuksBufferPtr();
|
||||||
|
|
||||||
|
try {
|
||||||
final handle = await future;
|
final handle = await future;
|
||||||
|
|
||||||
final response = await Isolate.run(
|
final response = await Isolate.run(
|
||||||
() => GomuksSubmitCommand(
|
() => callback(handle, bufferPointer.ref),
|
||||||
handle,
|
|
||||||
command.toNativeUtf8().cast<Char>(),
|
|
||||||
bufferPointer.ref,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
calloc.free(bufferPointer);
|
|
||||||
|
|
||||||
final json = response.buf.toJson();
|
final json = response.buf.toJson();
|
||||||
|
|
||||||
if (response.command.cast<Utf8>().toDartString() == "error") {
|
if (response.command.cast<Utf8>().toDartString() == "error") {
|
||||||
throw json;
|
throw json;
|
||||||
}
|
}
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
|
} finally {
|
||||||
|
calloc
|
||||||
|
..free(bufferPointer.ref.base)
|
||||||
|
..free(bufferPointer);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(Event, RoomMetadata)> handlePush(Map<String, dynamic> data) async {
|
||||||
|
final response = await callGomuksMethod(
|
||||||
|
data,
|
||||||
|
(handle, data) async => GomuksHandlePush(handle, data),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
Event.fromJson(response["event"]),
|
||||||
|
RoomMetadata.fromJson(response["room"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _sendCommand(
|
||||||
|
String command, [
|
||||||
|
Map<String, dynamic> data = const {},
|
||||||
|
]) => callGomuksMethod(data, (handle, data) {
|
||||||
|
final commandPointer = command.toNativeUtf8().cast<Char>();
|
||||||
|
try {
|
||||||
|
return GomuksSubmitCommand(handle, commandPointer, data);
|
||||||
|
} finally {
|
||||||
|
calloc.free(commandPointer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> redactEvent(RedactEventRequest report) =>
|
Future<void> redactEvent(RedactEventRequest report) =>
|
||||||
_sendCommand("redact_event", report.toJson());
|
_sendCommand("redact_event", report.toJson());
|
||||||
|
|
@ -213,21 +183,17 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> joinRoom(JoinRoomRequest request) async {
|
Future<String> joinRoom(JoinRoomRequest request) async =>
|
||||||
final response = await _sendCommand("join_room", request.toJson());
|
(await _sendCommand("join_room", request.toJson()))["room_id"];
|
||||||
return response["room_id"];
|
|
||||||
}
|
Future<RoomSummary> getRoomSummary(JoinRoomRequest request) async =>
|
||||||
|
.fromJson(await _sendCommand("get_room_summary", request.toJson()));
|
||||||
|
|
||||||
Future<void> leaveRoom(Room room) async {
|
Future<void> leaveRoom(Room room) async {
|
||||||
if (room.metadata == null) return;
|
if (room.metadata == null) return;
|
||||||
await _sendCommand("leave_room", {"room_id": room.metadata!.id});
|
await _sendCommand("leave_room", {"room_id": room.metadata!.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
// (await _sendCommand("get_event_context", {
|
|
||||||
// "room_id": request.roomId,
|
|
||||||
// "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc",
|
|
||||||
// }));
|
|
||||||
|
|
||||||
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
|
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
|
||||||
Future<List?> getState(GetRoomStateRequest request) async =>
|
Future<List?> getState(GetRoomStateRequest request) async =>
|
||||||
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
||||||
|
|
@ -248,6 +214,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
return .new(response?.map((event) => .fromJson(event)));
|
return .new(response?.map((event) => .fromJson(event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<IList<Event>> getMentions(GetMentionsRequest request) async => .new(
|
||||||
|
// TODO: Handle `related_events`
|
||||||
|
((await _sendCommand("get_mentions", request.toJson()))["events"] as List)
|
||||||
|
.map((event) => .fromJson(event)),
|
||||||
|
);
|
||||||
|
|
||||||
Future<Event?> getEvent(GetEventRequest request) async {
|
Future<Event?> getEvent(GetEventRequest request) async {
|
||||||
final json = await _sendCommand("get_event", request.toJson());
|
final json = await _sendCommand("get_event", request.toJson());
|
||||||
return json == null ? null : .fromJson(json);
|
return json == null ? null : .fromJson(json);
|
||||||
|
|
@ -259,8 +231,19 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<Paginate> paginate(PaginateRequest request) async =>
|
Future<Paginate> paginate(PaginateRequest request) async =>
|
||||||
.fromJson(await _sendCommand("paginate", request.toJson()));
|
.fromJson(await _sendCommand("paginate", request.toJson()));
|
||||||
|
|
||||||
Future<ProfileResponse> getProfile(String userId) async =>
|
Future<PaginateManual> paginateManual(PaginateManualRequest request) async =>
|
||||||
.fromJson(await _sendCommand("get_profile", {"user_id": userId}));
|
.fromJson(await _sendCommand("paginate_manual", request.toJson()));
|
||||||
|
|
||||||
|
Future<EventContext> getEventContext(GetEventContextRequest request) async =>
|
||||||
|
.fromJson(await _sendCommand("get_event_context", request.toJson()));
|
||||||
|
|
||||||
|
Future<ProfileResponse> getProfile(String userId) async {
|
||||||
|
try {
|
||||||
|
return .fromJson(await _sendCommand("get_profile", {"user_id": userId}));
|
||||||
|
} catch (_) {
|
||||||
|
return ProfileResponse(profile: .new(id: userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> reportEvent(ReportRequest request) =>
|
Future<void> reportEvent(ReportRequest request) =>
|
||||||
_sendCommand("report_event", request.toJson());
|
_sendCommand("report_event", request.toJson());
|
||||||
|
|
@ -271,6 +254,15 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<void> setAccountData(SetAccountDataRequest request) =>
|
Future<void> setAccountData(SetAccountDataRequest request) =>
|
||||||
_sendCommand("set_account_data", request.toJson());
|
_sendCommand("set_account_data", request.toJson());
|
||||||
|
|
||||||
|
Future<void> registerPusher(RegisterPusherRequest request) =>
|
||||||
|
_sendCommand("register_homeserver_push", request.toJson());
|
||||||
|
|
||||||
|
Future<void> deregisterPusher(DeregisterPusherRequest request) =>
|
||||||
|
_sendCommand("register_homeserver_push", {
|
||||||
|
...request.toJson(),
|
||||||
|
"kind": null,
|
||||||
|
});
|
||||||
|
|
||||||
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
Future<MessageContent> uploadMedia(UploadMediaRequest request) async =>
|
||||||
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
.fromJson(await _sendCommand("upload_media", request.toJson()));
|
||||||
|
|
||||||
|
|
@ -280,9 +272,11 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<void> logout() => _sendCommand("logout");
|
Future<void> logout() => _sendCommand("logout");
|
||||||
|
|
||||||
Future<void> markRead(Room room) async {
|
Future<void> markRead(Room room) async {
|
||||||
|
if (room.timeline.isEmpty || room.metadata == null) return;
|
||||||
final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
|
final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
|
||||||
final event = eventRowId == null ? null : room.events[eventRowId];
|
final event = eventRowId == null ? null : room.events[eventRowId];
|
||||||
if (event == null || room.metadata == null) return;
|
|
||||||
|
if (event == null) return;
|
||||||
|
|
||||||
await _sendCommand("mark_read", {
|
await _sendCommand("mark_read", {
|
||||||
"room_id": room.metadata!.id,
|
"room_id": room.metadata!.id,
|
||||||
|
|
@ -308,10 +302,14 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<SpecVersionsResponse> getSpecVersions() async =>
|
Future<SpecVersionsResponse> getSpecVersions() async =>
|
||||||
.fromJson(await _sendCommand("get_versions"));
|
.fromJson(await _sendCommand("get_versions"));
|
||||||
|
|
||||||
|
Future<Capabilities> getCapabilities() async => Capabilities.fromJson(
|
||||||
|
(await _sendCommand("get_capabilities"))["capabilities"],
|
||||||
|
);
|
||||||
|
|
||||||
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
||||||
try {
|
try {
|
||||||
final response = await _sendCommand("discover_homeserver", {
|
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"]);
|
return Uri.parse(response["m.homeserver"]?["base_url"]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
|
|
||||||
class ClientIdController extends AsyncNotifier<String> {
|
class ClientIdController(final Uri homeserver) extends AsyncNotifier<String> {
|
||||||
final Uri homeserver;
|
|
||||||
ClientIdController(this.homeserver);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> build() => ref
|
Future<String> build() => ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
|
|
|
||||||
|
|
@ -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<String, EmojiCategory>, IMap<String, List<String>>);
|
|
||||||
|
|
||||||
class EmojiController extends AsyncNotifier<EmojiTuple> {
|
|
||||||
@override
|
|
||||||
Future<EmojiTuple> 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<String, dynamic>>()
|
|
||||||
.map(Emoji.fromJson)
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
final categoryMap = entries.fold<IMap<String, IList<String>>>(
|
|
||||||
.new(),
|
|
||||||
(acc, entry) => acc.update(
|
|
||||||
entry.category,
|
|
||||||
(list) => list.add(entry.emoji),
|
|
||||||
ifAbsent: () => .new([entry.emoji]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final keywordMap = entries.fold<IMap<String, IList<String>>>(
|
|
||||||
.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, EmojiTuple>(
|
|
||||||
EmojiController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,10 +5,8 @@ import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_event.dart";
|
import "package:nexus/models/requests/get_event.dart";
|
||||||
|
|
||||||
class EventController extends AsyncNotifier<Event?> {
|
class EventController(final GetEventRequest request)
|
||||||
final GetEventRequest request;
|
extends AsyncNotifier<Event?> {
|
||||||
EventController(this.request);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Event?> build() async {
|
Future<Event?> build() async {
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
|
|
|
||||||
124
lib/controllers/gomuks_listener.dart
Normal file
|
|
@ -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<void> {
|
||||||
|
@override
|
||||||
|
Future<void> build() async {
|
||||||
|
debugPrint("Starting gomuks...");
|
||||||
|
final handle = await ref.watch(ClientController.provider.future);
|
||||||
|
|
||||||
|
final callable =
|
||||||
|
NativeCallable<
|
||||||
|
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
|
||||||
|
>.listener((
|
||||||
|
Pointer<Char> command,
|
||||||
|
int requestId,
|
||||||
|
GomuksOwnedBuffer data,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
final muksEventType = command.cast<Utf8>().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, void>(
|
||||||
|
GomuksListenerController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
16
lib/controllers/jump_to_event.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
|
class JumpToEventController(String? _) extends Notifier<String?> {
|
||||||
|
@override
|
||||||
|
String? build() => null;
|
||||||
|
|
||||||
|
void set(String? eventId) => state = eventId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_, _) => true;
|
||||||
|
|
||||||
|
static final provider = NotifierProvider.family
|
||||||
|
.autoDispose<JumpToEventController, String?, String?>(
|
||||||
|
JumpToEventController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,28 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/shared_prefs.dart";
|
import "package:nexus/controllers/shared_prefs.dart";
|
||||||
|
|
||||||
class KeyController extends Notifier<String?> {
|
class KeyController(final String key) extends AsyncNotifier<String?> {
|
||||||
final String key;
|
|
||||||
KeyController(this.key);
|
|
||||||
|
|
||||||
static const String spaceKey = "space";
|
static const String spaceKey = "space";
|
||||||
static const String roomKey = "room";
|
static const String roomKey = "room";
|
||||||
|
static const String pushKeyKey = "pushKey";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? build() =>
|
Future<String?> build() =>
|
||||||
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
|
ref.watch(SharedPrefsController.provider).getString(key);
|
||||||
|
|
||||||
Future<void> set(String? value) async {
|
Future<void> set(String? value) async {
|
||||||
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
|
final prefs = ref.watch(SharedPrefsController.provider);
|
||||||
state = value;
|
state = .data(value);
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
prefs.remove(key);
|
await prefs.remove(key);
|
||||||
} else {
|
} else {
|
||||||
prefs.setString(key, value);
|
await prefs.setString(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
NotifierProvider.family<KeyController, String?, String>(
|
AsyncNotifierProvider.family<KeyController, String?, String>(
|
||||||
KeyController.new,
|
KeyController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/shared_prefs.dart";
|
import "package:nexus/controllers/shared_prefs.dart";
|
||||||
|
|
||||||
class MemberListOpenedController extends Notifier<bool> {
|
class MemberListOpenedController extends AsyncNotifier<bool> {
|
||||||
static const String key = "memberListOpened";
|
static const String key = "memberListOpened";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool build() =>
|
Future<bool> build() async =>
|
||||||
ref.watch(SharedPrefsController.provider).requireValue.getBool(key) ??
|
await ref.watch(SharedPrefsController.provider).getBool(key) ?? true;
|
||||||
true;
|
|
||||||
|
|
||||||
Future<void> set(bool value) async {
|
Future<void> set(bool value) async {
|
||||||
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
|
state = .data(value);
|
||||||
state = value;
|
await ref.watch(SharedPrefsController.provider).setBool(key, value);
|
||||||
|
|
||||||
prefs.setBool(key, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = NotifierProvider<MemberListOpenedController, bool>(
|
static final provider =
|
||||||
|
AsyncNotifierProvider<MemberListOpenedController, bool>(
|
||||||
MemberListOpenedController.new,
|
MemberListOpenedController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_room_state.dart";
|
import "package:nexus/models/requests/get_room_state.dart";
|
||||||
|
|
||||||
class MembersController extends AsyncNotifier<ISet<Event>> {
|
class MembersController(final String roomId)
|
||||||
final String roomId;
|
extends AsyncNotifier<ISet<Event>> {
|
||||||
MembersController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ISet<Event>> build() async {
|
Future<ISet<Event>> build() async {
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ import "package:nexus/models/configs/members_by_status.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
class MembersByStatusController extends AsyncNotifier<ISet<Event>> {
|
class MembersByStatusController(final MembersByStatusConfig config)
|
||||||
final MembersByStatusConfig config;
|
extends AsyncNotifier<ISet<Event>> {
|
||||||
MembersByStatusController(this.config);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ISet<Event>> build() => ref.watch(
|
Future<ISet<Event>> build() => ref.watch(
|
||||||
MembersController.provider(config.roomId).selectAsync(
|
MembersController.provider(config.roomId).selectAsync(
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,8 @@ import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
class MembersGroupedController
|
class MembersGroupedController(final MembersByStatusConfig config)
|
||||||
extends AsyncNotifier<IList<MapEntry<int?, ISet<Event>>>> {
|
extends AsyncNotifier<IList<MapEntry<int?, ISet<Event>>>> {
|
||||||
final MembersByStatusConfig config;
|
|
||||||
MembersGroupedController(this.config);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<MapEntry<int?, ISet<Event>>>> build() async {
|
Future<IList<MapEntry<int?, ISet<Event>>>> build() async {
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
class MultiProviderController extends AsyncNotifier<void> {
|
class MultiProviderController(final IList<AsyncNotifierProvider> providers)
|
||||||
MultiProviderController(this.providers);
|
extends AsyncNotifier<void> {
|
||||||
final IList<AsyncNotifierProvider> providers;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> build() =>
|
Future<void> build() => .wait(
|
||||||
.wait(providers.map((provider) => ref.watch(provider.future)));
|
providers.map((provider) => ref.watch(provider.future)),
|
||||||
|
eagerError: true,
|
||||||
|
);
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider.family<
|
AsyncNotifierProvider.family<
|
||||||
|
|
|
||||||
162
lib/controllers/notification.dart
Normal file
|
|
@ -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<FlutterLocalNotificationsPlugin> {
|
||||||
|
@override
|
||||||
|
Future<FlutterLocalNotificationsPlugin> 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<bool> 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<void> 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);
|
||||||
|
}
|
||||||
53
lib/controllers/notifications.dart
Normal file
|
|
@ -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<IList<Event>> {
|
||||||
|
@override
|
||||||
|
Future<IList<Event>> 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<void> 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<Event>,
|
||||||
|
NotificationsRequest?
|
||||||
|
>(NotificationsController.new);
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,8 @@ import "package:nexus/controllers/event.dart";
|
||||||
import "package:nexus/controllers/pinned_ids.dart";
|
import "package:nexus/controllers/pinned_ids.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
class PinnedEventsController extends AsyncNotifier<IList<Event>> {
|
class PinnedEventsController(final String roomId)
|
||||||
final String roomId;
|
extends AsyncNotifier<IList<Event>> {
|
||||||
PinnedEventsController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<Event>> build() async {
|
Future<IList<Event>> build() async {
|
||||||
final pinIds = ref.watch(PinnedIdsController.provider(roomId));
|
final pinIds = ref.watch(PinnedIdsController.provider(roomId));
|
||||||
|
|
@ -15,9 +13,8 @@ class PinnedEventsController extends AsyncNotifier<IList<Event>> {
|
||||||
return (await Future.wait(
|
return (await Future.wait(
|
||||||
pinIds.map(
|
pinIds.map(
|
||||||
(eventId) => ref.watch(
|
(eventId) => ref.watch(
|
||||||
EventController.provider(
|
EventController.provider(.new(eventId: eventId, roomId: roomId))
|
||||||
.new(eventId: eventId, roomId: roomId),
|
.future,
|
||||||
).future,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)).nonNulls.toIList();
|
)).nonNulls.toIList();
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/pinned_events.dart";
|
import "package:nexus/models/content/pinned_events.dart";
|
||||||
|
|
||||||
class PinnedIdsController extends Notifier<IList<String>> {
|
class PinnedIdsController(final String roomId) extends Notifier<IList<String>> {
|
||||||
final String roomId;
|
|
||||||
PinnedIdsController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IList<String> build() {
|
IList<String> build() {
|
||||||
final room = ref.watch(
|
final room = ref.watch(
|
||||||
|
|
|
||||||
22
lib/controllers/portal.dart
Normal file
|
|
@ -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<XdgDesktopPortalClient> {
|
||||||
|
@override
|
||||||
|
Future<XdgDesktopPortalClient> 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, XdgDesktopPortalClient>(
|
||||||
|
PortalController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,10 +6,8 @@ import "package:nexus/models/configs/power_level.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
|
||||||
class PowerLevelController extends Notifier<bool> {
|
class PowerLevelController(final PowerLevelConfig config)
|
||||||
final PowerLevelConfig config;
|
extends Notifier<bool> {
|
||||||
PowerLevelController(this.config);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool build() {
|
bool build() {
|
||||||
if (config case EventPowerLevelConfig(:final eventType)) {
|
if (config case EventPowerLevelConfig(:final eventType)) {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/models/profile_response.dart";
|
import "package:nexus/models/profile_response.dart";
|
||||||
|
|
||||||
class ProfileController extends AsyncNotifier<ProfileResponse> {
|
class ProfileController(final String userId)
|
||||||
final String userId;
|
extends AsyncNotifier<ProfileResponse> {
|
||||||
ProfileController(this.userId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ProfileResponse> build() {
|
Future<ProfileResponse> build() {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
return client.getProfile(userId);
|
return client.getProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
31
lib/controllers/push_key.dart
Normal file
|
|
@ -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<String?> {
|
||||||
|
@override
|
||||||
|
Future<String?> build() async => json.decode(
|
||||||
|
await ref.watch(KeyController.provider(KeyController.pushKeyKey).future) ??
|
||||||
|
"{}",
|
||||||
|
)[instance];
|
||||||
|
|
||||||
|
Future<void> 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, String?, String>(
|
||||||
|
PushKeyController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,8 @@ import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/models/configs/reactions.dart";
|
import "package:nexus/models/configs/reactions.dart";
|
||||||
import "package:nexus/models/content/reaction.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
|
|
||||||
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
class ReactionsController(final ReactionsConfig config)
|
||||||
final ReactionsConfig config;
|
extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
ReactionsController(this.config);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IMap<String, IList<String>>> build() async {
|
Future<IMap<String, IList<String>>> build() async {
|
||||||
final eventInfo = ref.watch(
|
final eventInfo = ref.watch(
|
||||||
|
|
|
||||||
44
lib/controllers/recent_emoji.dart
Normal file
|
|
@ -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<IList<RecentEmoji>> {
|
||||||
|
@override
|
||||||
|
IList<RecentEmoji> build() => ref.watch(
|
||||||
|
AccountDataController.provider.select(
|
||||||
|
(value) => value.recentEmoji.recentEmoji,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> 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, IList<RecentEmoji>>(
|
||||||
|
RecentEmojiController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "dart:math";
|
import "dart:math";
|
||||||
|
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.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/controllers/rooms.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/reaction.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
|
import "package:nexus/models/direction.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/redact_event.dart";
|
import "package:nexus/models/requests/redact_event.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/requests/send_message.dart";
|
import "package:nexus/models/requests/send_message.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
import "package:nexus/models/room_chat.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
class RoomChatController(final (String roomId, String? contextualEvent) info)
|
||||||
final String roomId;
|
extends AsyncNotifier<RoomChat?> {
|
||||||
RoomChatController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<Event>?> build() async {
|
Future<RoomChat?> build() async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final (roomId, eventId) = info;
|
||||||
final room = ref.watch(
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
|
final room = ref.read(
|
||||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -30,21 +32,14 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
|
|
||||||
if (!room.hasFetchedState) {
|
if (!room.hasFetchedState) {
|
||||||
final state = await client.getRoomState(.new(roomId: roomId));
|
final state = await client.getRoomState(.new(roomId: roomId));
|
||||||
|
|
||||||
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// While there are under 20 events, try to load more
|
final timeline = room.timeline
|
||||||
// until there's no more or the conditions are met.
|
|
||||||
if (room.hasMore && room.timeline.length < 20) {
|
|
||||||
loadOlder();
|
|
||||||
}
|
|
||||||
|
|
||||||
return room.timeline
|
|
||||||
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||||
.map((element) => element.value)
|
.map((element) => element.value)
|
||||||
.toIList()
|
.toIList()
|
||||||
.addAll(room.sticky)
|
.addAll(room.clientSticky)
|
||||||
.map((entry) {
|
.map((entry) {
|
||||||
final foundEvent = entry == null ? null : room.events[entry];
|
final foundEvent = entry == null ? null : room.events[entry];
|
||||||
|
|
||||||
|
|
@ -62,6 +57,26 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
})
|
})
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toIList();
|
.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<void> deleteMessage(Event event, {String? reason}) => ref
|
Future<void> deleteMessage(Event event, {String? reason}) => ref
|
||||||
|
|
@ -69,32 +84,49 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
.redactEvent(
|
.redactEvent(
|
||||||
RedactEventRequest(
|
RedactEventRequest(
|
||||||
eventId: event.eventId,
|
eventId: event.eventId,
|
||||||
roomId: roomId,
|
roomId: info.$1,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> loadOlder() async {
|
Future<void> 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
|
final timelineKeys = ref
|
||||||
.read(RoomsController.provider.select((value) => value[roomId]))
|
.read(RoomsController.provider.select((value) => value[info.$1]))
|
||||||
?.timeline
|
?.timeline
|
||||||
.keys;
|
.keys;
|
||||||
final response = await ref
|
final response = await client.paginate(
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.paginate(
|
|
||||||
.new(
|
.new(
|
||||||
roomId: roomId,
|
roomId: info.$1,
|
||||||
maxTimelineId: timelineKeys?.isNotEmpty == true
|
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||||
? timelineKeys?.reduce(min)
|
? timelineKeys?.reduce(min)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (response.events.isEmpty) {
|
||||||
|
state = .data(state.value);
|
||||||
|
}
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(RoomsController.provider.notifier)
|
.read(RoomsController.provider.notifier)
|
||||||
.update(
|
.update(
|
||||||
IMap({
|
IMap({
|
||||||
roomId: Room(
|
info.$1: Room(
|
||||||
events: IMap.fromIterable(
|
events: IMap.fromIterable(
|
||||||
response.events.addAll(response.relatedEvents),
|
response.events.addAll(response.relatedEvents),
|
||||||
keyMapper: (event) => event.rowId,
|
keyMapper: (event) => event.rowId,
|
||||||
|
|
@ -110,8 +142,43 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
}),
|
}),
|
||||||
.new(),
|
.new(),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
final paginationResponse = await client.paginateManual(
|
||||||
|
.new(
|
||||||
|
roomId: info.$1,
|
||||||
|
direction: direction,
|
||||||
|
since: direction == .forward
|
||||||
|
? chat!.historicalData!.end
|
||||||
|
: chat!.historicalData!.start,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return response.hasMore;
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> send(
|
Future<void> send(
|
||||||
|
|
@ -125,8 +192,8 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
if (relationType == .edit) {
|
if (relationType == .edit) {
|
||||||
baseContent = relation?.content;
|
baseContent = relation?.content;
|
||||||
} else {
|
} else {
|
||||||
final provider = AttachmentController.provider(roomId);
|
final provider = AttachmentController.provider(info.$1);
|
||||||
baseContent = ref.watch(provider)?.$2;
|
baseContent = ref.read(provider)?.$2;
|
||||||
ref.invalidate(provider);
|
ref.invalidate(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,10 +209,10 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
final event = await client.sendMessage(
|
final event = await client.sendMessage(
|
||||||
SendMessageRequest(
|
SendMessageRequest(
|
||||||
roomId: roomId,
|
roomId: info.$1,
|
||||||
baseContent: baseContent,
|
baseContent: baseContent,
|
||||||
mentions: Mentions(
|
mentions: Mentions(
|
||||||
userIds: [
|
userIds: [
|
||||||
|
|
@ -164,12 +231,12 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(RoomsController.provider.notifier)
|
.read(RoomsController.provider.notifier)
|
||||||
.update(
|
.update(
|
||||||
.new({
|
.new({
|
||||||
roomId: .new(
|
info.$1: .new(
|
||||||
events: .new({event.rowId: event}),
|
events: .new({event.rowId: event}),
|
||||||
sticky: .new({event.rowId}),
|
clientSticky: .new({event.rowId}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
.new(),
|
.new(),
|
||||||
|
|
@ -181,10 +248,10 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
Event event,
|
Event event,
|
||||||
String userId,
|
String userId,
|
||||||
) async {
|
) async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
final allReactionEvents = await client.getRelatedEvents(
|
final allReactionEvents = await client.getRelatedEvents(
|
||||||
.new(
|
.new(
|
||||||
roomId: roomId,
|
roomId: info.$1,
|
||||||
eventId: event.eventId,
|
eventId: event.eventId,
|
||||||
relationType: "m.annotation",
|
relationType: "m.annotation",
|
||||||
),
|
),
|
||||||
|
|
@ -205,16 +272,16 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
if (reactionEvent != null) {
|
if (reactionEvent != null) {
|
||||||
await ref
|
await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId));
|
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: info.$1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendReaction(String reaction, Event event) async {
|
Future<void> sendReaction(String reaction, Event event) async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.read(ClientController.provider.notifier);
|
||||||
|
|
||||||
await client.sendEvent(
|
await client.sendEvent(
|
||||||
.new(
|
.new(
|
||||||
roomId: roomId,
|
roomId: info.$1,
|
||||||
type: EventType.reaction.type,
|
type: EventType.reaction.type,
|
||||||
content: ReactionContent(key: reaction),
|
content: ReactionContent(key: reaction),
|
||||||
synchronous: true,
|
synchronous: true,
|
||||||
|
|
@ -226,7 +293,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider = AsyncNotifierProvider.family
|
||||||
.autoDispose<RoomChatController, IList<Event>?, String>(
|
.autoDispose<RoomChatController, RoomChat?, (String, String?)>(
|
||||||
RoomChatController.new,
|
RoomChatController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@ import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/create.dart";
|
import "package:nexus/models/content/create.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomCreatorsController extends Notifier<IList<String>> {
|
class RoomCreatorsController(final Room room) extends Notifier<IList<String>> {
|
||||||
final Room room;
|
|
||||||
RoomCreatorsController(this.room);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IList<String> build() {
|
IList<String> build() {
|
||||||
final createRowId = room.state[EventType.create.type]?[""];
|
final createRowId = room.state[EventType.create.type]?[""];
|
||||||
|
|
|
||||||
16
lib/controllers/room_summary.dart
Normal file
|
|
@ -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<RoomSummary> {
|
||||||
|
@override
|
||||||
|
Future<RoomSummary> build() =>
|
||||||
|
ref.read(ClientController.provider.notifier).getRoomSummary(request);
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<RoomSummaryController, RoomSummary, JoinRoomRequest>(
|
||||||
|
RoomSummaryController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
|
|
@ -46,10 +47,10 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
roomId,
|
roomId,
|
||||||
existing?.copyWith(
|
existing?.copyWith(
|
||||||
hasMore: incoming.hasMore,
|
hasMore: incoming.hasMore,
|
||||||
sticky:
|
clientSticky:
|
||||||
(incoming.sticky.isEmpty == true
|
(incoming.clientSticky.isEmpty == true
|
||||||
? existing.sticky
|
? existing.clientSticky
|
||||||
: existing.sticky.addAll(incoming.sticky))
|
: existing.clientSticky.addAll(incoming.clientSticky))
|
||||||
.removeWhere(
|
.removeWhere(
|
||||||
(rowId) => incoming.timeline.values.contains(rowId),
|
(rowId) => incoming.timeline.values.contains(rowId),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ class SettingsController extends AsyncNotifier<Settings> {
|
||||||
final file = await ref.watch(SettingsFileController.provider.future);
|
final file = await ref.watch(SettingsFileController.provider.future);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Settings.fromJson(json.decode(await file.readAsString()));
|
return .fromJson(json.decode(await file.readAsString()));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return Settings();
|
return .new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> set(Settings settings) async {
|
Future<void> set(Settings settings) async {
|
||||||
state = AsyncData(settings);
|
state = .data(settings);
|
||||||
final file = await ref.watch(SettingsFileController.provider.future);
|
final file = await ref.watch(SettingsFileController.provider.future);
|
||||||
await file.writeAsString(json.encode(settings.toJson()));
|
await file.writeAsString(json.encode(settings.toJson()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:m3e_buttons/m3e_buttons.dart";
|
import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
import "package:nexus/controllers/account_data.dart";
|
import "package:nexus/controllers/account_data.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/settings.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/account_data.dart";
|
||||||
import "package:nexus/models/settings_category.dart";
|
import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -18,9 +24,6 @@ class SettingsSectionsController
|
||||||
@override
|
@override
|
||||||
Future<IMap<String, IList<SettingsCategory>>> build() async {
|
Future<IMap<String, IList<SettingsCategory>>> build() async {
|
||||||
final settings = await ref.watch(SettingsController.provider.future);
|
final settings = await ref.watch(SettingsController.provider.future);
|
||||||
final specVersionsResponse = await ref
|
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.getSpecVersions();
|
|
||||||
|
|
||||||
return .new({
|
return .new({
|
||||||
"General": .new([
|
"General": .new([
|
||||||
|
|
@ -49,8 +52,7 @@ class SettingsSectionsController
|
||||||
.new(
|
.new(
|
||||||
title: "Use Dynamic Theme",
|
title: "Use Dynamic Theme",
|
||||||
icon: Icons.palette,
|
icon: Icons.palette,
|
||||||
description:
|
description: "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.",
|
||||||
"Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.",
|
|
||||||
builder: (title, description, icon) => SwitchListTile(
|
builder: (title, description, icon) => SwitchListTile(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
|
|
@ -76,8 +78,7 @@ class SettingsSectionsController
|
||||||
settings: .new([
|
settings: .new([
|
||||||
.new(
|
.new(
|
||||||
title: "Linux Mobile Mode",
|
title: "Linux Mobile Mode",
|
||||||
description:
|
description: "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.",
|
||||||
"Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.",
|
|
||||||
icon: Icons.construction,
|
icon: Icons.construction,
|
||||||
builder: (title, description, icon) => SwitchListTile(
|
builder: (title, description, icon) => SwitchListTile(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
|
|
@ -98,17 +99,103 @@ class SettingsSectionsController
|
||||||
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
|
||||||
"Account": .new([
|
"Account": .new([
|
||||||
.new(title: "Profile", icon: Icons.person, settings: .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(
|
.new(
|
||||||
title: "Safety",
|
title: "Safety",
|
||||||
icon: Icons.gpp_good,
|
icon: Icons.gpp_good,
|
||||||
settings: .new([
|
settings: .new([
|
||||||
.new(
|
.new(
|
||||||
title: "Invite Blocking",
|
title: "Invite Blocking",
|
||||||
description:
|
description: "Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).",
|
||||||
"Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).",
|
|
||||||
builder: (title, description, icon) => Consumer(
|
builder: (title, description, icon) => Consumer(
|
||||||
builder: (context, ref, _) =>
|
builder: (context, ref, _) {
|
||||||
DialogListTile<DefaultInviteAction>(
|
final specVersionsResponse = ref.watch(
|
||||||
|
SpecVersionsController.provider,
|
||||||
|
);
|
||||||
|
return DialogListTile<DefaultInviteAction>(
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: Text(description),
|
subtitle: Text(description),
|
||||||
|
|
@ -116,17 +203,23 @@ class SettingsSectionsController
|
||||||
.watch(AccountDataController.provider)
|
.watch(AccountDataController.provider)
|
||||||
.invitePermissionConfig
|
.invitePermissionConfig
|
||||||
.defaultAction,
|
.defaultAction,
|
||||||
options: specVersionsResponse.unstableFeatures.msc4494
|
options:
|
||||||
|
specVersionsResponse.maybeWhen(
|
||||||
|
data: (data) => data.unstableFeatures.msc4494,
|
||||||
|
orElse: () => true,
|
||||||
|
)
|
||||||
? DefaultInviteAction.values
|
? DefaultInviteAction.values
|
||||||
: IList(
|
: IList(DefaultInviteAction.values)
|
||||||
DefaultInviteAction.values,
|
.remove(.denyPublic)
|
||||||
).remove(.denyPublic).toList(),
|
.toList(),
|
||||||
getName: (option) => switch (option) {
|
getName: (option) => switch (option) {
|
||||||
.allow => "Allow",
|
.allow => "Allow",
|
||||||
.deny => "Deny",
|
.deny => "Deny",
|
||||||
.denyPublic => "Deny public",
|
.denyPublic => "Deny public",
|
||||||
},
|
},
|
||||||
onChanged: (value) => ref
|
onChanged: specVersionsResponse.maybeWhen(
|
||||||
|
data: (_) =>
|
||||||
|
(value) => ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.setAccountData(
|
.setAccountData(
|
||||||
.new(
|
.new(
|
||||||
|
|
@ -137,7 +230,10 @@ class SettingsSectionsController
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.onError(showError),
|
.onError(showError),
|
||||||
|
orElse: () => null,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
icon: Icons.person_off,
|
icon: Icons.person_off,
|
||||||
),
|
),
|
||||||
|
|
@ -156,15 +252,14 @@ class SettingsSectionsController
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return M3EButton.icon(
|
return M3EButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(
|
await ref
|
||||||
context,
|
.watch(UnifiedPushController.provider.notifier)
|
||||||
).popUntil((route) => route.isFirst);
|
.deregister()
|
||||||
|
.onError(showError);
|
||||||
await WidgetsBinding.instance.endOfFrame;
|
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.logout();
|
.logout()
|
||||||
|
.onError(showError);
|
||||||
},
|
},
|
||||||
label: Text(title),
|
label: Text(title),
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:shared_preferences/shared_preferences.dart";
|
import "package:shared_preferences/shared_preferences.dart";
|
||||||
|
|
||||||
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
|
class SharedPrefsController extends Notifier<SharedPreferencesAsync> {
|
||||||
@override
|
@override
|
||||||
Future<SharedPreferences> build() async => .getInstance();
|
SharedPreferencesAsync build() => SharedPreferencesAsync();
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
|
NotifierProvider<SharedPrefsController, SharedPreferencesAsync>(
|
||||||
SharedPrefsController.new,
|
SharedPrefsController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import "package:collection/collection.dart";
|
import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/account_data.dart";
|
import "package:nexus/controllers/account_data.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
|
|
||||||
15
lib/controllers/spec_versions.dart
Normal file
|
|
@ -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<SpecVersionsResponse> {
|
||||||
|
@override
|
||||||
|
Future<SpecVersionsResponse> build() =>
|
||||||
|
ref.read(ClientController.provider.notifier).getSpecVersions();
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.autoDispose<
|
||||||
|
SpecVersionsController,
|
||||||
|
SpecVersionsResponse
|
||||||
|
>(SpecVersionsController.new);
|
||||||
|
}
|
||||||
178
lib/controllers/unified_push.dart
Normal file
|
|
@ -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<bool> {
|
||||||
|
@override
|
||||||
|
Future<bool> 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<void> 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<void> 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, bool>(
|
||||||
|
UnifiedPushController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
33
lib/controllers/unified_push_allowed.dart
Normal file
|
|
@ -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<String?> {
|
||||||
|
@override
|
||||||
|
Future<String?> 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, String?>(
|
||||||
|
UnifiedPushAllowedController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/models/open_graph_data.dart";
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
|
|
||||||
class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
class UrlPreviewController(final Uri url)
|
||||||
final Uri url;
|
extends AsyncNotifier<OpenGraphData?> {
|
||||||
UrlPreviewController(this.url);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OpenGraphData?> build() async {
|
Future<OpenGraphData?> build() async {
|
||||||
if (url.host == "matrix.to") return null;
|
if (url.host == "matrix.to") return null;
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class ViaController extends Notifier<String> {
|
class ViaController(final Room room) extends Notifier<String> {
|
||||||
final Room room;
|
|
||||||
ViaController(this.room);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String build() {
|
String build() {
|
||||||
final servers = <String>{};
|
final servers = <String>{};
|
||||||
|
|
|
||||||
266
lib/helpers/extensions/build_event_options.dart
Normal file
|
|
@ -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<PopupMenuEntry> 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<void> 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<void> 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ extension LinkToMention on String {
|
||||||
///
|
///
|
||||||
/// Returns the decoded identifier (e.g. "#room:matrix.org")
|
/// Returns the decoded identifier (e.g. "#room:matrix.org")
|
||||||
/// or null if this is not a Matrix link.
|
/// 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 {
|
String? get mention {
|
||||||
final trimmed = trim();
|
final trimmed = trim();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
extension SchemeToTheme on ColorScheme {
|
extension SchemeToTheme on ColorScheme {
|
||||||
ThemeData get theme {
|
ThemeData get theme {
|
||||||
|
|
|
||||||
|
|
@ -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:flutter_svg/flutter_svg.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:m3e_card_list/m3e_card_list.dart";
|
import "package:m3e_card_list/m3e_card_list.dart";
|
||||||
import "package:nexus/helpers/launch_helper.dart";
|
import "package:nexus/helpers/launch_helper.dart";
|
||||||
import "package:package_info_plus/package_info_plus.dart";
|
import "package:package_info_plus/package_info_plus.dart";
|
||||||
|
|
||||||
extension ShowContextMenu on BuildContext {
|
extension ShowAboutDialog on BuildContext {
|
||||||
Future<void> showAboutDialog(WidgetRef ref) async {
|
Future<void> showAboutDialog(WidgetRef ref) async {
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ extension ShowContextMenu on BuildContext {
|
||||||
Row(
|
Row(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset("assets/icon.svg", width: 64),
|
SvgPicture.asset("assets/bundled/icon.svg", width: 64),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: .start,
|
crossAxisAlignment: .start,
|
||||||
|
|
@ -50,8 +50,8 @@ extension ShowContextMenu on BuildContext {
|
||||||
M3ECardColumn(
|
M3ECardColumn(
|
||||||
onTap: (index) =>
|
onTap: (index) =>
|
||||||
ref.watch(LaunchHelper.provider).launchUrl(switch (index) {
|
ref.watch(LaunchHelper.provider).launchUrl(switch (index) {
|
||||||
0 => Uri.https("git.federated.nexus", "nexus/nexus"),
|
0 => .https("git.federated.nexus", "nexus/nexus"),
|
||||||
_ => Uri.https("liberapay.com", "QuadRadical"),
|
_ => .https("liberapay.com", "QuadRadical"),
|
||||||
}),
|
}),
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
extension ShowContextMenu on BuildContext {
|
extension ShowContextMenu on BuildContext {
|
||||||
void showContextMenu({
|
void showContextMenu({
|
||||||
|
|
@ -9,7 +9,7 @@ extension ShowContextMenu on BuildContext {
|
||||||
|
|
||||||
showMenu(
|
showMenu(
|
||||||
context: this,
|
context: this,
|
||||||
constraints: .loose(Size.infinite),
|
constraints: .loose(.infinite),
|
||||||
position: .fromLTRB(
|
position: .fromLTRB(
|
||||||
globalPosition.dx,
|
globalPosition.dx,
|
||||||
globalPosition.dy,
|
globalPosition.dy,
|
||||||
|
|
|
||||||
|
|
@ -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/models/content/membership.dart";
|
||||||
import "package:nexus/widgets/user_bottom_sheet.dart";
|
import "package:nexus/widgets/user_bottom_sheet.dart";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import "package:color_hash/color_hash.dart";
|
import "package:color_hash/color_hash.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
extension ToColor on String {
|
extension ToColor on String {
|
||||||
Color get colorHash => ColorHash(this, lightness: .5, saturation: .7).color;
|
Color get colorHash => ColorHash(this, lightness: .5, saturation: .7).color;
|
||||||
|
|
|
||||||
203
lib/helpers/hooks/chat_scroll.dart
Normal file
|
|
@ -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<Event> historyItems,
|
||||||
|
required final IList<Event> liveItems,
|
||||||
|
required final GlobalKey centerKey,
|
||||||
|
required final ScrollController scrollController,
|
||||||
|
required final bool atBottom,
|
||||||
|
required final Future<void> Function(String id) jumpToId,
|
||||||
|
required final Future<void> Function() jumpToBottom,
|
||||||
|
required final GlobalKey Function(String eventId) keyFor,
|
||||||
|
}) {
|
||||||
|
factory use({
|
||||||
|
required AsyncValue<RoomChat?> controllerData,
|
||||||
|
required Future<void> Function(Direction direction) paginate,
|
||||||
|
required Future<void> Function() markRead,
|
||||||
|
required ValueNotifier<String?> contextualEvent,
|
||||||
|
}) {
|
||||||
|
final anchorId = useState<String?>(null);
|
||||||
|
|
||||||
|
final itemKeys = useMemoized(() => <String, GlobalKey>{}, []);
|
||||||
|
GlobalKey keyFor(String eventId) =>
|
||||||
|
itemKeys.putIfAbsent(eventId, GlobalKey.new);
|
||||||
|
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
final centerKey = useMemoized(GlobalKey.new);
|
||||||
|
|
||||||
|
final atBottom = useState(true);
|
||||||
|
|
||||||
|
final pendingAnchorTarget = useState<String?>(null);
|
||||||
|
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(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<Event> history, IList<Event> 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<void> 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<BuildContext>();
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,7 @@ import "package:flutter/services.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:url_launcher/url_launcher.dart" as ul;
|
import "package:url_launcher/url_launcher.dart" as ul;
|
||||||
|
|
||||||
class LaunchHelper {
|
class LaunchHelper(Ref ref) {
|
||||||
final Ref ref;
|
|
||||||
LaunchHelper(this.ref);
|
|
||||||
|
|
||||||
Future<bool> launchUrl(Uri url, {bool useWebview = false}) async {
|
Future<bool> launchUrl(Uri url, {bool useWebview = false}) async {
|
||||||
try {
|
try {
|
||||||
return await ul.launchUrl(
|
return await ul.launchUrl(
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import "dart:ui";
|
import "dart:ui";
|
||||||
|
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/widgets.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/models/requests/download_media.dart";
|
import "package:nexus/models/requests/download_media.dart";
|
||||||
|
|
||||||
class MxcImage extends ImageProvider<MxcImage> {
|
class MxcImage(final WidgetRef ref, final DownloadMediaRequest request)
|
||||||
final WidgetRef ref;
|
extends ImageProvider<MxcImage> {
|
||||||
final DownloadMediaRequest request;
|
|
||||||
const MxcImage(this.ref, this.request);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MxcImage> obtainKey(ImageConfiguration configuration) =>
|
Future<MxcImage> obtainKey(ImageConfiguration configuration) =>
|
||||||
Future.value(this);
|
Future.value(this);
|
||||||
|
|
|
||||||
103
lib/main.dart
|
|
@ -1,26 +1,31 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:dynamic_color/dynamic_color.dart";
|
import "package:dynamic_color/dynamic_color.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter/foundation.dart";
|
import "package:flutter/foundation.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:media_kit/media_kit.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/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/multi_provider.dart";
|
||||||
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/shared_prefs.dart";
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
|
||||||
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
|
||||||
import "package:nexus/helpers/font_licenses.dart";
|
import "package:nexus/helpers/font_licenses.dart";
|
||||||
import "package:nexus/pages/chat.dart";
|
import "package:nexus/widgets/pages/chat.dart";
|
||||||
import "package:nexus/pages/select_server.dart";
|
import "package:nexus/widgets/pages/select_server.dart";
|
||||||
import "package:nexus/pages/verify.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/error_dialog.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
import "package:window_manager/window_manager.dart";
|
import "package:window_manager/window_manager.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
late final bool isInBackground;
|
||||||
|
|
||||||
final class Logger extends ProviderObserver {
|
final class Logger extends ProviderObserver {
|
||||||
@override
|
@override
|
||||||
|
|
@ -37,14 +42,7 @@ New Value: ${newValue is AsyncData ? newValue.value : newValue}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showError(Object error, [StackTrace? stackTrace]) {
|
void showError(Object error, [StackTrace? stackTrace]) {
|
||||||
if (error.toString().contains("DioException") ||
|
if (error.toString().contains("'_nextFrame != null': is not true.")) {
|
||||||
error.toString().contains(
|
|
||||||
"setState() or markNeedsBuild() called during build.",
|
|
||||||
) ||
|
|
||||||
error.toString().contains("Invalid source") ||
|
|
||||||
error.toString().contains("UTF-16") ||
|
|
||||||
error.toString().contains("HTTP request failed") ||
|
|
||||||
error.toString().contains("Invalid image data")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,10 +59,27 @@ void showError(Object error, [StackTrace? stackTrace]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main(List<String> args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
|
LicenseRegistry.addLicense(() => .fromIterable(fontLicenses));
|
||||||
|
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||||
|
showError(details.exception.toString(), details.stack);
|
||||||
|
|
||||||
|
isInBackground =
|
||||||
|
Platform.environment["FLUTTER_HEADLESS"] != null ||
|
||||||
|
args.contains("--unifiedpush-bg");
|
||||||
|
|
||||||
|
if (isInBackground) {
|
||||||
|
await ProviderContainer()
|
||||||
|
.read(UnifiedPushController.provider.future)
|
||||||
|
.timeout(Duration(seconds: 5));
|
||||||
|
|
||||||
|
await Future.delayed(Duration(seconds: 10));
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
|
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
await windowManager.waitUntilReadyToShow(
|
await windowManager.waitUntilReadyToShow(
|
||||||
|
|
@ -76,11 +91,6 @@ void main() async {
|
||||||
await windowManager.setMinimumSize(Size.square(500));
|
await windowManager.setMinimumSize(Size.square(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
LicenseRegistry.addLicense(() => Stream.fromIterable(fontLicenses));
|
|
||||||
|
|
||||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
|
||||||
showError(details.exception.toString(), details.stack);
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
retry: (_, _) => null,
|
retry: (_, _) => null,
|
||||||
|
|
@ -89,21 +99,19 @@ void main() async {
|
||||||
// ignore: dead_code
|
// ignore: dead_code
|
||||||
if (false && kDebugMode) Logger(),
|
if (false && kDebugMode) Logger(),
|
||||||
],
|
],
|
||||||
child: const App(),
|
child: App(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class const App({super.key}) extends StatelessWidget {
|
||||||
const App({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => DynamicColorBuilder(
|
Widget build(BuildContext context) => DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) => Consumer(
|
builder: (lightDynamic, darkDynamic) => Consumer(
|
||||||
builder: (context, ref, child) => MaterialApp(
|
builder: (context, ref, child) => MaterialApp(
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
// Use indigo to work around bugs in theme generation
|
|
||||||
theme:
|
theme:
|
||||||
(ref
|
(ref
|
||||||
.watch(SettingsController.provider)
|
.watch(SettingsController.provider)
|
||||||
|
|
@ -134,21 +142,21 @@ class App extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Consumer(
|
body: Consumer(
|
||||||
builder: (_, ref, _) => ref
|
builder: (_, ref, _) => switch (ref.watch(
|
||||||
.watch(
|
|
||||||
MultiProviderController.provider(
|
MultiProviderController.provider(
|
||||||
IListConst([
|
.new([
|
||||||
SharedPrefsController.provider,
|
GomuksListenerController.provider,
|
||||||
ClientController.provider,
|
NotificationController.provider,
|
||||||
|
UnifiedPushController.provider,
|
||||||
|
MemberListOpenedController.provider,
|
||||||
|
KeyController.provider(KeyController.roomKey),
|
||||||
|
KeyController.provider(KeyController.spaceKey),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)) {
|
||||||
.betterWhen(
|
AsyncData(value: _) || AsyncLoading(value: _?) => Consumer(
|
||||||
data: (_) => Consumer(
|
|
||||||
builder: (_, ref, _) {
|
builder: (_, ref, _) {
|
||||||
final clientState = ref.watch(
|
final clientState = ref.watch(ClientStateController.provider);
|
||||||
ClientStateController.provider,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clientState == null || !clientState.isInitialized) {
|
if (clientState == null || !clientState.isInitialized) {
|
||||||
return Loading();
|
return Loading();
|
||||||
|
|
@ -163,7 +171,26 @@ class App extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,60 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
part "account_data.freezed.dart";
|
part "account_data.freezed.dart";
|
||||||
part "account_data.g.dart";
|
part "account_data.g.dart";
|
||||||
|
|
||||||
@freezed
|
@Freezed(toJson: false, fromJson: false)
|
||||||
abstract class AccountData with _$AccountData {
|
@JsonSerializable()
|
||||||
const AccountData._();
|
class const AccountData({
|
||||||
static List<dynamic>? readRecentEmojiValue(
|
@JsonKey(name: AccountData.invitePermissionConfigKey)
|
||||||
Map<dynamic, dynamic> json,
|
final InvitePermissionConfig invitePermissionConfig =
|
||||||
String key,
|
const InvitePermissionConfig(),
|
||||||
) => json[key]?["recent_emoji"];
|
|
||||||
|
|
||||||
static Map<String, List<dynamic>>? recentEmojiToJson(
|
@JsonKey(name: AccountData.directKey)
|
||||||
IList<RecentEmoji> recentEmoji,
|
final IMap<String, IList<String>> directMessages = const IMap.empty(),
|
||||||
) => {"recent_emoji": recentEmoji.map((emoji) => emoji.toJson()).toList()};
|
|
||||||
|
|
||||||
|
@JsonKey(name: AccountData.recentEmojiKey)
|
||||||
|
final RecentEmojiData recentEmoji = const RecentEmojiData(),
|
||||||
|
}) with _$AccountData {
|
||||||
static const invitePermissionConfigKey = "m.invite_permission_config";
|
static const invitePermissionConfigKey = "m.invite_permission_config";
|
||||||
static const directKey = "m.direct";
|
static const directKey = "m.direct";
|
||||||
static const recentEmojiKey = "m.recent_emoji";
|
static const recentEmojiKey = "m.recent_emoji";
|
||||||
|
|
||||||
const factory AccountData({
|
Map<String, Object?> toJson() => _$AccountDataToJson(this);
|
||||||
@JsonKey(name: AccountData.invitePermissionConfigKey)
|
|
||||||
@Default(InvitePermissionConfig())
|
|
||||||
InvitePermissionConfig invitePermissionConfig,
|
|
||||||
|
|
||||||
@JsonKey(name: AccountData.directKey)
|
|
||||||
@Default(IMap.empty())
|
|
||||||
IMap<String, IList<String>> directMessages,
|
|
||||||
|
|
||||||
@JsonKey(
|
|
||||||
name: AccountData.recentEmojiKey,
|
|
||||||
readValue: AccountData.readRecentEmojiValue,
|
|
||||||
toJson: AccountData.recentEmojiToJson,
|
|
||||||
)
|
|
||||||
@Default(IList.empty())
|
|
||||||
IList<RecentEmoji> recentEmoji,
|
|
||||||
}) = _AccountData;
|
|
||||||
|
|
||||||
factory AccountData.fromJson(Map<String, Object?> json) =>
|
factory AccountData.fromJson(Map<String, Object?> json) =>
|
||||||
_$AccountDataFromJson(json);
|
_$AccountDataFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@Freezed(toJson: false, fromJson: false)
|
||||||
abstract class InvitePermissionConfig with _$InvitePermissionConfig {
|
@JsonSerializable()
|
||||||
const factory InvitePermissionConfig({
|
class const RecentEmojiData({
|
||||||
|
final IList<RecentEmoji> recentEmoji = const IList.empty(),
|
||||||
|
}) with _$RecentEmojiData {
|
||||||
|
Map<String, Object?> toJson() => _$RecentEmojiDataToJson(this);
|
||||||
|
|
||||||
|
factory RecentEmojiData.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$RecentEmojiDataFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const InvitePermissionConfig({
|
||||||
@JsonKey(unknownEnumValue: DefaultInviteAction.allow)
|
@JsonKey(unknownEnumValue: DefaultInviteAction.allow)
|
||||||
@Default(DefaultInviteAction.allow)
|
final DefaultInviteAction defaultAction = DefaultInviteAction.allow,
|
||||||
DefaultInviteAction defaultAction,
|
}) with _$InvitePermissionConfig {
|
||||||
}) = _InvitePermissionConfig;
|
Map<String, Object?> toJson() => _$InvitePermissionConfigToJson(this);
|
||||||
|
|
||||||
factory InvitePermissionConfig.fromJson(Map<String, Object?> json) =>
|
factory InvitePermissionConfig.fromJson(Map<String, Object?> json) =>
|
||||||
_$InvitePermissionConfigFromJson(json);
|
_$InvitePermissionConfigFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@Freezed(toJson: false, fromJson: false)
|
||||||
abstract class RecentEmoji with _$RecentEmoji {
|
@JsonSerializable()
|
||||||
const factory RecentEmoji({required String emoji, required int total}) =
|
class const RecentEmoji({required final String emoji, required final int total})
|
||||||
_RecentEmoji;
|
with _$RecentEmoji {
|
||||||
|
Map<String, Object?> toJson() => _$RecentEmojiToJson(this);
|
||||||
|
|
||||||
factory RecentEmoji.fromJson(Map<String, Object?> json) =>
|
factory RecentEmoji.fromJson(Map<String, Object?> json) =>
|
||||||
_$RecentEmojiFromJson(json);
|
_$RecentEmojiFromJson(json);
|
||||||
|
|
|
||||||
25
lib/models/capabilities.dart
Normal file
|
|
@ -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<String, Object?> toJson() => _$CapabilitiesToJson(this);
|
||||||
|
|
||||||
|
factory Capabilities.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$CapabilitiesFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const WebPush({required final bool enabled, final String? vapid})
|
||||||
|
with _$WebPush {
|
||||||
|
Map<String, Object?> toJson() => _$WebPushToJson(this);
|
||||||
|
|
||||||
|
factory WebPush.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$WebPushFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
part "client_state.freezed.dart";
|
part "client_state.freezed.dart";
|
||||||
part "client_state.g.dart";
|
part "client_state.g.dart";
|
||||||
|
|
||||||
@freezed
|
@Freezed(toJson: false, fromJson: false)
|
||||||
abstract class ClientState with _$ClientState {
|
@JsonSerializable()
|
||||||
const factory ClientState({
|
class const ClientState({
|
||||||
required bool isInitialized,
|
required final bool isInitialized,
|
||||||
required bool isLoggedIn,
|
required final bool isLoggedIn,
|
||||||
required bool isVerified,
|
required final bool isVerified,
|
||||||
required String? userId,
|
required final String? userId,
|
||||||
required String? homeserverUrl,
|
required final String? deviceId,
|
||||||
}) = _ClientState;
|
required final String? homeserverUrl,
|
||||||
|
}) with _$ClientState {
|
||||||
|
Map<String, Object?> toJson() => _$ClientStateToJson(this);
|
||||||
|
|
||||||
factory ClientState.fromJson(Map<String, Object?> json) =>
|
factory ClientState.fromJson(Map<String, Object?> json) =>
|
||||||
_$ClientStateFromJson(json);
|
_$ClientStateFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
import "package:nexus/models/membership_status.dart";
|
||||||
|
|
||||||
part "members_by_status.freezed.dart";
|
part "members_by_status.freezed.dart";
|
||||||
part "members_by_status.g.dart";
|
part "members_by_status.g.dart";
|
||||||
|
|
||||||
@freezed
|
@Freezed(toJson: false, fromJson: false)
|
||||||
abstract class MembersByStatusConfig with _$MembersByStatusConfig {
|
@JsonSerializable()
|
||||||
const factory MembersByStatusConfig({
|
class const MembersByStatusConfig({
|
||||||
required String roomId,
|
required final String roomId,
|
||||||
required MembershipStatus status,
|
required final MembershipStatus status,
|
||||||
}) = _MembersByStatusConfig;
|
}) with _$MembersByStatusConfig {
|
||||||
|
Map<String, Object?> toJson() => _$MembersByStatusConfigToJson(this);
|
||||||
|
|
||||||
factory MembersByStatusConfig.fromJson(Map<String, Object?> json) =>
|
factory MembersByStatusConfig.fromJson(Map<String, Object?> json) =>
|
||||||
_$MembersByStatusConfigFromJson(json);
|
_$MembersByStatusConfigFromJson(json);
|
||||||
|
|
|
||||||