Compare commits
146 commits
windows-de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
2d0f41000e |
|||
|
3349ca7253 |
|||
|
18657eb980 |
|||
|
b4b157c39f |
|||
|
a76b6e6ed3 |
|||
|
5bbd472999 |
|||
|
6c0e149c25 |
|||
|
b26c144eea |
|||
|
ddc8db8326 |
|||
|
bc307cbcda |
|||
|
e40de37b9d |
|||
|
7757825e27 |
|||
|
795e9b6e9b |
|||
|
abba60c28b |
|||
|
8cea4b6cf3 |
|||
|
d46646d781 |
|||
|
e15d947fac |
|||
|
562ce5d4ff |
|||
|
f60189bfb8 |
|||
|
309d0df581 |
|||
|
3310d9b907 |
|||
|
8c047827de |
|||
|
d513e466fd |
|||
|
84a729aea8 |
|||
|
23e0aa3c4e |
|||
|
f0a26b58d1 |
|||
|
78b5abea7d |
|||
|
e1d7a30a06 |
|||
|
0c61623a94 |
|||
|
0a9b71230b |
|||
|
15c057707f |
|||
|
9cc18e16b8 |
|||
|
240984a832 |
|||
|
633e83b915 |
|||
|
c5122fc34f |
|||
|
457de3c77c |
|||
|
c9aa0173d8 |
|||
|
22aa26fd05 |
|||
|
c10c74a1f3 |
|||
|
46136c09ad |
|||
|
5f9622e1c9 |
|||
|
6e02cce84f |
|||
|
7e7e6877e2 |
|||
|
895ab3c96f |
|||
|
b25840756d |
|||
|
735a3357d7 |
|||
|
02b7892fb0 |
|||
|
3afb4befa5 |
|||
|
a11663eece |
|||
|
7f12efd338 |
|||
|
fcdada6f3e |
|||
|
0c950247b0 |
|||
|
27dca24889 |
|||
|
621bb74cc9 |
|||
|
33c3a568f9 |
|||
|
561f6ecc84 |
|||
|
a7ccf0ff00 |
|||
|
0e984fd95b |
|||
|
c898b2671a |
|||
|
e63e5a8c08 |
|||
|
a87c9dc678 |
|||
|
5fcc31b427 |
|||
|
99270f4bd1 |
|||
|
d2ec5f035b |
|||
|
6281c1d13a |
|||
|
60315de04a |
|||
|
eb87cbc17b |
|||
|
130dbac879 |
|||
|
2dd3fed62f |
|||
|
f60a499875 |
|||
|
a72ef5ea2d |
|||
|
1b81e31c13 |
|||
|
db12fd2e9a |
|||
|
60454ed249 |
|||
|
0a333e92d1 |
|||
|
31c3173bb5 |
|||
|
c6904e9766 |
|||
|
e5b7512e79 |
|||
|
5daa861a31 |
|||
|
82860deff1 |
|||
|
2ec442b35b |
|||
|
786c8cb3e2 |
|||
|
a150ef2ecf |
|||
|
bb037c8162 |
|||
|
eac59c58f5 |
|||
|
4e5c709fb9 |
|||
|
451875b137 |
|||
|
e1c81b504a |
|||
|
f27db22151 |
|||
|
3d6ebedd94 |
|||
|
70eba46c76 |
|||
|
ec64a81fed |
|||
|
2ba620350d |
|||
|
a0c2eefc1e |
|||
|
9939e59429 |
|||
|
e69f04f6e7 |
|||
|
4848840538 |
|||
|
64b3127fd1 |
|||
|
b836c3b06e |
|||
|
d9f62a9de9 |
|||
|
32dd08fd91 |
|||
|
6de046bf7b |
|||
|
ebbb4dc662 |
|||
|
1d6a121ec4 |
|||
|
ed0292468a |
|||
|
8b310c955d |
|||
|
14b140e23d |
|||
|
b47e61e005 |
|||
|
05bc9034d1 |
|||
|
d7ea233b18 |
|||
|
70ffbedba4 |
|||
|
a1386790a9 |
|||
|
68d2f654cc |
|||
|
5368bacf1d |
|||
| 564fe3c964 | |||
| 16cf126df4 | |||
|
bd1d5ea745 |
|||
|
1ca802c78b |
|||
|
7b9eda2d36 |
|||
|
63eb001c09 |
|||
|
00c3503c1f |
|||
|
469a625c40 |
|||
|
66ef2de027 |
|||
|
972c143de2 |
|||
|
2f39985afb |
|||
|
0ab466d011 |
|||
|
7857bcdc2e |
|||
|
9faff092ed |
|||
|
0949fa6523 |
|||
|
e310f0f60e |
|||
|
7d8b267986 |
|||
|
c945c26413 |
|||
|
4dc16a5529 |
|||
|
ebd4b8a765 |
|||
|
26b95fac69 |
|||
|
152331b262 |
|||
|
5f56c52fe0 |
|||
|
a8ae10c9ce |
|||
| ad36dcb2f3 | |||
|
|
8b4cd75076 | ||
|
5a99616e9c |
|||
|
def69d85e7 |
|||
|
f4b2dcb824 |
|||
|
50e1a8e4c7 |
|||
|
82dab26fd4 |
|||
|
313dc377ec |
16
.github/workflows/windows.yml
vendored
|
|
@ -19,13 +19,20 @@ jobs:
|
||||||
- name: Set up Flutter
|
- name: Set up Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: 3.41.5
|
flutter-version: 3.41.9
|
||||||
|
|
||||||
- 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: Setup MSYS2
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: MINGW64
|
||||||
|
install: >-
|
||||||
|
mingw-w64-x86_64-gcc
|
||||||
|
|
||||||
- name: Go build
|
- name: Go build
|
||||||
run: |
|
run: |
|
||||||
cd gomuks/pkg/ffi
|
cd gomuks/pkg/ffi
|
||||||
|
|
@ -38,6 +45,13 @@ jobs:
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
flutter build windows --release
|
flutter build windows --release
|
||||||
|
|
||||||
|
- name: Copy MinGW runtime DLLs
|
||||||
|
shell: msys2 {0}
|
||||||
|
run: |
|
||||||
|
cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/
|
||||||
|
cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/
|
||||||
|
cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/
|
||||||
|
|
||||||
- name: Upload exe zip
|
- name: Upload exe zip
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
5
.vscode/settings.json
vendored
|
|
@ -5,8 +5,11 @@
|
||||||
"fluttertagger",
|
"fluttertagger",
|
||||||
"Gomuks",
|
"Gomuks",
|
||||||
"Homeserver",
|
"Homeserver",
|
||||||
|
"Linkified",
|
||||||
"localpart",
|
"localpart",
|
||||||
|
"msgtype",
|
||||||
"muks",
|
"muks",
|
||||||
"prefs"
|
"prefs",
|
||||||
|
"unban"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Development Documentation
|
||||||
|
|
||||||
|
## Build instructions
|
||||||
|
|
||||||
|
Build instructions can be found in [README.md](./README.md#build-it-yourself).
|
||||||
|
|
||||||
|
## Updating Gomuks
|
||||||
|
|
||||||
|
You can run the following command to update the Gomuks submodule:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git submodule update --remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
See [Effective Dart: Style](https://dart.dev/effective-dart/style) for general rules. There are some extra rules detailed below:
|
||||||
|
|
||||||
|
### Controllers and Helpers ([Riverpod](https://pub.dev/packages/riverpod))
|
||||||
|
|
||||||
|
Controllers live in `lib/controllers/` and provide a source that exposes data and logic via Riverpod providers, allowing other parts of the code to watch state changes with ref.watch (`ref.watch(MyController.provider)`), access the current value with ref.read (`ref.read(MyController.provider)`), and run helper methods on those classes using the notifier:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ref.watch(MyController.provider.notifier).helperMethod()
|
||||||
|
```
|
||||||
|
|
||||||
|
We use an object oriented style for controllers, where `provider` is a static member on the controller class. E.g.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyController extends AsyncNotifier<DataThisControllerExposes> {
|
||||||
|
final SomeInputType input;
|
||||||
|
MyController(this.input);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DataThisControllerExposes> build() async {
|
||||||
|
return input.foo;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<MyController, DataThisControllerExposes, SomeInputType>(
|
||||||
|
AuthorController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Providers which are not controllers, e.g. they expose no data, only methods, should instead live in `lib/helpers/`. For an example, see `lib/helpers/launch_helper.dart`. Other, non-provider helpers, like extensions or helper methods can also go in `lib/helpers/`.
|
||||||
|
|
||||||
|
### Don't use StatefulWidgets ([Flutter Hooks](https://pub.dev/packages/flutter_hooks))
|
||||||
|
|
||||||
|
This project uses Flutter Hooks to help with boilerplate that StatefulWidgets create. Instead of using a StatefulWidget, we just use hooks like `useState` or `useEffect` in the build method of a `HookWidget`, which is a drop in replacement for `StatelessWidget`. If you need both a `WidgetRef` to watch providers, and access to hooks, use `HookConsumerWidget`.
|
||||||
|
|
||||||
|
### Models ([Freezed](https://pub.dev/packages/freezed))
|
||||||
|
|
||||||
|
We use Freezed for our models to avoid boilerplate and enforce an immutable style of state and data modeling throughout the code. See their documentation for more info, or see our existing models in `lib/models/`.
|
||||||
|
|
||||||
|
### Immutable Data Collections ([Fast Immutable Collections](https://pub.dev/packages/fast_immutable_collections))
|
||||||
|
|
||||||
|
When possible, use immutable collections instead of the mutable equivalent. For example, use `IMap` over `Map`, `IList` over `List`, `ISet` over `Set`. This matches the immutable style of Riverpod and Freezed.
|
||||||
|
|
||||||
|
### Don't create globals
|
||||||
|
|
||||||
|
When possible, we prefer not to create global variables or methods. You can usually replace a global variable with a Riverpod controller, and a global method with an extension method.
|
||||||
|
|
||||||
|
## LLM/AI Assisted Contributions
|
||||||
|
|
||||||
|
Largely LLM generated code is NOT allowed. All contributions should be written by humans, with minimal to no LLM assistance. Please disclose any usage of LLMs.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
All contributions must follow the [Federated Nexus Code of Conduct](https://federated.nexus/code/).
|
||||||
73
README.md
|
|
@ -15,15 +15,11 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
- [ ] New logo
|
|
||||||
- [ ] Make context menus appear as bottom sheets on mobile
|
|
||||||
- [x] Move from the Dart SDK to the Gomuks Backend with Dart bindings: https://git.federated.nexus/Henry-Hiles/nexus/pulls/2
|
|
||||||
- [ ] Allow using remote Gomuks over websocket
|
|
||||||
- [ ] Platform Support
|
- [ ] Platform Support
|
||||||
- [x] Linux
|
- [x] Linux
|
||||||
- [ ] Windows (WIP)
|
- [x] Windows
|
||||||
- [ ] MacOS
|
|
||||||
- [x] Android
|
- [x] Android
|
||||||
|
- [ ] MacOS
|
||||||
- [ ] iOS
|
- [ ] iOS
|
||||||
- [ ] Web (may not be possible)
|
- [ ] Web (may not be possible)
|
||||||
- [x] Login
|
- [x] Login
|
||||||
|
|
@ -43,7 +39,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] `matrix:` Uri
|
- [x] `matrix:` Uri
|
||||||
- [x] Matrix.to link
|
- [x] Matrix.to link
|
||||||
- [ ] From space
|
- [ ] From space
|
||||||
- [ ] Exploring
|
- [ ] From directory
|
||||||
- [x] Leaving
|
- [x] Leaving
|
||||||
- [x] Subspaces
|
- [x] Subspaces
|
||||||
- [x] Messages
|
- [x] Messages
|
||||||
|
|
@ -54,7 +50,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] HTML/Markdown
|
- [x] HTML/Markdown
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [x] Choose ping on/off
|
- [x] Choose ping on/off
|
||||||
- [ ] Per message profiles
|
- [x] Per message profiles
|
||||||
- [ ] Attachments
|
- [ ] 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] Mentions
|
||||||
|
|
@ -85,7 +81,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] Users
|
- [x] Users
|
||||||
- [x] Clickable
|
- [x] Clickable
|
||||||
- [x] Rooms
|
- [x] Rooms
|
||||||
- [x] Clickable
|
- [ ] Clickable
|
||||||
- [x] Matrix URIs
|
- [x] Matrix URIs
|
||||||
- [x] Matrix.to links
|
- [x] Matrix.to links
|
||||||
- [x] Events
|
- [x] Events
|
||||||
|
|
@ -111,15 +107,23 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [ ] Reporting
|
- [ ] Reporting
|
||||||
- [x] Events
|
- [x] Events
|
||||||
- [ ] Rooms
|
- [ ] Rooms
|
||||||
- [ ] Notifications using UnifiedPush
|
- [x] Member list
|
||||||
|
- [x] Sort by power level
|
||||||
|
- [ ] Colors based off of power level
|
||||||
|
- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35))
|
||||||
- [ ] 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
|
||||||
- [ ] Settings
|
- [ ] Settings ([#37](https://git.federated.nexus/Nexus/nexus/issues/37))
|
||||||
- [ ] Matrix: URIs vs Matrix.to links
|
- [ ] Matrix: URIs vs Matrix.to links
|
||||||
- [ ] Light/Dark mode
|
- [ ] Light/Dark mode
|
||||||
|
- [ ] Remote Gomuks instance
|
||||||
- [ ] SSD or CSD
|
- [ ] SSD or CSD
|
||||||
|
- [ ] Align your message bubbles to left or right
|
||||||
- [ ] Show media by default
|
- [ ] Show media by default
|
||||||
- [ ] Dynamic Theming
|
- [ ] Dynamic Theming
|
||||||
|
- [ ] Personas
|
||||||
|
- [ ] Setting per-message profiles for users (MSC4461)
|
||||||
|
- [ ] Explain how to send messages using a certain PMP
|
||||||
- [ ] Devices
|
- [ ] Devices
|
||||||
- [ ] Viewing devices
|
- [ ] Viewing devices
|
||||||
- [ ] Verifying devices
|
- [ ] Verifying devices
|
||||||
|
|
@ -145,7 +149,7 @@ If you want to try out Nexus, grab one of the following artifacts from CI:
|
||||||
- [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)
|
||||||
|
|
||||||
Or, try the Nix package: `nix run git+https://git.federated.nexus/Henry-Hiles/nexus`
|
Or, try the Nix package: `nix run git+https://git.federated.nexus/Nexus/nexus`
|
||||||
|
|
||||||
## Build it yourself
|
## Build it yourself
|
||||||
|
|
||||||
|
|
@ -156,16 +160,39 @@ Or, try the Nix package: `nix run git+https://git.federated.nexus/Henry-Hiles/ne
|
||||||
- With Nix: Either use direnv and `direnv allow`, or `nix flake develop`
|
- With Nix: Either use direnv and `direnv allow`, or `nix flake develop`
|
||||||
- Without Nix: Install Flutter, Go, Git, Libclang, and Glibc. Do not use any Snap packages, they cause various compilation issues.
|
- Without Nix: Install Flutter, Go, Git, Libclang, and Glibc. Do not use any Snap packages, they cause various compilation issues.
|
||||||
|
|
||||||
#### Windows / MacOS
|
#### Windows
|
||||||
|
|
||||||
I don't really know. You will need Flutter, Git, Go, and Visual Studio tools, and otherwise I guess just keep installing stuff until there aren't any errors. I will look into this sometimeTM.
|
You will need:
|
||||||
|
|
||||||
|
- Flutter
|
||||||
|
- Android SDK + NDK
|
||||||
|
- Git
|
||||||
|
- Go
|
||||||
|
- Visual Studio 2022 (Desktop development with C++)
|
||||||
|
- [MSYS2/MinGW-w64 GCC](https://www.msys2.org/) (for CGO)
|
||||||
|
- [LLVM/Clang + libclang](https://clang.llvm.org/get_started.html) (for `ffigen`)
|
||||||
|
|
||||||
|
On Windows, make sure these are available in your shell `PATH`:
|
||||||
|
|
||||||
|
- `C:\msys64\ucrt64\bin` (or your MinGW bin path containing `x86_64-w64-mingw32-gcc.exe`)
|
||||||
|
- `C:\Program Files\LLVM\bin` (contains `clang.exe` and `libclang.dll`)
|
||||||
|
|
||||||
|
For `dart scripts/generate.dart`, you may also need:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:CPATH = "C:\msys64\ucrt64\include"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MacOS
|
||||||
|
|
||||||
|
Similar prerequisites apply (Flutter, Git, Go, C toolchain, LLVM/libclang), but exact setup has not been fully documented yet.
|
||||||
|
|
||||||
### Clone repo
|
### Clone repo
|
||||||
|
|
||||||
First, clone and open the repo:
|
First, clone and open the repo:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone --recurse-submodules https://git.federated.nexus/Henry-Hiles/nexus
|
git clone --recurse-submodules https://git.federated.nexus/Nexus/nexus
|
||||||
cd nexus
|
cd nexus
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -183,10 +210,17 @@ Generate Gomuks bindings:
|
||||||
dart scripts/generate.dart
|
dart scripts/generate.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you are having issues with `stddef.h` not being found, try setting CPATH manually:
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"
|
||||||
|
> ```
|
||||||
|
|
||||||
Build generated files, and watch for new changes:
|
Build generated files, and watch for new changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
flutter pub run build_runner watch --delete-conflicting-outputs
|
flutter pub run build_runner watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the app:
|
Run the app:
|
||||||
|
|
@ -195,6 +229,13 @@ Run the app:
|
||||||
flutter run
|
flutter run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Development instructions can be found in [DEVELOPMENT.md](./DEVELOPMENT.md).
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client.
|
Join the [Nexus Client Matrix Room](https://matrix.to/#/#nexus:federated.nexus) for questions or help with developing or using Nexus Client.
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
|
||||||
|
Thank you Hylke Bons (https://planetpeanut.studio) for making the amazing icon for Nexus!
|
||||||
|
Thank you Tulir Asokan for making [Gomuks](https://github.com/gomuks/gomuks), and helping us integrate it into Nexus!
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
|
@ -6,4 +6,9 @@
|
||||||
android:drawable="@drawable/ic_launcher_foreground"
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
android:inset="16%" />
|
android:inset="16%" />
|
||||||
</foreground>
|
</foreground>
|
||||||
|
<monochrome>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_monochrome"
|
||||||
|
android:inset="16%" />
|
||||||
|
</monochrome>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 21 KiB |
|
|
@ -1,21 +1,22 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="100mm"
|
width="512"
|
||||||
height="100mm"
|
height="512"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 512 512"
|
||||||
|
fill="none"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg11"
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
|
||||||
sodipodi:docname="background.svg"
|
sodipodi:docname="background.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
inkscape:export-filename="background.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
id="namedview1"
|
<sodipodi:namedview
|
||||||
|
id="namedview11"
|
||||||
pagecolor="#505050"
|
pagecolor="#505050"
|
||||||
bordercolor="#eeeeee"
|
bordercolor="#eeeeee"
|
||||||
borderopacity="1"
|
borderopacity="1"
|
||||||
|
|
@ -23,40 +24,234 @@
|
||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:document-units="mm"
|
inkscape:zoom="0.69191503"
|
||||||
inkscape:zoom="1.0847363"
|
inkscape:cx="-71.540576"
|
||||||
inkscape:cx="57.156749"
|
inkscape:cy="281.10388"
|
||||||
inkscape:cy="214.33781"
|
inkscape:window-width="2544"
|
||||||
inkscape:window-width="1904"
|
inkscape:window-height="1363"
|
||||||
inkscape:window-height="971"
|
|
||||||
inkscape:window-x="35"
|
inkscape:window-x="35"
|
||||||
inkscape:window-y="32"
|
inkscape:window-y="32"
|
||||||
inkscape:window-maximized="0"
|
inkscape:window-maximized="0"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="svg11" />
|
||||||
id="defs1"><linearGradient
|
<defs
|
||||||
id="linearGradient10"
|
id="defs11">
|
||||||
inkscape:collect="always"><stop
|
<radialGradient
|
||||||
style="stop-color:#c7a312;stop-opacity:1;"
|
id="paint0_radial_4033_8"
|
||||||
offset="0"
|
cx="0"
|
||||||
id="stop10" /><stop
|
cy="0"
|
||||||
style="stop-color:#26a0b3;stop-opacity:1;"
|
r="1"
|
||||||
|
gradientTransform="matrix(3.5,24.5214,-15.8099,2.26053,80,52.9904)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop11" /></linearGradient><linearGradient
|
stop-color="#3584E4"
|
||||||
inkscape:collect="always"
|
id="stop11" />
|
||||||
xlink:href="#linearGradient10"
|
</radialGradient>
|
||||||
id="linearGradient11"
|
<mask
|
||||||
x1="20.031296"
|
id="mask0_4033_8"
|
||||||
y1="32.697563"
|
maskUnits="userSpaceOnUse"
|
||||||
x2="90.709213"
|
x="56"
|
||||||
y2="66.3423"
|
y="46"
|
||||||
gradientUnits="userSpaceOnUse" /></defs><g
|
width="21"
|
||||||
inkscape:label="Layer 1"
|
height="36">
|
||||||
inkscape:groupmode="layer"
|
<path
|
||||||
id="layer1"><rect
|
d="m 76.4223,63.1501 c 0.3482,0.5123 0.3457,1.1859 -0.0063,1.6956 L 65.0175,81.3524 C 64.7375,81.7579 64.2761,82 63.7832,82 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.332 l 9.8906,-14.8359 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6641 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6568 z"
|
||||||
style="fill:url(#linearGradient11);fill-opacity:1;stroke:none;stroke-width:7.99999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
fill="#2779dd"
|
||||||
id="rect10"
|
id="path9" />
|
||||||
width="100"
|
</mask>
|
||||||
height="100"
|
<clipPath
|
||||||
x="0"
|
clipPathUnits="userSpaceOnUse"
|
||||||
y="0"
|
id="clipPath11">
|
||||||
ry="28.294127" /></g></svg>
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect12"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath12">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect13"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath13">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect14"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath14">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect15"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath15">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect16"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath16">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect17"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_4033_8-3"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(3.5,24.5214,-15.8099,2.26053,174.26633,65.9904)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10-6" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop11-7" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_4033_8-35"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(3.5,24.5214,-15.8099,2.26053,80,52.9904)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10-62" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop11-9" />
|
||||||
|
</radialGradient>
|
||||||
|
<mask
|
||||||
|
id="mask0_4033_8-1"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="56"
|
||||||
|
y="46"
|
||||||
|
width="21"
|
||||||
|
height="36">
|
||||||
|
<path
|
||||||
|
d="m 76.4223,63.1501 c 0.3482,0.5123 0.3457,1.1859 -0.0063,1.6956 L 65.0175,81.3524 C 64.7375,81.7579 64.2761,82 63.7832,82 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.332 l 9.8906,-14.8359 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6641 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6568 z"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="path9-2" />
|
||||||
|
</mask>
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_4033_8-9"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(14,98.0856,-63.2396,9.04212,320,211.9616)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10-3" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop11-6" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect1"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="stroke-width:4" />
|
||||||
|
<rect
|
||||||
|
x="-1.5384758"
|
||||||
|
y="-122.66472"
|
||||||
|
width="35.5569"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#9141ac"
|
||||||
|
id="rect2"
|
||||||
|
clip-path="url(#clipPath16)" />
|
||||||
|
<rect
|
||||||
|
x="34.018467"
|
||||||
|
y="-122.66468"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#62a0ea"
|
||||||
|
id="rect3"
|
||||||
|
clip-path="url(#clipPath15)" />
|
||||||
|
<rect
|
||||||
|
x="60.68605"
|
||||||
|
y="-122.66468"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#57e389"
|
||||||
|
id="rect4"
|
||||||
|
clip-path="url(#clipPath14)" />
|
||||||
|
<rect
|
||||||
|
x="87.353859"
|
||||||
|
y="-122.66468"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#f5c211"
|
||||||
|
id="rect5"
|
||||||
|
clip-path="url(#clipPath13)" />
|
||||||
|
<rect
|
||||||
|
x="114.02161"
|
||||||
|
y="-122.66477"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#ff7800"
|
||||||
|
id="rect6"
|
||||||
|
clip-path="url(#clipPath12)" />
|
||||||
|
<rect
|
||||||
|
x="140.68942"
|
||||||
|
y="-122.66477"
|
||||||
|
width="35.5569"
|
||||||
|
height="291.86301"
|
||||||
|
transform="matrix(3.4641016,2,-2,3.4641016,0,0)"
|
||||||
|
fill="#ed333b"
|
||||||
|
id="rect7"
|
||||||
|
clip-path="url(#clipPath11)" />
|
||||||
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -1,20 +1,19 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="100mm"
|
width="512"
|
||||||
height="100mm"
|
height="512"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 512 512"
|
||||||
|
fill="none"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg11"
|
||||||
xml:space="preserve"
|
sodipodi:docname="foreground.svg"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
sodipodi:docname="nexus.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
id="namedview1"
|
<sodipodi:namedview
|
||||||
|
id="namedview11"
|
||||||
pagecolor="#505050"
|
pagecolor="#505050"
|
||||||
bordercolor="#eeeeee"
|
bordercolor="#eeeeee"
|
||||||
borderopacity="1"
|
borderopacity="1"
|
||||||
|
|
@ -22,105 +21,137 @@
|
||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:document-units="mm"
|
inkscape:zoom="0.87695313"
|
||||||
inkscape:zoom="1.0847363"
|
inkscape:cx="152.23163"
|
||||||
inkscape:cx="58.07863"
|
inkscape:cy="347.22494"
|
||||||
inkscape:cy="214.3378"
|
inkscape:window-width="2544"
|
||||||
inkscape:window-width="1896"
|
inkscape:window-height="1363"
|
||||||
inkscape:window-height="987"
|
|
||||||
inkscape:window-x="35"
|
inkscape:window-x="35"
|
||||||
inkscape:window-y="32"
|
inkscape:window-y="32"
|
||||||
inkscape:window-maximized="0"
|
inkscape:window-maximized="0"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="svg11" />
|
||||||
id="defs1" /><g
|
<path
|
||||||
inkscape:label="Layer 1"
|
d="m 256,92 c 90.5748,0 164,73.4252 164,164 0,90.5748 -73.4252,164 -164,164 -34.5828,0 -66.6592,-10.712 -93.1092,-28.9884 l -39.2072,8.7656 c -6.8668,1.5348 -12.9952,-4.594 -11.4608,-11.4608 l 8.7616,-39.2108 C 102.7104,322.6564 92,290.5808 92,256 92,165.4252 165.4252,92 256,92 Z"
|
||||||
inkscape:groupmode="layer"
|
fill="#ffffff"
|
||||||
id="layer1"><path
|
id="path7"
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
style="stroke-width:4" />
|
||||||
d="M 19.377906,68.106953 80.937684,32.43771"
|
<path
|
||||||
id="path10" /><path
|
d="m 304.9188,251.4672 c 1.8572,2.732 1.844,6.3248 -0.0332,9.0432 L 260.6664,324.546 C 259.1728,326.7088 256.712,328 254.0836,328 H 234.948 c -6.3896,0 -10.2004,-7.1212 -6.6564,-12.4376 l 36.75,-55.1248 c 1.7916,-2.6872 1.7916,-6.188 0,-8.8752 l -36.75,-55.1248 C 224.7476,191.1212 228.5584,184 234.948,184 h 19.8748 c 2.6492,0 5.1268,1.3116 6.616,3.5028 z"
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
fill="url(#paint0_radial_4033_8)"
|
||||||
d="m 19.044488,32.469148 61.61782,35.569625"
|
id="path8"
|
||||||
id="path9" /><path
|
style="fill:url(#paint0_radial_4033_8);stroke-width:4" />
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
<g
|
||||||
d="M 50,85.574911 V 14.425087"
|
mask="url(#mask0_4033_8)"
|
||||||
id="path8" /><circle
|
id="g9"
|
||||||
style="fill:none;stroke:#ffffff;stroke-width:7.11498;stroke-linecap:round;stroke-linejoin:round"
|
transform="scale(4)">
|
||||||
id="path1"
|
<rect
|
||||||
cx="50"
|
x="52"
|
||||||
cy="50"
|
y="46"
|
||||||
r="35.574913" /><circle
|
width="17"
|
||||||
style="fill:#09bd05;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
height="4"
|
||||||
id="path2"
|
fill="#2779dd"
|
||||||
cx="50"
|
id="rect9" />
|
||||||
cy="84.604881"
|
</g>
|
||||||
r="8.2508707" /><circle
|
<defs
|
||||||
style="fill:#fe1e24;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
id="defs11">
|
||||||
id="circle2"
|
<radialGradient
|
||||||
cx="50"
|
id="paint0_radial_4033_8"
|
||||||
cy="15.395123"
|
cx="0"
|
||||||
r="8.2508707" /><circle
|
cy="0"
|
||||||
style="fill:#fe941d;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
r="1"
|
||||||
id="circle3"
|
gradientTransform="matrix(14,98.0856,-63.2396,9.04212,320,211.9616)"
|
||||||
cx="-68.30127"
|
gradientUnits="userSpaceOnUse">
|
||||||
cy="52.906147"
|
<stop
|
||||||
r="8.2508707"
|
stop-color="#72AAEE"
|
||||||
transform="rotate(-120)" /><circle
|
id="stop10" />
|
||||||
style="fill:#001996;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
<stop
|
||||||
id="circle4"
|
offset="1"
|
||||||
cx="-68.30127"
|
stop-color="#3584E4"
|
||||||
cy="-16.30361"
|
id="stop11" />
|
||||||
r="8.2508707"
|
</radialGradient>
|
||||||
transform="rotate(-120)" /><circle
|
<mask
|
||||||
style="fill:#ffff04;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
id="mask0_4033_8"
|
||||||
id="circle5"
|
maskUnits="userSpaceOnUse"
|
||||||
cx="-18.301271"
|
x="56"
|
||||||
cy="102.90615"
|
y="46"
|
||||||
r="8.2508707"
|
width="21"
|
||||||
transform="rotate(-60)" /><circle
|
height="36">
|
||||||
style="fill:#770287;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
<path
|
||||||
id="circle6"
|
d="m 76.4223,63.1501 c 0.3482,0.5123 0.3457,1.1859 -0.0063,1.6956 L 65.0175,81.3524 C 64.7375,81.7579 64.2761,82 63.7832,82 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.332 l 9.8906,-14.8359 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6641 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6568 z"
|
||||||
cx="-18.301271"
|
fill="#2779dd"
|
||||||
cy="33.696392"
|
id="path9" />
|
||||||
r="8.2508707"
|
</mask>
|
||||||
transform="rotate(-60)" /><circle
|
<clipPath
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.75;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
clipPathUnits="userSpaceOnUse"
|
||||||
id="path7"
|
id="clipPath11">
|
||||||
cx="50"
|
<rect
|
||||||
cy="50"
|
width="128"
|
||||||
r="9.7918472" /><g
|
height="128"
|
||||||
inkscape:label="Layer 1"
|
fill="#ffffff"
|
||||||
id="layer1-3"
|
id="rect12"
|
||||||
transform="matrix(0.08246781,0,0,0.08246781,38.828362,38.828362)"
|
x="0"
|
||||||
style="stroke:#ffffff"><text
|
y="0"
|
||||||
xml:space="preserve"
|
transform="rotate(-30)" />
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
</clipPath>
|
||||||
x="-305.64749"
|
<clipPath
|
||||||
y="194.14493"
|
clipPathUnits="userSpaceOnUse"
|
||||||
id="text2819"><tspan
|
id="clipPath12">
|
||||||
sodipodi:role="line"
|
<rect
|
||||||
id="tspan2817"
|
width="128"
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
height="128"
|
||||||
x="-305.64749"
|
fill="#ffffff"
|
||||||
y="194.14493" /></text><circle
|
id="rect13"
|
||||||
style="fill:#354b5f;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
x="0"
|
||||||
id="path342"
|
y="0"
|
||||||
cx="135.46666"
|
transform="rotate(-30)" />
|
||||||
cy="135.46666"
|
</clipPath>
|
||||||
r="135.46666" /><text
|
<clipPath
|
||||||
xml:space="preserve"
|
clipPathUnits="userSpaceOnUse"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
id="clipPath13">
|
||||||
x="-305.64749"
|
<rect
|
||||||
y="194.14493"
|
width="128"
|
||||||
id="text2819-3"><tspan
|
height="128"
|
||||||
sodipodi:role="line"
|
fill="#ffffff"
|
||||||
id="tspan2817-5"
|
id="rect14"
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
x="0"
|
||||||
x="-305.64749"
|
y="0"
|
||||||
y="194.14493" /></text><g
|
transform="rotate(-30)" />
|
||||||
aria-label="❯"
|
</clipPath>
|
||||||
id="text2827-6"
|
<clipPath
|
||||||
style="font-size:132.452px;line-height:0;font-family:PowerlineSymbols;-inkscape-font-specification:'PowerlineSymbols, Normal';text-align:end;text-anchor:end;fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0"><path
|
clipPathUnits="userSpaceOnUse"
|
||||||
d="M 95.096912,209.8167 143.88912,135.46666 95.096912,61.116629 h 32.818568 l 47.92093,74.350031 -47.92093,74.35004 z"
|
id="clipPath14">
|
||||||
id="path2883-2"
|
<rect
|
||||||
style="fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0" /></g></g></g></svg>
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect15"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath15">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect16"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath16">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect17"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 26 KiB |
448
assets/icon.svg
|
|
@ -1,21 +1,22 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="100mm"
|
width="512"
|
||||||
height="100mm"
|
height="512"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 512 512"
|
||||||
|
fill="none"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg35"
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon.svg"
|
sodipodi:docname="icon.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
inkscape:export-filename="icon.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
id="namedview1"
|
<sodipodi:namedview
|
||||||
|
id="namedview35"
|
||||||
pagecolor="#505050"
|
pagecolor="#505050"
|
||||||
bordercolor="#eeeeee"
|
bordercolor="#eeeeee"
|
||||||
borderopacity="1"
|
borderopacity="1"
|
||||||
|
|
@ -23,128 +24,311 @@
|
||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:document-units="mm"
|
inkscape:zoom="1.321682"
|
||||||
inkscape:zoom="1.0847363"
|
inkscape:cx="69.608271"
|
||||||
inkscape:cx="57.61769"
|
inkscape:cy="120.67956"
|
||||||
inkscape:cy="214.33781"
|
inkscape:window-width="2544"
|
||||||
inkscape:window-width="1896"
|
inkscape:window-height="1363"
|
||||||
inkscape:window-height="963"
|
|
||||||
inkscape:window-x="35"
|
inkscape:window-x="35"
|
||||||
inkscape:window-y="32"
|
inkscape:window-y="32"
|
||||||
inkscape:window-maximized="0"
|
inkscape:window-maximized="0"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="svg35" />
|
||||||
id="defs1"><linearGradient
|
<mask
|
||||||
id="linearGradient10"
|
id="mask0_4023_558"
|
||||||
inkscape:collect="always"><stop
|
maskUnits="userSpaceOnUse"
|
||||||
style="stop-color:#c7a312;stop-opacity:1;"
|
x="12"
|
||||||
offset="0"
|
y="100"
|
||||||
id="stop10" /><stop
|
width="88"
|
||||||
style="stop-color:#26a0b3;stop-opacity:1;"
|
height="16">
|
||||||
|
<path
|
||||||
|
d="m 100,104 c 0,6.627 -5.3726,12 -12,12 H 24 c -6.6274,0 -12,-5.373 -12,-12 v -4 c 0,6.627 5.3726,12 12,12 h 64 c 6.6274,0 12,-5.373 12,-12 z"
|
||||||
|
fill="#d9d9d9"
|
||||||
|
id="path6" />
|
||||||
|
</mask>
|
||||||
|
<mask
|
||||||
|
id="mask1_4023_558"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="77"
|
||||||
|
y="27"
|
||||||
|
width="21"
|
||||||
|
height="36">
|
||||||
|
<path
|
||||||
|
d="m 97.4223,44.1501 c 0.3482,0.5122 0.3457,1.1859 -0.0063,1.6956 L 86.0175,62.3523 C 85.7375,62.7579 85.2761,63 84.7832,63 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.3321 l 9.8906,-14.8358 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6642 L 78.0729,30.1094 C 77.1869,28.7803 78.1396,27 79.737,27 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6567 z"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="path14" />
|
||||||
|
</mask>
|
||||||
|
<g
|
||||||
|
id="g36"
|
||||||
|
transform="scale(4)">
|
||||||
|
<g
|
||||||
|
clip-path="url(#clip0_4023_558)"
|
||||||
|
id="g6">
|
||||||
|
<rect
|
||||||
|
x="62.0205"
|
||||||
|
y="-52.425598"
|
||||||
|
width="24"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,62.0205,-52.4256)"
|
||||||
|
fill="#9141ac"
|
||||||
|
id="rect1" />
|
||||||
|
<rect
|
||||||
|
x="82.805099"
|
||||||
|
y="-40.425598"
|
||||||
|
width="18"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,82.8051,-40.4256)"
|
||||||
|
fill="#62a0ea"
|
||||||
|
id="rect2" />
|
||||||
|
<rect
|
||||||
|
x="98.3936"
|
||||||
|
y="-31.4256"
|
||||||
|
width="18"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,98.3936,-31.4256)"
|
||||||
|
fill="#57e389"
|
||||||
|
id="rect3" />
|
||||||
|
<rect
|
||||||
|
x="113.982"
|
||||||
|
y="-22.4256"
|
||||||
|
width="18"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,113.982,-22.4256)"
|
||||||
|
fill="#f5c211"
|
||||||
|
id="rect4" />
|
||||||
|
<rect
|
||||||
|
x="129.57001"
|
||||||
|
y="-13.4256"
|
||||||
|
width="18"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,129.57,-13.4256)"
|
||||||
|
fill="#ff7800"
|
||||||
|
id="rect5" />
|
||||||
|
<rect
|
||||||
|
x="145.159"
|
||||||
|
y="-4.4256301"
|
||||||
|
width="24"
|
||||||
|
height="197"
|
||||||
|
transform="rotate(30,145.159,-4.42563)"
|
||||||
|
fill="#ed333b"
|
||||||
|
id="rect6" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
mask="url(#mask0_4023_558)"
|
||||||
|
id="g10">
|
||||||
|
<path
|
||||||
|
d="m 12,100 h 24 v 16 H 12 Z"
|
||||||
|
fill="url(#paint0_linear_4023_558)"
|
||||||
|
id="path7"
|
||||||
|
style="fill:url(#paint0_linear_4023_558)" />
|
||||||
|
<path
|
||||||
|
d="m 12,100 h 5 v 16 h -5 z"
|
||||||
|
fill="url(#paint1_linear_4023_558)"
|
||||||
|
id="path8"
|
||||||
|
style="fill:url(#paint1_linear_4023_558)" />
|
||||||
|
<rect
|
||||||
|
x="36"
|
||||||
|
y="100"
|
||||||
|
width="21"
|
||||||
|
height="16"
|
||||||
|
fill="#ca9005"
|
||||||
|
id="rect8" />
|
||||||
|
<rect
|
||||||
|
x="57"
|
||||||
|
y="100"
|
||||||
|
width="21"
|
||||||
|
height="16"
|
||||||
|
fill="#c64600"
|
||||||
|
id="rect9" />
|
||||||
|
<rect
|
||||||
|
x="78"
|
||||||
|
y="100"
|
||||||
|
width="22"
|
||||||
|
height="16"
|
||||||
|
fill="url(#paint2_linear_4023_558)"
|
||||||
|
id="rect10"
|
||||||
|
style="fill:url(#paint2_linear_4023_558)" />
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
opacity="0.2"
|
||||||
|
x="24.5"
|
||||||
|
y="110.5"
|
||||||
|
width="63"
|
||||||
|
height="1"
|
||||||
|
stroke="url(#paint3_linear_4023_558)"
|
||||||
|
id="rect11"
|
||||||
|
style="stroke:url(#paint3_linear_4023_558)" />
|
||||||
|
<path
|
||||||
|
d="m 85,4 c 22.644,0 41,18.3563 41,41 v 4 c 0,22.6437 -18.356,41 -41,41 -8.3322,0 -16.0825,-2.4875 -22.5526,-6.7576 -0.4653,-0.3071 -1.0341,-0.4203 -1.5783,-0.2987 l -8.8369,1.9749 C 52.0134,84.9228 52,84.9395 52,84.9588 52,84.9816 51.9816,85 51.9588,85 h -0.4617 c -0.0787,0.0036 -0.1564,0.004 -0.2334,0 H 51 c -1.1046,0 -2,-0.8954 -2,-2 v -0.2617 c -0.0043,-0.0811 -0.0042,-0.1632 0,-0.2461 V 78.9749 C 49,78.7126 49.2126,78.5 49.4749,78.5 c 0.2224,0 0.4151,-0.1543 0.4635,-0.3714 l 1.117,-4.9987 C 51.177,72.5857 51.0638,72.017 50.7567,71.5517 46.487,65.0818 44,57.3317 44,49 V 45 C 44,22.3563 62.3563,4 85,4 Z"
|
||||||
|
fill="url(#paint4_linear_4023_558)"
|
||||||
|
id="path11"
|
||||||
|
style="fill:url(#paint4_linear_4023_558)" />
|
||||||
|
<path
|
||||||
|
d="m 85,4 c 22.644,0 41,18.3563 41,41 0,22.6437 -18.356,41 -41,41 -8.6457,0 -16.6648,-2.6781 -23.2773,-7.2471 l -9.8018,2.1914 c -1.7167,0.3836 -3.2488,-1.1485 -2.8652,-2.8652 l 2.1904,-9.8027 C 46.6776,61.6641 44,53.6452 44,45 44,22.3563 62.3563,4 85,4 Z"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path12" />
|
||||||
|
<path
|
||||||
|
d="m 97.2297,43.8668 c 0.4642,0.683 0.4609,1.5812 -0.0083,2.2608 L 86.1666,62.1365 C 85.7932,62.6772 85.178,63 84.5209,63 H 79.737 c -1.5974,0 -2.5502,-1.7803 -1.6641,-3.1094 l 9.1875,-13.7812 c 0.4478,-0.6718 0.4478,-1.547 0,-2.2188 L 78.0729,30.1094 C 77.1868,28.7803 78.1396,27 79.737,27 h 4.9687 c 0.6623,0 1.2817,0.3279 1.654,0.8757 z"
|
||||||
|
fill="url(#paint5_radial_4023_558)"
|
||||||
|
id="path13"
|
||||||
|
style="fill:url(#paint5_radial_4023_558)" />
|
||||||
|
<g
|
||||||
|
mask="url(#mask1_4023_558)"
|
||||||
|
id="g14">
|
||||||
|
<rect
|
||||||
|
x="73"
|
||||||
|
y="27"
|
||||||
|
width="17"
|
||||||
|
height="4"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="rect14" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs35">
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_4023_558"
|
||||||
|
x1="34"
|
||||||
|
y1="108"
|
||||||
|
x2="12"
|
||||||
|
y2="108"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
offset="0.401381"
|
||||||
|
stop-color="#26A269"
|
||||||
|
id="stop14" />
|
||||||
|
<stop
|
||||||
|
offset="0.801049"
|
||||||
|
stop-color="#8AEB52"
|
||||||
|
id="stop15" />
|
||||||
|
<stop
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop11" /></linearGradient><linearGradient
|
stop-color="#26A269"
|
||||||
inkscape:collect="always"
|
id="stop16" />
|
||||||
xlink:href="#linearGradient10"
|
</linearGradient>
|
||||||
id="linearGradient11"
|
<linearGradient
|
||||||
x1="20.031296"
|
id="paint1_linear_4023_558"
|
||||||
y1="32.697563"
|
x1="12"
|
||||||
x2="90.709213"
|
y1="108"
|
||||||
y2="66.3423"
|
x2="17"
|
||||||
gradientUnits="userSpaceOnUse" /></defs><g
|
y2="108"
|
||||||
inkscape:label="Layer 1"
|
gradientUnits="userSpaceOnUse">
|
||||||
inkscape:groupmode="layer"
|
<stop
|
||||||
id="layer1"><rect
|
stop-color="#1A5FB4"
|
||||||
style="fill:url(#linearGradient11);fill-opacity:1;stroke:none;stroke-width:7.99999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
id="stop17" />
|
||||||
id="rect10"
|
<stop
|
||||||
width="100"
|
offset="1"
|
||||||
height="100"
|
stop-color="#35E0F6"
|
||||||
x="0"
|
id="stop18" />
|
||||||
y="0"
|
</linearGradient>
|
||||||
ry="28.294127" /><path
|
<linearGradient
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
id="paint2_linear_4023_558"
|
||||||
d="M 19.377906,68.106953 80.937684,32.43771"
|
x1="100"
|
||||||
id="path10" /><path
|
y1="108"
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
x2="78"
|
||||||
d="m 19.044488,32.469148 61.61782,35.569625"
|
y2="108"
|
||||||
id="path9" /><path
|
gradientUnits="userSpaceOnUse">
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
<stop
|
||||||
d="M 50,85.574911 V 14.425087"
|
stop-color="#A51D2D"
|
||||||
id="path8" /><circle
|
id="stop19" />
|
||||||
style="fill:none;stroke:#ffffff;stroke-width:7.11498;stroke-linecap:round;stroke-linejoin:round"
|
<stop
|
||||||
id="path1"
|
offset="0.195858"
|
||||||
cx="50"
|
stop-color="#E5673C"
|
||||||
cy="50"
|
id="stop20" />
|
||||||
r="35.574913" /><circle
|
<stop
|
||||||
style="fill:#09bd05;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
offset="0.5983"
|
||||||
id="path2"
|
stop-color="#A51D2D"
|
||||||
cx="50"
|
id="stop21" />
|
||||||
cy="84.604881"
|
</linearGradient>
|
||||||
r="8.2508707" /><circle
|
<linearGradient
|
||||||
style="fill:#fe1e24;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
id="paint3_linear_4023_558"
|
||||||
id="circle2"
|
x1="88"
|
||||||
cx="50"
|
y1="111.329"
|
||||||
cy="15.395123"
|
x2="24"
|
||||||
r="8.2508707" /><circle
|
y2="111.329"
|
||||||
style="fill:#fe941d;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
gradientUnits="userSpaceOnUse">
|
||||||
id="circle3"
|
<stop
|
||||||
cx="-68.30127"
|
offset="0.102371"
|
||||||
cy="52.906147"
|
stop-color="white"
|
||||||
r="8.2508707"
|
stop-opacity="0"
|
||||||
transform="rotate(-120)" /><circle
|
id="stop22" />
|
||||||
style="fill:#001996;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
<stop
|
||||||
id="circle4"
|
offset="0.253808"
|
||||||
cx="-68.30127"
|
stop-color="white"
|
||||||
cy="-16.30361"
|
id="stop23" />
|
||||||
r="8.2508707"
|
<stop
|
||||||
transform="rotate(-120)" /><circle
|
offset="0.747697"
|
||||||
style="fill:#ffff04;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
stop-color="white"
|
||||||
id="circle5"
|
id="stop24" />
|
||||||
cx="-18.301271"
|
<stop
|
||||||
cy="102.90615"
|
offset="0.895556"
|
||||||
r="8.2508707"
|
stop-color="white"
|
||||||
transform="rotate(-60)" /><circle
|
stop-opacity="0"
|
||||||
style="fill:#770287;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;fill-opacity:1;stroke-dasharray:none"
|
id="stop25" />
|
||||||
id="circle6"
|
</linearGradient>
|
||||||
cx="-18.301271"
|
<linearGradient
|
||||||
cy="33.696392"
|
id="paint4_linear_4023_558"
|
||||||
r="8.2508707"
|
x1="44"
|
||||||
transform="rotate(-60)" /><circle
|
y1="48.036098"
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.75;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
x2="126"
|
||||||
id="path7"
|
y2="48.036098"
|
||||||
cx="50"
|
gradientUnits="userSpaceOnUse">
|
||||||
cy="50"
|
<stop
|
||||||
r="9.7918472" /><g
|
stop-color="#DBEBF4"
|
||||||
inkscape:label="Layer 1"
|
id="stop26" />
|
||||||
id="layer1-3"
|
<stop
|
||||||
transform="matrix(0.08246781,0,0,0.08246781,38.828362,38.828362)"
|
offset="0.147387"
|
||||||
style="stroke:#ffffff"><text
|
stop-color="#B1D4E7"
|
||||||
xml:space="preserve"
|
id="stop27" />
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
<stop
|
||||||
x="-305.64749"
|
offset="0.186621"
|
||||||
y="194.14493"
|
stop-color="#8DC0DC"
|
||||||
id="text2819"><tspan
|
id="stop28" />
|
||||||
sodipodi:role="line"
|
<stop
|
||||||
id="tspan2817"
|
offset="0.203755"
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
stop-color="#49AEE7"
|
||||||
x="-305.64749"
|
id="stop29" />
|
||||||
y="194.14493" /></text><circle
|
<stop
|
||||||
style="fill:#354b5f;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
offset="0.276122"
|
||||||
id="path342"
|
stop-color="#7AB5D7"
|
||||||
cx="135.46666"
|
id="stop30" />
|
||||||
cy="135.46666"
|
<stop
|
||||||
r="135.46666" /><text
|
offset="0.399628"
|
||||||
xml:space="preserve"
|
stop-color="#B3D6E7"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
id="stop31" />
|
||||||
x="-305.64749"
|
<stop
|
||||||
y="194.14493"
|
offset="0.507537"
|
||||||
id="text2819-3"><tspan
|
stop-color="#B3D6E7"
|
||||||
sodipodi:role="line"
|
id="stop32" />
|
||||||
id="tspan2817-5"
|
<stop
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
offset="1"
|
||||||
x="-305.64749"
|
stop-color="#DBEBF4"
|
||||||
y="194.14493" /></text><g
|
id="stop33" />
|
||||||
aria-label="❯"
|
</linearGradient>
|
||||||
id="text2827-6"
|
<radialGradient
|
||||||
style="font-size:132.452px;line-height:0;font-family:PowerlineSymbols;-inkscape-font-specification:'PowerlineSymbols, Normal';text-align:end;text-anchor:end;fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0"><path
|
id="paint5_radial_4023_558"
|
||||||
d="M 95.096912,209.8167 143.88912,135.46666 95.096912,61.116629 h 32.818568 l 47.92093,74.350031 -47.92093,74.35004 z"
|
cx="0"
|
||||||
id="path2883-2"
|
cy="0"
|
||||||
style="fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0" /></g></g></g></svg>
|
r="1"
|
||||||
|
gradientTransform="matrix(3.5,24.5214,-15.8099,2.26053,101,33.9904)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop34" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop35" />
|
||||||
|
</radialGradient>
|
||||||
|
<clipPath
|
||||||
|
id="clip0_4023_558">
|
||||||
|
<rect
|
||||||
|
x="12"
|
||||||
|
y="36"
|
||||||
|
width="88"
|
||||||
|
height="80"
|
||||||
|
rx="12"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect35" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
assets/mobile.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
156
assets/mobile.svg
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
sodipodi:docname="mobile.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview11"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="30.03125"
|
||||||
|
inkscape:cx="14.51821"
|
||||||
|
inkscape:cy="11.038502"
|
||||||
|
inkscape:window-width="2544"
|
||||||
|
inkscape:window-height="1363"
|
||||||
|
inkscape:window-x="35"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg11" />
|
||||||
|
<g
|
||||||
|
id="g11"
|
||||||
|
transform="scale(4)">
|
||||||
|
<g
|
||||||
|
clip-path="url(#clip0_4033_8)"
|
||||||
|
id="g10">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect1"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
<rect
|
||||||
|
x="60"
|
||||||
|
y="-107"
|
||||||
|
width="35.5569"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,60,-107)"
|
||||||
|
fill="#9141ac"
|
||||||
|
id="rect2" />
|
||||||
|
<rect
|
||||||
|
x="90.793198"
|
||||||
|
y="-89.221497"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,90.7932,-89.2215)"
|
||||||
|
fill="#62a0ea"
|
||||||
|
id="rect3" />
|
||||||
|
<rect
|
||||||
|
x="113.888"
|
||||||
|
y="-75.887703"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,113.888,-75.8877)"
|
||||||
|
fill="#57e389"
|
||||||
|
id="rect4" />
|
||||||
|
<rect
|
||||||
|
x="136.983"
|
||||||
|
y="-62.553799"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,136.983,-62.5538)"
|
||||||
|
fill="#f5c211"
|
||||||
|
id="rect5" />
|
||||||
|
<rect
|
||||||
|
x="160.078"
|
||||||
|
y="-49.220001"
|
||||||
|
width="26.6677"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,160.078,-49.22)"
|
||||||
|
fill="#ff7800"
|
||||||
|
id="rect6" />
|
||||||
|
<rect
|
||||||
|
x="183.173"
|
||||||
|
y="-35.886101"
|
||||||
|
width="35.5569"
|
||||||
|
height="291.86301"
|
||||||
|
transform="rotate(30,183.173,-35.8861)"
|
||||||
|
fill="#ed333b"
|
||||||
|
id="rect7" />
|
||||||
|
<path
|
||||||
|
d="m 64,23 c 22.6437,0 41,18.3563 41,41 0,22.6437 -18.3563,41 -41,41 -8.6457,0 -16.6648,-2.678 -23.2773,-7.2471 l -9.8018,2.1914 c -1.7167,0.3837 -3.2488,-1.1485 -2.8652,-2.8652 l 2.1904,-9.8027 C 25.6776,80.6641 23,72.6452 23,64 23,41.3563 41.3563,23 64,23 Z"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path7" />
|
||||||
|
<path
|
||||||
|
d="m 76.2297,62.8668 c 0.4643,0.683 0.461,1.5812 -0.0083,2.2608 L 65.1666,81.1365 C 64.7932,81.6772 64.178,82 63.5209,82 H 58.737 c -1.5974,0 -2.5501,-1.7803 -1.6641,-3.1094 l 9.1875,-13.7812 c 0.4479,-0.6718 0.4479,-1.547 0,-2.2188 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 4.9687 c 0.6623,0 1.2817,0.3279 1.654,0.8757 z"
|
||||||
|
fill="url(#paint0_radial_4033_8)"
|
||||||
|
id="path8"
|
||||||
|
style="fill:url(#paint0_radial_4033_8)" />
|
||||||
|
<mask
|
||||||
|
id="mask0_4033_8"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="56"
|
||||||
|
y="46"
|
||||||
|
width="21"
|
||||||
|
height="36">
|
||||||
|
<path
|
||||||
|
d="m 76.4223,63.1501 c 0.3482,0.5123 0.3457,1.1859 -0.0063,1.6956 L 65.0175,81.3524 C 64.7375,81.7579 64.2761,82 63.7832,82 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.332 l 9.8906,-14.8359 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6641 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6568 z"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="path9" />
|
||||||
|
</mask>
|
||||||
|
<g
|
||||||
|
mask="url(#mask0_4033_8)"
|
||||||
|
id="g9">
|
||||||
|
<rect
|
||||||
|
x="52"
|
||||||
|
y="46"
|
||||||
|
width="17"
|
||||||
|
height="4"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="rect9" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs11">
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_4033_8"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(3.5,24.5214,-15.8099,2.26053,80,52.9904)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop11" />
|
||||||
|
</radialGradient>
|
||||||
|
<clipPath
|
||||||
|
id="clip0_4033_8">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect11"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/monochrome.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
178
assets/monochrome.svg
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
sodipodi:docname="monochrome.svg"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
inkscape:export-filename="foreground.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview11"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="1.240199"
|
||||||
|
inkscape:cx="156.02335"
|
||||||
|
inkscape:cy="321.3194"
|
||||||
|
inkscape:window-width="2544"
|
||||||
|
inkscape:window-height="1363"
|
||||||
|
inkscape:window-x="35"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg11" />
|
||||||
|
<path
|
||||||
|
d="m 256,92 c 90.5748,0 164,73.4252 164,164 0,90.5748 -73.4252,164 -164,164 -34.5828,0 -66.6592,-10.712 -93.1092,-28.9884 l -39.2072,8.7656 c -6.8668,1.5348 -12.9952,-4.594 -11.4608,-11.4608 l 8.7616,-39.2108 C 102.7104,322.6564 92,290.5808 92,256 92,165.4252 165.4252,92 256,92 Z"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path7"
|
||||||
|
style="stroke-width:4"
|
||||||
|
clip-path="url(#clipPath1)"
|
||||||
|
inkscape:path-effect="#path-effect1" />
|
||||||
|
<defs
|
||||||
|
id="defs11">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="powerclip"
|
||||||
|
message=""
|
||||||
|
id="path-effect1"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
inverse="true"
|
||||||
|
flatten="false"
|
||||||
|
hide_clip="false" />
|
||||||
|
<radialGradient
|
||||||
|
id="paint0_radial_4033_8"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(14,98.0856,-63.2396,9.04212,320,211.9616)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#72AAEE"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#3584E4"
|
||||||
|
id="stop11" />
|
||||||
|
</radialGradient>
|
||||||
|
<mask
|
||||||
|
id="mask0_4033_8"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="56"
|
||||||
|
y="46"
|
||||||
|
width="21"
|
||||||
|
height="36">
|
||||||
|
<path
|
||||||
|
d="m 76.4223,63.1501 c 0.3482,0.5123 0.3457,1.1859 -0.0063,1.6956 L 65.0175,81.3524 C 64.7375,81.7579 64.2761,82 63.7832,82 h -5.9804 c -1.1981,0 -1.9127,-1.3352 -1.2481,-2.332 l 9.8906,-14.8359 c 0.3359,-0.5039 0.3359,-1.1603 0,-1.6641 L 57.0729,49.1094 C 56.1869,47.7803 57.1396,46 58.737,46 h 5.2334 c 0.4967,0 0.9613,0.2459 1.2405,0.6568 z"
|
||||||
|
fill="#2779dd"
|
||||||
|
id="path9" />
|
||||||
|
</mask>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath11">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect12"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath12">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect13"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath13">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect14"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath14">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect15"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath15">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect16"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath16">
|
||||||
|
<rect
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect17"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
transform="rotate(-30)" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath1">
|
||||||
|
<path
|
||||||
|
d="m 304.9188,251.4672 c 1.8572,2.732 1.844,6.3248 -0.0332,9.0432 L 260.6664,324.546 C 259.1728,326.7088 256.712,328 254.0836,328 H 234.948 c -6.3896,0 -10.2004,-7.1212 -6.6564,-12.4376 l 36.75,-55.1248 c 1.7916,-2.6872 1.7916,-6.188 0,-8.8752 l -36.75,-55.1248 C 224.7476,191.1212 228.5584,184 234.948,184 h 19.8748 c 2.6492,0 5.1268,1.3116 6.616,3.5028 z"
|
||||||
|
fill="url(#paint0_radial_4033_8)"
|
||||||
|
id="path1"
|
||||||
|
style="display:none;fill:url(#radialGradient1);stroke-width:4" />
|
||||||
|
<path
|
||||||
|
id="lpe_path-effect1"
|
||||||
|
style="fill:url(#radialGradient1);stroke-width:4"
|
||||||
|
class="powerclip"
|
||||||
|
d="M 87,87 H 425 V 425 H 87 Z m 217.9188,164.4672 -43.48,-63.9644 C 259.9496,185.3116 257.472,184 254.8228,184 H 234.948 c -6.3896,0 -10.2004,7.1212 -6.6564,12.4376 l 36.75,55.1248 c 1.7916,2.6872 1.7916,6.188 0,8.8752 l -36.75,55.1248 C 224.7476,320.8788 228.5584,328 234.948,328 h 19.1356 c 2.6284,0 5.0892,-1.2912 6.5828,-3.454 l 44.2192,-64.0356 c 1.8772,-2.7184 1.8904,-6.3112 0.0332,-9.0432 z" />
|
||||||
|
</clipPath>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#paint0_radial_4033_8"
|
||||||
|
id="radialGradient1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(14,98.0856,-63.2396,9.04212,320,211.9616)"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1" />
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 616 KiB |
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 616 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -1,126 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="100mm"
|
|
||||||
height="100mm"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
|
||||||
sodipodi:docname="smallerForeground.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#505050"
|
|
||||||
bordercolor="#eeeeee"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:showpageshadow="0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#505050"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="1.0847363"
|
|
||||||
inkscape:cx="58.078632"
|
|
||||||
inkscape:cy="213.41592"
|
|
||||||
inkscape:window-width="2544"
|
|
||||||
inkscape:window-height="1363"
|
|
||||||
inkscape:window-x="35"
|
|
||||||
inkscape:window-y="32"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer1" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.41161;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
d="M 29.285638,62.248476 70.927843,38.119963"
|
|
||||||
id="path10" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.41161;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
d="M 29.060097,38.141229 70.741564,62.202356"
|
|
||||||
id="path9" /><path
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5.41161;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
d="M 50,74.064703 V 25.935297"
|
|
||||||
id="path8" /><circle
|
|
||||||
style="fill:none;stroke:#ffffff;stroke-width:4.81294;stroke-linecap:round;stroke-linejoin:round"
|
|
||||||
id="path1"
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="24.064703" /><circle
|
|
||||||
style="fill:#09bd05;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="path2"
|
|
||||||
cx="50"
|
|
||||||
cy="73.408524"
|
|
||||||
r="5.5813141" /><circle
|
|
||||||
style="fill:#fe1e24;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="circle2"
|
|
||||||
cx="50"
|
|
||||||
cy="26.59148"
|
|
||||||
r="5.5813141" /><circle
|
|
||||||
style="fill:#fe941d;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="circle3"
|
|
||||||
cx="-68.30127"
|
|
||||||
cy="41.709789"
|
|
||||||
r="5.5813141"
|
|
||||||
transform="rotate(-120)" /><circle
|
|
||||||
style="fill:#001996;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="circle4"
|
|
||||||
cx="-68.30127"
|
|
||||||
cy="-5.1072531"
|
|
||||||
r="5.5813141"
|
|
||||||
transform="rotate(-120)" /><circle
|
|
||||||
style="fill:#ffff04;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="circle5"
|
|
||||||
cx="-18.301271"
|
|
||||||
cy="91.709793"
|
|
||||||
r="5.5813141"
|
|
||||||
transform="rotate(-60)" /><circle
|
|
||||||
style="fill:#770287;fill-opacity:1;stroke:#ffffff;stroke-width:3.38226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="circle6"
|
|
||||||
cx="-18.301271"
|
|
||||||
cy="44.89275"
|
|
||||||
r="5.5813141"
|
|
||||||
transform="rotate(-60)" /><circle
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.56605;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
|
||||||
id="path7"
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
r="6.6237097" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
id="layer1-3"
|
|
||||||
transform="matrix(0.05578547,0,0,0.05578547,42.442929,42.442929)"
|
|
||||||
style="stroke:#ffffff"><text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
x="-305.64749"
|
|
||||||
y="194.14493"
|
|
||||||
id="text2819"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan2817"
|
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
|
||||||
x="-305.64749"
|
|
||||||
y="194.14493" /></text><circle
|
|
||||||
style="fill:#354b5f;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path342"
|
|
||||||
cx="135.46666"
|
|
||||||
cy="135.46666"
|
|
||||||
r="135.46666" /><text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:900;font-stretch:normal;font-size:96.3615px;line-height:0;font-family:'Noto Serif';-inkscape-font-specification:'Noto Serif, Heavy';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
x="-305.64749"
|
|
||||||
y="194.14493"
|
|
||||||
id="text2819-3"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan2817-5"
|
|
||||||
style="stroke:#ffffff;stroke-width:0"
|
|
||||||
x="-305.64749"
|
|
||||||
y="194.14493" /></text><g
|
|
||||||
aria-label="❯"
|
|
||||||
id="text2827-6"
|
|
||||||
style="font-size:132.452px;line-height:0;font-family:PowerlineSymbols;-inkscape-font-specification:'PowerlineSymbols, Normal';text-align:end;text-anchor:end;fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0"><path
|
|
||||||
d="M 95.096912,209.8167 143.88912,135.46666 95.096912,61.116629 h 32.818568 l 47.92093,74.350031 -47.92093,74.35004 z"
|
|
||||||
id="path2883-2"
|
|
||||||
style="fill:#4e94e4;fill-opacity:1;stroke:#ffffff;stroke-width:0" /></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 6 KiB |
24
flake.lock
generated
|
|
@ -5,11 +5,11 @@
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767609335,
|
"lastModified": 1778716662,
|
||||||
"narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=",
|
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "250481aafeb741edfe23d29195671c19b36b6dca",
|
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -42,11 +42,11 @@
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774604963,
|
"lastModified": 1774860670,
|
||||||
"narHash": "sha256-MtAW1FIdirSlUAAO7s1u9auv5y3I6t3uJ+GeEbqiqxI=",
|
"narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=",
|
||||||
"owner": "neobrain",
|
"owner": "neobrain",
|
||||||
"repo": "nix2flatpak",
|
"repo": "nix2flatpak",
|
||||||
"rev": "3e04657fbcb49956ac301410b071a7f0b2ad5988",
|
"rev": "61d68e21e3fbc2d57590051f48736bea271f4aba",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -73,11 +73,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"nixpkgs-lib": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765674936,
|
"lastModified": 1777168982,
|
||||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs.lib",
|
||||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -88,11 +88,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767640445,
|
"lastModified": 1778869304,
|
||||||
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
2
gomuks
|
|
@ -1 +1 @@
|
||||||
Subproject commit daa0ba028e7d89ba9fc7580fc8099348e6145cb3
|
Subproject commit da3b823e1435afd6f2a1ea6c354d9a35c489b624
|
||||||
|
|
@ -3,6 +3,7 @@ import "package:hooks/hooks.dart";
|
||||||
import "package:code_assets/code_assets.dart";
|
import "package:code_assets/code_assets.dart";
|
||||||
|
|
||||||
Future<void> main(List<String> args) => build(args, (input, output) async {
|
Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
|
if (!input.config.buildCodeAssets) return;
|
||||||
final codeConfig = input.config.code;
|
final codeConfig = input.config.code;
|
||||||
final targetOS = codeConfig.targetOS;
|
final targetOS = codeConfig.targetOS;
|
||||||
final targetArch = codeConfig.targetArchitecture;
|
final targetArch = codeConfig.targetArchitecture;
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 712 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.9 KiB |
|
|
@ -4,10 +4,10 @@ import "package:nexus/models/account_data.dart";
|
||||||
|
|
||||||
class AccountDataController extends Notifier<IMap<String, AccountData>> {
|
class AccountDataController extends Notifier<IMap<String, AccountData>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, AccountData> build() => const IMap.empty();
|
IMap<String, AccountData> build() => .new();
|
||||||
|
|
||||||
void update(IMap<String, AccountData> newData) =>
|
void update(IMap<String, AccountData> newData) =>
|
||||||
state = IMap({...state.unlock, ...newData.unlock});
|
state = .new({...state.unlock, ...newData.unlock});
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
NotifierProvider<AccountDataController, IMap<String, AccountData>>(
|
NotifierProvider<AccountDataController, IMap<String, AccountData>>(
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,30 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
|
||||||
import "package:nexus/controllers/user_controller.dart";
|
import "package:nexus/controllers/user_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/membership.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
|
|
||||||
class AuthorController extends AsyncNotifier<Membership> {
|
class AuthorController extends AsyncNotifier<MembershipContent> {
|
||||||
final Message message;
|
final Event event;
|
||||||
AuthorController(this.message);
|
AuthorController(this.event);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Membership> build() async {
|
Future<MembershipContent> build() async {
|
||||||
final member = await ref.watch(
|
final member = await ref.watch(
|
||||||
UserController.provider(message.authorId).future,
|
UserController.provider(
|
||||||
|
.new(roomId: event.roomId, userId: event.sender),
|
||||||
|
).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
final pmp = message.metadata?["pmp"] == null
|
return .new(
|
||||||
? null
|
status: member.status,
|
||||||
: Membership.fromContent(
|
avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
|
||||||
IMap(message.metadata?["pmp"]),
|
displayName: event.pmp?.displayName ?? member.displayName,
|
||||||
message.authorId,
|
|
||||||
ref.watch(
|
|
||||||
ClientStateController.provider.select(
|
|
||||||
(value) => value?.homeserverUrl,
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
return Membership(
|
|
||||||
status: member?.status ?? MembershipStatus.leave,
|
|
||||||
avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl,
|
|
||||||
displayName:
|
|
||||||
pmp?.displayName ?? member?.displayName ?? message.authorId.localpart,
|
|
||||||
userId: message.authorId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider.family<AuthorController, Membership, Message>(
|
AsyncNotifierProvider.family<AuthorController, MembershipContent, Event>(
|
||||||
AuthorController.new,
|
AuthorController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import "dart:developer";
|
|
||||||
import "dart:ffi";
|
import "dart:ffi";
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
import "dart:isolate";
|
import "dart:isolate";
|
||||||
import "package:collection/collection.dart";
|
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/foundation.dart";
|
||||||
import "package:nexus/controllers/account_data_controller.dart";
|
import "package:nexus/controllers/account_data_controller.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/controllers/init_complete_controller.dart";
|
import "package:nexus/controllers/init_complete_controller.dart";
|
||||||
import "package:nexus/controllers/new_events_controller.dart";
|
|
||||||
import "package:nexus/controllers/rooms_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/space_edges_controller.dart";
|
import "package:nexus/controllers/space_edges_controller.dart";
|
||||||
import "package:nexus/controllers/sync_status_controller.dart";
|
import "package:nexus/controllers/sync_status_controller.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
import "package:nexus/controllers/top_level_spaces_controller.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/main.dart";
|
||||||
import "package:nexus/models/client_state.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/paginate.dart";
|
import "package:nexus/models/paginate.dart";
|
||||||
import "package:nexus/models/requests/get_event_request.dart";
|
import "package:nexus/models/requests/get_event_request.dart";
|
||||||
|
|
@ -33,7 +30,6 @@ import "package:nexus/models/requests/send_message_request.dart";
|
||||||
import "package:nexus/models/requests/set_membership_request.dart";
|
import "package:nexus/models/requests/set_membership_request.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/sync_data.dart";
|
import "package:nexus/models/sync_data.dart";
|
||||||
import "package:nexus/models/sync_status.dart";
|
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package: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";
|
||||||
|
|
@ -68,26 +64,27 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
case "client_state":
|
case "client_state":
|
||||||
ref
|
ref
|
||||||
.watch(ClientStateController.provider.notifier)
|
.watch(ClientStateController.provider.notifier)
|
||||||
.set(ClientState.fromJson(decodedMuksEvent));
|
.set(.fromJson(decodedMuksEvent));
|
||||||
break;
|
break;
|
||||||
case "sync_status":
|
case "sync_status":
|
||||||
ref
|
ref
|
||||||
.watch(SyncStatusController.provider.notifier)
|
.watch(SyncStatusController.provider.notifier)
|
||||||
.set(SyncStatus.fromJson(decodedMuksEvent));
|
.set(.fromJson(decodedMuksEvent));
|
||||||
break;
|
break;
|
||||||
case "init_complete":
|
case "init_complete":
|
||||||
ref.watch(InitCompleteController.provider.notifier).complete();
|
ref.watch(InitCompleteController.provider.notifier).complete();
|
||||||
break;
|
break;
|
||||||
case "send_complete":
|
case "send_complete":
|
||||||
final event = Event.fromJson(decodedMuksEvent["event"]);
|
final event = Event.fromJson(decodedMuksEvent["event"]);
|
||||||
|
ref
|
||||||
|
.watch(RoomsController.provider.notifier)
|
||||||
|
.update(
|
||||||
|
.new({
|
||||||
|
event.roomId: .new(events: .new({event.rowId: event})),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
|
|
||||||
if (event.type == "m.room.message") {
|
|
||||||
ref
|
|
||||||
.watch(
|
|
||||||
NewEventsController.provider(event.roomId).notifier,
|
|
||||||
)
|
|
||||||
.add(IList([event]));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "sync_complete":
|
case "sync_complete":
|
||||||
final syncData = SyncData.fromJson(decodedMuksEvent);
|
final syncData = SyncData.fromJson(decodedMuksEvent);
|
||||||
|
|
@ -127,9 +124,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
debugPrint("Finished handling $muksEventType...");
|
debugPrint("Finished handling $muksEventType...");
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
debugger();
|
if (kDebugMode) {
|
||||||
showError(error, stackTrace);
|
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
||||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
rethrow;
|
||||||
|
} else {
|
||||||
|
showError(error, stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -206,9 +206,11 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
||||||
final response = await getState(request);
|
final response = await getState(request);
|
||||||
|
|
||||||
return (response ?? await getState(request.copyWith(refetch: true)) ?? [])
|
return .new(
|
||||||
.map((event) => Event.fromJson(event))
|
(response ?? await getState(request.copyWith(refetch: true)) ?? []).map(
|
||||||
.toIList();
|
(event) => .fromJson(event),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<IList<Event>?> getRelatedEvents(
|
Future<IList<Event>?> getRelatedEvents(
|
||||||
|
|
@ -216,24 +218,21 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
) async {
|
) async {
|
||||||
final response =
|
final response =
|
||||||
(await _sendCommand("get_related_events", request.toJson())) as List?;
|
(await _sendCommand("get_related_events", request.toJson())) as List?;
|
||||||
return response?.map((event) => Event.fromJson(event)).toIList();
|
return .new(response?.map((event) => .fromJson(event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Event?> getEvent(GetEventRequest request) async {
|
Future<Event?> getEvent(GetEventRequest request) async {
|
||||||
final event = request.room.events.firstWhereOrNull(
|
|
||||||
(event) => event.eventId == request.eventId,
|
|
||||||
);
|
|
||||||
if (event != null) return event;
|
|
||||||
|
|
||||||
final json = await _sendCommand("get_event", request.toJson());
|
final json = await _sendCommand("get_event", request.toJson());
|
||||||
return json == null ? null : Event.fromJson(json);
|
return json == null ? null : .fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Paginate> paginate(PaginateRequest request) async =>
|
Future<Paginate> paginate(PaginateRequest request) async =>
|
||||||
Paginate.fromJson(await _sendCommand("paginate", request.toJson()));
|
.fromJson(await _sendCommand("paginate", request.toJson()));
|
||||||
|
|
||||||
Future<Profile> getProfile(String userId) async =>
|
Future<Profile> getProfile(String userId) async {
|
||||||
Profile.fromJson(await _sendCommand("get_profile", {"user_id": userId}));
|
final json = await _sendCommand("get_profile", {"user_id": userId});
|
||||||
|
return .fromJsonWithCatch({...json, "id": userId});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> reportEvent(ReportRequest request) =>
|
Future<void> reportEvent(ReportRequest request) =>
|
||||||
_sendCommand("report_event", request.toJson());
|
_sendCommand("report_event", request.toJson());
|
||||||
|
|
@ -242,9 +241,8 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
_sendCommand("set_membership", request.toJson());
|
_sendCommand("set_membership", request.toJson());
|
||||||
|
|
||||||
Future<void> markRead(Room room) async {
|
Future<void> markRead(Room room) async {
|
||||||
final event = room.events.firstWhereOrNull(
|
final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
|
||||||
(event) => event.rowId == room.timeline.last.eventRowId,
|
final event = eventRowId == null ? null : room.events[eventRowId];
|
||||||
);
|
|
||||||
if (event == null || room.metadata == null) return;
|
if (event == null || room.metadata == null) return;
|
||||||
|
|
||||||
await _sendCommand("mark_read", {
|
await _sendCommand("mark_read", {
|
||||||
|
|
@ -263,12 +261,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> 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.host}",
|
||||||
});
|
});
|
||||||
return response["m.homeserver"]?["base_url"];
|
return Uri.parse(response["m.homeserver"]?["base_url"]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ class ClientStateController extends Notifier<ClientState?> {
|
||||||
@override
|
@override
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
void set(ClientState newState) {
|
void set(ClientState newState) => state = newState;
|
||||||
state = newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = NotifierProvider<ClientStateController, ClientState?>(
|
static final provider = NotifierProvider<ClientStateController, ClientState?>(
|
||||||
ClientStateController.new,
|
ClientStateController.new,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ import "package:cross_cache/cross_cache.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
class CrossCacheController extends Notifier<CrossCache> {
|
class CrossCacheController extends Notifier<CrossCache> {
|
||||||
static const String spaceKey = "space";
|
|
||||||
static const String roomKey = "room";
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CrossCache build() => CrossCache();
|
CrossCache build() => .new();
|
||||||
|
|
||||||
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
|
static final provider = NotifierProvider<CrossCacheController, CrossCache>(
|
||||||
CrossCacheController.new,
|
CrossCacheController.new,
|
||||||
|
|
|
||||||
84
lib/controllers/emoji_controller.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import "dart:convert";
|
||||||
|
import "package:emoji_text_field/models/emoji_category.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:http/http.dart";
|
||||||
|
import "package:nexus/models/emoji.dart";
|
||||||
|
|
||||||
|
typedef EmojiTuple = (IMap<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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_controller.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_event_request.dart";
|
import "package:nexus/models/requests/get_event_request.dart";
|
||||||
|
|
||||||
|
|
@ -9,8 +11,18 @@ class EventController extends AsyncNotifier<Event?> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Event?> build() async {
|
Future<Event?> build() async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final room = ref.watch(
|
||||||
return await client.getEvent(request).onError((_, _) => null);
|
RoomsController.provider.select((value) => value[request.roomId]),
|
||||||
|
);
|
||||||
|
final event = room?.events.values.firstWhereOrNull(
|
||||||
|
(event) => event.eventId == request.eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return event ??
|
||||||
|
await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getEvent(request)
|
||||||
|
.onError((_, _) => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ class KeyController extends Notifier<String?> {
|
||||||
String? build() =>
|
String? build() =>
|
||||||
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
|
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
|
||||||
|
|
||||||
Future<void> set(String? id) async {
|
Future<void> set(String? value) async {
|
||||||
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
|
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
|
||||||
state = id;
|
state = value;
|
||||||
|
|
||||||
if (id == null) {
|
if (value == null) {
|
||||||
prefs.remove(key);
|
prefs.remove(key);
|
||||||
} else {
|
} else {
|
||||||
prefs.setString(key, id);
|
prefs.setString(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
32
lib/controllers/members_by_status_controller.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/members_controller.dart";
|
||||||
|
import "package:nexus/models/configs/members_by_status_config.dart";
|
||||||
|
import "package:nexus/models/content/membership.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class MembersByStatusController extends AsyncNotifier<ISet<Event>> {
|
||||||
|
final MembersByStatusConfig config;
|
||||||
|
MembersByStatusController(this.config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ISet<Event>> build() => ref.watch(
|
||||||
|
MembersController.provider(config.roomId).selectAsync(
|
||||||
|
(members) => members
|
||||||
|
.where(
|
||||||
|
(membership) => switch (membership.content) {
|
||||||
|
MembershipContent(:final status) => config.status == status,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toISet(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<
|
||||||
|
MembersByStatusController,
|
||||||
|
ISet<Event>,
|
||||||
|
MembersByStatusConfig
|
||||||
|
>(MembersByStatusController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/members_controller.dart";
|
|
||||||
import "package:nexus/models/membership.dart";
|
|
||||||
import "package:nexus/models/membership_status.dart";
|
|
||||||
|
|
||||||
class MembersByTypeController extends AsyncNotifier<IList<Membership>> {
|
|
||||||
final MembershipStatus status;
|
|
||||||
MembersByTypeController(this.status);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<IList<Membership>> build() => ref.watch(
|
|
||||||
MembersController.provider.selectAsync(
|
|
||||||
(members) =>
|
|
||||||
members.where((membership) => membership.status == status).toIList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final provider =
|
|
||||||
AsyncNotifierProvider.family<
|
|
||||||
MembersByTypeController,
|
|
||||||
IList<Membership>,
|
|
||||||
MembershipStatus
|
|
||||||
>(MembersByTypeController.new);
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +1,46 @@
|
||||||
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/controllers/client_controller.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/selected_room_controller.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/membership.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_room_state_request.dart";
|
import "package:nexus/models/requests/get_room_state_request.dart";
|
||||||
|
|
||||||
class MembersController extends AsyncNotifier<IList<Membership>> {
|
class MembersController extends AsyncNotifier<ISet<Event>> {
|
||||||
|
final String roomId;
|
||||||
|
MembersController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<Membership>> build() async {
|
Future<ISet<Event>> build() async {
|
||||||
final data = ref.watch(
|
final room = ref.watch(
|
||||||
SelectedRoomController.provider.select(
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
(value) => value?.metadata == null
|
|
||||||
? null
|
|
||||||
: (value!.metadata!.id, value.metadata!.hasMemberList),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (data == null) return const IList.empty();
|
|
||||||
|
|
||||||
final state = await ref
|
if (room == null) return .new();
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.getRoomState(
|
|
||||||
GetRoomStateRequest(
|
|
||||||
roomId: data.$1,
|
|
||||||
fetchMembers: data.$2 == false,
|
|
||||||
includeMembers: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return state.nonNulls
|
if (!room.hasFetchedMembers) {
|
||||||
.where((state) => state.type == "m.room.member")
|
final fetchedState = await ref
|
||||||
.map(
|
.watch(ClientController.provider.notifier)
|
||||||
(membership) => Membership.fromContent(
|
.getRoomState(
|
||||||
membership.content,
|
GetRoomStateRequest(
|
||||||
membership.stateKey!,
|
roomId: roomId,
|
||||||
ref.watch(
|
fetchMembers: !(room.metadata?.hasMemberList ?? false),
|
||||||
ClientStateController.provider.select(
|
includeMembers: true,
|
||||||
(value) => value?.homeserverUrl,
|
),
|
||||||
),
|
);
|
||||||
) ??
|
|
||||||
"",
|
await ref
|
||||||
),
|
.read(RoomsController.provider.notifier)
|
||||||
)
|
.addState(roomId, fetchedState, isMembers: true);
|
||||||
.toIList();
|
}
|
||||||
|
|
||||||
|
return room.state[EventType.membership.type]?.values
|
||||||
|
.map((rowId) => room.events[rowId])
|
||||||
|
.nonNulls
|
||||||
|
.toISet() ??
|
||||||
|
.new();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider = AsyncNotifierProvider.autoDispose
|
||||||
AsyncNotifierProvider<MembersController, IList<Membership>>(
|
.family<MembersController, ISet<Event>, String>(MembersController.new);
|
||||||
MembersController.new,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
lib/controllers/members_grouped_controller.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/members_by_status_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/configs/members_by_status_config.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class MembersGroupedController
|
||||||
|
extends AsyncNotifier<IList<MapEntry<int?, ISet<Event>>>> {
|
||||||
|
final MembersByStatusConfig config;
|
||||||
|
MembersGroupedController(this.config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<IList<MapEntry<int?, ISet<Event>>>> build() async {
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((value) => value[config.roomId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
final createRowId = room?.state[EventType.create.type]?[""];
|
||||||
|
final createEvent = createRowId == null ? null : room?.events[createRowId];
|
||||||
|
final createEventContent = switch (createEvent?.content) {
|
||||||
|
CreateContent content => content,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
final creators = createEventContent?.additionalCreatorIds.add(
|
||||||
|
createEvent!.sender,
|
||||||
|
);
|
||||||
|
|
||||||
|
final powerLevelsRowId = room?.state[EventType.powerLevels.type]?[""];
|
||||||
|
final powerLevelsEvent = powerLevelsRowId == null
|
||||||
|
? null
|
||||||
|
: room?.events[powerLevelsRowId];
|
||||||
|
|
||||||
|
final content = switch (powerLevelsEvent?.content) {
|
||||||
|
PowerLevelsContent content => content,
|
||||||
|
_ => PowerLevelsContent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final members = await ref.watch(
|
||||||
|
MembersByStatusController.provider(config).future,
|
||||||
|
);
|
||||||
|
|
||||||
|
return members
|
||||||
|
.fold<IMap<int?, ISet<Event>>>(.new(), (result, event) {
|
||||||
|
final groupKey = creators?.contains(event.stateKey!) == true
|
||||||
|
? null
|
||||||
|
: content.users[event.stateKey!] ?? content.usersDefault;
|
||||||
|
|
||||||
|
return result.update(
|
||||||
|
groupKey,
|
||||||
|
(value) => value.add(event),
|
||||||
|
ifAbsent: () => .new({event}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toEntryIList(
|
||||||
|
compare: (a, b) =>
|
||||||
|
(b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<
|
||||||
|
MembersGroupedController,
|
||||||
|
IList<MapEntry<int?, ISet<Event>>>,
|
||||||
|
MembersByStatusConfig
|
||||||
|
>(MembersGroupedController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/client_controller.dart";
|
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
|
||||||
import "package:nexus/models/configs/message_config.dart";
|
|
||||||
import "package:nexus/models/requests/get_related_events_request.dart";
|
|
||||||
|
|
||||||
class MessageController extends AsyncNotifier<Message?> {
|
|
||||||
final MessageConfig config;
|
|
||||||
MessageController(this.config);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Message?> build() async {
|
|
||||||
try {
|
|
||||||
final isEdit = config.event.relationType == "m.replace";
|
|
||||||
if ((isEdit && !config.includeEdits) || config.room.metadata == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final event = config.event.lastEditRowId == null
|
|
||||||
? config.event
|
|
||||||
: config.room.events.firstWhereOrNull(
|
|
||||||
(e) => e.rowId == config.event.lastEditRowId,
|
|
||||||
) ??
|
|
||||||
config.event;
|
|
||||||
|
|
||||||
final decrypted = (event.decrypted ?? event.content);
|
|
||||||
final type = (config.event.decryptedType ?? config.event.type);
|
|
||||||
final content = decrypted["m.new_content"] == null
|
|
||||||
? decrypted
|
|
||||||
: IMap(decrypted["m.new_content"]);
|
|
||||||
|
|
||||||
final homeserver = ref
|
|
||||||
.read(ClientStateController.provider)
|
|
||||||
?.homeserverUrl;
|
|
||||||
final source = homeserver == null || content["url"] == null
|
|
||||||
? "null"
|
|
||||||
: Uri.parse(content["url"]).mxcToHttps(homeserver).toString();
|
|
||||||
|
|
||||||
final metadata = {
|
|
||||||
"body": config.event.redactedBy == null
|
|
||||||
? (content["body"] ?? "")
|
|
||||||
: "Deleted Message",
|
|
||||||
"flashing": false,
|
|
||||||
"timelineId": event.timelineRowId,
|
|
||||||
"big": event.localContent?.bigEmoji == true,
|
|
||||||
"eventType": type,
|
|
||||||
"pmp": content["com.beeper.per_message_profile"],
|
|
||||||
"error": event.sendError,
|
|
||||||
"format": content["format"] ?? content["format"],
|
|
||||||
"editSource": event.localContent?.editSource ?? content["body"],
|
|
||||||
"txnId": config.event.transactionId,
|
|
||||||
};
|
|
||||||
|
|
||||||
final editedAt = event.relationType == "m.replace"
|
|
||||||
? event.timestamp
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if ((event.redactedBy != null && !config.alwaysReturn) ||
|
|
||||||
(!config.includeEdits &&
|
|
||||||
(config.event.relationType == "m.replace"))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final replyId =
|
|
||||||
config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"];
|
|
||||||
|
|
||||||
final reactionEvents = config.event.reactions.isEmpty && !isEdit
|
|
||||||
? null
|
|
||||||
: await ref
|
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.getRelatedEvents(
|
|
||||||
GetRelatedEventsRequest(
|
|
||||||
roomId: config.room.metadata!.id,
|
|
||||||
eventId:
|
|
||||||
(isEdit ? config.event.relatesTo : null) ??
|
|
||||||
config.event.eventId,
|
|
||||||
relationType: "m.annotation",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final reactions = reactionEvents
|
|
||||||
?.where((event) => event.redactedBy == null)
|
|
||||||
.fold<IMap<String, IList<String>>>(IMap(), (acc, event) {
|
|
||||||
final key = event.content["m.relates_to"]?["key"];
|
|
||||||
if (key == null) return acc;
|
|
||||||
|
|
||||||
return acc.update(
|
|
||||||
key,
|
|
||||||
(list) => list.add(event.authorId),
|
|
||||||
ifAbsent: () => IList([event.authorId]),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((key, value) => MapEntry(key, value.unlock))
|
|
||||||
.unlock;
|
|
||||||
|
|
||||||
final asText =
|
|
||||||
Message.text(
|
|
||||||
metadata: metadata,
|
|
||||||
id: config.event.eventId,
|
|
||||||
reactions: reactions,
|
|
||||||
authorId: event.authorId,
|
|
||||||
text: content["formatted_body"] ?? content["body"] ?? "",
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
editedAt: editedAt,
|
|
||||||
)
|
|
||||||
as TextMessage;
|
|
||||||
|
|
||||||
Message toSystemMessage(String content) => Message.system(
|
|
||||||
metadata: {...metadata, "body": content},
|
|
||||||
id: config.event.eventId,
|
|
||||||
reactions: reactions,
|
|
||||||
authorId: event.authorId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
text: content,
|
|
||||||
);
|
|
||||||
|
|
||||||
return switch (type) {
|
|
||||||
"m.room.encrypted" => asText.copyWith(
|
|
||||||
text: "Unable to decrypt message.",
|
|
||||||
metadata: {...metadata, "body": "Unable to decrypt message."},
|
|
||||||
),
|
|
||||||
// "org.matrix.msc3381.poll.start" => Message.custom(
|
|
||||||
// metadata: {
|
|
||||||
// ...metadata,
|
|
||||||
// "poll": event.parsedPollEventContent.pollStartContent,
|
|
||||||
// "responses": event.getPollResponses(timeline),
|
|
||||||
// },
|
|
||||||
// id: eventId,
|
|
||||||
// deliveredAt: originServerTs,
|
|
||||||
// authorId: senderId,
|
|
||||||
// ),
|
|
||||||
("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
|
|
||||||
null || "m.image" => Message.image(
|
|
||||||
id: config.event.eventId,
|
|
||||||
authorId: event.authorId,
|
|
||||||
reactions: reactions,
|
|
||||||
source: source,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
metadata: metadata,
|
|
||||||
text: asText.text,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"],
|
|
||||||
),
|
|
||||||
"m.audio" || "m.file" => Message.file(
|
|
||||||
name: content["filename"].toString(),
|
|
||||||
size: content["info"]["size"],
|
|
||||||
metadata: metadata,
|
|
||||||
id: config.event.eventId,
|
|
||||||
reactions: reactions,
|
|
||||||
authorId: event.authorId,
|
|
||||||
source: source,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
),
|
|
||||||
_ => asText,
|
|
||||||
},
|
|
||||||
"m.room.member" =>
|
|
||||||
content["membership"] == event.unsigned["prev_content"]?["membership"]
|
|
||||||
? null
|
|
||||||
: toSystemMessage(
|
|
||||||
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
|
|
||||||
"invite" => "was invited to",
|
|
||||||
"join" => "joined",
|
|
||||||
"leave" => event.authorId == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"),
|
|
||||||
"ban" => "was banned from",
|
|
||||||
"knock" => "asked to join",
|
|
||||||
_ => "did something relating to",
|
|
||||||
}} the room. ${content["reason"] ?? ""}",
|
|
||||||
),
|
|
||||||
|
|
||||||
"m.room.server_acl" => toSystemMessage(
|
|
||||||
"${event.authorId} updated the server ban list.",
|
|
||||||
),
|
|
||||||
|
|
||||||
"m.room.redaction" =>
|
|
||||||
config.alwaysReturn
|
|
||||||
? asText.copyWith(
|
|
||||||
metadata: {
|
|
||||||
...(asText.metadata ?? {}),
|
|
||||||
"body": "Deleted Message",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
_ =>
|
|
||||||
config.alwaysReturn
|
|
||||||
? asText
|
|
||||||
: (
|
|
||||||
// Turn this on for debugging purposes
|
|
||||||
false
|
|
||||||
// ignore: dead_code
|
|
||||||
? Message.unsupported(
|
|
||||||
metadata: metadata,
|
|
||||||
reactions: reactions,
|
|
||||||
id: config.event.eventId,
|
|
||||||
authorId: event.authorId,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
)
|
|
||||||
: null),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
|
||||||
.autoDispose<MessageController, Message?, MessageConfig>(
|
|
||||||
MessageController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/message_controller.dart";
|
|
||||||
import "package:nexus/models/configs/message_config.dart";
|
|
||||||
import "package:nexus/models/configs/messages_config.dart";
|
|
||||||
|
|
||||||
class MessagesController extends AsyncNotifier<IList<Message>> {
|
|
||||||
final MessagesConfig config;
|
|
||||||
MessagesController(this.config);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<IList<Message>> build() async => (await Future.wait(
|
|
||||||
config.events.map(
|
|
||||||
(event) => ref.watch(
|
|
||||||
MessageController.provider(
|
|
||||||
MessageConfig(event: event, room: config.room),
|
|
||||||
).future,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)).nonNulls.toIList();
|
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
|
||||||
.autoDispose<MessagesController, IList<Message>, MessagesConfig>(
|
|
||||||
MessagesController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -7,9 +7,8 @@ class MultiProviderController extends AsyncNotifier<void> {
|
||||||
final IList<AsyncNotifierProvider> providers;
|
final IList<AsyncNotifierProvider> providers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> build() async => await Future.wait(
|
Future<void> build() =>
|
||||||
providers.map((provider) => ref.watch(provider.future)),
|
.wait(providers.map((provider) => ref.watch(provider.future)));
|
||||||
);
|
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider.family<
|
AsyncNotifierProvider.family<
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/models/event.dart";
|
|
||||||
|
|
||||||
class NewEventsController extends Notifier<IList<Event>> {
|
|
||||||
final String roomId;
|
|
||||||
NewEventsController(this.roomId);
|
|
||||||
|
|
||||||
@override
|
|
||||||
IList<Event> build() => const IList.empty();
|
|
||||||
|
|
||||||
void add(IList<Event> newEvents) => state = newEvents;
|
|
||||||
|
|
||||||
static final provider = NotifierProvider.autoDispose
|
|
||||||
.family<NewEventsController, IList<Event>, String>(
|
|
||||||
NewEventsController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/controllers/selected_room_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/models/configs/power_level_config.dart";
|
import "package:nexus/models/configs/power_level_config.dart";
|
||||||
import "package:nexus/models/requests/membership_action.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
|
||||||
class PowerLevelController extends Notifier<bool> {
|
class PowerLevelController extends Notifier<bool> {
|
||||||
final PowerLevelConfig config;
|
final PowerLevelConfig config;
|
||||||
|
|
@ -11,56 +11,61 @@ class PowerLevelController extends Notifier<bool> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool build() {
|
bool build() {
|
||||||
final room = ref.watch(SelectedRoomController.provider);
|
if (config case EventPowerLevelConfig(:final eventType)) {
|
||||||
final event = room?.events.firstWhereOrNull(
|
assert(
|
||||||
(event) => event.rowId == room.state["m.room.power_levels"]?[""],
|
eventType != .redaction,
|
||||||
|
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((value) => value[config.roomId]),
|
||||||
);
|
);
|
||||||
final user = ref.watch(ClientStateController.provider)?.userId;
|
|
||||||
if (event == null || user == null) return false;
|
|
||||||
|
|
||||||
final users = (event.content["users"] as Map<String, dynamic>? ?? {});
|
final eventRowId = room?.state[EventType.powerLevels.type]?[""];
|
||||||
final events = (event.content["events"] as Map<String, dynamic>? ?? {});
|
|
||||||
|
|
||||||
int powerLevelOf(String userId) => users.containsKey(userId)
|
final event = eventRowId == null ? null : room?.events[eventRowId];
|
||||||
? (users[userId] as int)
|
final content = event?.content is PowerLevelsContent
|
||||||
: (event.content["users_default"] as int? ?? 0);
|
? event!.content
|
||||||
|
: PowerLevelsContent();
|
||||||
|
|
||||||
|
final user = ref.watch(
|
||||||
|
ClientStateController.provider.select((value) => value?.userId),
|
||||||
|
);
|
||||||
|
if (user == null || content is! PowerLevelsContent) return false;
|
||||||
|
|
||||||
|
int powerLevelOf(String userId) =>
|
||||||
|
content.users[userId] ?? content.usersDefault;
|
||||||
|
|
||||||
final userLevel = powerLevelOf(user);
|
final userLevel = powerLevelOf(user);
|
||||||
final targetLevel = config.targetUser != null
|
|
||||||
? powerLevelOf(config.targetUser!)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (config.action != null) {
|
return switch (config) {
|
||||||
return switch (config.action!) {
|
EventPowerLevelConfig(:final eventType) =>
|
||||||
MembershipAction.invite =>
|
userLevel >= (content.events[eventType.type] ?? content.eventsDefault),
|
||||||
userLevel >= (event.content["invite"] as int? ?? 0),
|
|
||||||
|
|
||||||
MembershipAction.kick =>
|
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
|
||||||
targetLevel != null &&
|
switch (action) {
|
||||||
userLevel >= (event.content["kick"] as int? ?? 50) &&
|
.invite => userLevel >= content.invite,
|
||||||
userLevel > targetLevel,
|
|
||||||
|
|
||||||
MembershipAction.ban =>
|
.kick =>
|
||||||
targetLevel != null &&
|
userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
|
||||||
userLevel >= (event.content["ban"] as int? ?? 50) &&
|
|
||||||
userLevel > targetLevel,
|
|
||||||
|
|
||||||
MembershipAction.unban =>
|
.ban =>
|
||||||
userLevel >= (event.content["ban"] as int? ?? 50),
|
userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.eventType == "m.room.redaction") {
|
.unban => userLevel >= content.ban,
|
||||||
return userLevel >= (event.content["redact"] as int? ?? 50);
|
},
|
||||||
}
|
|
||||||
|
|
||||||
final requiredLevel = events.containsKey(config.eventType)
|
StatePowerLevelConfig(:final eventType) =>
|
||||||
? (events[config.eventType] as int)
|
userLevel >= (content.events[eventType.type] ?? content.stateDefault),
|
||||||
: (config.isStateEvent
|
|
||||||
? (event.content["state_default"] as int? ?? 50)
|
|
||||||
: (event.content["events_default"] as int? ?? 0));
|
|
||||||
|
|
||||||
return userLevel >= requiredLevel;
|
RedactionPowerLevelConfig(:final targetUser) =>
|
||||||
|
userLevel >=
|
||||||
|
(targetUser == user
|
||||||
|
? (content.events[EventType.redaction.type] ??
|
||||||
|
content.eventsDefault)
|
||||||
|
: content.redact),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = NotifierProvider.autoDispose
|
static final provider = NotifierProvider.autoDispose
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@ class ProfileController extends AsyncNotifier<Profile> {
|
||||||
return client.getProfile(userId);
|
return client.getProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.autoDispose
|
static final provider = AsyncNotifierProvider.family
|
||||||
.family<ProfileController, Profile, String>(ProfileController.new);
|
.autoDispose<ProfileController, Profile, String>(ProfileController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
lib/controllers/reactions_controller.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/configs/reactions_config.dart";
|
||||||
|
import "package:nexus/models/content/reaction.dart";
|
||||||
|
|
||||||
|
class ReactionsController extends AsyncNotifier<IMap<String, IList<String>>> {
|
||||||
|
final ReactionsConfig config;
|
||||||
|
ReactionsController(this.config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<IMap<String, IList<String>>> build() async {
|
||||||
|
final eventInfo = ref.watch(
|
||||||
|
RoomsController.provider.select((value) {
|
||||||
|
final event = value[config.roomId]?.events[config.eventRowId];
|
||||||
|
return event == null ? null : (event.eventId, event.reactions);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final reactionEvents = eventInfo?.$2.isNotEmpty == true
|
||||||
|
? await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getRelatedEvents(
|
||||||
|
.new(
|
||||||
|
roomId: config.roomId,
|
||||||
|
eventId: eventInfo!.$1,
|
||||||
|
relationType: "m.annotation",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return reactionEvents
|
||||||
|
?.where((event) => event.redactedBy == null)
|
||||||
|
.fold<IMap<String, IList<String>>>(.new(), (acc, event) {
|
||||||
|
if (event.content case ReactionContent(:final key?)) {
|
||||||
|
return acc.update(
|
||||||
|
key,
|
||||||
|
(list) => list.add(event.sender),
|
||||||
|
ifAbsent: () => .new([event.sender]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}) ??
|
||||||
|
.new();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<
|
||||||
|
ReactionsController,
|
||||||
|
IMap<String, IList<String>>,
|
||||||
|
ReactionsConfig
|
||||||
|
>(ReactionsController.new);
|
||||||
|
}
|
||||||
|
|
@ -1,223 +1,88 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
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_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:fluttertagger/fluttertagger.dart";
|
import "package:fluttertagger/fluttertagger.dart";
|
||||||
import "package:nexus/controllers/client_controller.dart";
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/controllers/message_controller.dart";
|
|
||||||
import "package:nexus/controllers/messages_controller.dart";
|
|
||||||
import "package:nexus/controllers/new_events_controller.dart";
|
|
||||||
import "package:nexus/controllers/rooms_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/selected_room_controller.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/configs/messages_config.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
import "package:nexus/models/configs/message_config.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/requests/get_related_events_request.dart";
|
|
||||||
import "package:nexus/models/requests/get_room_state_request.dart";
|
|
||||||
import "package:nexus/models/requests/paginate_request.dart";
|
|
||||||
import "package:nexus/models/requests/redact_event_request.dart";
|
import "package:nexus/models/requests/redact_event_request.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/models/requests/send_event_request.dart";
|
|
||||||
import "package:nexus/models/requests/send_message_request.dart";
|
import "package:nexus/models/requests/send_message_request.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
RoomChatController(this.roomId);
|
RoomChatController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<InMemoryChatController> build() async {
|
Future<IList<Event>?> build() async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
var room = ref.read(RoomsController.provider)[roomId];
|
final room = ref.watch(
|
||||||
if (room == null) return InMemoryChatController();
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
final state = await client.getRoomState(
|
|
||||||
GetRoomStateRequest(roomId: roomId),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
if (room == null) return null;
|
||||||
.read(RoomsController.provider.notifier)
|
|
||||||
.update(
|
|
||||||
{
|
|
||||||
roomId: Room(
|
|
||||||
events: state,
|
|
||||||
state: state.fold(
|
|
||||||
const IMap.empty(),
|
|
||||||
(previousValue, stateEvent) => previousValue.add(
|
|
||||||
stateEvent.type,
|
|
||||||
(previousValue[stateEvent.type] ?? const IMap.empty()).addAll(
|
|
||||||
IMap({
|
|
||||||
if (stateEvent.stateKey != null)
|
|
||||||
stateEvent.stateKey!: stateEvent.rowId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}.toIMap(),
|
|
||||||
const ISet.empty(),
|
|
||||||
);
|
|
||||||
|
|
||||||
room = ref.read(RoomsController.provider)[roomId];
|
if (!room.hasFetchedState) {
|
||||||
if (room == null) return InMemoryChatController();
|
final state = await client.getRoomState(.new(roomId: roomId));
|
||||||
|
|
||||||
final messages = await ref.watch(
|
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
||||||
MessagesController.provider(
|
|
||||||
MessagesConfig(
|
|
||||||
room: room,
|
|
||||||
events: room.timeline
|
|
||||||
.map(
|
|
||||||
(timelineRowTuple) => room!.events.firstWhereOrNull(
|
|
||||||
(event) => event.rowId == timelineRowTuple.eventRowId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.nonNulls
|
|
||||||
.toIList(),
|
|
||||||
),
|
|
||||||
).future,
|
|
||||||
);
|
|
||||||
final controller = InMemoryChatController(messages: messages.toList());
|
|
||||||
|
|
||||||
ref.onDispose(
|
|
||||||
ref.listen(NewEventsController.provider(roomId), (_, next) async {
|
|
||||||
for (final event in next) {
|
|
||||||
if (event.type == "m.reaction") {
|
|
||||||
final message = controller.messages.firstWhereOrNull(
|
|
||||||
(message) =>
|
|
||||||
message.id == event.content["m.relates_to"]?["event_id"],
|
|
||||||
);
|
|
||||||
final key = event.content["m.relates_to"]?["key"];
|
|
||||||
if (message == null || key == null || !ref.mounted) return;
|
|
||||||
|
|
||||||
return await controller.updateMessage(
|
|
||||||
message,
|
|
||||||
message.copyWith(
|
|
||||||
reactions: IMap(message.reactions)
|
|
||||||
.update(
|
|
||||||
key,
|
|
||||||
(reactors) => [...reactors, event.authorId],
|
|
||||||
ifAbsent: () => [event.authorId],
|
|
||||||
)
|
|
||||||
.unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == "m.room.redaction") {
|
|
||||||
final controller = await future;
|
|
||||||
final redactsId = event.content["redacts"];
|
|
||||||
final originalMessage = controller.messages.firstWhereOrNull(
|
|
||||||
(message) => message.id == redactsId,
|
|
||||||
);
|
|
||||||
if (!ref.mounted) return;
|
|
||||||
|
|
||||||
if (originalMessage != null) {
|
|
||||||
return await controller.removeMessage(originalMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
final redacts = ref
|
|
||||||
.read(SelectedRoomController.provider)
|
|
||||||
?.events
|
|
||||||
.firstWhere((event) => event.eventId == redactsId);
|
|
||||||
|
|
||||||
if (redacts?.type == "m.reaction") {
|
|
||||||
final message = controller.messages.firstWhereOrNull(
|
|
||||||
(message) =>
|
|
||||||
message.id == redacts!.content["m.relates_to"]?["event_id"],
|
|
||||||
);
|
|
||||||
final key = redacts!.content["m.relates_to"]?["key"];
|
|
||||||
if (message == null || key == null || !ref.mounted) return;
|
|
||||||
|
|
||||||
return await controller.updateMessage(
|
|
||||||
message,
|
|
||||||
message.copyWith(
|
|
||||||
reactions: IMap(message.reactions)
|
|
||||||
.update(
|
|
||||||
key,
|
|
||||||
(reactors) =>
|
|
||||||
IList(reactors).remove(redacts.authorId).unlock,
|
|
||||||
)
|
|
||||||
.where((_, value) => value.isNotEmpty)
|
|
||||||
.unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final message = await ref.watch(
|
|
||||||
MessageController.provider(
|
|
||||||
MessageConfig(event: event, room: room!, includeEdits: true),
|
|
||||||
).future,
|
|
||||||
);
|
|
||||||
if (event.relationType == "m.replace") {
|
|
||||||
final controller = await future;
|
|
||||||
final oldMessage = controller.messages.firstWhereOrNull(
|
|
||||||
(element) => element.id == event.relatesTo,
|
|
||||||
);
|
|
||||||
if (oldMessage == null || message == null || !ref.mounted) return;
|
|
||||||
|
|
||||||
return await controller.updateMessage(
|
|
||||||
oldMessage,
|
|
||||||
message.copyWith(
|
|
||||||
id: oldMessage.id,
|
|
||||||
replyToMessageId: oldMessage.replyToMessageId,
|
|
||||||
metadata: {
|
|
||||||
...(oldMessage.metadata ?? {}),
|
|
||||||
...(message.metadata ?? {})
|
|
||||||
.toIMap()
|
|
||||||
.where((key, value) => value != null)
|
|
||||||
.unlock,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message != null && ref.mounted) {
|
|
||||||
await insertMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, weak: true).close,
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.onDispose(controller.dispose);
|
|
||||||
|
|
||||||
// While there are under 20 messages, try up to load more messages until theres no more or we have 20 messages.
|
|
||||||
for (var more = true; more == true && controller.messages.length < 20;) {
|
|
||||||
more = await loadOlder(controller);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller;
|
// While there are under 20 events, try to load more
|
||||||
|
// until there's no more or the conditions are met.
|
||||||
|
if (room.hasMore && room.timeline.length < 20) {
|
||||||
|
loadOlder();
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.timeline
|
||||||
|
.toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
|
||||||
|
.map((element) => element.value)
|
||||||
|
.toIList()
|
||||||
|
.addAll(room.sticky)
|
||||||
|
.map((entry) {
|
||||||
|
final foundEvent = entry == null ? null : room.events[entry];
|
||||||
|
|
||||||
|
final editedEvent =
|
||||||
|
foundEvent == null || foundEvent.lastEditRowId == 0
|
||||||
|
? null
|
||||||
|
: room.events[foundEvent.lastEditRowId];
|
||||||
|
|
||||||
|
return editedEvent == null
|
||||||
|
? foundEvent
|
||||||
|
: foundEvent?.copyWith(content: editedEvent.content);
|
||||||
|
})
|
||||||
|
.nonNulls
|
||||||
|
.toIList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertMessage(Message message) async {
|
Future<void> deleteMessage(Event event, {String? reason}) => ref
|
||||||
final controller = await future;
|
|
||||||
final oldMessage = message.metadata?["txnId"] == null
|
|
||||||
? null
|
|
||||||
: controller.messages.firstWhereOrNull(
|
|
||||||
(element) =>
|
|
||||||
element.metadata?["txnId"] == message.metadata?["txnId"],
|
|
||||||
);
|
|
||||||
|
|
||||||
return oldMessage == null
|
|
||||||
? controller.insertMessage(message)
|
|
||||||
: controller.updateMessage(oldMessage, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteMessage(Message message, {String? reason}) => ref
|
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.redactEvent(
|
.redactEvent(
|
||||||
RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason),
|
RedactEventRequest(
|
||||||
|
eventId: event.eventId,
|
||||||
|
roomId: roomId,
|
||||||
|
reason: reason,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> loadOlder([InMemoryChatController? chatController]) async {
|
Future<bool> loadOlder() async {
|
||||||
|
final timelineKeys = ref
|
||||||
|
.read(RoomsController.provider.select((value) => value[roomId]))
|
||||||
|
?.timeline
|
||||||
|
.keys;
|
||||||
final response = await ref
|
final response = await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.paginate(
|
.paginate(
|
||||||
PaginateRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
maxTimelineId: ref
|
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||||
.read(RoomsController.provider)[roomId]
|
? timelineKeys?.reduce(min)
|
||||||
?.timeline
|
: null,
|
||||||
.firstOrNull
|
|
||||||
?.timelineRowId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -226,42 +91,22 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
.update(
|
.update(
|
||||||
IMap({
|
IMap({
|
||||||
roomId: Room(
|
roomId: Room(
|
||||||
events: response.events.addAll(response.relatedEvents),
|
events: IMap.fromIterable(
|
||||||
|
response.events.addAll(response.relatedEvents),
|
||||||
|
keyMapper: (event) => event.rowId,
|
||||||
|
valueMapper: (event) => event,
|
||||||
|
),
|
||||||
hasMore: response.hasMore,
|
hasMore: response.hasMore,
|
||||||
timeline: response.events
|
timeline: IMap.fromIterable(
|
||||||
.map(
|
response.events,
|
||||||
(event) => TimelineRowTuple(
|
keyMapper: (event) => event.timelineRowId,
|
||||||
timelineRowId: event.timelineRowId,
|
valueMapper: (event) => event.rowId,
|
||||||
eventRowId: event.rowId,
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toIList(),
|
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
const ISet.empty(),
|
.new(),
|
||||||
addToNewEvents: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
|
||||||
if (room != null) {
|
|
||||||
final messages = await ref.watch(
|
|
||||||
MessagesController.provider(
|
|
||||||
MessagesConfig(room: room, events: response.events.reversed),
|
|
||||||
).future,
|
|
||||||
);
|
|
||||||
|
|
||||||
final controller = chatController ?? await future;
|
|
||||||
await controller.insertAllMessages(
|
|
||||||
messages
|
|
||||||
.where(
|
|
||||||
(newMessage) => !controller.messages.any(
|
|
||||||
(message) => message.id == newMessage.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
index: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return response.hasMore;
|
return response.hasMore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +115,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
bool shouldMention = true,
|
bool shouldMention = true,
|
||||||
required IList<Tag> tags,
|
required IList<Tag> tags,
|
||||||
required RelationType relationType,
|
required RelationType relationType,
|
||||||
Message? relation,
|
Event? relation,
|
||||||
}) async {
|
}) async {
|
||||||
var taggedMessage = text;
|
var taggedMessage = text;
|
||||||
|
|
||||||
|
|
@ -285,7 +130,6 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
|
||||||
final event = await client.sendMessage(
|
final event = await client.sendMessage(
|
||||||
SendMessageRequest(
|
SendMessageRequest(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
|
|
@ -294,52 +138,40 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
if (shouldMention == true &&
|
if (shouldMention == true &&
|
||||||
relation != null &&
|
relation != null &&
|
||||||
relationType == RelationType.reply)
|
relationType == RelationType.reply)
|
||||||
relation.authorId,
|
relation.sender,
|
||||||
].toIList(),
|
].toIList(),
|
||||||
room: taggedMessage.contains("@room"),
|
room: taggedMessage.contains("@room"),
|
||||||
),
|
),
|
||||||
text: taggedMessage,
|
text: taggedMessage,
|
||||||
relation: relation == null
|
relation: relation == null
|
||||||
? null
|
? null
|
||||||
: Relation(eventId: relation.id, relationType: relationType),
|
: .new(eventId: relation.eventId, relationType: relationType),
|
||||||
),
|
|
||||||
);
|
|
||||||
final message = room == null
|
|
||||||
? null
|
|
||||||
: await ref.watch(
|
|
||||||
MessageController.provider(
|
|
||||||
MessageConfig(room: room, event: event),
|
|
||||||
).future,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (message != null) insertMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> scrollToMessage(Message message) async {
|
|
||||||
final controller = await future;
|
|
||||||
Future<void> setFlashing(bool flashing) => controller.updateMessage(
|
|
||||||
message,
|
|
||||||
message.copyWith(
|
|
||||||
metadata: {...(message.metadata ?? {}), "flashing": flashing},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await setFlashing(true);
|
ref
|
||||||
Timer(Duration(seconds: 1), () => setFlashing(false));
|
.watch(RoomsController.provider.notifier)
|
||||||
|
.update(
|
||||||
return await controller.scrollToMessage(message.id);
|
.new({
|
||||||
|
roomId: .new(
|
||||||
|
events: .new({event.rowId: event}),
|
||||||
|
sticky: .new({event.rowId}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeReaction(
|
Future<void> removeReaction(
|
||||||
String reaction,
|
String reaction,
|
||||||
Message message,
|
Event event,
|
||||||
String userId,
|
String userId,
|
||||||
) async {
|
) async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final allReactionEvents = await client.getRelatedEvents(
|
final allReactionEvents = await client.getRelatedEvents(
|
||||||
GetRelatedEventsRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
eventId: message.id,
|
eventId: event.eventId,
|
||||||
relationType: "m.annotation",
|
relationType: "m.annotation",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -349,30 +181,30 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
||||||
final reactionEvent = reactionEvents?.firstWhereOrNull(
|
final reactionEvent = reactionEvents?.firstWhereOrNull(
|
||||||
(event) =>
|
(event) => switch (event.content) {
|
||||||
event.authorId == userId &&
|
ReactionContent(:final key) =>
|
||||||
event.content["m.relates_to"]?["key"] == reaction,
|
key == reaction && event.sender == userId,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reactionEvent != null) {
|
if (reactionEvent != null) {
|
||||||
await ref
|
await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.redactEvent(
|
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId));
|
||||||
RedactEventRequest(eventId: reactionEvent.eventId, roomId: roomId),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendReaction(String reaction, Message message) async {
|
Future<void> sendReaction(String reaction, Event event) async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
await client.sendEvent(
|
await client.sendEvent(
|
||||||
SendEventRequest(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
type: "m.reaction",
|
type: EventType.reaction.type,
|
||||||
content: {
|
content: {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": message.id,
|
"event_id": event.eventId,
|
||||||
"rel_type": "m.annotation",
|
"rel_type": "m.annotation",
|
||||||
"key": reaction,
|
"key": reaction,
|
||||||
},
|
},
|
||||||
|
|
@ -384,7 +216,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider = AsyncNotifierProvider.family
|
||||||
.autoDispose<RoomChatController, InMemoryChatController, String>(
|
.autoDispose<RoomChatController, IList<Event>?, String>(
|
||||||
RoomChatController.new,
|
RoomChatController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,86 @@
|
||||||
import "package:collection/collection.dart";
|
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/controllers/client_state_controller.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/controllers/new_events_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
|
||||||
import "package:nexus/models/read_receipt.dart";
|
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomsController extends Notifier<IMap<String, Room>> {
|
class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, Room> build() => const IMap.empty();
|
IMap<String, Room> build() => .new();
|
||||||
|
|
||||||
void update(
|
Future<void> addState(
|
||||||
IMap<String, Room> rooms,
|
String roomId,
|
||||||
ISet<String> leftRooms, {
|
IList<Event> state, {
|
||||||
bool addToNewEvents = true,
|
bool isMembers = false,
|
||||||
}) {
|
}) async => update(
|
||||||
final homeserver =
|
.new({
|
||||||
ref.watch(
|
roomId: Room(
|
||||||
ClientStateController.provider.select(
|
events: .fromEntries(state.map((event) => .new(event.rowId, event))),
|
||||||
(value) => value?.homeserverUrl,
|
hasFetchedState: true,
|
||||||
),
|
hasFetchedMembers: isMembers,
|
||||||
) ??
|
state: await Isolate.run(() {
|
||||||
"";
|
final newState = state.fold<IMap<String, IMap<String, int>>>(
|
||||||
|
.new(),
|
||||||
|
(previousValue, stateEvent) => previousValue.add(
|
||||||
|
stateEvent.type,
|
||||||
|
(previousValue[stateEvent.type] ?? .new()).add(
|
||||||
|
stateEvent.stateKey!,
|
||||||
|
stateEvent.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return newState;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
|
||||||
final merged = rooms.entries.fold(state, (acc, entry) {
|
final merged = rooms.entries.fold(state, (acc, entry) {
|
||||||
final roomId = entry.key;
|
final roomId = entry.key;
|
||||||
final incoming = entry.value;
|
final incoming = entry.value;
|
||||||
final existing = acc[roomId];
|
final existing = acc[roomId];
|
||||||
|
|
||||||
final events = existing?.events.updateById(
|
|
||||||
incoming.events,
|
|
||||||
(item) => item.eventId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addToNewEvents) {
|
|
||||||
ref
|
|
||||||
.watch(NewEventsController.provider(roomId).notifier)
|
|
||||||
.add(
|
|
||||||
incoming.timeline
|
|
||||||
.map(
|
|
||||||
(timelineTuple) => events?.firstWhereOrNull(
|
|
||||||
(event) => timelineTuple.eventRowId == event.rowId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.nonNulls
|
|
||||||
.toIList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc.add(
|
return acc.add(
|
||||||
roomId,
|
roomId,
|
||||||
existing?.copyWith(
|
existing?.copyWith(
|
||||||
hasMore: incoming.hasMore,
|
hasMore: incoming.hasMore,
|
||||||
metadata:
|
sticky:
|
||||||
incoming.metadata?.copyWith(
|
(incoming.sticky.isEmpty == true
|
||||||
avatar:
|
? existing.sticky
|
||||||
incoming.metadata?.avatar?.mxcToHttps(homeserver) ??
|
: existing.sticky.addAll(incoming.sticky))
|
||||||
existing.metadata?.avatar,
|
.removeWhere(
|
||||||
) ??
|
(rowId) => incoming.timeline.values.contains(rowId),
|
||||||
existing.metadata,
|
),
|
||||||
events: events!,
|
metadata: incoming.metadata ?? existing.metadata,
|
||||||
|
events: incoming.events.isEmpty
|
||||||
|
? existing.events
|
||||||
|
: existing.events.addAll(incoming.events),
|
||||||
state: incoming.state.entries.fold(
|
state: incoming.state.entries.fold(
|
||||||
existing.state,
|
existing.state,
|
||||||
(previousValue, event) => previousValue.add(
|
(previousValue, event) => previousValue.add(
|
||||||
event.key,
|
event.key,
|
||||||
(previousValue[event.key] ?? const IMap.empty()).addAll(
|
(previousValue[event.key] ?? .new()).addAll(event.value),
|
||||||
event.value,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timeline:
|
reset: false,
|
||||||
(incoming.reset
|
hasFetchedMembers:
|
||||||
? incoming.timeline
|
incoming.hasFetchedMembers || existing.hasFetchedMembers,
|
||||||
: existing.timeline.updateById(
|
hasFetchedState:
|
||||||
incoming.timeline,
|
incoming.hasFetchedState || existing.hasFetchedState,
|
||||||
(item) => item.timelineRowId,
|
timeline: (incoming.reset
|
||||||
))
|
? incoming.timeline
|
||||||
.sortedBy((element) => element.timelineRowId)
|
: existing.timeline.addAll(incoming.timeline)),
|
||||||
.toIList(),
|
|
||||||
receipts: incoming.receipts.entries.fold(
|
receipts: incoming.receipts.entries.fold(
|
||||||
existing.receipts,
|
existing.receipts,
|
||||||
(receiptAcc, event) => receiptAcc.add(
|
(receiptAcc, event) => receiptAcc.add(
|
||||||
event.key,
|
event.key,
|
||||||
(receiptAcc[event.key] ?? IList<ReadReceipt>()).addAll(
|
(receiptAcc[event.key] ?? .new()).addAll(event.value),
|
||||||
event.value,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
incoming.copyWith(
|
incoming,
|
||||||
metadata: incoming.metadata?.copyWith(
|
|
||||||
avatar: incoming.metadata?.avatar?.mxcToHttps(homeserver),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -100,6 +88,7 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
merged,
|
merged,
|
||||||
(acc, roomId) => acc.remove(roomId),
|
(acc, roomId) => acc.remove(roomId),
|
||||||
);
|
);
|
||||||
|
|
||||||
state = prunedList;
|
state = prunedList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/key_controller.dart";
|
|
||||||
import "package:nexus/controllers/selected_space_controller.dart";
|
|
||||||
import "package:nexus/models/room.dart";
|
|
||||||
|
|
||||||
class SelectedRoomController extends Notifier<Room?> {
|
|
||||||
@override
|
|
||||||
Room? build() {
|
|
||||||
final space = ref.watch(SelectedSpaceController.provider);
|
|
||||||
final selectedRoomId = ref.watch(
|
|
||||||
KeyController.provider(KeyController.roomKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
return space.children.firstWhereOrNull(
|
|
||||||
(room) => room.metadata?.id == selectedRoomId,
|
|
||||||
) ??
|
|
||||||
space.children.firstOrNull;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = NotifierProvider<SelectedRoomController, Room?>(
|
|
||||||
SelectedRoomController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/key_controller.dart";
|
|
||||||
import "package:nexus/controllers/spaces_controller.dart";
|
|
||||||
import "package:nexus/models/space.dart";
|
|
||||||
|
|
||||||
class SelectedSpaceController extends Notifier<Space> {
|
|
||||||
@override
|
|
||||||
Space build() {
|
|
||||||
final spaces = ref.watch(SpacesController.provider);
|
|
||||||
final selectedSpaceId = ref.watch(
|
|
||||||
KeyController.provider(KeyController.spaceKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
return spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ??
|
|
||||||
spaces.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final provider = NotifierProvider<SelectedSpaceController, Space>(
|
|
||||||
SelectedSpaceController.new,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ import "package:shared_preferences/shared_preferences.dart";
|
||||||
|
|
||||||
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
|
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
|
||||||
@override
|
@override
|
||||||
Future<SharedPreferences> build() => SharedPreferences.getInstance();
|
Future<SharedPreferences> build() async => .getInstance();
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
|
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "package:nexus/models/space_edge.dart";
|
||||||
|
|
||||||
class SpaceEdgesController extends Notifier<IMap<String, IList<SpaceEdge>>> {
|
class SpaceEdgesController extends Notifier<IMap<String, IList<SpaceEdge>>> {
|
||||||
@override
|
@override
|
||||||
IMap<String, IList<SpaceEdge>> build() => const IMap.empty();
|
IMap<String, IList<SpaceEdge>> build() => .new();
|
||||||
|
|
||||||
void set(IMap<String, IList<SpaceEdge>> newEdges) =>
|
void set(IMap<String, IList<SpaceEdge>> newEdges) =>
|
||||||
state = state.addAll(newEdges);
|
state = state.addAll(newEdges);
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import "package:nexus/controllers/account_data_controller.dart";
|
||||||
import "package:nexus/controllers/rooms_controller.dart";
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
import "package:nexus/controllers/top_level_spaces_controller.dart";
|
||||||
import "package:nexus/controllers/space_edges_controller.dart";
|
import "package:nexus/controllers/space_edges_controller.dart";
|
||||||
import "package:nexus/models/space.dart";
|
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
import "package:nexus/models/space_edge.dart";
|
import "package:nexus/models/space.dart";
|
||||||
|
import "package:nexus/models/subspace.dart";
|
||||||
|
|
||||||
class SpacesController extends Notifier<IList<Space>> {
|
class SpacesController extends Notifier<IList<Space>> {
|
||||||
@override
|
@override
|
||||||
|
|
@ -16,118 +16,130 @@ class SpacesController extends Notifier<IList<Space>> {
|
||||||
final rooms = ref.watch(RoomsController.provider);
|
final rooms = ref.watch(RoomsController.provider);
|
||||||
final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
|
final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
|
||||||
final spaceEdges = ref.watch(SpaceEdgesController.provider);
|
final spaceEdges = ref.watch(SpaceEdgesController.provider);
|
||||||
|
final accountData = ref.watch(AccountDataController.provider);
|
||||||
|
|
||||||
final childRoomsBySpaceId = IMap.fromEntries(
|
final childrenById = {
|
||||||
topLevelSpaceIds.map((spaceId) {
|
for (final entry in spaceEdges.entries)
|
||||||
ISet<String> walk(String currentId) {
|
entry.key: entry.value.map((e) => e.childId).toList(),
|
||||||
final children = spaceEdges[currentId] ?? IList<SpaceEdge>();
|
};
|
||||||
|
|
||||||
return children.fold<ISet<String>>(const ISet.empty(), (acc, edge) {
|
Set<String> collectDescendants(String startId) {
|
||||||
final childId = edge.childId;
|
final visited = <String>{};
|
||||||
final isSpace = spaceEdges.containsKey(childId);
|
final stack = [startId];
|
||||||
|
|
||||||
return acc
|
while (stack.isNotEmpty) {
|
||||||
.addAll(!isSpace ? ISet([childId]) : const ISet.empty())
|
final current = stack.removeLast();
|
||||||
.addAll(isSpace ? walk(childId) : const ISet.empty());
|
final children = childrenById[current] ?? const [];
|
||||||
});
|
|
||||||
|
for (final child in children) {
|
||||||
|
if (visited.add(child)) {
|
||||||
|
stack.add(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return MapEntry(
|
return visited;
|
||||||
spaceId,
|
}
|
||||||
walk(spaceId).map((id) => rooms[id]).nonNulls.toIList(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
final allNestedRoomIds = childRoomsBySpaceId.values
|
Space buildSpace(String spaceId) {
|
||||||
.expand((l) => l)
|
final space = rooms[spaceId];
|
||||||
.map(
|
final directChildrenIds = childrenById[spaceId] ?? const [];
|
||||||
(room) => rooms.entries
|
|
||||||
.firstWhere(
|
final directRooms = <Room>[];
|
||||||
(entry) => entry.value.metadata?.id == room.metadata?.id,
|
final subSpaces = <Subspace>[];
|
||||||
)
|
|
||||||
.key,
|
for (final childId in directChildrenIds) {
|
||||||
)
|
final room = rooms[childId];
|
||||||
.toISet();
|
if (room == null) continue;
|
||||||
|
|
||||||
|
if (childrenById.containsKey(childId)) {
|
||||||
|
final descendants = collectDescendants(childId);
|
||||||
|
|
||||||
|
subSpaces.add(
|
||||||
|
.new(
|
||||||
|
room: room,
|
||||||
|
children: .new(descendants.map((id) => rooms[id]).nonNulls),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
directRooms.add(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .new(
|
||||||
|
id: spaceId,
|
||||||
|
room: space,
|
||||||
|
title: space?.metadata?.name ?? "Unnamed Space",
|
||||||
|
children: .new(directRooms),
|
||||||
|
subSpaces: .new(subSpaces),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final spaces = topLevelSpaceIds.map(buildSpace).toIList();
|
||||||
|
|
||||||
|
final usedRoomIds = {
|
||||||
|
for (final space in spaces) ...[
|
||||||
|
...space.children.map((r) => r.metadata?.id),
|
||||||
|
...space.subSpaces.expand((s) => s.children.map((r) => r.metadata?.id)),
|
||||||
|
],
|
||||||
|
}.nonNulls.toISet();
|
||||||
|
|
||||||
|
final directMessages = IMap(
|
||||||
|
accountData["m.direct"]?.content ?? {},
|
||||||
|
).values.expand((e) => e).toISet();
|
||||||
|
|
||||||
final otherRooms = rooms.entries
|
final otherRooms = rooms.entries
|
||||||
.where(
|
.where(
|
||||||
(e) =>
|
(e) =>
|
||||||
!allNestedRoomIds.contains(e.key) &&
|
!usedRoomIds.contains(e.key) &&
|
||||||
!topLevelSpaceIds.contains(e.key) &&
|
!topLevelSpaceIds.contains(e.key) &&
|
||||||
!spaceEdges.containsKey(e.key),
|
!childrenById.containsKey(e.key),
|
||||||
)
|
)
|
||||||
.map((e) => e.value);
|
.map((e) => e.value)
|
||||||
|
.toIList();
|
||||||
final accountData = ref.watch(AccountDataController.provider);
|
|
||||||
|
|
||||||
final directMessages = IMap(
|
|
||||||
accountData["m.direct"]?.content ?? {},
|
|
||||||
).values.expand((element) => element);
|
|
||||||
|
|
||||||
final homeRooms = otherRooms
|
final homeRooms = otherRooms
|
||||||
.where(
|
.where((r) => !directMessages.contains(r.metadata?.id))
|
||||||
(room) =>
|
|
||||||
directMessages.any(
|
|
||||||
(directMessage) => directMessage == room.metadata?.id,
|
|
||||||
) ==
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
||||||
final dmRooms = otherRooms
|
final dmRooms = otherRooms
|
||||||
.where(
|
.where((r) => directMessages.contains(r.metadata?.id))
|
||||||
(room) => directMessages.any(
|
|
||||||
(directMessage) => directMessage == room.metadata?.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
||||||
final topLevelSpacesList = topLevelSpaceIds
|
final allSpaces = <Space>[
|
||||||
.map((id) {
|
.new(
|
||||||
final room = rooms[id];
|
id: "home",
|
||||||
if (room == null) return null;
|
title: "Home",
|
||||||
|
icon: Icons.home,
|
||||||
|
children: homeRooms,
|
||||||
|
subSpaces: .new(),
|
||||||
|
),
|
||||||
|
.new(
|
||||||
|
id: "dms",
|
||||||
|
title: "Direct Messages",
|
||||||
|
icon: Icons.people,
|
||||||
|
children: dmRooms,
|
||||||
|
subSpaces: .new(),
|
||||||
|
),
|
||||||
|
...spaces,
|
||||||
|
];
|
||||||
|
|
||||||
final children = childRoomsBySpaceId[id] ?? IList<Room>();
|
return allSpaces
|
||||||
return Space(
|
|
||||||
id: id,
|
|
||||||
title: room.metadata?.name ?? "Unnamed Room",
|
|
||||||
room: room,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.nonNulls
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
return <Space>[
|
|
||||||
Space(
|
|
||||||
id: "home",
|
|
||||||
title: "Home",
|
|
||||||
icon: Icons.home,
|
|
||||||
children: homeRooms,
|
|
||||||
),
|
|
||||||
Space(
|
|
||||||
id: "dms",
|
|
||||||
title: "Direct Messages",
|
|
||||||
icon: Icons.people,
|
|
||||||
children: dmRooms,
|
|
||||||
),
|
|
||||||
...topLevelSpacesList,
|
|
||||||
]
|
|
||||||
.map(
|
.map(
|
||||||
(space) => space.copyWith(
|
(space) => space.copyWith(
|
||||||
children: space.children
|
children: .new(
|
||||||
.sortedBy(
|
space.children
|
||||||
(element) =>
|
.sortedBy(
|
||||||
element
|
(element) =>
|
||||||
.metadata
|
element
|
||||||
?.sortingTimestamp
|
.metadata
|
||||||
.millisecondsSinceEpoch ??
|
?.sortingTimestamp
|
||||||
0,
|
.millisecondsSinceEpoch ??
|
||||||
)
|
0,
|
||||||
.sortedBy((room) => room.metadata?.unreadMessages ?? 0)
|
)
|
||||||
.reversed
|
.sortedBy((room) => room.metadata?.unreadMessages ?? 0)
|
||||||
.toIList(),
|
.reversed,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class SyncStatusController extends Notifier<SyncStatus?> {
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
void set(SyncStatus newStatus) {
|
void set(SyncStatus newStatus) {
|
||||||
if (newStatus.type == SyncStatusType.permanentlyFailed) {
|
if (newStatus.type == .permanentlyFailed) {
|
||||||
showError(newStatus.error ?? "Syncing failed");
|
showError(newStatus.error ?? "Syncing failed");
|
||||||
}
|
}
|
||||||
state = newStatus;
|
state = newStatus;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
||||||
class TopLevelSpacesController extends Notifier<IList<String>> {
|
class TopLevelSpacesController extends Notifier<IList<String>> {
|
||||||
@override
|
@override
|
||||||
IList<String> build() => const IList.empty();
|
IList<String> build() => .new();
|
||||||
|
|
||||||
void set(IList<String> newSpaces) => state = newSpaces;
|
void set(IList<String> newSpaces) => state = newSpaces;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:http/http.dart";
|
import "package:http/http.dart";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
import "package:nexus/controllers/header_controller.dart";
|
import "package:nexus/controllers/header_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
|
|
||||||
class UrlPreviewController extends AsyncNotifier<LinkPreviewData?> {
|
class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
final String link;
|
final String link;
|
||||||
UrlPreviewController(this.link);
|
UrlPreviewController(this.link);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LinkPreviewData?> build() async {
|
Future<OpenGraphData?> build() async {
|
||||||
final homeserver = ref.watch(ClientStateController.provider)?.homeserverUrl;
|
final homeserver = ref.watch(
|
||||||
|
ClientStateController.provider.select((value) => value?.homeserverUrl),
|
||||||
|
);
|
||||||
|
|
||||||
if (homeserver != null && !link.contains("matrix.to")) {
|
if (homeserver != null && !link.contains("matrix.to")) {
|
||||||
{
|
{
|
||||||
final response = await get(
|
final response = await get(
|
||||||
Uri.parse(homeserver)
|
.parse(homeserver)
|
||||||
.resolve("/_matrix/client/v1/media/preview_url")
|
.resolve("/_matrix/client/v1/media/preview_url")
|
||||||
.replace(queryParameters: {"url": link}),
|
.replace(queryParameters: {"url": link}),
|
||||||
headers: await ref.watch(HeaderController.provider.future),
|
headers: await ref.watch(HeaderController.provider.future),
|
||||||
|
|
@ -25,27 +27,14 @@ class UrlPreviewController extends AsyncNotifier<LinkPreviewData?> {
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decodedValue = json.decode(response.body);
|
final decodedValue = json.decode(response.body);
|
||||||
|
if (decodedValue is! Map<String, dynamic>) return null;
|
||||||
|
|
||||||
final mxc = decodedValue["og:image"];
|
final mxc = decodedValue["og:image"];
|
||||||
final image = mxc == null
|
final image = mxc == null
|
||||||
? null
|
? null
|
||||||
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
||||||
|
|
||||||
return LinkPreviewData(
|
return .fromJson(decodedValue).copyWith(imageUrl: image);
|
||||||
link: link,
|
|
||||||
title: decodedValue["og:title"],
|
|
||||||
description: decodedValue["og:description"],
|
|
||||||
image: image == null
|
|
||||||
? null
|
|
||||||
: ImagePreviewData(
|
|
||||||
url: image.toString(),
|
|
||||||
width:
|
|
||||||
(decodedValue["og:image:width"] as int?)?.toDouble() ??
|
|
||||||
0,
|
|
||||||
height:
|
|
||||||
(decodedValue["og:image:height"] as int?)?.toDouble() ??
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,8 +42,10 @@ class UrlPreviewController extends AsyncNotifier<LinkPreviewData?> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.autoDispose
|
static final provider =
|
||||||
.family<UrlPreviewController, LinkPreviewData?, String>(
|
AsyncNotifierProvider.family<
|
||||||
UrlPreviewController.new,
|
UrlPreviewController,
|
||||||
);
|
OpenGraphData?,
|
||||||
|
String
|
||||||
|
>(UrlPreviewController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,44 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/members_controller.dart";
|
import "package:nexus/controllers/members_controller.dart";
|
||||||
import "package:nexus/controllers/profile_controller.dart";
|
import "package:nexus/controllers/profile_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/models/membership.dart";
|
import "package:nexus/models/configs/user_config.dart";
|
||||||
import "package:nexus/models/membership_status.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
|
|
||||||
class UserController extends AsyncNotifier<Membership?> {
|
class UserController extends AsyncNotifier<MembershipContent> {
|
||||||
final String userId;
|
final UserConfig config;
|
||||||
UserController(this.userId);
|
UserController(this.config);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Membership?> build() async {
|
Future<MembershipContent> build() async {
|
||||||
final member = await ref.watch(
|
final member = config.roomId == null
|
||||||
MembersController.provider.selectAsync(
|
? null
|
||||||
(value) =>
|
: await ref.watch(
|
||||||
value.firstWhereOrNull((membership) => membership.userId == userId),
|
MembersController.provider(config.roomId!).selectAsync(
|
||||||
),
|
(value) => value.firstWhereOrNull(
|
||||||
|
(membership) => membership.stateKey == config.userId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (member?.content case final MembershipContent content) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
final profile = await ref.watch(
|
||||||
|
ProfileController.provider(config.userId).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (member != null) return member;
|
return .new(
|
||||||
|
status: .leave,
|
||||||
final profile = await ref.watch(ProfileController.provider(userId).future);
|
avatarUrl: profile.avatarUrl,
|
||||||
return Membership(
|
displayName: profile.displayName ?? config.userId.localpart,
|
||||||
status: MembershipStatus.leave,
|
|
||||||
avatarUrl: profile.avatarUrl == null
|
|
||||||
? null
|
|
||||||
: Uri.tryParse(profile.avatarUrl!),
|
|
||||||
displayName: profile.displayName ?? userId.localpart,
|
|
||||||
userId: userId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider =
|
static final provider =
|
||||||
AsyncNotifierProvider.family<UserController, Membership?, String>(
|
AsyncNotifierProvider.family<
|
||||||
UserController.new,
|
UserController,
|
||||||
);
|
MembershipContent,
|
||||||
|
UserConfig
|
||||||
|
>(UserController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ 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";
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/membership.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 extends Notifier<String> {
|
||||||
|
|
@ -21,23 +24,29 @@ class ViaController extends Notifier<String> {
|
||||||
|
|
||||||
addUserId(ref.watch(ClientStateController.provider)?.userId);
|
addUserId(ref.watch(ClientStateController.provider)?.userId);
|
||||||
|
|
||||||
final powerLevels = room.events.firstWhereOrNull(
|
final powerLevelsEventId = room.state[EventType.powerLevels.type]?[""];
|
||||||
(event) => event.rowId == room.state["m.room.power_levels"]?[""],
|
final powerLevels = powerLevelsEventId == null
|
||||||
);
|
? null
|
||||||
|
: room.events[powerLevelsEventId];
|
||||||
|
|
||||||
for (final userId in IMap(powerLevels?.content["users"]).keys) {
|
if (powerLevels?.content case PowerLevelsContent(:final users)) {
|
||||||
addUserId(userId);
|
for (final userId in users.keys) {
|
||||||
if (servers.length >= 5) break;
|
addUserId(userId);
|
||||||
|
if (servers.length >= 5) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final members = room.state["m.room.member"]?.values.toIList();
|
final members = room.state[EventType.membership.type]?.values.toIList();
|
||||||
for (var i = 0; servers.length < 5; i++) {
|
for (var i = 0; servers.length < 5; i++) {
|
||||||
final member = room.events.firstWhereOrNull(
|
final membershipEventId = members?.getOrNull(i);
|
||||||
(event) => event.rowId == members?.getOrNull(i),
|
final member = membershipEventId == null
|
||||||
);
|
? null
|
||||||
|
: room.events[membershipEventId];
|
||||||
|
|
||||||
if (member?.content["membership"] == "join") {
|
if (member?.content case MembershipContent(:final status)) {
|
||||||
addUserId(member?.stateKey);
|
if (status == .join) {
|
||||||
|
addUserId(member?.stateKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members?.getOrNull(i) == null) break;
|
if (members?.getOrNull(i) == null) break;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
extension GetLocalpart on String {
|
extension GetLocalpart on String {
|
||||||
String get localpart => substring(1).split(":").first;
|
String get localpart => length > 1 ? substring(1).split(":").first : "?";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
extension GomuksOwnedBufferToX on GomuksOwnedBuffer {
|
extension GomuksOwnedBufferToX on GomuksOwnedBuffer {
|
||||||
Uint8List toBytes() {
|
Uint8List toBytes() {
|
||||||
try {
|
try {
|
||||||
if (base == nullptr || length <= 0) return Uint8List(0);
|
if (base == nullptr || length <= 0) return .new(0);
|
||||||
return Uint8List.fromList(base.asTypedList(length));
|
return .fromList(base.asTypedList(length));
|
||||||
} finally {
|
} finally {
|
||||||
calloc.free(base);
|
calloc.free(base);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extension MxcToHttps on Uri {
|
extension MxcToHttps on Uri {
|
||||||
Uri mxcToHttps(String homeserver) => Uri.parse(
|
Uri mxcToHttps(String homeserver) =>
|
||||||
homeserver,
|
.parse(homeserver).resolve("_matrix/client/v1/media/download/$host$path");
|
||||||
).resolve("_matrix/client/v1/media/download/$host$path");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
extension SchemeToTheme on ColorScheme {
|
extension SchemeToTheme on ColorScheme {
|
||||||
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
ThemeData get theme => .from(colorScheme: this).copyWith(
|
||||||
cardTheme: CardThemeData(color: primaryContainer),
|
cardTheme: .new(color: primaryContainer),
|
||||||
|
popupMenuTheme: .new(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: .circular(16)),
|
||||||
|
color: surfaceContainerHigh,
|
||||||
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
backgroundColor: surfaceContainerLow,
|
backgroundColor: surfaceContainerLow,
|
||||||
),
|
),
|
||||||
menuTheme: MenuThemeData(
|
|
||||||
style: MenuStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(primaryContainer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
textTheme: ThemeData(
|
textTheme: ThemeData(
|
||||||
fontFamilyFallback: ["sans", "emoji"],
|
fontFamilyFallback: ["sans", "emoji"],
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ extension ShowContextMenu on BuildContext {
|
||||||
|
|
||||||
showMenu(
|
showMenu(
|
||||||
context: this,
|
context: this,
|
||||||
constraints: BoxConstraints.loose(Size.infinite),
|
constraints: .loose(Size.infinite),
|
||||||
position: RelativeRect.fromLTRB(
|
position: .fromLTRB(
|
||||||
globalPosition.dx,
|
globalPosition.dx,
|
||||||
globalPosition.dy,
|
globalPosition.dy,
|
||||||
overlay.size.width - globalPosition.dx,
|
overlay.size.width - globalPosition.dx,
|
||||||
overlay.size.height - globalPosition.dy,
|
overlay.size.height - globalPosition.dy,
|
||||||
),
|
),
|
||||||
color: Theme.of(this).colorScheme.surfaceContainerHighest,
|
|
||||||
items: children,
|
items: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||