Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4fcb5b0d9 | ||
|
|
1911004d05 | ||
|
|
5d67564445 | ||
|
|
be04c5a46e | ||
|
|
bd7a4c9dfb | ||
|
|
10ee57722e | ||
|
|
ff5f7ab50e | ||
|
|
277885a61e | ||
|
|
6633ebc376 | ||
|
|
b2d9986cd3 | ||
|
|
a0b9bb277f | ||
|
|
d381705cdd | ||
| 3820d4264a | |||
|
|
cf4e2d3fad | ||
|
|
002dc87577 | ||
|
|
922e7ad0ff | ||
|
|
5d7be8a672 | ||
|
|
431b357cfa | ||
|
|
2938acf152 | ||
|
|
4127f70e4d | ||
|
|
c07221cc12 | ||
|
|
33b2f95e3f | ||
|
|
2a5cd9b218 | ||
|
|
a15fed034d | ||
|
|
b9641ac021 | ||
|
|
2145bd8846 | ||
|
|
672b97b310 | ||
|
|
974da6ec90 | ||
|
|
f19bbcd010 | ||
|
|
a1468c92c8 | ||
|
|
6dd125a087 | ||
|
|
842ecc4235 | ||
|
|
db66793d28 | ||
|
|
324da027e9 | ||
|
|
f2f45e53a8 | ||
|
|
cacab45fe3 | ||
|
|
908d428220 | ||
|
|
8a6d726b8c | ||
|
|
40275d3d14 | ||
|
|
5039f1ba3b | ||
|
|
299aac134d | ||
|
|
044171c002 | ||
|
|
ec9155d8f0 | ||
|
|
221fe3edcc | ||
|
|
e166f17cae | ||
|
|
640fe0d476 | ||
|
|
3942de3222 | ||
|
|
b00111381a | ||
|
|
715dca561f | ||
|
|
44d7f61788 | ||
|
|
518739e29f | ||
|
|
06e6e9c5b1 | ||
|
|
17cfff153b | ||
|
|
9f224b9839 | ||
|
|
56e1bb6bfd | ||
|
|
6529ce0d0f | ||
|
|
81c7f7decd | ||
|
|
85868cc0bd | ||
|
|
1d0c842bad | ||
|
|
02bcc98037 | ||
|
|
1a1c166ab0 | ||
|
|
692d6042c5 | ||
|
|
fa2ed930eb | ||
|
|
9785b0023c | ||
|
|
313c94a4f1 | ||
|
|
2bb0dce7a1 | ||
|
|
b1785d4b8a | ||
|
|
3f15fa365f | ||
|
|
a56ebb245e | ||
|
|
d300cdb58f | ||
|
|
d88a1cd2d6 | ||
|
|
3070b38d2e | ||
|
|
38db3b35aa | ||
|
|
c4447adf8c | ||
|
|
0b91d3cecc | ||
|
|
3b3d20f250 | ||
|
|
0376e7f4b8 | ||
|
|
77f43fbde0 | ||
|
|
e203508c42 | ||
|
|
8b6745763a | ||
|
|
8c4b2ade88 | ||
|
|
4e5f2ff05f | ||
|
|
fd1b62fc8d | ||
|
|
f53d17eab7 | ||
|
|
c3b3f762eb | ||
|
|
d288603c07 | ||
|
|
e61682ef46 | ||
|
|
3b228bb58b | ||
|
|
b62c41ce57 | ||
|
|
a4397e9cec | ||
|
|
981d69706c | ||
|
|
0a4683d8a6 | ||
|
|
43f6284ada | ||
|
|
8cf6297560 | ||
|
|
1f42d7dff0 | ||
|
|
abbf18e1dc | ||
|
|
102f3bba8e | ||
|
|
8faba7bdf2 | ||
|
|
d41d21f8e1 | ||
|
|
298cd6245c | ||
|
|
8bd88c4845 | ||
|
|
e3bf76d8e2 | ||
|
|
3445d5be21 | ||
|
|
a838ba3000 | ||
|
|
15b655413d | ||
|
|
2418c61498 | ||
|
|
dfacbf9e32 | ||
|
|
85f5b69d6e | ||
|
|
08797da53d | ||
|
|
d3bd2c3a08 | ||
|
|
1086c0e5cd | ||
|
|
60df0761a5 | ||
|
|
650d878649 | ||
|
|
06f399c76d | ||
|
|
7c5b60474f | ||
|
|
d81f92f2e2 | ||
|
|
1a8038e51d | ||
|
|
54a6ce8391 | ||
|
|
b1c38d766f | ||
|
|
f44e24aec1 | ||
|
|
325dcf901a | ||
|
|
77e1da8318 | ||
|
|
b0a58d8524 | ||
|
|
623c3bfc2e | ||
|
|
5cd6cf79a2 | ||
|
|
e1d54fa992 | ||
|
|
b1c481c4d1 | ||
|
|
64821e4ec5 | ||
|
|
88585fb192 | ||
|
|
25f5d5f4fd | ||
|
|
d0d33ce2c8 | ||
|
|
32d8791fe9 | ||
|
|
e76f42a706 | ||
|
|
32f4e471cf | ||
|
|
dd86a0fb00 | ||
|
|
a603597b20 | ||
|
|
23b14e8730 | ||
|
|
c4a7650e39 | ||
|
|
c4bb5fa523 | ||
|
|
fa684d265d | ||
|
|
3eeaad17f0 | ||
|
|
8d2697ffc6 | ||
|
|
150ba308e9 | ||
|
|
ce18cfdf2a | ||
|
|
17bccc0dea | ||
|
|
f8b7a6fef5 | ||
|
|
a68f7e8936 | ||
|
|
74ef50af49 | ||
|
|
70698cf4ec | ||
|
|
9bed679568 | ||
|
|
99595caee5 | ||
|
|
b39018e454 | ||
|
|
08bed5d668 | ||
|
|
b78868397c | ||
|
|
cbb4553fc0 | ||
|
|
3ddd661fb5 | ||
|
|
4e8f2c3040 | ||
|
|
52ee4b923a | ||
|
|
166fcce8ba | ||
|
|
92072904e6 | ||
|
|
9ace2fe07b | ||
|
|
8913b42803 | ||
|
|
253a3afbdd | ||
|
|
af2907b946 | ||
|
|
02a0c4760a | ||
|
|
e9899fce8e | ||
|
|
63a9f9ca90 | ||
|
|
b5364e1619 | ||
|
|
742dcb8f41 | ||
|
|
dda90c85a4 | ||
|
|
16455da39f | ||
|
|
b69e3969cb | ||
|
|
14c904b214 | ||
|
|
0d12c31393 | ||
|
|
d6b3482ce2 | ||
|
|
f4eb6318cc | ||
|
|
c8b421d43e | ||
|
|
87280a0e21 | ||
|
|
96d3f83933 | ||
|
|
bb3eddb021 | ||
|
|
59c403603b | ||
|
|
fd7d732762 | ||
|
|
fc8fe60613 | ||
|
|
2acf49a12b | ||
|
|
2f6799470c | ||
|
|
507cd1f17e | ||
|
|
f1a8716832 | ||
|
|
f99aad5a18 | ||
|
|
caa816beec | ||
|
|
605ba186d6 | ||
|
|
8fa14659be | ||
|
|
e1fe8c2ed5 | ||
|
|
2e73051b81 | ||
|
|
1a61ed5d85 | ||
|
|
9ad8550449 | ||
|
|
8422c2bf3c | ||
|
|
bf2e6b2787 | ||
|
|
9e01d51520 | ||
|
|
07b92bf876 | ||
|
|
4863c4a8f3 | ||
|
|
9def8de5a8 | ||
|
|
f08968800e | ||
|
|
d8dee5905d | ||
|
|
c2768ae40a | ||
|
|
a4a852ede8 | ||
|
|
ede1e289ce | ||
|
|
d211dd4aeb | ||
|
|
20c1dbd00a | ||
|
|
fa02384808 | ||
|
|
dd4f2fcc35 | ||
|
|
1b2708f4b0 | ||
|
|
7c9e2bfe3c | ||
|
|
13365488b4 | ||
|
|
02f564f7ed | ||
|
|
0222e6ecd5 | ||
|
|
df91290a18 | ||
|
|
57618417a1 | ||
|
|
5e3c47a433 | ||
|
|
b30622c1b9 | ||
|
|
eb651212e6 | ||
|
|
78aa686699 | ||
|
|
156217c3ae | ||
|
|
644ef388de | ||
|
|
71eac7078e | ||
|
|
511dd41d30 | ||
|
|
f24b3ee09b | ||
|
|
3b6321383e | ||
|
|
8888fd5884 | ||
|
|
0accfe4a26 | ||
|
|
535081b483 | ||
|
|
0f10dfaf91 | ||
|
|
f0ac88c9ee | ||
|
|
f4d13e86b6 | ||
|
|
303ccbe965 | ||
|
|
ade164d824 | ||
|
|
e40c86c7e6 | ||
|
|
a87946a34b | ||
|
|
863c5f1c13 | ||
|
|
6d21b54c62 | ||
|
|
c984a272b3 | ||
|
|
6cf0903af3 | ||
|
|
16a533cf40 | ||
|
|
3e8d14a53f | ||
|
|
1925578a86 | ||
|
|
fc62ff3eaa | ||
|
|
15dbe4433d | ||
|
|
a8d6dadf84 | ||
|
|
ee2b55ca78 | ||
|
|
090b026b92 | ||
|
|
e461cb1f53 | ||
|
|
5212d7ce4d | ||
|
|
ed9e58d0bf | ||
|
|
2b2c230fcf | ||
|
|
ecf5adcdea | ||
|
|
6851d34d35 | ||
|
|
006f2f527f | ||
|
|
1cb20c9ec5 | ||
|
|
ec49c5a541 | ||
|
|
7bbd898131 | ||
|
|
031da77da6 | ||
|
|
bfd76fdb79 | ||
|
|
0c28169043 | ||
|
|
fc1b49cf69 | ||
|
|
2b701cf56c | ||
|
|
8b386142cf | ||
|
|
cd7e27a6e0 | ||
|
|
9ab3332824 | ||
|
|
a60e1435c2 | ||
|
|
d56ca697ea | ||
|
|
9eaaef1048 | ||
|
|
a95cd85eb0 | ||
|
|
32f347ee6f | ||
|
|
d9645480ac | ||
|
|
73174003a9 | ||
|
|
3c17d812b3 | ||
|
|
e08f601bfd | ||
|
|
799bec5ad9 | ||
|
|
a602dc08d5 | ||
|
|
a48e42dfcf | ||
|
|
9384ef0503 | ||
|
|
d8d9df4e50 | ||
|
|
db2c768029 | ||
|
|
3353eef728 | ||
|
|
fba6f60500 | ||
|
|
10fcf2bac4 | ||
|
|
1cbfb592e9 | ||
|
|
dfc933115f | ||
|
|
2d0e7a491e | ||
|
|
e49408e5b0 | ||
|
|
69e0c48c8e | ||
|
|
4345ab9772 | ||
|
|
a76ea48d84 | ||
|
|
78a5206ba7 | ||
|
|
ac11341677 | ||
|
|
446fa75a90 | ||
|
|
d52bba3523 | ||
|
|
55c8475eba | ||
|
|
80b5a2b3d4 | ||
|
|
da91c2bd9f | ||
|
|
116b4ca361 | ||
|
|
5fc4230f9b | ||
|
|
cf88eb1fd7 | ||
|
|
cef5fbe3c8 | ||
|
|
2ccc30879b | ||
|
|
c670dc2700 | ||
|
|
d3240fd261 | ||
| 9cfffdfa66 | |||
|
|
564d1180ed | ||
|
|
ca03042ff8 | ||
|
|
50dc19b0de | ||
|
|
72767f0716 | ||
|
|
59d3793060 | ||
|
|
a090346046 | ||
|
|
ede5fdc348 | ||
|
|
90482009fc | ||
|
|
4c91ea6002 | ||
|
|
92fa8e1ca5 | ||
|
|
3442e44a07 | ||
|
|
2cab1fb03b | ||
|
|
13a3321bab | ||
|
|
0f69a1fefb | ||
|
|
ab5aea4caf | ||
|
|
8eba2b2a1f | ||
|
|
44ea0a9da5 | ||
|
|
538715843b | ||
|
|
0c07b2967a | ||
|
|
0262d776d5 | ||
|
|
9bad93c2df | ||
|
|
5c10e96a7b | ||
|
|
a309cb78ac | ||
|
|
918acb9beb | ||
|
|
ecd549cd9e | ||
|
|
bd41fa3873 | ||
|
|
b493f18e54 | ||
|
|
215e78f3d5 | ||
|
|
ad9e34a49e | ||
|
|
8503bbd33c | ||
|
|
426ca2480f | ||
|
|
1d1710302a | ||
|
|
972df73dd9 | ||
|
|
ce889e2c23 | ||
|
|
1eca7c9b07 | ||
|
|
85d3a11030 | ||
|
|
bf084f1ccc | ||
|
|
4afb9a4790 | ||
|
|
ddb7cc841b | ||
|
|
a1f60b7ff9 | ||
|
|
8632154832 | ||
|
|
1f71227221 | ||
|
|
3d9e94f08d | ||
|
|
b76f270e24 | ||
|
|
8f9e1a9142 | ||
|
|
fbb68686ea | ||
|
|
9eee50dbae | ||
|
|
3be35991b5 | ||
|
|
1aa0ea2cea | ||
|
|
a7dd62c721 | ||
|
|
1542a4b66c | ||
|
|
d39bcbafde | ||
|
|
8513d74cc1 | ||
|
|
22abd54176 | ||
|
|
ba885ca69e | ||
|
|
754b919531 | ||
|
|
8fd2d3918c | ||
|
|
d000f6e5a7 | ||
|
|
66858cdf12 | ||
|
|
4fb1a76060 | ||
|
|
de23cb0f5b | ||
|
|
cf7053f338 | ||
|
|
20e26b3747 | ||
|
|
ed075a35b6 | ||
|
|
6b3252b6ad | ||
|
|
5e5132c290 | ||
|
|
b4df8c129d | ||
|
|
37bf943ac7 | ||
|
|
264f36ea59 | ||
|
|
b894a4542a | ||
|
|
6e9e3d05d2 | ||
|
|
d0b32e44ce | ||
|
|
caa3823c26 | ||
|
|
df33df35da | ||
|
|
63abbf403a | ||
|
|
6ff4f480ac | ||
|
|
fd152baa28 | ||
|
|
09a74bf3ee | ||
|
|
4cd501d00c |
2
.gitignore
vendored
@ -10,7 +10,6 @@
|
|||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
lib/generated_plugin_registrant.dart
|
|
||||||
prime
|
prime
|
||||||
|
|
||||||
# libolm package
|
# libolm package
|
||||||
@ -38,7 +37,6 @@ prime
|
|||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Web related
|
# Web related
|
||||||
lib/generated_plugin_registrant.dart
|
|
||||||
docs/build/
|
docs/build/
|
||||||
docs/.jekyll-cache/
|
docs/.jekyll-cache/
|
||||||
docs/_site/
|
docs/_site/
|
||||||
|
|||||||
140
.gitlab-ci.yml
@ -1,7 +1,7 @@
|
|||||||
variables:
|
variables:
|
||||||
FLUTTER_VERSION: 3.3.9
|
FLUTTER_VERSION: 3.10.0
|
||||||
|
|
||||||
image: cirrusci/flutter:${FLUTTER_VERSION}
|
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||||
|
|
||||||
.shared_windows_runners:
|
.shared_windows_runners:
|
||||||
tags:
|
tags:
|
||||||
@ -16,20 +16,22 @@ stages:
|
|||||||
|
|
||||||
code_analyze:
|
code_analyze:
|
||||||
stage: test
|
stage: test
|
||||||
script: [./scripts/code_analyze.sh]
|
script:
|
||||||
|
- flutter pub get
|
||||||
|
- dart run import_sorter:main --no-comments --exit-if-changed
|
||||||
|
- dart format lib/ test/ --set-exit-if-changed
|
||||||
|
- flutter analyze
|
||||||
|
- git apply ./scripts/enable-android-google-services.patch
|
||||||
|
- flutter pub get
|
||||||
|
- flutter analyze
|
||||||
|
- flutter pub run dart_code_metrics:metrics lib -r gitlab > code-quality-report.json || true
|
||||||
artifacts:
|
artifacts:
|
||||||
reports:
|
reports:
|
||||||
codequality: code-quality-report.json
|
codequality: code-quality-report.json
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- famedly
|
|
||||||
|
|
||||||
widget_test:
|
widget_test:
|
||||||
stage: test
|
stage: test
|
||||||
script: [flutter test]
|
script: [flutter test]
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- famedly
|
|
||||||
|
|
||||||
# the basic integration test configuration testing FLOSS builds on Synapse
|
# the basic integration test configuration testing FLOSS builds on Synapse
|
||||||
.integration_test:
|
.integration_test:
|
||||||
@ -49,15 +51,13 @@ widget_test:
|
|||||||
FF_NETWORK_PER_BUILD: "true"
|
FF_NETWORK_PER_BUILD: "true"
|
||||||
# Tell docker CLI how to talk to Docker daemon.
|
# Tell docker CLI how to talk to Docker daemon.
|
||||||
DOCKER_HOST: tcp://docker:2375/
|
DOCKER_HOST: tcp://docker:2375/
|
||||||
# Use the overlayfs driver for improved performance.
|
# Use the btrfs driver for improved performance.
|
||||||
DOCKER_DRIVER: overlay2
|
DOCKER_DRIVER: btrfs
|
||||||
# Disable TLS since we're running inside local network.
|
# Disable TLS since we're running inside local network.
|
||||||
DOCKER_TLS_CERTDIR: ""
|
DOCKER_TLS_CERTDIR: ""
|
||||||
HOMESERVER: "docker"
|
HOMESERVER: docker
|
||||||
before_script:
|
before_script:
|
||||||
# start AVD and keep running in background
|
- scripts/integration-prepare-host.sh
|
||||||
- scripts/integration-start-avd.sh &
|
|
||||||
- scripts/integration-prepare-alpine.sh
|
|
||||||
# create test user environment variables
|
# create test user environment variables
|
||||||
- source scripts/integration-create-environment-variables.sh
|
- source scripts/integration-create-environment-variables.sh
|
||||||
# create Synapse instance
|
# create Synapse instance
|
||||||
@ -65,31 +65,57 @@ widget_test:
|
|||||||
# properly set the homeserver IP and create test users
|
# properly set the homeserver IP and create test users
|
||||||
- scripts/integration-prepare-homeserver.sh
|
- scripts/integration-prepare-homeserver.sh
|
||||||
script:
|
script:
|
||||||
|
# start AVD and keep running in background
|
||||||
|
- scripts/integration-start-avd.sh &
|
||||||
- flutter pub get
|
- flutter pub get
|
||||||
- flutter test integration_test
|
- scrcpy --no-display --record video.mkv &
|
||||||
timeout: 20m
|
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||||
|
after_script:
|
||||||
|
- ffmpeg -i video.mkv -vf scale=iw/2:-2 -crf 40 -b:v 2000k -preset fast video.mp4 || true
|
||||||
|
timeout: 30m
|
||||||
|
retry: 2
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- video.mp4
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- famedly
|
- famedly
|
||||||
|
|
||||||
|
|
||||||
# integration tests for Linux builds
|
# integration tests for Linux builds
|
||||||
|
### disabled because of Linux headless issues
|
||||||
.integration_test_linux:
|
.integration_test_linux:
|
||||||
extends: .integration_test
|
extends: .integration_test
|
||||||
|
parallel:
|
||||||
|
matrix:
|
||||||
|
- HOMESERVER_IMPLEMENTATION:
|
||||||
|
- conduit
|
||||||
script:
|
script:
|
||||||
- apk add cmake ninja gtk+3.0-dev clang pkgconf xz-dev libsecret-dev jsoncpp-dev
|
- apt-get update
|
||||||
|
- apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libjsoncpp-dev
|
||||||
- flutter pub get
|
- flutter pub get
|
||||||
- flutter test integration_test -d linux
|
- flutter test integration_test -d linux --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||||
|
after_script: []
|
||||||
|
artifacts:
|
||||||
|
|
||||||
# extending the default tests to test the Google-flavored builds
|
# extending the default tests to test the Google-flavored builds
|
||||||
.integration_test_proprietary:
|
.integration_test_proprietary:
|
||||||
extends: .integration_test
|
extends: .integration_test
|
||||||
|
parallel:
|
||||||
|
matrix:
|
||||||
|
- HOMESERVER_IMPLEMENTATION:
|
||||||
|
- conduit
|
||||||
script:
|
script:
|
||||||
|
# start AVD and keep running in background
|
||||||
|
- scripts/integration-start-avd.sh &
|
||||||
- git apply ./scripts/enable-android-google-services.patch
|
- git apply ./scripts/enable-android-google-services.patch
|
||||||
- flutter pub get
|
- flutter pub get
|
||||||
- flutter test integration_test
|
- scrcpy --no-display --record video.mkv &
|
||||||
|
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
|
||||||
|
|
||||||
.release_mode_launches:
|
release_mode_launches:
|
||||||
parallel:
|
parallel:
|
||||||
matrix:
|
matrix:
|
||||||
- FLAVOR:
|
- FLAVOR:
|
||||||
@ -99,15 +125,17 @@ widget_test:
|
|||||||
stage: test
|
stage: test
|
||||||
before_script:
|
before_script:
|
||||||
- |
|
- |
|
||||||
if [ "$FLAVOR" == "proprietary" ]; then
|
if [ "$FLAVOR" == "proprietary" ]; then
|
||||||
git apply ./scripts/enable-android-google-services.patch
|
git apply ./scripts/enable-android-google-services.patch
|
||||||
fi
|
fi
|
||||||
script:
|
script:
|
||||||
# start AVD and keep running in background
|
# start AVD and keep running in background
|
||||||
- scripts/integration-start-avd.sh &
|
- scripts/integration-start-avd.sh &
|
||||||
# generate temporary release build configuration and ensure app launches
|
# generate temporary release build configuration and ensure app launches
|
||||||
- scripts/integration-check-release-build.sh
|
- scripts/integration-check-release-build.sh
|
||||||
timeout: 20m
|
timeout: 20m
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- famedly
|
- famedly
|
||||||
@ -116,13 +144,11 @@ build_web:
|
|||||||
stage: build
|
stage: build
|
||||||
before_script:
|
before_script:
|
||||||
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
|
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
|
||||||
script: [./scripts/build-web.sh]
|
script:
|
||||||
|
- flutter build web --release --verbose --source-maps
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/web/
|
- build/web/
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- famedly
|
|
||||||
|
|
||||||
# yes, we *do* build a Windows DLL on Linux. More reliable.
|
# yes, we *do* build a Windows DLL on Linux. More reliable.
|
||||||
build_olm_windows:
|
build_olm_windows:
|
||||||
@ -137,6 +163,7 @@ build_olm_windows:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- libolm.dll
|
- libolm.dll
|
||||||
|
allow_failure: true
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- tags
|
- tags
|
||||||
@ -160,34 +187,35 @@ build_windows:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/windows/runner/Release
|
- build/windows/runner/Release
|
||||||
|
allow_failure: true
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- tags
|
- tags
|
||||||
|
|
||||||
build_android_debug:
|
build_android_debug:
|
||||||
stage: build
|
stage: build
|
||||||
script: [./scripts/build-android-debug.sh]
|
script: [flutter build apk --debug]
|
||||||
artifacts:
|
artifacts:
|
||||||
when: on_success
|
when: on_success
|
||||||
paths:
|
paths:
|
||||||
- build/app/outputs/apk/debug/app-debug.apk
|
- build/app/outputs/apk/debug/app-debug.apk
|
||||||
except:
|
|
||||||
- main
|
|
||||||
- tags
|
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- famedly
|
- famedly
|
||||||
|
except:
|
||||||
|
- main
|
||||||
|
- tags
|
||||||
|
|
||||||
build_android_apk:
|
build_android_apk:
|
||||||
stage: build
|
stage: build
|
||||||
before_script:
|
before_script:
|
||||||
- git apply ./scripts/enable-android-google-services.patch
|
- git apply ./scripts/enable-android-google-services.patch
|
||||||
- ./scripts/prepare-android-release.sh
|
- ./scripts/prepare-android-release.sh
|
||||||
script: [./scripts/build-android-apk.sh]
|
script: [flutter build apk --release]
|
||||||
artifacts:
|
artifacts:
|
||||||
when: on_success
|
when: on_success
|
||||||
paths:
|
paths:
|
||||||
- build/android/app-release.apk
|
- build/app/outputs/apk/release/app-release.apk
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- famedly
|
- famedly
|
||||||
@ -227,9 +255,6 @@ fdroid_repo:
|
|||||||
needs:
|
needs:
|
||||||
- "build_android_apk"
|
- "build_android_apk"
|
||||||
resource_group: playstore_release
|
resource_group: playstore_release
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- famedly
|
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
@ -249,12 +274,7 @@ pages:
|
|||||||
- cd ..
|
- cd ..
|
||||||
- mv docs public
|
- mv docs public
|
||||||
- mv repo public || true
|
- mv repo public || true
|
||||||
- mv build/web/ public/nightly
|
- mv build/web/ public/web
|
||||||
# ensure the nightly deployment knows its location
|
|
||||||
- sed -i "s/href=\"\/web\/\"/href=\"\/nightly\/\"/g" public/nightly/index.html
|
|
||||||
- rm -rf build
|
|
||||||
- ./scripts/download-web-stable.sh
|
|
||||||
- mv stable public/web
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
@ -263,11 +283,11 @@ pages:
|
|||||||
|
|
||||||
build_linux_x86:
|
build_linux_x86:
|
||||||
stage: build
|
stage: build
|
||||||
|
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/flutter-linux/stable:${FLUTTER_VERSION}
|
||||||
before_script:
|
before_script:
|
||||||
[
|
- sudo apt-get update
|
||||||
sudo apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration -y && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y,
|
- sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y
|
||||||
]
|
script: [flutter build linux --release]
|
||||||
script: [./scripts/build-linux.sh]
|
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
- famedly
|
- famedly
|
||||||
@ -278,8 +298,9 @@ build_linux_x86:
|
|||||||
|
|
||||||
build_linux_arm64:
|
build_linux_arm64:
|
||||||
stage: build
|
stage: build
|
||||||
before_script: [flutter upgrade]
|
before_script:
|
||||||
script: [./scripts/build-linux.sh]
|
- flutter upgrade $FLUTTER_VERSION --force
|
||||||
|
script: [flutter build linux --release]
|
||||||
tags: [docker_arm64]
|
tags: [docker_arm64]
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
@ -293,8 +314,6 @@ build_linux_arm64:
|
|||||||
update_dependencies:
|
update_dependencies:
|
||||||
stage: build
|
stage: build
|
||||||
needs: []
|
needs: []
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
only:
|
only:
|
||||||
- schedules
|
- schedules
|
||||||
variables:
|
variables:
|
||||||
@ -319,9 +338,6 @@ update_dependencies:
|
|||||||
.release:
|
.release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: curlimages/curl:latest
|
image: curlimages/curl:latest
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- famedly
|
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
|
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
|
||||||
- if: '$CI_COMMIT_TAG =~ /^rc\d+\.\d+\.\d+-\d+$/'
|
- if: '$CI_COMMIT_TAG =~ /^rc\d+\.\d+\.\d+-\d+$/'
|
||||||
@ -334,28 +350,29 @@ upload_android:
|
|||||||
extends: .release
|
extends: .release
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file build/android/app-release.apk ${PACKAGE_REGISTRY_URL}/fluffychat.apk
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file build/app/outputs/apk/release/app-release.apk ${PACKAGE_REGISTRY_URL}/fluffychat.apk
|
||||||
|
|
||||||
upload_web:
|
upload_web:
|
||||||
extends: .release
|
extends: .release
|
||||||
script:
|
script:
|
||||||
- tar czf package.tar.gz -C build/web/ .
|
- tar czf package.tar.gz -C build/web/ .
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
|
||||||
|
|
||||||
upload_linux_x86:
|
upload_linux_x86:
|
||||||
extends: .release
|
extends: .release
|
||||||
script:
|
script:
|
||||||
- tar czf package.tar.gz -C build/linux/x64/release/bundle/ .
|
- tar czf package.tar.gz -C build/linux/x64/release/bundle/ .
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-x86.tar.gz
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-x86.tar.gz
|
||||||
|
|
||||||
upload_linux_arm64:
|
upload_linux_arm64:
|
||||||
extends: .release
|
extends: .release
|
||||||
script:
|
script:
|
||||||
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
|
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
upload_windows:
|
upload_windows:
|
||||||
extends: .release
|
extends: .release
|
||||||
@ -365,8 +382,9 @@ upload_windows:
|
|||||||
- mv build/windows/runner/Release/fluffychat.msix fluffychat.msix
|
- mv build/windows/runner/Release/fluffychat.msix fluffychat.msix
|
||||||
- cd build/windows/runner/Release; zip -r ../../../../package.zip . ; cd -
|
- cd build/windows/runner/Release; zip -r ../../../../package.zip . ; cd -
|
||||||
- |
|
- |
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
|
||||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
|
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
deploy_playstore:
|
deploy_playstore:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
25
.metadata
@ -4,7 +4,7 @@
|
|||||||
# This file should be version controlled.
|
# This file should be version controlled.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
|
||||||
channel: stable
|
channel: stable
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@ -13,26 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
|
||||||
- platform: android
|
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
- platform: ios
|
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
- platform: linux
|
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
- platform: macos
|
- platform: macos
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
|
||||||
- platform: web
|
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
- platform: windows
|
|
||||||
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
|
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
200
CHANGELOG.md
@ -1,3 +1,203 @@
|
|||||||
|
## v1.12.0
|
||||||
|
- Added translation using Weblate (Toki Pona) (Mæve Rey)
|
||||||
|
- Translated using Weblate (Arabic) (Rex_sa)
|
||||||
|
- Translated using Weblate (Chinese (Simplified)) (Eric)
|
||||||
|
- Translated using Weblate (Croatian) (Milo Ivir)
|
||||||
|
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (Indonesian) (Linerly)
|
||||||
|
- Translated using Weblate (Polish) (lauren n. liberda)
|
||||||
|
- Translated using Weblate (Romanian) (Riley)
|
||||||
|
- Translated using Weblate (Russian) (DarkCoder15)
|
||||||
|
- Translated using Weblate (Spanish) (José Muñoz)
|
||||||
|
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||||
|
- build: Remove dependency overwrite for ffi (Krille)
|
||||||
|
- build: Update dependencies (Krille)
|
||||||
|
- builds: Change minsdkversion of Android from 16 to 19 (Krille)
|
||||||
|
- builds: Do not allow failure for linux x86 (Krille)
|
||||||
|
- builds: Do not use verbose mode on building linux (Krille)
|
||||||
|
- builds: Linux with flutter 3.10 (Krille)
|
||||||
|
- builds: Remove workaround for building linux arm64 (Krille)
|
||||||
|
- builds: Update file_picker to 5.3.0 (Krille)
|
||||||
|
- builds: Update flutter table html (Krille)
|
||||||
|
- builds: Update flutter_html (Krille)
|
||||||
|
- builds: migrate to dart 3.0/flutter 3.10 (lauren n. liberda)
|
||||||
|
- chore: Add missing blockquote style (Krille)
|
||||||
|
- chore: Allow failure in build linux for now (Krille)
|
||||||
|
- chore: Ask for storage persistence (Krille)
|
||||||
|
- chore: Clean unused translations (Malin Errenst)
|
||||||
|
- chore: Enhance room pills (Krille)
|
||||||
|
- chore: Minor code clean up (Krille)
|
||||||
|
- chore: Update flutter webrtc (Krille)
|
||||||
|
- chore: Upgrade to Flutter 3.10.1 (Malin Errenst)
|
||||||
|
- chore: change release curl calls to use --fail-with-body (Tim Flink)
|
||||||
|
- chore: update macOS icons and add build script (TheOneWithTheBraid)
|
||||||
|
- design: Replace anime images with neutral cupertino icons (Krille)
|
||||||
|
- feat: Add toggle to mute notifications from chat groups (fbievan)
|
||||||
|
- feat: Allow ruby tags in html (Krille)
|
||||||
|
- feat: Display progress value for initial sync (Krille)
|
||||||
|
- feat: Implement new error reporting tool when critical features break like playing audio or video messages or opening a chat (Krille)
|
||||||
|
- feat: clean up macOS build metadata (TheOneWithTheBraid)
|
||||||
|
- feat: set display information correctly (TheOneWithTheBraid)
|
||||||
|
- feat: update macOS build files (TheOneWithTheBraid)
|
||||||
|
- feat: update macOS build information for macOS Ventura (TheOneWithTheBraid)
|
||||||
|
- fix "Unhandled Exception: VRouter.of(context) was called with a context which does not contain a VRouter." (Lauren N. Liberda)
|
||||||
|
- fix: Broken arb file (Krille)
|
||||||
|
- fix: Do not unnecessary request all members in public rooms (Krille)
|
||||||
|
- fix: Remove wrong rendered linebreak in html (Krille)
|
||||||
|
- fix: Scroll down button (Krille)
|
||||||
|
- fix: Scroll up and scroll down buttons in chat list (Krille)
|
||||||
|
- fix: Scrolldown button (Krille)
|
||||||
|
- fix: Too long file name cause a render overflow (Skying)
|
||||||
|
- fix: Try to reload timeline on IOException (Krille)
|
||||||
|
- fix: User pills (Krille)
|
||||||
|
- fix: broken CI artifact uploads (TheOneWithTheBraid)
|
||||||
|
- fix: custom emote placeholder (TheOneWithTheBraid)
|
||||||
|
- fix: path of libolm (TheOneWithTheBraid)
|
||||||
|
- fix: Quick account switching (JHansen)
|
||||||
|
- fix: read reciepts (JHansen)
|
||||||
|
- perf: Use valuenotifier to not rebuild chatlist (Krille)
|
||||||
|
- refactor: Reimplement flutter matrix html locally (Krille)
|
||||||
|
- refactor: Update Roboto and Noto Emoji (The one with the Braid)
|
||||||
|
- refactor: Use AnimatedSize for FAB (Krille)
|
||||||
|
- refactor: Use DateTime for weekday localization (Malin Errenst)
|
||||||
|
|
||||||
|
## v1.11.2
|
||||||
|
- Translated using Weblate (Croatian) (Milo Ivir)
|
||||||
|
- Translated using Weblate (Dutch) (Jelv)
|
||||||
|
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (Polish) (Eryk Michalak)
|
||||||
|
- Translated using Weblate (Turkish) (Oğuz Ersen)
|
||||||
|
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||||
|
- feat: Permission dialog before open link in browser (Krille)
|
||||||
|
- fix: Chats do not load (Krille)
|
||||||
|
|
||||||
|
## v1.11.1 - 2023-04-20
|
||||||
|
- fix: Download files on web and iOS with correct mimetype
|
||||||
|
|
||||||
|
## v1.11.0 - 2023-04-14
|
||||||
|
- feat: Add visual read marker (Krille)
|
||||||
|
- feat: Jump to last read event (Krille)
|
||||||
|
- feat: Use fragmented timeline to jump to event (Krille)
|
||||||
|
- feat: change to flutterwebauth2 (ShootingStarDragons)
|
||||||
|
- fix: Join public room (Krille)
|
||||||
|
- fix: Set fcm priority to max on android (Krille)
|
||||||
|
- refactor: CI scripts and old workarounds for build scripts (Krille)
|
||||||
|
- refactor: Client in ChatPage (Krille)
|
||||||
|
- refactor: Not nullable room in ChatPage (Krille)
|
||||||
|
- refactor: Switch to file_picker package and get rid of some dependency overrides (Krille)
|
||||||
|
- refactor: Use correct Matrix instance (Krille)
|
||||||
|
- style: Make emptypage logo bigger (Krille)
|
||||||
|
- style: Minor adjustments for modal bottom sheets (Krille)
|
||||||
|
- style: Move chats to top (Krille)
|
||||||
|
- style: Use SliverList for chatlist (Krille)
|
||||||
|
- refactor: Container -> SizedBox.shrink() (noob_tea)
|
||||||
|
- Translated using Weblate (Chinese (Simplified)) (Eric)
|
||||||
|
- Translated using Weblate (Dutch) (Jelv)
|
||||||
|
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||||
|
- Translated using Weblate (French) (Anne Onyme 017)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (Indonesian) (Linerly)
|
||||||
|
- Translated using Weblate (Persian) (Parsa)
|
||||||
|
- Translated using Weblate (Persian) (Siavash)
|
||||||
|
- Translated using Weblate (Polish) (Luna)
|
||||||
|
- Translated using Weblate (Swedish) (Kristoffer Grundström)
|
||||||
|
- Translated using Weblate (Turkish) (Oğuz Ersen)
|
||||||
|
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||||
|
|
||||||
|
## v1.10.0 - 2023-02-25
|
||||||
|
- Added translation using Weblate (Thai) (Wphaoka)
|
||||||
|
- Added translation using Weblate (Tibetan) (Nathan Freitas)
|
||||||
|
- Default hardcoded message when l10n is not available (fabienli)
|
||||||
|
- Fix: The stable repo fingerprint (TODO the qr-code should be updated) (machiav3lli)
|
||||||
|
- Translated using Weblate (Basque) (xabirequejo)
|
||||||
|
- Translated using Weblate (Dutch) (Jelv)
|
||||||
|
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||||
|
- Translated using Weblate (French) (Anne Onyme 017)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (Indonesian) (Linerly)
|
||||||
|
- Translated using Weblate (Japanese) (Suguru Hirahara)
|
||||||
|
- Translated using Weblate (Persian) (Farooq Karimi Zadeh)
|
||||||
|
- Translated using Weblate (Swedish) (Joaquim Homrighausen)
|
||||||
|
- Translated using Weblate (Turkish) (Oğuz Ersen)
|
||||||
|
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||||
|
- chore: Disable stable for web until script is fixed (Krille)
|
||||||
|
- chore: Display warning when logout without backup (Krille)
|
||||||
|
- chore: Downgrade flutter CI version (Krille)
|
||||||
|
- chore: Follow up audioplayer on linux (Krille)
|
||||||
|
- chore: Follow up chat encryption desgin (Krille)
|
||||||
|
- chore: Follow up fix audioplayer on android (Christian Pauly)
|
||||||
|
- chore: Follow up formatting (Christian Pauly)
|
||||||
|
- chore: Follow up formatting (Krille)
|
||||||
|
- chore: Follow up remove hero animation (Krille)
|
||||||
|
- chore: Follow up secrity settings design (Krille)
|
||||||
|
- chore: Follow up settings page (Krille)
|
||||||
|
- chore: Follow up settings page design (Christian Pauly)
|
||||||
|
- chore: Follow up style adjustments (Krille)
|
||||||
|
- chore: Lookup l10n in pushhelper if null (Krille)
|
||||||
|
- chore: Update matrix package to 0.17.0 (Krille)
|
||||||
|
- chore: Update to Flutter 3.7.1 (Krille)
|
||||||
|
- docs/qr-stable.svg: update the QR code (Aminda Suomalainen)
|
||||||
|
- feat: Enable audioplayer for web and linux (Christian Pauly)
|
||||||
|
- fix: Display error when user tries to send too large file (Christian Pauly)
|
||||||
|
- refactor: Do only instantiate AudioPlayer() object when in use (Christian Pauly)
|
||||||
|
- refactor: Remove syncstatus verbose logs (Christian Pauly)
|
||||||
|
- refactor: Store cached files in tmp directory so OS will clear file cache from time to time (Krille)
|
||||||
|
- style: Adjust key verification dialog (Christian Pauly)
|
||||||
|
- style: Bootstrap design adjustments (Christian Pauly)
|
||||||
|
- style: Encryption page adjustments (Christian Pauly)
|
||||||
|
- style: Enhance user device settings design (Krille)
|
||||||
|
- style: Enhanced chat details design (Krille)
|
||||||
|
- style: Give chat list list tiles rounded corners (Krille)
|
||||||
|
- style: Link underline color (Christian Pauly)
|
||||||
|
- style: Make adaptive bottom sheets scrollable by default (Krille)
|
||||||
|
- style: Make invite page more pretty (Krille)
|
||||||
|
- style: New settings design (Krille)
|
||||||
|
- style: Nicer chips in encryption settings and icons showing device status (Krille)
|
||||||
|
- style: Use emojis on web as well (Christian Pauly)
|
||||||
|
- style: Use robotomono to display device keys (Christian Pauly)
|
||||||
|
- utils/url_launcher: force opening http(s) links in external browser (Marcus Hoffmann)
|
||||||
|
|
||||||
|
## v1.9.0 - 2023-01-29
|
||||||
|
- Translated using Weblate (Czech) (Michal Bedáň)
|
||||||
|
- Translated using Weblate (Czech) (grreby)
|
||||||
|
- Translated using Weblate (Dutch) (Jelv)
|
||||||
|
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||||
|
- Translated using Weblate (Galician) (josé m)
|
||||||
|
- Translated using Weblate (German) (Christian)
|
||||||
|
- Translated using Weblate (German) (Vri 🌈)
|
||||||
|
- Translated using Weblate (Indonesian) (Linerly)
|
||||||
|
- Translated using Weblate (Korean) (Youngbin Han)
|
||||||
|
- Translated using Weblate (Polish) (Wiktor)
|
||||||
|
- Translated using Weblate (Turkish) (Oğuz Ersen)
|
||||||
|
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||||
|
- chore: Change invite link textfield label (Krille)
|
||||||
|
- chore: Remove unused dependency (Krille)
|
||||||
|
- chore: Remove unused translations (Krille)
|
||||||
|
- chore: Update Matrix SDK and refactor (Krille)
|
||||||
|
- chore: Update dependencies (Krille)
|
||||||
|
- chore: Update flutter_map (Krille)
|
||||||
|
- chore: add integration tests (TheOneWithTheBraid)
|
||||||
|
- chore: add integration tests for spaces (TheOneWithTheBraid)
|
||||||
|
- design: More clear chat background and rounded popup menu (Krille)
|
||||||
|
- design: Nicer navigationrail (Krille)
|
||||||
|
- design: Upgrade to Flutter 3.7
|
||||||
|
- feat: Bring back disabling the header bar on Linux desktop (q234rty)
|
||||||
|
- feat: Nicer design for abandonded DM rooms (Christian Pauly)
|
||||||
|
- fix: Archive (Krille)
|
||||||
|
- fix: Shared preferences package for flutter 3.7 (Christian Pauly)
|
||||||
|
- fix: permission of web builds (TheOneWithTheBraid)
|
||||||
|
- fix: Notification Settings (Krille)
|
||||||
|
- refactor: Migrate to Flutter 3.7.0 (Christian Pauly)
|
||||||
|
- refactor: Same animations everywhere in app (Krille)
|
||||||
|
- refactor: Stories header with futurebuilder (Krille)
|
||||||
|
- refactor: disable some redundant tests (TheOneWithTheBraid)
|
||||||
|
- style: Animate in out search results (Krille)
|
||||||
|
- style: New modal bottom sheets (Krille)
|
||||||
|
- style: Redesign public room bottomsheets (Krille)
|
||||||
|
|
||||||
## v1.8.0 2022-12-30
|
## v1.8.0 2022-12-30
|
||||||
- Added translation using Weblate (Yue (yue_HK)) (Raatty)
|
- Added translation using Weblate (Yue (yue_HK)) (Raatty)
|
||||||
- Translated using Weblate (Chinese (Simplified)) (Mike Evans)
|
- Translated using Weblate (Chinese (Simplified)) (Mike Evans)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM cirrusci/flutter as builder
|
FROM ghcr.io/cirruslabs/flutter as builder
|
||||||
RUN sudo apt update && sudo apt install curl -y
|
RUN sudo apt update && sudo apt install curl -y
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@ -8,6 +8,7 @@ linter:
|
|||||||
- prefer_final_locals
|
- prefer_final_locals
|
||||||
- prefer_final_in_for_each
|
- prefer_final_in_for_each
|
||||||
- sort_pub_dependencies
|
- sort_pub_dependencies
|
||||||
|
- require_trailing_commas
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
|
|||||||
@ -44,7 +44,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "chat.fluffy.fluffychat"
|
applicationId "chat.fluffy.fluffychat"
|
||||||
minSdkVersion 16
|
minSdkVersion 19
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@ -93,8 +93,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity"
|
<activity
|
||||||
android:exported="true">
|
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter android:label="flutter_web_auth">
|
<intent-filter android:label="flutter_web_auth">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.6.10'
|
ext.kotlin_version = '1.8.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -27,6 +27,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: update_fastlane" time="0.000202">
|
<testcase classname="fastlane.lanes" name="0: update_fastlane" time="1.455419">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: default_platform" time="7.9e-05">
|
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000127">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="2: google_play_track_version_codes" time="2.638619">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 44 KiB |
@ -1,30 +1,25 @@
|
|||||||
{
|
{
|
||||||
"@@last_modified": "2021-08-14 12:41:10.154280",
|
"@@last_modified": "2021-08-14 12:41:10.154280",
|
||||||
"about": "সম্পর্কে",
|
"about": "সম্পর্কে",
|
||||||
"@about": {
|
"@about": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"accept": "স্বীকার করি",
|
"accept": "স্বীকার করি",
|
||||||
"@accept": {
|
"@accept": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
|
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
|
||||||
"@acceptedTheInvitation": {
|
"@acceptedTheInvitation": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"username": {}
|
"username": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account": "অ্যাকাউন্ট",
|
||||||
|
"@account": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"account": "অ্যাকাউন্ট",
|
|
||||||
"@account": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"accountInformation": "অ্যাকাউন্ট তথ্য",
|
|
||||||
"@accountInformation": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
1
assets/l10n/intl_bo.arb
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
assets/l10n/intl_el.arb
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -1 +1 @@
|
|||||||
{}
|
{}
|
||||||
@ -1,155 +1,120 @@
|
|||||||
{
|
{
|
||||||
"@@last_modified": "2021-08-14 12:41:09.940318",
|
"@@last_modified": "2021-08-14 12:41:09.940318",
|
||||||
"copiedToClipboard": "Copiada para a área de transferência",
|
"copiedToClipboard": "Copiada para a área de transferência",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"login": "Iniciar sessão",
|
"login": "Iniciar sessão",
|
||||||
"@login": {
|
"@login": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"monday": "segunda-feira",
|
"about": "Sobre",
|
||||||
"@monday": {
|
"@about": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"saturday": "sábado",
|
"admin": "Admin",
|
||||||
"@saturday": {
|
"@admin": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"wednesday": "quarta-feira",
|
"areYouSure": "Tens a certeza?",
|
||||||
"@wednesday": {
|
"@areYouSure": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"about": "Sobre",
|
"notifications": "Notificações",
|
||||||
"@about": {
|
"@notifications": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"admin": "Admin",
|
"account": "Conta",
|
||||||
"@admin": {
|
"@account": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"areYouSure": "Tens a certeza?",
|
"cancel": "Cancelar",
|
||||||
"@areYouSure": {
|
"@cancel": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"notifications": "Notificações",
|
"delete": "Eliminar",
|
||||||
"@notifications": {
|
"@delete": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
},
|
},
|
||||||
"account": "Conta",
|
"dateAndTimeOfDay": "{date}, {timeOfDay}",
|
||||||
"@account": {
|
"@dateAndTimeOfDay": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {}
|
"placeholders": {
|
||||||
},
|
"date": {},
|
||||||
"cancel": "Cancelar",
|
"timeOfDay": {}
|
||||||
"@cancel": {
|
}
|
||||||
"type": "text",
|
},
|
||||||
"placeholders": {}
|
"dateWithYear": "{day}-{month}-{year}",
|
||||||
},
|
"@dateWithYear": {
|
||||||
"delete": "Eliminar",
|
"type": "text",
|
||||||
"@delete": {
|
"placeholders": {
|
||||||
"type": "text",
|
"year": {},
|
||||||
"placeholders": {}
|
"month": {},
|
||||||
},
|
"day": {}
|
||||||
"dateAndTimeOfDay": "{date}, {timeOfDay}",
|
}
|
||||||
"@dateAndTimeOfDay": {
|
},
|
||||||
"type": "text",
|
"help": "Ajuda",
|
||||||
"placeholders": {
|
"@help": {
|
||||||
"date": {},
|
"type": "text",
|
||||||
"timeOfDay": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"messages": "Mensagens",
|
||||||
|
"@messages": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"reason": "Razão",
|
||||||
|
"@reason": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"privacy": "Privacidade",
|
||||||
|
"@privacy": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"openCamera": "Abrir câmara",
|
||||||
|
"@openCamera": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"settings": "Configurações",
|
||||||
|
"@settings": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"logout": "Terminar sessão",
|
||||||
|
"@logout": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"search": "Pesquisar",
|
||||||
|
"@search": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"users": "Utilizadores",
|
||||||
|
"@users": {},
|
||||||
|
"close": "Fechar",
|
||||||
|
"@close": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"dateWithoutYear": "{day}-{month}",
|
||||||
|
"@dateWithoutYear": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {
|
||||||
|
"month": {},
|
||||||
|
"day": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"dateWithYear": "{day}-{month}-{year}",
|
|
||||||
"@dateWithYear": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {
|
|
||||||
"year": {},
|
|
||||||
"month": {},
|
|
||||||
"day": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"help": "Ajuda",
|
|
||||||
"@help": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"messages": "Mensagens",
|
|
||||||
"@messages": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"reason": "Razão",
|
|
||||||
"@reason": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"privacy": "Privacidade",
|
|
||||||
"@privacy": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"openCamera": "Abrir câmara",
|
|
||||||
"@openCamera": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"settings": "Configurações",
|
|
||||||
"@settings": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"tuesday": "terça-feira",
|
|
||||||
"@tuesday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"logout": "Terminar sessão",
|
|
||||||
"@logout": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"search": "Pesquisar",
|
|
||||||
"@search": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"sunday": "domingo",
|
|
||||||
"@sunday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"users": "Utilizadores",
|
|
||||||
"@users": {},
|
|
||||||
"close": "Fechar",
|
|
||||||
"@close": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"dateWithoutYear": "{day}-{month}",
|
|
||||||
"@dateWithoutYear": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {
|
|
||||||
"month": {},
|
|
||||||
"day": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"friday": "sexta-feira",
|
|
||||||
"@friday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"thursday": "quinta-feira",
|
|
||||||
"@thursday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,374 +0,0 @@
|
|||||||
{
|
|
||||||
"@@last_modified": "2021-08-14 12:41:09.895217",
|
|
||||||
"about": "පිළිබඳව",
|
|
||||||
"@about": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"accept": "පිළිගන්න",
|
|
||||||
"@accept": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"account": "ගිණුම",
|
|
||||||
"@account": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"accountInformation": "ගිණුමේ තොරතුරු",
|
|
||||||
"@accountInformation": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"addEmail": "වි-තැපෑල එකතු කරන්න",
|
|
||||||
"@addEmail": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"admin": "පරිපාලක",
|
|
||||||
"@admin": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"allChats": "සියලුම සංවාද",
|
|
||||||
"@allChats": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"alreadyHaveAnAccount": "දැනටමත් ගිණුමක් තිබේද?",
|
|
||||||
"@alreadyHaveAnAccount": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"anyoneCanJoin": "ඕනෑම කෙනෙකුට එක්විය හැකිය",
|
|
||||||
"@anyoneCanJoin": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"archive": "සංරක්ෂිතය",
|
|
||||||
"@archive": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"archivedRoom": "සංරක්ෂිත කාමරය",
|
|
||||||
"@archivedRoom": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"areGuestsAllowedToJoin": "ආගන්තුක පරිශීලකයින්ට එක්වීමට අවසර තිබේද",
|
|
||||||
"@areGuestsAllowedToJoin": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"areYouSure": "ඔබට විශ්වාසද?",
|
|
||||||
"@areYouSure": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"areYouSureYouWantToLogout": "ඔබට නික්මීමට අවශ්ය බව විශ්වාසද?",
|
|
||||||
"@areYouSureYouWantToLogout": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"audioPlayerPlay": "ධාවනය",
|
|
||||||
"@audioPlayerPlay": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"blockDevice": "උපාංගය අවහිර කරන්න",
|
|
||||||
"@blockDevice": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"cachedKeys": "යතුරු නිහිතගතයි",
|
|
||||||
"@cachedKeys": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"cancel": "අවලංගු කරන්න",
|
|
||||||
"@cancel": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"changeDeviceName": "උපාංගයේ නම වෙනස් කරන්න",
|
|
||||||
"@changeDeviceName": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"changePassword": "මුරපදය වෙනස් කරන්න",
|
|
||||||
"@changePassword": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chat": "සංවාදය",
|
|
||||||
"@chat": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chatBackup": "සංවාද උපස්ථය",
|
|
||||||
"@chatBackup": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chatDetails": "සංවාදයේ විස්තර",
|
|
||||||
"@chatDetails": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chats": "සංවාද",
|
|
||||||
"@chats": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chooseAStrongPassword": "ශක්තිමත් මුරපදයක් තෝරන්න",
|
|
||||||
"@chooseAStrongPassword": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"chooseAUsername": "පරිශීලක නාමයක් තෝරන්න",
|
|
||||||
"@chooseAUsername": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"clearArchive": "සංරක්ෂිතය හිස් කරන්න",
|
|
||||||
"@clearArchive": {},
|
|
||||||
"close": "වසන්න",
|
|
||||||
"@close": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"commandHint_join": "දී ඇති කාමරයට එක්වන්න",
|
|
||||||
"@commandHint_join": {
|
|
||||||
"type": "text",
|
|
||||||
"description": "Usage hint for the command /join"
|
|
||||||
},
|
|
||||||
"commandHint_leave": "මෙම කාමරය හැරයන්න",
|
|
||||||
"@commandHint_leave": {
|
|
||||||
"type": "text",
|
|
||||||
"description": "Usage hint for the command /leave"
|
|
||||||
},
|
|
||||||
"commandInvalid": "විධානය වලංගු නොවේ",
|
|
||||||
"@commandInvalid": {
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
"commandMissing": "{{command} විධානයක් නොවේ.",
|
|
||||||
"@commandMissing": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {
|
|
||||||
"command": {}
|
|
||||||
},
|
|
||||||
"description": "State that {command} is not a valid /command."
|
|
||||||
},
|
|
||||||
"compareEmojiMatch": "සසඳා බලා පහත දැක්වෙන ඉමොජි අනෙක් උපාංගයට නිසැකවම ගැලපෙන බවට වග බලා ගන්න:",
|
|
||||||
"@compareEmojiMatch": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"compareNumbersMatch": "සංසන්දනය කර පහත දැක්වෙන අංක අනෙක් උපාංගට නිසැකව ගැලපෙන බවට වග බලා ගන්න:",
|
|
||||||
"@compareNumbersMatch": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"confirm": "තහවුරු කරන්න",
|
|
||||||
"@confirm": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"connect": "සබඳින්න",
|
|
||||||
"@connect": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"connectionAttemptFailed": "සබැඳීමේ උත්සාහය අසාර්ථකයි",
|
|
||||||
"@connectionAttemptFailed": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"contactHasBeenInvitedToTheGroup": "සමූහය වෙත සබඳතාවයකට ආරාධනා කර ඇත",
|
|
||||||
"@contactHasBeenInvitedToTheGroup": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"copy": "පිටපත්",
|
|
||||||
"@copy": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"create": "සාදන්න",
|
|
||||||
"@create": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"createAccountNow": "දැන් ගිණුමක් සාදන්න",
|
|
||||||
"@createAccountNow": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"createNewGroup": "නව සමූහයක් සාදන්න",
|
|
||||||
"@createNewGroup": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"donate": "පරිත්යාග",
|
|
||||||
"@donate": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"encryption": "සංකේතාංකනය",
|
|
||||||
"@encryption": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"everythingReady": "සියල්ල සූදානම්!",
|
|
||||||
"@everythingReady": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"fontSize": "මුද්රණඅකුරේ ප්රමාණය",
|
|
||||||
"@fontSize": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"goToTheNewRoom": "නව කාමරයට යන්න",
|
|
||||||
"@goToTheNewRoom": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"joinRoom": "කාමරයට එක්වන්න",
|
|
||||||
"@joinRoom": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"keysCached": "යතුරු නිහිතගත යි",
|
|
||||||
"@keysCached": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"next": "ඊලඟ",
|
|
||||||
"@next": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"noPublicRoomsFound": "ප්රසිද්ධ කාමර හමු නොවිණි…",
|
|
||||||
"@noPublicRoomsFound": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"people": "මිනිසුන්",
|
|
||||||
"@people": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"publicGroups": "ප්රසිද්ධ සමූහ",
|
|
||||||
"@publicGroups": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"removeDevice": "උපාංගය ඉවත්කරන්න",
|
|
||||||
"@removeDevice": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"roomVersion": "කාමරයේ අනුවාදය",
|
|
||||||
"@roomVersion": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"savedFileAs": "ලෙස ගොනුව සුරකින්න {filename}",
|
|
||||||
"@savedFileAs": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {
|
|
||||||
"filename": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"saveFile": "ගොනුව සුරකින්න",
|
|
||||||
"@saveFile": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"saveFileToFolder": "ගොනුව මෙම බහාලුමට සුරකින්න",
|
|
||||||
"@saveFileToFolder": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"securityKey": "ආරක්ෂක යතුර",
|
|
||||||
"@securityKey": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"securityKeyLost": "ආරක්ෂක යතුර නැතිවුනාද?",
|
|
||||||
"@securityKeyLost": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"send": "යවන්න",
|
|
||||||
"@send": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"showPassword": "මුරපදය පෙන්වන්න",
|
|
||||||
"@showPassword": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"sunday": "ඉරිදා",
|
|
||||||
"@sunday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"username": "පරිශීලක නාමය",
|
|
||||||
"@username": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"videoCall": "දෘශ්ය ඇමතුම",
|
|
||||||
"@videoCall": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"wallpaper": "බිතුපත",
|
|
||||||
"@wallpaper": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"warning": "අවවාදයයි!",
|
|
||||||
"@warning": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"wednesday": "බදාදා",
|
|
||||||
"@wednesday": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"writeAMessage": "පණිවිඩයක් ලියන්න…",
|
|
||||||
"@writeAMessage": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"yes": "ඔව්",
|
|
||||||
"@yes": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"you": "ඔබ",
|
|
||||||
"@you": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"yourOwnUsername": "ඔබට හිමි පරිශීලකනාමය",
|
|
||||||
"@yourOwnUsername": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"zoomIn": "විශාලනය",
|
|
||||||
"@zoomIn": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"zoomOut": "කුඩාලනය",
|
|
||||||
"@zoomOut": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"@@last_modified": "2021-08-14 12:41:09.826673",
|
"@@last_modified": "2021-08-14 12:41:09.826673",
|
||||||
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
|
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
|
||||||
"@acceptedTheInvitation": {
|
"@acceptedTheInvitation": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"username": {}
|
"username": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accept": "ஏற்றுக்கொள்",
|
||||||
|
"@accept": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"about": "பற்றி",
|
||||||
|
"@about": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"accept": "ஏற்றுக்கொள்",
|
|
||||||
"@accept": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
},
|
|
||||||
"about": "பற்றி",
|
|
||||||
"@about": {
|
|
||||||
"type": "text",
|
|
||||||
"placeholders": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
assets/l10n/intl_th.arb
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
@ -7,11 +7,9 @@ are also published on it.
|
|||||||
|
|
||||||
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click it**.
|
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click it**.
|
||||||
|
|
||||||
{::nomarkdown}
|
<a href="fdroidrepos://fluffychat.im/repo/stable/repo/?fingerprint=5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07" >
|
||||||
<a href="fdroidrepos://fluffychat.im/repo/stable/repo/?fingerprint=8E2637AEF6697CC6DD486AF044A6EE45B1A742AE3EF56566E748CDE8BC65C1FB" >
|
|
||||||
<img src="qr-stable.svg" width="300" height="300"/>
|
<img src="qr-stable.svg" width="300" height="300"/>
|
||||||
</a>
|
</a>
|
||||||
{:/}
|
|
||||||
|
|
||||||
|
|
||||||
### If the QR-Code doesn't work:
|
### If the QR-Code doesn't work:
|
||||||
@ -24,11 +22,11 @@ If this still isn't working follow the next steps:
|
|||||||
2. Go to the `Settings` Tab in the Bottom bar
|
2. Go to the `Settings` Tab in the Bottom bar
|
||||||
3. Click the `Repositories` Action
|
3. Click the `Repositories` Action
|
||||||
4. Click on the plus sign at the top.
|
4. Click on the plus sign at the top.
|
||||||
5. Fill in `https://fluffychat.im/repo/stable/repo/` into the top field and `8E2637AEF6697CC6DD486AF044A6EE45B1A742AE3EF56566E748CDE8BC65C1FB` in the bottom field.
|
5. Fill in `https://fluffychat.im/repo/stable/repo/` into the top field and `5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07` in the bottom field.
|
||||||
|
|
||||||
## What is the fingerprint?
|
## What is the fingerprint?
|
||||||
|
|
||||||
The fingerprint of the Repository is: `8E2637AEF6697CC6DD486AF044A6EE45B1A742AE3EF56566E748CDE8BC65C1FB`
|
The fingerprint of the Repository is: `5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07`
|
||||||
|
|
||||||
# Nightly Repository
|
# Nightly Repository
|
||||||
|
|
||||||
@ -36,11 +34,9 @@ The fingerprint of the Repository is: `8E2637AEF6697CC6DD486AF044A6EE45B1A742AE3
|
|||||||
|
|
||||||
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click** it.
|
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click** it.
|
||||||
|
|
||||||
{::nomarkdown}
|
|
||||||
<a href="fdroidrepos://fluffychat.im/repo/nightly/repo/?fingerprint=21A469657300576478B623DF99D8EB889A80BCD939ACA60A4074741BEAEC397D" >
|
<a href="fdroidrepos://fluffychat.im/repo/nightly/repo/?fingerprint=21A469657300576478B623DF99D8EB889A80BCD939ACA60A4074741BEAEC397D" >
|
||||||
<img src="qr-nightly.svg" width="300" height="300"/>
|
<img src="qr-nightly.svg" width="300" height="300"/>
|
||||||
</a>
|
</a>
|
||||||
{:/}
|
|
||||||
|
|
||||||
|
|
||||||
### If the QR-Code doesn't work:
|
### If the QR-Code doesn't work:
|
||||||
|
|||||||
638
docs/index.html
@ -1,557 +1,119 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>FluffyChat Official Website</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta name="identifier-url" content="https://fluffychat.im" />
|
||||||
<title>FluffyChat - Official Website</title>
|
<meta name="title" content="FluffyChat Official Website" />
|
||||||
<meta name="description" content="A cute and secure chatclient for the matrix protocol">
|
<meta name="description" content="The cutest messenger in the Matrix network" />
|
||||||
<meta name="keywords"
|
<meta name="abstract" content="FluffyChat is the cutest messenger in the Matrix network" />
|
||||||
content="Fluffychat, Matrix, Web, Android, iOS, Desktop, Chat, Client, Chatclient, Matrix.org, Secure, E2EE, End to End, Encryption, End to End Encryption, F-Droid, Foss, FOSS, OpenSource, Free, Community, Open">
|
<meta name="keywords" content="FluffyChat, Matrix, Flutter, App" />
|
||||||
<script type="application/ld+json">
|
<meta name="author" content="Krille Fear" />
|
||||||
{
|
<meta name="revisit-after" content="15" />
|
||||||
"@context": "https://schema.org",
|
<meta name="language" content="EN" />
|
||||||
"@type": "MobileApplication",
|
<meta name="robots" content="All" />
|
||||||
"name": "Fluffychat",
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
"applicationCategory": "CommunicationApplication",
|
<link rel="icon" type="image/x-icon" href="favicon.png">
|
||||||
"countriesNotSupported": "fr",
|
<link href="tailwind.css" rel="stylesheet">
|
||||||
"operatingSystem": "ANDROID",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.5",
|
|
||||||
"ratingCount": "133"
|
|
||||||
},
|
|
||||||
"installUrl": "https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "MobileApplication",
|
|
||||||
"name": "Fluffychat",
|
|
||||||
"applicationCategory": "CommunicationApplication",
|
|
||||||
"countriesNotSupported": "fr",
|
|
||||||
"operatingSystem": "ANDROID",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.5",
|
|
||||||
"ratingCount": "133"
|
|
||||||
},
|
|
||||||
"installUrl": "https://f-droid.org/de/packages/chat.fluffy.fluffychat/"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "MobileApplication",
|
|
||||||
"name": "Fluffychat",
|
|
||||||
"applicationCategory": "CommunicationApplication",
|
|
||||||
"countriesNotSupported": "fr",
|
|
||||||
"operatingSystem": "IOS",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.4",
|
|
||||||
"ratingCount": "28"
|
|
||||||
},
|
|
||||||
"installUrl": "https://apps.apple.com/app/fluffychat/id1551469600"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "WebApplication",
|
|
||||||
"name": "Fluffychat",
|
|
||||||
"applicationCategory": "CommunicationApplication",
|
|
||||||
"countriesNotSupported": "fr",
|
|
||||||
"operatingSystem": "WEB",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.5",
|
|
||||||
"ratingCount": "133"
|
|
||||||
},
|
|
||||||
"url": "https://fluffychat.im/web",
|
|
||||||
"downloadUrl": "https://fluffychat.im/web",
|
|
||||||
"installUrl": "https://fluffychat.im/web"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
"name": "Fluffychat",
|
|
||||||
"applicationCategory": "CommunicationApplication",
|
|
||||||
"countriesNotSupported": "fr",
|
|
||||||
"operatingSystem": "LINUX",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.5",
|
|
||||||
"ratingCount": "133"
|
|
||||||
},
|
|
||||||
"downloadUrl": "https://snapcraft.io/fluffychat",
|
|
||||||
"installUrl": "https://snapcraft.io/fluffychat"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
"name": "Fluffychat",
|
|
||||||
"applicationCategory": "CommunicationApplication",
|
|
||||||
"countriesNotSupported": "fr",
|
|
||||||
"operatingSystem": "LINUX",
|
|
||||||
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
|
|
||||||
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
|
|
||||||
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"callSign": "KrilleFear"
|
|
||||||
},
|
|
||||||
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
|
|
||||||
"offers": {
|
|
||||||
"@type": "Offer",
|
|
||||||
"price": "0",
|
|
||||||
"priceCurrency": "USD"
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "4.5",
|
|
||||||
"ratingCount": "133"
|
|
||||||
},
|
|
||||||
"downloadUrl": "https://flathub.org/apps/details/im.fluffychat.Fluffychat",
|
|
||||||
"installUrl": "https://flathub.org/apps/details/im.fluffychat.Fluffychat"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "WebSite",
|
|
||||||
"name": "FluffyChat - Official Website",
|
|
||||||
"url": "https://fluffychat.im",
|
|
||||||
"description": "A cute and secure chatclient for the matrix protocol",
|
|
||||||
"thumbnailUrl": "https://fluffychat.im/favicon.png",
|
|
||||||
"inLanguage": "de-de"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "BreadcrumbList",
|
|
||||||
"description": "Breadcrumbs list",
|
|
||||||
"name": "Breadcrumbs",
|
|
||||||
"itemListElement": [
|
|
||||||
{
|
|
||||||
"@type": "ListItem",
|
|
||||||
"item": {
|
|
||||||
"@id": "https://fluffychat.im",
|
|
||||||
"name": "Homepage"
|
|
||||||
},
|
|
||||||
"position": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<link rel="icon" type="image/png" href="favicon.png">
|
|
||||||
<link rel="stylesheet" href="tailwind.css">
|
|
||||||
<!-- Animation CSS-->
|
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Zen Kurenaido;
|
font-family: Zen Kurenaido;
|
||||||
src: url(ZenKurenaido-Regular.ttf);
|
src: url(ZenKurenaido-Regular.ttf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------
|
|
||||||
* Generated by Animista
|
|
||||||
* w: http://animista.net, t: @cssanimista
|
|
||||||
* ---------------------------------------------- */
|
|
||||||
|
|
||||||
.slide-in-bottom {
|
|
||||||
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) both;
|
|
||||||
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) both
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-in-bottom-h1 {
|
|
||||||
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .5s both;
|
|
||||||
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .5s both
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-in-bottom-subtitle {
|
|
||||||
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .75s both;
|
|
||||||
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .75s both
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
|
||||||
-webkit-animation: fade-in 1.2s cubic-bezier(.39, .575, .565, 1.000) 1s both;
|
|
||||||
animation: fade-in 1.2s cubic-bezier(.39, .575, .565, 1.000) 1s both
|
|
||||||
}
|
|
||||||
|
|
||||||
.bounce-top-icons {
|
|
||||||
-webkit-animation: bounce-top .9s 1s both;
|
|
||||||
animation: bounce-top .9s 1s both
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes slide-in-bottom {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: translateY(1000px);
|
|
||||||
transform: translateY(1000px);
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-in-bottom {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: translateY(1000px);
|
|
||||||
transform: translateY(1000px);
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes bounce-top {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: translateY(-45px);
|
|
||||||
transform: translateY(-45px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
24% {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
40% {
|
|
||||||
-webkit-transform: translateY(-24px);
|
|
||||||
transform: translateY(-24px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
65% {
|
|
||||||
-webkit-transform: translateY(-12px);
|
|
||||||
transform: translateY(-12px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
82% {
|
|
||||||
-webkit-transform: translateY(-6px);
|
|
||||||
transform: translateY(-6px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
93% {
|
|
||||||
-webkit-transform: translateY(-4px);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
25%,
|
|
||||||
55%,
|
|
||||||
75%,
|
|
||||||
87% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
animation-timing-function: ease-out
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-top {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: translateY(-45px);
|
|
||||||
transform: translateY(-45px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
24% {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
40% {
|
|
||||||
-webkit-transform: translateY(-24px);
|
|
||||||
transform: translateY(-24px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
65% {
|
|
||||||
-webkit-transform: translateY(-12px);
|
|
||||||
transform: translateY(-12px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
82% {
|
|
||||||
-webkit-transform: translateY(-6px);
|
|
||||||
transform: translateY(-6px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
93% {
|
|
||||||
-webkit-transform: translateY(-4px);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
-webkit-animation-timing-function: ease-in;
|
|
||||||
animation-timing-function: ease-in
|
|
||||||
}
|
|
||||||
|
|
||||||
25%,
|
|
||||||
55%,
|
|
||||||
75%,
|
|
||||||
87% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
animation-timing-function: ease-out
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
-webkit-animation-timing-function: ease-out;
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fade-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
<body
|
||||||
|
class="flex flex-col items-center justify-center min-h-screen w-screen bg-gradient-to-t from-purple-200 to-blue-50 dark:from-gray-800 dark:to-slate-900 p-4"
|
||||||
|
style="font-family: 'Zen Kurenaido', sans-serif;">
|
||||||
|
<img src="favicon.png" class="h-10" />
|
||||||
|
<h1 class="flex text-4xl items-center mb-4">
|
||||||
|
<span style="color: #5625BA">Fluffy</span>
|
||||||
|
<span style="color: #41a2bc">Chat</span>
|
||||||
|
</h1>
|
||||||
|
<img src="screenshots/screenshots.png" class="sm:max-w-lg max-w-screen mb-8" />
|
||||||
|
|
||||||
<body class="leading-normal tracking-normal text-gray-900" style="font-family: 'Zen Kurenaido', sans-serif;">
|
<div class="max-w-lg mb-8 flex justify-center flex-wrap">
|
||||||
|
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"
|
||||||
|
class="w-36 pr-2 mb-2 inline hover:scale-105 transition-transform"></a>
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"><img src="google-play-badge.png"
|
||||||
<div class="h-screen pb-14 bg-right bg-cover" style="background-image:url('bg.svg');">
|
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
|
||||||
<!--Nav-->
|
</a><a href="https://f-droid.org/packages/chat.fluffy.fluffychat/"><img src="fdroid_button.png"
|
||||||
<div class="w-full container mx-auto p-6">
|
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
|
||||||
|
</a>
|
||||||
<div class="w-full flex items-center justify-between">
|
<a href="https://fluffychat.im/web">
|
||||||
<a class="flex items-center no-underline hover:no-underline font-bold text-2xl lg:text-4xl" href="#">
|
<img src="browser-badge.png" class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||||
<img src="favicon.png" class="h-8 fill-current text-indigo-600 pr-2" /> <span
|
<a href="https://snapcraft.io/fluffychat"><img
|
||||||
style="color: #5625BA">Fluffy</span><span style="color: #41a2bc">Chat</span>
|
src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg"
|
||||||
</a>
|
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||||
|
<a href="https://flathub.org/apps/details/im.fluffychat.Fluffychat"><img src="flathub-badge-en.png"
|
||||||
<div class="flex w-1/2 justify-end content-center">
|
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||||
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
|
</div>
|
||||||
href="https://matrix.to/#/#fluffychat:matrix.org">
|
|
||||||
<svg class="fill-current h-6" enable-background="new -91 49.217 56.693 56.693" id="Layer_1"
|
|
||||||
version="1.1" viewBox="-91 49.217 56.693 56.693" xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<path
|
|
||||||
d="M-38.3289,79.8244c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.5351,1.5264l-3.0737-9.1321l4.4169-1.4866 c2.2362-0.7526,3.4388-3.1756,2.6861-5.4117c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.4168,1.4866l-1.4877-4.4201 c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861v0c-2.2362,0.7526-3.4388,3.1756-2.6861,5.4117l1.4877,4.4201l-9.3246,3.1385 l-1.4697-4.3666c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117l1.4697,4.3666 l-4.445,1.4961c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117v0c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861l4.445-1.4961 l3.0737,9.1321l-4.3268,1.4563c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861 l4.3268-1.4563l1.5778,4.6877c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5778-4.6877 l9.3246-3.1385l1.5598,4.6342c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5598-4.6342 l4.5351-1.5264C-38.7789,84.4835-37.5762,82.0606-38.3289,79.8244z M-65.6982,84.5288l-3.0737-9.1321l9.3246-3.1385l3.0737,9.1321 L-65.6982,84.5288z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
|
|
||||||
href="https://metalhead.club/@krille">
|
|
||||||
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xml:space="preserve"
|
|
||||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
|
||||||
<clipPath id="_clip1">
|
|
||||||
<rect x="33.6" y="-0.035" width="932.844" height="1000" />
|
|
||||||
</clipPath>
|
|
||||||
<g clip-path="url(#_clip1)">
|
|
||||||
<path
|
|
||||||
d="M946.586,599.455c-13.713,70.541 -122.816,147.742 -248.121,162.703c-65.341,7.796 -129.674,14.962 -198.275,11.815c-112.191,-5.139 -200.716,-26.776 -200.716,-26.776c0,10.92 0.673,21.319 2.02,31.044c14.586,110.711 109.787,117.344 199.967,120.436c91.021,3.114 172.068,-22.44 172.068,-22.44l3.74,82.281c0,0 -63.666,34.185 -177.079,40.473c-62.539,3.437 -140.192,-1.573 -230.636,-25.511c-196.158,-51.916 -229.893,-260.996 -235.055,-473.143c-1.573,-62.987 -0.603,-122.381 -0.603,-172.056c0,-216.931 142.142,-280.516 142.142,-280.516c71.672,-32.914 194.655,-46.755 322.508,-47.8l3.142,0c127.853,1.045 250.917,14.886 322.583,47.8c0,0 142.138,63.585 142.138,280.516c0,0 1.783,160.053 -19.823,271.174"
|
|
||||||
style="fill-rule:nonzero;" />
|
|
||||||
<path
|
|
||||||
d="M798.748,345.11l0,262.667l-104.07,0l0,-254.946c0,-53.743 -22.614,-81.021 -67.847,-81.021c-50.012,0 -75.077,32.359 -75.077,96.343l0,139.547l-103.457,0l0,-139.547c0,-63.984 -25.07,-96.343 -75.082,-96.343c-45.233,0 -67.847,27.278 -67.847,81.021l0,254.946l-104.07,0l0,-262.667c0,-53.683 13.669,-96.343 41.127,-127.904c28.314,-31.561 65.395,-47.741 111.425,-47.741c53.256,0 93.585,20.468 120.251,61.41l25.922,43.451l25.927,-43.451c26.66,-40.942 66.99,-61.41 120.251,-61.41c46.025,0 83.106,16.18 111.425,47.741c27.453,31.561 41.122,74.221 41.122,127.904"
|
|
||||||
style="fill:#fff;fill-rule:nonzero;" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
|
|
||||||
href="https://ko-fi.com/krille">
|
|
||||||
<img class="w-10 hover:animate-bounce" src="Kofi_pixel_logo.png"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--Main-->
|
|
||||||
<div class="container pt-8 px-6 mx-auto flex flex-wrap flex-col md:flex-row items-center">
|
|
||||||
|
|
||||||
<!--Left Col-->
|
|
||||||
<div class="flex flex-col w-full xl:w-2/5 justify-center lg:items-start overflow-y-hidden">
|
|
||||||
<h1
|
|
||||||
class="my-4 text-3xl md:text-5xl text-purple-800 font-bold leading-tight text-center md:text-left slide-in-bottom-h1">
|
|
||||||
Open. Nonprofit. Cute.</h1>
|
|
||||||
<p class="leading-normal text-base md:text-2xl mb-8 text-center md:text-left slide-in-bottom-subtitle">
|
|
||||||
Easy to use (<a class="underline hover:text-blue-700 transition-all"
|
|
||||||
href="https://matrix.org">matrix</a>) messenger. Secure and decentralized.</p>
|
|
||||||
|
|
||||||
<p class="text-blue-700 font-bold pb-4 text-center md:text-left fade-in">Mobile app:</p>
|
|
||||||
<div class="w-full flex justify-center md:justify-start pb-24 lg:pb-0 fade-in">
|
|
||||||
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"
|
|
||||||
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"><img
|
|
||||||
src="google-play-badge.png" class="max-h-12 pr-2 mb-2 bounce-top-icons inline">
|
|
||||||
</a><a href="https://f-droid.org/de/packages/chat.fluffy.fluffychat/"><img src="fdroid_button.png"
|
|
||||||
class="max-h-12 pr-2 mb-2 bounce-top-icons inline">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p class="text-blue-700 font-bold py-4 text-center md:text-left fade-in">Desktop app:</p>
|
|
||||||
<div class="w-full flex justify-center md:justify-start pb-24 lg:pb-0 fade-in">
|
|
||||||
<a href="https://fluffychat.im/web">
|
|
||||||
<img src="browser-badge.png" class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
|
|
||||||
<a href="https://snapcraft.io/fluffychat"><img
|
|
||||||
src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg"
|
|
||||||
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
|
|
||||||
<a href="https://flathub.org/apps/details/im.fluffychat.Fluffychat"><img src="flathub-badge-en.png"
|
|
||||||
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--Right Col-->
|
|
||||||
<div class="w-full xl:w-3/5 py-6 relative">
|
|
||||||
<img class="w-full mx-auto slide-in-bottom" src="screenshots/screenshots.png">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--Footer-->
|
|
||||||
<div class="w-full pt-16 pb-6 text-sm text-center md:text-left fade-in">
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://gitlab.com/famedly/fluffychat">Source code</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md">Privacy</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md">Changelog</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://gitlab.com/famedly/fluffychat/-/blob/main/docs/fdroid_repo.md">FluffyChat F-Droid repository</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://liberapay.com/KrilleChritzelius/donate">Donate</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800"
|
|
||||||
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
|
|
||||||
-
|
|
||||||
<a class="text-gray-500 no-underline hover:text-purple-800" href="https://krillefear.gitlab.io">Created
|
|
||||||
by Krille Fear</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="flex mb-8 justify-center content-center">
|
||||||
|
<a rel="me"
|
||||||
|
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
||||||
|
rel="me" href="https://mastodon.art/@krille">
|
||||||
|
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
||||||
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||||
|
<clipPath id="_clip1">
|
||||||
|
<rect x="33.6" y="-0.035" width="932.844" height="1000" />
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip1)">
|
||||||
|
<path
|
||||||
|
d="M946.586,599.455c-13.713,70.541 -122.816,147.742 -248.121,162.703c-65.341,7.796 -129.674,14.962 -198.275,11.815c-112.191,-5.139 -200.716,-26.776 -200.716,-26.776c0,10.92 0.673,21.319 2.02,31.044c14.586,110.711 109.787,117.344 199.967,120.436c91.021,3.114 172.068,-22.44 172.068,-22.44l3.74,82.281c0,0 -63.666,34.185 -177.079,40.473c-62.539,3.437 -140.192,-1.573 -230.636,-25.511c-196.158,-51.916 -229.893,-260.996 -235.055,-473.143c-1.573,-62.987 -0.603,-122.381 -0.603,-172.056c0,-216.931 142.142,-280.516 142.142,-280.516c71.672,-32.914 194.655,-46.755 322.508,-47.8l3.142,0c127.853,1.045 250.917,14.886 322.583,47.8c0,0 142.138,63.585 142.138,280.516c0,0 1.783,160.053 -19.823,271.174"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<path
|
||||||
|
d="M798.748,345.11l0,262.667l-104.07,0l0,-254.946c0,-53.743 -22.614,-81.021 -67.847,-81.021c-50.012,0 -75.077,32.359 -75.077,96.343l0,139.547l-103.457,0l0,-139.547c0,-63.984 -25.07,-96.343 -75.082,-96.343c-45.233,0 -67.847,27.278 -67.847,81.021l0,254.946l-104.07,0l0,-262.667c0,-53.683 13.669,-96.343 41.127,-127.904c28.314,-31.561 65.395,-47.741 111.425,-47.741c53.256,0 93.585,20.468 120.251,61.41l25.922,43.451l25.927,-43.451c26.66,-40.942 66.99,-61.41 120.251,-61.41c46.025,0 83.106,16.18 111.425,47.741c27.453,31.561 41.122,74.221 41.122,127.904"
|
||||||
|
style="fill:#fff;fill-rule:nonzero;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
||||||
|
href="https://matrix.to/#/#fluffychat:matrix.org">
|
||||||
|
<svg class="fill-current h-6" enable-background="new -91 49.217 56.693 56.693" id="Layer_1" version="1.1"
|
||||||
|
viewBox="-91 49.217 56.693 56.693" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path
|
||||||
|
d="M-38.3289,79.8244c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.5351,1.5264l-3.0737-9.1321l4.4169-1.4866 c2.2362-0.7526,3.4388-3.1756,2.6861-5.4117c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.4168,1.4866l-1.4877-4.4201 c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861v0c-2.2362,0.7526-3.4388,3.1756-2.6861,5.4117l1.4877,4.4201l-9.3246,3.1385 l-1.4697-4.3666c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117l1.4697,4.3666 l-4.445,1.4961c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117v0c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861l4.445-1.4961 l3.0737,9.1321l-4.3268,1.4563c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861 l4.3268-1.4563l1.5778,4.6877c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5778-4.6877 l9.3246-3.1385l1.5598,4.6342c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5598-4.6342 l4.5351-1.5264C-38.7789,84.4835-37.5762,82.0606-38.3289,79.8244z M-65.6982,84.5288l-3.0737-9.1321l9.3246-3.1385l3.0737,9.1321 L-65.6982,84.5288z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="inline-block no-underline hover:scale-105 transition-all text-center h-auto p-4"
|
||||||
|
href="https://ko-fi.com/krille">
|
||||||
|
<img src="kofi_button_dark.png" class="h-6 fill-current" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--Footer-->
|
||||||
|
<div class="w-full text-sm text-center max-w-lg">
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://gitlab.com/famedly/fluffychat">Source
|
||||||
|
code</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md">Privacy</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md">Changelog</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://gitlab.com/famedly/fluffychat/-/blob/main/docs/fdroid_repo.md">FluffyChat F-Droid
|
||||||
|
repository</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
|
||||||
|
-
|
||||||
|
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||||
|
href="https://krillefear.gitlab.io">Created
|
||||||
|
by Krille Fear</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
BIN
docs/kofi_button_dark.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 38 KiB |
BIN
fonts/Roboto/RobotoMono-Regular.ttf
Normal file
@ -1,49 +1,193 @@
|
|||||||
import 'dart:developer';
|
import 'package:fluffychat/config/setting_keys.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||||
|
import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/main.dart' as app;
|
import 'package:fluffychat/main.dart' as app;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'extensions/default_flows.dart';
|
||||||
|
import 'extensions/wait_for.dart';
|
||||||
import 'users.dart';
|
import 'users.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('Integration Test', () {
|
group(
|
||||||
testWidgets('Test if the app starts', (WidgetTester tester) async {
|
'Integration Test',
|
||||||
app.main();
|
() {
|
||||||
await tester.pumpAndSettle();
|
setUpAll(
|
||||||
|
() async {
|
||||||
|
// this random dialog popping up is super hard to cover in tests
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
SettingKeys.showNoGoogle: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
Hive.deleteFromDisk();
|
||||||
|
Hive.initFlutter();
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 10));
|
testWidgets(
|
||||||
|
'Start app, login and logout',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.ensureAppStartedHomescreen();
|
||||||
|
await tester.ensureLoggedOut();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
testWidgets(
|
||||||
|
'Login again',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.ensureAppStartedHomescreen();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(find.text('Connect'), findsOneWidget);
|
testWidgets(
|
||||||
|
'Start chat and send message',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.ensureAppStartedHomescreen();
|
||||||
|
await tester.waitFor(find.byType(TextField));
|
||||||
|
await tester.enterText(find.byType(TextField), Users.user2.name);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final input = find.byType(TextField);
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Chats').first,
|
||||||
|
500,
|
||||||
|
scrollable: find
|
||||||
|
.descendant(
|
||||||
|
of: find.byType(ChatListViewBody),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
)
|
||||||
|
.first,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Chats'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.waitFor(find.byType(SearchTitle));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(input, findsOneWidget);
|
await tester.scrollUntilVisible(
|
||||||
|
find.text(Users.user2.name).first,
|
||||||
|
500,
|
||||||
|
scrollable: find
|
||||||
|
.descendant(
|
||||||
|
of: find.byType(ChatListViewBody),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
)
|
||||||
|
.first,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text(Users.user2.name).first);
|
||||||
|
|
||||||
await tester.enterText(input, homeserver);
|
try {
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
await tester.waitFor(
|
||||||
await tester.pumpAndSettle();
|
find.byType(ChatView),
|
||||||
|
timeout: const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// in case the homeserver sends the username as search result
|
||||||
|
if (find.byIcon(Icons.send_outlined).evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(find.byIcon(Icons.send_outlined));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// in case registration is allowed
|
await tester.waitFor(find.byType(ChatView));
|
||||||
try {
|
await tester.enterText(find.byType(TextField).last, 'Test');
|
||||||
await tester.tap(find.text('Login'));
|
await tester.pumpAndSettle();
|
||||||
|
try {
|
||||||
|
await tester.waitFor(find.byIcon(Icons.send_outlined));
|
||||||
|
await tester.tap(find.byIcon(Icons.send_outlined));
|
||||||
|
} catch (_) {
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
}
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.waitFor(find.text('Test'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Spaces', (tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.ensureAppStartedHomescreen();
|
||||||
|
|
||||||
|
await tester.waitFor(find.byTooltip('Show menu'));
|
||||||
|
await tester.tap(find.byTooltip('Show menu'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
} catch (e) {
|
|
||||||
log('Registration is not allowed. Proceeding with login...');
|
|
||||||
}
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
final inputs = find.byType(TextField);
|
await tester.waitFor(find.byIcon(Icons.workspaces_outlined));
|
||||||
|
await tester.tap(find.byIcon(Icons.workspaces_outlined));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.enterText(inputs.first, Users.user1.name);
|
await tester.waitFor(find.byType(TextField));
|
||||||
await tester.enterText(inputs.last, Users.user1.password);
|
await tester.enterText(find.byType(TextField).last, 'Test Space');
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
await tester.pumpAndSettle();
|
||||||
});
|
|
||||||
});
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.waitFor(find.text('Invite contact'));
|
||||||
|
|
||||||
|
await tester.tap(find.text('Invite contact'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.waitFor(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(InvitationSelectionView),
|
||||||
|
matching: find.byType(TextField),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.enterText(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(InvitationSelectionView),
|
||||||
|
matching: find.byType(TextField),
|
||||||
|
),
|
||||||
|
Users.user2.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find
|
||||||
|
.descendant(
|
||||||
|
of: find.descendant(
|
||||||
|
of: find.byType(InvitationSelectionView),
|
||||||
|
matching: find.byType(ListTile),
|
||||||
|
),
|
||||||
|
matching: find.text(Users.user2.name),
|
||||||
|
)
|
||||||
|
.last,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.waitFor(find.maybeUppercaseText('Yes'));
|
||||||
|
await tester.tap(find.maybeUppercaseText('Yes'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.waitFor(find.text('Load 2 more participants'));
|
||||||
|
await tester.tap(find.text('Load 2 more participants'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text(Users.user2.name), findsOneWidget);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
171
integration_test/extensions/default_flows.dart
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||||
|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../users.dart';
|
||||||
|
import 'wait_for.dart';
|
||||||
|
|
||||||
|
extension DefaultFlowExtensions on WidgetTester {
|
||||||
|
Future<void> login() async {
|
||||||
|
final tester = this;
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.waitFor(find.text('Let\'s start'));
|
||||||
|
|
||||||
|
expect(find.text('Let\'s start'), findsOneWidget);
|
||||||
|
|
||||||
|
final input = find.byType(TextField);
|
||||||
|
|
||||||
|
expect(input, findsOneWidget);
|
||||||
|
|
||||||
|
// getting the placeholder in place
|
||||||
|
await tester.tap(find.byIcon(Icons.search));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(input, homeserver);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// in case registration is allowed
|
||||||
|
// try {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Login'),
|
||||||
|
500,
|
||||||
|
scrollable: find.descendant(
|
||||||
|
of: find.byKey(const Key('ConnectPageListView')),
|
||||||
|
matching: find.byType(Scrollable).first,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Login'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
/*} catch (e) {
|
||||||
|
log('Registration is not allowed. Proceeding with login...');
|
||||||
|
}*/
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
final inputs = find.byType(TextField);
|
||||||
|
|
||||||
|
await tester.enterText(inputs.first, Users.user1.name);
|
||||||
|
await tester.enterText(inputs.last, Users.user1.password);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// pumpAndSettle does not work in here as setState is called
|
||||||
|
// asynchronously
|
||||||
|
await tester.waitFor(
|
||||||
|
find.byType(LinearProgressIndicator),
|
||||||
|
timeout: const Duration(milliseconds: 1500),
|
||||||
|
skipPumpAndSettle: true,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// in case the input action does not work on the desired platform
|
||||||
|
if (find.text('Login').evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(find.text('Login'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
} catch (_) {
|
||||||
|
// may fail because of ongoing animation below dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.waitFor(
|
||||||
|
find.byType(ChatListViewBody),
|
||||||
|
skipPumpAndSettle: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ensure PushProvider check passes
|
||||||
|
Future<void> acceptPushWarning() async {
|
||||||
|
final tester = this;
|
||||||
|
|
||||||
|
final matcher = find.maybeUppercaseText('Do not show again');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tester.waitFor(matcher, timeout: const Duration(seconds: 5));
|
||||||
|
|
||||||
|
// the FCM push error dialog to be handled...
|
||||||
|
await tester.tap(matcher);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureLoggedOut() async {
|
||||||
|
final tester = this;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
if (find.byType(ChatListViewBody).evaluate().isNotEmpty) {
|
||||||
|
await tester.tap(find.byTooltip('Show menu'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Settings'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
find.text('Account'),
|
||||||
|
500,
|
||||||
|
scrollable: find.descendant(
|
||||||
|
of: find.byKey(const Key('SettingsListViewContent')),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Logout'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.maybeUppercaseText('Yes'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> ensureAppStartedHomescreen({
|
||||||
|
Duration timeout = const Duration(seconds: 20),
|
||||||
|
}) async {
|
||||||
|
final tester = this;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final homeserverPickerFinder = find.byType(HomeserverPicker);
|
||||||
|
final chatListFinder = find.byType(ChatListViewBody);
|
||||||
|
|
||||||
|
final end = DateTime.now().add(timeout);
|
||||||
|
|
||||||
|
log(
|
||||||
|
'Waiting for HomeserverPicker or ChatListViewBody...',
|
||||||
|
name: 'Test Runner',
|
||||||
|
);
|
||||||
|
do {
|
||||||
|
if (DateTime.now().isAfter(end)) {
|
||||||
|
throw Exception(
|
||||||
|
'Timed out waiting for HomeserverPicker or ChatListViewBody',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pumpAndSettle();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
} while (homeserverPickerFinder.evaluate().isEmpty &&
|
||||||
|
chatListFinder.evaluate().isEmpty);
|
||||||
|
|
||||||
|
if (homeserverPickerFinder.evaluate().isNotEmpty) {
|
||||||
|
log(
|
||||||
|
'Found HomeserverPicker, performing login.',
|
||||||
|
name: 'Test Runner',
|
||||||
|
);
|
||||||
|
await tester.login();
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
'Found ChatListViewBody, skipping login.',
|
||||||
|
name: 'Test Runner',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.acceptPushWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
integration_test/extensions/wait_for.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Workaround for https://github.com/flutter/flutter/issues/88765
|
||||||
|
extension WaitForExtension on WidgetTester {
|
||||||
|
Future<void> waitFor(
|
||||||
|
Finder finder, {
|
||||||
|
Duration timeout = const Duration(seconds: 20),
|
||||||
|
bool skipPumpAndSettle = false,
|
||||||
|
}) async {
|
||||||
|
final end = DateTime.now().add(timeout);
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (DateTime.now().isAfter(end)) {
|
||||||
|
throw Exception('Timed out waiting for $finder');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipPumpAndSettle) {
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
} while (finder.evaluate().isEmpty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MaybeUppercaseFinder on CommonFinders {
|
||||||
|
/// On Android some button labels are in uppercase while on iOS they
|
||||||
|
/// are not. This method tries both.
|
||||||
|
Finder maybeUppercaseText(
|
||||||
|
String text, {
|
||||||
|
bool findRichText = false,
|
||||||
|
bool skipOffstage = true,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
final finder = find.text(
|
||||||
|
text.toUpperCase(),
|
||||||
|
findRichText: findRichText,
|
||||||
|
skipOffstage: skipOffstage,
|
||||||
|
);
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
return finder;
|
||||||
|
} catch (_) {
|
||||||
|
return find.text(
|
||||||
|
text,
|
||||||
|
findRichText: findRichText,
|
||||||
|
skipOffstage: skipOffstage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,25 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
abstract class Users {
|
abstract class Users {
|
||||||
const Users._();
|
const Users._();
|
||||||
|
|
||||||
static final user1 = User(
|
static const user1 = User(
|
||||||
Platform.environment['USER1_NAME'] ?? 'alice',
|
String.fromEnvironment(
|
||||||
Platform.environment['USER1_PW'] ?? 'AliceInWonderland',
|
'USER1_NAME',
|
||||||
|
defaultValue: 'alice',
|
||||||
|
),
|
||||||
|
String.fromEnvironment(
|
||||||
|
'USER1_PW',
|
||||||
|
defaultValue: 'AliceInWonderland',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
static final user2 = User(
|
static const user2 = User(
|
||||||
Platform.environment['USER2_NAME'] ?? 'bob',
|
String.fromEnvironment(
|
||||||
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas',
|
'USER2_NAME',
|
||||||
|
defaultValue: 'bob',
|
||||||
|
),
|
||||||
|
String.fromEnvironment(
|
||||||
|
'USER2_PW',
|
||||||
|
defaultValue: 'JoWirSchaffenDas',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,5 +30,7 @@ class User {
|
|||||||
const User(this.name, this.password);
|
const User(this.name, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
final homeserver =
|
const homeserver = 'http://${const String.fromEnvironment(
|
||||||
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}';
|
'HOMESERVER',
|
||||||
|
defaultValue: 'localhost',
|
||||||
|
)}';
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 51;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@ -286,10 +286,12 @@
|
|||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
);
|
);
|
||||||
name = "Thin Binary";
|
name = "Thin Binary";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@ -322,6 +324,7 @@
|
|||||||
};
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
|
|||||||
@ -110,5 +110,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
app_identifier("im.fluffychat.app") # The bundle identifier of your app
|
app_identifier("im.fluffychat.app") # The bundle identifier of your app
|
||||||
apple_id("christian.pauly@wtal.de") # Your Apple email address
|
apple_id("christian-kussowski@posteo.de") # Your Apple email address
|
||||||
|
|
||||||
itc_team_id("122628977") # App Store Connect Team ID
|
itc_team_id("122628977") # App Store Connect Team ID
|
||||||
team_id("4NXF6Z997G") # Developer Portal Team ID
|
team_id("4NXF6Z997G") # Developer Portal Team ID
|
||||||
|
|||||||
@ -16,7 +16,7 @@ abstract class AppConfig {
|
|||||||
static const double messageFontSize = 15.75;
|
static const double messageFontSize = 15.75;
|
||||||
static const bool allowOtherHomeservers = true;
|
static const bool allowOtherHomeservers = true;
|
||||||
static const bool enableRegistration = true;
|
static const bool enableRegistration = true;
|
||||||
static const Color primaryColor = Color.fromARGB(255, 135, 103, 172);
|
static const Color primaryColor = Color(0xFF5625BA);
|
||||||
static const Color primaryColorLight = Color(0xFFCCBDEA);
|
static const Color primaryColorLight = Color(0xFFCCBDEA);
|
||||||
static const Color secondaryColor = Color(0xFF41a2bc);
|
static const Color secondaryColor = Color(0xFF41a2bc);
|
||||||
static String _privacyUrl =
|
static String _privacyUrl =
|
||||||
@ -33,6 +33,11 @@ abstract class AppConfig {
|
|||||||
static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat';
|
static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat';
|
||||||
static const String supportUrl =
|
static const String supportUrl =
|
||||||
'https://gitlab.com/famedly/fluffychat/issues';
|
'https://gitlab.com/famedly/fluffychat/issues';
|
||||||
|
static final Uri newIssueUrl = Uri(
|
||||||
|
scheme: 'https',
|
||||||
|
host: 'gitlab.com',
|
||||||
|
path: '/famedly/fluffychat/-/issues/new',
|
||||||
|
);
|
||||||
static const bool enableSentry = true;
|
static const bool enableSentry = true;
|
||||||
static const String sentryDns =
|
static const String sentryDns =
|
||||||
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';
|
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';
|
||||||
@ -70,8 +75,9 @@ abstract class AppConfig {
|
|||||||
colorSchemeSeed = Color(json['chat_color']);
|
colorSchemeSeed = Color(json['chat_color']);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logs().w(
|
Logs().w(
|
||||||
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
|
||||||
e);
|
e,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (json['application_name'] is String) {
|
if (json['application_name'] is String) {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
|||||||
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
||||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
|
||||||
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
||||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||||
@ -19,7 +18,6 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart';
|
|||||||
import 'package:fluffychat/pages/new_space/new_space.dart';
|
import 'package:fluffychat/pages/new_space/new_space.dart';
|
||||||
import 'package:fluffychat/pages/settings/settings.dart';
|
import 'package:fluffychat/pages/settings/settings.dart';
|
||||||
import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart';
|
import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart';
|
||||||
import 'package:fluffychat/pages/settings_account/settings_account.dart';
|
|
||||||
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
import 'package:fluffychat/pages/settings_chat/settings_chat.dart';
|
||||||
import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart';
|
import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart';
|
||||||
import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart';
|
import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart';
|
||||||
@ -28,7 +26,6 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
|
|||||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||||
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
|
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
|
||||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||||
import 'package:fluffychat/pages/sign_up/signup.dart';
|
|
||||||
import 'package:fluffychat/pages/story/story_page.dart';
|
import 'package:fluffychat/pages/story/story_page.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/loading_view.dart';
|
import 'package:fluffychat/widgets/layouts/loading_view.dart';
|
||||||
@ -71,21 +68,25 @@ class AppRoutes {
|
|||||||
widget: const ChatDetails(),
|
widget: const ChatDetails(),
|
||||||
stackedRoutes: _chatDetailsRoutes,
|
stackedRoutes: _chatDetailsRoutes,
|
||||||
),
|
),
|
||||||
VWidget(path: ':roomid', widget: const Chat(), stackedRoutes: [
|
VWidget(
|
||||||
VWidget(
|
path: ':roomid',
|
||||||
path: 'encryption',
|
widget: const ChatPage(),
|
||||||
widget: const ChatEncryptionSettings(),
|
stackedRoutes: [
|
||||||
),
|
VWidget(
|
||||||
VWidget(
|
path: 'encryption',
|
||||||
path: 'invite',
|
widget: const ChatEncryptionSettings(),
|
||||||
widget: const InvitationSelection(),
|
),
|
||||||
),
|
VWidget(
|
||||||
VWidget(
|
path: 'invite',
|
||||||
path: 'details',
|
widget: const InvitationSelection(),
|
||||||
widget: const ChatDetails(),
|
),
|
||||||
stackedRoutes: _chatDetailsRoutes,
|
VWidget(
|
||||||
),
|
path: 'details',
|
||||||
]),
|
widget: const ChatDetails(),
|
||||||
|
stackedRoutes: _chatDetailsRoutes,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
widget: const Settings(),
|
widget: const Settings(),
|
||||||
@ -94,6 +95,13 @@ class AppRoutes {
|
|||||||
VWidget(
|
VWidget(
|
||||||
path: '/archive',
|
path: '/archive',
|
||||||
widget: const Archive(),
|
widget: const Archive(),
|
||||||
|
stackedRoutes: [
|
||||||
|
VWidget(
|
||||||
|
path: ':roomid',
|
||||||
|
widget: const ChatPage(),
|
||||||
|
buildTransition: _dynamicTransition,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '/newprivatechat',
|
path: '/newprivatechat',
|
||||||
@ -164,14 +172,14 @@ class AppRoutes {
|
|||||||
VNester(
|
VNester(
|
||||||
path: ':roomid',
|
path: ':roomid',
|
||||||
widgetBuilder: (child) => SideViewLayout(
|
widgetBuilder: (child) => SideViewLayout(
|
||||||
mainView: const Chat(),
|
mainView: const ChatPage(),
|
||||||
sideView: child,
|
sideView: child,
|
||||||
),
|
),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
nestedRoutes: [
|
nestedRoutes: [
|
||||||
VWidget(
|
VWidget(
|
||||||
path: '',
|
path: '',
|
||||||
widget: const Chat(),
|
widget: const ChatPage(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
@ -220,13 +228,25 @@ class AppRoutes {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
VWidget(
|
VNester(
|
||||||
path: '/archive',
|
path: '/archive',
|
||||||
widget: const TwoColumnLayout(
|
widgetBuilder: (child) => TwoColumnLayout(
|
||||||
mainView: Archive(),
|
mainView: const Archive(),
|
||||||
sideView: EmptyPage(),
|
sideView: child,
|
||||||
),
|
),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
|
nestedRoutes: [
|
||||||
|
VWidget(
|
||||||
|
path: '',
|
||||||
|
widget: const EmptyPage(),
|
||||||
|
buildTransition: _dynamicTransition,
|
||||||
|
),
|
||||||
|
VWidget(
|
||||||
|
path: ':roomid',
|
||||||
|
widget: const ChatPage(),
|
||||||
|
buildTransition: _dynamicTransition,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -244,22 +264,6 @@ class AppRoutes {
|
|||||||
widget: const Login(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
),
|
),
|
||||||
VWidget(
|
|
||||||
path: 'connect',
|
|
||||||
widget: const ConnectPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
stackedRoutes: [
|
|
||||||
VWidget(
|
|
||||||
path: 'login',
|
|
||||||
widget: const Login(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
VWidget(
|
|
||||||
path: 'signup',
|
|
||||||
widget: const SignupPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
widget: const LogViewer(),
|
widget: const LogViewer(),
|
||||||
@ -326,37 +330,14 @@ class AppRoutes {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'account',
|
path: 'addaccount',
|
||||||
widget: const SettingsAccount(),
|
widget: const HomeserverPicker(),
|
||||||
buildTransition: _dynamicTransition,
|
buildTransition: _fadeTransition,
|
||||||
stackedRoutes: [
|
stackedRoutes: [
|
||||||
VWidget(
|
VWidget(
|
||||||
path: 'add',
|
path: 'login',
|
||||||
widget: const HomeserverPicker(),
|
widget: const Login(),
|
||||||
buildTransition: _fadeTransition,
|
buildTransition: _fadeTransition,
|
||||||
stackedRoutes: [
|
|
||||||
VWidget(
|
|
||||||
path: 'login',
|
|
||||||
widget: const Login(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
VWidget(
|
|
||||||
path: 'connect',
|
|
||||||
widget: const ConnectPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
stackedRoutes: [
|
|
||||||
VWidget(
|
|
||||||
path: 'login',
|
|
||||||
widget: const Login(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
VWidget(
|
|
||||||
path: 'signup',
|
|
||||||
widget: const SignupPage(),
|
|
||||||
buildTransition: _fadeTransition,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import 'app_config.dart';
|
|||||||
abstract class FluffyThemes {
|
abstract class FluffyThemes {
|
||||||
static const double columnWidth = 360.0;
|
static const double columnWidth = 360.0;
|
||||||
|
|
||||||
static bool isColumnModeByWidth(double width) => width > columnWidth * 2 + 64;
|
static const double navRailWidth = 64.0;
|
||||||
|
|
||||||
|
static bool isColumnModeByWidth(double width) =>
|
||||||
|
width > columnWidth * 2 + navRailWidth;
|
||||||
|
|
||||||
static bool isColumnMode(BuildContext context) =>
|
static bool isColumnMode(BuildContext context) =>
|
||||||
isColumnModeByWidth(MediaQuery.of(context).size.width);
|
isColumnModeByWidth(MediaQuery.of(context).size.width);
|
||||||
@ -23,28 +26,47 @@ abstract class FluffyThemes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static var fallbackTextTheme = const TextTheme(
|
static var fallbackTextTheme = const TextTheme(
|
||||||
bodyText1: fallbackTextStyle,
|
bodyLarge: fallbackTextStyle,
|
||||||
bodyText2: fallbackTextStyle,
|
bodyMedium: fallbackTextStyle,
|
||||||
button: fallbackTextStyle,
|
labelLarge: fallbackTextStyle,
|
||||||
caption: fallbackTextStyle,
|
bodySmall: fallbackTextStyle,
|
||||||
overline: fallbackTextStyle,
|
labelSmall: fallbackTextStyle,
|
||||||
headline1: fallbackTextStyle,
|
displayLarge: fallbackTextStyle,
|
||||||
headline2: fallbackTextStyle,
|
displayMedium: fallbackTextStyle,
|
||||||
headline3: fallbackTextStyle,
|
displaySmall: fallbackTextStyle,
|
||||||
headline4: fallbackTextStyle,
|
headlineMedium: fallbackTextStyle,
|
||||||
headline5: fallbackTextStyle,
|
headlineSmall: fallbackTextStyle,
|
||||||
headline6: fallbackTextStyle,
|
titleLarge: fallbackTextStyle,
|
||||||
subtitle1: fallbackTextStyle,
|
titleMedium: fallbackTextStyle,
|
||||||
subtitle2: fallbackTextStyle,
|
titleSmall: fallbackTextStyle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static LinearGradient backgroundGradient(
|
||||||
|
BuildContext context,
|
||||||
|
int alpha,
|
||||||
|
) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.secondaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.tertiaryContainer.withAlpha(alpha),
|
||||||
|
colorScheme.primaryContainer.withAlpha(alpha),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Duration animationDuration = Duration(milliseconds: 250);
|
||||||
|
static const Curve animationCurve = Curves.easeInOut;
|
||||||
|
|
||||||
static ThemeData buildTheme(Brightness brightness, [Color? seed]) =>
|
static ThemeData buildTheme(Brightness brightness, [Color? seed]) =>
|
||||||
ThemeData(
|
ThemeData(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
colorSchemeSeed: seed ?? AppConfig.colorSchemeSeed,
|
colorSchemeSeed: seed ?? AppConfig.colorSchemeSeed,
|
||||||
textTheme: PlatformInfos.isDesktop
|
textTheme: PlatformInfos.isDesktop || PlatformInfos.isWeb
|
||||||
? brightness == Brightness.light
|
? brightness == Brightness.light
|
||||||
? Typography.material2018().black.merge(fallbackTextTheme)
|
? Typography.material2018().black.merge(fallbackTextTheme)
|
||||||
: Typography.material2018().white.merge(fallbackTextTheme)
|
: Typography.material2018().white.merge(fallbackTextTheme)
|
||||||
@ -55,6 +77,11 @@ abstract class FluffyThemes {
|
|||||||
dividerColor: brightness == Brightness.light
|
dividerColor: brightness == Brightness.light
|
||||||
? Colors.blueGrey.shade50
|
? Colors.blueGrey.shade50
|
||||||
: Colors.blueGrey.shade900,
|
: Colors.blueGrey.shade900,
|
||||||
|
popupMenuTheme: PopupMenuThemeData(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
border: UnderlineInputBorder(
|
border: UnderlineInputBorder(
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
@ -86,10 +113,18 @@ abstract class FluffyThemes {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
dialogTheme: DialogTheme(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
textStyle: const TextStyle(fontSize: 16),
|
textStyle: const TextStyle(fontSize: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
@ -68,14 +69,15 @@ class AddStoryController extends State<AddStoryPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void importMedia() async {
|
void importMedia() async {
|
||||||
final picked = await FilePickerCross.importFromStorage(
|
final picked = await FilePicker.platform.pickFiles(
|
||||||
type: FileTypeCross.image,
|
type: FileType.image,
|
||||||
|
withData: true,
|
||||||
);
|
);
|
||||||
final fileName = picked.fileName;
|
final file = picked?.files.firstOrNull;
|
||||||
if (fileName == null) return;
|
if (file == null) return;
|
||||||
final matrixFile = MatrixImageFile(
|
final matrixFile = MatrixImageFile(
|
||||||
bytes: picked.toUint8List(),
|
bytes: file.bytes!,
|
||||||
name: fileName,
|
name: file.name,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
image = matrixFile;
|
image = matrixFile;
|
||||||
@ -88,14 +90,15 @@ class AddStoryController extends State<AddStoryPage> {
|
|||||||
);
|
);
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
final matrixFile = await showFutureLoadingDialog(
|
final matrixFile = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
final bytes = await picked.readAsBytes();
|
final bytes = await picked.readAsBytes();
|
||||||
return MatrixImageFile(
|
return MatrixImageFile(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
name: picked.name,
|
name: picked.name,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
image = matrixFile.result;
|
image = matrixFile.result;
|
||||||
|
|||||||
@ -92,34 +92,39 @@ class InviteStoryPageState extends State<InviteStoryPage> {
|
|||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FutureBuilder<List<User>>(
|
child: FutureBuilder<List<User>>(
|
||||||
future: loadContacts,
|
future: loadContacts,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final contacts = snapshot.data;
|
final contacts = snapshot.data;
|
||||||
if (contacts == null) {
|
if (contacts == null) {
|
||||||
final error = snapshot.error;
|
final error = snapshot.error;
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(error.toLocalizedString(context)));
|
child: Text(error.toLocalizedString(context)),
|
||||||
}
|
);
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive());
|
|
||||||
}
|
}
|
||||||
_undecided = contacts.map((u) => u.id).toSet();
|
return const Center(
|
||||||
return ListView.builder(
|
child: CircularProgressIndicator.adaptive(),
|
||||||
itemCount: contacts.length,
|
|
||||||
itemBuilder: (context, i) => SwitchListTile.adaptive(
|
|
||||||
value: _invite.contains(contacts[i].id),
|
|
||||||
onChanged: (b) => setState(() => b
|
|
||||||
? _invite.add(contacts[i].id)
|
|
||||||
: _invite.remove(contacts[i].id)),
|
|
||||||
secondary: Avatar(
|
|
||||||
mxContent: contacts[i].avatarUrl,
|
|
||||||
name: contacts[i].calcDisplayname(),
|
|
||||||
),
|
|
||||||
title: Text(contacts[i].calcDisplayname()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}
|
||||||
|
_undecided = contacts.map((u) => u.id).toSet();
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: contacts.length,
|
||||||
|
itemBuilder: (context, i) => SwitchListTile.adaptive(
|
||||||
|
value: _invite.contains(contacts[i].id),
|
||||||
|
onChanged: (b) => setState(
|
||||||
|
() => b
|
||||||
|
? _invite.add(contacts[i].id)
|
||||||
|
: _invite.remove(contacts[i].id),
|
||||||
|
),
|
||||||
|
secondary: Avatar(
|
||||||
|
mxContent: contacts[i].avatarUrl,
|
||||||
|
name: contacts[i].calcDisplayname(),
|
||||||
|
),
|
||||||
|
title: Text(contacts[i].calcDisplayname()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -21,11 +21,9 @@ class ArchiveController extends State<Archive> {
|
|||||||
Future<List<Room>> getArchive(BuildContext context) async {
|
Future<List<Room>> getArchive(BuildContext context) async {
|
||||||
final archive = this.archive;
|
final archive = this.archive;
|
||||||
if (archive != null) return archive;
|
if (archive != null) return archive;
|
||||||
return await Matrix.of(context).client.loadArchive();
|
return this.archive = await Matrix.of(context).client.loadArchive();
|
||||||
}
|
}
|
||||||
|
|
||||||
void forgetAction(int i) => setState(() => archive?.removeAt(i));
|
|
||||||
|
|
||||||
void forgetAllAction() async {
|
void forgetAllAction() async {
|
||||||
final archive = this.archive;
|
final archive = this.archive;
|
||||||
if (archive == null) return;
|
if (archive == null) return;
|
||||||
@ -44,7 +42,7 @@ class ArchiveController extends State<Archive> {
|
|||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
while (archive.isNotEmpty) {
|
while (archive.isNotEmpty) {
|
||||||
Logs().v('Forget room ${archive.last.displayname}');
|
Logs().v('Forget room ${archive.last.getLocalizedDisplayname()}');
|
||||||
await archive.last.forget();
|
await archive.last.forget();
|
||||||
archive.removeLast();
|
archive.removeLast();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,14 @@ class ArchiveView extends StatelessWidget {
|
|||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: Text(L10n.of(context)!.archive),
|
title: Text(L10n.of(context)!.archive),
|
||||||
actions: [
|
actions: [
|
||||||
if (snapshot.hasData && archive != null && archive!.isNotEmpty)
|
if (snapshot.data?.isNotEmpty ?? false)
|
||||||
TextButton(
|
Padding(
|
||||||
onPressed: controller.forgetAllAction,
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(L10n.of(context)!.clearArchive),
|
child: TextButton.icon(
|
||||||
|
onPressed: controller.forgetAllAction,
|
||||||
|
label: Text(L10n.of(context)!.clearArchive),
|
||||||
|
icon: const Icon(Icons.cleaning_services_outlined),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -32,25 +36,27 @@ class ArchiveView extends StatelessWidget {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
L10n.of(context)!.oopsSomethingWentWrong,
|
L10n.of(context)!.oopsSomethingWentWrong,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
archive = snapshot.data;
|
archive = snapshot.data;
|
||||||
if (archive == null || archive!.isEmpty) {
|
if (archive == null || archive!.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Icon(Icons.archive_outlined, size: 80));
|
child: Icon(Icons.archive_outlined, size: 80),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: archive!.length,
|
itemCount: archive!.length,
|
||||||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||||
archive![i],
|
archive![i],
|
||||||
onForget: controller.forgetAction,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,9 +125,12 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
trailing: Icon(
|
trailing: CircleAvatar(
|
||||||
Icons.info_outlined,
|
backgroundColor: Colors.transparent,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
child: Icon(
|
||||||
|
Icons.info_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(L10n.of(context)!.chatBackupDescription),
|
subtitle: Text(L10n.of(context)!.chatBackupDescription),
|
||||||
),
|
),
|
||||||
@ -136,11 +139,15 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
minLines: 4,
|
minLines: 2,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||||
controller: TextEditingController(text: key),
|
controller: TextEditingController(text: key),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.all(16),
|
||||||
|
suffixIcon: Icon(Icons.key_outlined),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (_supportsSecureStorage)
|
if (_supportsSecureStorage)
|
||||||
@ -234,12 +241,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: Navigator.of(context).pop,
|
onPressed: Navigator.of(context).pop,
|
||||||
),
|
),
|
||||||
title: Text(L10n.of(context)!.unlockOldMessages),
|
title: Text(L10n.of(context)!.chatBackup),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: FluffyThemes.columnWidth * 1.5),
|
maxWidth: FluffyThemes.columnWidth * 1.5,
|
||||||
|
),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
children: [
|
||||||
@ -251,11 +259,12 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
L10n.of(context)!.pleaseEnterRecoveryKeyDescription),
|
L10n.of(context)!.pleaseEnterRecoveryKeyDescription,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
TextField(
|
TextField(
|
||||||
minLines: 2,
|
minLines: 1,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
readOnly: _recoveryKeyInputLoading,
|
readOnly: _recoveryKeyInputLoading,
|
||||||
@ -263,68 +272,72 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
? null
|
? null
|
||||||
: [AutofillHints.password],
|
: [AutofillHints.password],
|
||||||
controller: _recoveryKeyTextEditingController,
|
controller: _recoveryKeyTextEditingController,
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Abc123 Def456',
|
contentPadding: const EdgeInsets.all(16),
|
||||||
labelStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontFamily: Theme.of(context)
|
fontFamily:
|
||||||
.textTheme
|
Theme.of(context).textTheme.bodyLarge?.fontFamily,
|
||||||
.bodyText1
|
),
|
||||||
?.fontFamily),
|
hintText: L10n.of(context)!.recoveryKey,
|
||||||
labelText: L10n.of(context)!.recoveryKey,
|
|
||||||
errorText: _recoveryKeyInputError,
|
errorText: _recoveryKeyInputError,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor:
|
foregroundColor:
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
),
|
|
||||||
icon: _recoveryKeyInputLoading
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: const Icon(Icons.lock_open_outlined),
|
|
||||||
label: Text(L10n.of(context)!.unlockOldMessages),
|
|
||||||
onPressed: _recoveryKeyInputLoading
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
setState(() {
|
|
||||||
_recoveryKeyInputError = null;
|
|
||||||
_recoveryKeyInputLoading = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
final key =
|
|
||||||
_recoveryKeyTextEditingController.text;
|
|
||||||
await bootstrap.newSsssKey!.unlock(
|
|
||||||
keyOrPassphrase: key,
|
|
||||||
);
|
|
||||||
Logs().d('SSSS unlocked');
|
|
||||||
await bootstrap
|
|
||||||
.client.encryption!.crossSigning
|
|
||||||
.selfSign(
|
|
||||||
keyOrPassphrase: key,
|
|
||||||
);
|
|
||||||
Logs().d('Successful elfsigned');
|
|
||||||
await bootstrap.openExistingSsss();
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().w('Unable to unlock SSSS', e, s);
|
|
||||||
setState(() => _recoveryKeyInputError =
|
|
||||||
L10n.of(context)!.oopsSomethingWentWrong);
|
|
||||||
} finally {
|
|
||||||
setState(
|
|
||||||
() => _recoveryKeyInputLoading = false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Text(L10n.of(context)!.or),
|
|
||||||
),
|
),
|
||||||
const Expanded(child: Divider()),
|
icon: _recoveryKeyInputLoading
|
||||||
]),
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.lock_open_outlined),
|
||||||
|
label: Text(L10n.of(context)!.unlockOldMessages),
|
||||||
|
onPressed: _recoveryKeyInputLoading
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
setState(() {
|
||||||
|
_recoveryKeyInputError = null;
|
||||||
|
_recoveryKeyInputLoading = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final key =
|
||||||
|
_recoveryKeyTextEditingController.text;
|
||||||
|
await bootstrap.newSsssKey!.unlock(
|
||||||
|
keyOrPassphrase: key,
|
||||||
|
);
|
||||||
|
Logs().d('SSSS unlocked');
|
||||||
|
await bootstrap.client.encryption!.crossSigning
|
||||||
|
.selfSign(
|
||||||
|
keyOrPassphrase: key,
|
||||||
|
);
|
||||||
|
Logs().d('Successful elfsigned');
|
||||||
|
await bootstrap.openExistingSsss();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().w('Unable to unlock SSSS', e, s);
|
||||||
|
setState(
|
||||||
|
() => _recoveryKeyInputError =
|
||||||
|
L10n.of(context)!.oopsSomethingWentWrong,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(
|
||||||
|
() => _recoveryKeyInputLoading = false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Text(L10n.of(context)!.or),
|
||||||
|
),
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.cast_connected_outlined),
|
icon: const Icon(Icons.cast_connected_outlined),
|
||||||
@ -401,11 +414,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
case BootstrapState.error:
|
case BootstrapState.error:
|
||||||
titleText = L10n.of(context)!.oopsSomethingWentWrong;
|
titleText = L10n.of(context)!.oopsSomethingWentWrong;
|
||||||
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
|
body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
|
||||||
buttons.add(AdaptiveFlatButton(
|
buttons.add(
|
||||||
label: L10n.of(context)!.close,
|
AdaptiveFlatButton(
|
||||||
onPressed: () =>
|
label: L10n.of(context)!.close,
|
||||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
onPressed: () =>
|
||||||
));
|
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case BootstrapState.done:
|
case BootstrapState.done:
|
||||||
titleText = L10n.of(context)!.everythingReady;
|
titleText = L10n.of(context)!.everythingReady;
|
||||||
@ -416,11 +431,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||||||
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
|
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
buttons.add(AdaptiveFlatButton(
|
buttons.add(
|
||||||
label: L10n.of(context)!.close,
|
AdaptiveFlatButton(
|
||||||
onPressed: () =>
|
label: L10n.of(context)!.close,
|
||||||
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
onPressed: () =>
|
||||||
));
|
Navigator.of(context, rootNavigator: false).pop<bool>(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,8 @@ class AddWidgetTileState extends State<AddWidgetTile> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)));
|
SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,15 @@ class AddWidgetTileView extends StatelessWidget {
|
|||||||
'm.jitsi': Text(L10n.of(context)!.widgetJitsi),
|
'm.jitsi': Text(L10n.of(context)!.widgetJitsi),
|
||||||
'm.video': Text(L10n.of(context)!.widgetVideo),
|
'm.video': Text(L10n.of(context)!.widgetVideo),
|
||||||
'm.custom': Text(L10n.of(context)!.widgetCustom),
|
'm.custom': Text(L10n.of(context)!.widgetCustom),
|
||||||
}.map((key, value) => MapEntry(
|
}.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
key,
|
key,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: value,
|
child: value,
|
||||||
))),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
onValueChanged: controller.setWidgetType,
|
onValueChanged: controller.setWidgetType,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
import 'package:file_picker_cross/file_picker_cross.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -22,6 +22,8 @@ import 'package:vrouter/vrouter.dart';
|
|||||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||||
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
|
import 'package:fluffychat/utils/error_reporter.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/ios_badge_client_extension.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/ios_badge_client_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
@ -34,25 +36,56 @@ import 'send_file_dialog.dart';
|
|||||||
import 'send_location_dialog.dart';
|
import 'send_location_dialog.dart';
|
||||||
import 'sticker_picker_dialog.dart';
|
import 'sticker_picker_dialog.dart';
|
||||||
|
|
||||||
class Chat extends StatefulWidget {
|
class ChatPage extends StatelessWidget {
|
||||||
final Widget? sideView;
|
final Widget? sideView;
|
||||||
|
|
||||||
const Chat({Key? key, this.sideView}) : super(key: key);
|
const ChatPage({Key? key, this.sideView}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final roomId = context.vRouter.pathParameters['roomid'];
|
||||||
|
final room =
|
||||||
|
roomId == null ? null : Matrix.of(context).client.getRoomById(roomId);
|
||||||
|
if (room == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child:
|
||||||
|
Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ChatPageWithRoom(sideView: sideView, room: room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatPageWithRoom extends StatefulWidget {
|
||||||
|
final Widget? sideView;
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
const ChatPageWithRoom({
|
||||||
|
Key? key,
|
||||||
|
required this.sideView,
|
||||||
|
required this.room,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ChatController createState() => ChatController();
|
ChatController createState() => ChatController();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatController extends State<Chat> {
|
class ChatController extends State<ChatPageWithRoom> {
|
||||||
Room? room;
|
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
|
||||||
|
|
||||||
Client? sendingClient;
|
late Client sendingClient;
|
||||||
|
|
||||||
Timeline? timeline;
|
Timeline? timeline;
|
||||||
|
|
||||||
MatrixState? matrix;
|
String? readMarkerEventId;
|
||||||
|
|
||||||
String? get roomId => context.vRouter.pathParameters['roomid'];
|
String get roomId => widget.room.id;
|
||||||
|
|
||||||
final AutoScrollController scrollController = AutoScrollController();
|
final AutoScrollController scrollController = AutoScrollController();
|
||||||
|
|
||||||
@ -81,10 +114,12 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
final matrixFiles = <MatrixFile>[];
|
final matrixFiles = <MatrixFile>[];
|
||||||
for (var i = 0; i < bytesList.result!.length; i++) {
|
for (var i = 0; i < bytesList.result!.length; i++) {
|
||||||
matrixFiles.add(MatrixFile(
|
matrixFiles.add(
|
||||||
bytes: bytesList.result![i],
|
MatrixFile(
|
||||||
name: details.files[i].name,
|
bytes: bytesList.result![i],
|
||||||
).detectFileType);
|
name: details.files[i].name,
|
||||||
|
).detectFileType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@ -92,7 +127,7 @@ class ChatController extends State<Chat> {
|
|||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendFileDialog(
|
builder: (c) => SendFileDialog(
|
||||||
files: matrixFiles,
|
files: matrixFiles,
|
||||||
room: room!,
|
room: room,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,7 +152,10 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
Event? editEvent;
|
Event? editEvent;
|
||||||
|
|
||||||
bool showScrollDownButton = false;
|
bool _scrolledUp = false;
|
||||||
|
|
||||||
|
bool get showScrollDownButton =>
|
||||||
|
_scrolledUp || timeline?.allowNewEvent == false;
|
||||||
|
|
||||||
bool get selectMode => selectedEvents.isNotEmpty;
|
bool get selectMode => selectedEvents.isNotEmpty;
|
||||||
|
|
||||||
@ -127,28 +165,78 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
String pendingText = '';
|
String pendingText = '';
|
||||||
|
|
||||||
bool get canLoadMore =>
|
|
||||||
timeline!.events.isEmpty ||
|
|
||||||
timeline!.events.last.type != EventTypes.RoomCreate;
|
|
||||||
|
|
||||||
bool showEmojiPicker = false;
|
bool showEmojiPicker = false;
|
||||||
|
|
||||||
|
void recreateChat() async {
|
||||||
|
final room = this.room;
|
||||||
|
final userId = room.directChatMatrixID;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception(
|
||||||
|
'Try to recreate a room with is not a DM room. This should not be possible from the UI!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final success = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: () async {
|
||||||
|
final client = room.client;
|
||||||
|
final waitForSync = client.onSync.stream
|
||||||
|
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
|
||||||
|
await room.leave();
|
||||||
|
await waitForSync;
|
||||||
|
return await client.startDirectChat(userId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final roomId = success.result;
|
||||||
|
if (roomId == null) return;
|
||||||
|
VRouter.of(context).toSegments(['rooms', roomId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leaveChat() async {
|
||||||
|
final success = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: room.leave,
|
||||||
|
);
|
||||||
|
if (success.error != null) return;
|
||||||
|
VRouter.of(context).to('/rooms');
|
||||||
|
}
|
||||||
|
|
||||||
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
|
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
|
||||||
|
|
||||||
void requestHistory() async {
|
void requestHistory() async {
|
||||||
if (canLoadMore) {
|
if (!timeline!.canRequestHistory) return;
|
||||||
try {
|
Logs().v('Requesting history...');
|
||||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
try {
|
||||||
} catch (err) {
|
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
} catch (err) {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(
|
SnackBar(
|
||||||
(err).toLocalizedString(context),
|
content: Text(
|
||||||
),
|
(err).toLocalizedString(context),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
rethrow;
|
);
|
||||||
}
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestFuture() async {
|
||||||
|
final timeline = this.timeline;
|
||||||
|
if (timeline == null) return;
|
||||||
|
if (!timeline.canRequestFuture) return;
|
||||||
|
Logs().v('Requesting future...');
|
||||||
|
try {
|
||||||
|
final mostRecentEventId = timeline.events.first.eventId;
|
||||||
|
await timeline.requestFuture(historyCount: _loadHistoryCount);
|
||||||
|
setReadMarker(eventId: mostRecentEventId);
|
||||||
|
} catch (err) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
(err).toLocalizedString(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,18 +246,11 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
setReadMarker();
|
setReadMarker();
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
if (scrollController.position.pixels ==
|
if (timeline?.allowNewEvent == false ||
|
||||||
scrollController.position.maxScrollExtent &&
|
scrollController.position.pixels > 0 && _scrolledUp == false) {
|
||||||
timeline!.events.isNotEmpty &&
|
setState(() => _scrolledUp = true);
|
||||||
timeline!.events[timeline!.events.length - 1].type !=
|
} else if (scrollController.position.pixels == 0 && _scrolledUp == true) {
|
||||||
EventTypes.RoomCreate) {
|
setState(() => _scrolledUp = false);
|
||||||
requestHistory();
|
|
||||||
}
|
|
||||||
if (scrollController.position.pixels > 0 && showScrollDownButton == false) {
|
|
||||||
setState(() => showScrollDownButton = true);
|
|
||||||
} else if (scrollController.position.pixels == 0 &&
|
|
||||||
showScrollDownButton == true) {
|
|
||||||
setState(() => showScrollDownButton = false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +269,12 @@ class ChatController extends State<Chat> {
|
|||||||
inputFocus.addListener(_inputFocusListener);
|
inputFocus.addListener(_inputFocusListener);
|
||||||
_loadDraft();
|
_loadDraft();
|
||||||
super.initState();
|
super.initState();
|
||||||
|
sendingClient = Matrix.of(context).client;
|
||||||
|
readMarkerEventId = room.fullyRead;
|
||||||
|
loadTimelineFuture =
|
||||||
|
_getTimeline(eventContextId: readMarkerEventId).onError(
|
||||||
|
ErrorReporter(context, 'Unable to load timeline').onErrorCallback,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateView() {
|
void updateView() {
|
||||||
@ -195,48 +282,84 @@ class ChatController extends State<Chat> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> getTimeline() async {
|
Future<void>? loadTimelineFuture;
|
||||||
if (timeline == null) {
|
|
||||||
await Matrix.of(context).client.roomsLoading;
|
|
||||||
await Matrix.of(context).client.accountDataLoading;
|
|
||||||
timeline = await room!.getTimeline(onUpdate: updateView);
|
|
||||||
if (timeline!.events.isNotEmpty) {
|
|
||||||
if (room!.markedUnread) room!.markUnread(false);
|
|
||||||
setReadMarker();
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the scroll controller is attached we want to scroll to an event id, if specified
|
Future<void> _getTimeline({
|
||||||
// and update the scroll controller...which will trigger a request history, if the
|
String? eventContextId,
|
||||||
// "load more" button is visible on the screen
|
Duration timeout = const Duration(seconds: 7),
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
}) async {
|
||||||
if (mounted) {
|
await Matrix.of(context).client.roomsLoading;
|
||||||
final event = VRouter.of(context).queryParameters['event'];
|
await Matrix.of(context).client.accountDataLoading;
|
||||||
if (event != null) {
|
if (eventContextId != null &&
|
||||||
scrollToEventId(event);
|
(!eventContextId.isValidMatrixId || eventContextId.sigil != '\$')) {
|
||||||
}
|
eventContextId = null;
|
||||||
_updateScrollController();
|
}
|
||||||
}
|
try {
|
||||||
});
|
timeline = await room
|
||||||
|
.getTimeline(
|
||||||
|
onUpdate: updateView,
|
||||||
|
eventContextId: eventContextId,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
|
||||||
|
if (!mounted) return;
|
||||||
|
timeline = await room.getTimeline(onUpdate: updateView);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (e is TimeoutException || e is IOException) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(L10n.of(context)!.jumpToLastReadMessage),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: L10n.of(context)!.jump,
|
||||||
|
onPressed: () => scrollToEventId(eventContextId!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
timeline!.requestKeys(onlineKeyBackupOnly: false);
|
||||||
return true;
|
if (timeline!.events.isNotEmpty) {
|
||||||
|
if (room.markedUnread) room.markUnread(false);
|
||||||
|
setReadMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the scroll controller is attached we want to scroll to an event id, if specified
|
||||||
|
// and update the scroll controller...which will trigger a request history, if the
|
||||||
|
// "load more" button is visible on the screen
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (mounted) {
|
||||||
|
final event = VRouter.of(context).queryParameters['event'];
|
||||||
|
if (event != null) {
|
||||||
|
scrollToEventId(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void>? _setReadMarkerFuture;
|
Future<void>? _setReadMarkerFuture;
|
||||||
|
|
||||||
void setReadMarker([_]) {
|
void setReadMarker({String? eventId}) {
|
||||||
if (_setReadMarkerFuture == null &&
|
if (_setReadMarkerFuture != null) return;
|
||||||
(room!.hasNewMessages || room!.notificationCount > 0) &&
|
if (eventId == null &&
|
||||||
timeline != null &&
|
!room.hasNewMessages &&
|
||||||
timeline!.events.isNotEmpty &&
|
room.notificationCount == 0) {
|
||||||
Matrix.of(context).webHasFocus) {
|
return;
|
||||||
Logs().v('Set read marker...');
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
_setReadMarkerFuture = timeline!.setReadMarker().then((_) {
|
|
||||||
_setReadMarkerFuture = null;
|
|
||||||
});
|
|
||||||
room!.client.updateIosBadge();
|
|
||||||
}
|
}
|
||||||
|
if (!Matrix.of(context).webHasFocus) return;
|
||||||
|
|
||||||
|
final timeline = this.timeline;
|
||||||
|
if (timeline == null || timeline.events.isEmpty) return;
|
||||||
|
|
||||||
|
eventId ??= timeline.events.first.eventId;
|
||||||
|
Logs().v('Set read marker...', eventId);
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
_setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) {
|
||||||
|
_setReadMarkerFuture = null;
|
||||||
|
});
|
||||||
|
room.client.updateIosBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -249,15 +372,24 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
TextEditingController sendController = TextEditingController();
|
TextEditingController sendController = TextEditingController();
|
||||||
|
|
||||||
void setSendingClient(Client? c) {
|
void setSendingClient(Client c) {
|
||||||
// first cancle typing with the old sending client
|
// first cancel typing with the old sending client
|
||||||
if (currentlyTyping) {
|
if (currentlyTyping) {
|
||||||
// no need to have the setting typing to false be blocking
|
// no need to have the setting typing to false be blocking
|
||||||
typingCoolDown?.cancel();
|
typingCoolDown?.cancel();
|
||||||
typingCoolDown = null;
|
typingCoolDown = null;
|
||||||
room!.setTyping(false);
|
room.setTyping(false);
|
||||||
currentlyTyping = false;
|
currentlyTyping = false;
|
||||||
}
|
}
|
||||||
|
// then cancel the old timeline
|
||||||
|
// fixes bug with read reciepts and quick switching
|
||||||
|
loadTimelineFuture = _getTimeline(eventContextId: room.fullyRead).onError(
|
||||||
|
ErrorReporter(
|
||||||
|
context,
|
||||||
|
'Unable to load timeline after changing sending Client',
|
||||||
|
).onErrorCallback,
|
||||||
|
);
|
||||||
|
|
||||||
// then set the new sending client
|
// then set the new sending client
|
||||||
setState(() => sendingClient = c);
|
setState(() => sendingClient = c);
|
||||||
}
|
}
|
||||||
@ -275,7 +407,7 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
|
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
|
||||||
if (commandMatch != null &&
|
if (commandMatch != null &&
|
||||||
!room!.client.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
|
!sendingClient.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
|
||||||
final l10n = L10n.of(context)!;
|
final l10n = L10n.of(context)!;
|
||||||
final dialogResult = await showOkCancelAlertDialog(
|
final dialogResult = await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -290,10 +422,12 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
// ignore: unawaited_futures
|
||||||
room!.sendTextEvent(sendController.text,
|
room.sendTextEvent(
|
||||||
inReplyTo: replyEvent,
|
sendController.text,
|
||||||
editEventId: editEvent?.eventId,
|
inReplyTo: replyEvent,
|
||||||
parseCommands: parseCommands);
|
editEventId: editEvent?.eventId,
|
||||||
|
parseCommands: parseCommands,
|
||||||
|
);
|
||||||
sendController.value = TextEditingValue(
|
sendController.value = TextEditingValue(
|
||||||
text: pendingText,
|
text: pendingText,
|
||||||
selection: const TextSelection.collapsed(offset: 0),
|
selection: const TextSelection.collapsed(offset: 0),
|
||||||
@ -308,42 +442,49 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendFileAction() async {
|
void sendFileAction() async {
|
||||||
final result = await FilePickerCross.importMultipleFromStorage(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileTypeCross.any,
|
allowMultiple: true,
|
||||||
|
withData: true,
|
||||||
);
|
);
|
||||||
if (result.isEmpty) return;
|
if (result == null || result.files.isEmpty) return;
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendFileDialog(
|
builder: (c) => SendFileDialog(
|
||||||
files: result
|
files: result.files
|
||||||
.map((xfile) => MatrixFile(
|
.map(
|
||||||
bytes: xfile.toUint8List(),
|
(xfile) => MatrixFile(
|
||||||
name: xfile.fileName!,
|
bytes: xfile.bytes!,
|
||||||
).detectFileType)
|
name: xfile.name,
|
||||||
|
).detectFileType,
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
room: room!,
|
room: room,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendImageAction() async {
|
void sendImageAction() async {
|
||||||
final result = await FilePickerCross.importMultipleFromStorage(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileTypeCross.image,
|
type: FileType.image,
|
||||||
|
withData: true,
|
||||||
|
allowMultiple: true,
|
||||||
);
|
);
|
||||||
if (result.isEmpty) return;
|
if (result == null || result.files.isEmpty) return;
|
||||||
|
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendFileDialog(
|
builder: (c) => SendFileDialog(
|
||||||
files: result
|
files: result.files
|
||||||
.map((xfile) => MatrixFile(
|
.map(
|
||||||
bytes: xfile.toUint8List(),
|
(xfile) => MatrixFile(
|
||||||
name: xfile.fileName!,
|
bytes: xfile.bytes!,
|
||||||
).detectFileType)
|
name: xfile.name,
|
||||||
|
).detectFileType,
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
room: room!,
|
room: room,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -364,7 +505,7 @@ class ChatController extends State<Chat> {
|
|||||||
name: file.path,
|
name: file.path,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
room: room!,
|
room: room,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -385,16 +526,15 @@ class ChatController extends State<Chat> {
|
|||||||
name: file.path,
|
name: file.path,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
room: room!,
|
room: room,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendStickerAction() async {
|
void sendStickerAction() async {
|
||||||
final sticker = await showModalBottomSheet<ImagePackImageContent>(
|
final sticker = await showAdaptiveBottomSheet<ImagePackImageContent>(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
builder: (c) => StickerPickerDialog(room: room),
|
||||||
builder: (c) => StickerPickerDialog(room: room!),
|
|
||||||
);
|
);
|
||||||
if (sticker == null) return;
|
if (sticker == null) return;
|
||||||
final eventContent = <String, dynamic>{
|
final eventContent = <String, dynamic>{
|
||||||
@ -403,13 +543,14 @@ class ChatController extends State<Chat> {
|
|||||||
'url': sticker.url.toString(),
|
'url': sticker.url.toString(),
|
||||||
};
|
};
|
||||||
// send the sticker
|
// send the sticker
|
||||||
await room!.sendEvent(
|
await room.sendEvent(
|
||||||
eventContent,
|
eventContent,
|
||||||
type: EventTypes.Sticker,
|
type: EventTypes.Sticker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void voiceMessageAction() async {
|
void voiceMessageAction() async {
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
if (PlatformInfos.isAndroid) {
|
if (PlatformInfos.isAndroid) {
|
||||||
final info = await DeviceInfoPlugin().androidInfo;
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
if (info.version.sdkInt < 19) {
|
if (info.version.sdkInt < 19) {
|
||||||
@ -436,7 +577,7 @@ class ChatController extends State<Chat> {
|
|||||||
bytes: audioFile.readAsBytesSync(),
|
bytes: audioFile.readAsBytesSync(),
|
||||||
name: audioFile.path,
|
name: audioFile.path,
|
||||||
);
|
);
|
||||||
await room!.sendFileEvent(
|
await room.sendFileEvent(
|
||||||
file,
|
file,
|
||||||
inReplyTo: replyEvent,
|
inReplyTo: replyEvent,
|
||||||
extraContent: {
|
extraContent: {
|
||||||
@ -450,7 +591,16 @@ class ChatController extends State<Chat> {
|
|||||||
'waveform': result.waveform,
|
'waveform': result.waveform,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
).catchError((e) {
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
(e as Object).toLocalizedString(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
replyEvent = null;
|
replyEvent = null;
|
||||||
});
|
});
|
||||||
@ -477,7 +627,7 @@ class ChatController extends State<Chat> {
|
|||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (c) => SendLocationDialog(room: room!),
|
builder: (c) => SendLocationDialog(room: room),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,8 +641,9 @@ class ChatController extends State<Chat> {
|
|||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
if (copyString.isNotEmpty) copyString += '\n\n';
|
if (copyString.isNotEmpty) copyString += '\n\n';
|
||||||
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||||
MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: true);
|
withSenderNamePrefix: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return copyString;
|
return copyString;
|
||||||
}
|
}
|
||||||
@ -508,33 +659,35 @@ class ChatController extends State<Chat> {
|
|||||||
void reportEventAction() async {
|
void reportEventAction() async {
|
||||||
final event = selectedEvents.single;
|
final event = selectedEvents.single;
|
||||||
final score = await showConfirmationDialog<int>(
|
final score = await showConfirmationDialog<int>(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.reportMessage,
|
title: L10n.of(context)!.reportMessage,
|
||||||
message: L10n.of(context)!.howOffensiveIsThisContent,
|
message: L10n.of(context)!.howOffensiveIsThisContent,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
actions: [
|
actions: [
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -100,
|
key: -100,
|
||||||
label: L10n.of(context)!.extremeOffensive,
|
label: L10n.of(context)!.extremeOffensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: -50,
|
key: -50,
|
||||||
label: L10n.of(context)!.offensive,
|
label: L10n.of(context)!.offensive,
|
||||||
),
|
),
|
||||||
AlertDialogAction(
|
AlertDialogAction(
|
||||||
key: 0,
|
key: 0,
|
||||||
label: L10n.of(context)!.inoffensive,
|
label: L10n.of(context)!.inoffensive,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (score == null) return;
|
if (score == null) return;
|
||||||
final reason = await showTextInputDialog(
|
final reason = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
title: L10n.of(context)!.whyDoYouWantToReportThis,
|
||||||
okLabel: L10n.of(context)!.ok,
|
okLabel: L10n.of(context)!.ok,
|
||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
|
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
|
||||||
|
);
|
||||||
if (reason == null || reason.single.isEmpty) return;
|
if (reason == null || reason.single.isEmpty) return;
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -551,7 +704,8 @@ class ChatController extends State<Chat> {
|
|||||||
selectedEvents.clear();
|
selectedEvents.clear();
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
|
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void redactEventsAction() async {
|
void redactEventsAction() async {
|
||||||
@ -566,25 +720,27 @@ class ChatController extends State<Chat> {
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () async {
|
future: () async {
|
||||||
if (event.status.isSent) {
|
if (event.status.isSent) {
|
||||||
if (event.canRedact) {
|
if (event.canRedact) {
|
||||||
await event.redactEvent();
|
await event.redactEvent();
|
||||||
} else {
|
|
||||||
final client = currentRoomBundle.firstWhere(
|
|
||||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
|
||||||
orElse: () => null);
|
|
||||||
if (client == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final room = client.getRoomById(roomId!)!;
|
|
||||||
await Event.fromJson(event.toJson(), room).redactEvent();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await event.remove();
|
final client = currentRoomBundle.firstWhere(
|
||||||
|
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final room = client.getRoomById(roomId)!;
|
||||||
|
await Event.fromJson(event.toJson(), room).redactEvent();
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
await event.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
showEmojiPicker = false;
|
showEmojiPicker = false;
|
||||||
@ -593,13 +749,14 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Client?> get currentRoomBundle {
|
List<Client?> get currentRoomBundle {
|
||||||
final clients = matrix!.currentBundle!;
|
final clients = Matrix.of(context).currentBundle!;
|
||||||
clients.removeWhere((c) => c!.getRoomById(roomId!) == null);
|
clients.removeWhere((c) => c!.getRoomById(roomId) == null);
|
||||||
return clients;
|
return clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get canRedactSelectedEvents {
|
bool get canRedactSelectedEvents {
|
||||||
final clients = matrix!.currentBundle;
|
if (isArchived) return false;
|
||||||
|
final clients = Matrix.of(context).currentBundle;
|
||||||
for (final event in selectedEvents) {
|
for (final event in selectedEvents) {
|
||||||
if (event.canRedact == false &&
|
if (event.canRedact == false &&
|
||||||
!(clients!.any((cl) => event.senderId == cl!.userID))) return false;
|
!(clients!.any((cl) => event.senderId == cl!.userID))) return false;
|
||||||
@ -608,7 +765,9 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get canEditSelectedEvents {
|
bool get canEditSelectedEvents {
|
||||||
if (selectedEvents.length != 1 || !selectedEvents.first.status.isSent) {
|
if (isArchived ||
|
||||||
|
selectedEvents.length != 1 ||
|
||||||
|
!selectedEvents.first.status.isSent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return currentRoomBundle
|
return currentRoomBundle
|
||||||
@ -652,48 +811,23 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void scrollToEventId(String eventId) async {
|
void scrollToEventId(String eventId) async {
|
||||||
var eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
|
final eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
|
||||||
if (eventIndex == -1) {
|
if (eventIndex == -1) {
|
||||||
// event id not found...maybe we can fetch it?
|
setState(() {
|
||||||
// the try...finally is here to start and close the loading dialog reliably
|
timeline = null;
|
||||||
await showFutureLoadingDialog(
|
_scrolledUp = false;
|
||||||
context: context,
|
loadTimelineFuture = _getTimeline(
|
||||||
future: () async {
|
eventContextId: eventId,
|
||||||
// okay, we first have to fetch if the event is in the room
|
timeout: const Duration(seconds: 30),
|
||||||
try {
|
).onError(
|
||||||
final event = await timeline!.getEventById(eventId);
|
ErrorReporter(context, 'Unable to load timeline after scroll to ID')
|
||||||
if (event == null) {
|
.onErrorCallback,
|
||||||
// event is null...meaning something is off
|
);
|
||||||
return;
|
});
|
||||||
}
|
await loadTimelineFuture;
|
||||||
} catch (err) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
|
scrollToEventId(eventId);
|
||||||
// event wasn't found, as the server gave a 404 or something
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
// okay, we know that the event *is* in the room
|
|
||||||
while (eventIndex == -1) {
|
|
||||||
if (!canLoadMore) {
|
|
||||||
// we can't load any more events but still haven't found ours yet...better stop here
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await timeline!.requestHistory(historyCount: _loadHistoryCount);
|
|
||||||
} catch (err) {
|
|
||||||
if (err is TimeoutException) {
|
|
||||||
// loading the history timed out...so let's do nothing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
eventIndex =
|
|
||||||
timeline!.events.indexWhere((e) => e.eventId == eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await scrollController.scrollToIndex(
|
await scrollController.scrollToIndex(
|
||||||
@ -703,7 +837,21 @@ class ChatController extends State<Chat> {
|
|||||||
_updateScrollController();
|
_updateScrollController();
|
||||||
}
|
}
|
||||||
|
|
||||||
void scrollDown() => scrollController.jumpTo(0);
|
void scrollDown() async {
|
||||||
|
if (!timeline!.allowNewEvent) {
|
||||||
|
setState(() {
|
||||||
|
timeline = null;
|
||||||
|
_scrolledUp = false;
|
||||||
|
loadTimelineFuture = _getTimeline().onError(
|
||||||
|
ErrorReporter(context, 'Unable to load timeline after scroll down')
|
||||||
|
.onErrorCallback,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await loadTimelineFuture;
|
||||||
|
setReadMarker(eventId: timeline!.events.first.eventId);
|
||||||
|
}
|
||||||
|
scrollController.jumpTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
void onEmojiSelected(_, Emoji? emoji) {
|
void onEmojiSelected(_, Emoji? emoji) {
|
||||||
switch (emojiPickerType) {
|
switch (emojiPickerType) {
|
||||||
@ -721,11 +869,23 @@ class ChatController extends State<Chat> {
|
|||||||
setState(() => showEmojiPicker = false);
|
setState(() => showEmojiPicker = false);
|
||||||
if (emoji == null) return;
|
if (emoji == null) return;
|
||||||
// make sure we don't send the same emoji twice
|
// make sure we don't send the same emoji twice
|
||||||
if (_allReactionEvents
|
if (_allReactionEvents.any(
|
||||||
.any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return;
|
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return sendEmojiAction(emoji.emoji);
|
return sendEmojiAction(emoji.emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void forgetRoom() async {
|
||||||
|
final result = await showFutureLoadingDialog(
|
||||||
|
context: context,
|
||||||
|
future: room.forget,
|
||||||
|
);
|
||||||
|
if (result.error != null) return;
|
||||||
|
VRouter.of(context).to('/archive');
|
||||||
|
}
|
||||||
|
|
||||||
void typeEmoji(Emoji? emoji) {
|
void typeEmoji(Emoji? emoji) {
|
||||||
if (emoji == null) return;
|
if (emoji == null) return;
|
||||||
final text = sendController.text;
|
final text = sendController.text;
|
||||||
@ -753,7 +913,8 @@ class ChatController extends State<Chat> {
|
|||||||
sendController
|
sendController
|
||||||
..text = sendController.text.characters.skipLast(1).toString()
|
..text = sendController.text.characters.skipLast(1).toString()
|
||||||
..selection = TextSelection.fromPosition(
|
..selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: sendController.text.length));
|
TextPosition(offset: sendController.text.length),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -768,7 +929,7 @@ class ChatController extends State<Chat> {
|
|||||||
final events = List<Event>.from(selectedEvents);
|
final events = List<Event>.from(selectedEvents);
|
||||||
setState(() => selectedEvents.clear());
|
setState(() => selectedEvents.clear());
|
||||||
for (final event in events) {
|
for (final event in events) {
|
||||||
await room!.sendReaction(
|
await room.sendReaction(
|
||||||
event.eventId,
|
event.eventId,
|
||||||
emoji!,
|
emoji!,
|
||||||
);
|
);
|
||||||
@ -788,8 +949,9 @@ class ChatController extends State<Chat> {
|
|||||||
|
|
||||||
void editSelectedEventAction() {
|
void editSelectedEventAction() {
|
||||||
final client = currentRoomBundle.firstWhere(
|
final client = currentRoomBundle.firstWhere(
|
||||||
(cl) => selectedEvents.first.senderId == cl!.userID,
|
(cl) => selectedEvents.first.senderId == cl!.userID,
|
||||||
orElse: () => null);
|
orElse: () => null,
|
||||||
|
);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -797,10 +959,12 @@ class ChatController extends State<Chat> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
pendingText = sendController.text;
|
pendingText = sendController.text;
|
||||||
editEvent = selectedEvents.first;
|
editEvent = selectedEvents.first;
|
||||||
inputText = sendController.text = editEvent!
|
inputText = sendController.text =
|
||||||
.getDisplayEvent(timeline!)
|
editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
|
||||||
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!),
|
MatrixLocals(L10n.of(context)!),
|
||||||
withSenderNamePrefix: false, hideReply: true);
|
withSenderNamePrefix: false,
|
||||||
|
hideReply: true,
|
||||||
|
);
|
||||||
selectedEvents.clear();
|
selectedEvents.clear();
|
||||||
});
|
});
|
||||||
inputFocus.requestFocus();
|
inputFocus.requestFocus();
|
||||||
@ -812,7 +976,7 @@ class ChatController extends State<Chat> {
|
|||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context)!.goToTheNewRoom,
|
title: L10n.of(context)!.goToTheNewRoom,
|
||||||
message: room!
|
message: room
|
||||||
.getState(EventTypes.RoomTombstone)!
|
.getState(EventTypes.RoomTombstone)!
|
||||||
.parsedTombstoneContent
|
.parsedTombstoneContent
|
||||||
.body,
|
.body,
|
||||||
@ -823,14 +987,16 @@ class ChatController extends State<Chat> {
|
|||||||
}
|
}
|
||||||
final result = await showFutureLoadingDialog(
|
final result = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => room!.client.joinRoom(room!
|
future: () => room.client.joinRoom(
|
||||||
.getState(EventTypes.RoomTombstone)!
|
room
|
||||||
.parsedTombstoneContent
|
.getState(EventTypes.RoomTombstone)!
|
||||||
.replacementRoom),
|
.parsedTombstoneContent
|
||||||
|
.replacementRoom,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await showFutureLoadingDialog(
|
await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: room!.leave,
|
future: room.leave,
|
||||||
);
|
);
|
||||||
if (result.error == null) {
|
if (result.error == null) {
|
||||||
VRouter.of(context).toSegments(['rooms', result.result!]);
|
VRouter.of(context).toSegments(['rooms', result.result!]);
|
||||||
@ -907,18 +1073,16 @@ class ChatController extends State<Chat> {
|
|||||||
cancelLabel: L10n.of(context)!.cancel,
|
cancelLabel: L10n.of(context)!.cancel,
|
||||||
);
|
);
|
||||||
if (response == OkCancelResult.ok) {
|
if (response == OkCancelResult.ok) {
|
||||||
final events = room!.pinnedEventIds
|
final events = room.pinnedEventIds
|
||||||
..removeWhere((oldEvent) => oldEvent == eventId);
|
..removeWhere((oldEvent) => oldEvent == eventId);
|
||||||
showFutureLoadingDialog(
|
showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () => room!.setPinnedEvents(events),
|
future: () => room.setPinnedEvents(events),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void pinEvent() {
|
void pinEvent() {
|
||||||
final room = this.room;
|
|
||||||
if (room == null) return;
|
|
||||||
final pinnedEventIds = room.pinnedEventIds;
|
final pinnedEventIds = room.pinnedEventIds;
|
||||||
final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet();
|
final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet();
|
||||||
final unpin = selectedEventIds.length == 1 &&
|
final unpin = selectedEventIds.length == 1 &&
|
||||||
@ -944,7 +1108,7 @@ class ChatController extends State<Chat> {
|
|||||||
await prefs.setString('draft_$roomId', text);
|
await prefs.setString('draft_$roomId', text);
|
||||||
});
|
});
|
||||||
setReadMarker();
|
setReadMarker();
|
||||||
if (text.endsWith(' ') && matrix!.hasComplexBundles) {
|
if (text.endsWith(' ') && Matrix.of(context).hasComplexBundles) {
|
||||||
final clients = currentRoomBundle;
|
final clients = currentRoomBundle;
|
||||||
for (final client in clients) {
|
for (final client in clients) {
|
||||||
final prefix = client!.sendPrefix;
|
final prefix = client!.sendPrefix;
|
||||||
@ -963,7 +1127,7 @@ class ChatController extends State<Chat> {
|
|||||||
typingCoolDown = Timer(const Duration(seconds: 2), () {
|
typingCoolDown = Timer(const Duration(seconds: 2), () {
|
||||||
typingCoolDown = null;
|
typingCoolDown = null;
|
||||||
currentlyTyping = false;
|
currentlyTyping = false;
|
||||||
room!.setTyping(false);
|
room.setTyping(false);
|
||||||
});
|
});
|
||||||
typingTimeout ??= Timer(const Duration(seconds: 30), () {
|
typingTimeout ??= Timer(const Duration(seconds: 30), () {
|
||||||
typingTimeout = null;
|
typingTimeout = null;
|
||||||
@ -971,12 +1135,14 @@ class ChatController extends State<Chat> {
|
|||||||
});
|
});
|
||||||
if (!currentlyTyping) {
|
if (!currentlyTyping) {
|
||||||
currentlyTyping = true;
|
currentlyTyping = true;
|
||||||
room!
|
room.setTyping(true, timeout: const Duration(seconds: 30).inMilliseconds);
|
||||||
.setTyping(true, timeout: const Duration(seconds: 30).inMilliseconds);
|
|
||||||
}
|
}
|
||||||
setState(() => inputText = text);
|
setState(() => inputText = text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isArchived =>
|
||||||
|
{Membership.leave, Membership.ban}.contains(room.membership);
|
||||||
|
|
||||||
void showEventInfo([Event? event]) =>
|
void showEventInfo([Event? event]) =>
|
||||||
(event ?? selectedEvents.single).showInfoDialog(context);
|
(event ?? selectedEvents.single).showInfoDialog(context);
|
||||||
|
|
||||||
@ -1016,16 +1182,19 @@ class ChatController extends State<Chat> {
|
|||||||
if (callType == null) return;
|
if (callType == null) return;
|
||||||
|
|
||||||
final success = await showFutureLoadingDialog(
|
final success = await showFutureLoadingDialog(
|
||||||
context: context,
|
context: context,
|
||||||
future: () =>
|
future: () =>
|
||||||
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials());
|
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials(),
|
||||||
|
);
|
||||||
if (success.result != null) {
|
if (success.result != null) {
|
||||||
final voipPlugin = Matrix.of(context).voipPlugin;
|
final voipPlugin = Matrix.of(context).voipPlugin;
|
||||||
await voipPlugin!.voip.inviteToCall(room!.id, callType).catchError((e) {
|
try {
|
||||||
|
await voipPlugin!.voip.inviteToCall(room.id, callType);
|
||||||
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text((e as Object).toLocalizedString(context))),
|
SnackBar(content: Text(e.toLocalizedString(context))),
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
await showOkAlertDialog(
|
await showOkAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:vrouter/vrouter.dart';
|
|||||||
|
|
||||||
import 'package:fluffychat/pages/chat/chat.dart';
|
import 'package:fluffychat/pages/chat/chat.dart';
|
||||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||||
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
|
|
||||||
@ -15,9 +16,6 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final room = controller.room;
|
final room = controller.room;
|
||||||
if (room == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
if (controller.selectedEvents.isNotEmpty) {
|
if (controller.selectedEvents.isNotEmpty) {
|
||||||
return Text(controller.selectedEvents.length.toString());
|
return Text(controller.selectedEvents.length.toString());
|
||||||
}
|
}
|
||||||
@ -26,7 +24,7 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||||||
splashColor: Colors.transparent,
|
splashColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
onTap: directChatMatrixID != null
|
onTap: directChatMatrixID != null
|
||||||
? () => showModalBottomSheet(
|
? () => showAdaptiveBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (c) => UserBottomSheet(
|
builder: (c) => UserBottomSheet(
|
||||||
user: room
|
user: room
|
||||||
@ -36,14 +34,19 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||||||
'${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ',
|
'${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),
|
: controller.isArchived
|
||||||
|
? null
|
||||||
|
: () =>
|
||||||
|
VRouter.of(context).toSegments(['rooms', room.id, 'details']),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: 'content_banner',
|
tag: 'content_banner',
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
mxContent: room.avatar,
|
mxContent: room.avatar,
|
||||||
name: room.displayname,
|
name: room.getLocalizedDisplayname(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||