Compare commits
117 Commits
v1.7.3-coo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 5df709e12b | |||
| cba5fa2daf | |||
| cab78f3571 | |||
| 4cd501d00c | |||
| edfd8f36ab | |||
| 7635104505 | |||
| 835d97f439 | |||
| c9f4904d99 | |||
| bd5a6e5578 | |||
| 29ec1735e9 | |||
| 7436cb4aa8 | |||
| ee3351f643 | |||
| fff3dc9946 | |||
| 1e1e591d27 | |||
| abb99df271 | |||
| 6616314d77 | |||
| 3cb7842a7b | |||
| 77aca413fb | |||
| 04f34a4301 | |||
| d3e3252de8 | |||
| ca203608fa | |||
| e9d02336e1 | |||
| b3ad9a3a70 | |||
| d2f472e86c | |||
| 338331d6e1 | |||
| b65357576c | |||
| 99b0ee194c | |||
| d930b569fc | |||
| 7da70ebeba | |||
| 54303ef635 | |||
| 05285b46d8 | |||
| 220dda715a | |||
| a702a12c71 | |||
| 17f8eda6e4 | |||
| 139d1f01ca | |||
| 7b3cfe875f | |||
| 3b455d7801 | |||
| ed68fc55fc | |||
| 108c620326 | |||
| accd4b9a23 | |||
| 0a4f7c9d26 | |||
| 2512630172 | |||
| 30b17beaa7 | |||
| e368227780 | |||
| f9e4b9356a | |||
| b24a7d9510 | |||
| 6084d36ed2 | |||
| 781a02cea7 | |||
| fbb9f40f01 | |||
| c19946c184 | |||
| 2ba6e15e59 | |||
| 0052b6d42f | |||
| 94b19cf6a6 | |||
| b1699cfa16 | |||
| c7dcceb7dd | |||
| 496bb0dbd4 | |||
| 1294479974 | |||
| ca7bf8bd0d | |||
| 8f89d539d0 | |||
| 7cdeb98671 | |||
| bb74754851 | |||
| 4680a1c507 | |||
| 51a5e7f9cc | |||
| c4f601f651 | |||
| f6c2fa8588 | |||
| 576d46eb4c | |||
| b21ab55451 | |||
| 56b3297610 | |||
| 43b408fe5e | |||
| 12cc876b83 | |||
| 1d53fccfe6 | |||
| 857eea428e | |||
| 715e98cae2 | |||
| bb9410accb |
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,7 +10,6 @@
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
lib/generated_plugin_registrant.dart
|
||||
prime
|
||||
|
||||
# libolm package
|
||||
@ -38,7 +37,6 @@ prime
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
docs/build/
|
||||
docs/.jekyll-cache/
|
||||
docs/_site/
|
||||
@ -62,3 +60,4 @@ ios/Podfile.lock
|
||||
/linux/out
|
||||
/macos/out
|
||||
.vs
|
||||
olm
|
||||
|
||||
132
.gitlab-ci.yml
132
.gitlab-ci.yml
@ -1,7 +1,9 @@
|
||||
variables:
|
||||
FLUTTER_VERSION: 3.3.9
|
||||
|
||||
image: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
image:
|
||||
name: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
pull_policy: if-not-present
|
||||
|
||||
.shared_windows_runners:
|
||||
tags:
|
||||
@ -16,7 +18,7 @@ stages:
|
||||
|
||||
code_analyze:
|
||||
stage: test
|
||||
script: [./scripts/code_analyze.sh]
|
||||
script: [ ./scripts/code_analyze.sh ]
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: code-quality-report.json
|
||||
@ -26,13 +28,13 @@ code_analyze:
|
||||
|
||||
widget_test:
|
||||
stage: test
|
||||
script: [flutter test]
|
||||
script: [ flutter test ]
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
|
||||
# the basic integration test configuration testing FLOSS builds on Synapse
|
||||
.integration_test:
|
||||
integration_test:
|
||||
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/integration/stable:${FLUTTER_VERSION}
|
||||
stage: test
|
||||
services:
|
||||
@ -49,15 +51,13 @@ widget_test:
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
# Tell docker CLI how to talk to Docker daemon.
|
||||
DOCKER_HOST: tcp://docker:2375/
|
||||
# Use the overlayfs driver for improved performance.
|
||||
DOCKER_DRIVER: overlay2
|
||||
# Use the btrfs driver for improved performance.
|
||||
DOCKER_DRIVER: btrfs
|
||||
# Disable TLS since we're running inside local network.
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
HOMESERVER: "docker"
|
||||
HOMESERVER: docker
|
||||
before_script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- scripts/integration-prepare-alpine.sh
|
||||
- scripts/integration-prepare-host.sh
|
||||
# create test user environment variables
|
||||
- source scripts/integration-create-environment-variables.sh
|
||||
# create Synapse instance
|
||||
@ -65,31 +65,60 @@ widget_test:
|
||||
# properly set the homeserver IP and create test users
|
||||
- scripts/integration-prepare-homeserver.sh
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- flutter pub get
|
||||
- flutter test integration_test
|
||||
timeout: 20m
|
||||
- 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 )
|
||||
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
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: always
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- video.mp4
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
|
||||
|
||||
# integration tests for Linux builds
|
||||
### disabled because of Linux headless issues
|
||||
.integration_test_linux:
|
||||
extends: .integration_test
|
||||
image: cirrusci/flutter:${FLUTTER_VERSION}
|
||||
extends: integration_test
|
||||
parallel:
|
||||
matrix:
|
||||
- HOMESERVER_IMPLEMENTATION:
|
||||
- conduit
|
||||
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 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
|
||||
.integration_test_proprietary:
|
||||
extends: .integration_test
|
||||
integration_test_proprietary:
|
||||
extends: integration_test
|
||||
parallel:
|
||||
matrix:
|
||||
- HOMESERVER_IMPLEMENTATION:
|
||||
- conduit
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- 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:
|
||||
matrix:
|
||||
- FLAVOR:
|
||||
@ -99,9 +128,9 @@ widget_test:
|
||||
stage: test
|
||||
before_script:
|
||||
- |
|
||||
if [ "$FLAVOR" == "proprietary" ]; then
|
||||
git apply ./scripts/enable-android-google-services.patch
|
||||
fi
|
||||
if [ "$FLAVOR" == "proprietary" ]; then
|
||||
git apply ./scripts/enable-android-google-services.patch
|
||||
fi
|
||||
script:
|
||||
# start AVD and keep running in background
|
||||
- scripts/integration-start-avd.sh &
|
||||
@ -115,8 +144,8 @@ widget_test:
|
||||
build_web:
|
||||
stage: build
|
||||
before_script:
|
||||
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
|
||||
script: [./scripts/build-web.sh]
|
||||
[ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
|
||||
script: [ ./scripts/build-web.sh ]
|
||||
artifacts:
|
||||
paths:
|
||||
- build/web/
|
||||
@ -124,23 +153,49 @@ build_web:
|
||||
- docker
|
||||
- famedly
|
||||
|
||||
# yes, we *do* build a Windows DLL on Linux. More reliable.
|
||||
build_olm_windows:
|
||||
image: archlinux:latest
|
||||
stage: test
|
||||
before_script:
|
||||
- pacman-key --init
|
||||
- pacman --noconfirm -Sy mingw-w64 cmake git base-devel
|
||||
script:
|
||||
- ./scripts/build-olm-windows.sh
|
||||
- mv olm/build/libolm.dll .
|
||||
artifacts:
|
||||
paths:
|
||||
- libolm.dll
|
||||
only:
|
||||
- main
|
||||
- tags
|
||||
|
||||
build_windows:
|
||||
extends:
|
||||
- .shared_windows_runners
|
||||
stage: build
|
||||
before_script: [./scripts/prepare-windows.ps1]
|
||||
script: [./scripts/build-windows.ps1]
|
||||
stage: test
|
||||
before_script:
|
||||
- ./scripts/prepare-windows.ps1
|
||||
# workarounding artifacts download being broken
|
||||
- $response = Invoke-WebRequest -Uri "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs" -UseBasicParsing
|
||||
- $jobs = $response | ConvertFrom-Json
|
||||
- $job = $jobs | where { $_.name -eq "build_olm_windows" }
|
||||
- $jobId = $job.id
|
||||
- Invoke-WebRequest -Uri "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$jobId/artifacts/libolm.dll" -UseBasicParsing -OutFile libolm.dll
|
||||
script:
|
||||
- ./scripts/build-windows.ps1
|
||||
- Copy-Item -Path "libolm.dll" -Destination "build/windows/runner/Release"
|
||||
- ./scripts/package-windows.ps1
|
||||
artifacts:
|
||||
paths:
|
||||
- build/windows/runner/Release
|
||||
allow_failure: true
|
||||
only:
|
||||
- main
|
||||
- tags
|
||||
|
||||
build_android_debug:
|
||||
stage: build
|
||||
script: [./scripts/build-android-debug.sh]
|
||||
script: [ ./scripts/build-android-debug.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
@ -157,7 +212,7 @@ build_android_apk:
|
||||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/build-android-apk.sh]
|
||||
script: [ ./scripts/build-android-apk.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
@ -174,7 +229,7 @@ deploy_playstore_internal:
|
||||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/release-playstore-beta.sh]
|
||||
script: [ ./scripts/release-playstore-beta.sh ]
|
||||
artifacts:
|
||||
when: on_success
|
||||
paths:
|
||||
@ -241,7 +296,7 @@ build_linux_x86:
|
||||
[
|
||||
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,
|
||||
]
|
||||
script: [./scripts/build-linux.sh]
|
||||
script: [ ./scripts/build-linux.sh ]
|
||||
tags:
|
||||
- docker
|
||||
- famedly
|
||||
@ -252,9 +307,9 @@ build_linux_x86:
|
||||
|
||||
build_linux_arm64:
|
||||
stage: build
|
||||
before_script: [flutter upgrade]
|
||||
script: [./scripts/build-linux.sh]
|
||||
tags: [docker_arm64]
|
||||
before_script: [ flutter upgrade ]
|
||||
script: [ ./scripts/build-linux.sh ]
|
||||
tags: [ docker_arm64 ]
|
||||
only:
|
||||
- main
|
||||
- tags
|
||||
@ -266,7 +321,7 @@ build_linux_arm64:
|
||||
|
||||
update_dependencies:
|
||||
stage: build
|
||||
needs: []
|
||||
needs: [ ]
|
||||
tags:
|
||||
- docker
|
||||
only:
|
||||
@ -313,7 +368,8 @@ upload_android:
|
||||
upload_web:
|
||||
extends: .release
|
||||
script:
|
||||
- tar czf package.tar.gz -C build/web/ .
|
||||
# workaround bug of Flutter engine
|
||||
- tar czf package.tar.gz --ignore-failed-read -C build/web/ .
|
||||
- |
|
||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
|
||||
|
||||
@ -347,7 +403,7 @@ deploy_playstore:
|
||||
before_script:
|
||||
- git apply ./scripts/enable-android-google-services.patch
|
||||
- ./scripts/prepare-android-release.sh
|
||||
script: [./scripts/release-playstore.sh]
|
||||
script: [ ./scripts/release-playstore.sh ]
|
||||
resource_group: playstore_release
|
||||
only:
|
||||
- tags
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@ -1,3 +1,29 @@
|
||||
## v1.8.0 2022-12-30
|
||||
- Added translation using Weblate (Yue (yue_HK)) (Raatty)
|
||||
- Translated using Weblate (Chinese (Simplified)) (Mike Evans)
|
||||
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||
- Translated using Weblate (French) (Anne Onyme 017)
|
||||
- Translated using Weblate (Indonesian) (Linerly)
|
||||
- Translated using Weblate (Turkish) (Oğuz Ersen)
|
||||
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||
- design: New encryption page (Krille Fear)
|
||||
- feat: Add audio message support to linux (Krille Fear)
|
||||
- feat: Use Android system accent color (Krille Fear)
|
||||
- feat: include olm to Windows builds (TheOneWithTheBraid)
|
||||
- feat: Store drafts (Krille)
|
||||
- fix: Android push notification follow-up (TheOneWithTheBraid)
|
||||
- fix: Content banner (Krille Fear)
|
||||
- fix: Correct redacted by username (Krille Fear)
|
||||
- fix: Do not setup push on every app resume (Krille Fear)
|
||||
- fix: Encryption button is orange in public rooms (Krille Fear)
|
||||
- fix: File event design (Krille Fear)
|
||||
- fix: Hide google services warning after marked (Krille Fear)
|
||||
- fix: Improve story page appearance (Reinhart Previano Koentjoro)
|
||||
- fix: Libhandy windows (Krille Fear)
|
||||
- fix: Monochromatic icon rendering for Android 13+ (Reinhart Previano Koentjoro)
|
||||
- fix: homeserver error text not visible in app bar (TheOneWithTheBraid)
|
||||
- fix: minor issues in room list (TheOneWithTheBraid)
|
||||
|
||||
## v1.7.2 2022-12-19
|
||||
Update dependencies and translations.
|
||||
|
||||
|
||||
11
android/app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
11
android/app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
File diff suppressed because one or more lines are too long
@ -2,5 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
BIN
assets/banner_transparent.png
Normal file
BIN
assets/banner_transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
assets/encryption.png
Normal file
BIN
assets/encryption.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
File diff suppressed because it is too large
Load Diff
@ -1,30 +1,25 @@
|
||||
{
|
||||
"@@last_modified": "2021-08-14 12:41:10.154280",
|
||||
"about": "সম্পর্কে",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accept": "স্বীকার করি",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
"@@last_modified": "2021-08-14 12:41:10.154280",
|
||||
"about": "সম্পর্কে",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accept": "স্বীকার করি",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"account": "অ্যাকাউন্ট",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
},
|
||||
"account": "অ্যাকাউন্ট",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accountInformation": "অ্যাকাউন্ট তথ্য",
|
||||
"@accountInformation": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1 +1 @@
|
||||
{}
|
||||
{}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,155 +1,155 @@
|
||||
{
|
||||
"@@last_modified": "2021-08-14 12:41:09.940318",
|
||||
"copiedToClipboard": "Copiada para a área de transferência",
|
||||
"@copiedToClipboard": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"login": "Iniciar sessão",
|
||||
"@login": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"monday": "segunda-feira",
|
||||
"@monday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"saturday": "sábado",
|
||||
"@saturday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"wednesday": "quarta-feira",
|
||||
"@wednesday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"about": "Sobre",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"admin": "Admin",
|
||||
"@admin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areYouSure": "Tens a certeza?",
|
||||
"@areYouSure": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"notifications": "Notificações",
|
||||
"@notifications": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"account": "Conta",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"cancel": "Cancelar",
|
||||
"@cancel": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"delete": "Eliminar",
|
||||
"@delete": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dateAndTimeOfDay": "{date}, {timeOfDay}",
|
||||
"@dateAndTimeOfDay": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"date": {},
|
||||
"timeOfDay": {}
|
||||
"@@last_modified": "2021-08-14 12:41:09.940318",
|
||||
"copiedToClipboard": "Copiada para a área de transferência",
|
||||
"@copiedToClipboard": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"login": "Iniciar sessão",
|
||||
"@login": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"monday": "segunda-feira",
|
||||
"@monday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"saturday": "sábado",
|
||||
"@saturday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"wednesday": "quarta-feira",
|
||||
"@wednesday": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"about": "Sobre",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"admin": "Admin",
|
||||
"@admin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areYouSure": "Tens a certeza?",
|
||||
"@areYouSure": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"notifications": "Notificações",
|
||||
"@notifications": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"account": "Conta",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"cancel": "Cancelar",
|
||||
"@cancel": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"delete": "Eliminar",
|
||||
"@delete": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"dateAndTimeOfDay": "{date}, {timeOfDay}",
|
||||
"@dateAndTimeOfDay": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"date": {},
|
||||
"timeOfDay": {}
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,182 +1,147 @@
|
||||
{
|
||||
"@@last_modified": "2021-08-14 12:41:09.918296",
|
||||
"about": "Despre",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accept": "Accept",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"acceptedTheInvitation": "{username} a aceptat invitați",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
"@@last_modified": "2021-08-14 12:41:09.918296",
|
||||
"about": "Despre",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accept": "Accept",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"acceptedTheInvitation": "{username} a aceptat invitați",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"account": "Cont",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"activatedEndToEndEncryption": "{username} a activat criptarea end-to-end",
|
||||
"@activatedEndToEndEncryption": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"addGroupDescription": "Adaugă o descriere de",
|
||||
"@addGroupDescription": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"admin": "Administrator",
|
||||
"@admin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"alias": "poreclă",
|
||||
"@alias": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"answeredTheCall": "{sendername} a acceptat apelul",
|
||||
"@answeredTheCall": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"senderName": {}
|
||||
}
|
||||
},
|
||||
"anyoneCanJoin": "Oricine se poate alătura",
|
||||
"@anyoneCanJoin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"archive": "Arhivă",
|
||||
"@archive": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"archivedRoom": "Grup arhivat",
|
||||
"@archivedRoom": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areGuestsAllowedToJoin": "Vizitatorii \"guest\" se pot alătura",
|
||||
"@areGuestsAllowedToJoin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areYouSure": "Ești sigur?",
|
||||
"@areYouSure": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askSSSSSign": "Pentru a putea conecta cealaltă persoană, te rog introdu parola sau cheia ta de recuperare.",
|
||||
"@askSSSSSign": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askVerificationRequest": "Accepți cererea de verificare de la {username}?",
|
||||
"@askVerificationRequest": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"banFromChat": "Interzis din conversație",
|
||||
"@banFromChat": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"banned": "Interzis",
|
||||
"@banned": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"bannedUser": "{username} a interzis pe {targetName}",
|
||||
"@bannedUser": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"targetName": {}
|
||||
}
|
||||
},
|
||||
"blockDevice": "Blochează dispozitiv",
|
||||
"@blockDevice": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"cancel": "Anulează",
|
||||
"@cancel": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeDeviceName": "Schimbă numele dispozitiv",
|
||||
"@changeDeviceName": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changedTheChatAvatar": "{username} a schimbat poza conversați",
|
||||
"@changedTheChatAvatar": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheChatDescriptionTo": "{username} a schimbat descrierea grupului în '{description}'",
|
||||
"@changedTheChatDescriptionTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"description": {}
|
||||
}
|
||||
},
|
||||
"changedTheChatNameTo": "{username} a schimbat porecla în '{chatname}'",
|
||||
"@changedTheChatNameTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"chatname": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"account": "Cont",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accountInformation": "Informații despre cont",
|
||||
"@accountInformation": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"activatedEndToEndEncryption": "{username} a activat criptarea end-to-end",
|
||||
"@activatedEndToEndEncryption": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"addGroupDescription": "Adaugă o descriere de",
|
||||
"@addGroupDescription": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"admin": "Administrator",
|
||||
"@admin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"alias": "poreclă",
|
||||
"@alias": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"alreadyHaveAnAccount": "Ai deja un cont?",
|
||||
"@alreadyHaveAnAccount": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"answeredTheCall": "{sendername} a acceptat apelul",
|
||||
"@answeredTheCall": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"senderName": {}
|
||||
}
|
||||
},
|
||||
"anyoneCanJoin": "Oricine se poate alătura",
|
||||
"@anyoneCanJoin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"archive": "Arhivă",
|
||||
"@archive": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"archivedRoom": "Grup arhivat",
|
||||
"@archivedRoom": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areGuestsAllowedToJoin": "Vizitatorii \"guest\" se pot alătura",
|
||||
"@areGuestsAllowedToJoin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"areYouSure": "Ești sigur?",
|
||||
"@areYouSure": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askSSSSCache": "Te rog introdu parola ta sau cheile de recuparare pentru a depozita cheile.",
|
||||
"@askSSSSCache": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askSSSSSign": "Pentru a putea conecta cealaltă persoană, te rog introdu parola sau cheia ta de recuperare.",
|
||||
"@askSSSSSign": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askSSSSVerify": "Te rog introdu parola sau cheia ta de recuperare pentru a-ți verifica sesiunea.",
|
||||
"@askSSSSVerify": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"askVerificationRequest": "Accepți cererea de verificare de la {username}?",
|
||||
"@askVerificationRequest": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"authentication": "Autentificare",
|
||||
"@authentication": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"avatarHasBeenChanged": "Image de profil schimbată",
|
||||
"@avatarHasBeenChanged": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"banFromChat": "Interzis din conversație",
|
||||
"@banFromChat": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"banned": "Interzis",
|
||||
"@banned": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"bannedUser": "{username} a interzis pe {targetName}",
|
||||
"@bannedUser": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"targetName": {}
|
||||
}
|
||||
},
|
||||
"blockDevice": "Blochează dispozitiv",
|
||||
"@blockDevice": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"cachedKeys": "Chei salvate",
|
||||
"@cachedKeys": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"cancel": "Anulează",
|
||||
"@cancel": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeDeviceName": "Schimbă numele dispozitiv",
|
||||
"@changeDeviceName": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changedTheChatAvatar": "{username} a schimbat poza conversați",
|
||||
"@changedTheChatAvatar": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"changedTheChatDescriptionTo": "{username} a schimbat descrierea grupului în '{description}'",
|
||||
"@changedTheChatDescriptionTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"description": {}
|
||||
}
|
||||
},
|
||||
"changedTheChatNameTo": "{username} a schimbat porecla în '{chatname}'",
|
||||
"@changedTheChatNameTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {},
|
||||
"chatname": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,374 +1,292 @@
|
||||
{
|
||||
"@@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": {}
|
||||
"@@last_modified": "2021-08-14 12:41:09.895217",
|
||||
"about": "පිළිබඳව",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"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": {}
|
||||
"accept": "පිළිගන්න",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"account": "ගිණුම",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"addEmail": "වි-තැපෑල එකතු කරන්න",
|
||||
"@addEmail": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"admin": "පරිපාලක",
|
||||
"@admin": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"allChats": "සියලුම සංවාද",
|
||||
"@allChats": {
|
||||
"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": {}
|
||||
},
|
||||
"blockDevice": "උපාංගය අවහිර කරන්න",
|
||||
"@blockDevice": {
|
||||
"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": {}
|
||||
},
|
||||
"contactHasBeenInvitedToTheGroup": "සමූහය වෙත සබඳතාවයකට ආරාධනා කර ඇත",
|
||||
"@contactHasBeenInvitedToTheGroup": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"copy": "පිටපත්",
|
||||
"@copy": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"create": "සාදන්න",
|
||||
"@create": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"createNewGroup": "නව සමූහයක් සාදන්න",
|
||||
"@createNewGroup": {
|
||||
"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": {}
|
||||
},
|
||||
"people": "මිනිසුන්",
|
||||
"@people": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"removeDevice": "උපාංගය ඉවත්කරන්න",
|
||||
"@removeDevice": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"roomVersion": "කාමරයේ අනුවාදය",
|
||||
"@roomVersion": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"saveFile": "ගොනුව සුරකින්න",
|
||||
"@saveFile": {
|
||||
"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": {}
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,20 @@
|
||||
{
|
||||
"@@last_modified": "2021-08-14 12:41:09.826673",
|
||||
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
"@@last_modified": "2021-08-14 12:41:09.826673",
|
||||
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"accept": "ஏற்றுக்கொள்",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"about": "பற்றி",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
},
|
||||
"accept": "ஏற்றுக்கொள்",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"about": "பற்றி",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
BIN
assets/start_chat.png
Normal file
BIN
assets/start_chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
646
docs/index.html
646
docs/index.html
@ -1,565 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>FluffyChat - Official Website</title>
|
||||
<meta name="description" content="A cute and secure chatclient for the matrix protocol">
|
||||
<meta name="keywords"
|
||||
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">
|
||||
<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://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-->
|
||||
<meta charset="utf-8">
|
||||
<title>FluffyChat Official Website</title>
|
||||
<meta name="identifier-url" content="https://fluffychat.im" />
|
||||
<meta name="title" content="FluffyChat Official Website" />
|
||||
<meta name="description" content="The cutest messenger in the Matrix network" />
|
||||
<meta name="abstract" content="FluffyChat is the cutest messenger in the Matrix network" />
|
||||
<meta name="keywords" content="FluffyChat, Matrix, Flutter, App" />
|
||||
<meta name="author" content="Krille Fear" />
|
||||
<meta name="revisit-after" content="15" />
|
||||
<meta name="language" content="EN" />
|
||||
<meta name="robots" content="All" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.png">
|
||||
<link href="tailwind.css" rel="stylesheet">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: Zen Kurenaido;
|
||||
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>
|
||||
|
||||
</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="h-screen pb-14 bg-right bg-cover" style="background-image:url('bg.svg');">
|
||||
<!--Nav-->
|
||||
<div class="w-full container mx-auto p-6">
|
||||
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<a class="flex items-center no-underline hover:no-underline font-bold text-2xl lg:text-4xl" href="#">
|
||||
<img src="favicon.png" class="h-8 fill-current text-indigo-600 pr-2" /> <span
|
||||
style="color: #5625BA">Fluffy</span><span style="color: #41a2bc">Chat</span>
|
||||
</a>
|
||||
|
||||
<div class="flex w-1/2 justify-end content-center">
|
||||
<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://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://twitter.com/KrilleFear">
|
||||
<svg class="fill-current h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M30.063 7.313c-.813 1.125-1.75 2.125-2.875 2.938v.75c0 1.563-.188 3.125-.688 4.625a15.088 15.088 0 0 1-2.063 4.438c-.875 1.438-2 2.688-3.25 3.813a15.015 15.015 0 0 1-4.625 2.563c-1.813.688-3.75 1-5.75 1-3.25 0-6.188-.875-8.875-2.625.438.063.875.125 1.375.125 2.688 0 5.063-.875 7.188-2.5-1.25 0-2.375-.375-3.375-1.125s-1.688-1.688-2.063-2.875c.438.063.813.125 1.125.125.5 0 1-.063 1.5-.25-1.313-.25-2.438-.938-3.313-1.938a5.673 5.673 0 0 1-1.313-3.688v-.063c.813.438 1.688.688 2.625.688a5.228 5.228 0 0 1-1.875-2c-.5-.875-.688-1.813-.688-2.75 0-1.063.25-2.063.75-2.938 1.438 1.75 3.188 3.188 5.25 4.25s4.313 1.688 6.688 1.813a5.579 5.579 0 0 1 1.5-5.438c1.125-1.125 2.5-1.688 4.125-1.688s3.063.625 4.188 1.813a11.48 11.48 0 0 0 3.688-1.375c-.438 1.375-1.313 2.438-2.563 3.188 1.125-.125 2.188-.438 3.313-.875z">
|
||||
</path>
|
||||
</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="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"
|
||||
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
|
||||
</a><a href="https://f-droid.org/de/packages/chat.fluffy.fluffychat/"><img src="fdroid_button.png"
|
||||
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
|
||||
</a>
|
||||
<a href="https://fluffychat.im/web">
|
||||
<img src="browser-badge.png" class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||
<a href="https://snapcraft.io/fluffychat"><img
|
||||
src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg"
|
||||
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"
|
||||
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||
</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"
|
||||
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-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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
BIN
docs/kofi_button_dark.png
Normal file
BIN
docs/kofi_button_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@ -1,49 +1,188 @@
|
||||
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_test/flutter_test.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
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';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Integration Test', () {
|
||||
testWidgets('Test if the app starts', (WidgetTester tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
group(
|
||||
'Integration Test',
|
||||
() {
|
||||
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);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
try {
|
||||
await tester.waitFor(
|
||||
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
|
||||
try {
|
||||
await tester.tap(find.text('Login'));
|
||||
await tester.waitFor(find.byType(ChatView));
|
||||
await tester.enterText(find.byType(TextField).last, 'Test');
|
||||
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();
|
||||
} 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.enterText(inputs.last, Users.user1.password);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
});
|
||||
});
|
||||
await tester.waitFor(find.byType(TextField));
|
||||
await tester.enterText(find.byType(TextField).last, 'Test Space');
|
||||
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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
182
integration_test/extensions/default_flows.dart
Normal file
182
integration_test/extensions/default_flows.dart
Normal file
@ -0,0 +1,182 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
|
||||
import 'package:fluffychat/pages/settings_account/settings_account_view.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('Account'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Logout'),
|
||||
500,
|
||||
scrollable: find.descendant(
|
||||
of: find.byType(SettingsAccountView),
|
||||
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
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 {
|
||||
const Users._();
|
||||
|
||||
static final user1 = User(
|
||||
Platform.environment['USER1_NAME'] ?? 'alice',
|
||||
Platform.environment['USER1_PW'] ?? 'AliceInWonderland',
|
||||
static const user1 = User(
|
||||
String.fromEnvironment(
|
||||
'USER1_NAME',
|
||||
defaultValue: 'alice',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER1_PW',
|
||||
defaultValue: 'AliceInWonderland',
|
||||
),
|
||||
);
|
||||
static final user2 = User(
|
||||
Platform.environment['USER2_NAME'] ?? 'bob',
|
||||
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas',
|
||||
static const user2 = User(
|
||||
String.fromEnvironment(
|
||||
'USER2_NAME',
|
||||
defaultValue: 'bob',
|
||||
),
|
||||
String.fromEnvironment(
|
||||
'USER2_PW',
|
||||
defaultValue: 'JoWirSchaffenDas',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,5 +30,7 @@ class User {
|
||||
const User(this.name, this.password);
|
||||
}
|
||||
|
||||
final homeserver =
|
||||
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}';
|
||||
const homeserver = 'http://${const String.fromEnvironment(
|
||||
'HOMESERVER',
|
||||
defaultValue: 'localhost',
|
||||
)}';
|
||||
|
||||
@ -24,6 +24,8 @@ abstract class AppConfig {
|
||||
static String get privacyUrl => _privacyUrl;
|
||||
static const String enablePushTutorial =
|
||||
'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services';
|
||||
static const String encryptionTutorial =
|
||||
'https://gitlab.com/famedly/fluffychat/-/wikis/How-to-use-end-to-end-encryption-in-FluffyChat';
|
||||
static const String appId = 'im.fluffychat.FluffyChat';
|
||||
static const String appOpenUrlScheme = 'im.fluffychat';
|
||||
static String _webBaseUrl = 'https://fluffychat.im/web';
|
||||
|
||||
@ -94,6 +94,13 @@ class AppRoutes {
|
||||
VWidget(
|
||||
path: '/archive',
|
||||
widget: const Archive(),
|
||||
stackedRoutes: [
|
||||
VWidget(
|
||||
path: ':roomid',
|
||||
widget: const Chat(),
|
||||
buildTransition: _dynamicTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
path: '/newprivatechat',
|
||||
@ -220,13 +227,25 @@ class AppRoutes {
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
VNester(
|
||||
path: '/archive',
|
||||
widget: const TwoColumnLayout(
|
||||
mainView: Archive(),
|
||||
sideView: EmptyPage(),
|
||||
widgetBuilder: (child) => TwoColumnLayout(
|
||||
mainView: const Archive(),
|
||||
sideView: child,
|
||||
),
|
||||
buildTransition: _fadeTransition,
|
||||
nestedRoutes: [
|
||||
VWidget(
|
||||
path: '',
|
||||
widget: const EmptyPage(),
|
||||
buildTransition: _dynamicTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: ':roomid',
|
||||
widget: const Chat(),
|
||||
buildTransition: _dynamicTransition,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -8,7 +8,6 @@ abstract class SettingKeys {
|
||||
static const String showDirectChatsInSpaces =
|
||||
'chat.fluffy.showDirectChatsInSpaces';
|
||||
static const String separateChatTypes = 'chat.fluffy.separateChatTypes';
|
||||
static const String chatColor = 'chat.fluffy.chat_color';
|
||||
static const String sentry = 'sentry';
|
||||
static const String theme = 'theme';
|
||||
static const String amoledEnabled = 'amoled_enabled';
|
||||
|
||||
@ -38,15 +38,15 @@ abstract class FluffyThemes {
|
||||
subtitle2: fallbackTextStyle,
|
||||
);
|
||||
|
||||
static ThemeData buildTheme(Brightness brightness,
|
||||
[ColorScheme? colorScheme]) =>
|
||||
static const Duration animationDuration = Duration(milliseconds: 250);
|
||||
static const Curve animationCurve = Curves.easeInOut;
|
||||
|
||||
static ThemeData buildTheme(Brightness brightness, [Color? seed]) =>
|
||||
ThemeData(
|
||||
visualDensity: VisualDensity.standard,
|
||||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
colorSchemeSeed: AppConfig.colorSchemeSeed ??
|
||||
colorScheme?.primary ??
|
||||
AppConfig.chatColor,
|
||||
colorSchemeSeed: seed ?? AppConfig.colorSchemeSeed,
|
||||
textTheme: PlatformInfos.isDesktop
|
||||
? brightness == Brightness.light
|
||||
? Typography.material2018().black.merge(fallbackTextTheme)
|
||||
@ -58,8 +58,11 @@ abstract class FluffyThemes {
|
||||
dividerColor: brightness == Brightness.light
|
||||
? Colors.blueGrey.shade50
|
||||
: Colors.blueGrey.shade900,
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: InputBorder.none,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
filled: true,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
@ -72,6 +75,20 @@ abstract class FluffyThemes {
|
||||
statusBarBrightness: brightness,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
||||
@ -12,12 +12,12 @@ import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/add_story/add_story_view.dart';
|
||||
import 'package:fluffychat/pages/add_story/invite_story_page.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'package:fluffychat/utils/resize_image.dart';
|
||||
import 'package:fluffychat/utils/story_theme_data.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
|
||||
class AddStoryPage extends StatefulWidget {
|
||||
const AddStoryPage({Key? key}) : super(key: key);
|
||||
|
||||
@ -6,7 +6,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
||||
@ -21,11 +21,9 @@ class ArchiveController extends State<Archive> {
|
||||
Future<List<Room>> getArchive(BuildContext context) async {
|
||||
final archive = this.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 {
|
||||
final archive = this.archive;
|
||||
if (archive == null) return;
|
||||
|
||||
@ -21,10 +21,14 @@ class ArchiveView extends StatelessWidget {
|
||||
leading: const BackButton(),
|
||||
title: Text(L10n.of(context)!.archive),
|
||||
actions: [
|
||||
if (snapshot.hasData && archive != null && archive!.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: controller.forgetAllAction,
|
||||
child: Text(L10n.of(context)!.clearArchive),
|
||||
if (snapshot.data?.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextButton.icon(
|
||||
onPressed: controller.forgetAllAction,
|
||||
label: Text(L10n.of(context)!.clearArchive),
|
||||
icon: const Icon(Icons.cleaning_services_outlined),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -50,7 +54,6 @@ class ArchiveView extends StatelessWidget {
|
||||
itemCount: archive!.length,
|
||||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||
archive![i],
|
||||
onForget: controller.forgetAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -139,6 +139,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
minLines: 4,
|
||||
maxLines: 4,
|
||||
readOnly: true,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
controller: TextEditingController(text: key),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@ -254,16 +255,22 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
||||
),
|
||||
const Divider(height: 32),
|
||||
TextField(
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
minLines: 2,
|
||||
maxLines: 2,
|
||||
autocorrect: false,
|
||||
readOnly: _recoveryKeyInputLoading,
|
||||
autofillHints: _recoveryKeyInputLoading
|
||||
? null
|
||||
: [AutofillHints.password],
|
||||
controller: _recoveryKeyTextEditingController,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Abc123 Def456',
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.fontFamily),
|
||||
labelText: L10n.of(context)!.recoveryKey,
|
||||
errorText: _recoveryKeyInputError,
|
||||
),
|
||||
|
||||
@ -16,19 +16,21 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:record/record.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat/recording_dialog.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/ios_badge_client_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.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/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/account_bundles.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'send_file_dialog.dart';
|
||||
import 'send_location_dialog.dart';
|
||||
import 'sticker_picker_dialog.dart';
|
||||
@ -132,6 +134,51 @@ class ChatController extends State<Chat> {
|
||||
|
||||
bool showEmojiPicker = false;
|
||||
|
||||
bool get isLeftDMRoom {
|
||||
final room = this.room;
|
||||
final userId = room?.directChatMatrixID;
|
||||
if (room == null || userId == null) return false;
|
||||
return room.isDirectChat &&
|
||||
room.unsafeGetUserFromMemoryOrFallback(userId).membership ==
|
||||
Membership.leave;
|
||||
}
|
||||
|
||||
void recreateChat() async {
|
||||
final room = this.room;
|
||||
final userId = room?.directChatMatrixID;
|
||||
if (room == null || 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 room = this.room;
|
||||
if (room == null) {
|
||||
throw Exception(
|
||||
'Leave room button clicked while room is null. This should not be possible from the UI!');
|
||||
}
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.leave,
|
||||
);
|
||||
if (success.error != null) return;
|
||||
VRouter.of(context).to('/rooms');
|
||||
}
|
||||
|
||||
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
|
||||
|
||||
void requestHistory() async {
|
||||
@ -172,10 +219,20 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
}
|
||||
|
||||
void _loadDraft() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final draft = prefs.getString('draft_$roomId');
|
||||
if (draft != null && draft.isNotEmpty) {
|
||||
sendController.text = draft;
|
||||
setState(() => inputText = draft);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
scrollController.addListener(_updateScrollController);
|
||||
inputFocus.addListener(_inputFocusListener);
|
||||
_loadDraft();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -257,6 +314,9 @@ class ChatController extends State<Chat> {
|
||||
|
||||
Future<void> send() async {
|
||||
if (sendController.text.trim().isEmpty) return;
|
||||
_storeInputTimeoutTimer?.cancel();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.remove('draft_$roomId');
|
||||
var parseCommands = true;
|
||||
|
||||
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
|
||||
@ -377,9 +437,8 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
|
||||
void sendStickerAction() async {
|
||||
final sticker = await showModalBottomSheet<ImagePackImageContent>(
|
||||
final sticker = await showAdaptiveBottomSheet<ImagePackImageContent>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => StickerPickerDialog(room: room!),
|
||||
);
|
||||
if (sticker == null) return;
|
||||
@ -413,6 +472,7 @@ class ChatController extends State<Chat> {
|
||||
final result = await showDialog<RecordingResult>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
barrierDismissible: false,
|
||||
builder: (c) => const RecordingDialog(),
|
||||
);
|
||||
if (result == null) return;
|
||||
@ -584,6 +644,7 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
|
||||
bool get canRedactSelectedEvents {
|
||||
if (isArchived) return false;
|
||||
final clients = matrix!.currentBundle;
|
||||
for (final event in selectedEvents) {
|
||||
if (event.canRedact == false &&
|
||||
@ -593,7 +654,9 @@ class ChatController extends State<Chat> {
|
||||
}
|
||||
|
||||
bool get canEditSelectedEvents {
|
||||
if (selectedEvents.length != 1 || !selectedEvents.first.status.isSent) {
|
||||
if (isArchived ||
|
||||
selectedEvents.length != 1 ||
|
||||
!selectedEvents.first.status.isSent) {
|
||||
return false;
|
||||
}
|
||||
return currentRoomBundle
|
||||
@ -711,6 +774,15 @@ class ChatController extends State<Chat> {
|
||||
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) {
|
||||
if (emoji == null) return;
|
||||
final text = sendController.text;
|
||||
@ -919,7 +991,15 @@ class ChatController extends State<Chat> {
|
||||
);
|
||||
}
|
||||
|
||||
Timer? _storeInputTimeoutTimer;
|
||||
static const Duration _storeInputTimeout = Duration(milliseconds: 500);
|
||||
|
||||
void onInputBarChanged(String text) {
|
||||
_storeInputTimeoutTimer?.cancel();
|
||||
_storeInputTimeoutTimer = Timer(_storeInputTimeout, () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('draft_$roomId', text);
|
||||
});
|
||||
setReadMarker();
|
||||
if (text.endsWith(' ') && matrix!.hasComplexBundles) {
|
||||
final clients = currentRoomBundle;
|
||||
@ -954,6 +1034,9 @@ class ChatController extends State<Chat> {
|
||||
setState(() => inputText = text);
|
||||
}
|
||||
|
||||
bool get isArchived =>
|
||||
{Membership.leave, Membership.ban}.contains(room?.membership);
|
||||
|
||||
void showEventInfo([Event? event]) =>
|
||||
(event ?? selectedEvents.single).showInfoDialog(context);
|
||||
|
||||
|
||||
@ -5,7 +5,8 @@ import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class ChatAppBarTitle extends StatelessWidget {
|
||||
@ -26,7 +27,7 @@ class ChatAppBarTitle extends StatelessWidget {
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onTap: directChatMatrixID != null
|
||||
? () => showModalBottomSheet(
|
||||
? () => showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: room
|
||||
@ -36,7 +37,10 @@ class ChatAppBarTitle extends StatelessWidget {
|
||||
'${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(
|
||||
children: [
|
||||
Hero(
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'chat.dart';
|
||||
|
||||
class ChatEmojiPicker extends StatelessWidget {
|
||||
@ -11,7 +12,8 @@ class ChatEmojiPicker extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
|
||||
@ -10,7 +10,8 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
||||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class ChatEventList extends StatelessWidget {
|
||||
@ -89,7 +90,7 @@ class ChatEventList extends StatelessWidget {
|
||||
onSwipe: (direction) =>
|
||||
controller.replyAction(replyTo: event),
|
||||
onInfoTab: controller.showEventInfo,
|
||||
onAvatarTab: (Event event) => showModalBottomSheet(
|
||||
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: event.senderFromMemoryOrFallback,
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../config/themes.dart';
|
||||
import '../../widgets/m2_popup_menu_button.dart';
|
||||
import 'chat.dart';
|
||||
import 'input_bar.dart';
|
||||
@ -84,7 +85,8 @@ class ChatInputRow extends StatelessWidget {
|
||||
controller.onAddPopupMenuButtonSelected('file'),
|
||||
helpLabel: L10n.of(context)!.sendFile,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: 56,
|
||||
width: controller.inputText.isEmpty ? 56 : 0,
|
||||
alignment: Alignment.center,
|
||||
|
||||
@ -108,6 +108,20 @@ class ChatView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
];
|
||||
} else if (controller.isArchived) {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextButton.icon(
|
||||
onPressed: controller.forgetRoom,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
icon: const Icon(Icons.delete_forever_outlined),
|
||||
label: Text(L10n.of(context)!.delete),
|
||||
),
|
||||
)
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
if (Matrix.of(context).voipPlugin != null &&
|
||||
@ -145,6 +159,7 @@ class ChatView extends StatelessWidget {
|
||||
context: context, future: () => controller.room!.join());
|
||||
}
|
||||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
@ -213,6 +228,20 @@ class ChatView extends StatelessWidget {
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
)
|
||||
else
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
colors: [
|
||||
colorScheme.primaryContainer.withAlpha(64),
|
||||
colorScheme.secondaryContainer.withAlpha(64),
|
||||
colorScheme.tertiaryContainer.withAlpha(64),
|
||||
colorScheme.primaryContainer.withAlpha(64),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
@ -262,16 +291,53 @@ class ChatView extends StatelessWidget {
|
||||
Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
child: controller.isLeftDMRoom
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(16),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(16),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.chat_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -1,93 +1,46 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class EncryptionButton extends StatefulWidget {
|
||||
class EncryptionButton extends StatelessWidget {
|
||||
final Room room;
|
||||
const EncryptionButton(this.room, {Key? key}) : super(key: key);
|
||||
@override
|
||||
EncryptionButtonState createState() => EncryptionButtonState();
|
||||
}
|
||||
|
||||
class EncryptionButtonState extends State<EncryptionButton> {
|
||||
StreamSubscription? _onSyncSub;
|
||||
|
||||
void _enableEncryptionAction() async {
|
||||
if (widget.room.encrypted) {
|
||||
VRouter.of(context).toSegments(['rooms', widget.room.id, 'encryption']);
|
||||
return;
|
||||
}
|
||||
if (widget.room.joinRules == JoinRules.public) {
|
||||
await showOkAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
message: L10n.of(context)!.noEncryptionForPublicRooms,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.enableEncryption,
|
||||
message: widget.room.client.encryptionEnabled
|
||||
? L10n.of(context)!.enableEncryptionWarning
|
||||
: L10n.of(context)!.needPantalaimonWarning,
|
||||
okLabel: L10n.of(context)!.yes,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
) ==
|
||||
OkCancelResult.ok) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.room.enableEncryption(),
|
||||
);
|
||||
// we want to enable the lock icon
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_onSyncSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.room.encrypted) {
|
||||
_onSyncSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null)
|
||||
.listen((s) => setState(() {}));
|
||||
}
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: widget.room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: widget.room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
widget.room.encrypted
|
||||
? Icons.lock_outlined
|
||||
: Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: widget.room.joinRules != JoinRules.public &&
|
||||
!widget.room.encrypted
|
||||
? Colors.red
|
||||
: snapshot.data == EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null),
|
||||
onPressed: _enableEncryptionAction,
|
||||
));
|
||||
return StreamBuilder<SyncUpdate>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
room.encrypted
|
||||
? Icons.lock_outlined
|
||||
: Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: room.joinRules != JoinRules.public &&
|
||||
!room.encrypted
|
||||
? Colors.red
|
||||
: room.joinRules != JoinRules.public &&
|
||||
snapshot.data ==
|
||||
EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', room.id, 'encryption']),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
extension EventInfoDialogExtension on Event {
|
||||
void showInfoDialog(BuildContext context) => showModalBottomSheet(
|
||||
void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EventInfoDialog(l10n: L10n.of(context)!, event: this),
|
||||
|
||||
@ -8,8 +8,9 @@ import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import '../../../utils/matrix_sdk_extensions.dart/event_extension.dart';
|
||||
import '../../../utils/matrix_sdk_extensions/event_extension.dart';
|
||||
|
||||
class AudioPlayerWidget extends StatefulWidget {
|
||||
final Color color;
|
||||
@ -101,6 +102,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
||||
}
|
||||
|
||||
onAudioPositionChanged ??= audioPlayer.positionStream.listen((state) {
|
||||
if (maxPosition <= 0) return;
|
||||
setState(() {
|
||||
statusText =
|
||||
'${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}';
|
||||
@ -109,9 +111,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
||||
.round();
|
||||
});
|
||||
});
|
||||
onDurationChanged ??= audioPlayer.durationStream.listen((max) => max == null
|
||||
? null
|
||||
: setState(() => maxPosition = max.inMilliseconds.toDouble()));
|
||||
onDurationChanged ??= audioPlayer.durationStream.listen((max) {
|
||||
if (max == null || max == Duration.zero) return;
|
||||
setState(() => maxPosition = max.inMilliseconds.toDouble());
|
||||
});
|
||||
onPlayerStateChanged ??=
|
||||
audioPlayer.playingStream.listen((_) => setState(() {}));
|
||||
audioPlayer.setFilePath(audioFile!.path);
|
||||
@ -169,10 +172,11 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
final statusText = this.statusText ??= _durationString ?? '00:00';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
padding: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
|
||||
@ -17,11 +17,11 @@ class CuteContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CuteContentState extends State<CuteContent> {
|
||||
static final List<OverlayEntry> overlays = [];
|
||||
static bool _isOverlayShown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (AppConfig.autoplayImages && overlays.isEmpty) {
|
||||
if (AppConfig.autoplayImages && !_isOverlayShown) {
|
||||
addOverlay();
|
||||
}
|
||||
super.initState();
|
||||
@ -55,52 +55,21 @@ class _CuteContentState extends State<CuteContent> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget overlayBuilder(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final position = Size(
|
||||
Random().nextInt(constraints.maxWidth.round() - 64).toDouble(),
|
||||
Random().nextInt(constraints.maxHeight.round() - 64).toDouble());
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: position.height,
|
||||
left: position.width,
|
||||
bottom: constraints.maxHeight - 64 - position.height,
|
||||
right: constraints.maxWidth - 64 - position.width),
|
||||
child: SizedBox.square(
|
||||
dimension: 64,
|
||||
child: GestureDetector(
|
||||
onTap: removeOverlay,
|
||||
child: Text(
|
||||
widget.event.text,
|
||||
style: const TextStyle(fontSize: 48),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addOverlay() async {
|
||||
_isOverlayShown = true;
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final overlay = OverlayEntry(
|
||||
builder: overlayBuilder,
|
||||
);
|
||||
Overlay.of(context)?.insert(overlay);
|
||||
|
||||
Future.delayed(Duration(seconds: Random().nextInt(35))).then((_) {
|
||||
overlay.remove();
|
||||
overlays.remove(overlay);
|
||||
});
|
||||
overlays.add(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
void removeOverlay() {
|
||||
if (overlays.isEmpty) return;
|
||||
final overlay = overlays.removeLast();
|
||||
overlay.remove();
|
||||
OverlayEntry? overlay;
|
||||
overlay = OverlayEntry(
|
||||
builder: (context) => CuteEventOverlay(
|
||||
emoji: widget.event.text,
|
||||
onAnimationEnd: () {
|
||||
_isOverlayShown = false;
|
||||
overlay?.remove();
|
||||
},
|
||||
),
|
||||
);
|
||||
Overlay.of(context)?.insert(overlay);
|
||||
}
|
||||
|
||||
generateLabel(User? user) {
|
||||
@ -126,3 +95,101 @@ class _CuteContentState extends State<CuteContent> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CuteEventOverlay extends StatefulWidget {
|
||||
final String emoji;
|
||||
final VoidCallback onAnimationEnd;
|
||||
|
||||
const CuteEventOverlay({
|
||||
Key? key,
|
||||
required this.emoji,
|
||||
required this.onAnimationEnd,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CuteEventOverlay> createState() => _CuteEventOverlayState();
|
||||
}
|
||||
|
||||
class _CuteEventOverlayState extends State<CuteEventOverlay>
|
||||
with TickerProviderStateMixin {
|
||||
final List<Size> items = List.generate(
|
||||
50,
|
||||
(index) => Size(
|
||||
Random().nextDouble(),
|
||||
4 + (Random().nextDouble() * 4),
|
||||
),
|
||||
);
|
||||
|
||||
AnimationController? controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
vsync: this,
|
||||
);
|
||||
controller?.forward();
|
||||
controller?.addStatusListener(_hideOverlay);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller!,
|
||||
builder: (context, _) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth - _CuteOverlayContent.size;
|
||||
final height = constraints.maxHeight + _CuteOverlayContent.size;
|
||||
return SizedBox(
|
||||
height: constraints.maxHeight,
|
||||
width: constraints.maxWidth,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
fit: StackFit.expand,
|
||||
children: items
|
||||
.map(
|
||||
(position) => Positioned(
|
||||
left: position.width * width,
|
||||
bottom: (height *
|
||||
.25 *
|
||||
position.height *
|
||||
(controller?.value ?? 0)) -
|
||||
_CuteOverlayContent.size,
|
||||
child: _CuteOverlayContent(
|
||||
emoji: widget.emoji,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _hideOverlay(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
widget.onAnimationEnd.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CuteOverlayContent extends StatelessWidget {
|
||||
static const double size = 64.0;
|
||||
final String emoji;
|
||||
|
||||
const _CuteOverlayContent({Key? key, required this.emoji}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 48),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../config/setting_keys.dart';
|
||||
import '../../../pages/image_viewer/image_viewer.dart';
|
||||
import '../../../utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../../../utils/url_launcher.dart';
|
||||
|
||||
class HtmlMessage extends StatelessWidget {
|
||||
|
||||
@ -85,7 +85,7 @@ class ImageBubble extends StatelessWidget {
|
||||
child: Hero(
|
||||
tag: event.eventId,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
duration: const Duration(seconds: 1),
|
||||
child: Container(
|
||||
constraints: maxSize
|
||||
? BoxConstraints(
|
||||
|
||||
@ -35,15 +35,15 @@ class MapBubble extends StatelessWidget {
|
||||
center: LatLng(latitude, longitude),
|
||||
zoom: zoom,
|
||||
),
|
||||
layers: [
|
||||
TileLayerOptions(
|
||||
children: [
|
||||
TileLayer(
|
||||
maxZoom: 20,
|
||||
minZoom: 0,
|
||||
urlTemplate:
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
subdomains: ['a', 'b', 'c'],
|
||||
subdomains: const ['a', 'b', 'c'],
|
||||
),
|
||||
MarkerLayerOptions(
|
||||
MarkerLayer(
|
||||
rotate: true,
|
||||
markers: [
|
||||
Marker(
|
||||
|
||||
@ -100,9 +100,13 @@ class Message extends StatelessWidget {
|
||||
final noBubble = {
|
||||
MessageTypes.Video,
|
||||
MessageTypes.Image,
|
||||
MessageTypes.Sticker,
|
||||
MessageTypes.Sticker
|
||||
}.contains(event.messageType) &&
|
||||
!event.redacted;
|
||||
final noPadding = {
|
||||
MessageTypes.File,
|
||||
MessageTypes.Audio,
|
||||
}.contains(event.messageType);
|
||||
|
||||
if (ownMessage) {
|
||||
color = displayEvent.status.isError
|
||||
@ -187,7 +191,7 @@ class Message extends StatelessWidget {
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
padding: noBubble
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
|
||||
constraints: const BoxConstraints(
|
||||
|
||||
@ -5,8 +5,9 @@ import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix_link_text/link_text.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
@ -52,7 +53,7 @@ class MessageContent extends StatelessWidget {
|
||||
}
|
||||
event.requestKey();
|
||||
final sender = event.senderFromMemoryOrFallback;
|
||||
await showModalBottomSheet(
|
||||
await showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
@ -112,7 +113,12 @@ class MessageContent extends StatelessWidget {
|
||||
case CuteEventContent.eventType:
|
||||
return CuteContent(event);
|
||||
case MessageTypes.Audio:
|
||||
if (PlatformInfos.isMobile || PlatformInfos.isMacOS) {
|
||||
if (PlatformInfos.isMobile || PlatformInfos.isMacOS
|
||||
// || latformInfos.isLinux
|
||||
// disabled until
|
||||
// https://github.com/bleonard252/just_audio_mpv/issues/3 is
|
||||
// fixed
|
||||
) {
|
||||
return AudioPlayerWidget(
|
||||
event,
|
||||
color: textColor,
|
||||
@ -205,7 +211,7 @@ class MessageContent extends StatelessWidget {
|
||||
default:
|
||||
if (event.redacted) {
|
||||
return FutureBuilder<User?>(
|
||||
future: event.fetchSenderUser(),
|
||||
future: event.redactedBecause?.fetchSenderUser(),
|
||||
builder: (context, snapshot) {
|
||||
return _ButtonContent(
|
||||
label: L10n.of(context)!.redactedAnEvent(snapshot.data
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
|
||||
class MessageDownloadContent extends StatelessWidget {
|
||||
final Event event;
|
||||
@ -30,45 +28,47 @@ class MessageDownloadContent extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.file_download_outlined,
|
||||
color: textColor,
|
||||
),
|
||||
title: Text(
|
||||
filename,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
trailing: PlatformInfos.isAndroid
|
||||
? IconButton(
|
||||
onPressed: () => event.shareFile(context),
|
||||
tooltip: L10n.of(context)!.share,
|
||||
icon: Icon(Icons.adaptive.share_outlined),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
filetype,
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_download_outlined,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (sizeString != null)
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
sizeString,
|
||||
filename,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
filetype,
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (sizeString != null)
|
||||
Text(
|
||||
sizeString,
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(150),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'html_message.dart';
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class StateMessage extends StatelessWidget {
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
|
||||
class EventVideoPlayer extends StatefulWidget {
|
||||
final Event event;
|
||||
|
||||
@ -9,7 +9,7 @@ import 'package:matrix_link_text/link_text.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
|
||||
class PinnedEvents extends StatelessWidget {
|
||||
@ -21,10 +21,11 @@ class PinnedEvents extends StatelessWidget {
|
||||
BuildContext context, List<Event?> events) async {
|
||||
final eventId = events.length == 1
|
||||
? events.single?.eventId
|
||||
: await showModalActionSheet<String>(
|
||||
: await showConfirmationDialog<String>(
|
||||
context: context,
|
||||
title: L10n.of(context)!.pinMessage,
|
||||
actions: events
|
||||
.map((event) => SheetAction(
|
||||
.map((event) => AlertDialogAction(
|
||||
key: event?.eventId ?? '',
|
||||
label: event?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/app_emojis.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import '../../config/themes.dart';
|
||||
|
||||
class ReactionsPicker extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
@ -20,7 +21,8 @@ class ReactionsPicker extends StatelessWidget {
|
||||
controller.room!.canSendDefaultMessages &&
|
||||
controller.selectedEvents.isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: (display) ? 56 : 0,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
|
||||
@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import '../../config/themes.dart';
|
||||
import 'chat.dart';
|
||||
import 'events/reply_content.dart';
|
||||
|
||||
@ -14,7 +15,8 @@ class ReplyDisplay extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: controller.editEvent != null || controller.replyEvent != null
|
||||
? 56
|
||||
: 0,
|
||||
|
||||
@ -22,8 +22,9 @@ class SeenByRow extends StatelessWidget {
|
||||
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
|
||||
height: seenByUsers.isEmpty ? 0 : 24,
|
||||
duration: seenByUsers.isEmpty
|
||||
? const Duration(milliseconds: 0)
|
||||
: const Duration(milliseconds: 300),
|
||||
? Duration.zero
|
||||
: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
alignment: controller.timeline!.events.isNotEmpty &&
|
||||
controller.timeline!.events.first.senderId ==
|
||||
Matrix.of(context).client.userID
|
||||
|
||||
@ -24,8 +24,8 @@ class TypingIndicators extends StatelessWidget {
|
||||
constraints:
|
||||
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
|
||||
height: typingUsers.isEmpty ? 0 : Avatar.defaultSize + bottomPadding,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.bounceInOut,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
alignment: controller.timeline!.events.isNotEmpty &&
|
||||
controller.timeline!.events.first.senderId ==
|
||||
Matrix.of(context).client.userID
|
||||
|
||||
@ -11,7 +11,7 @@ import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_details/chat_details_view.dart';
|
||||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/content_banner.dart';
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import '../../widgets/avatar.dart';
|
||||
import '../user_bottom_sheet/user_bottom_sheet.dart';
|
||||
|
||||
@ -28,7 +29,7 @@ class ParticipantListItem extends StatelessWidget {
|
||||
return Opacity(
|
||||
opacity: user.membership == Membership.join ? 1 : 0.5,
|
||||
child: ListTile(
|
||||
onTap: () => showModalBottomSheet(
|
||||
onTap: () => showAdaptiveBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => UserBottomSheet(
|
||||
user: user,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
@ -19,49 +22,68 @@ class ChatEncryptionSettings extends StatefulWidget {
|
||||
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
||||
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||
|
||||
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
|
||||
|
||||
Future<void> unblock(DeviceKeys key) async {
|
||||
if (key.blocked) {
|
||||
await key.setBlocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onSelected(
|
||||
BuildContext context, String action, DeviceKeys key) async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||
switch (action) {
|
||||
case 'verify':
|
||||
await unblock(key);
|
||||
final req = key.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'verify_user':
|
||||
await unblock(key);
|
||||
final req =
|
||||
await room!.client.userDeviceKeys[key.userId]!.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'block':
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() {});
|
||||
break;
|
||||
case 'unblock':
|
||||
await unblock(key);
|
||||
setState(() {});
|
||||
break;
|
||||
void enableEncryption(_) async {
|
||||
if (room.encrypted) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.disableEncryptionWarning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (room.joinRules == JoinRules.public) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.noEncryptionForPublicRooms,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!room.canChangeStateEvent(EventTypes.Encryption)) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.noPermission,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
message: L10n.of(context)!.enableEncryptionWarning,
|
||||
okLabel: L10n.of(context)!.yes,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.enableEncryption(),
|
||||
);
|
||||
}
|
||||
|
||||
void startVerification() async {
|
||||
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
|
||||
.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
}
|
||||
|
||||
void toggleDeviceKey(DeviceKeys key) {
|
||||
setState(() {
|
||||
key.setBlocked(!key.blocked);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/device_extension.dart';
|
||||
import '../../widgets/m2_popup_menu_button.dart';
|
||||
|
||||
class ChatEncryptionSettingsView extends StatelessWidget {
|
||||
final ChatEncryptionSettingsController controller;
|
||||
@ -19,184 +16,149 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.roomId!)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () =>
|
||||
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.tapOnDeviceToVerify),
|
||||
elevation: 0,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.deviceVerifyDescription),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
foregroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: const Icon(Icons.lock),
|
||||
final room = controller.room;
|
||||
return StreamBuilder<Object>(
|
||||
stream: room.client.onSync.stream.where(
|
||||
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
|
||||
builder: (context, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.endToEndEncryption),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.encryptionTutorial),
|
||||
child: Text(L10n.of(context)!.help),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (i == 0 ||
|
||||
deviceKeys[i].userId !=
|
||||
deviceKeys[i - 1].userId) ...{
|
||||
const Divider(height: 1, thickness: 1),
|
||||
M2PopupMenuButton(
|
||||
onSelected: (dynamic action) => controller
|
||||
.onSelected(context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (room
|
||||
.client
|
||||
.userDeviceKeys[deviceKeys[i].userId]!
|
||||
.verified ==
|
||||
UserVerifiedStatus.unknown) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'verify_user',
|
||||
child: Text(L10n.of(context)!.verifyUser),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.avatarUrl,
|
||||
name: room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
title: Text(
|
||||
room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
subtitle: Text(
|
||||
deviceKeys[i].userId,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
M2PopupMenuButton(
|
||||
onSelected: (dynamic action) => controller
|
||||
.onSelected(context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (deviceKeys[i].blocked ||
|
||||
!deviceKeys[i].verified) {
|
||||
items.add(PopupMenuItem(
|
||||
value: deviceKeys[i].userId ==
|
||||
room.client.userID
|
||||
? 'verify'
|
||||
: 'verify_user',
|
||||
child: Text(L10n.of(context)!.verifyStart),
|
||||
));
|
||||
body: ListView(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
secondary: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.lock_outlined)),
|
||||
title: Text(L10n.of(context)!.encryptThisChat),
|
||||
value: room.encrypted,
|
||||
onChanged: controller.enableEncryption,
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/encryption.png',
|
||||
width: 212,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (room.isDirectChat)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.startVerification,
|
||||
icon: const Icon(Icons.verified_outlined),
|
||||
label: Text(L10n.of(context)!.verifyStart),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (room.encrypted) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.deviceKeys,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) =>
|
||||
FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
||||
);
|
||||
}
|
||||
if (deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'unblock',
|
||||
child:
|
||||
Text(L10n.of(context)!.unblockDevice),
|
||||
));
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
if (!deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'block',
|
||||
child: Text(L10n.of(context)!.blockDevice),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: deviceKeys[i].color,
|
||||
child: Icon(deviceKeys[i].icon),
|
||||
),
|
||||
title: Text(
|
||||
deviceKeys[i].displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
deviceKeys[i].deviceId!,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300),
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
SwitchListTile(
|
||||
value: !deviceKeys[i].blocked,
|
||||
activeColor: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
onChanged: (_) => controller
|
||||
.toggleDeviceKey(deviceKeys[i]),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
deviceKeys[i].deviceId ??
|
||||
L10n.of(context)!.unknownDevice,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0),
|
||||
child: Chip(
|
||||
label: Text(
|
||||
deviceKeys[i].userId,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].blocked
|
||||
? L10n.of(context)!.blocked
|
||||
: deviceKeys[i].verified
|
||||
? L10n.of(context)!.verified
|
||||
: L10n.of(context)!.unverified,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: deviceKeys[i].color,
|
||||
subtitle: Text(
|
||||
deviceKeys[i]
|
||||
.ed25519Key
|
||||
?.replaceAllMapped(
|
||||
RegExp(r'.{4}'),
|
||||
(s) => '${s.group(0)} ') ??
|
||||
L10n.of(context)!
|
||||
.unknownEncryptionAlgorithm,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
] else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.encryptionNotEnabled,
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeviceKeys {
|
||||
Color get color => blocked
|
||||
? Colors.red
|
||||
: verified
|
||||
? Colors.green
|
||||
: Colors.orange;
|
||||
}
|
||||
|
||||
@ -17,10 +17,10 @@ import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/utils/famedlysdk_store.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import '../../../utils/account_bundles.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
import '../../utils/voip/callkeep_manager.dart';
|
||||
import '../../widgets/fluffy_chat_app.dart';
|
||||
@ -55,6 +55,7 @@ enum ActiveFilter {
|
||||
|
||||
class ChatList extends StatefulWidget {
|
||||
static BuildContext? contextForVoip;
|
||||
|
||||
const ChatList({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -224,7 +225,7 @@ class ChatListController extends State<ChatList>
|
||||
|
||||
void onSearchEnter(String text) {
|
||||
if (text.isEmpty) {
|
||||
cancelSearch();
|
||||
cancelSearch(unfocus: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -235,12 +236,15 @@ class ChatListController extends State<ChatList>
|
||||
_coolDown = Timer(const Duration(milliseconds: 500), _search);
|
||||
}
|
||||
|
||||
void cancelSearch() => setState(() {
|
||||
searchController.clear();
|
||||
isSearchMode = false;
|
||||
roomSearchResult = userSearchResult = null;
|
||||
isSearching = false;
|
||||
});
|
||||
void cancelSearch({bool unfocus = true}) {
|
||||
setState(() {
|
||||
searchController.clear();
|
||||
isSearchMode = false;
|
||||
roomSearchResult = userSearchResult = null;
|
||||
isSearching = false;
|
||||
});
|
||||
if (unfocus) FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
|
||||
bool isTorBrowser = false;
|
||||
|
||||
@ -358,11 +362,13 @@ class ChatListController extends State<ChatList>
|
||||
_hackyWebRTCFixForWeb();
|
||||
CallKeepManager().initialize();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
searchServer = await Store().getItem(_serverStoreNamespace);
|
||||
if (mounted) {
|
||||
searchServer = await Store().getItem(_serverStoreNamespace);
|
||||
Matrix.of(context).backgroundPush?.setupPush();
|
||||
}
|
||||
});
|
||||
|
||||
_checkTorBrowser();
|
||||
Matrix.of(context).backgroundPush?.setupPush();
|
||||
_onSyncStatus =
|
||||
Matrix.of(context).client.onSyncStatus.stream.listen((status) {
|
||||
Logs().v('Sync Status: ${status.status.name}');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user