117 Commits

Author SHA1 Message Date
85d3a11030 Translated using Weblate (Galician)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/
2023-01-16 07:47:59 +01:00
bf084f1ccc chore: Remove unused dependency 2023-01-15 10:06:02 +01:00
4afb9a4790 chore: Follow up leave abandoned DM room 2023-01-15 10:05:54 +01:00
ddb7cc841b chore: add integration tests for spaces
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-01-14 10:46:18 +01:00
a1f60b7ff9 fix: Archive 2023-01-13 10:38:39 +00:00
8632154832 style: Redesign public room bottomsheets 2023-01-13 10:38:39 +00:00
1f71227221 Merge branch 'braid/less-tests' into 'main'
refactor: disable some redundant tests

See merge request famedly/fluffychat!1075
2023-01-13 09:45:09 +00:00
3d9e94f08d refactor: disable some redundant tests
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-01-13 10:26:43 +01:00
b76f270e24 Deleted translation using Weblate (Yue (yue_HK)) 2023-01-08 11:37:34 +01:00
8f9e1a9142 Merge branch 'krille/nicer-modal-bottom-sheet' into 'main'
style: New modal bottom sheets

See merge request famedly/fluffychat!1071
2023-01-08 10:35:31 +00:00
fbb68686ea style: New modal bottom sheets 2023-01-08 11:07:31 +01:00
9eee50dbae Translated using Weblate (Estonian)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/
2023-01-08 00:08:00 +01:00
3be35991b5 Added translation using Weblate (Yue (yue_HK)) 2023-01-08 00:00:41 +01:00
1aa0ea2cea Translated using Weblate (Indonesian)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2023-01-07 19:49:08 +01:00
a7dd62c721 Translated using Weblate (Dutch)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nl/
2023-01-07 19:49:08 +01:00
1542a4b66c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2023-01-07 19:49:07 +01:00
d39bcbafde Translated using Weblate (Turkish)
Currently translated at 100.0% (551 of 551 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2023-01-07 19:49:07 +01:00
8513d74cc1 refactor: Same animations everywhere in app 2023-01-07 09:22:31 +01:00
22abd54176 style: Animate in out search results 2023-01-07 09:14:14 +01:00
ba885ca69e Translated using Weblate (Galician)
Currently translated at 100.0% (550 of 550 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/
2023-01-06 08:54:29 +01:00
754b919531 feat: Nicer design for abandonded DM rooms 2023-01-06 08:54:17 +01:00
8fd2d3918c chore: Fix google services patch 2023-01-06 08:47:03 +01:00
d000f6e5a7 chore: Update dependencies 2023-01-05 18:21:13 +01:00
66858cdf12 Translated using Weblate (Indonesian)
Currently translated at 100.0% (550 of 550 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2023-01-04 23:48:23 +01:00
4fb1a76060 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (550 of 550 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2023-01-04 23:48:23 +01:00
de23cb0f5b Translated using Weblate (Turkish)
Currently translated at 100.0% (550 of 550 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2023-01-04 23:48:23 +01:00
cf7053f338 Translated using Weblate (Estonian)
Currently translated at 100.0% (550 of 550 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/
2023-01-04 23:48:22 +01:00
20e26b3747 Merge branch 'braid/integration-tests' into 'main'
chore: add integration tests

See merge request famedly/fluffychat!1062
2023-01-03 19:17:06 +00:00
ed075a35b6 chore: add integration tests
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-01-03 19:59:56 +01:00
6b3252b6ad docs: Update website 2023-01-03 19:13:03 +01:00
5e5132c290 chore: Change invite link textfield label 2023-01-03 18:33:58 +01:00
b4df8c129d design: More clear chat background and rounded popup menu 2023-01-03 18:29:03 +01:00
37bf943ac7 chore: Follow up dark mode color 2023-01-03 18:01:53 +01:00
264f36ea59 design: Nicer navigationrail 2023-01-03 18:00:56 +01:00
b894a4542a chore: Update flutter_map 2023-01-03 17:31:03 +01:00
6e9e3d05d2 chore: Remove unused translations 2023-01-03 17:23:09 +01:00
d0b32e44ce Merge branch 'main' into 'main'
feat: Bring back disabling the header bar on Linux desktop

See merge request famedly/fluffychat!1064
2023-01-03 16:19:52 +00:00
caa3823c26 Translated using Weblate (German)
Currently translated at 100.0% (649 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/
2023-01-03 12:49:18 +01:00
df33df35da Translated using Weblate (Czech)
Currently translated at 85.6% (556 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/
2023-01-03 12:49:18 +01:00
63abbf403a Merge branch 'braid/web-build-job-permissions' into 'main'
fix: permission of web builds

See merge request famedly/fluffychat!1067
2023-01-03 07:54:41 +00:00
6ff4f480ac fix: permission of web builds
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2023-01-03 07:40:13 +01:00
fd152baa28 refactor: Stories header with futurebuilder 2023-01-02 17:12:24 +01:00
09a74bf3ee docs: More minimalistic website 2023-01-01 18:44:00 +01:00
5df709e12b docs: Remove twitter link 2023-01-01 12:10:38 +00:00
cba5fa2daf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (649 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2022-12-31 22:51:01 +01:00
cab78f3571 Translated using Weblate (Turkish)
Currently translated at 100.0% (649 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2022-12-31 22:51:01 +01:00
4cd501d00c feat: Bring back disabling the header bar on Linux desktop
(cherry picked from commit 5ca4b66157)
2022-12-31 22:43:36 +08:00
edfd8f36ab chore: Revert push channel changes 2022-12-31 15:14:38 +01:00
7635104505 Revert "fix: Android push notification follow-up"
This reverts commit b24a7d9510
2022-12-31 14:13:09 +00:00
835d97f439 chore: Follow up draft fix 2022-12-30 18:24:34 +01:00
c9f4904d99 refactor: Remove dart from folder name 2022-12-30 17:54:50 +01:00
bd5a6e5578 Translated using Weblate (Indonesian)
Currently translated at 100.0% (649 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2022-12-30 17:50:29 +01:00
29ec1735e9 Translated using Weblate (German)
Currently translated at 100.0% (649 of 649 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/
2022-12-30 17:50:29 +01:00
7436cb4aa8 chore: Minor lable fixes 2022-12-30 17:45:58 +01:00
ee3351f643 chore: Follow up fix 2022-12-30 16:27:44 +01:00
fff3dc9946 refactor: New private chat 2022-12-30 14:37:13 +01:00
1e1e591d27 feat: Store drafts 2022-12-30 13:32:52 +01:00
abb99df271 chore: Bump version 2022-12-30 13:12:20 +01:00
6616314d77 fix: Hide google services warning after marked 2022-12-30 13:09:21 +01:00
3cb7842a7b chore: Make audio player dialog not dismissable 2022-12-30 12:53:42 +01:00
77aca413fb chore: Remove deprecated share button 2022-12-30 12:52:29 +01:00
04f34a4301 chore: Remove deprecated share button 2022-12-30 12:49:17 +01:00
d3e3252de8 chore: Disable audio player on linux 2022-12-30 12:36:20 +01:00
ca203608fa feat: Add audio message support to linux 2022-12-30 10:51:26 +01:00
e9d02336e1 fix: File event design 2022-12-30 10:40:53 +01:00
b3ad9a3a70 chore: Minor design adjustments 2022-12-30 09:12:27 +01:00
d2f472e86c fix: Content banner 2022-12-30 09:04:46 +01:00
338331d6e1 fix: Correct redacted by username 2022-12-30 09:01:43 +01:00
b65357576c fix: Encryption button is orange in public rooms 2022-12-30 09:00:01 +01:00
99b0ee194c chore: Nicer new chat design 2022-12-30 08:59:00 +01:00
d930b569fc chore: Adjust onboarding design 2022-12-29 20:38:13 +01:00
7da70ebeba fix: Do not setup push on every app resume 2022-12-29 10:35:02 +01:00
54303ef635 chore: Follow up fix search bar 2022-12-29 10:33:13 +01:00
05285b46d8 chore: Follow up fix chat list 2022-12-29 10:26:01 +01:00
220dda715a chore: Remove broken arb file 2022-12-29 10:08:50 +01:00
a702a12c71 Added translation using Weblate (Yue (yue_HK)) 2022-12-28 23:47:12 +01:00
17f8eda6e4 Translated using Weblate (Indonesian)
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2022-12-28 08:41:47 +01:00
139d1f01ca Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/zh_Hans/
2022-12-28 08:41:47 +01:00
7b3cfe875f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2022-12-28 08:41:47 +01:00
3b455d7801 Translated using Weblate (Turkish)
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2022-12-28 08:41:47 +01:00
ed68fc55fc Translated using Weblate (French)
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fr/
2022-12-28 08:41:47 +01:00
108c620326 Translated using Weblate (Estonian)
Currently translated at 100.0% (647 of 647 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/
2022-12-28 08:41:47 +01:00
accd4b9a23 Merge branch 'braid/room-list-fixes' into 'main'
fix: minor issues in room list

See merge request famedly/fluffychat!1056
2022-12-28 07:41:40 +00:00
0a4f7c9d26 fix: minor issues in room list
- allow to discard focus of search field
- properly circle the search field's progress indicator
- always keep search sections visible in order to workaround annoying
  behavior: When quickly searching for a chat and one is fast at
clicking on a room, it often happens that server side results just drop
in at this moment and one clicks at the wrong item -> with a static
height as now set, this no longer happens.

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-28 08:35:06 +01:00
2512630172 Merge branch 'braid/android-push-follow-up' into 'main'
fix: Android push notification follow-up

See merge request famedly/fluffychat!1061
2022-12-28 07:35:02 +00:00
30b17beaa7 Merge branch 'braid/windows-olm' into 'main'
feat: include olm to Windows builds

See merge request famedly/fluffychat!1007
2022-12-28 07:16:47 +00:00
e368227780 feat: include olm to Windows builds
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-28 08:08:58 +01:00
f9e4b9356a Merge branch 'braid/matrix-version-bump' into 'main'
chore: bump matrix sdk

See merge request famedly/fluffychat!1060
2022-12-28 06:50:42 +00:00
b24a7d9510 fix: Android push notification follow-up
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-27 20:25:45 +01:00
6084d36ed2 chore: bump matrix sdk
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-27 14:38:22 +01:00
781a02cea7 Merge branch 'braid/cute-events' into 'main'
chore: improve cute events rendering

See merge request famedly/fluffychat!1054
2022-12-27 12:16:57 +00:00
fbb9f40f01 chore: Remove workaround of record package 2022-12-27 13:15:46 +01:00
c19946c184 chore: Follow up root navigator fix 2022-12-26 20:21:18 +01:00
2ba6e15e59 chore: Update dependencies 2022-12-26 19:22:36 +01:00
0052b6d42f chore: Use correct mono font 2022-12-26 19:20:52 +01:00
94b19cf6a6 chore: Nicer verification dialog 2022-12-26 18:30:18 +01:00
b1699cfa16 Translated using Weblate (Indonesian)
Currently translated at 100.0% (642 of 642 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/id/
2022-12-26 18:07:07 +01:00
c7dcceb7dd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (642 of 642 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/
2022-12-26 18:07:07 +01:00
496bb0dbd4 Translated using Weblate (Turkish)
Currently translated at 100.0% (642 of 642 strings)

Translation: FluffyChat/Translations
Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/tr/
2022-12-26 18:07:07 +01:00
1294479974 Merge branch 'krille/nicer-encryption-page' into 'main'
design: New encryption page

See merge request famedly/fluffychat!1059
2022-12-26 17:07:00 +00:00
ca7bf8bd0d design: New encryption page 2022-12-26 17:59:43 +01:00
8f89d539d0 chore: follow up fix 2022-12-26 16:03:14 +01:00
7cdeb98671 chore: Update sdk 2022-12-25 13:20:52 +01:00
bb74754851 chore: Follow up start chat label 2022-12-25 11:04:22 +01:00
4680a1c507 chore: Follow up homeserver picker input 2022-12-25 10:50:41 +01:00
51a5e7f9cc chore: Add new start chat image 2022-12-25 10:45:13 +01:00
c4f601f651 chore: improve cute events rendering
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-24 19:28:09 +01:00
f6c2fa8588 chore: Add gradient to chat background 2022-12-24 12:07:41 +01:00
576d46eb4c feat: Use Android system accent color 2022-12-24 11:48:48 +01:00
b21ab55451 fix: Libhandy windows 2022-12-24 09:37:12 +01:00
56b3297610 Merge branch 'braid/homeserver-appbar' into 'main'
fix: homeserver error text not visible in app bar

See merge request famedly/fluffychat!1042
2022-12-23 14:12:47 +00:00
43b408fe5e chore: Fix import sorting 2022-12-23 15:10:26 +01:00
12cc876b83 Merge branch 'improve-story' into 'main'
fix: Improve story page appearance

See merge request famedly/fluffychat!1051
2022-12-23 14:08:59 +00:00
1d53fccfe6 Merge branch 'fix-adaptive-icon' into 'main'
fix: Monochromatic icon rendering for Android 13+

See merge request famedly/fluffychat!1050
2022-12-23 14:07:53 +00:00
857eea428e fix: Improve story page appearance 2022-12-23 11:58:40 +07:00
715e98cae2 fix: Monochromatic icon rendering for Android 13+ 2022-12-23 10:54:40 +07:00
bb9410accb fix: homeserver error text not visible in app bar
Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
2022-12-02 10:12:44 +01:00
182 changed files with 69517 additions and 85546 deletions

3
.gitignore vendored
View File

@ -10,7 +10,6 @@
.buildlog/
.history
.svn/
lib/generated_plugin_registrant.dart
prime
# libolm package
@ -38,7 +37,6 @@ prime
/build/
# Web related
lib/generated_plugin_registrant.dart
docs/build/
docs/.jekyll-cache/
docs/_site/
@ -62,3 +60,4 @@ ios/Podfile.lock
/linux/out
/macos/out
.vs
olm

View File

@ -1,7 +1,9 @@
variables:
FLUTTER_VERSION: 3.3.9
image: cirrusci/flutter:${FLUTTER_VERSION}
image:
name: cirrusci/flutter:${FLUTTER_VERSION}
pull_policy: if-not-present
.shared_windows_runners:
tags:
@ -16,7 +18,7 @@ stages:
code_analyze:
stage: test
script: [./scripts/code_analyze.sh]
script: [ ./scripts/code_analyze.sh ]
artifacts:
reports:
codequality: code-quality-report.json
@ -26,13 +28,13 @@ code_analyze:
widget_test:
stage: test
script: [flutter test]
script: [ flutter test ]
tags:
- docker
- famedly
# the basic integration test configuration testing FLOSS builds on Synapse
.integration_test:
integration_test:
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/integration/stable:${FLUTTER_VERSION}
stage: test
services:
@ -49,15 +51,13 @@ widget_test:
FF_NETWORK_PER_BUILD: "true"
# Tell docker CLI how to talk to Docker daemon.
DOCKER_HOST: tcp://docker:2375/
# Use the overlayfs driver for improved performance.
DOCKER_DRIVER: overlay2
# Use the btrfs driver for improved performance.
DOCKER_DRIVER: btrfs
# Disable TLS since we're running inside local network.
DOCKER_TLS_CERTDIR: ""
HOMESERVER: "docker"
HOMESERVER: docker
before_script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- scripts/integration-prepare-alpine.sh
- scripts/integration-prepare-host.sh
# create test user environment variables
- source scripts/integration-create-environment-variables.sh
# create Synapse instance
@ -65,31 +65,60 @@ widget_test:
# properly set the homeserver IP and create test users
- scripts/integration-prepare-homeserver.sh
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- flutter pub get
- flutter test integration_test
timeout: 20m
- scrcpy --no-display --record video.mkv &
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
after_script:
- ffmpeg -i video.mkv -vf scale=iw/2:-2 -crf 40 -b:v 2000k -preset fast video.mp4 || true
timeout: 30m
retry: 2
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: always
artifacts:
when: always
paths:
- video.mp4
tags:
- docker
- famedly
# integration tests for Linux builds
### disabled because of Linux headless issues
.integration_test_linux:
extends: .integration_test
image: cirrusci/flutter:${FLUTTER_VERSION}
extends: integration_test
parallel:
matrix:
- HOMESERVER_IMPLEMENTATION:
- conduit
script:
- apk add cmake ninja gtk+3.0-dev clang pkgconf xz-dev libsecret-dev jsoncpp-dev
- apt-get update
- apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libsecret-1-dev libjsoncpp-dev
- flutter pub get
- flutter test integration_test -d linux
- flutter test integration_test -d linux --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
after_script: [ ]
artifacts:
# extending the default tests to test the Google-flavored builds
.integration_test_proprietary:
extends: .integration_test
integration_test_proprietary:
extends: integration_test
parallel:
matrix:
- HOMESERVER_IMPLEMENTATION:
- conduit
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
- git apply ./scripts/enable-android-google-services.patch
- flutter pub get
- flutter test integration_test
- scrcpy --no-display --record video.mkv &
- flutter test integration_test --dart-define=HOMESERVER=$HOMESERVER --dart-define=USER1_NAME=$USER1_NAME --dart-define=USER2_NAME=$USER2_NAME --dart-define=USER1_PW=$USER1_PW --dart-define=USER2_PW=$USER2_PW || ( sleep 10 && exit 1 )
.release_mode_launches:
release_mode_launches:
parallel:
matrix:
- FLAVOR:
@ -99,9 +128,9 @@ widget_test:
stage: test
before_script:
- |
if [ "$FLAVOR" == "proprietary" ]; then
git apply ./scripts/enable-android-google-services.patch
fi
if [ "$FLAVOR" == "proprietary" ]; then
git apply ./scripts/enable-android-google-services.patch
fi
script:
# start AVD and keep running in background
- scripts/integration-start-avd.sh &
@ -115,8 +144,8 @@ widget_test:
build_web:
stage: build
before_script:
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
script: [./scripts/build-web.sh]
[ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
script: [ ./scripts/build-web.sh ]
artifacts:
paths:
- build/web/
@ -124,23 +153,49 @@ build_web:
- docker
- famedly
# yes, we *do* build a Windows DLL on Linux. More reliable.
build_olm_windows:
image: archlinux:latest
stage: test
before_script:
- pacman-key --init
- pacman --noconfirm -Sy mingw-w64 cmake git base-devel
script:
- ./scripts/build-olm-windows.sh
- mv olm/build/libolm.dll .
artifacts:
paths:
- libolm.dll
only:
- main
- tags
build_windows:
extends:
- .shared_windows_runners
stage: build
before_script: [./scripts/prepare-windows.ps1]
script: [./scripts/build-windows.ps1]
stage: test
before_script:
- ./scripts/prepare-windows.ps1
# workarounding artifacts download being broken
- $response = Invoke-WebRequest -Uri "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs" -UseBasicParsing
- $jobs = $response | ConvertFrom-Json
- $job = $jobs | where { $_.name -eq "build_olm_windows" }
- $jobId = $job.id
- Invoke-WebRequest -Uri "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$jobId/artifacts/libolm.dll" -UseBasicParsing -OutFile libolm.dll
script:
- ./scripts/build-windows.ps1
- Copy-Item -Path "libolm.dll" -Destination "build/windows/runner/Release"
- ./scripts/package-windows.ps1
artifacts:
paths:
- build/windows/runner/Release
allow_failure: true
only:
- main
- tags
build_android_debug:
stage: build
script: [./scripts/build-android-debug.sh]
script: [ ./scripts/build-android-debug.sh ]
artifacts:
when: on_success
paths:
@ -157,7 +212,7 @@ build_android_apk:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/build-android-apk.sh]
script: [ ./scripts/build-android-apk.sh ]
artifacts:
when: on_success
paths:
@ -174,7 +229,7 @@ deploy_playstore_internal:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore-beta.sh]
script: [ ./scripts/release-playstore-beta.sh ]
artifacts:
when: on_success
paths:
@ -241,7 +296,7 @@ build_linux_x86:
[
sudo apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install keyboard-configuration -y && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y,
]
script: [./scripts/build-linux.sh]
script: [ ./scripts/build-linux.sh ]
tags:
- docker
- famedly
@ -252,9 +307,9 @@ build_linux_x86:
build_linux_arm64:
stage: build
before_script: [flutter upgrade]
script: [./scripts/build-linux.sh]
tags: [docker_arm64]
before_script: [ flutter upgrade ]
script: [ ./scripts/build-linux.sh ]
tags: [ docker_arm64 ]
only:
- main
- tags
@ -266,7 +321,7 @@ build_linux_arm64:
update_dependencies:
stage: build
needs: []
needs: [ ]
tags:
- docker
only:
@ -313,7 +368,8 @@ upload_android:
upload_web:
extends: .release
script:
- tar czf package.tar.gz -C build/web/ .
# workaround bug of Flutter engine
- tar czf package.tar.gz --ignore-failed-read -C build/web/ .
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
@ -347,7 +403,7 @@ deploy_playstore:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [./scripts/release-playstore.sh]
script: [ ./scripts/release-playstore.sh ]
resource_group: playstore_release
only:
- tags

View File

@ -1,3 +1,29 @@
## v1.8.0 2022-12-30
- Added translation using Weblate (Yue (yue_HK)) (Raatty)
- Translated using Weblate (Chinese (Simplified)) (Mike Evans)
- Translated using Weblate (Estonian) (Priit Jõerüüt)
- Translated using Weblate (French) (Anne Onyme 017)
- Translated using Weblate (Indonesian) (Linerly)
- Translated using Weblate (Turkish) (Oğuz Ersen)
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
- design: New encryption page (Krille Fear)
- feat: Add audio message support to linux (Krille Fear)
- feat: Use Android system accent color (Krille Fear)
- feat: include olm to Windows builds (TheOneWithTheBraid)
- feat: Store drafts (Krille)
- fix: Android push notification follow-up (TheOneWithTheBraid)
- fix: Content banner (Krille Fear)
- fix: Correct redacted by username (Krille Fear)
- fix: Do not setup push on every app resume (Krille Fear)
- fix: Encryption button is orange in public rooms (Krille Fear)
- fix: File event design (Krille Fear)
- fix: Hide google services warning after marked (Krille Fear)
- fix: Improve story page appearance (Reinhart Previano Koentjoro)
- fix: Libhandy windows (Krille Fear)
- fix: Monochromatic icon rendering for Android 13+ (Reinhart Previano Koentjoro)
- fix: homeserver error text not visible in app bar (TheOneWithTheBraid)
- fix: minor issues in room list (TheOneWithTheBraid)
## v1.7.2 2022-12-19
Update dependencies and translations.

File diff suppressed because one or more lines are too long

View File

@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/encryption.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,25 @@
{
"@@last_modified": "2021-08-14 12:41:10.154280",
"about": "সম্পর্কে",
"@about": {
"type": "text",
"placeholders": {}
},
"accept": "স্বীকার করি",
"@accept": {
"type": "text",
"placeholders": {}
},
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
"@@last_modified": "2021-08-14 12:41:10.154280",
"about": "সম্পর্কে",
"@about": {
"type": "text",
"placeholders": {}
},
"accept": "স্বীকার করি",
"@accept": {
"type": "text",
"placeholders": {}
},
"acceptedTheInvitation": "{username} আমন্ত্রণ গ্রহণ করেছে",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
}
},
"account": "অ্যাকাউন্ট",
"@account": {
"type": "text",
"placeholders": {}
}
},
"account": "অ্যাকাউন্ট",
"@account": {
"type": "text",
"placeholders": {}
},
"accountInformation": "অ্যাকাউন্ট তথ্য",
"@accountInformation": {
"type": "text",
"placeholders": {}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
{}
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +1,155 @@
{
"@@last_modified": "2021-08-14 12:41:09.940318",
"copiedToClipboard": "Copiada para a área de transferência",
"@copiedToClipboard": {
"type": "text",
"placeholders": {}
},
"login": "Iniciar sessão",
"@login": {
"type": "text",
"placeholders": {}
},
"monday": "segunda-feira",
"@monday": {
"type": "text",
"placeholders": {}
},
"saturday": "sábado",
"@saturday": {
"type": "text",
"placeholders": {}
},
"wednesday": "quarta-feira",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"about": "Sobre",
"@about": {
"type": "text",
"placeholders": {}
},
"admin": "Admin",
"@admin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "Tens a certeza?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"notifications": "Notificações",
"@notifications": {
"type": "text",
"placeholders": {}
},
"account": "Conta",
"@account": {
"type": "text",
"placeholders": {}
},
"cancel": "Cancelar",
"@cancel": {
"type": "text",
"placeholders": {}
},
"delete": "Eliminar",
"@delete": {
"type": "text",
"placeholders": {}
},
"dateAndTimeOfDay": "{date}, {timeOfDay}",
"@dateAndTimeOfDay": {
"type": "text",
"placeholders": {
"date": {},
"timeOfDay": {}
"@@last_modified": "2021-08-14 12:41:09.940318",
"copiedToClipboard": "Copiada para a área de transferência",
"@copiedToClipboard": {
"type": "text",
"placeholders": {}
},
"login": "Iniciar sessão",
"@login": {
"type": "text",
"placeholders": {}
},
"monday": "segunda-feira",
"@monday": {
"type": "text",
"placeholders": {}
},
"saturday": "sábado",
"@saturday": {
"type": "text",
"placeholders": {}
},
"wednesday": "quarta-feira",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"about": "Sobre",
"@about": {
"type": "text",
"placeholders": {}
},
"admin": "Admin",
"@admin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "Tens a certeza?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"notifications": "Notificações",
"@notifications": {
"type": "text",
"placeholders": {}
},
"account": "Conta",
"@account": {
"type": "text",
"placeholders": {}
},
"cancel": "Cancelar",
"@cancel": {
"type": "text",
"placeholders": {}
},
"delete": "Eliminar",
"@delete": {
"type": "text",
"placeholders": {}
},
"dateAndTimeOfDay": "{date}, {timeOfDay}",
"@dateAndTimeOfDay": {
"type": "text",
"placeholders": {
"date": {},
"timeOfDay": {}
}
},
"dateWithYear": "{day}-{month}-{year}",
"@dateWithYear": {
"type": "text",
"placeholders": {
"year": {},
"month": {},
"day": {}
}
},
"help": "Ajuda",
"@help": {
"type": "text",
"placeholders": {}
},
"messages": "Mensagens",
"@messages": {
"type": "text",
"placeholders": {}
},
"reason": "Razão",
"@reason": {
"type": "text",
"placeholders": {}
},
"privacy": "Privacidade",
"@privacy": {
"type": "text",
"placeholders": {}
},
"openCamera": "Abrir câmara",
"@openCamera": {
"type": "text",
"placeholders": {}
},
"settings": "Configurações",
"@settings": {
"type": "text",
"placeholders": {}
},
"tuesday": "terça-feira",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"logout": "Terminar sessão",
"@logout": {
"type": "text",
"placeholders": {}
},
"search": "Pesquisar",
"@search": {
"type": "text",
"placeholders": {}
},
"sunday": "domingo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"users": "Utilizadores",
"@users": {},
"close": "Fechar",
"@close": {
"type": "text",
"placeholders": {}
},
"dateWithoutYear": "{day}-{month}",
"@dateWithoutYear": {
"type": "text",
"placeholders": {
"month": {},
"day": {}
}
},
"friday": "sexta-feira",
"@friday": {
"type": "text",
"placeholders": {}
},
"thursday": "quinta-feira",
"@thursday": {
"type": "text",
"placeholders": {}
}
},
"dateWithYear": "{day}-{month}-{year}",
"@dateWithYear": {
"type": "text",
"placeholders": {
"year": {},
"month": {},
"day": {}
}
},
"help": "Ajuda",
"@help": {
"type": "text",
"placeholders": {}
},
"messages": "Mensagens",
"@messages": {
"type": "text",
"placeholders": {}
},
"reason": "Razão",
"@reason": {
"type": "text",
"placeholders": {}
},
"privacy": "Privacidade",
"@privacy": {
"type": "text",
"placeholders": {}
},
"openCamera": "Abrir câmara",
"@openCamera": {
"type": "text",
"placeholders": {}
},
"settings": "Configurações",
"@settings": {
"type": "text",
"placeholders": {}
},
"tuesday": "terça-feira",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"logout": "Terminar sessão",
"@logout": {
"type": "text",
"placeholders": {}
},
"search": "Pesquisar",
"@search": {
"type": "text",
"placeholders": {}
},
"sunday": "domingo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"users": "Utilizadores",
"@users": {},
"close": "Fechar",
"@close": {
"type": "text",
"placeholders": {}
},
"dateWithoutYear": "{day}-{month}",
"@dateWithoutYear": {
"type": "text",
"placeholders": {
"month": {},
"day": {}
}
},
"friday": "sexta-feira",
"@friday": {
"type": "text",
"placeholders": {}
},
"thursday": "quinta-feira",
"@thursday": {
"type": "text",
"placeholders": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,182 +1,147 @@
{
"@@last_modified": "2021-08-14 12:41:09.918296",
"about": "Despre",
"@about": {
"type": "text",
"placeholders": {}
},
"accept": "Accept",
"@accept": {
"type": "text",
"placeholders": {}
},
"acceptedTheInvitation": "{username} a aceptat invitați",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
"@@last_modified": "2021-08-14 12:41:09.918296",
"about": "Despre",
"@about": {
"type": "text",
"placeholders": {}
},
"accept": "Accept",
"@accept": {
"type": "text",
"placeholders": {}
},
"acceptedTheInvitation": "{username} a aceptat invitați",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
}
},
"account": "Cont",
"@account": {
"type": "text",
"placeholders": {}
},
"activatedEndToEndEncryption": "{username} a activat criptarea end-to-end",
"@activatedEndToEndEncryption": {
"type": "text",
"placeholders": {
"username": {}
}
},
"addGroupDescription": "Adaugă o descriere de",
"@addGroupDescription": {
"type": "text",
"placeholders": {}
},
"admin": "Administrator",
"@admin": {
"type": "text",
"placeholders": {}
},
"alias": "poreclă",
"@alias": {
"type": "text",
"placeholders": {}
},
"answeredTheCall": "{sendername} a acceptat apelul",
"@answeredTheCall": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"anyoneCanJoin": "Oricine se poate alătura",
"@anyoneCanJoin": {
"type": "text",
"placeholders": {}
},
"archive": "Arhivă",
"@archive": {
"type": "text",
"placeholders": {}
},
"archivedRoom": "Grup arhivat",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Vizitatorii \"guest\" se pot alătura",
"@areGuestsAllowedToJoin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "Ești sigur?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"askSSSSSign": "Pentru a putea conecta cealaltă persoană, te rog introdu parola sau cheia ta de recuperare.",
"@askSSSSSign": {
"type": "text",
"placeholders": {}
},
"askVerificationRequest": "Accepți cererea de verificare de la {username}?",
"@askVerificationRequest": {
"type": "text",
"placeholders": {
"username": {}
}
},
"banFromChat": "Interzis din conversație",
"@banFromChat": {
"type": "text",
"placeholders": {}
},
"banned": "Interzis",
"@banned": {
"type": "text",
"placeholders": {}
},
"bannedUser": "{username} a interzis pe {targetName}",
"@bannedUser": {
"type": "text",
"placeholders": {
"username": {},
"targetName": {}
}
},
"blockDevice": "Blochează dispozitiv",
"@blockDevice": {
"type": "text",
"placeholders": {}
},
"cancel": "Anulează",
"@cancel": {
"type": "text",
"placeholders": {}
},
"changeDeviceName": "Schimbă numele dispozitiv",
"@changeDeviceName": {
"type": "text",
"placeholders": {}
},
"changedTheChatAvatar": "{username} a schimbat poza conversați",
"@changedTheChatAvatar": {
"type": "text",
"placeholders": {
"username": {}
}
},
"changedTheChatDescriptionTo": "{username} a schimbat descrierea grupului în '{description}'",
"@changedTheChatDescriptionTo": {
"type": "text",
"placeholders": {
"username": {},
"description": {}
}
},
"changedTheChatNameTo": "{username} a schimbat porecla în '{chatname}'",
"@changedTheChatNameTo": {
"type": "text",
"placeholders": {
"username": {},
"chatname": {}
}
}
},
"account": "Cont",
"@account": {
"type": "text",
"placeholders": {}
},
"accountInformation": "Informații despre cont",
"@accountInformation": {
"type": "text",
"placeholders": {}
},
"activatedEndToEndEncryption": "{username} a activat criptarea end-to-end",
"@activatedEndToEndEncryption": {
"type": "text",
"placeholders": {
"username": {}
}
},
"addGroupDescription": "Adaugă o descriere de",
"@addGroupDescription": {
"type": "text",
"placeholders": {}
},
"admin": "Administrator",
"@admin": {
"type": "text",
"placeholders": {}
},
"alias": "poreclă",
"@alias": {
"type": "text",
"placeholders": {}
},
"alreadyHaveAnAccount": "Ai deja un cont?",
"@alreadyHaveAnAccount": {
"type": "text",
"placeholders": {}
},
"answeredTheCall": "{sendername} a acceptat apelul",
"@answeredTheCall": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"anyoneCanJoin": "Oricine se poate alătura",
"@anyoneCanJoin": {
"type": "text",
"placeholders": {}
},
"archive": "Arhivă",
"@archive": {
"type": "text",
"placeholders": {}
},
"archivedRoom": "Grup arhivat",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Vizitatorii \"guest\" se pot alătura",
"@areGuestsAllowedToJoin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "Ești sigur?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"askSSSSCache": "Te rog introdu parola ta sau cheile de recuparare pentru a depozita cheile.",
"@askSSSSCache": {
"type": "text",
"placeholders": {}
},
"askSSSSSign": "Pentru a putea conecta cealaltă persoană, te rog introdu parola sau cheia ta de recuperare.",
"@askSSSSSign": {
"type": "text",
"placeholders": {}
},
"askSSSSVerify": "Te rog introdu parola sau cheia ta de recuperare pentru a-ți verifica sesiunea.",
"@askSSSSVerify": {
"type": "text",
"placeholders": {}
},
"askVerificationRequest": "Accepți cererea de verificare de la {username}?",
"@askVerificationRequest": {
"type": "text",
"placeholders": {
"username": {}
}
},
"authentication": "Autentificare",
"@authentication": {
"type": "text",
"placeholders": {}
},
"avatarHasBeenChanged": "Image de profil schimbată",
"@avatarHasBeenChanged": {
"type": "text",
"placeholders": {}
},
"banFromChat": "Interzis din conversație",
"@banFromChat": {
"type": "text",
"placeholders": {}
},
"banned": "Interzis",
"@banned": {
"type": "text",
"placeholders": {}
},
"bannedUser": "{username} a interzis pe {targetName}",
"@bannedUser": {
"type": "text",
"placeholders": {
"username": {},
"targetName": {}
}
},
"blockDevice": "Blochează dispozitiv",
"@blockDevice": {
"type": "text",
"placeholders": {}
},
"cachedKeys": "Chei salvate",
"@cachedKeys": {
"type": "text",
"placeholders": {}
},
"cancel": "Anulează",
"@cancel": {
"type": "text",
"placeholders": {}
},
"changeDeviceName": "Schimbă numele dispozitiv",
"@changeDeviceName": {
"type": "text",
"placeholders": {}
},
"changedTheChatAvatar": "{username} a schimbat poza conversați",
"@changedTheChatAvatar": {
"type": "text",
"placeholders": {
"username": {}
}
},
"changedTheChatDescriptionTo": "{username} a schimbat descrierea grupului în '{description}'",
"@changedTheChatDescriptionTo": {
"type": "text",
"placeholders": {
"username": {},
"description": {}
}
},
"changedTheChatNameTo": "{username} a schimbat porecla în '{chatname}'",
"@changedTheChatNameTo": {
"type": "text",
"placeholders": {
"username": {},
"chatname": {}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,374 +1,292 @@
{
"@@last_modified": "2021-08-14 12:41:09.895217",
"about": "පිළිබඳව",
"@about": {
"type": "text",
"placeholders": {}
},
"accept": "පිළිගන්න",
"@accept": {
"type": "text",
"placeholders": {}
},
"account": "ගිණුම",
"@account": {
"type": "text",
"placeholders": {}
},
"accountInformation": "ගිණුමේ තොරතුරු",
"@accountInformation": {
"type": "text",
"placeholders": {}
},
"addEmail": "වි-තැපෑල එකතු කරන්න",
"@addEmail": {
"type": "text",
"placeholders": {}
},
"admin": "පරිපාලක",
"@admin": {
"type": "text",
"placeholders": {}
},
"allChats": "සියලුම සංවාද",
"@allChats": {
"type": "text",
"placeholders": {}
},
"alreadyHaveAnAccount": "දැනටමත් ගිණුමක් තිබේද?",
"@alreadyHaveAnAccount": {
"type": "text",
"placeholders": {}
},
"anyoneCanJoin": "ඕනෑම කෙනෙකුට එක්විය හැකිය",
"@anyoneCanJoin": {
"type": "text",
"placeholders": {}
},
"archive": "සංරක්ෂිතය",
"@archive": {
"type": "text",
"placeholders": {}
},
"archivedRoom": "සංරක්ෂිත කාමරය",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "ආගන්තුක පරිශීලකයින්ට එක්වීමට අවසර තිබේද",
"@areGuestsAllowedToJoin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "ඔබට විශ්වාසද?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"areYouSureYouWantToLogout": "ඔබට නික්මීමට අවශ්‍ය බව විශ්වාසද?",
"@areYouSureYouWantToLogout": {
"type": "text",
"placeholders": {}
},
"audioPlayerPlay": "ධාවනය",
"@audioPlayerPlay": {
"type": "text",
"placeholders": {}
},
"blockDevice": "උපාංගය අවහිර කරන්න",
"@blockDevice": {
"type": "text",
"placeholders": {}
},
"cachedKeys": "යතුරු නිහිතගතයි",
"@cachedKeys": {
"type": "text",
"placeholders": {}
},
"cancel": "අවලංගු කරන්න",
"@cancel": {
"type": "text",
"placeholders": {}
},
"changeDeviceName": "උපාංගයේ නම වෙනස් කරන්න",
"@changeDeviceName": {
"type": "text",
"placeholders": {}
},
"changePassword": "මුරපදය වෙනස් කරන්න",
"@changePassword": {
"type": "text",
"placeholders": {}
},
"chat": "සංවාදය",
"@chat": {
"type": "text",
"placeholders": {}
},
"chatBackup": "සංවාද උපස්ථය",
"@chatBackup": {
"type": "text",
"placeholders": {}
},
"chatDetails": "සංවාදයේ විස්තර",
"@chatDetails": {
"type": "text",
"placeholders": {}
},
"chats": "සංවාද",
"@chats": {
"type": "text",
"placeholders": {}
},
"chooseAStrongPassword": "ශක්තිමත් මුරපදයක් තෝරන්න",
"@chooseAStrongPassword": {
"type": "text",
"placeholders": {}
},
"chooseAUsername": "පරිශීලක නාමයක් තෝරන්න",
"@chooseAUsername": {
"type": "text",
"placeholders": {}
},
"clearArchive": "සංරක්ෂිතය හිස් කරන්න",
"@clearArchive": {},
"close": "වසන්න",
"@close": {
"type": "text",
"placeholders": {}
},
"commandHint_join": "දී ඇති කාමරයට එක්වන්න",
"@commandHint_join": {
"type": "text",
"description": "Usage hint for the command /join"
},
"commandHint_leave": "මෙම කාමරය හැරයන්න",
"@commandHint_leave": {
"type": "text",
"description": "Usage hint for the command /leave"
},
"commandInvalid": "විධානය වලංගු නොවේ",
"@commandInvalid": {
"type": "text"
},
"commandMissing": "{{command} විධානයක් නොවේ.",
"@commandMissing": {
"type": "text",
"placeholders": {
"command": {}
"@@last_modified": "2021-08-14 12:41:09.895217",
"about": "පිළිබඳව",
"@about": {
"type": "text",
"placeholders": {}
},
"description": "State that {command} is not a valid /command."
},
"compareEmojiMatch": "සසඳා බලා පහත දැක්වෙන ඉමොජි අනෙක් උපාංගයට නිසැකවම ගැලපෙන බවට වග බලා ගන්න:",
"@compareEmojiMatch": {
"type": "text",
"placeholders": {}
},
"compareNumbersMatch": "සංසන්දනය කර පහත දැක්වෙන අංක අනෙක් උපාංගට නිසැකව ගැලපෙන බවට වග බලා ගන්න:",
"@compareNumbersMatch": {
"type": "text",
"placeholders": {}
},
"confirm": "තහවුරු කරන්න",
"@confirm": {
"type": "text",
"placeholders": {}
},
"connect": "සබඳින්න",
"@connect": {
"type": "text",
"placeholders": {}
},
"connectionAttemptFailed": "සබැඳීමේ උත්සාහය අසාර්ථකයි",
"@connectionAttemptFailed": {
"type": "text",
"placeholders": {}
},
"contactHasBeenInvitedToTheGroup": "සමූහය වෙත සබඳතාවයකට ආරාධනා කර ඇත",
"@contactHasBeenInvitedToTheGroup": {
"type": "text",
"placeholders": {}
},
"copy": "පිටපත්",
"@copy": {
"type": "text",
"placeholders": {}
},
"create": "සාදන්න",
"@create": {
"type": "text",
"placeholders": {}
},
"createAccountNow": "දැන් ගිණුමක් සාදන්න",
"@createAccountNow": {
"type": "text",
"placeholders": {}
},
"createNewGroup": "නව සමූහයක් සාදන්න",
"@createNewGroup": {
"type": "text",
"placeholders": {}
},
"donate": "පරිත්‍යාග",
"@donate": {
"type": "text",
"placeholders": {}
},
"encryption": "සංකේතාංකනය",
"@encryption": {
"type": "text",
"placeholders": {}
},
"everythingReady": "සියල්ල සූදානම්!",
"@everythingReady": {
"type": "text",
"placeholders": {}
},
"fontSize": "මුද්‍රණඅකුරේ ප්‍රමාණය",
"@fontSize": {
"type": "text",
"placeholders": {}
},
"goToTheNewRoom": "නව කාමරයට යන්න",
"@goToTheNewRoom": {
"type": "text",
"placeholders": {}
},
"joinRoom": "කාමරයට එක්වන්න",
"@joinRoom": {
"type": "text",
"placeholders": {}
},
"keysCached": "යතුරු නිහිතගත යි",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"next": "ඊලඟ",
"@next": {
"type": "text",
"placeholders": {}
},
"noPublicRoomsFound": "ප්‍රසිද්ධ කාමර හමු නොවිණි…",
"@noPublicRoomsFound": {
"type": "text",
"placeholders": {}
},
"people": "මිනිසුන්",
"@people": {
"type": "text",
"placeholders": {}
},
"publicGroups": "ප්‍රසිද්ධ සමූහ",
"@publicGroups": {
"type": "text",
"placeholders": {}
},
"removeDevice": "උපාංගය ඉවත්කරන්න",
"@removeDevice": {
"type": "text",
"placeholders": {}
},
"roomVersion": "කාමරයේ අනුවාදය",
"@roomVersion": {
"type": "text",
"placeholders": {}
},
"savedFileAs": "ලෙස ගොනුව සුරකින්න {filename}",
"@savedFileAs": {
"type": "text",
"placeholders": {
"filename": {}
"accept": "පිළිගන්න",
"@accept": {
"type": "text",
"placeholders": {}
},
"account": "ගිණුම",
"@account": {
"type": "text",
"placeholders": {}
},
"addEmail": "වි-තැපෑල එකතු කරන්න",
"@addEmail": {
"type": "text",
"placeholders": {}
},
"admin": "පරිපාලක",
"@admin": {
"type": "text",
"placeholders": {}
},
"allChats": "සියලුම සංවාද",
"@allChats": {
"type": "text",
"placeholders": {}
},
"anyoneCanJoin": "ඕනෑම කෙනෙකුට එක්විය හැකිය",
"@anyoneCanJoin": {
"type": "text",
"placeholders": {}
},
"archive": "සංරක්ෂිතය",
"@archive": {
"type": "text",
"placeholders": {}
},
"archivedRoom": "සංරක්ෂිත කාමරය",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "ආගන්තුක පරිශීලකයින්ට එක්වීමට අවසර තිබේද",
"@areGuestsAllowedToJoin": {
"type": "text",
"placeholders": {}
},
"areYouSure": "ඔබට විශ්වාසද?",
"@areYouSure": {
"type": "text",
"placeholders": {}
},
"areYouSureYouWantToLogout": "ඔබට නික්මීමට අවශ්‍ය බව විශ්වාසද?",
"@areYouSureYouWantToLogout": {
"type": "text",
"placeholders": {}
},
"blockDevice": "උපාංගය අවහිර කරන්න",
"@blockDevice": {
"type": "text",
"placeholders": {}
},
"cancel": "අවලංගු කරන්න",
"@cancel": {
"type": "text",
"placeholders": {}
},
"changeDeviceName": "උපාංගයේ නම වෙනස් කරන්න",
"@changeDeviceName": {
"type": "text",
"placeholders": {}
},
"changePassword": "මුරපදය වෙනස් කරන්න",
"@changePassword": {
"type": "text",
"placeholders": {}
},
"chat": "සංවාදය",
"@chat": {
"type": "text",
"placeholders": {}
},
"chatBackup": "සංවාද උපස්ථය",
"@chatBackup": {
"type": "text",
"placeholders": {}
},
"chatDetails": "සංවාදයේ විස්තර",
"@chatDetails": {
"type": "text",
"placeholders": {}
},
"chats": "සංවාද",
"@chats": {
"type": "text",
"placeholders": {}
},
"chooseAStrongPassword": "ශක්තිමත් මුරපදයක් තෝරන්න",
"@chooseAStrongPassword": {
"type": "text",
"placeholders": {}
},
"chooseAUsername": "පරිශීලක නාමයක් තෝරන්න",
"@chooseAUsername": {
"type": "text",
"placeholders": {}
},
"clearArchive": "සංරක්ෂිතය හිස් කරන්න",
"@clearArchive": {},
"close": "වසන්න",
"@close": {
"type": "text",
"placeholders": {}
},
"commandHint_join": "දී ඇති කාමරයට එක්වන්න",
"@commandHint_join": {
"type": "text",
"description": "Usage hint for the command /join"
},
"commandHint_leave": "මෙම කාමරය හැරයන්න",
"@commandHint_leave": {
"type": "text",
"description": "Usage hint for the command /leave"
},
"commandInvalid": "විධානය වලංගු නොවේ",
"@commandInvalid": {
"type": "text"
},
"commandMissing": "{{command} විධානයක් නොවේ.",
"@commandMissing": {
"type": "text",
"placeholders": {
"command": {}
},
"description": "State that {command} is not a valid /command."
},
"compareEmojiMatch": "සසඳා බලා පහත දැක්වෙන ඉමොජි අනෙක් උපාංගයට නිසැකවම ගැලපෙන බවට වග බලා ගන්න:",
"@compareEmojiMatch": {
"type": "text",
"placeholders": {}
},
"compareNumbersMatch": "සංසන්දනය කර පහත දැක්වෙන අංක අනෙක් උපාංගට නිසැකව ගැලපෙන බවට වග බලා ගන්න:",
"@compareNumbersMatch": {
"type": "text",
"placeholders": {}
},
"confirm": "තහවුරු කරන්න",
"@confirm": {
"type": "text",
"placeholders": {}
},
"connect": "සබඳින්න",
"@connect": {
"type": "text",
"placeholders": {}
},
"contactHasBeenInvitedToTheGroup": "සමූහය වෙත සබඳතාවයකට ආරාධනා කර ඇත",
"@contactHasBeenInvitedToTheGroup": {
"type": "text",
"placeholders": {}
},
"copy": "පිටපත්",
"@copy": {
"type": "text",
"placeholders": {}
},
"create": "සාදන්න",
"@create": {
"type": "text",
"placeholders": {}
},
"createNewGroup": "නව සමූහයක් සාදන්න",
"@createNewGroup": {
"type": "text",
"placeholders": {}
},
"encryption": "සංකේතාංකනය",
"@encryption": {
"type": "text",
"placeholders": {}
},
"everythingReady": "සියල්ල සූදානම්!",
"@everythingReady": {
"type": "text",
"placeholders": {}
},
"fontSize": "මුද්‍රණඅකුරේ ප්‍රමාණය",
"@fontSize": {
"type": "text",
"placeholders": {}
},
"goToTheNewRoom": "නව කාමරයට යන්න",
"@goToTheNewRoom": {
"type": "text",
"placeholders": {}
},
"joinRoom": "කාමරයට එක්වන්න",
"@joinRoom": {
"type": "text",
"placeholders": {}
},
"keysCached": "යතුරු නිහිතගත යි",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"next": "ඊලඟ",
"@next": {
"type": "text",
"placeholders": {}
},
"people": "මිනිසුන්",
"@people": {
"type": "text",
"placeholders": {}
},
"removeDevice": "උපාංගය ඉවත්කරන්න",
"@removeDevice": {
"type": "text",
"placeholders": {}
},
"roomVersion": "කාමරයේ අනුවාදය",
"@roomVersion": {
"type": "text",
"placeholders": {}
},
"saveFile": "ගොනුව සුරකින්න",
"@saveFile": {
"type": "text",
"placeholders": {}
},
"send": "යවන්න",
"@send": {
"type": "text",
"placeholders": {}
},
"showPassword": "මුරපදය පෙන්වන්න",
"@showPassword": {
"type": "text",
"placeholders": {}
},
"sunday": "ඉරිදා",
"@sunday": {
"type": "text",
"placeholders": {}
},
"username": "පරිශීලක නාමය",
"@username": {
"type": "text",
"placeholders": {}
},
"videoCall": "දෘශ්‍ය ඇමතුම",
"@videoCall": {
"type": "text",
"placeholders": {}
},
"wallpaper": "බිතුපත",
"@wallpaper": {
"type": "text",
"placeholders": {}
},
"warning": "අවවාදයයි!",
"@warning": {
"type": "text",
"placeholders": {}
},
"wednesday": "බදාදා",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"writeAMessage": "පණිවිඩයක් ලියන්න…",
"@writeAMessage": {
"type": "text",
"placeholders": {}
},
"yes": "ඔව්",
"@yes": {
"type": "text",
"placeholders": {}
},
"you": "ඔබ",
"@you": {
"type": "text",
"placeholders": {}
}
},
"saveFile": "ගොනුව සුරකින්න",
"@saveFile": {
"type": "text",
"placeholders": {}
},
"saveFileToFolder": "ගොනුව මෙම බහාලුමට සුරකින්න",
"@saveFileToFolder": {
"type": "text",
"placeholders": {}
},
"securityKey": "ආරක්ෂක යතුර",
"@securityKey": {
"type": "text",
"placeholders": {}
},
"securityKeyLost": "ආරක්ෂක යතුර නැතිවුනාද?",
"@securityKeyLost": {
"type": "text",
"placeholders": {}
},
"send": "යවන්න",
"@send": {
"type": "text",
"placeholders": {}
},
"showPassword": "මුරපදය පෙන්වන්න",
"@showPassword": {
"type": "text",
"placeholders": {}
},
"sunday": "ඉරිදා",
"@sunday": {
"type": "text",
"placeholders": {}
},
"username": "පරිශීලක නාමය",
"@username": {
"type": "text",
"placeholders": {}
},
"videoCall": "දෘශ්‍ය ඇමතුම",
"@videoCall": {
"type": "text",
"placeholders": {}
},
"wallpaper": "බිතුපත",
"@wallpaper": {
"type": "text",
"placeholders": {}
},
"warning": "අවවාදයයි!",
"@warning": {
"type": "text",
"placeholders": {}
},
"wednesday": "බදාදා",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"writeAMessage": "පණිවිඩයක් ලියන්න…",
"@writeAMessage": {
"type": "text",
"placeholders": {}
},
"yes": "ඔව්",
"@yes": {
"type": "text",
"placeholders": {}
},
"you": "ඔබ",
"@you": {
"type": "text",
"placeholders": {}
},
"yourOwnUsername": "ඔබට හිමි පරිශීලකනාමය",
"@yourOwnUsername": {
"type": "text",
"placeholders": {}
},
"zoomIn": "විශාලනය",
"@zoomIn": {
"type": "text",
"placeholders": {}
},
"zoomOut": "කුඩාලනය",
"@zoomOut": {
"type": "text",
"placeholders": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,20 @@
{
"@@last_modified": "2021-08-14 12:41:09.826673",
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
"@@last_modified": "2021-08-14 12:41:09.826673",
"acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்",
"@acceptedTheInvitation": {
"type": "text",
"placeholders": {
"username": {}
}
},
"accept": "ஏற்றுக்கொள்",
"@accept": {
"type": "text",
"placeholders": {}
},
"about": "பற்றி",
"@about": {
"type": "text",
"placeholders": {}
}
},
"accept": "ஏற்றுக்கொள்",
"@accept": {
"type": "text",
"placeholders": {}
},
"about": "பற்றி",
"@about": {
"type": "text",
"placeholders": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

BIN
assets/start_chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,565 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>FluffyChat - Official Website</title>
<meta name="description" content="A cute and secure chatclient for the matrix protocol">
<meta name="keywords"
content="Fluffychat, Matrix, Web, Android, iOS, Desktop, Chat, Client, Chatclient, Matrix.org, Secure, E2EE, End to End, Encryption, End to End Encryption, F-Droid, Foss, FOSS, OpenSource, Free, Community, Open">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MobileApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "ANDROID",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "133"
},
"installUrl": "https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MobileApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "ANDROID",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "133"
},
"installUrl": "https://f-droid.org/de/packages/chat.fluffy.fluffychat/"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "MobileApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "IOS",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.4",
"ratingCount": "28"
},
"installUrl": "https://apps.apple.com/app/fluffychat/id1551469600"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "WEB",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "133"
},
"url": "https://fluffychat.im/web",
"downloadUrl": "https://fluffychat.im/web",
"installUrl": "https://fluffychat.im/web"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "LINUX",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "133"
},
"downloadUrl": "https://snapcraft.io/fluffychat",
"installUrl": "https://snapcraft.io/fluffychat"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Fluffychat",
"applicationCategory": "CommunicationApplication",
"countriesNotSupported": "fr",
"operatingSystem": "LINUX",
"releaseNotes": "https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md",
"screenshot": "https://gitlab.com/famedly/fluffychat/-/raw/main/docs/screenshots/mobile.png",
"softwareHelp": "https://gitlab.com/famedly/fluffychat/-/wikis/FAQ",
"author": {
"@type": "Person",
"callSign": "KrilleFear"
},
"license": "https://gitlab.com/famedly/fluffychat/-/blob/main/LICENSE",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "133"
},
"downloadUrl": "https://flathub.org/apps/details/im.fluffychat.Fluffychat",
"installUrl": "https://flathub.org/apps/details/im.fluffychat.Fluffychat"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "FluffyChat - Official Website",
"url": "https://fluffychat.im",
"description": "A cute and secure chatclient for the matrix protocol",
"thumbnailUrl": "https://fluffychat.im/favicon.png",
"inLanguage": "de-de"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"description": "Breadcrumbs list",
"name": "Breadcrumbs",
"itemListElement": [
{
"@type": "ListItem",
"item": {
"@id": "https://fluffychat.im",
"name": "Homepage"
},
"position": 1
}
]
}
</script>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="tailwind.css">
<!-- Animation CSS-->
<meta charset="utf-8">
<title>FluffyChat Official Website</title>
<meta name="identifier-url" content="https://fluffychat.im" />
<meta name="title" content="FluffyChat Official Website" />
<meta name="description" content="The cutest messenger in the Matrix network" />
<meta name="abstract" content="FluffyChat is the cutest messenger in the Matrix network" />
<meta name="keywords" content="FluffyChat, Matrix, Flutter, App" />
<meta name="author" content="Krille Fear" />
<meta name="revisit-after" content="15" />
<meta name="language" content="EN" />
<meta name="robots" content="All" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.png">
<link href="tailwind.css" rel="stylesheet">
<style>
@font-face {
font-family: Zen Kurenaido;
src: url(ZenKurenaido-Regular.ttf);
}
/* ----------------------------------------------
* Generated by Animista
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
.slide-in-bottom {
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) both;
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) both
}
.slide-in-bottom-h1 {
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .5s both;
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .5s both
}
.slide-in-bottom-subtitle {
-webkit-animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .75s both;
animation: slide-in-bottom .5s cubic-bezier(.25, .46, .45, .94) .75s both
}
.fade-in {
-webkit-animation: fade-in 1.2s cubic-bezier(.39, .575, .565, 1.000) 1s both;
animation: fade-in 1.2s cubic-bezier(.39, .575, .565, 1.000) 1s both
}
.bounce-top-icons {
-webkit-animation: bounce-top .9s 1s both;
animation: bounce-top .9s 1s both
}
@-webkit-keyframes slide-in-bottom {
0% {
-webkit-transform: translateY(1000px);
transform: translateY(1000px);
opacity: 0
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1
}
}
@keyframes slide-in-bottom {
0% {
-webkit-transform: translateY(1000px);
transform: translateY(1000px);
opacity: 0
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1
}
}
@-webkit-keyframes bounce-top {
0% {
-webkit-transform: translateY(-45px);
transform: translateY(-45px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
opacity: 1
}
24% {
opacity: 1
}
40% {
-webkit-transform: translateY(-24px);
transform: translateY(-24px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
65% {
-webkit-transform: translateY(-12px);
transform: translateY(-12px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
82% {
-webkit-transform: translateY(-6px);
transform: translateY(-6px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
93% {
-webkit-transform: translateY(-4px);
transform: translateY(-4px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
25%,
55%,
75%,
87% {
-webkit-transform: translateY(0);
transform: translateY(0);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
opacity: 1
}
}
@keyframes bounce-top {
0% {
-webkit-transform: translateY(-45px);
transform: translateY(-45px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
opacity: 1
}
24% {
opacity: 1
}
40% {
-webkit-transform: translateY(-24px);
transform: translateY(-24px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
65% {
-webkit-transform: translateY(-12px);
transform: translateY(-12px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
82% {
-webkit-transform: translateY(-6px);
transform: translateY(-6px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
93% {
-webkit-transform: translateY(-4px);
transform: translateY(-4px);
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in
}
25%,
55%,
75%,
87% {
-webkit-transform: translateY(0);
transform: translateY(0);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
opacity: 1
}
}
@-webkit-keyframes fade-in {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
@keyframes fade-in {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
</style>
</head>
<body
class="flex flex-col items-center justify-center min-h-screen w-screen bg-gradient-to-t from-purple-200 to-blue-50 dark:from-gray-800 dark:to-slate-900 p-4"
style="font-family: 'Zen Kurenaido', sans-serif;">
<img src="favicon.png" class="h-10" />
<h1 class="flex text-4xl items-center mb-4">
<span style="color: #5625BA">Fluffy</span>
<span style="color: #41a2bc">Chat</span>
</h1>
<img src="screenshots/screenshots.png" class="sm:max-w-lg max-w-screen mb-8" />
<body class="leading-normal tracking-normal text-gray-900" style="font-family: 'Zen Kurenaido', sans-serif;">
<div class="h-screen pb-14 bg-right bg-cover" style="background-image:url('bg.svg');">
<!--Nav-->
<div class="w-full container mx-auto p-6">
<div class="w-full flex items-center justify-between">
<a class="flex items-center no-underline hover:no-underline font-bold text-2xl lg:text-4xl" href="#">
<img src="favicon.png" class="h-8 fill-current text-indigo-600 pr-2" /> <span
style="color: #5625BA">Fluffy</span><span style="color: #41a2bc">Chat</span>
</a>
<div class="flex w-1/2 justify-end content-center">
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
href="https://matrix.to/#/#fluffychat:matrix.org">
<svg class="fill-current h-6" enable-background="new -91 49.217 56.693 56.693" id="Layer_1"
version="1.1" viewBox="-91 49.217 56.693 56.693" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path
d="M-38.3289,79.8244c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.5351,1.5264l-3.0737-9.1321l4.4169-1.4866 c2.2362-0.7526,3.4388-3.1756,2.6861-5.4117c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.4168,1.4866l-1.4877-4.4201 c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861v0c-2.2362,0.7526-3.4388,3.1756-2.6861,5.4117l1.4877,4.4201l-9.3246,3.1385 l-1.4697-4.3666c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117l1.4697,4.3666 l-4.445,1.4961c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117v0c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861l4.445-1.4961 l3.0737,9.1321l-4.3268,1.4563c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861 l4.3268-1.4563l1.5778,4.6877c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5778-4.6877 l9.3246-3.1385l1.5598,4.6342c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5598-4.6342 l4.5351-1.5264C-38.7789,84.4835-37.5762,82.0606-38.3289,79.8244z M-65.6982,84.5288l-3.0737-9.1321l9.3246-3.1385l3.0737,9.1321 L-65.6982,84.5288z" />
</svg>
</a>
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
href="https://twitter.com/KrilleFear">
<svg class="fill-current h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path
d="M30.063 7.313c-.813 1.125-1.75 2.125-2.875 2.938v.75c0 1.563-.188 3.125-.688 4.625a15.088 15.088 0 0 1-2.063 4.438c-.875 1.438-2 2.688-3.25 3.813a15.015 15.015 0 0 1-4.625 2.563c-1.813.688-3.75 1-5.75 1-3.25 0-6.188-.875-8.875-2.625.438.063.875.125 1.375.125 2.688 0 5.063-.875 7.188-2.5-1.25 0-2.375-.375-3.375-1.125s-1.688-1.688-2.063-2.875c.438.063.813.125 1.125.125.5 0 1-.063 1.5-.25-1.313-.25-2.438-.938-3.313-1.938a5.673 5.673 0 0 1-1.313-3.688v-.063c.813.438 1.688.688 2.625.688a5.228 5.228 0 0 1-1.875-2c-.5-.875-.688-1.813-.688-2.75 0-1.063.25-2.063.75-2.938 1.438 1.75 3.188 3.188 5.25 4.25s4.313 1.688 6.688 1.813a5.579 5.579 0 0 1 1.5-5.438c1.125-1.125 2.5-1.688 4.125-1.688s3.063.625 4.188 1.813a11.48 11.48 0 0 0 3.688-1.375c-.438 1.375-1.313 2.438-2.563 3.188 1.125-.125 2.188-.438 3.313-.875z">
</path>
</svg>
</a>
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
href="https://metalhead.club/@krille">
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<clipPath id="_clip1">
<rect x="33.6" y="-0.035" width="932.844" height="1000" />
</clipPath>
<g clip-path="url(#_clip1)">
<path
d="M946.586,599.455c-13.713,70.541 -122.816,147.742 -248.121,162.703c-65.341,7.796 -129.674,14.962 -198.275,11.815c-112.191,-5.139 -200.716,-26.776 -200.716,-26.776c0,10.92 0.673,21.319 2.02,31.044c14.586,110.711 109.787,117.344 199.967,120.436c91.021,3.114 172.068,-22.44 172.068,-22.44l3.74,82.281c0,0 -63.666,34.185 -177.079,40.473c-62.539,3.437 -140.192,-1.573 -230.636,-25.511c-196.158,-51.916 -229.893,-260.996 -235.055,-473.143c-1.573,-62.987 -0.603,-122.381 -0.603,-172.056c0,-216.931 142.142,-280.516 142.142,-280.516c71.672,-32.914 194.655,-46.755 322.508,-47.8l3.142,0c127.853,1.045 250.917,14.886 322.583,47.8c0,0 142.138,63.585 142.138,280.516c0,0 1.783,160.053 -19.823,271.174"
style="fill-rule:nonzero;" />
<path
d="M798.748,345.11l0,262.667l-104.07,0l0,-254.946c0,-53.743 -22.614,-81.021 -67.847,-81.021c-50.012,0 -75.077,32.359 -75.077,96.343l0,139.547l-103.457,0l0,-139.547c0,-63.984 -25.07,-96.343 -75.082,-96.343c-45.233,0 -67.847,27.278 -67.847,81.021l0,254.946l-104.07,0l0,-262.667c0,-53.683 13.669,-96.343 41.127,-127.904c28.314,-31.561 65.395,-47.741 111.425,-47.741c53.256,0 93.585,20.468 120.251,61.41l25.922,43.451l25.927,-43.451c26.66,-40.942 66.99,-61.41 120.251,-61.41c46.025,0 83.106,16.18 111.425,47.741c27.453,31.561 41.122,74.221 41.122,127.904"
style="fill:#fff;fill-rule:nonzero;" />
</g>
</svg>
</a>
<a class="inline-block text-blue-300 no-underline hover:text-indigo-800 hover:text-underline text-center h-10 p-2 md:h-auto md:p-4"
href="https://ko-fi.com/krille">
<img class="w-10 hover:animate-bounce" src="Kofi_pixel_logo.png"/>
</a>
</div>
</div>
</div>
<!--Main-->
<div class="container pt-8 px-6 mx-auto flex flex-wrap flex-col md:flex-row items-center">
<!--Left Col-->
<div class="flex flex-col w-full xl:w-2/5 justify-center lg:items-start overflow-y-hidden">
<h1
class="my-4 text-3xl md:text-5xl text-purple-800 font-bold leading-tight text-center md:text-left slide-in-bottom-h1">
Open. Nonprofit. Cute.</h1>
<p class="leading-normal text-base md:text-2xl mb-8 text-center md:text-left slide-in-bottom-subtitle">
Easy to use (<a class="underline hover:text-blue-700 transition-all"
href="https://matrix.org">matrix</a>) messenger. Secure and decentralized.</p>
<p class="text-blue-700 font-bold pb-4 text-center md:text-left fade-in">Mobile app:</p>
<div class="w-full flex justify-center md:justify-start pb-24 lg:pb-0 fade-in">
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
<a href="https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"><img
src="google-play-badge.png" class="max-h-12 pr-2 mb-2 bounce-top-icons inline">
</a><a href="https://f-droid.org/de/packages/chat.fluffy.fluffychat/"><img src="fdroid_button.png"
class="max-h-12 pr-2 mb-2 bounce-top-icons inline">
</a>
</div>
<p class="text-blue-700 font-bold py-4 text-center md:text-left fade-in">Desktop app:</p>
<div class="w-full flex justify-center md:justify-start pb-24 lg:pb-0 fade-in">
<a href="https://fluffychat.im/web">
<img src="browser-badge.png" class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
<a href="https://snapcraft.io/fluffychat"><img
src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg"
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
<a href="https://flathub.org/apps/details/im.fluffychat.Fluffychat"><img src="flathub-badge-en.png"
class="max-h-12 pr-2 mb-2 bounce-top-icons inline"></a>
</div>
</div>
<!--Right Col-->
<div class="w-full xl:w-3/5 py-6 relative">
<img class="w-full mx-auto slide-in-bottom" src="screenshots/screenshots.png">
</div>
<!--Footer-->
<div class="w-full pt-16 pb-6 text-sm text-center md:text-left fade-in">
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat">Source code</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md">Privacy</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md">Changelog</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/docs/fdroid_repo.md">FluffyChat F-Droid repository</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://liberapay.com/KrilleChritzelius/donate">Donate</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800"
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
-
<a class="text-gray-500 no-underline hover:text-purple-800" href="https://krillefear.gitlab.io">Created
by Krille Fear</a>
</div>
</div>
<div class="max-w-lg mb-8 flex justify-center flex-wrap">
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"
class="w-36 pr-2 mb-2 inline hover:scale-105 transition-transform"></a>
<a href="https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"><img src="google-play-badge.png"
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
</a><a href="https://f-droid.org/de/packages/chat.fluffy.fluffychat/"><img src="fdroid_button.png"
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline">
</a>
<a href="https://fluffychat.im/web">
<img src="browser-badge.png" class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
<a href="https://snapcraft.io/fluffychat"><img
src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg"
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
<a href="https://flathub.org/apps/details/im.fluffychat.Fluffychat"><img src="flathub-badge-en.png"
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
</div>
<div class="flex mb-8 justify-center content-center">
<a rel="me"
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
href="https://metalhead.club/@krille">
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<clipPath id="_clip1">
<rect x="33.6" y="-0.035" width="932.844" height="1000" />
</clipPath>
<g clip-path="url(#_clip1)">
<path
d="M946.586,599.455c-13.713,70.541 -122.816,147.742 -248.121,162.703c-65.341,7.796 -129.674,14.962 -198.275,11.815c-112.191,-5.139 -200.716,-26.776 -200.716,-26.776c0,10.92 0.673,21.319 2.02,31.044c14.586,110.711 109.787,117.344 199.967,120.436c91.021,3.114 172.068,-22.44 172.068,-22.44l3.74,82.281c0,0 -63.666,34.185 -177.079,40.473c-62.539,3.437 -140.192,-1.573 -230.636,-25.511c-196.158,-51.916 -229.893,-260.996 -235.055,-473.143c-1.573,-62.987 -0.603,-122.381 -0.603,-172.056c0,-216.931 142.142,-280.516 142.142,-280.516c71.672,-32.914 194.655,-46.755 322.508,-47.8l3.142,0c127.853,1.045 250.917,14.886 322.583,47.8c0,0 142.138,63.585 142.138,280.516c0,0 1.783,160.053 -19.823,271.174"
style="fill-rule:nonzero;" />
<path
d="M798.748,345.11l0,262.667l-104.07,0l0,-254.946c0,-53.743 -22.614,-81.021 -67.847,-81.021c-50.012,0 -75.077,32.359 -75.077,96.343l0,139.547l-103.457,0l0,-139.547c0,-63.984 -25.07,-96.343 -75.082,-96.343c-45.233,0 -67.847,27.278 -67.847,81.021l0,254.946l-104.07,0l0,-262.667c0,-53.683 13.669,-96.343 41.127,-127.904c28.314,-31.561 65.395,-47.741 111.425,-47.741c53.256,0 93.585,20.468 120.251,61.41l25.922,43.451l25.927,-43.451c26.66,-40.942 66.99,-61.41 120.251,-61.41c46.025,0 83.106,16.18 111.425,47.741c27.453,31.561 41.122,74.221 41.122,127.904"
style="fill:#fff;fill-rule:nonzero;" />
</g>
</svg>
</a>
<a class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
href="https://matrix.to/#/#fluffychat:matrix.org">
<svg class="fill-current h-6" enable-background="new -91 49.217 56.693 56.693" id="Layer_1" version="1.1"
viewBox="-91 49.217 56.693 56.693" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path
d="M-38.3289,79.8244c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.5351,1.5264l-3.0737-9.1321l4.4169-1.4866 c2.2362-0.7526,3.4388-3.1756,2.6861-5.4117c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.4168,1.4866l-1.4877-4.4201 c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861v0c-2.2362,0.7526-3.4388,3.1756-2.6861,5.4117l1.4877,4.4201l-9.3246,3.1385 l-1.4697-4.3666c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117l1.4697,4.3666 l-4.445,1.4961c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117v0c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861l4.445-1.4961 l3.0737,9.1321l-4.3268,1.4563c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861 l4.3268-1.4563l1.5778,4.6877c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5778-4.6877 l9.3246-3.1385l1.5598,4.6342c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5598-4.6342 l4.5351-1.5264C-38.7789,84.4835-37.5762,82.0606-38.3289,79.8244z M-65.6982,84.5288l-3.0737-9.1321l9.3246-3.1385l3.0737,9.1321 L-65.6982,84.5288z" />
</svg>
</a>
<a class="inline-block no-underline hover:scale-105 transition-all text-center h-auto p-4"
href="https://ko-fi.com/krille">
<img src="kofi_button_dark.png" class="h-6 fill-current" />
</a>
</div>
<!--Footer-->
<div class="w-full text-sm text-center max-w-lg">
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat">Source
code</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md">Privacy</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/CHANGELOG.md">Changelog</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://gitlab.com/famedly/fluffychat/-/blob/main/docs/fdroid_repo.md">FluffyChat F-Droid
repository</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
-
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
href="https://krillefear.gitlab.io">Created
by Krille Fear</a>
</div>
</body>
</html>
</html>

BIN
docs/kofi_button_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,49 +1,188 @@
import 'dart:developer';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:integration_test/integration_test.dart';
import 'package:fluffychat/main.dart' as app;
import 'package:shared_preferences/shared_preferences.dart';
import 'extensions/default_flows.dart';
import 'extensions/wait_for.dart';
import 'users.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Integration Test', () {
testWidgets('Test if the app starts', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
group(
'Integration Test',
() {
setUpAll(
() async {
// this random dialog popping up is super hard to cover in tests
SharedPreferences.setMockInitialValues({
SettingKeys.showNoGoogle: false,
});
try {
Hive.deleteFromDisk();
Hive.initFlutter();
} catch (_) {}
},
);
await Future.delayed(const Duration(seconds: 10));
testWidgets(
'Start app, login and logout',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.ensureLoggedOut();
},
);
await tester.pumpAndSettle();
testWidgets(
'Login again',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
},
);
expect(find.text('Connect'), findsOneWidget);
testWidgets(
'Start chat and send message',
(WidgetTester tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.waitFor(find.byType(TextField));
await tester.enterText(find.byType(TextField), Users.user2.name);
await tester.pumpAndSettle();
final input = find.byType(TextField);
await tester.scrollUntilVisible(
find.text('Chats').first,
500,
scrollable: find
.descendant(
of: find.byType(ChatListViewBody),
matching: find.byType(Scrollable),
)
.first,
);
await tester.pumpAndSettle();
await tester.tap(find.text('Chats'));
await tester.pumpAndSettle();
await tester.waitFor(find.byType(SearchTitle));
await tester.pumpAndSettle();
expect(input, findsOneWidget);
await tester.scrollUntilVisible(
find.text(Users.user2.name).first,
500,
scrollable: find
.descendant(
of: find.byType(ChatListViewBody),
matching: find.byType(Scrollable),
)
.first,
);
await tester.pumpAndSettle();
await tester.tap(find.text(Users.user2.name).first);
await tester.enterText(input, homeserver);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
try {
await tester.waitFor(
find.byType(ChatView),
timeout: const Duration(seconds: 5),
);
} catch (_) {
// in case the homeserver sends the username as search result
if (find.byIcon(Icons.send_outlined).evaluate().isNotEmpty) {
await tester.tap(find.byIcon(Icons.send_outlined));
await tester.pumpAndSettle();
}
}
// in case registration is allowed
try {
await tester.tap(find.text('Login'));
await tester.waitFor(find.byType(ChatView));
await tester.enterText(find.byType(TextField).last, 'Test');
await tester.pumpAndSettle();
try {
await tester.waitFor(find.byIcon(Icons.send_outlined));
await tester.tap(find.byIcon(Icons.send_outlined));
} catch (_) {
await tester.testTextInput.receiveAction(TextInputAction.done);
}
await tester.pumpAndSettle();
await tester.waitFor(find.text('Test'));
await tester.pumpAndSettle();
},
);
testWidgets('Spaces', (tester) async {
app.main();
await tester.ensureAppStartedHomescreen();
await tester.waitFor(find.byTooltip('Show menu'));
await tester.tap(find.byTooltip('Show menu'));
await tester.pumpAndSettle();
} catch (e) {
log('Registration is not allowed. Proceeding with login...');
}
await tester.pumpAndSettle();
final inputs = find.byType(TextField);
await tester.waitFor(find.byIcon(Icons.workspaces_outlined));
await tester.tap(find.byIcon(Icons.workspaces_outlined));
await tester.pumpAndSettle();
await tester.enterText(inputs.first, Users.user1.name);
await tester.enterText(inputs.last, Users.user1.password);
await tester.testTextInput.receiveAction(TextInputAction.done);
});
});
await tester.waitFor(find.byType(TextField));
await tester.enterText(find.byType(TextField).last, 'Test Space');
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
await tester.waitFor(find.text('Invite contact'));
await tester.tap(find.text('Invite contact'));
await tester.pumpAndSettle();
await tester.waitFor(
find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)),
);
await tester.enterText(
find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)),
Users.user2.name,
);
await Future.delayed(const Duration(milliseconds: 250));
await tester.testTextInput.receiveAction(TextInputAction.done);
await Future.delayed(const Duration(milliseconds: 1000));
await tester.pumpAndSettle();
await tester.tap(find
.descendant(
of: find.descendant(
of: find.byType(InvitationSelectionView),
matching: find.byType(ListTile),
),
matching: find.text(Users.user2.name))
.last);
await tester.pumpAndSettle();
await tester.waitFor(find.maybeUppercaseText('Yes'));
await tester.tap(find.maybeUppercaseText('Yes'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
await tester.waitFor(find.text('Load 2 more participants'));
await tester.tap(find.text('Load 2 more participants'));
await tester.pumpAndSettle();
expect(find.text(Users.user2.name), findsOneWidget);
});
},
);
}

View File

@ -0,0 +1,182 @@
import 'dart:developer';
import 'package:fluffychat/pages/chat_list/chat_list_body.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pages/settings_account/settings_account_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../users.dart';
import 'wait_for.dart';
extension DefaultFlowExtensions on WidgetTester {
Future<void> login() async {
final tester = this;
await tester.pumpAndSettle();
await tester.waitFor(find.text('Let\'s start'));
expect(find.text('Let\'s start'), findsOneWidget);
final input = find.byType(TextField);
expect(input, findsOneWidget);
// getting the placeholder in place
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.enterText(input, homeserver);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// in case registration is allowed
// try {
await Future.delayed(const Duration(milliseconds: 50));
await tester.scrollUntilVisible(
find.text('Login'),
500,
scrollable: find.descendant(
of: find.byKey(const Key('ConnectPageListView')),
matching: find.byType(Scrollable).first,
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
/*} catch (e) {
log('Registration is not allowed. Proceeding with login...');
}*/
await tester.pumpAndSettle();
await Future.delayed(const Duration(milliseconds: 50));
final inputs = find.byType(TextField);
await tester.enterText(inputs.first, Users.user1.name);
await tester.enterText(inputs.last, Users.user1.password);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
try {
// pumpAndSettle does not work in here as setState is called
// asynchronously
await tester.waitFor(
find.byType(LinearProgressIndicator),
timeout: const Duration(milliseconds: 1500),
skipPumpAndSettle: true,
);
} catch (_) {
// in case the input action does not work on the desired platform
if (find.text('Login').evaluate().isNotEmpty) {
await tester.tap(find.text('Login'));
}
}
try {
await tester.pumpAndSettle();
} catch (_) {
// may fail because of ongoing animation below dialog
}
await tester.waitFor(
find.byType(ChatListViewBody),
skipPumpAndSettle: true,
);
}
/// ensure PushProvider check passes
Future<void> acceptPushWarning() async {
final tester = this;
final matcher = find.maybeUppercaseText('Do not show again');
try {
await tester.waitFor(matcher, timeout: const Duration(seconds: 5));
// the FCM push error dialog to be handled...
await tester.tap(matcher);
await tester.pumpAndSettle();
} catch (_) {}
}
Future<void> ensureLoggedOut() async {
final tester = this;
await tester.pumpAndSettle();
if (find.byType(ChatListViewBody).evaluate().isNotEmpty) {
await tester.tap(find.byTooltip('Show menu'));
await tester.pumpAndSettle();
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(
find.text('Account'),
500,
scrollable: find.descendant(
of: find.byKey(const Key('SettingsListViewContent')),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Account'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(
find.text('Logout'),
500,
scrollable: find.descendant(
of: find.byType(SettingsAccountView),
matching: find.byType(Scrollable),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Logout'));
await tester.pumpAndSettle();
await tester.tap(find.maybeUppercaseText('Yes'));
await tester.pumpAndSettle();
}
}
Future<void> ensureAppStartedHomescreen({
Duration timeout = const Duration(seconds: 20),
}) async {
final tester = this;
await tester.pumpAndSettle();
final homeserverPickerFinder = find.byType(HomeserverPicker);
final chatListFinder = find.byType(ChatListViewBody);
final end = DateTime.now().add(timeout);
log(
'Waiting for HomeserverPicker or ChatListViewBody...',
name: 'Test Runner',
);
do {
if (DateTime.now().isAfter(end)) {
throw Exception(
'Timed out waiting for HomeserverPicker or ChatListViewBody');
}
await pumpAndSettle();
await Future.delayed(const Duration(milliseconds: 100));
} while (homeserverPickerFinder.evaluate().isEmpty &&
chatListFinder.evaluate().isEmpty);
if (homeserverPickerFinder.evaluate().isNotEmpty) {
log(
'Found HomeserverPicker, performing login.',
name: 'Test Runner',
);
await tester.login();
} else {
log(
'Found ChatListViewBody, skipping login.',
name: 'Test Runner',
);
}
await tester.acceptPushWarning();
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
/// Workaround for https://github.com/flutter/flutter/issues/88765
extension WaitForExtension on WidgetTester {
Future<void> waitFor(
Finder finder, {
Duration timeout = const Duration(seconds: 20),
bool skipPumpAndSettle = false,
}) async {
final end = DateTime.now().add(timeout);
do {
if (DateTime.now().isAfter(end)) {
throw Exception('Timed out waiting for $finder');
}
if (!skipPumpAndSettle) {
await pumpAndSettle();
}
await Future.delayed(const Duration(milliseconds: 100));
} while (finder.evaluate().isEmpty);
}
}
extension MaybeUppercaseFinder on CommonFinders {
/// On Android some button labels are in uppercase while on iOS they
/// are not. This method tries both.
Finder maybeUppercaseText(
String text, {
bool findRichText = false,
bool skipOffstage = true,
}) {
try {
final finder = find.text(
text.toUpperCase(),
findRichText: findRichText,
skipOffstage: skipOffstage,
);
expect(finder, findsOneWidget);
return finder;
} catch (_) {
return find.text(
text,
findRichText: findRichText,
skipOffstage: skipOffstage,
);
}
}
}

View File

@ -1,15 +1,25 @@
import 'dart:io';
abstract class Users {
const Users._();
static final user1 = User(
Platform.environment['USER1_NAME'] ?? 'alice',
Platform.environment['USER1_PW'] ?? 'AliceInWonderland',
static const user1 = User(
String.fromEnvironment(
'USER1_NAME',
defaultValue: 'alice',
),
String.fromEnvironment(
'USER1_PW',
defaultValue: 'AliceInWonderland',
),
);
static final user2 = User(
Platform.environment['USER2_NAME'] ?? 'bob',
Platform.environment['USER2_PW'] ?? 'JoWirSchaffenDas',
static const user2 = User(
String.fromEnvironment(
'USER2_NAME',
defaultValue: 'bob',
),
String.fromEnvironment(
'USER2_PW',
defaultValue: 'JoWirSchaffenDas',
),
);
}
@ -20,5 +30,7 @@ class User {
const User(this.name, this.password);
}
final homeserver =
'http://${Platform.environment['HOMESERVER'] ?? 'localhost'}';
const homeserver = 'http://${const String.fromEnvironment(
'HOMESERVER',
defaultValue: 'localhost',
)}';

View File

@ -24,6 +24,8 @@ abstract class AppConfig {
static String get privacyUrl => _privacyUrl;
static const String enablePushTutorial =
'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services';
static const String encryptionTutorial =
'https://gitlab.com/famedly/fluffychat/-/wikis/How-to-use-end-to-end-encryption-in-FluffyChat';
static const String appId = 'im.fluffychat.FluffyChat';
static const String appOpenUrlScheme = 'im.fluffychat';
static String _webBaseUrl = 'https://fluffychat.im/web';

View File

@ -94,6 +94,13 @@ class AppRoutes {
VWidget(
path: '/archive',
widget: const Archive(),
stackedRoutes: [
VWidget(
path: ':roomid',
widget: const Chat(),
buildTransition: _dynamicTransition,
),
],
),
VWidget(
path: '/newprivatechat',
@ -220,13 +227,25 @@ class AppRoutes {
),
],
),
VWidget(
VNester(
path: '/archive',
widget: const TwoColumnLayout(
mainView: Archive(),
sideView: EmptyPage(),
widgetBuilder: (child) => TwoColumnLayout(
mainView: const Archive(),
sideView: child,
),
buildTransition: _fadeTransition,
nestedRoutes: [
VWidget(
path: '',
widget: const EmptyPage(),
buildTransition: _dynamicTransition,
),
VWidget(
path: ':roomid',
widget: const Chat(),
buildTransition: _dynamicTransition,
),
],
),
],
),

View File

@ -8,7 +8,6 @@ abstract class SettingKeys {
static const String showDirectChatsInSpaces =
'chat.fluffy.showDirectChatsInSpaces';
static const String separateChatTypes = 'chat.fluffy.separateChatTypes';
static const String chatColor = 'chat.fluffy.chat_color';
static const String sentry = 'sentry';
static const String theme = 'theme';
static const String amoledEnabled = 'amoled_enabled';

View File

@ -38,15 +38,15 @@ abstract class FluffyThemes {
subtitle2: fallbackTextStyle,
);
static ThemeData buildTheme(Brightness brightness,
[ColorScheme? colorScheme]) =>
static const Duration animationDuration = Duration(milliseconds: 250);
static const Curve animationCurve = Curves.easeInOut;
static ThemeData buildTheme(Brightness brightness, [Color? seed]) =>
ThemeData(
visualDensity: VisualDensity.standard,
useMaterial3: true,
brightness: brightness,
colorSchemeSeed: AppConfig.colorSchemeSeed ??
colorScheme?.primary ??
AppConfig.chatColor,
colorSchemeSeed: seed ?? AppConfig.colorSchemeSeed,
textTheme: PlatformInfos.isDesktop
? brightness == Brightness.light
? Typography.material2018().black.merge(fallbackTextTheme)
@ -58,8 +58,11 @@ abstract class FluffyThemes {
dividerColor: brightness == Brightness.light
? Colors.blueGrey.shade50
: Colors.blueGrey.shade900,
inputDecorationTheme: const InputDecorationTheme(
border: InputBorder.none,
inputDecorationTheme: InputDecorationTheme(
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
filled: true,
),
appBarTheme: AppBarTheme(
@ -72,6 +75,20 @@ abstract class FluffyThemes {
statusBarBrightness: brightness,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),

View File

@ -12,12 +12,12 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/add_story/add_story_view.dart';
import 'package:fluffychat/pages/add_story/invite_story_page.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/resize_image.dart';
import 'package:fluffychat/utils/story_theme_data.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
import '../../utils/matrix_sdk_extensions/client_stories_extension.dart';
class AddStoryPage extends StatefulWidget {
const AddStoryPage({Key? key}) : super(key: key);

View File

@ -6,7 +6,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';

View File

@ -21,11 +21,9 @@ class ArchiveController extends State<Archive> {
Future<List<Room>> getArchive(BuildContext context) async {
final archive = this.archive;
if (archive != null) return archive;
return await Matrix.of(context).client.loadArchive();
return this.archive = await Matrix.of(context).client.loadArchive();
}
void forgetAction(int i) => setState(() => archive?.removeAt(i));
void forgetAllAction() async {
final archive = this.archive;
if (archive == null) return;

View File

@ -21,10 +21,14 @@ class ArchiveView extends StatelessWidget {
leading: const BackButton(),
title: Text(L10n.of(context)!.archive),
actions: [
if (snapshot.hasData && archive != null && archive!.isNotEmpty)
TextButton(
onPressed: controller.forgetAllAction,
child: Text(L10n.of(context)!.clearArchive),
if (snapshot.data?.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
onPressed: controller.forgetAllAction,
label: Text(L10n.of(context)!.clearArchive),
icon: const Icon(Icons.cleaning_services_outlined),
),
)
],
),
@ -50,7 +54,6 @@ class ArchiveView extends StatelessWidget {
itemCount: archive!.length,
itemBuilder: (BuildContext context, int i) => ChatListItem(
archive![i],
onForget: controller.forgetAction,
),
);
}

View File

@ -139,6 +139,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
minLines: 4,
maxLines: 4,
readOnly: true,
style: const TextStyle(fontFamily: 'monospace'),
controller: TextEditingController(text: key),
),
const SizedBox(height: 16),
@ -254,16 +255,22 @@ class BootstrapDialogState extends State<BootstrapDialog> {
),
const Divider(height: 32),
TextField(
minLines: 1,
maxLines: 1,
minLines: 2,
maxLines: 2,
autocorrect: false,
readOnly: _recoveryKeyInputLoading,
autofillHints: _recoveryKeyInputLoading
? null
: [AutofillHints.password],
controller: _recoveryKeyTextEditingController,
style: const TextStyle(fontFamily: 'monospace'),
decoration: InputDecoration(
hintText: 'Abc123 Def456',
labelStyle: TextStyle(
fontFamily: Theme.of(context)
.textTheme
.bodyText1
?.fontFamily),
labelText: L10n.of(context)!.recoveryKey,
errorText: _recoveryKeyInputError,
),

View File

@ -16,19 +16,21 @@ import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:record/record.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'send_file_dialog.dart';
import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart';
@ -132,6 +134,51 @@ class ChatController extends State<Chat> {
bool showEmojiPicker = false;
bool get isLeftDMRoom {
final room = this.room;
final userId = room?.directChatMatrixID;
if (room == null || userId == null) return false;
return room.isDirectChat &&
room.unsafeGetUserFromMemoryOrFallback(userId).membership ==
Membership.leave;
}
void recreateChat() async {
final room = this.room;
final userId = room?.directChatMatrixID;
if (room == null || userId == null) {
throw Exception(
'Try to recreate a room with is not a DM room. This should not be possible from the UI!');
}
final success = await showFutureLoadingDialog(
context: context,
future: () async {
final client = room.client;
final waitForSync = client.onSync.stream
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
await room.leave();
await waitForSync;
return await client.startDirectChat(userId);
});
final roomId = success.result;
if (roomId == null) return;
VRouter.of(context).toSegments(['rooms', roomId]);
}
void leaveChat() async {
final room = this.room;
if (room == null) {
throw Exception(
'Leave room button clicked while room is null. This should not be possible from the UI!');
}
final success = await showFutureLoadingDialog(
context: context,
future: room.leave,
);
if (success.error != null) return;
VRouter.of(context).to('/rooms');
}
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
void requestHistory() async {
@ -172,10 +219,20 @@ class ChatController extends State<Chat> {
}
}
void _loadDraft() async {
final prefs = await SharedPreferences.getInstance();
final draft = prefs.getString('draft_$roomId');
if (draft != null && draft.isNotEmpty) {
sendController.text = draft;
setState(() => inputText = draft);
}
}
@override
void initState() {
scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
_loadDraft();
super.initState();
}
@ -257,6 +314,9 @@ class ChatController extends State<Chat> {
Future<void> send() async {
if (sendController.text.trim().isEmpty) return;
_storeInputTimeoutTimer?.cancel();
final prefs = await SharedPreferences.getInstance();
prefs.remove('draft_$roomId');
var parseCommands = true;
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
@ -377,9 +437,8 @@ class ChatController extends State<Chat> {
}
void sendStickerAction() async {
final sticker = await showModalBottomSheet<ImagePackImageContent>(
final sticker = await showAdaptiveBottomSheet<ImagePackImageContent>(
context: context,
useRootNavigator: false,
builder: (c) => StickerPickerDialog(room: room!),
);
if (sticker == null) return;
@ -413,6 +472,7 @@ class ChatController extends State<Chat> {
final result = await showDialog<RecordingResult>(
context: context,
useRootNavigator: false,
barrierDismissible: false,
builder: (c) => const RecordingDialog(),
);
if (result == null) return;
@ -584,6 +644,7 @@ class ChatController extends State<Chat> {
}
bool get canRedactSelectedEvents {
if (isArchived) return false;
final clients = matrix!.currentBundle;
for (final event in selectedEvents) {
if (event.canRedact == false &&
@ -593,7 +654,9 @@ class ChatController extends State<Chat> {
}
bool get canEditSelectedEvents {
if (selectedEvents.length != 1 || !selectedEvents.first.status.isSent) {
if (isArchived ||
selectedEvents.length != 1 ||
!selectedEvents.first.status.isSent) {
return false;
}
return currentRoomBundle
@ -711,6 +774,15 @@ class ChatController extends State<Chat> {
return sendEmojiAction(emoji.emoji);
}
void forgetRoom() async {
final result = await showFutureLoadingDialog(
context: context,
future: room!.forget,
);
if (result.error != null) return;
VRouter.of(context).to('/archive');
}
void typeEmoji(Emoji? emoji) {
if (emoji == null) return;
final text = sendController.text;
@ -919,7 +991,15 @@ class ChatController extends State<Chat> {
);
}
Timer? _storeInputTimeoutTimer;
static const Duration _storeInputTimeout = Duration(milliseconds: 500);
void onInputBarChanged(String text) {
_storeInputTimeoutTimer?.cancel();
_storeInputTimeoutTimer = Timer(_storeInputTimeout, () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('draft_$roomId', text);
});
setReadMarker();
if (text.endsWith(' ') && matrix!.hasComplexBundles) {
final clients = currentRoomBundle;
@ -954,6 +1034,9 @@ class ChatController extends State<Chat> {
setState(() => inputText = text);
}
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room?.membership);
void showEventInfo([Event? event]) =>
(event ?? selectedEvents.single).showInfoDialog(context);

View File

@ -5,7 +5,8 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
class ChatAppBarTitle extends StatelessWidget {
@ -26,7 +27,7 @@ class ChatAppBarTitle extends StatelessWidget {
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onTap: directChatMatrixID != null
? () => showModalBottomSheet(
? () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: room
@ -36,7 +37,10 @@ class ChatAppBarTitle extends StatelessWidget {
'${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ',
),
)
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),
: controller.isArchived
? null
: () =>
VRouter.of(context).toSegments(['rooms', room.id, 'details']),
child: Row(
children: [
Hero(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:fluffychat/config/themes.dart';
import 'chat.dart';
class ChatEmojiPicker extends StatelessWidget {
@ -11,7 +12,8 @@ class ChatEmojiPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: controller.showEmojiPicker
? MediaQuery.of(context).size.height / 2
: 0,

View File

@ -10,7 +10,8 @@ import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
class ChatEventList extends StatelessWidget {
@ -89,7 +90,7 @@ class ChatEventList extends StatelessWidget {
onSwipe: (direction) =>
controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showModalBottomSheet(
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback,

View File

@ -10,6 +10,7 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../config/themes.dart';
import '../../widgets/m2_popup_menu_button.dart';
import 'chat.dart';
import 'input_bar.dart';
@ -84,7 +85,8 @@ class ChatInputRow extends StatelessWidget {
controller.onAddPopupMenuButtonSelected('file'),
helpLabel: L10n.of(context)!.sendFile,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: 56,
width: controller.inputText.isEmpty ? 56 : 0,
alignment: Alignment.center,

View File

@ -108,6 +108,20 @@ class ChatView extends StatelessWidget {
],
),
];
} else if (controller.isArchived) {
return [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
onPressed: controller.forgetRoom,
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
icon: const Icon(Icons.delete_forever_outlined),
label: Text(L10n.of(context)!.delete),
),
)
];
} else {
return [
if (Matrix.of(context).voipPlugin != null &&
@ -145,6 +159,7 @@ class ChatView extends StatelessWidget {
context: context, future: () => controller.room!.join());
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
final colorScheme = Theme.of(context).colorScheme;
return VWidgetGuard(
onSystemPop: (redirector) async {
@ -213,6 +228,20 @@ class ChatView extends StatelessWidget {
height: double.infinity,
fit: BoxFit.cover,
filterQuality: FilterQuality.medium,
)
else
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
colors: [
colorScheme.primaryContainer.withAlpha(64),
colorScheme.secondaryContainer.withAlpha(64),
colorScheme.tertiaryContainer.withAlpha(64),
colorScheme.primaryContainer.withAlpha(64),
],
),
),
),
SafeArea(
child: Column(
@ -262,16 +291,53 @@ class ChatView extends StatelessWidget {
Brightness.light
? Colors.white
: Colors.black,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
child: controller.isLeftDMRoom
? Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(16),
foregroundColor:
Theme.of(context)
.colorScheme
.error,
),
icon: const Icon(
Icons.archive_outlined,
),
onPressed: controller.leaveChat,
label: Text(
L10n.of(context)!.leave,
),
),
TextButton.icon(
style: TextButton.styleFrom(
padding:
const EdgeInsets.all(16),
),
icon: const Icon(
Icons.chat_outlined,
),
onPressed:
controller.recreateChat,
label: Text(
L10n.of(context)!.reopenChat),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectionStatusHeader(),
ReactionsPicker(controller),
ReplyDisplay(controller),
ChatInputRow(controller),
ChatEmojiPicker(controller),
],
),
),
),
],

View File

@ -1,93 +1,46 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart';
import '../../widgets/matrix.dart';
class EncryptionButton extends StatefulWidget {
class EncryptionButton extends StatelessWidget {
final Room room;
const EncryptionButton(this.room, {Key? key}) : super(key: key);
@override
EncryptionButtonState createState() => EncryptionButtonState();
}
class EncryptionButtonState extends State<EncryptionButton> {
StreamSubscription? _onSyncSub;
void _enableEncryptionAction() async {
if (widget.room.encrypted) {
VRouter.of(context).toSegments(['rooms', widget.room.id, 'encryption']);
return;
}
if (widget.room.joinRules == JoinRules.public) {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
okLabel: L10n.of(context)!.ok,
message: L10n.of(context)!.noEncryptionForPublicRooms,
);
return;
}
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.enableEncryption,
message: widget.room.client.encryptionEnabled
? L10n.of(context)!.enableEncryptionWarning
: L10n.of(context)!.needPantalaimonWarning,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => widget.room.enableEncryption(),
);
// we want to enable the lock icon
setState(() {});
}
}
@override
void dispose() {
_onSyncSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.room.encrypted) {
_onSyncSub ??= Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.deviceLists != null)
.listen((s) => setState(() {}));
}
return FutureBuilder<EncryptionHealthState>(
future: widget.room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: widget.room.encrypted
? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled,
icon: Icon(
widget.room.encrypted
? Icons.lock_outlined
: Icons.lock_open_outlined,
size: 20,
color: widget.room.joinRules != JoinRules.public &&
!widget.room.encrypted
? Colors.red
: snapshot.data == EncryptionHealthState.unverifiedDevices
? Colors.orange
: null),
onPressed: _enableEncryptionAction,
));
return StreamBuilder<SyncUpdate>(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.deviceLists != null),
builder: (context, snapshot) {
return FutureBuilder<EncryptionHealthState>(
future: room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: room.encrypted
? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled,
icon: Icon(
room.encrypted
? Icons.lock_outlined
: Icons.lock_open_outlined,
size: 20,
color: room.joinRules != JoinRules.public &&
!room.encrypted
? Colors.red
: room.joinRules != JoinRules.public &&
snapshot.data ==
EncryptionHealthState.unverifiedDevices
? Colors.orange
: null),
onPressed: () => VRouter.of(context)
.toSegments(['rooms', room.id, 'encryption']),
));
});
}
}

View File

@ -6,11 +6,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
extension EventInfoDialogExtension on Event {
void showInfoDialog(BuildContext context) => showModalBottomSheet(
void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet(
context: context,
builder: (context) =>
EventInfoDialog(l10n: L10n.of(context)!, event: this),

View File

@ -8,8 +8,9 @@ import 'package:just_audio/just_audio.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import '../../../utils/matrix_sdk_extensions.dart/event_extension.dart';
import '../../../utils/matrix_sdk_extensions/event_extension.dart';
class AudioPlayerWidget extends StatefulWidget {
final Color color;
@ -101,6 +102,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
}
onAudioPositionChanged ??= audioPlayer.positionStream.listen((state) {
if (maxPosition <= 0) return;
setState(() {
statusText =
'${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}';
@ -109,9 +111,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
.round();
});
});
onDurationChanged ??= audioPlayer.durationStream.listen((max) => max == null
? null
: setState(() => maxPosition = max.inMilliseconds.toDouble()));
onDurationChanged ??= audioPlayer.durationStream.listen((max) {
if (max == null || max == Duration.zero) return;
setState(() => maxPosition = max.inMilliseconds.toDouble());
});
onPlayerStateChanged ??=
audioPlayer.playingStream.listen((_) => setState(() {}));
audioPlayer.setFilePath(audioFile!.path);
@ -169,10 +172,11 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
Widget build(BuildContext context) {
final statusText = this.statusText ??= _durationString ?? '00:00';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
padding: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(width: 4),
SizedBox(
width: buttonSize,
height: buttonSize,

View File

@ -17,11 +17,11 @@ class CuteContent extends StatefulWidget {
}
class _CuteContentState extends State<CuteContent> {
static final List<OverlayEntry> overlays = [];
static bool _isOverlayShown = false;
@override
void initState() {
if (AppConfig.autoplayImages && overlays.isEmpty) {
if (AppConfig.autoplayImages && !_isOverlayShown) {
addOverlay();
}
super.initState();
@ -55,52 +55,21 @@ class _CuteContentState extends State<CuteContent> {
);
}
Widget overlayBuilder(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final position = Size(
Random().nextInt(constraints.maxWidth.round() - 64).toDouble(),
Random().nextInt(constraints.maxHeight.round() - 64).toDouble());
return Padding(
padding: EdgeInsets.only(
top: position.height,
left: position.width,
bottom: constraints.maxHeight - 64 - position.height,
right: constraints.maxWidth - 64 - position.width),
child: SizedBox.square(
dimension: 64,
child: GestureDetector(
onTap: removeOverlay,
child: Text(
widget.event.text,
style: const TextStyle(fontSize: 48),
),
),
),
);
});
}
Future<void> addOverlay() async {
_isOverlayShown = true;
await Future.delayed(const Duration(milliseconds: 50));
for (int i = 0; i < 5; i++) {
final overlay = OverlayEntry(
builder: overlayBuilder,
);
Overlay.of(context)?.insert(overlay);
Future.delayed(Duration(seconds: Random().nextInt(35))).then((_) {
overlay.remove();
overlays.remove(overlay);
});
overlays.add(overlay);
}
}
void removeOverlay() {
if (overlays.isEmpty) return;
final overlay = overlays.removeLast();
overlay.remove();
OverlayEntry? overlay;
overlay = OverlayEntry(
builder: (context) => CuteEventOverlay(
emoji: widget.event.text,
onAnimationEnd: () {
_isOverlayShown = false;
overlay?.remove();
},
),
);
Overlay.of(context)?.insert(overlay);
}
generateLabel(User? user) {
@ -126,3 +95,101 @@ class _CuteContentState extends State<CuteContent> {
}
}
}
class CuteEventOverlay extends StatefulWidget {
final String emoji;
final VoidCallback onAnimationEnd;
const CuteEventOverlay({
Key? key,
required this.emoji,
required this.onAnimationEnd,
}) : super(key: key);
@override
State<CuteEventOverlay> createState() => _CuteEventOverlayState();
}
class _CuteEventOverlayState extends State<CuteEventOverlay>
with TickerProviderStateMixin {
final List<Size> items = List.generate(
50,
(index) => Size(
Random().nextDouble(),
4 + (Random().nextDouble() * 4),
),
);
AnimationController? controller;
@override
void initState() {
controller = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
);
controller?.forward();
controller?.addStatusListener(_hideOverlay);
super.initState();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller!,
builder: (context, _) => LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth - _CuteOverlayContent.size;
final height = constraints.maxHeight + _CuteOverlayContent.size;
return SizedBox(
height: constraints.maxHeight,
width: constraints.maxWidth,
child: Stack(
alignment: Alignment.bottomLeft,
fit: StackFit.expand,
children: items
.map(
(position) => Positioned(
left: position.width * width,
bottom: (height *
.25 *
position.height *
(controller?.value ?? 0)) -
_CuteOverlayContent.size,
child: _CuteOverlayContent(
emoji: widget.emoji,
),
),
)
.toList(),
),
);
},
),
);
}
void _hideOverlay(AnimationStatus status) {
if (status == AnimationStatus.completed) {
widget.onAnimationEnd.call();
}
}
}
class _CuteOverlayContent extends StatelessWidget {
static const double size = 64.0;
final String emoji;
const _CuteOverlayContent({Key? key, required this.emoji}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: size,
child: Text(
emoji,
style: const TextStyle(fontSize: 48),
),
);
}
}

View File

@ -8,7 +8,7 @@ import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart';
import '../../../config/setting_keys.dart';
import '../../../pages/image_viewer/image_viewer.dart';
import '../../../utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../utils/url_launcher.dart';
class HtmlMessage extends StatelessWidget {

View File

@ -85,7 +85,7 @@ class ImageBubble extends StatelessWidget {
child: Hero(
tag: event.eventId,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 1000),
duration: const Duration(seconds: 1),
child: Container(
constraints: maxSize
? BoxConstraints(

View File

@ -35,15 +35,15 @@ class MapBubble extends StatelessWidget {
center: LatLng(latitude, longitude),
zoom: zoom,
),
layers: [
TileLayerOptions(
children: [
TileLayer(
maxZoom: 20,
minZoom: 0,
urlTemplate:
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c'],
subdomains: const ['a', 'b', 'c'],
),
MarkerLayerOptions(
MarkerLayer(
rotate: true,
markers: [
Marker(

View File

@ -100,9 +100,13 @@ class Message extends StatelessWidget {
final noBubble = {
MessageTypes.Video,
MessageTypes.Image,
MessageTypes.Sticker,
MessageTypes.Sticker
}.contains(event.messageType) &&
!event.redacted;
final noPadding = {
MessageTypes.File,
MessageTypes.Audio,
}.contains(event.messageType);
if (ownMessage) {
color = displayEvent.status.isError
@ -187,7 +191,7 @@ class Message extends StatelessWidget {
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
padding: noBubble
padding: noBubble || noPadding
? EdgeInsets.zero
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
constraints: const BoxConstraints(

View File

@ -5,8 +5,9 @@ import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart';
@ -52,7 +53,7 @@ class MessageContent extends StatelessWidget {
}
event.requestKey();
final sender = event.senderFromMemoryOrFallback;
await showModalBottomSheet(
await showAdaptiveBottomSheet(
context: context,
builder: (context) => Scaffold(
appBar: AppBar(
@ -112,7 +113,12 @@ class MessageContent extends StatelessWidget {
case CuteEventContent.eventType:
return CuteContent(event);
case MessageTypes.Audio:
if (PlatformInfos.isMobile || PlatformInfos.isMacOS) {
if (PlatformInfos.isMobile || PlatformInfos.isMacOS
// || latformInfos.isLinux
// disabled until
// https://github.com/bleonard252/just_audio_mpv/issues/3 is
// fixed
) {
return AudioPlayerWidget(
event,
color: textColor,
@ -205,7 +211,7 @@ class MessageContent extends StatelessWidget {
default:
if (event.redacted) {
return FutureBuilder<User?>(
future: event.fetchSenderUser(),
future: event.redactedBecause?.fetchSenderUser(),
builder: (context, snapshot) {
return _ButtonContent(
label: L10n.of(context)!.redactedAnEvent(snapshot.data

View File

@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
class MessageDownloadContent extends StatelessWidget {
final Event event;
@ -30,45 +28,47 @@ class MessageDownloadContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(
Icons.file_download_outlined,
color: textColor,
),
title: Text(
filename,
maxLines: 1,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
trailing: PlatformInfos.isAndroid
? IconButton(
onPressed: () => event.shareFile(context),
tooltip: L10n.of(context)!.share,
icon: Icon(Icons.adaptive.share_outlined),
)
: null,
),
const Divider(),
Row(
children: [
Text(
filetype,
style: TextStyle(
color: textColor.withAlpha(150),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
Icons.file_download_outlined,
color: textColor,
),
),
const Spacer(),
if (sizeString != null)
const SizedBox(width: 16),
Text(
sizeString,
filename,
maxLines: 1,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row(
children: [
Text(
filetype,
style: TextStyle(
color: textColor.withAlpha(150),
),
),
],
const Spacer(),
if (sizeString != null)
Text(
sizeString,
style: TextStyle(
color: textColor.withAlpha(150),
),
),
],
),
),
],
),

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../config/app_config.dart';
import 'html_message.dart';

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../config/app_config.dart';
class StateMessage extends StatelessWidget {

View File

@ -13,7 +13,7 @@ import 'package:video_player/video_player.dart';
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
class EventVideoPlayer extends StatefulWidget {
final Event event;

View File

@ -9,7 +9,7 @@ import 'package:matrix_link_text/link_text.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/url_launcher.dart';
class PinnedEvents extends StatelessWidget {
@ -21,10 +21,11 @@ class PinnedEvents extends StatelessWidget {
BuildContext context, List<Event?> events) async {
final eventId = events.length == 1
? events.single?.eventId
: await showModalActionSheet<String>(
: await showConfirmationDialog<String>(
context: context,
title: L10n.of(context)!.pinMessage,
actions: events
.map((event) => SheetAction(
.map((event) => AlertDialogAction(
key: event?.eventId ?? '',
label: event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),

View File

@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import '../../config/themes.dart';
class ReactionsPicker extends StatelessWidget {
final ChatController controller;
@ -20,7 +21,8 @@ class ReactionsPicker extends StatelessWidget {
controller.room!.canSendDefaultMessages &&
controller.selectedEvents.isNotEmpty;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: (display) ? 56 : 0,
child: Material(
color: Colors.transparent,

View File

@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../config/themes.dart';
import 'chat.dart';
import 'events/reply_content.dart';
@ -14,7 +15,8 @@ class ReplyDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: controller.editEvent != null || controller.replyEvent != null
? 56
: 0,

View File

@ -22,8 +22,9 @@ class SeenByRow extends StatelessWidget {
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
height: seenByUsers.isEmpty ? 0 : 24,
duration: seenByUsers.isEmpty
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 300),
? Duration.zero
: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: controller.timeline!.events.isNotEmpty &&
controller.timeline!.events.first.senderId ==
Matrix.of(context).client.userID

View File

@ -24,8 +24,8 @@ class TypingIndicators extends StatelessWidget {
constraints:
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 2.5),
height: typingUsers.isEmpty ? 0 : Avatar.defaultSize + bottomPadding,
duration: const Duration(milliseconds: 300),
curve: Curves.bounceInOut,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: controller.timeline!.events.isNotEmpty &&
controller.timeline!.events.first.senderId ==
Matrix.of(context).client.userID

View File

@ -11,7 +11,7 @@ import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/chat_details/chat_details_view.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';

View File

@ -9,7 +9,7 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/content_banner.dart';

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import '../../widgets/avatar.dart';
import '../user_bottom_sheet/user_bottom_sheet.dart';
@ -28,7 +29,7 @@ class ParticipantListItem extends StatelessWidget {
return Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,
child: ListTile(
onTap: () => showModalBottomSheet(
onTap: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: user,

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart';
@ -19,49 +22,68 @@ class ChatEncryptionSettings extends StatefulWidget {
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
String? get roomId => VRouter.of(context).pathParameters['roomid'];
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
Future<void> unblock(DeviceKeys key) async {
if (key.blocked) {
await key.setBlocked(false);
}
}
Future<void> onSelected(
BuildContext context, String action, DeviceKeys key) async {
final room = Matrix.of(context).client.getRoomById(roomId!);
switch (action) {
case 'verify':
await unblock(key);
final req = key.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
break;
case 'verify_user':
await unblock(key);
final req =
await room!.client.userDeviceKeys[key.userId]!.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
break;
case 'block':
if (key.directVerified) {
await key.setVerified(false);
}
await key.setBlocked(true);
setState(() {});
break;
case 'unblock':
await unblock(key);
setState(() {});
break;
void enableEncryption(_) async {
if (room.encrypted) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.disableEncryptionWarning,
);
return;
}
if (room.joinRules == JoinRules.public) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.noEncryptionForPublicRooms,
);
return;
}
if (!room.canChangeStateEvent(EventTypes.Encryption)) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.noPermission,
);
return;
}
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context)!.areYouSure,
message: L10n.of(context)!.enableEncryptionWarning,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
);
if (consent != OkCancelResult.ok) return;
await showFutureLoadingDialog(
context: context,
future: () => room.enableEncryption(),
);
}
void startVerification() async {
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
}
void toggleDeviceKey(DeviceKeys key) {
setState(() {
key.setBlocked(!key.blocked);
});
}
@override

View File

@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/matrix_sdk_extensions.dart/device_extension.dart';
import '../../widgets/m2_popup_menu_button.dart';
class ChatEncryptionSettingsView extends StatelessWidget {
final ChatEncryptionSettingsController controller;
@ -19,184 +16,149 @@ class ChatEncryptionSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(controller.roomId!)!;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () =>
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
),
title: Text(L10n.of(context)!.tapOnDeviceToVerify),
elevation: 0,
),
body: MaxWidthBody(
withScrolling: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(L10n.of(context)!.deviceVerifyDescription),
leading: CircleAvatar(
backgroundColor: Theme.of(context).secondaryHeaderColor,
foregroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.lock),
final room = controller.room;
return StreamBuilder<Object>(
stream: room.client.onSync.stream.where(
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
builder: (context, _) => Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () => VRouter.of(context)
.toSegments(['rooms', controller.roomId!]),
),
title: Text(L10n.of(context)!.endToEndEncryption),
actions: [
TextButton(
onPressed: () => launch(AppConfig.encryptionTutorial),
child: Text(L10n.of(context)!.help),
),
],
),
),
const Divider(height: 1),
StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) {
return FutureBuilder<List<DeviceKeys>>(
future: room.getUserDeviceKeys(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2));
}
final deviceKeys = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: deviceKeys.length,
itemBuilder: (BuildContext context, int i) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (i == 0 ||
deviceKeys[i].userId !=
deviceKeys[i - 1].userId) ...{
const Divider(height: 1, thickness: 1),
M2PopupMenuButton(
onSelected: (dynamic action) => controller
.onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
final items = <PopupMenuEntry<String>>[];
if (room
.client
.userDeviceKeys[deviceKeys[i].userId]!
.verified ==
UserVerifiedStatus.unknown) {
items.add(PopupMenuItem(
value: 'verify_user',
child: Text(L10n.of(context)!.verifyUser),
));
}
return items;
},
child: ListTile(
leading: Avatar(
mxContent: room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.avatarUrl,
name: room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(),
),
title: Text(
room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(),
),
subtitle: Text(
deviceKeys[i].userId,
style: const TextStyle(
fontWeight: FontWeight.w300),
),
),
),
},
M2PopupMenuButton(
onSelected: (dynamic action) => controller
.onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
final items = <PopupMenuEntry<String>>[];
if (deviceKeys[i].blocked ||
!deviceKeys[i].verified) {
items.add(PopupMenuItem(
value: deviceKeys[i].userId ==
room.client.userID
? 'verify'
: 'verify_user',
child: Text(L10n.of(context)!.verifyStart),
));
body: ListView(
children: [
SwitchListTile(
secondary: CircleAvatar(
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
child: const Icon(Icons.lock_outlined)),
title: Text(L10n.of(context)!.encryptThisChat),
value: room.encrypted,
onChanged: controller.enableEncryption,
),
Center(
child: Image.asset(
'assets/encryption.png',
width: 212,
),
),
const Divider(height: 1),
if (room.isDirectChat)
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: ElevatedButton.icon(
onPressed: controller.startVerification,
icon: const Icon(Icons.verified_outlined),
label: Text(L10n.of(context)!.verifyStart),
),
),
),
if (room.encrypted) ...[
const Divider(height: 1),
ListTile(
title: Text(
L10n.of(context)!.deviceKeys,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) =>
FutureBuilder<List<DeviceKeys>>(
future: room.getUserDeviceKeys(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
);
}
if (deviceKeys[i].blocked) {
items.add(PopupMenuItem(
value: 'unblock',
child:
Text(L10n.of(context)!.unblockDevice),
));
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2));
}
if (!deviceKeys[i].blocked) {
items.add(PopupMenuItem(
value: 'block',
child: Text(L10n.of(context)!.blockDevice),
));
}
return items;
},
child: ListTile(
leading: CircleAvatar(
foregroundColor: Colors.white,
backgroundColor: deviceKeys[i].color,
child: Icon(deviceKeys[i].icon),
),
title: Text(
deviceKeys[i].displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [
Text(
deviceKeys[i].deviceId!,
style: const TextStyle(
fontWeight: FontWeight.w300),
final deviceKeys = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: deviceKeys.length,
itemBuilder: (BuildContext context, int i) =>
SwitchListTile(
value: !deviceKeys[i].blocked,
activeColor: deviceKeys[i].verified
? Colors.green
: Colors.orange,
onChanged: (_) => controller
.toggleDeviceKey(deviceKeys[i]),
title: Row(
children: [
Expanded(
child: Text(
deviceKeys[i].deviceId ??
L10n.of(context)!.unknownDevice,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0),
child: Chip(
label: Text(
deviceKeys[i].userId,
style: const TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
),
),
],
),
const Spacer(),
Text(
deviceKeys[i].blocked
? L10n.of(context)!.blocked
: deviceKeys[i].verified
? L10n.of(context)!.verified
: L10n.of(context)!.unverified,
style: TextStyle(
fontSize: 14,
color: deviceKeys[i].color,
subtitle: Text(
deviceKeys[i]
.ed25519Key
?.replaceAllMapped(
RegExp(r'.{4}'),
(s) => '${s.group(0)} ') ??
L10n.of(context)!
.unknownEncryptionAlgorithm,
style: const TextStyle(
fontFamily: 'monospace',
),
),
],
),
),
),
],
),
);
}),
),
] else
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
L10n.of(context)!.encryptionNotEnabled,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
);
},
);
}),
],
),
),
);
),
),
],
),
));
}
}
extension on DeviceKeys {
Color get color => blocked
? Colors.red
: verified
? Colors.green
: Colors.orange;
}

View File

@ -17,10 +17,10 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/utils/famedlysdk_store.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import '../../../utils/account_bundles.dart';
import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart';
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
import '../../utils/url_launcher.dart';
import '../../utils/voip/callkeep_manager.dart';
import '../../widgets/fluffy_chat_app.dart';
@ -55,6 +55,7 @@ enum ActiveFilter {
class ChatList extends StatefulWidget {
static BuildContext? contextForVoip;
const ChatList({Key? key}) : super(key: key);
@override
@ -224,7 +225,7 @@ class ChatListController extends State<ChatList>
void onSearchEnter(String text) {
if (text.isEmpty) {
cancelSearch();
cancelSearch(unfocus: false);
return;
}
@ -235,12 +236,15 @@ class ChatListController extends State<ChatList>
_coolDown = Timer(const Duration(milliseconds: 500), _search);
}
void cancelSearch() => setState(() {
searchController.clear();
isSearchMode = false;
roomSearchResult = userSearchResult = null;
isSearching = false;
});
void cancelSearch({bool unfocus = true}) {
setState(() {
searchController.clear();
isSearchMode = false;
roomSearchResult = userSearchResult = null;
isSearching = false;
});
if (unfocus) FocusManager.instance.primaryFocus?.unfocus();
}
bool isTorBrowser = false;
@ -358,11 +362,13 @@ class ChatListController extends State<ChatList>
_hackyWebRTCFixForWeb();
CallKeepManager().initialize();
WidgetsBinding.instance.addPostFrameCallback((_) async {
searchServer = await Store().getItem(_serverStoreNamespace);
if (mounted) {
searchServer = await Store().getItem(_serverStoreNamespace);
Matrix.of(context).backgroundPush?.setupPush();
}
});
_checkTorBrowser();
Matrix.of(context).backgroundPush?.setupPush();
_onSyncStatus =
Matrix.of(context).client.onSyncStatus.stream.listen((status) {
Logs().v('Sync Status: ${status.status.name}');

Some files were not shown because too many files have changed in this diff Show More