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