Compare commits
320 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b727d0342d |
|||
|
db5735e9e2 |
|||
|
7c01611859 |
|||
|
632c02a517 |
|||
|
355c564123 |
|||
|
1af957d4a1 |
|||
|
1986e2a627 |
|||
| ab40600746 | |||
|
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 |
|||
|
b93f4c979c |
|||
|
b701da19dc |
|||
|
327c4066f3 |
|||
|
1282a8b897 |
|||
|
6ca974e6fc |
|||
|
e16a780fa3 |
|||
|
dc1eb52fe0 |
|||
|
3e8eba0872 |
|||
|
4954fb8c09 |
|||
|
4ff507e93f |
|||
|
1dcf3018a2 |
|||
|
6b8eef3f17 |
|||
|
f997e257a2 |
|||
|
07decc10e2 |
|||
|
e9b78a14d5 |
|||
|
7b2a6b84ad |
|||
|
5154e0fc6b |
|||
|
3cfbe7c078 |
|||
|
133e613214 |
|||
|
5e07cec14d |
|||
|
116649e8d7 |
|||
|
5f5ad911c2 |
|||
|
624127f3a8 |
|||
|
f860d9651f |
|||
|
2850b015a1 |
|||
|
3a7e708e39 |
|||
|
798eb3c3fd |
|||
|
ee648ab105 |
|||
|
2c23951ea8 |
|||
|
f4b2669f3d |
|||
|
6fe5677a13 |
|||
|
729f71e529 |
|||
|
b80bd557dd |
|||
|
15d02458ab |
|||
|
8dff27c56f |
|||
|
c857b89899 |
|||
|
06d6bf0cbc |
|||
|
9fdf08a5d8 |
|||
|
7fc314036e |
|||
|
aac843d793 |
|||
|
639d27a5fc |
|||
|
92f6b2fbba |
|||
|
4aa962193d |
|||
|
24f5f7d0b6 |
|||
|
9464b2bf78 |
|||
|
fd4b16c700 |
|||
|
a8383951ba |
|||
|
185ee37f04 |
|||
|
f38715c8ef |
|||
|
63535fb462 |
|||
|
7c1918857a |
|||
|
20f0ce9fa5 |
|||
|
fa8b8ddd14 |
|||
|
c3ca1e3491 |
|||
|
8154d41dc5 |
|||
|
d70c439278 |
|||
|
5796d250c7 |
|||
|
a9c4acaa74 |
|||
|
73ac0018ca |
|||
|
a64cfd35be |
|||
|
f460a3bacc |
|||
|
51dd8c5668 |
|||
|
35bf379f03 |
|||
|
f4624c2866 |
|||
|
cadd5c1255 |
|||
|
3a1bcb5b8f |
|||
|
c130d28b93 |
|||
|
e669ede6fe |
|||
|
5a0a5cb138 |
|||
|
e30355a6f1 |
|||
|
4e4e387aa2 |
|||
|
2ead857805 |
|||
|
a562d043a8 |
|||
|
bb842abfb1 |
|||
|
0b9ddbfbc8 |
|||
|
7ee165b300 |
|||
|
60b7f22566 |
|||
|
e42aaeb30a |
|||
|
6d903a8882 |
|||
|
388e09abb7 |
|||
|
4dc692634e |
|||
|
0a6c097c50 |
|||
|
08cca4d3d3 |
|||
|
cdba3c480e |
|||
|
55ecbc3590 |
|||
|
8c7adbc9d3 |
|||
|
60be7aaf72 |
|||
|
e0ba99d9b9 |
|||
|
e2d29439d5 |
|||
|
92e5206326 |
|||
| ecc40bfe49 | |||
| eaf1f3a178 | |||
|
18ee13901c |
|||
|
c784094a4c |
|||
|
690d2549bc |
|||
|
ab61338382 |
|||
|
48adf82b7e |
|||
|
1f274307ad |
|||
|
c609de8279 |
|||
|
1d33eed829 |
|||
|
e6c47c036d |
|||
|
c6767863eb |
|||
|
f5c277e8b0 |
|||
|
84bed89632 |
|||
|
6c064cfcef |
|||
|
2c4d5670e5 |
|||
|
a51c869d7e |
|||
|
a1401f20ea |
|||
|
f8d6dcead5 |
|||
|
02c79a6d7c |
|||
|
11aef9fc5a |
|||
|
2f39949a2e |
|||
|
166295fdb5 |
|||
|
607cd54e02 |
|||
|
67b96ae731 |
|||
|
ed81b4afa1 |
|||
|
345fa3b5ff |
|||
|
f50fb6ab09 |
|||
|
5601fb27c0 |
|||
|
0d1f7c1819 |
|||
|
4bbf694479 |
|||
|
dd9b9fdc62 |
|||
|
8b7f88cc0b |
|||
|
70793a2f77 |
|||
|
b2c763deef |
|||
|
5c66d35017 |
|||
|
a25f9d2e73 |
|||
|
4a3b7e9a14 |
|||
|
6974e5cc06 |
|||
|
42c32b1b1c |
|||
|
87466f9d05 |
|||
|
ea72654887 |
|||
|
f1af130a63 |
|||
|
e7b772ef66 |
|||
|
28dfe9e981 |
|||
|
d4c98a0cfb |
|||
|
01772b567a |
|||
|
f9927d1eb3 |
|||
|
0d44d10e05 |
|||
|
11ecec5ab3 |
|||
|
b407bbfdee |
|||
|
04b7ab8e2e |
|||
|
0cae2692bc |
|||
|
b387f0755a |
|||
|
840f2fe464 |
|||
|
e5062683e8 |
|||
|
ffe879680d |
|||
|
32dfba178a |
|||
|
fe845e6cd6 |
|||
|
d3e6340b28 |
|||
|
b6e7bb82da |
|||
|
eb503ba647 |
|||
|
cda971a335 |
|||
| e4f666b824 | |||
| cf2150466e | |||
| 23bbbb533e | |||
|
db9fc597f3 |
|||
|
6839f0bdae |
|||
|
6f4ad046b0 |
|||
|
4494705ef9 |
|||
|
237886971c |
|||
|
95a4e03f00 |
|||
|
9054b6b357 |
|||
|
8b056d8ed1 |
|||
|
edbc647a06 |
|||
| 91516c2f20 |
39
.github/workflows/android.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
name: "Build APK"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
tags: ["*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-apk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Lix GHA Installer Action
|
||||||
|
uses: samueldr/lix-gha-installer-action@v2026-02-22
|
||||||
|
with:
|
||||||
|
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
|
||||||
|
|
||||||
|
- name: Decode keystore
|
||||||
|
run: echo "$KEYSTORE_CONTENT" | base64 --decode > keystore.jks
|
||||||
|
env:
|
||||||
|
KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }}
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --release"
|
||||||
|
env:
|
||||||
|
KEYSTORE_PATH: ../../keystore.jks
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload installer artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: APK
|
||||||
|
path: build/app/outputs/flutter-apk/app-release.apk
|
||||||
37
.github/workflows/flatpak.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
name: "Build Flatpaks"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
tags: ["*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-flatpak:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: x86_64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- arch: aarch64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Lix GHA Installer Action
|
||||||
|
uses: samueldr/lix-gha-installer-action@v2026-02-22
|
||||||
|
with:
|
||||||
|
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: nix build .#flatpak
|
||||||
|
|
||||||
|
- name: Upload installer artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: flatpak-${{ matrix.arch }}
|
||||||
|
path: result/nexus.federated.Nexus.flatpak
|
||||||
79
.github/workflows/windows.yml
vendored
|
|
@ -1,46 +1,71 @@
|
||||||
name: "Build Windows Version"
|
name: "Build EXE"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
tags: ["*"]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-exe:
|
||||||
runs-on: "windows-latest"
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout repository"
|
- name: Checkout repository
|
||||||
uses: "actions/checkout@v4"
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: "Set up Flutter"
|
|
||||||
uses: "subosito/flutter-action@v2"
|
|
||||||
|
|
||||||
- name: "Set up Rust"
|
|
||||||
uses: "dtolnay/rust-toolchain@stable"
|
|
||||||
with:
|
with:
|
||||||
targets: "x86_64-pc-windows-msvc"
|
submodules: recursive
|
||||||
|
|
||||||
- name: "Install Flutter dependencies"
|
- name: Set up Flutter
|
||||||
run: flutter pub get
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: 3.41.9
|
||||||
|
|
||||||
- name: "Run build_runner & build Windows EXE"
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: gomuks/go.mod
|
||||||
|
|
||||||
|
- name: Setup MSYS2
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: MINGW64
|
||||||
|
install: >-
|
||||||
|
mingw-w64-x86_64-gcc
|
||||||
|
|
||||||
|
- name: Go build
|
||||||
run: |
|
run: |
|
||||||
flutter pub run build_runner build --delete-conflicting-outputs
|
cd gomuks/pkg/ffi
|
||||||
|
go build -tags goolm,sqlite_fts5 -o ../../../libgomuks.dll -buildmode=c-shared
|
||||||
|
|
||||||
|
- name: Build with Flutter
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
dart scripts/generate.dart
|
||||||
|
flutter pub run build_runner build
|
||||||
flutter build windows --release
|
flutter build windows --release
|
||||||
|
|
||||||
- name: "Upload exe zip"
|
- name: Copy MinGW runtime DLLs
|
||||||
uses: "actions/upload-artifact@v4"
|
shell: msys2 {0}
|
||||||
with:
|
run: |
|
||||||
name: "windows-portable"
|
cp /mingw64/bin/libgcc_s_seh-1.dll build/windows/x64/runner/Release/
|
||||||
path: "build/windows/x64/runner/Release/"
|
cp /mingw64/bin/libwinpthread-1.dll build/windows/x64/runner/Release/
|
||||||
|
cp /mingw64/bin/libstdc++-6.dll build/windows/x64/runner/Release/
|
||||||
|
|
||||||
- name: "Install Inno Setup"
|
- name: Upload exe zip
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: windows-portable
|
||||||
|
path: build/windows/x64/runner/Release/
|
||||||
|
|
||||||
|
- name: Install Inno Setup
|
||||||
run: choco install innosetup -y
|
run: choco install innosetup -y
|
||||||
|
|
||||||
- name: "Build Inno Setup installer"
|
- name: Build Inno Setup installer
|
||||||
run: iscc windows/installer.iss
|
run: iscc windows/installer.iss
|
||||||
|
|
||||||
- name: "Upload installer artifact"
|
- name: Upload installer artifact
|
||||||
uses: "actions/upload-artifact@v4"
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: "windows-installer"
|
name: windows-installer
|
||||||
path: "windows/dist/Nexus-Setup.exe"
|
path: windows/dist/Nexus-Setup.exe
|
||||||
6
.gitignore
vendored
|
|
@ -36,7 +36,9 @@ key.properties
|
||||||
# Generated Files
|
# Generated Files
|
||||||
*.g.dart
|
*.g.dart
|
||||||
*.freezed.dart
|
*.freezed.dart
|
||||||
src/
|
|
||||||
|
|
||||||
# Devel Password
|
# Devel Password
|
||||||
password.txt
|
password.txt
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
/result
|
||||||
4
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[submodule "gomuks"]
|
||||||
|
path = gomuks
|
||||||
|
url = https://github.com/gomuks/gomuks
|
||||||
|
branch = main
|
||||||
8
.vscode/settings.json
vendored
|
|
@ -2,8 +2,14 @@
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Appbar",
|
"Appbar",
|
||||||
"Displayname",
|
"Displayname",
|
||||||
|
"fluttertagger",
|
||||||
|
"Gomuks",
|
||||||
"Homeserver",
|
"Homeserver",
|
||||||
|
"Linkified",
|
||||||
|
"localpart",
|
||||||
|
"msgtype",
|
||||||
|
"muks",
|
||||||
"prefs",
|
"prefs",
|
||||||
"vodozemac"
|
"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/).
|
||||||
137
README.md
|
|
@ -1,11 +1,11 @@
|
||||||
# Nexus Client
|
# Nexus Client
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Nexus Client is still heavily in development, and is not ready for use!
|
> Nexus Client is still in development, and doesn't support everything needed for daily use.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
A simple and user-friendly Matrix client made with Flutter and the Matrix Dart SDK.
|
A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
@ -15,15 +15,11 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
- [ ] New logo
|
|
||||||
- [ ] Make context menus appear as bottom sheets on mobile
|
|
||||||
- [x] Move from the Dart SDK to the Gomuks SDK 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
|
||||||
- [x] Windows
|
- [x] Windows
|
||||||
|
- [x] Android
|
||||||
- [ ] MacOS
|
- [ ] MacOS
|
||||||
- [ ] Android
|
|
||||||
- [ ] iOS
|
- [ ] iOS
|
||||||
- [ ] Web (may not be possible)
|
- [ ] Web (may not be possible)
|
||||||
- [x] Login
|
- [x] Login
|
||||||
|
|
@ -37,13 +33,13 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
- [ ] Searching
|
- [ ] Searching
|
||||||
- [ ] Creating (Rooms, Spaces, and DMs)
|
- [ ] Creating (Rooms, Spaces, and DMs)
|
||||||
- [x] Joining
|
- [x] Joining
|
||||||
- [ ] Parse vias
|
- [x] Parse vias
|
||||||
- [x] Using a text/uri/link
|
- [x] Using a text/uri/link
|
||||||
- [x] Plain text
|
- [x] Plain text
|
||||||
- [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,6 +50,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
- [x] HTML/Markdown
|
- [x] HTML/Markdown
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [x] Choose ping on/off
|
- [x] Choose ping on/off
|
||||||
|
- [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
|
||||||
|
|
@ -62,9 +59,11 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
- [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions)
|
- [ ] Inline emoji picker (Putting this here since it'll be implemented the same way as mentions)
|
||||||
- [ ] Custom emojis/stickers
|
- [ ] Custom emojis/stickers
|
||||||
- [ ] GIFs using Gomuks' GIF proxies
|
- [ ] GIFs using Gomuks' GIF proxies
|
||||||
- [x] Recieving
|
- [x] Receiving
|
||||||
- [x] Plain text
|
- [x] Plain text
|
||||||
|
- [x] Per message profiles
|
||||||
- [x] HTML
|
- [x] HTML
|
||||||
|
- [x] URL Previews
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [x] Viewing
|
- [x] Viewing
|
||||||
- [ ] Jump to original message
|
- [ ] Jump to original message
|
||||||
|
|
@ -77,43 +76,58 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
- [x] Blurhashing
|
- [x] Blurhashing
|
||||||
- [ ] Downloading attachments
|
- [ ] Downloading attachments
|
||||||
- [x] Opening attachments in their own view
|
- [x] Opening attachments in their own view
|
||||||
- [ ] Polls: Waiting on https://github.com/SwanFlutter/dynamic_polls/issues/1
|
- [ ] Polls
|
||||||
- [x] Mentions
|
- [x] Mentions
|
||||||
- [x] Users
|
- [x] Users
|
||||||
|
- [x] Clickable
|
||||||
- [x] Rooms
|
- [x] Rooms
|
||||||
- [ ] Plain text (not sure if I want to add this or not, I probably won't unless there's interest)
|
- [ ] Clickable
|
||||||
- [x] Matrix URIs
|
- [x] Matrix URIs
|
||||||
- [x] Matrix.to links
|
- [x] Matrix.to links
|
||||||
- [ ] Do some fancy fetching to get nice names
|
- [x] Events
|
||||||
- [ ] Make clickable
|
- [ ] Render more nicely
|
||||||
|
- [ ] Clickable
|
||||||
- [x] Custom emojis/stickers
|
- [x] Custom emojis/stickers
|
||||||
- [x] History loading
|
- [x] History loading
|
||||||
- [x] Backwards
|
- [x] Backwards
|
||||||
- [ ] Forwards
|
- [ ] Forwards
|
||||||
- [x] Editing
|
- [x] Editing
|
||||||
- [x] Deleting
|
- [x] Deleting
|
||||||
- [ ] Reactions: Waiting on https://github.com/flyerhq/flutter_chat_ui/pull/838 or me doing a custom impl
|
- [x] Reactions
|
||||||
- [ ] Pins
|
- [ ] Pins
|
||||||
- [ ] Displaying
|
- [ ] Displaying
|
||||||
- [ ] Creating
|
- [ ] Creating
|
||||||
- [ ] Threads
|
- [ ] Threads
|
||||||
- [ ] Profile popouts
|
- [x] Profile popouts
|
||||||
- [ ] Copy link to [room, space]
|
- [x] Working actions
|
||||||
|
- [x] Copy link to:
|
||||||
|
- [x] Room
|
||||||
|
- [x] Space
|
||||||
|
- [x] Message
|
||||||
- [ ] 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
|
||||||
- [ ] 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
|
||||||
- [ ] URL preview: Server / Client / None
|
- [ ] URL preview: Server / Sending Client (Beeper spec) / None
|
||||||
- [ ] Account changes
|
- [ ] Account changes
|
||||||
- [ ] Display name
|
- [ ] Display name
|
||||||
- [ ] Profile picture
|
- [ ] Profile picture
|
||||||
|
|
@ -123,25 +137,64 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S
|
||||||
- [ ] About
|
- [ ] About
|
||||||
- [x] Log Out
|
- [x] Log Out
|
||||||
|
|
||||||
## Build Instructions
|
## Try it out
|
||||||
|
|
||||||
First, clone and open the repo:
|
If you want to try out Nexus, grab one of the following artifacts from CI:
|
||||||
|
|
||||||
```sh
|
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
||||||
git clone https://git.federated.nexus/Henry-Hiles/nexus
|
- Windows
|
||||||
cd nexus
|
- [Portable Build](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-portable.zip)
|
||||||
```
|
- [Installer](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
||||||
|
- Flatpak
|
||||||
|
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
||||||
|
- [x86_64/AMD64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-x86_64.zip)
|
||||||
|
|
||||||
|
Or, try the Nix package: `nix run git+https://git.federated.nexus/Nexus/nexus`
|
||||||
|
|
||||||
|
## Build it yourself
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
- With Nix: Either use direnv, or `nix flake develop`
|
- With Nix: Either use direnv and `direnv allow`, or `nix flake develop`
|
||||||
- Without Nix: Install Flutter, Go, Olm, Git, Clang, and GLibc.
|
- 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, Olm, 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
|
||||||
|
|
||||||
|
First, clone and open the repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --recurse-submodules https://git.federated.nexus/Nexus/nexus
|
||||||
|
cd nexus
|
||||||
|
```
|
||||||
|
|
||||||
### Set up Flutter
|
### Set up Flutter
|
||||||
|
|
||||||
|
|
@ -151,22 +204,23 @@ Get dependencies:
|
||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
|
|
||||||
Get dependencies:
|
Generate Gomuks bindings:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
flutter pub get
|
dart scripts/generate.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
Clone Gomuks and generate bindings:
|
> [!NOTE]
|
||||||
|
> If you are having issues with `stddef.h` not being found, try setting CPATH manually:
|
||||||
```sh
|
>
|
||||||
scripts/generate.sh
|
> ```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:
|
||||||
|
|
@ -175,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!
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@ android {
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
// do we want to update.. eventually?
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "nexus.federated.Nexus"
|
applicationId = "nexus.federated.Nexus"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
|
|
@ -50,7 +55,8 @@ android {
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
release {
|
release {
|
||||||
keyAlias "key"
|
keyAlias "key"
|
||||||
storeFile keystoreProperties['path'] ? file(keystoreProperties['path']) : file(System.getenv("KEYSTORE_PATH"))
|
def storePath = keystoreProperties['path'] ?: System.getenv("KEYSTORE_PATH")
|
||||||
|
storeFile storePath ? file(storePath) : null
|
||||||
keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
|
keyPassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
|
||||||
storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
|
storePassword keystoreProperties['password'] ?: System.getenv("KEYSTORE_PASSWORD")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
android:label="Nexus"
|
android:label="Nexus"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/nexus_round"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="false">
|
android:fullBackupContent="false">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
|
@ -1,9 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground>
|
<foreground>
|
||||||
<inset
|
<inset
|
||||||
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 |
BIN
assets/background.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
257
assets/background.svg
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
<?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="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: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="0.69191503"
|
||||||
|
inkscape:cx="-71.540576"
|
||||||
|
inkscape:cy="281.10388"
|
||||||
|
inkscape:window-width="2544"
|
||||||
|
inkscape:window-height="1363"
|
||||||
|
inkscape:window-x="35"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg11" />
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 20 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: 16 KiB |
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 616 KiB |
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 616 KiB |
113
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": {
|
||||||
|
|
@ -18,13 +18,81 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix2flatpak": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774860670,
|
||||||
|
"narHash": "sha256-YjJkQrvxrErXtfDi3obUn6rNmkA+CIAZ3f5NgL5xuYE=",
|
||||||
|
"owner": "neobrain",
|
||||||
|
"repo": "nix2flatpak",
|
||||||
|
"rev": "61d68e21e3fbc2d57590051f48736bea271f4aba",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "neobrain",
|
||||||
|
"repo": "nix2flatpak",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767640445,
|
"lastModified": 1773389992,
|
||||||
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
|
"narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c06b4ae3d6599a672a6210b7021d699c351eebda",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1777168982,
|
||||||
|
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778869304,
|
||||||
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -34,25 +102,26 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1765674936,
|
|
||||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nixpkgs.lib",
|
|
||||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nixpkgs.lib",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs"
|
"nix2flatpak": "nix2flatpak",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
44
flake.nix
|
|
@ -2,8 +2,10 @@
|
||||||
description = "Nexus Flutter Flake";
|
description = "Nexus Flutter Flake";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
|
self.submodules = true;
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
nix2flatpak.url = "github:neobrain/nix2flatpak";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -33,36 +35,42 @@
|
||||||
_module.args.pkgs = import nixpkgs {
|
_module.args.pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = {
|
config = {
|
||||||
permittedInsecurePackages = [ "olm-3.2.16" ];
|
|
||||||
android_sdk.accept_license = true;
|
android_sdk.accept_license = true;
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells =
|
packages =
|
||||||
let
|
let
|
||||||
packages = with pkgs; [
|
default = pkgs.callPackage ./linux/nix/pkg {
|
||||||
go
|
src = self;
|
||||||
olm
|
|
||||||
git
|
|
||||||
];
|
|
||||||
|
|
||||||
env = {
|
|
||||||
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.libclang ];
|
|
||||||
LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}";
|
|
||||||
CPATH = lib.makeSearchPath "include" [ pkgs.glibc.dev ];
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
inherit default;
|
||||||
inherit env;
|
|
||||||
packages = packages ++ [
|
flatpak = inputs.nix2flatpak.lib.${system}.mkFlatpak {
|
||||||
pkgs.flutter
|
appName = "Nexus";
|
||||||
];
|
developer = "QuadRadical";
|
||||||
|
appId = "nexus.federated.Nexus";
|
||||||
|
package = default;
|
||||||
|
runtime = "org.gnome.Platform/49";
|
||||||
|
permissions = {
|
||||||
|
share = [ "network" ];
|
||||||
|
sockets = [
|
||||||
|
"fallback-x11"
|
||||||
|
"wayland"
|
||||||
|
];
|
||||||
|
devices = [ "dri" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
nix = pkgs.mkShell { inherit packages env; };
|
gomuks = pkgs.callPackage ./linux/nix/pkg/gomuks.nix {
|
||||||
|
src = self;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.callPackage ./linux/nix/devshell.nix { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
gomuks
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 23638a8d2b5ad7ed9f72a0ec39f56cac119c45fb
|
||||||
108
hook/build.dart
|
|
@ -3,11 +3,13 @@ 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 {
|
||||||
final buildDir = input.packageRoot.resolve("src/");
|
if (!input.config.buildCodeAssets) return;
|
||||||
if (await File(buildDir.resolve("lock").toFilePath()).exists()) return;
|
final codeConfig = input.config.code;
|
||||||
|
final targetOS = codeConfig.targetOS;
|
||||||
|
final targetArch = codeConfig.targetArchitecture;
|
||||||
|
|
||||||
final targetOS = input.config.code.targetOS;
|
|
||||||
String libFileName;
|
String libFileName;
|
||||||
|
Map<String, String> env = {};
|
||||||
switch (targetOS) {
|
switch (targetOS) {
|
||||||
case OS.linux:
|
case OS.linux:
|
||||||
libFileName = "libgomuks.so";
|
libFileName = "libgomuks.so";
|
||||||
|
|
@ -18,23 +20,61 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
case OS.windows:
|
case OS.windows:
|
||||||
libFileName = "libgomuks.dll";
|
libFileName = "libgomuks.dll";
|
||||||
break;
|
break;
|
||||||
|
case OS.android:
|
||||||
|
libFileName = "libgomuks.so";
|
||||||
|
|
||||||
|
final targetNdkApi = codeConfig.android.targetNdkApi;
|
||||||
|
|
||||||
|
final ndkHome =
|
||||||
|
Platform.environment["ANDROID_NDK_HOME"] ??
|
||||||
|
Platform.environment["ANDROID_NDK_ROOT"] ??
|
||||||
|
Platform.environment["NDK_HOME"] ??
|
||||||
|
await _findNdkFromSdk();
|
||||||
|
if (ndkHome == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Could not find Android NDK. Set ANDROID_NDK_HOME or install via sdkmanager.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hostTag = _ndkHostTag();
|
||||||
|
final (goArch, ccTriple) = _androidArch(targetArch);
|
||||||
|
final cc =
|
||||||
|
"$ndkHome/toolchains/llvm/prebuilt/$hostTag/bin/$ccTriple$targetNdkApi-clang";
|
||||||
|
|
||||||
|
env = {"CGO_ENABLED": "1", "GOOS": "android", "GOARCH": goArch, "CC": cc};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw UnsupportedError("Unsupported OS: $targetOS");
|
throw UnsupportedError("Unsupported OS: $targetOS");
|
||||||
}
|
}
|
||||||
|
|
||||||
final gomuksBuildDir = buildDir.resolve("gomuks/");
|
var libFile = input.packageRoot.resolve(libFileName);
|
||||||
final libFile = gomuksBuildDir.resolve(libFileName);
|
final gomuksBuildDir = input.packageRoot.resolve("gomuks/");
|
||||||
|
|
||||||
print("Building Gomuks shared library $libFileName from source...");
|
if (!(await File.fromUri(libFile).exists())) {
|
||||||
final result = await Process.run("go", [
|
final buildDir = input.packageRoot.resolve("build/");
|
||||||
"build",
|
libFile = buildDir.resolve("${targetArch.name}/$libFileName");
|
||||||
"-o",
|
|
||||||
libFile.path,
|
|
||||||
"-buildmode=c-shared",
|
|
||||||
], workingDirectory: gomuksBuildDir.resolve("source/pkg/ffi/").toFilePath());
|
|
||||||
|
|
||||||
if (result.exitCode != 0) {
|
// goheif/dav1d supported on Android would need to fix upstream
|
||||||
throw Exception("Failed to build Gomuks shared library\n${result.stderr}");
|
final tags = [
|
||||||
|
"sqlite_fts5",
|
||||||
|
"goolm",
|
||||||
|
if (targetOS == OS.android) "noheic",
|
||||||
|
].join(",");
|
||||||
|
print(
|
||||||
|
"Building Gomuks shared library $libFileName (${targetOS.name}/${targetArch.name}) to ${libFile.path}...",
|
||||||
|
);
|
||||||
|
final result = await Process.run(
|
||||||
|
"go",
|
||||||
|
["build", "-tags", tags, "-o", libFile.path, "-buildmode=c-shared"],
|
||||||
|
workingDirectory: gomuksBuildDir.resolve("pkg/ffi/").toFilePath(),
|
||||||
|
environment: env.isNotEmpty ? env : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw Exception(
|
||||||
|
"Failed to build Gomuks shared library\n${result.stderr}",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final generatedFile = "src/third_party/gomuks.g.dart";
|
final generatedFile = "src/third_party/gomuks.g.dart";
|
||||||
|
|
@ -52,3 +92,43 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
..dependencies.add(gomuksBuildDir);
|
..dependencies.add(gomuksBuildDir);
|
||||||
print("Done!");
|
print("Done!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<String?> _findNdkFromSdk() async {
|
||||||
|
// pretty sure this wont be needed with nix, i'll get this removed
|
||||||
|
final androidHome =
|
||||||
|
Platform.environment["ANDROID_HOME"] ??
|
||||||
|
Platform.environment["ANDROID_SDK_ROOT"];
|
||||||
|
if (androidHome == null) return null;
|
||||||
|
final ndkDir = Directory("$androidHome/ndk");
|
||||||
|
if (!await ndkDir.exists()) return null;
|
||||||
|
final versions = await ndkDir.list().toList();
|
||||||
|
if (versions.isEmpty) return null;
|
||||||
|
versions.sort((a, b) => a.path.compareTo(b.path));
|
||||||
|
return versions.last.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _ndkHostTag() {
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
return "darwin-x86_64";
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
return "linux-x86_64";
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
return "windows-x86_64";
|
||||||
|
}
|
||||||
|
throw UnsupportedError("Unsupported host platform for Android NDK");
|
||||||
|
}
|
||||||
|
|
||||||
|
(String goArch, String ccTriple) _androidArch(Architecture arch) {
|
||||||
|
switch (arch) {
|
||||||
|
case Architecture.arm64:
|
||||||
|
return ("arm64", "aarch64-linux-android");
|
||||||
|
case Architecture.arm:
|
||||||
|
return ("arm", "armv7a-linux-androideabi");
|
||||||
|
case Architecture.x64:
|
||||||
|
return ("amd64", "x86_64-linux-android");
|
||||||
|
case Architecture.ia32:
|
||||||
|
return ("386", "i686-linux-android");
|
||||||
|
default:
|
||||||
|
throw UnsupportedError("Unsupported Android architecture: $arch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
|
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -519,7 +519,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
|
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
|
@ -545,7 +545,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.nexus;
|
PRODUCT_BUNDLE_IDENTIFIER = nexus.federated.Nexus;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
|
||||||
|
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>>(
|
||||||
|
|
|
||||||
30
lib/controllers/author_controller.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import "dart:async";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/user_controller.dart";
|
||||||
|
import "package:nexus/models/content/membership.dart";
|
||||||
|
import "package:nexus/models/event.dart";
|
||||||
|
|
||||||
|
class AuthorController extends AsyncNotifier<MembershipContent> {
|
||||||
|
final Event event;
|
||||||
|
AuthorController(this.event);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MembershipContent> build() async {
|
||||||
|
final member = await ref.watch(
|
||||||
|
UserController.provider(
|
||||||
|
.new(roomId: event.roomId, userId: event.sender),
|
||||||
|
).future,
|
||||||
|
);
|
||||||
|
|
||||||
|
return .new(
|
||||||
|
status: member.status,
|
||||||
|
avatarUrl: event.pmp?.avatarUrl ?? member.avatarUrl,
|
||||||
|
displayName: event.pmp?.displayName ?? member.displayName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<AuthorController, MembershipContent, Event>(
|
||||||
|
AuthorController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "dart:developer";
|
|
||||||
import "dart:ffi";
|
import "dart:ffi";
|
||||||
|
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";
|
||||||
|
|
@ -13,7 +13,7 @@ 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/models/client_state.dart";
|
import "package:nexus/main.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";
|
||||||
|
|
@ -25,17 +25,27 @@ import "package:nexus/models/profile.dart";
|
||||||
import "package:nexus/models/requests/paginate_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/requests/report_request.dart";
|
import "package:nexus/models/requests/report_request.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/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";
|
||||||
|
|
||||||
class ClientController extends AsyncNotifier<int> {
|
class ClientController extends AsyncNotifier<int> {
|
||||||
@override
|
@override
|
||||||
Future<int> build() async {
|
Future<int> build() async {
|
||||||
final handle = await Isolate.run(GomuksInit);
|
final Pointer<Char> root;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final dir = await getApplicationSupportDirectory();
|
||||||
|
root = "${dir.path}/gomuks".toNativeUtf8().cast();
|
||||||
|
} else {
|
||||||
|
root = nullptr.cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
final handle = GomuksInit(root);
|
||||||
|
|
||||||
final callable =
|
final callable =
|
||||||
NativeCallable<
|
NativeCallable<
|
||||||
|
|
@ -54,15 +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;
|
||||||
|
case "send_complete":
|
||||||
|
final event = Event.fromJson(decodedMuksEvent["event"]);
|
||||||
|
ref
|
||||||
|
.watch(RoomsController.provider.notifier)
|
||||||
|
.update(
|
||||||
|
.new({
|
||||||
|
event.roomId: .new(events: .new({event.rowId: event})),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "sync_complete":
|
case "sync_complete":
|
||||||
final syncData = SyncData.fromJson(decodedMuksEvent);
|
final syncData = SyncData.fromJson(decodedMuksEvent);
|
||||||
|
|
@ -102,8 +124,12 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
debugPrint("Finished handling $muksEventType...");
|
debugPrint("Finished handling $muksEventType...");
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
debugger();
|
if (kDebugMode) {
|
||||||
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
debugPrintStack(stackTrace: stackTrace, label: error.toString());
|
||||||
|
rethrow;
|
||||||
|
} else {
|
||||||
|
showError(error, stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -140,15 +166,18 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<void> redactEvent(RedactEventRequest report) =>
|
Future<void> redactEvent(RedactEventRequest report) =>
|
||||||
_sendCommand("redact_event", report.toJson());
|
_sendCommand("redact_event", report.toJson());
|
||||||
|
|
||||||
Future<void> sendMessage(SendMessageRequest request) =>
|
Future<Event> sendMessage(SendMessageRequest request) async =>
|
||||||
_sendCommand("send_message", request.toJson());
|
Event.fromJson(await _sendCommand("send_message", request.toJson()));
|
||||||
|
|
||||||
Future<bool> verify(String recoveryKey) async {
|
Future<Event> sendEvent(SendEventRequest request) async =>
|
||||||
|
Event.fromJson(await _sendCommand("send_event", request.toJson()));
|
||||||
|
|
||||||
|
Future<String?> verify(String recoveryKey) async {
|
||||||
try {
|
try {
|
||||||
await _sendCommand("verify", {"recovery_key": recoveryKey});
|
await _sendCommand("verify", {"recovery_key": recoveryKey});
|
||||||
return true;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return error.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,9 +202,15 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
// }));
|
// }));
|
||||||
|
|
||||||
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
|
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
|
||||||
final response =
|
Future<List?> getState(GetRoomStateRequest request) async =>
|
||||||
(await _sendCommand("get_room_state", request.toJson())) as List;
|
(await _sendCommand("get_room_state", request.toJson())) as List?;
|
||||||
return response.map((event) => Event.fromJson(event)).toIList();
|
final response = await getState(request);
|
||||||
|
|
||||||
|
return .new(
|
||||||
|
(response ?? await getState(request.copyWith(refetch: true)) ?? []).map(
|
||||||
|
(event) => .fromJson(event),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<IList<Event>?> getRelatedEvents(
|
Future<IList<Event>?> getRelatedEvents(
|
||||||
|
|
@ -183,32 +218,31 @@ 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 report) =>
|
Future<void> reportEvent(ReportRequest request) =>
|
||||||
_sendCommand("report_event", report.toJson());
|
_sendCommand("report_event", request.toJson());
|
||||||
|
|
||||||
|
Future<void> setMembership(SetMembershipRequest request) =>
|
||||||
|
_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", {
|
||||||
|
|
@ -218,21 +252,21 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> login(LoginRequest login) async {
|
Future<String?> login(LoginRequest login) async {
|
||||||
try {
|
try {
|
||||||
await _sendCommand("login", login.toJson());
|
await _sendCommand("login", login.toJson());
|
||||||
return true;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return error.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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": "@fakeuser:${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 +1,46 @@
|
||||||
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_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/event.dart";
|
import "package:nexus/models/event.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/requests/get_room_state_request.dart";
|
||||||
|
|
||||||
class MembersController extends Notifier<IList<Event>> {
|
class MembersController extends AsyncNotifier<ISet<Event>> {
|
||||||
final Room room;
|
final String roomId;
|
||||||
MembersController(this.room);
|
MembersController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IList<Event> build() => (room.state["m.room.member"]?.values ?? [])
|
Future<ISet<Event>> build() async {
|
||||||
.map(
|
final room = ref.watch(
|
||||||
(eventRowId) =>
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
room.events.firstWhereOrNull((event) => event.rowId == eventRowId),
|
);
|
||||||
)
|
|
||||||
.nonNulls
|
|
||||||
.where((member) => member.content["membership"] == "join")
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
static final provider = NotifierProvider.family
|
if (room == null) return .new();
|
||||||
.autoDispose<MembersController, IList<Event>, Room>(
|
|
||||||
MembersController.new,
|
if (!room.hasFetchedMembers) {
|
||||||
);
|
final fetchedState = await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.getRoomState(
|
||||||
|
GetRoomStateRequest(
|
||||||
|
roomId: roomId,
|
||||||
|
fetchMembers: !(room.metadata?.hasMemberList ?? false),
|
||||||
|
includeMembers: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(RoomsController.provider.notifier)
|
||||||
|
.addState(roomId, fetchedState, isMembers: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.state[EventType.membership.type]?.values
|
||||||
|
.map((rowId) => room.events[rowId])
|
||||||
|
.nonNulls
|
||||||
|
.toISet() ??
|
||||||
|
.new();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.autoDispose
|
||||||
|
.family<MembersController, ISet<Event>, String>(MembersController.new);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
lib/controllers/members_grouped_controller.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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/room_creators_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/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 roomCreators = room == null
|
||||||
|
? null
|
||||||
|
: ref.watch((RoomCreatorsController.provider(room)));
|
||||||
|
|
||||||
|
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 = roomCreators?.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,205 +0,0 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
|
||||||
import "package:nexus/controllers/client_state_controller.dart";
|
|
||||||
import "package:nexus/controllers/members_controller.dart";
|
|
||||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
|
||||||
import "package:nexus/models/message_config.dart";
|
|
||||||
|
|
||||||
class MessageController extends AsyncNotifier<Message?> {
|
|
||||||
final MessageConfig config;
|
|
||||||
MessageController(this.config);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Message?> build() async {
|
|
||||||
try {
|
|
||||||
if (config.event.relationType == "m.replace" && !config.includeEdits) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ref.mounted) return null;
|
|
||||||
final event = config.event.lastEditRowId == null
|
|
||||||
? config.event
|
|
||||||
: config.room.events.firstWhereOrNull(
|
|
||||||
(e) => e.rowId == config.event.lastEditRowId,
|
|
||||||
) ??
|
|
||||||
config.event;
|
|
||||||
|
|
||||||
if (!ref.mounted) return null;
|
|
||||||
|
|
||||||
final members = ref.read(MembersController.provider(config.room));
|
|
||||||
final author = members.firstWhereOrNull(
|
|
||||||
(member) => member.stateKey == event.authorId,
|
|
||||||
);
|
|
||||||
if (!ref.mounted) return null;
|
|
||||||
|
|
||||||
final content = (event.decrypted ?? event.content);
|
|
||||||
final type = (config.event.decryptedType ?? config.event.type);
|
|
||||||
final newContent = content["m.new_content"] as Map?;
|
|
||||||
|
|
||||||
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
|
|
||||||
? (newContent?["body"] ?? content["body"] ?? "")
|
|
||||||
: "Deleted Message",
|
|
||||||
"flashing": false,
|
|
||||||
"timelineId": event.timelineRowId,
|
|
||||||
"big": event.localContent?.bigEmoji == true,
|
|
||||||
"eventType": type,
|
|
||||||
"avatarUrl": author?.content["avatar_url"],
|
|
||||||
"editSource":
|
|
||||||
event.localContent?.editSource ??
|
|
||||||
newContent?["body"] ??
|
|
||||||
content["body"],
|
|
||||||
"displayName": author?.content["displayname"]?.isNotEmpty == true
|
|
||||||
? author?.content["displayname"]
|
|
||||||
: event.authorId.substring(1).split(":")[0],
|
|
||||||
"txnId": config.event.transactionId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!ref.mounted) return null;
|
|
||||||
|
|
||||||
final editedAt = event.relationType == "m.replace"
|
|
||||||
? event.timestamp
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if ((event.redactedBy != null && !config.alwaysReturn) ||
|
|
||||||
(!config.includeEdits &&
|
|
||||||
(config.event.relationType == "m.replace"))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use server-generated preview if enabled
|
|
||||||
|
|
||||||
// final match = Uri.tryParse(
|
|
||||||
// RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "",
|
|
||||||
// );
|
|
||||||
|
|
||||||
final replyId =
|
|
||||||
config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"];
|
|
||||||
|
|
||||||
final asText =
|
|
||||||
Message.text(
|
|
||||||
metadata: metadata,
|
|
||||||
id: config.event.eventId,
|
|
||||||
authorId: event.authorId,
|
|
||||||
text:
|
|
||||||
newContent?["formatted_body"] ??
|
|
||||||
newContent?["body"] ??
|
|
||||||
content["formatted_body"] ??
|
|
||||||
content["body"] ??
|
|
||||||
"",
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
editedAt: editedAt,
|
|
||||||
)
|
|
||||||
as TextMessage;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
authorId: event.authorId,
|
|
||||||
source: source,
|
|
||||||
replyToMessageId: replyId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
),
|
|
||||||
_ => asText,
|
|
||||||
},
|
|
||||||
"m.room.member" =>
|
|
||||||
content["membership"] == event.unsigned["prev_content"]?["membership"]
|
|
||||||
? null
|
|
||||||
: Message.system(
|
|
||||||
metadata: {
|
|
||||||
...metadata,
|
|
||||||
"body":
|
|
||||||
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
|
|
||||||
"invite" => "was invited to",
|
|
||||||
"join" => "joined",
|
|
||||||
"leave" => "left",
|
|
||||||
"knock" => "asked to join",
|
|
||||||
"ban" => "was banned from",
|
|
||||||
_ => "did something relating to",
|
|
||||||
}} the room.",
|
|
||||||
},
|
|
||||||
id: config.event.eventId,
|
|
||||||
authorId: event.authorId,
|
|
||||||
deliveredAt: config.event.timestamp,
|
|
||||||
text:
|
|
||||||
"${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) {
|
|
||||||
"invite" => "was invited to",
|
|
||||||
"join" => "joined",
|
|
||||||
"leave" => "left",
|
|
||||||
"knock" => "asked to join",
|
|
||||||
"ban" => "was banned from",
|
|
||||||
_ => "did something relating to",
|
|
||||||
}} the room.",
|
|
||||||
),
|
|
||||||
|
|
||||||
"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,
|
|
||||||
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/message_config.dart";
|
|
||||||
import "package:nexus/models/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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
81
lib/controllers/power_level_controller.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
|
import "package:nexus/controllers/room_creators_controller.dart";
|
||||||
|
import "package:nexus/controllers/rooms_controller.dart";
|
||||||
|
import "package:nexus/models/configs/power_level_config.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
|
||||||
|
class PowerLevelController extends Notifier<bool> {
|
||||||
|
final PowerLevelConfig config;
|
||||||
|
PowerLevelController(this.config);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
if (config case EventPowerLevelConfig(:final eventType)) {
|
||||||
|
assert(
|
||||||
|
eventType != .redaction,
|
||||||
|
"Checking power level for a redaction should use [PowerLevelConfig.redaction].",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((value) => value[config.roomId]),
|
||||||
|
);
|
||||||
|
|
||||||
|
final roomCreators = room == null
|
||||||
|
? null
|
||||||
|
: ref.watch(RoomCreatorsController.provider(room));
|
||||||
|
|
||||||
|
final eventRowId = room?.state[EventType.powerLevels.type]?[""];
|
||||||
|
|
||||||
|
final event = eventRowId == null ? null : room?.events[eventRowId];
|
||||||
|
final content = event?.content is PowerLevelsContent
|
||||||
|
? event!.content
|
||||||
|
: PowerLevelsContent();
|
||||||
|
|
||||||
|
final user = ref.watch(
|
||||||
|
ClientStateController.provider.select((value) => value?.userId),
|
||||||
|
);
|
||||||
|
if (user == null || content is! PowerLevelsContent) return false;
|
||||||
|
|
||||||
|
double powerLevelOf(String userId) => roomCreators?.contains(userId) == true
|
||||||
|
? double.infinity
|
||||||
|
: (content.users[userId] ?? content.usersDefault).toDouble();
|
||||||
|
|
||||||
|
final userLevel = powerLevelOf(user);
|
||||||
|
|
||||||
|
return switch (config) {
|
||||||
|
EventPowerLevelConfig(:final eventType) =>
|
||||||
|
userLevel >= (content.events[eventType.type] ?? content.eventsDefault),
|
||||||
|
|
||||||
|
MembershipActionPowerLevelConfig(:final action, :final targetUser) =>
|
||||||
|
switch (action) {
|
||||||
|
.invite => userLevel >= content.invite,
|
||||||
|
|
||||||
|
.kick =>
|
||||||
|
userLevel >= content.kick && userLevel > powerLevelOf(targetUser),
|
||||||
|
|
||||||
|
.ban =>
|
||||||
|
userLevel >= content.ban && userLevel > powerLevelOf(targetUser),
|
||||||
|
|
||||||
|
.unban => userLevel >= content.ban,
|
||||||
|
},
|
||||||
|
|
||||||
|
StatePowerLevelConfig(:final eventType) =>
|
||||||
|
userLevel >= (content.events[eventType.type] ?? content.stateDefault),
|
||||||
|
|
||||||
|
RedactionPowerLevelConfig(:final targetUser) =>
|
||||||
|
userLevel >=
|
||||||
|
(targetUser == user
|
||||||
|
? (content.events[EventType.redaction.type] ??
|
||||||
|
content.eventsDefault)
|
||||||
|
: content.redact),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = NotifierProvider.autoDispose
|
||||||
|
.family<PowerLevelController, bool, PowerLevelConfig>(
|
||||||
|
PowerLevelController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
17
lib/controllers/profile_controller.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/models/profile.dart";
|
||||||
|
|
||||||
|
class ProfileController extends AsyncNotifier<Profile> {
|
||||||
|
final String userId;
|
||||||
|
ProfileController(this.userId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Profile> build() {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
return client.getProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.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,187 +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_chat_core/flutter_chat_core.dart" as chat;
|
|
||||||
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/models/message_config.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/messages_config.dart";
|
import "package:nexus/models/content/reaction.dart";
|
||||||
import "package:nexus/models/requests/get_room_state_request.dart";
|
import "package:nexus/models/event.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_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,
|
|
||||||
fetchMembers: room.metadata?.hasMemberList == false,
|
|
||||||
includeMembers: true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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 {
|
|
||||||
final controller = await future;
|
|
||||||
for (final event in next) {
|
|
||||||
if (event.type == "m.room.redaction") {
|
|
||||||
final controller = await future;
|
|
||||||
final message = controller.messages.firstWhereOrNull(
|
|
||||||
(message) => message.id == event.content["redacts"],
|
|
||||||
);
|
|
||||||
if (message == null || !ref.mounted) return;
|
|
||||||
|
|
||||||
await controller.removeMessage(message);
|
|
||||||
} 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 &&
|
|
||||||
!controller.messages.any(
|
|
||||||
(oldMessage) => oldMessage.id == message.id,
|
|
||||||
) &&
|
|
||||||
ref.mounted) {
|
|
||||||
await controller.insertMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, weak: true).close,
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.onDispose(controller.dispose);
|
|
||||||
|
|
||||||
// While there are under 20 messages, try up to two times to load more messages.
|
|
||||||
for (var i = 0; i < 2 && messages.length < 20; i++) {
|
|
||||||
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;
|
.watch(ClientController.provider.notifier)
|
||||||
final oldMessage = message.metadata?["txnId"] == null
|
.redactEvent(
|
||||||
? null
|
RedactEventRequest(
|
||||||
: controller.messages.firstWhereOrNull(
|
eventId: event.eventId,
|
||||||
(element) =>
|
roomId: roomId,
|
||||||
element.metadata?["txnId"] == message.metadata?["txnId"],
|
reason: reason,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return oldMessage == null
|
Future<bool> loadOlder() async {
|
||||||
? controller.insertMessage(message)
|
final timelineKeys = ref
|
||||||
: controller.updateMessage(oldMessage, message);
|
.read(RoomsController.provider.select((value) => value[roomId]))
|
||||||
}
|
?.timeline
|
||||||
|
.keys;
|
||||||
Future<void> deleteMessage(Message message, {String? reason}) async {
|
|
||||||
final controller = await future;
|
|
||||||
await controller.removeMessage(message);
|
|
||||||
await ref
|
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.redactEvent(
|
|
||||||
RedactEventRequest(
|
|
||||||
eventId: message.id,
|
|
||||||
roomId: roomId,
|
|
||||||
reason: reason,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadOlder([InMemoryChatController? chatController]) async {
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -190,51 +91,33 @@ 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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
return response.hasMore;
|
||||||
if (room == null) return;
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> send(
|
Future<void> send(
|
||||||
String message, {
|
String text, {
|
||||||
bool shouldMention = true,
|
bool shouldMention = true,
|
||||||
required Iterable<Tag> tags,
|
required IList<Tag> tags,
|
||||||
required RelationType relationType,
|
required RelationType relationType,
|
||||||
Message? relation,
|
Event? relation,
|
||||||
}) async {
|
}) async {
|
||||||
var taggedMessage = message;
|
var taggedMessage = text;
|
||||||
|
|
||||||
for (final tag in tags) {
|
for (final tag in tags) {
|
||||||
final escaped = RegExp.escape(tag.id);
|
final escaped = RegExp.escape(tag.id);
|
||||||
|
|
@ -247,7 +130,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
client.sendMessage(
|
final event = await client.sendMessage(
|
||||||
SendMessageRequest(
|
SendMessageRequest(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
mentions: Mentions(
|
mentions: Mentions(
|
||||||
|
|
@ -255,50 +138,85 @@ 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),
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<chat.User> resolveUser(String id) async {
|
|
||||||
final user = await ref
|
|
||||||
.watch(ClientController.provider.notifier)
|
|
||||||
.getProfile(id);
|
|
||||||
return chat.User(
|
|
||||||
id: id,
|
|
||||||
name: user.displayName,
|
|
||||||
// imageSource: user.avatarUrl == null
|
|
||||||
// ? null
|
|
||||||
// : (await ref.watch(
|
|
||||||
// AvatarController.provider(user.avatarUrl!.toString()).future,
|
|
||||||
// )).toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
||||||
|
.new({
|
||||||
|
roomId: .new(
|
||||||
|
events: .new({event.rowId: event}),
|
||||||
|
sticky: .new({event.rowId}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
.new(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return await controller.scrollToMessage(message.id);
|
Future<void> removeReaction(
|
||||||
|
String reaction,
|
||||||
|
Event event,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
final allReactionEvents = await client.getRelatedEvents(
|
||||||
|
.new(
|
||||||
|
roomId: roomId,
|
||||||
|
eventId: event.eventId,
|
||||||
|
relationType: "m.annotation",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final reactionEvents = allReactionEvents
|
||||||
|
?.where((event) => event.redactedBy == null)
|
||||||
|
.toIList();
|
||||||
|
|
||||||
|
final reactionEvent = reactionEvents?.firstWhereOrNull(
|
||||||
|
(event) => switch (event.content) {
|
||||||
|
ReactionContent(:final key) =>
|
||||||
|
key == reaction && event.sender == userId,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reactionEvent != null) {
|
||||||
|
await ref
|
||||||
|
.watch(ClientController.provider.notifier)
|
||||||
|
.redactEvent(.new(eventId: reactionEvent.eventId, roomId: roomId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendReaction(String reaction, Event event) async {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
|
await client.sendEvent(
|
||||||
|
.new(
|
||||||
|
roomId: roomId,
|
||||||
|
type: EventType.reaction.type,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": event.eventId,
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
"key": reaction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
synchronous: true,
|
||||||
|
disableEncryption: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = AsyncNotifierProvider.family
|
static final provider = AsyncNotifierProvider.family
|
||||||
.autoDispose<RoomChatController, InMemoryChatController, String>(
|
.autoDispose<RoomChatController, IList<Event>?, String>(
|
||||||
RoomChatController.new,
|
RoomChatController.new,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
lib/controllers/room_creators_controller.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/models/content/content.dart";
|
||||||
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
|
class RoomCreatorsController extends Notifier<IList<String>> {
|
||||||
|
final Room room;
|
||||||
|
RoomCreatorsController(this.room);
|
||||||
|
|
||||||
|
@override
|
||||||
|
IList<String> build() {
|
||||||
|
final createRowId = room.state[EventType.create.type]?[""];
|
||||||
|
final createEvent = createRowId == null ? null : room.events[createRowId];
|
||||||
|
|
||||||
|
if (createEvent == null) return .new();
|
||||||
|
|
||||||
|
final createEventContent = switch (createEvent.content) {
|
||||||
|
CreateContent content => content,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return switch (createEventContent?.additionalCreatorIds) {
|
||||||
|
IList<String> creators => creators.add(createEvent.sender),
|
||||||
|
_ => .new([createEvent.sender]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
NotifierProvider.family<RoomCreatorsController, IList<String>, Room>(
|
||||||
|
RoomCreatorsController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,40 @@
|
||||||
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/new_events_controller.dart";
|
import "package:nexus/models/event.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();
|
||||||
|
|
||||||
|
Future<void> addState(
|
||||||
|
String roomId,
|
||||||
|
IList<Event> state, {
|
||||||
|
bool isMembers = false,
|
||||||
|
}) async => update(
|
||||||
|
.new({
|
||||||
|
roomId: Room(
|
||||||
|
events: .fromEntries(state.map((event) => .new(event.rowId, event))),
|
||||||
|
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) {
|
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) {
|
||||||
|
|
@ -15,54 +42,41 @@ class RoomsController extends Notifier<IMap<String, Room>> {
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
||||||
|
sticky:
|
||||||
|
(incoming.sticky.isEmpty == true
|
||||||
|
? existing.sticky
|
||||||
|
: existing.sticky.addAll(incoming.sticky))
|
||||||
|
.removeWhere(
|
||||||
|
(rowId) => incoming.timeline.values.contains(rowId),
|
||||||
|
),
|
||||||
metadata: incoming.metadata ?? existing.metadata,
|
metadata: incoming.metadata ?? existing.metadata,
|
||||||
events: events!,
|
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
|
|
@ -74,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);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
@ -5,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
|
||||||
|
|
@ -15,99 +16,133 @@ 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,
|
||||||
final children = childRoomsBySpaceId[id] ?? IList<Room>();
|
children: homeRooms,
|
||||||
return Space(
|
subSpaces: .new(),
|
||||||
id: id,
|
),
|
||||||
title: room.metadata?.name ?? "Unnamed Room",
|
.new(
|
||||||
room: room,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.nonNulls
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
return <Space>[
|
|
||||||
Space(id: "home", title: "Home", icon: Icons.home, children: homeRooms),
|
|
||||||
Space(
|
|
||||||
id: "dms",
|
id: "dms",
|
||||||
title: "Direct Messages",
|
title: "Direct Messages",
|
||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
children: dmRooms,
|
children: dmRooms,
|
||||||
|
subSpaces: .new(),
|
||||||
),
|
),
|
||||||
...topLevelSpacesList,
|
...spaces,
|
||||||
].toIList();
|
];
|
||||||
|
|
||||||
|
return allSpaces
|
||||||
|
.map(
|
||||||
|
(space) => space.copyWith(
|
||||||
|
children: .new(
|
||||||
|
space.children
|
||||||
|
.sortedBy(
|
||||||
|
(element) =>
|
||||||
|
element
|
||||||
|
.metadata
|
||||||
|
?.sortingTimestamp
|
||||||
|
.millisecondsSinceEpoch ??
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.sortedBy((room) => room.metadata?.unreadMessages ?? 0)
|
||||||
|
.reversed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toIList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final provider = NotifierProvider<SpacesController, IList<Space>>(
|
static final provider = NotifierProvider<SpacesController, IList<Space>>(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/models/sync_status.dart";
|
import "package:nexus/models/sync_status.dart";
|
||||||
|
|
||||||
class SyncStatusController extends Notifier<SyncStatus?> {
|
class SyncStatusController extends Notifier<SyncStatus?> {
|
||||||
@override
|
@override
|
||||||
Null build() => null;
|
Null build() => null;
|
||||||
|
|
||||||
void set(SyncStatus newStatus) => state = newStatus;
|
void set(SyncStatus newStatus) {
|
||||||
|
if (newStatus.type == .permanentlyFailed) {
|
||||||
|
showError(newStatus.error ?? "Syncing failed");
|
||||||
|
}
|
||||||
|
state = newStatus;
|
||||||
|
}
|
||||||
|
|
||||||
static final provider = NotifierProvider<SyncStatusController, SyncStatus?>(
|
static final provider = NotifierProvider<SyncStatusController, SyncStatus?>(
|
||||||
SyncStatusController.new,
|
SyncStatusController.new,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
51
lib/controllers/url_preview_controller.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import "dart:convert";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:http/http.dart";
|
||||||
|
import "package:nexus/controllers/client_state_controller.dart";
|
||||||
|
import "package:nexus/controllers/header_controller.dart";
|
||||||
|
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||||
|
import "package:nexus/models/open_graph_data.dart";
|
||||||
|
|
||||||
|
class UrlPreviewController extends AsyncNotifier<OpenGraphData?> {
|
||||||
|
final String link;
|
||||||
|
UrlPreviewController(this.link);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<OpenGraphData?> build() async {
|
||||||
|
final homeserver = ref.watch(
|
||||||
|
ClientStateController.provider.select((value) => value?.homeserverUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (homeserver != null && !link.contains("matrix.to")) {
|
||||||
|
{
|
||||||
|
final response = await get(
|
||||||
|
.parse(homeserver)
|
||||||
|
.resolve("/_matrix/client/v1/media/preview_url")
|
||||||
|
.replace(queryParameters: {"url": link}),
|
||||||
|
headers: await ref.watch(HeaderController.provider.future),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final decodedValue = json.decode(response.body);
|
||||||
|
if (decodedValue is! Map<String, dynamic>) return null;
|
||||||
|
|
||||||
|
final mxc = decodedValue["og:image"];
|
||||||
|
final image = mxc == null
|
||||||
|
? null
|
||||||
|
: Uri.tryParse(mxc)?.mxcToHttps(homeserver);
|
||||||
|
|
||||||
|
return .fromJson(decodedValue).copyWith(imageUrl: image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider.family<
|
||||||
|
UrlPreviewController,
|
||||||
|
OpenGraphData?,
|
||||||
|
String
|
||||||
|
>(UrlPreviewController.new);
|
||||||
|
}
|
||||||