Compare commits

..

No commits in common. "main" and "rc1.11.2-1" have entirely different histories.

158 changed files with 35434 additions and 36050 deletions

View File

@ -1,5 +1,5 @@
variables:
FLUTTER_VERSION: 3.10.0
FLUTTER_VERSION: 3.7.11
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
@ -19,7 +19,7 @@ code_analyze:
script:
- flutter pub get
- dart run import_sorter:main --no-comments --exit-if-changed
- dart format lib/ test/ --set-exit-if-changed
- flutter format lib/ test/ --set-exit-if-changed
- flutter analyze
- git apply ./scripts/enable-android-google-services.patch
- flutter pub get
@ -31,7 +31,7 @@ code_analyze:
widget_test:
stage: test
script: [flutter test]
script: [ flutter test ]
# the basic integration test configuration testing FLOSS builds on Synapse
.integration_test:
@ -84,6 +84,7 @@ widget_test:
- docker
- famedly
# integration tests for Linux builds
### disabled because of Linux headless issues
.integration_test_linux:
@ -97,7 +98,7 @@ widget_test:
- 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 --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: []
after_script: [ ]
artifacts:
# extending the default tests to test the Google-flavored builds
@ -143,7 +144,7 @@ release_mode_launches:
build_web:
stage: build
before_script:
[sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh]
[ sudo apt update && sudo apt install curl -y, ./scripts/prepare-web.sh ]
script:
- flutter build web --release --verbose --source-maps
artifacts:
@ -194,7 +195,7 @@ build_windows:
build_android_debug:
stage: build
script: [flutter build apk --debug]
script: [ flutter build apk --debug ]
artifacts:
when: on_success
paths:
@ -211,7 +212,7 @@ build_android_apk:
before_script:
- git apply ./scripts/enable-android-google-services.patch
- ./scripts/prepare-android-release.sh
script: [flutter build apk --release]
script: [ flutter build apk --release ]
artifacts:
when: on_success
paths:
@ -228,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:
@ -283,11 +284,11 @@ pages:
build_linux_x86:
stage: build
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/flutter-linux/stable:${FLUTTER_VERSION}
image: registry.gitlab.com/famedly/company/frontend/flutter-dockerimages/flutter-linux/stable
before_script:
- sudo apt-get update
- sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y
script: [flutter build linux --release]
script: [ flutter build linux --release -v ]
tags:
- docker
- famedly
@ -299,9 +300,18 @@ build_linux_x86:
build_linux_arm64:
stage: build
before_script:
- flutter upgrade $FLUTTER_VERSION --force
script: [flutter build linux --release]
tags: [docker_arm64]
# Workaround from https://github.com/flutter/flutter/issues/116703#issuecomment-1403956612
- rm -rf workaround116703
- mkdir workaround116703
- cd workaround116703
- curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz
- tar -xvf flutter_linux_${FLUTTER_VERSION}-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib
- mkdir -p /opt/flutter/bin/cache/artifacts/engine/linux-arm64
- cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter/bin/cache/artifacts/engine/linux-arm64
- rm -rf flutter flutter_linux_${FLUTTER_VERSION}-stable.tar.xz
- cd ..
script: [ flutter build linux --release -v ]
tags: [ docker_arm64 ]
only:
- main
- tags
@ -313,7 +323,7 @@ build_linux_arm64:
update_dependencies:
stage: build
needs: []
needs: [ ]
only:
- schedules
variables:
@ -350,29 +360,29 @@ upload_android:
extends: .release
script:
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file build/app/outputs/apk/release/app-release.apk ${PACKAGE_REGISTRY_URL}/fluffychat.apk
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file build/android/app-release.apk ${PACKAGE_REGISTRY_URL}/fluffychat.apk
upload_web:
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 --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-web.tar.gz
upload_linux_x86:
extends: .release
script:
- tar czf package.tar.gz -C build/linux/x64/release/bundle/ .
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-x86.tar.gz
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-x86.tar.gz
upload_linux_arm64:
extends: .release
script:
- tar czf package.tar.gz -C build/linux/arm64/release/bundle/ .
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
allow_failure: true
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.tar.gz ${PACKAGE_REGISTRY_URL}/fluffychat-linux-arm64.tar.gz
upload_windows:
extends: .release
@ -382,16 +392,15 @@ upload_windows:
- mv build/windows/runner/Release/fluffychat.msix fluffychat.msix
- cd build/windows/runner/Release; zip -r ../../../../package.zip . ; cd -
- |
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
allow_failure: true
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file package.zip ${PACKAGE_REGISTRY_URL}/fluffychat-windows.zip
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file fluffychat.msix ${PACKAGE_REGISTRY_URL}/fluffychat-windows.msix
deploy_playstore:
stage: deploy
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

@ -4,7 +4,7 @@
# This file should be version controlled.
version:
revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
channel: stable
project_type: app
@ -13,11 +13,26 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: android
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: ios
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: linux
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: macos
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: web
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
- platform: windows
create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849
# User provided section

View File

@ -1,67 +1,3 @@
## v1.12.0
- Added translation using Weblate (Toki Pona) (Mæve Rey)
- Translated using Weblate (Arabic) (Rex_sa)
- Translated using Weblate (Chinese (Simplified)) (Eric)
- Translated using Weblate (Croatian) (Milo Ivir)
- Translated using Weblate (Estonian) (Priit Jõerüüt)
- Translated using Weblate (Galician) (josé m)
- Translated using Weblate (Indonesian) (Linerly)
- Translated using Weblate (Polish) (lauren n. liberda)
- Translated using Weblate (Romanian) (Riley)
- Translated using Weblate (Russian) (DarkCoder15)
- Translated using Weblate (Spanish) (José Muñoz)
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
- build: Remove dependency overwrite for ffi (Krille)
- build: Update dependencies (Krille)
- builds: Change minsdkversion of Android from 16 to 19 (Krille)
- builds: Do not allow failure for linux x86 (Krille)
- builds: Do not use verbose mode on building linux (Krille)
- builds: Linux with flutter 3.10 (Krille)
- builds: Remove workaround for building linux arm64 (Krille)
- builds: Update file_picker to 5.3.0 (Krille)
- builds: Update flutter table html (Krille)
- builds: Update flutter_html (Krille)
- builds: migrate to dart 3.0/flutter 3.10 (lauren n. liberda)
- chore: Add missing blockquote style (Krille)
- chore: Allow failure in build linux for now (Krille)
- chore: Ask for storage persistence (Krille)
- chore: Clean unused translations (Malin Errenst)
- chore: Enhance room pills (Krille)
- chore: Minor code clean up (Krille)
- chore: Update flutter webrtc (Krille)
- chore: Upgrade to Flutter 3.10.1 (Malin Errenst)
- chore: change release curl calls to use --fail-with-body (Tim Flink)
- chore: update macOS icons and add build script (TheOneWithTheBraid)
- design: Replace anime images with neutral cupertino icons (Krille)
- feat: Add toggle to mute notifications from chat groups (fbievan)
- feat: Allow ruby tags in html (Krille)
- feat: Display progress value for initial sync (Krille)
- feat: Implement new error reporting tool when critical features break like playing audio or video messages or opening a chat (Krille)
- feat: clean up macOS build metadata (TheOneWithTheBraid)
- feat: set display information correctly (TheOneWithTheBraid)
- feat: update macOS build files (TheOneWithTheBraid)
- feat: update macOS build information for macOS Ventura (TheOneWithTheBraid)
- fix "Unhandled Exception: VRouter.of(context) was called with a context which does not contain a VRouter." (Lauren N. Liberda)
- fix: Broken arb file (Krille)
- fix: Do not unnecessary request all members in public rooms (Krille)
- fix: Remove wrong rendered linebreak in html (Krille)
- fix: Scroll down button (Krille)
- fix: Scroll up and scroll down buttons in chat list (Krille)
- fix: Scrolldown button (Krille)
- fix: Too long file name cause a render overflow (Skying)
- fix: Try to reload timeline on IOException (Krille)
- fix: User pills (Krille)
- fix: broken CI artifact uploads (TheOneWithTheBraid)
- fix: custom emote placeholder (TheOneWithTheBraid)
- fix: path of libolm (TheOneWithTheBraid)
- fix: Quick account switching (JHansen)
- fix: read reciepts (JHansen)
- perf: Use valuenotifier to not rebuild chatlist (Krille)
- refactor: Reimplement flutter matrix html locally (Krille)
- refactor: Update Roboto and Noto Emoji (The one with the Braid)
- refactor: Use AnimatedSize for FAB (Krille)
- refactor: Use DateTime for weekday localization (Malin Errenst)
## v1.11.2
- Translated using Weblate (Croatian) (Milo Ivir)
- Translated using Weblate (Dutch) (Jelv)

View File

@ -44,7 +44,7 @@ android {
defaultConfig {
applicationId "chat.fluffy.fluffychat"
minSdkVersion 19
minSdkVersion 16
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -27,6 +27,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
task clean(type: Delete) {
delete rootProject.buildDir
}

BIN
assets/encryption.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
{}
{}

View File

@ -71,6 +71,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Sala arxivada",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Accés dels usuaris convidats",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -395,6 +400,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Signatura creuada activada",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Actiu actualment",
"@currentlyActive": {
"type": "text",
@ -592,6 +602,11 @@
"type": "text",
"placeholders": {}
},
"friday": "divendres",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Des de la unió",
"@fromJoining": {
"type": "text",
@ -757,6 +772,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Les claus estan desades a la memòria cau",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} ha expulsat a {targetName}",
"@kicked": {
"type": "text",
@ -886,6 +906,11 @@
"type": "text",
"placeholders": {}
},
"monday": "dilluns",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Silencia el xat",
"@muteChat": {
"type": "text",
@ -1176,6 +1201,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "dissabte",
"@saturday": {
"type": "text",
"placeholders": {}
},
"security": "Seguretat",
"@security": {
"type": "text",
@ -1364,6 +1394,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "diumenge",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Sistema",
"@systemTheme": {
"type": "text",
@ -1379,6 +1414,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Aquesta sala ha estat arxivada.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "dijous",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1400,6 +1445,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "dimarts",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "No disponible",
"@unavailable": {
"type": "text",
@ -1560,6 +1610,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "dimecres",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Us hem enviat un missatge de correu electrònic",
"@weSentYouAnEmail": {
"type": "text",
@ -1746,6 +1801,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "El vostre id. dusuari:",
"@yourUserId": {},
"enableEncryption": "Activa el xifratge",
"@enableEncryption": {
"type": "text",
@ -2008,6 +2065,11 @@
"supportedVersions": {}
}
},
"discover": "Descobreix",
"@discover": {
"type": "text",
"placeholders": {}
},
"editChatPermissions": "Edita els permisos del xat",
"@editChatPermissions": {
"type": "text",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
{}

View File

@ -134,6 +134,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Archived Room",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Are guest users allowed to join",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -383,6 +388,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "Your user ID:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "Your chat backup has been set up.",
"@yourChatBackupHasBeenSetUp": {},
"chatBackup": "Chat backup",
@ -626,6 +633,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Cross-signing on",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Currently active",
"@currentlyActive": {
"type": "text",
@ -711,11 +723,6 @@
"type": "text",
"placeholders": {}
},
"allRooms": "All Group Chats",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"discover": "Discover",
"@discover": {
"type": "text",
@ -889,6 +896,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Friday",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "From joining",
"@fromJoining": {
"type": "text",
@ -1079,6 +1091,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Keys are cached",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} kicked {targetName}",
"@kicked": {
"type": "text",
@ -1143,6 +1160,8 @@
"@dehydrate": {},
"dehydrateWarning": "This action cannot be undone. Ensure you safely store the backup file.",
"@dehydrateWarning": {},
"dehydrateShare": "This is your private FluffyChat export. Ensure you don't lose it and keep it private.",
"@dehydrateShare": {},
"dehydrateTor": "TOR Users: Export session",
"@dehydrateTor": {},
"dehydrateTorLong": "For TOR users, it is recommended to export the session before closing the window.",
@ -1217,11 +1236,17 @@
"type": "text",
"placeholders": {}
},
"noSearchResult": "No matching search results.",
"moderator": "Moderator",
"@moderator": {
"type": "text",
"placeholders": {}
},
"monday": "Monday",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Mute chat",
"@muteChat": {
"type": "text",
@ -1287,6 +1312,8 @@
},
"shareYourInviteLink": "Share your invite link",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "Type in invite link manually...",
"@typeInInviteLinkManually": {},
"scanQrCode": "Scan QR code",
"@scanQrCode": {},
"none": "None",
@ -1644,6 +1671,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Saturday",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Save file",
"@saveFile": {
"type": "text",
@ -1897,6 +1929,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Sunday",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Synchronizing… Please wait.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1917,6 +1954,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "This room has been archived.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Thursday",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1953,6 +2000,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Tuesday",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Unavailable",
"@unavailable": {
"type": "text",
@ -2125,6 +2177,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Wednesday",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "We sent you an email",
"@weSentYouAnEmail": {
"type": "text",
@ -2379,6 +2436,8 @@
"@stories": {},
"users": "Users",
"@users": {},
"enableAutoBackups": "Enable auto backups",
"@enableAutoBackups": {},
"unlockOldMessages": "Unlock old messages",
"@unlockOldMessages": {},
"storeInSecureStorageDescription": "Store the recovery key in the secure storage of this device.",
@ -2471,10 +2530,5 @@
"jumpToLastReadMessage": "Jump to last read message",
"readUpToHere": "Read up to here",
"jump": "Jump",
"openLinkInBrowser": "Open link in browser",
"reportErrorDescription": "Oh no. Something went wrong. Please try again later. If you want, you can report the bug to the developers.",
"report": "report",
"signInWithPassword": "Sign in with password",
"continueWith": "Continue with:",
"pleaseTryAgainLaterOrChooseDifferentServer": "Please try again later or choose a different server."
"openLinkInBrowser": "Open link in browser"
}

View File

@ -81,6 +81,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arĥivita ĉambro",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Ĉu gastoj rajtas aliĝi",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -528,6 +533,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Delegaj subskriboj estas ŝaltitaj",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Nun aktiva",
"@currentlyActive": {
"type": "text",
@ -613,6 +623,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Trovi",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Prezenta nomo ŝanĝiĝis",
"@displaynameHasBeenChanged": {
"type": "text",
@ -779,6 +794,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Vendredo",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Ekde aliĝo",
"@fromJoining": {
"type": "text",
@ -969,6 +989,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Ŝlosiloj estas kaŝmemoritaj",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} forpelis uzanton {targetName}",
"@kicked": {
"type": "text",
@ -1093,6 +1118,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Lundo",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Silentigi babilon",
"@muteChat": {
"type": "text",
@ -1473,6 +1503,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sabato",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Konservi dosieron",
"@saveFile": {
"type": "text",
@ -1707,6 +1742,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Dimanĉo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Spegulante… Bonvolu atendi.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1727,6 +1767,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Ĉi tiu ĉambro arĥiviĝis.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Ĵaŭdo",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1763,6 +1813,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Mardo",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Nedisponeble",
"@unavailable": {
"type": "text",
@ -1933,6 +1988,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Merkredo",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Ni sendis retleteron al vi",
"@weSentYouAnEmail": {
"type": "text",

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arhiveeritud jututuba",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Kas külalised võivad liituda",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -539,6 +544,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Risttunnustamine on kasutusel",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Hetkel aktiivne",
"@currentlyActive": {
"type": "text",
@ -624,6 +634,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Avasta",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Kuvatav nimi on muudetud",
"@displaynameHasBeenChanged": {
"type": "text",
@ -790,6 +805,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Reede",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Alates liitumise hetkest",
"@fromJoining": {
"type": "text",
@ -980,6 +1000,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Krüptovõtmed on puhverdatud",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} müksas kasutaja {targetName} välja",
"@kicked": {
"type": "text",
@ -1114,6 +1139,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Esmaspäev",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Summuta vestlus",
"@muteChat": {
"type": "text",
@ -1507,6 +1537,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Laupäev",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Salvesta fail",
"@saveFile": {
"type": "text",
@ -1745,6 +1780,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Pühapäev",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Sünkroniseerin andmeid… Palun oota.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1765,6 +1805,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "See jututuba on arhiveeritud.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Neljapäev",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1801,6 +1851,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Teisipäev",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Eemal",
"@unavailable": {
"type": "text",
@ -1971,6 +2026,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Kolmapäev",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Me saatsime sulle e-kirja",
"@weSentYouAnEmail": {
"type": "text",
@ -2047,6 +2107,8 @@
"@addToSpace": {},
"scanQrCode": "Skaneeri QR-koodi",
"@scanQrCode": {},
"typeInInviteLinkManually": "Sisesta kutse link käsitsi...",
"@typeInInviteLinkManually": {},
"shareYourInviteLink": "Jaga oma kutselinki",
"@shareYourInviteLink": {},
"sendOnEnter": "Saada sõnum sisestusklahvi vajutusel",
@ -2073,6 +2135,8 @@
"@link": {},
"yourChatBackupHasBeenSetUp": "Sinu vestluste varundus on seadistatud.",
"@yourChatBackupHasBeenSetUp": {},
"yourUserId": "Sinu kasutajatunnus:",
"@yourUserId": {},
"unverified": "Verifitseerimata",
"@unverified": {},
"repeatPassword": "Korda salasõna",
@ -2310,6 +2374,8 @@
"@recoveryKey": {},
"users": "Kasutajad",
"@users": {},
"enableAutoBackups": "Võta kasutusele automaatne varundus",
"@enableAutoBackups": {},
"stories": "Jutustused",
"@stories": {},
"storeInSecureStorageDescription": "Salvesta taastevõti selle seadme turvahoidlas.",
@ -2336,6 +2402,8 @@
},
"dehydrate": "Ekspordi sessiooni teave ja kustuta nutiseadmest rakenduse andmed",
"@dehydrate": {},
"dehydrateShare": "See on sinu FluffyChat'i privaatsete andmete eksport. Palun hoia teda turvaliselt ja vaata, et sa seda ei kaotaks.",
"@dehydrateShare": {},
"dehydrateTor": "TOR'i kasutajad: Ekspordi sessioon",
"@dehydrateTor": {},
"hydrateTor": "TOR'i kasutajatele: impordi viimati eksporditud sessiooni andmed",
@ -2464,6 +2532,8 @@
"@deviceKeys": {},
"newSpaceDescription": "Kogukonnad võimaldavad sul koondada erinevaid vestlusi ning korraldada avalikku või privaatset ühistegevust.",
"@newSpaceDescription": {},
"noSearchResult": "Sobivaid otsingutulemusi ei leidu.",
"@noSearchResult": {},
"enterInviteLinkOrMatrixId": "Sisesta kutse link või Matrix ID...",
"@enterInviteLinkOrMatrixId": {},
"letsStart": "Sõidame!",
@ -2490,25 +2560,5 @@
"jump": "Hüppa",
"@jump": {},
"openLinkInBrowser": "Ava link veebibrauseris",
"@openLinkInBrowser": {},
"discover": "Otsi ja leia",
"@discover": {
"type": "text",
"placeholders": {}
},
"report": "teata",
"@report": {},
"allRooms": "Kõik vestlusrühmad",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "Oh appike! Midagi läks valesti. Palun proovi hiljem uuesti. Kui soovid, võid sellest veast arendajatele teatada.",
"@reportErrorDescription": {},
"continueWith": "Jätkamiseks kasuta:",
"@continueWith": {},
"signInWithPassword": "Logi sisse salasõnaga",
"@signInWithPassword": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Palun proovi hiljem uuesti või muuda serveri nime.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@openLinkInBrowser": {}
}

View File

@ -62,6 +62,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Artxibatutako gela",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Ba al dute gonbidatutako erabiltzaileek bat egiteko baimenik?",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -266,12 +271,12 @@
"type": "text",
"placeholders": {}
},
"compareEmojiMatch": "Konparatu ondorengo emojiak:",
"compareEmojiMatch": "Konparatu eta egiaztatu ondorengo emojiak beste gailukoarekin bat datozela:",
"@compareEmojiMatch": {
"type": "text",
"placeholders": {}
},
"compareNumbersMatch": "Konparatu ondorengo zenbakiak:",
"compareNumbersMatch": "Konparatu eta egiaztatu ondorengo zenbakiak beste gailukoarekin bat datozela:",
"@compareNumbersMatch": {
"type": "text",
"placeholders": {}
@ -332,6 +337,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Zeharkako sinadura gaituta dago",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Unean aktibo",
"@currentlyActive": {
"type": "text",
@ -484,12 +494,17 @@
"type": "text",
"placeholders": {}
},
"fromJoining": "Bat egiteaz geroztik",
"friday": "Ostirala",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "sartzeaz",
"@fromJoining": {
"type": "text",
"placeholders": {}
},
"fromTheInvitation": "Gonbidapenaz geroztik",
"fromTheInvitation": "gonbidapenaz",
"@fromTheInvitation": {
"type": "text",
"placeholders": {}
@ -614,6 +629,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Gakoak katxean daude",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username}(e)k {targetName} kaleratu du",
"@kicked": {
"type": "text",
@ -716,6 +736,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Astelehena",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Mututu berriketa",
"@muteChat": {
"type": "text",
@ -924,6 +949,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Larunbata",
"@saturday": {
"type": "text",
"placeholders": {}
},
"seenByUser": "{username}(e)k ikusi du",
"@seenByUser": {
"type": "text",
@ -1080,6 +1110,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Igandea",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Sistemak darabilena",
"@systemTheme": {
"type": "text",
@ -1095,6 +1130,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Gela hau artxibatu da.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Osteguna",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1106,6 +1151,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Asteartea",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unbannedUser": "{username}(e)k {targetName} baimendu du",
"@unbannedUser": {
"type": "text",
@ -1254,6 +1304,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Asteazkena",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"whoIsAllowedToJoinThisGroup": "Nork duen baimena talde honetara batzeko",
"@whoIsAllowedToJoinThisGroup": {
"type": "text",
@ -1470,7 +1525,7 @@
"type": "text",
"placeholders": {}
},
"unreadChats": "{unreadCount, plural, =1{irakurri gabeko txat 1} other {irakurri gabeko {unreadCount} txat}}",
"unreadChats": "{unreadCount, plural, =1{irakurri gabeko txat 1} other {{unreadCount} txat irakurri gabe}}",
"@unreadChats": {
"type": "text",
"placeholders": {
@ -1685,6 +1740,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "Zure erabiltzaile IDa:",
"@yourUserId": {},
"chatBackup": "Txataren babeskopia",
"@chatBackup": {
"type": "text",
@ -1816,6 +1873,8 @@
"type": "text",
"placeholders": {}
},
"typeInInviteLinkManually": "Idatzi eskuz gonbidapen esteka…",
"@typeInInviteLinkManually": {},
"online": "Linean",
"@online": {
"type": "text",
@ -1950,6 +2009,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Deskubritu",
"@discover": {
"type": "text",
"placeholders": {}
},
"emotePacks": "Emote sortak gelarako",
"@emotePacks": {
"type": "text",
@ -2000,7 +2064,7 @@
"type": "text",
"placeholders": {}
},
"noEncryptionForPublicRooms": "Zifraketa aktiba dezakezu soilik gelak publikoa izateari utzi badio.",
"noEncryptionForPublicRooms": "Zifraketa aktiba dezakezu soilik gela publikoa ez bada.",
"@noEncryptionForPublicRooms": {
"type": "text",
"placeholders": {}
@ -2232,7 +2296,7 @@
"@widgetEtherpad": {},
"widgetJitsi": "Jitsi Meet",
"@widgetJitsi": {},
"widgetCustom": "Norberak ezarritakoa",
"widgetCustom": "Neurrira egindakoa",
"@widgetCustom": {},
"widgetName": "Izena",
"@widgetName": {},
@ -2343,6 +2407,10 @@
"@hydrateTorLong": {},
"noEmailWarning": "Sartu baliozko posta helbide bat. Bestela ezingo duzu pasahitza berrezarri. Hala ere nahi ez baduzu, sakatu berriro botoia aurrera egiteko.",
"@noEmailWarning": {},
"enableAutoBackups": "Gaitu babeskopia automatikoak",
"@enableAutoBackups": {},
"dehydrateShare": "Hau zure FluffyChaten esportazio pribatua da. Ez galdu eta ez partekatu inorekin.",
"@dehydrateShare": {},
"dehydrateTor": "TOR Erabiltzaileak: Esportatu saioa",
"@dehydrateTor": {},
"hydrateTor": "TOR Erabiltzaileak: Inportatu esportatutako saioa",
@ -2430,7 +2498,7 @@
"@startFirstChat": {},
"newSpaceDescription": "Guneek txatak taldekatzea ahalbidetzen dute eta komunitate pribatu edo publikoak osatzea.",
"@newSpaceDescription": {},
"endToEndEncryption": "Ertzetik ertzerako zifratzea",
"endToEndEncryption": "Puntuz puntuko zifraketa",
"@endToEndEncryption": {},
"disableEncryptionWarning": "Segurtasun arrazoiak direla-eta, ezin duzu lehendik zifratuta zegoen txat bateko zifraketa ezgaitu.",
"@disableEncryptionWarning": {},
@ -2451,57 +2519,6 @@
"@sorryThatsNotPossible": {},
"reopenChat": "Ireki txata berriro",
"@reopenChat": {},
"commandHint_googly": "Bidali begi dibertigarri batzuk",
"@commandHint_googly": {},
"commandHint_cuddle": "Bidali besarkada goxoa",
"@commandHint_cuddle": {},
"googlyEyesContent": "{senderName}(e)k begi dibertigarri batzuk bidali dizkizu",
"@googlyEyesContent": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"allRooms": "Talde-txat guztiak",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"jumpToLastReadMessage": "Joan irakurritako azken mezura",
"@jumpToLastReadMessage": {},
"reportErrorDescription": "Ez! Zerbaitek huts egin du. Saiatu berriro geroago. Nahi izanez gero, eman garatzaileei errorearen berri.",
"@reportErrorDescription": {},
"cuddleContent": "{senderName}(e)k samurki besarkatu zaitu",
"@cuddleContent": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"readUpToHere": "Honaino irakurrita",
"@readUpToHere": {},
"discover": "Deskubritu",
"@discover": {
"type": "text",
"placeholders": {}
},
"fileHasBeenSavedAt": "Fitxategia {path}(e)n gorde da",
"@fileHasBeenSavedAt": {
"type": "text",
"placeholders": {
"path": {}
}
},
"jump": "Joan",
"@jump": {},
"openLinkInBrowser": "Ireki esteka nabigatzailean",
"@openLinkInBrowser": {},
"report": "eman berri",
"@report": {},
"signInWithPassword": "Hasi saioa pasahitzarekin",
"@signInWithPassword": {},
"continueWith": "Jarraitu honekin:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Saiatu geroago edo hautatu beste zerbitzari bat.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"noSearchResult": "Ez da emaitzarik aurkitu.",
"@noSearchResult": {}
}

View File

@ -79,6 +79,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "اتاق بایگانی شده",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areYouSure": "مطمئن هستید؟",
"@areYouSure": {
"type": "text",
@ -642,6 +647,11 @@
"type": "text",
"placeholders": {}
},
"discover": "کشف کردن",
"@discover": {
"type": "text",
"placeholders": {}
},
"copy": "کپی",
"@copy": {
"type": "text",
@ -672,6 +682,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "هويت‌ کاربری شما:",
"@yourUserId": {},
"commandHint_html": "متن با فرمت HTML بفرستید",
"@commandHint_html": {
"type": "text",
@ -739,6 +751,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "امضا کردن متقابل فعال",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"dateWithYear": "{year}-{month}-{day}",
"@dateWithYear": {
"type": "text",
@ -1081,6 +1098,11 @@
"type": "text",
"placeholders": {}
},
"friday": "جمعه",
"@friday": {
"type": "text",
"placeholders": {}
},
"groupDescription": "توصیف گروه",
"@groupDescription": {
"type": "text",
@ -1162,6 +1184,11 @@
"type": "text",
"placeholders": {}
},
"monday": "دوشنبه",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "بی‌صدا کردن گپ",
"@muteChat": {
"type": "text",
@ -1388,6 +1415,8 @@
"type": "text",
"placeholders": {}
},
"noSearchResult": "نتایج جستجوی منطبقی وجود ندارد.",
"@noSearchResult": {},
"moderator": "مدیر",
"@moderator": {
"type": "text",
@ -1398,6 +1427,8 @@
"type": "text",
"placeholders": {}
},
"typeInInviteLinkManually": "پیوند دعوت را به صورت دستی وارد کنید...",
"@typeInInviteLinkManually": {},
"noPermission": "بدون اجازه",
"@noPermission": {
"type": "text",
@ -1490,6 +1521,11 @@
"targetName": {}
}
},
"keysCached": "کلیدها در حافظه پنهان هستند",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"joinedTheChat": "👋 {username} به گپ پیوست",
"@joinedTheChat": {
"type": "text",
@ -1512,6 +1548,8 @@
},
"dehydrateWarning": "این عمل قابل لغو نیست. مطمئن شوید که فایل پشتیبان را به صورت امن ذخیره می کنید.",
"@dehydrateWarning": {},
"dehydrateShare": "این صدور فلافی‌چت خصوصی شماست. مطمئن شوید که آن را از دست نمی‌دهید و آن را خصوصی نگه می‌دارید.",
"@dehydrateShare": {},
"locationDisabledNotice": "خدمات مکان غیرفعال است. لطفا آن را فعال کنید تا بتوانید موقعیت مکانی خود را به اشتراک بگذارید.",
"@locationDisabledNotice": {
"type": "text",
@ -1707,6 +1745,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "شنبه",
"@saturday": {
"type": "text",
"placeholders": {}
},
"search": "جستجو",
"@search": {
"type": "text",
@ -1936,6 +1979,11 @@
"type": "text",
"placeholders": {}
},
"thursday": "پنج‌شنبه",
"@thursday": {
"type": "text",
"placeholders": {}
},
"nextAccount": "حساب بعدی",
"@nextAccount": {},
"editWidgets": "ویرایش ویجت‌ها",
@ -1979,6 +2027,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "سه‌شنبه",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "خارج از دسترس",
"@unavailable": {
"type": "text",
@ -2009,6 +2062,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "چهارشنبه",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"whoCanPerformWhichAction": "چه کسی توان انجام کدام عمل را داراست",
"@whoCanPerformWhichAction": {
"type": "text",
@ -2073,11 +2131,21 @@
},
"startFirstChat": "اولین گپ خود را شروع کنید",
"@startFirstChat": {},
"sunday": "یک‌شنبه",
"@sunday": {
"type": "text",
"placeholders": {}
},
"theyMatch": "با هم منطبق هستند",
"@theyMatch": {
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "این اتاق بایگانی شده است.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"title": "فلافی‌چت",
"@title": {
"description": "Title for the application",
@ -2477,30 +2545,10 @@
"@stories": {},
"users": "کاربرها",
"@users": {},
"enableAutoBackups": "فعال‌سازی پشتیبان‌گیری خودکار",
"@enableAutoBackups": {},
"storeInSecureStorageDescription": "کلید بازیابی را در محل ذخیره‌سازی امن این دستگاه ذخیره کنید.",
"@storeInSecureStorageDescription": {},
"jump": "پرش",
"@jump": {},
"discover": "کشف کنید",
"@discover": {
"type": "text",
"placeholders": {}
},
"allRooms": "تمام چت‌های گروهی",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"report": "گزارش",
"@report": {},
"openLinkInBrowser": "بازکردن پیوند در مرورگر",
"@openLinkInBrowser": {},
"reportErrorDescription": "اوه نه. اشتباهی رخ داد. لطفا بعدا تلاش کنید. اگر تمایل دارید، می‌توانید این اشکال را با توسعه‌دهندگان گزارش دهید.",
"@reportErrorDescription": {},
"signInWithPassword": "ورود با رمزعبور",
"@signInWithPassword": {},
"continueWith": "ادامه دادن با:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "لطفا بعدا تلاش کنید یا سرور دیگری انتخاب کنید.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@jump": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Dé Céadaoin",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"warning": "Rabhadh!",
"@warning": {
"type": "text",
@ -44,17 +49,32 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Dé Máirt",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
"type": "text",
"placeholders": {}
},
"thursday": "Déardaoin",
"@thursday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Córas",
"@systemTheme": {
"type": "text",
"placeholders": {}
},
"sunday": "Dé Domhnaigh",
"@sunday": {
"type": "text",
"placeholders": {}
},
"submit": "Cuir isteach",
"@submit": {
"type": "text",
@ -100,6 +120,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Dé Sathairn",
"@saturday": {
"type": "text",
"placeholders": {}
},
"reply": "Freagair",
"@reply": {
"type": "text",
@ -180,6 +205,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Tá cros-shíniú tosaithe",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"createNewSpace": "Spás nua",
"@createNewSpace": {
"type": "text",
@ -237,6 +267,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Dé Luain",
"@monday": {
"type": "text",
"placeholders": {}
},
"moderator": "Modhnóir",
"@moderator": {
"type": "text",
@ -317,6 +352,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Dé hAoine",
"@friday": {
"type": "text",
"placeholders": {}
},
"forward": "Seol ar aghaidh",
"@forward": {
"type": "text",
@ -344,6 +384,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Tar ar",
"@discover": {
"type": "text",
"placeholders": {}
},
"devices": "Gléasanna",
"@devices": {
"type": "text",
@ -489,6 +534,11 @@
},
"sendOnEnter": "Seol ar iontráil",
"@sendOnEnter": {},
"archivedRoom": "Seomra cartlainne",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"archive": "Cartlann",
"@archive": {
"type": "text",
@ -1262,6 +1312,8 @@
},
"scanQrCode": "Scan cód QR",
"@scanQrCode": {},
"typeInInviteLinkManually": "Clóscríobh an nasc cuiridh de láimh...",
"@typeInInviteLinkManually": {},
"inviteText": "Thug {username} cuireadh duit chuig FluffyChat.\n1. Suiteáil FluffyChat: https://fluffychat.im\n2. Cláraigh nó sínigh isteach\n3. Oscail an nasc cuiridh: {link}",
"@inviteText": {
"type": "text",
@ -1399,6 +1451,11 @@
"targetName": {}
}
},
"keysCached": "Cuirtear eochracha i dtaisce",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"joinedTheChat": "Tháinig {username} isteach sa chomhrá",
"@joinedTheChat": {
"type": "text",
@ -1683,6 +1740,11 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Tá an seomra seo curtha i gcartlann.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"theyDontMatch": "Níl siad céanna",
"@theyDontMatch": {
"type": "text",
@ -2091,6 +2153,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "D'aitheantas úsáideora:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "Bunaíodh do chúltaca comhrá.",
"@yourChatBackupHasBeenSetUp": {},
"openVideoCamera": "Oscail físcheamara",

View File

@ -23,7 +23,7 @@
"type": "text",
"placeholders": {}
},
"activatedEndToEndEncryption": "🔐 {username} activou a cifraxe extremo-a-extremo",
"activatedEndToEndEncryption": "🔐 {username} activou o cifrado extremo-a-extremo",
"@activatedEndToEndEncryption": {
"type": "text",
"placeholders": {
@ -82,6 +82,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Sala arquivada",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Permitir o acceso de convidadas",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -314,7 +319,7 @@
"type": "text",
"placeholders": {}
},
"channelCorruptedDecryptError": "A cifraxe está estragada",
"channelCorruptedDecryptError": "O cifrado está corrompido",
"@channelCorruptedDecryptError": {
"type": "text",
"placeholders": {}
@ -539,6 +544,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Sinatura-Cruzada activada",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Actualmente activo",
"@currentlyActive": {
"type": "text",
@ -624,6 +634,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Descubrir",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "O nome público mudou",
"@displaynameHasBeenChanged": {
"type": "text",
@ -704,12 +719,12 @@
"type": "text",
"placeholders": {}
},
"enableEncryption": "Activar cifraxe",
"enableEncryption": "Activar cifrado",
"@enableEncryption": {
"type": "text",
"placeholders": {}
},
"enableEncryptionWarning": "Non poderás desactivar a cifraxe posteriormente, tes certeza?",
"enableEncryptionWarning": "Non poderás desactivar o cifrado posteriormente, tes certeza?",
"@enableEncryptionWarning": {
"type": "text",
"placeholders": {}
@ -719,12 +734,12 @@
"type": "text",
"placeholders": {}
},
"encryption": "Cifraxe",
"encryption": "Cifrado",
"@encryption": {
"type": "text",
"placeholders": {}
},
"encryptionNotEnabled": "A cifraxe non está activada",
"encryptionNotEnabled": "O cifrado non está activado",
"@encryptionNotEnabled": {
"type": "text",
"placeholders": {}
@ -790,6 +805,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Venres",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Desde que se una",
"@fromJoining": {
"type": "text",
@ -980,6 +1000,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Chaves almacenadas",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} expulsou a {targetName}",
"@kicked": {
"type": "text",
@ -1114,12 +1139,17 @@
"type": "text",
"placeholders": {}
},
"monday": "Luns",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Acalar chat",
"@muteChat": {
"type": "text",
"placeholders": {}
},
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para a cifraxe extremo-a-extremo.",
"needPantalaimonWarning": "Ten en conta que polo de agora precisas Pantalaimon para o cifrado extremo-a-extremo.",
"@needPantalaimonWarning": {
"type": "text",
"placeholders": {}
@ -1159,7 +1189,7 @@
"type": "text",
"placeholders": {}
},
"noEncryptionForPublicRooms": "Só podes activar a cifraxe tan pronto como a sala non sexa públicamente accesible.",
"noEncryptionForPublicRooms": "Só podes activar o cifrado tan pronto como a sala non sexa públicamente accesible.",
"@noEncryptionForPublicRooms": {
"type": "text",
"placeholders": {}
@ -1507,6 +1537,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sábado",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Gardar ficheiro",
"@saveFile": {
"type": "text",
@ -1745,6 +1780,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Domingo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Sincronizando... Agarda.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1765,6 +1805,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "A sala foi arquivada.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Xoves",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1801,6 +1851,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Martes",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Non dispoñible",
"@unavailable": {
"type": "text",
@ -1824,7 +1879,7 @@
"type": "text",
"placeholders": {}
},
"unknownEncryptionAlgorithm": "Algoritmo de cifraxe descoñecido",
"unknownEncryptionAlgorithm": "Algoritmo de cifrado descoñecido",
"@unknownEncryptionAlgorithm": {
"type": "text",
"placeholders": {}
@ -1971,6 +2026,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Mércores",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Enviamosche un email",
"@weSentYouAnEmail": {
"type": "text",
@ -2047,6 +2107,8 @@
"@addToSpace": {},
"scanQrCode": "Escanear código QR",
"@scanQrCode": {},
"typeInInviteLinkManually": "Escribe manualmente a ligazón do convite...",
"@typeInInviteLinkManually": {},
"shareYourInviteLink": "Comparte a túa ligazón de convite",
"@shareYourInviteLink": {},
"sendOnEnter": "Enter para enviar",
@ -2067,6 +2129,8 @@
"@yourChatBackupHasBeenSetUp": {},
"unverified": "Sen verificar",
"@unverified": {},
"yourUserId": "O teu ID:",
"@yourUserId": {},
"pleaseEnterValidEmail": "Escribe un enderezo de email válido.",
"@pleaseEnterValidEmail": {},
"passwordsDoNotMatch": "Os contrasinais non concordan!",
@ -2103,7 +2167,7 @@
"type": "text",
"description": "Usage hint for the command /discardsession"
},
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar a cifraxe",
"commandHint_create": "Crear un grupo de conversa baleiro\nUsa --no-encryption para desactivar o cifrado",
"@commandHint_create": {
"type": "text",
"description": "Usage hint for the command /create"
@ -2113,7 +2177,7 @@
"type": "text",
"description": "Usage hint for the command /clearcache"
},
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar a cifraxe",
"commandHint_dm": "Iniciar un chat directo\nUsa --no-encryption para desactivar o cifrado",
"@commandHint_dm": {
"type": "text",
"description": "Usage hint for the command /dm"
@ -2324,6 +2388,8 @@
"@pleaseEnterRecoveryKeyDescription": {},
"users": "Usuarias",
"@users": {},
"enableAutoBackups": "Activar copia automática",
"@enableAutoBackups": {},
"storeInSecureStorageDescription": "Gardar a chave de recuperación na almacenaxe segura deste dispositivo.",
"@storeInSecureStorageDescription": {},
"countFiles": "{count} ficheiros",
@ -2334,6 +2400,8 @@
},
"unlockOldMessages": "Desbloquear mensaxes antigas",
"@unlockOldMessages": {},
"dehydrateShare": "Esta é a copia de apoio privada de FluffyChat. Pon coidado en non perdela e mantela segura.",
"@dehydrateShare": {},
"dehydrateTorLong": "Para usuarias de TOR, é recomendable exportar a sesión antes de pechar a ventál.",
"@dehydrateTorLong": {},
"hydrateTor": "Usuarias TOR: Importar a sesión exportada",
@ -2443,13 +2511,15 @@
"senderName": {}
}
},
"noSearchResult": "Non hai resultados para a busca.",
"@noSearchResult": {},
"enterInviteLinkOrMatrixId": "Escribe a ligazón de convite ou ID Matrix...",
"@enterInviteLinkOrMatrixId": {},
"encryptThisChat": "Cifrar esta conversa",
"@encryptThisChat": {},
"endToEndEncryption": "Cifraxe de extremo a extremo",
"endToEndEncryption": "Cifrado de extremo a extremo",
"@endToEndEncryption": {},
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar a cifraxe dunha conversa onde foi activada previamente.",
"disableEncryptionWarning": "Por razóns de seguridade non podes desactivar o cifrado dunha conversa onde foi activado previamente.",
"@disableEncryptionWarning": {},
"sorryThatsNotPossible": "Lamentámolo... iso non é posible",
"@sorryThatsNotPossible": {},
@ -2488,27 +2558,5 @@
"readUpToHere": "Lin ate aquí",
"@readUpToHere": {},
"openLinkInBrowser": "Abrir ligazón no navegador",
"@openLinkInBrowser": {},
"jump": "Ir alá",
"@jump": {},
"report": "informar",
"@report": {},
"allRooms": "Todas as Conversas en grupo",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "Vaia! Algo fallou. Inténtao máis tarde. Se queres, podes informar do problema aos desenvolvedores.",
"@reportErrorDescription": {},
"discover": "Descubrir",
"@discover": {
"type": "text",
"placeholders": {}
},
"signInWithPassword": "Accede con contrasinal",
"@signInWithPassword": {},
"continueWith": "Continuar con:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Inténtao máis tarde ou elixe un servidor diferente.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@openLinkInBrowser": {}
}

View File

@ -61,6 +61,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "חדר בארכיון",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "האם משתמשים אורחים מורשים להצטרף",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -338,6 +343,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "מזהה המשתמש שלך:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "גיבוי הצ'אט שלך הוגדר.",
"@yourChatBackupHasBeenSetUp": {},
"chatBackup": "גיבוי צ'אט",
@ -470,6 +477,11 @@
"username": {}
}
},
"crossSigningEnabled": "חתימה צולבת על",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "פעיל כעת",
"@currentlyActive": {
"type": "text",
@ -531,6 +543,11 @@
"type": "text",
"placeholders": {}
},
"discover": "לגלות",
"@discover": {
"type": "text",
"placeholders": {}
},
"downloadFile": "הורד קובץ",
"@downloadFile": {
"type": "text",
@ -613,6 +630,11 @@
"type": "text",
"placeholders": {}
},
"friday": "יום שישי",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "מהצטרפות",
"@fromJoining": {
"type": "text",
@ -733,6 +755,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "המפתחות נשמרים במטמון",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} בעט ב {targetName}",
"@kicked": {
"type": "text",
@ -1119,6 +1146,8 @@
},
"shareYourInviteLink": "שתף את קישור ההזמנה שלך",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "הקלד את קישור ההזמנה באופן ידני...",
"@typeInInviteLinkManually": {},
"noRoomsFound": "לא נמצאו חדרים…",
"@noRoomsFound": {
"type": "text",
@ -1283,6 +1312,11 @@
"type": "text",
"placeholders": {}
},
"monday": "יום שני",
"@monday": {
"type": "text",
"placeholders": {}
},
"noGoogleServicesWarning": "נראה שאין לך שירותי גוגל בטלפון שלך. זו החלטה טובה לפרטיות שלך! כדי לקבל התרעות ב- FluffyChat אנו ממליצים להשתמש https://microg.org/ או https://unifiedpush.org/.",
"@noGoogleServicesWarning": {
"type": "text",

View File

@ -77,6 +77,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arhivirana soba",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Smiju li se gosti pridružiti",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -529,6 +534,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Unakrsno potpisivanje uključeno",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Trenutačno aktivni",
"@currentlyActive": {
"type": "text",
@ -614,6 +624,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Otkrij",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Prikazno ime je promijenjeno",
"@displaynameHasBeenChanged": {
"type": "text",
@ -773,6 +788,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Petak",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Od pridruživanja",
"@fromJoining": {
"type": "text",
@ -963,6 +983,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Ključevi su spremljeni u predmemoriji",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} je izbacio/la {targetName}",
"@kicked": {
"type": "text",
@ -1087,6 +1112,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Ponedjeljak",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Isključi zvuk razgovora",
"@muteChat": {
"type": "text",
@ -1462,6 +1492,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Subota",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Spremi datoteku",
"@saveFile": {
"type": "text",
@ -1695,6 +1730,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Nedjelja",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Sinkronizira se … Pričekaj.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1715,6 +1755,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Ova soba je arhivirana.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Četvrtak",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1751,6 +1801,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Utorak",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Nedostupno",
"@unavailable": {
"type": "text",
@ -1921,6 +1976,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Srijeda",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Poslali smo ti e-poruku",
"@weSentYouAnEmail": {
"type": "text",
@ -2049,6 +2109,8 @@
"@serverRequiresEmail": {},
"scanQrCode": "Snimi QR kod",
"@scanQrCode": {},
"typeInInviteLinkManually": "Upiši poveznicu za pozivnicu …",
"@typeInInviteLinkManually": {},
"shareYourInviteLink": "Dijeli svoju poveznicu za pozivnicu",
"@shareYourInviteLink": {},
"homeserver": "Domaći poslužitelj",
@ -2071,6 +2133,8 @@
"@addAccount": {},
"oneClientLoggedOut": "Jedan od tvojih klijenata je odjavljen",
"@oneClientLoggedOut": {},
"yourUserId": "Tvoj korisnički ID:",
"@yourUserId": {},
"unverified": "Nepotvrđeno",
"@unverified": {},
"yourChatBackupHasBeenSetUp": "Sigurnosna kopija tvog razgovora je postavljena.",
@ -2317,6 +2381,10 @@
},
"dehydrate": "Izvezi sesiju i izbriši uređaj",
"@dehydrate": {},
"dehydrateShare": "Ovo je izvoz tvog privatnog FluffyChata. Pazi da ga ne izgubiš i zadrži ga privatnim.",
"@dehydrateShare": {},
"enableAutoBackups": "Uključi automatsko spremanje sigurnosnih kopija",
"@enableAutoBackups": {},
"unlockOldMessages": "Otključaj stare poruke",
"@unlockOldMessages": {},
"storeInSecureStorageDescription": "Ključ za obnavljanje spremi u sigurno spremište na ovom uređaju.",
@ -2485,30 +2553,12 @@
"senderName": {}
}
},
"noSearchResult": "Nema poklapajućih rezultata.",
"@noSearchResult": {},
"noKeyForThisMessage": "To se može dogoditi ako je poruka poslana prije prijave na tvoj račun na ovom uređaju.\n\nTakođer je moguće da je pošiljatelj blokirao tvoj uređaj ili je došlo do greške s internetskom vezom.\n\nMožeš li pročitati poruku na jednoj drugoj sesiji? U tom slučaju možeš prenijeti poruku iz nje! Idi na Postavke > Uređaji i uvjeri se da su se tvoji uređaji međusobno provjerili. Kada sljedeći put otvoriš sobu i obje sesije su u prednjem planu, ključevi će se automatski prenijeti.\n\nNe želiš izgubiti ključeve kada se odjaviš ili zamijeniš uređaje? Aktiviraj spremanje sigurnosne kopije razgovora u postavkama.",
"@noKeyForThisMessage": {},
"reopenChat": "Ponovo otvori razgovor",
"@reopenChat": {},
"openLinkInBrowser": "Otvori poveznicu u pregledniku",
"@openLinkInBrowser": {},
"discover": "Otkrij",
"@discover": {
"type": "text",
"placeholders": {}
},
"report": "prijavi",
"@report": {},
"allRooms": "Svi grupni razgovori",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "Dogodila se greška. Pokušaj ponovo kasnije. Ako želiš, grešku možeš prijaviti programerima.",
"@reportErrorDescription": {},
"signInWithPassword": "Prijavi se s lozinkom",
"@signInWithPassword": {},
"continueWith": "Nastavi sa:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Pokušaj ponovo kasnije ili odaberi jedan drugi poslužitelj.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@openLinkInBrowser": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -247,6 +247,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Jumat",
"@friday": {
"type": "text",
"placeholders": {}
},
"forward": "Teruskan",
"@forward": {
"type": "text",
@ -444,6 +449,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Tanda tangan silang dinyalakan",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"createNewGroup": "Buat grup baru",
"@createNewGroup": {
"type": "text",
@ -719,6 +729,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Ruangan yang Diarsipkan",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"appLock": "Kunci aplikasi",
"@appLock": {
"type": "text",
@ -870,6 +885,11 @@
"targetName": {}
}
},
"keysCached": "Kunci telah ditembolok",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"joinRoom": "Bergabung dengan ruangan",
"@joinRoom": {
"type": "text",
@ -947,6 +967,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Senin",
"@monday": {
"type": "text",
"placeholders": {}
},
"moderator": "Moderator",
"@moderator": {
"type": "text",
@ -1151,6 +1176,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Rabu",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"warning": "Peringatan!",
"@warning": {
"type": "text",
@ -1304,6 +1334,11 @@
"targetName": {}
}
},
"tuesday": "Selasa",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"tryToSendAgain": "Coba kirim lagi",
"@tryToSendAgain": {
"type": "text",
@ -1335,6 +1370,16 @@
"type": "text",
"placeholders": {}
},
"thursday": "Kamis",
"@thursday": {
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Ruangan ini telah diarsipkan.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"theyMatch": "Cocok",
"@theyMatch": {
"type": "text",
@ -1355,6 +1400,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Minggu",
"@sunday": {
"type": "text",
"placeholders": {}
},
"submit": "Kirim",
"@submit": {
"type": "text",
@ -1725,6 +1775,8 @@
},
"scanQrCode": "Pindai kode QR",
"@scanQrCode": {},
"typeInInviteLinkManually": "Masukkan tautan undangan secara manual...",
"@typeInInviteLinkManually": {},
"shareYourInviteLink": "Bagikan tautan undanganmu",
"@shareYourInviteLink": {},
"noMatrixServer": "{server1} itu bukan server Matrix, gunakan {server2} saja?",
@ -1833,6 +1885,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Temukan",
"@discover": {
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Level izin default",
"@defaultPermissionLevel": {
"type": "text",
@ -2011,6 +2068,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sabtu",
"@saturday": {
"type": "text",
"placeholders": {}
},
"removeDevice": "Hapus perangkat",
"@removeDevice": {
"type": "text",
@ -2072,6 +2134,8 @@
"@link": {},
"yourChatBackupHasBeenSetUp": "Cadangan obrolanmu telah disiapkan.",
"@yourChatBackupHasBeenSetUp": {},
"yourUserId": "ID penggunamu:",
"@yourUserId": {},
"unverified": "Tidak terverifikasi",
"@unverified": {},
"pleaseEnterValidEmail": "Mohon masukkan alamat email yang valid.",
@ -2325,6 +2389,8 @@
"@recoveryKeyLost": {},
"storeInAndroidKeystore": "Simpan di Android KeyStore",
"@storeInAndroidKeystore": {},
"enableAutoBackups": "Aktifkan cadangan otomatis",
"@enableAutoBackups": {},
"storeSecurlyOnThisDevice": "Simpan secara aman di perangkat ini",
"@storeSecurlyOnThisDevice": {},
"countFiles": "{count} file",
@ -2333,6 +2399,8 @@
"count": {}
}
},
"dehydrateShare": "Ini adalah ekspor FluffyChat privat kamu. Pastikan kamu tidak menghilangkannya dan tetap rahasia.",
"@dehydrateShare": {},
"hydrate": "Pulihkan dari file cadangan",
"@hydrate": {},
"indexedDbErrorTitle": "Masalah dengan mode privat",
@ -2463,6 +2531,8 @@
"@endToEndEncryption": {},
"disableEncryptionWarning": "Demi keamanan kamu tidak bisa menonaktifkan enkripsi dalam sebuah obrolan di mana sebelumbya sudah diaktifkan.",
"@disableEncryptionWarning": {},
"noSearchResult": "Tidak ada hasil pencarian yang cocok.",
"@noSearchResult": {},
"letsStart": "Mari kita mulai",
"@letsStart": {},
"enterInviteLinkOrMatrixId": "Masukkan tautan undangan atau ID Matrix...",
@ -2487,27 +2557,5 @@
"readUpToHere": "Baca sampai sini",
"@readUpToHere": {},
"jump": "Lompat",
"@jump": {},
"openLinkInBrowser": "Buka tautan dalam peramban",
"@openLinkInBrowser": {},
"discover": "Jelajahi",
"@discover": {
"type": "text",
"placeholders": {}
},
"allRooms": "Semua Percakapan Grup",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"report": "laporkan",
"@report": {},
"reportErrorDescription": "Aduh. Ada yang salah. Silakan coba lahi nanti. Jika kamu mau, kamu bisa melaporkan kutu ini kepada para pengembang.",
"@reportErrorDescription": {},
"signInWithPassword": "Masuk dengan kata sandi",
"@signInWithPassword": {},
"continueWith": "Lanjutkan dengan:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Silakan coba lagi nanti atau pilih server yang lain.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@jump": {}
}

View File

@ -34,6 +34,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Decovrir",
"@discover": {
"type": "text",
"placeholders": {}
},
"containsUserName": "Contene li nómine",
"@containsUserName": {
"type": "text",
@ -184,6 +189,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Claves es in cache",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"lastActiveAgo": "Ultim activité: {localizedTimeShort}",
"@lastActiveAgo": {
"type": "text",
@ -330,6 +340,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Mardí",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Índisponibil",
"@unavailable": {
"type": "text",
@ -450,6 +465,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Saturdí",
"@saturday": {
"type": "text",
"placeholders": {}
},
"dateWithYear": "{day}.{month}.{year}",
"@dateWithYear": {
"type": "text",
@ -474,6 +494,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Venerdí",
"@friday": {
"type": "text",
"placeholders": {}
},
"lightTheme": "Lucid",
"@lightTheme": {
"type": "text",
@ -504,6 +529,11 @@
"type": "text",
"placeholders": {}
},
"thursday": "Jovedí",
"@thursday": {
"type": "text",
"placeholders": {}
},
"username": "Nómine de usator",
"@username": {
"type": "text",
@ -514,6 +544,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Mercurdí",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"submit": "Inviar",
"@submit": {
"type": "text",
@ -648,6 +683,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Archivat chambre",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"changePassword": "Cambiar li contrasigne",
"@changePassword": {
"type": "text",
@ -930,6 +970,8 @@
},
"updateAvailable": "Un actualisament de FluffyChat es disponibil",
"@updateAvailable": {},
"yourUserId": "Vor ID de usator:",
"@yourUserId": {},
"editWidgets": "Modificar li widgets",
"@editWidgets": {},
"widgetEtherpad": "Textual nota",
@ -1123,6 +1165,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Lunedí",
"@monday": {
"type": "text",
"placeholders": {}
},
"newGroup": "Crear un gruppe",
"@newGroup": {},
"newSpace": "Crear un spacie",
@ -1171,6 +1218,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Soledí",
"@sunday": {
"type": "text",
"placeholders": {}
},
"unverified": "Ínverificat",
"@unverified": {},
"deviceId": "ID de aparate",

View File

@ -76,6 +76,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Stanza archiviata",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Gli utenti ospiti possono partecipare",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -427,6 +432,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Firma incrociata abilitata",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Attualmente attivo",
"@currentlyActive": {
"type": "text",
@ -512,6 +522,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Scopri",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Il nominativo è stato cambiato",
"@displaynameHasBeenChanged": {
"type": "text",
@ -664,6 +679,11 @@
"type": "text",
"placeholders": {}
},
"friday": "venerdì",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Dall'adesione",
"@fromJoining": {
"type": "text",
@ -854,6 +874,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Le chiave sono memorizzate nella cache",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} ha espulso {targetName}",
"@kicked": {
"type": "text",
@ -971,6 +996,11 @@
"type": "text",
"placeholders": {}
},
"monday": "lunedì",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Silenzia discussione",
"@muteChat": {
"type": "text",
@ -1316,6 +1346,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "sabato",
"@saturday": {
"type": "text",
"placeholders": {}
},
"search": "Cerca",
"@search": {
"type": "text",
@ -1515,6 +1550,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "domenica",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Sistema",
"@systemTheme": {
"type": "text",
@ -1530,6 +1570,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Questa stanza è stata archiviata.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "giovedì",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1566,6 +1616,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "martedì",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Non disponibile",
"@unavailable": {
"type": "text",
@ -1736,6 +1791,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "mercoledì",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Ti abbiamo inviato un'e-mail",
"@weSentYouAnEmail": {
"type": "text",
@ -1876,6 +1936,8 @@
"@passwordsDoNotMatch": {},
"pleaseEnterValidEmail": "Inserire un indirizzo email valido.",
"@pleaseEnterValidEmail": {},
"yourUserId": "Il tuo ID utente:",
"@yourUserId": {},
"commandHint_leave": "Lascia questa stanza",
"@commandHint_leave": {
"type": "text",

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

@ -71,6 +71,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arkivert rom",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Skal gjester tillates å ta del",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -396,6 +401,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Videreformidling av tillit på",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Aktiv nå",
"@currentlyActive": {
"type": "text",
@ -481,6 +491,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Oppdag",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Visningsnavn endret",
"@displaynameHasBeenChanged": {
"type": "text",
@ -633,6 +648,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Fredag",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Fra å ta del",
"@fromJoining": {
"type": "text",
@ -818,6 +838,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Nøkler hurtiglagret",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} kastet ut {targetName}",
"@kicked": {
"type": "text",
@ -935,6 +960,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Mandag",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Forstum sludring",
"@muteChat": {
"type": "text",
@ -1245,6 +1275,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Lørdag",
"@saturday": {
"type": "text",
"placeholders": {}
},
"search": "Søk",
"@search": {
"type": "text",
@ -1439,6 +1474,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Søndag",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "System",
"@systemTheme": {
"type": "text",
@ -1454,6 +1494,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Dette rommet har blitt arkivert.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Torsdag",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1475,6 +1525,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Tirsdag",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Utilgjengelig",
"@unavailable": {
"type": "text",
@ -1630,6 +1685,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Onsdag",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Du har fått en e-post",
"@weSentYouAnEmail": {
"type": "text",
@ -1779,5 +1839,7 @@
"@changeYourAvatar": {
"type": "text",
"placeholders": {}
}
},
"yourUserId": "Din bruker ID:",
"@yourUserId": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Zarchiwizowane pokoje",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Czy użytkownicy-goście mogą dołączyć",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -527,7 +532,7 @@
"day": {}
}
},
"deactivateAccountWarning": "To zdezaktywuje twoje konto. To jest nieodwracalne! Na pewno chcesz to zrobić?",
"deactivateAccountWarning": "To dezaktywuje twoje konto. To jest nieodwracalne ! Czy jesteś pewien?",
"@deactivateAccountWarning": {
"type": "text",
"placeholders": {}
@ -699,6 +704,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Piątek",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Od dołączenia",
"@fromJoining": {
"type": "text",
@ -869,6 +879,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Klucze są załadowane",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} wyrzucił/-a {targetName}",
"@kicked": {
"type": "text",
@ -981,6 +996,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Poniedziałek",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Wycisz czat",
"@muteChat": {
"type": "text",
@ -1209,6 +1229,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sobota",
"@saturday": {
"type": "text",
"placeholders": {}
},
"seenByUser": "Zobaczone przez {username}",
"@seenByUser": {
"type": "text",
@ -1344,6 +1369,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Niedziela",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Synchronizacja… Proszę czekać.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1354,6 +1384,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Ten pokój został przeniesiony do archiwum.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Czwartek",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1370,6 +1410,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Wtorek",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unbannedUser": "{username} odbanował/-a {targetName}",
"@unbannedUser": {
"type": "text",
@ -1495,6 +1540,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Środa",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"whoIsAllowedToJoinThisGroup": "Kto może dołączyć do tej grupy",
"@whoIsAllowedToJoinThisGroup": {
"type": "text",
@ -1771,6 +1821,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "Twoja nazwa użytkownika:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "Twoja kopia zapasowa chatu została ustawiona.",
"@yourChatBackupHasBeenSetUp": {},
"chatHasBeenAddedToThisSpace": "Chat został dodany do tej przestrzeni",
@ -1780,6 +1832,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Odkrywaj",
"@discover": {
"type": "text",
"placeholders": {}
},
"editRoomAvatar": "Edytuj zdjęcie pokoju",
"@editRoomAvatar": {
"type": "text",
@ -1890,6 +1947,8 @@
"@scanQrCode": {},
"addToStory": "Dodaj do relacji",
"@addToStory": {},
"typeInInviteLinkManually": "Wpisz link ręcznie...",
"@typeInInviteLinkManually": {},
"createNewSpace": "Nowa przestrzeń",
"@createNewSpace": {
"type": "text",
@ -2124,8 +2183,12 @@
"@unsubscribeStories": {},
"updateNow": "Rozpocznij aktualizację w tle",
"@updateNow": {},
"dehydrateShare": "To jest twój prywatny eksport FluffyChat. Upewnij się, że nie zgubisz go i zachowaj go dla siebie.",
"@dehydrateShare": {},
"hydrateTorLong": "Czy ostatnio eksportowałeś/-aś swoją sesję na TOR? Szybko ją zaimportuj i kontynuuj rozmowy.",
"@hydrateTorLong": {},
"noSearchResult": "Brak pasujących wyników wyszukiwania.",
"@noSearchResult": {},
"dehydrateTorLong": "W przypadku użytkowników sieci TOR zaleca się eksportowanie sesji przed zamknięciem okna.",
"@dehydrateTorLong": {},
"hydrate": "Przywracanie z pliku kopii zapasowej",
@ -2204,6 +2267,11 @@
},
"commandHint_markasdm": "Oznacz jako pokój wiadomości bezpośrednich",
"@commandHint_markasdm": {},
"crossSigningEnabled": "Weryfikacja krzyżowa jest włączona",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"confirmMatrixId": "Potwierdź swój identyfikator Matrix w celu usunięcia konta.",
"@confirmMatrixId": {},
"commandHint_markasgroup": "Oznacz jako grupę",
@ -2265,6 +2333,8 @@
},
"noOtherDevicesFound": "Nie znaleziono innych urządzeń",
"@noOtherDevicesFound": {},
"enableAutoBackups": "Włącz automatyczne tworzenie kopii zapasowych",
"@enableAutoBackups": {},
"widgetUrlError": "Niepoprawny URL.",
"@widgetUrlError": {},
"widgetNameError": "Podaj nazwę wyświetlaną.",

View File

@ -10,6 +10,21 @@
"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",
@ -92,6 +107,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "terça-feira",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"logout": "Terminar sessão",
"@logout": {
"type": "text",
@ -102,6 +122,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "domingo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"users": "Utilizadores",
"@users": {},
"close": "Fechar",
@ -116,5 +141,15 @@
"month": {},
"day": {}
}
},
"friday": "sexta-feira",
"@friday": {
"type": "text",
"placeholders": {}
},
"thursday": "quinta-feira",
"@thursday": {
"type": "text",
"placeholders": {}
}
}

View File

@ -81,6 +81,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Sala arquivada",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Usuários convidados podem participar",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -538,6 +543,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Assinatura cruzada ativada",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Ativo",
"@currentlyActive": {
"type": "text",
@ -623,6 +633,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Desvendar",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "O nome de exibição foi alterado",
"@displaynameHasBeenChanged": {
"type": "text",
@ -789,6 +804,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Sexta-feira",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Desde que entrou",
"@fromJoining": {
"type": "text",
@ -979,6 +999,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Chaves guardadas",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} enxotou {targetName}",
"@kicked": {
"type": "text",
@ -1113,6 +1138,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Segunda-feira",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Silenciar",
"@muteChat": {
"type": "text",
@ -1506,6 +1536,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sábado",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Salvar arquivo",
"@saveFile": {
"type": "text",
@ -1744,6 +1779,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Domingo",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Sincronizando… Por favor, aguarde.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1764,6 +1804,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Esta sala foi arquivada.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Quinta-feira",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1800,6 +1850,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Terça-feira",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Indisponível",
"@unavailable": {
"type": "text",
@ -1970,6 +2025,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Quarta-feira",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Enviamos um e-mail para você",
"@weSentYouAnEmail": {
"type": "text",
@ -2042,6 +2102,8 @@
},
"shareYourInviteLink": "Compartilhar o link do convite",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "Digitar o link do convite manualmente...",
"@typeInInviteLinkManually": {},
"oneClientLoggedOut": "Um dos seus clientes foi desvinculado",
"@oneClientLoggedOut": {},
"addAccount": "Adicionar conta",
@ -2114,6 +2176,8 @@
"@sendOnEnter": {},
"homeserver": "Servidor matriz",
"@homeserver": {},
"yourUserId": "Seu ID de usuário:",
"@yourUserId": {},
"chatHasBeenAddedToThisSpace": "A conversa foi adicionada a este espaço",
"@chatHasBeenAddedToThisSpace": {},
"commandHint_clearcache": "Limpar dados temporários",
@ -2355,6 +2419,8 @@
"@commandHint_markasdm": {},
"commandHint_markasgroup": "Marcar como grupo",
"@commandHint_markasgroup": {},
"dehydrateShare": "Este é seu extrato FluffyChat. Cuidado para não perdê-lo e o mantenha privado.",
"@dehydrateShare": {},
"hydrateTor": "Usuários TOR: Importar sessão",
"@hydrateTor": {},
"hydrateTorLong": "Você exportou sua última sessão no TOR? Importe ela rapidamente e continue conversando.",
@ -2425,6 +2491,8 @@
"@dehydrateWarning": {},
"dehydrateTorLong": "Para usuários TOR, é recomendado exportar a sessão antes de fechar a janela.",
"@dehydrateTorLong": {},
"enableAutoBackups": "Habilitar backups automáticos",
"@enableAutoBackups": {},
"whyIsThisMessageEncrypted": "Por que esta mensagem está ilegível?",
"@whyIsThisMessageEncrypted": {},
"screenSharingTitle": "Compartilhar tela",

View File

@ -90,6 +90,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Sala arquivada",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Todos os visitantes podem entrar",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -289,6 +294,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "O teu ID de utilizador:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "A cópia de segurança foi configurada.",
"@yourChatBackupHasBeenSetUp": {},
"chatBackup": "Cópia de segurança de conversas",
@ -508,6 +515,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Assinatura cruzada ativada",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Ativo(a) agora",
"@currentlyActive": {
"type": "text",
@ -593,6 +605,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Descobrir",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Nome de exibição alterado",
"@displaynameHasBeenChanged": {
"type": "text",
@ -761,6 +778,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Sexta-feira",
"@friday": {
"type": "text",
"placeholders": {}
},
"goToTheNewRoom": "Ir para a nova sala",
"@goToTheNewRoom": {
"type": "text",
@ -941,6 +963,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Chaves estão armazenadas em cache",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} expulsou {targetName}",
"@kicked": {
"type": "text",
@ -1070,6 +1097,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Segunda-feira",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Silenciar conversa",
"@muteChat": {
"type": "text",
@ -1135,6 +1167,8 @@
},
"shareYourInviteLink": "Partilhar a ligação de convite",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "Escrever a ligação de convite manualmente...",
"@typeInInviteLinkManually": {},
"none": "Nenhum",
"@none": {
"type": "text",
@ -1547,6 +1581,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sábado",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Guardar ficheiro",
"@saveFile": {
"type": "text",
@ -1670,6 +1709,8 @@
"@dehydrate": {},
"dehydrateWarning": "Esta ação não pode ser revertida. Assegura-te que guardas bem a cópia de segurança.",
"@dehydrateWarning": {},
"dehydrateShare": "Esta é a tua exportação privada do FluffyChat. Assegura-te que não a perdes e que a manténs privada.",
"@dehydrateShare": {},
"hydrateTorLong": "Exportaste a tua sessão na última vez que estiveste no TOR? Importa-a rapidamente e continua a conversar.",
"@hydrateTorLong": {},
"dehydrateTor": "Utilizadores do TOR: Exportar sessão",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -55,6 +55,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Archivovaná miestnosť",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Môžu sa pripojiť hostia",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -325,6 +330,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Vzájomné overenie je zapnuté",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Momentálne prítomní",
"@currentlyActive": {
"type": "text",
@ -470,6 +480,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Piatok",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Od pripojenia",
"@fromJoining": {
"type": "text",
@ -595,6 +610,11 @@
"username": {}
}
},
"keysCached": "Kľúče sú uložené",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} vyhodili {targetName}",
"@kicked": {
"type": "text",
@ -697,6 +717,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Pondelok",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Stlmiť chat",
"@muteChat": {
"type": "text",
@ -895,6 +920,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Sobota",
"@saturday": {
"type": "text",
"placeholders": {}
},
"seenByUser": "Videné užívateľom {username}",
"@seenByUser": {
"type": "text",
@ -1030,6 +1060,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Nedeľa",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Systémová farba",
"@systemTheme": {
"type": "text",
@ -1045,6 +1080,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Táto miestnosť bola archivovaná.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Štvrtok",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1056,6 +1101,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Utorok",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unbannedUser": "{username} odbanovali {targetName}",
"@unbannedUser": {
"type": "text",
@ -1206,6 +1256,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Streda",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"whoIsAllowedToJoinThisGroup": "Kto môže vstúpiť do tejto skupiny",
"@whoIsAllowedToJoinThisGroup": {
"type": "text",
@ -1385,6 +1440,8 @@
},
"sendOnEnter": "Odoslať pri vstupe",
"@sendOnEnter": {},
"yourUserId": "Vaše užívateľské ID:",
"@yourUserId": {},
"ignoredUsers": "Ignorovaní užívatelia",
"@ignoredUsers": {
"type": "text",

View File

@ -71,6 +71,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arhivirana soba",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"askSSSSSign": "Če želite podpisati drugo osebo, vnesite geslo za varno trgovino ali obnovitveni ključ.",
"@askSSSSSign": {
"type": "text",
@ -254,6 +259,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "Vaš ID uporabnika:",
"@yourUserId": {},
"yourChatBackupHasBeenSetUp": "Varnostna kopija klepeta je nastavljena.",
"@yourChatBackupHasBeenSetUp": {},
"chatBackup": "Varnostno kopiranje klepeta",
@ -551,6 +558,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Navzkrižno podpisovanje DA",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Trenutno aktiven",
"@currentlyActive": {
"type": "text",

View File

@ -76,6 +76,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Архивирана соба",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Да ли је гостима дозвољен приступ",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -499,6 +504,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Међу-потписивање укључено",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Тренутно активно",
"@currentlyActive": {
"type": "text",
@ -584,6 +594,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Истражи",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Име за приказ је измењено",
"@displaynameHasBeenChanged": {
"type": "text",
@ -741,6 +756,11 @@
"type": "text",
"placeholders": {}
},
"friday": "петак",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "од приступања",
"@fromJoining": {
"type": "text",
@ -931,6 +951,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Кључеви су кеширани",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} избаци корисника {targetName}",
"@kicked": {
"type": "text",
@ -1055,6 +1080,11 @@
"type": "text",
"placeholders": {}
},
"monday": "понедељак",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Ућуткај ћаскање",
"@muteChat": {
"type": "text",
@ -1430,6 +1460,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "субота",
"@saturday": {
"type": "text",
"placeholders": {}
},
"search": "Претражи",
"@search": {
"type": "text",
@ -1639,6 +1674,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "недеља",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "системски",
"@systemTheme": {
"type": "text",
@ -1654,6 +1694,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Ова соба је архивирана.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "четвртак",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1690,6 +1740,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "уторак",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Недоступно",
"@unavailable": {
"type": "text",
@ -1860,6 +1915,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "среда",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Послали смо вам е-пошту",
"@weSentYouAnEmail": {
"type": "text",

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
{}
{}

View File

@ -84,6 +84,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Arşivlenmiş Oda",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Misafir kullanıcıların katılmasına izin veriliyor mu",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -543,6 +548,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Çapraz imzalama açık",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Şu anda etkin",
"@currentlyActive": {
"type": "text",
@ -628,6 +638,11 @@
"type": "text",
"placeholders": {}
},
"discover": "Keşfet",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "Görünen ad değiştirildi",
"@displaynameHasBeenChanged": {
"type": "text",
@ -794,6 +809,11 @@
"type": "text",
"placeholders": {}
},
"friday": "Cuma",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "Katılmadan",
"@fromJoining": {
"type": "text",
@ -984,6 +1004,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "Anahtarlar önbelleğe alındı",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username}, {targetName} kişisini attı",
"@kicked": {
"type": "text",
@ -1118,6 +1143,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Pazartesi",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Sohbeti sessize al",
"@muteChat": {
"type": "text",
@ -1511,6 +1541,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Cumartesi",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "Dosyayı kaydet",
"@saveFile": {
"type": "text",
@ -1749,6 +1784,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Pazar",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "Eşzamanlanıyor… Lütfen bekleyin.",
"@synchronizingPleaseWait": {
"type": "text",
@ -1769,6 +1809,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Bu oda arşivlendi.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Perşembe",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1805,6 +1855,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Salı",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "Yok",
"@unavailable": {
"type": "text",
@ -1975,6 +2030,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Çarşamba",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "Size bir e-posta gönderdik",
"@weSentYouAnEmail": {
"type": "text",
@ -2047,6 +2107,8 @@
},
"scanQrCode": "QR kodunu tarayın",
"@scanQrCode": {},
"typeInInviteLinkManually": "Davet bağlantısını el ile yazın...",
"@typeInInviteLinkManually": {},
"shareYourInviteLink": "Davet bağlantınızı paylaşın",
"@shareYourInviteLink": {},
"sendOnEnter": "Enter tuşu ile gönder",
@ -2075,6 +2137,8 @@
"@yourChatBackupHasBeenSetUp": {},
"unverified": "Doğrulanmadı",
"@unverified": {},
"yourUserId": "Kullanıcı kimliğiniz:",
"@yourUserId": {},
"repeatPassword": "Parolayı tekrarlayın",
"@repeatPassword": {},
"passwordsDoNotMatch": "Parolalar eşleşmiyor!",
@ -2316,6 +2380,8 @@
"@users": {},
"storeInSecureStorageDescription": "Kurtarma anahtarını bu aygıtın güvenli deposunda saklayın.",
"@storeInSecureStorageDescription": {},
"enableAutoBackups": "Otomatik yedeklemeleri etkinleştir",
"@enableAutoBackups": {},
"recoveryKey": "Kurtarma anahtarı",
"@recoveryKey": {},
"stories": "Hikayeler",
@ -2348,6 +2414,8 @@
"@indexedDbErrorTitle": {},
"dehydrateWarning": "Bu eylem geri alınamaz. Yedekleme dosyasını güvenli bir şekilde sakladığınızdan emin olun.",
"@dehydrateWarning": {},
"dehydrateShare": "Bu sizin özel FluffyChat dışa aktarımınızdır. Kaybetmediğinizden ve gizli tuttuğunuzdan emin olun.",
"@dehydrateShare": {},
"hydrateTorLong": "TOR'da en son oturumunuzu dışa aktardınız mı? Hızlıca içe aktarın ve sohbete devam edin.",
"@hydrateTorLong": {},
"indexedDbErrorLong": "Mesaj saklama özelliği ne yazık ki öntanımlı olarak gizli modda etkin değildir.\nLütfen\n - about:config sayfasına gidin ve\n - dom.indexedDB.privateBrowsing.enabled seçeneğini true olarak ayarlayın\nAksi takdirde FluffyChat çalıştırılamaz.",
@ -2464,6 +2532,8 @@
"@sorryThatsNotPossible": {},
"deviceKeys": "Aygıt anahtarları:",
"@deviceKeys": {},
"noSearchResult": "Eşleşen arama sonucu yok.",
"@noSearchResult": {},
"letsStart": "Başlayalım",
"@letsStart": {},
"enterInviteLinkOrMatrixId": "Davet bağlantısını veya Matris kimliğini girin...",
@ -2490,25 +2560,5 @@
"jump": "Atla",
"@jump": {},
"openLinkInBrowser": "Bağlantıyı tarayıcıda aç",
"@openLinkInBrowser": {},
"allRooms": "Tüm Grup Sohbetleri",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"discover": "Keşfet",
"@discover": {
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "Olamaz. Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyin. İsterseniz hatayı geliştiricilere bildirebilirsiniz.",
"@reportErrorDescription": {},
"report": "bildir",
"@report": {},
"signInWithPassword": "Parola ile oturum aç",
"@signInWithPassword": {},
"continueWith": "Devam et:",
"@continueWith": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "Lütfen daha sonra tekrar deneyin veya farklı bir sunucu seçin.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {}
"@openLinkInBrowser": {}
}

View File

@ -62,6 +62,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Заархівована кімната",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Чи дозволено гостям приєднуватись",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -332,6 +337,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Перехресне підписування увімкнено",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "Зараз у мережі",
"@currentlyActive": {
"type": "text",
@ -484,6 +494,11 @@
"type": "text",
"placeholders": {}
},
"friday": "П'ятниця",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "З моменту приєднання",
"@fromJoining": {
"type": "text",
@ -609,6 +624,11 @@
"username": {}
}
},
"keysCached": "Ключі кешовано",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} вилучає {targetName}",
"@kicked": {
"type": "text",
@ -711,6 +731,11 @@
"type": "text",
"placeholders": {}
},
"monday": "Понеділок",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "Вимкнути сповіщення",
"@muteChat": {
"type": "text",
@ -909,6 +934,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "Субота",
"@saturday": {
"type": "text",
"placeholders": {}
},
"seenByUser": "Переглянуто {username}",
"@seenByUser": {
"type": "text",
@ -1036,6 +1066,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "Неділя",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "Системна",
"@systemTheme": {
"type": "text",
@ -1051,6 +1086,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "Цю кімнату було заархівовано.",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "Четвер",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1062,6 +1107,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "Вівторок",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unbannedUser": "{username} розблоковує {targetName}",
"@unbannedUser": {
"type": "text",
@ -1205,6 +1255,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "Середа",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"whoIsAllowedToJoinThisGroup": "Кому дозволено приєднуватися до цієї групи",
"@whoIsAllowedToJoinThisGroup": {
"type": "text",
@ -1439,6 +1494,8 @@
},
"shareYourInviteLink": "Поділіться своїм посиланням запрошення",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "Введіть посилання запрошення власноруч...",
"@typeInInviteLinkManually": {},
"scanQrCode": "Сканувати QR-код",
"@scanQrCode": {},
"noPasswordRecoveryDescription": "Ви ще не додали спосіб відновлення пароля.",
@ -1794,6 +1851,8 @@
"type": "text",
"description": "Usage hint for the command /html"
},
"yourUserId": "Ваш ID користувача:",
"@yourUserId": {},
"commandHint_invite": "Запросіть цього користувача до цієї кімнати",
"@commandHint_invite": {
"type": "text",
@ -1931,6 +1990,11 @@
"senderName": {}
}
},
"discover": "Огляд",
"@discover": {
"type": "text",
"placeholders": {}
},
"cantOpenUri": "Не вдалося відкрити URI {uri}",
"@cantOpenUri": {
"type": "text",
@ -2320,6 +2384,8 @@
"@recoveryKey": {},
"recoveryKeyLost": "Ключ відновлення втрачено?",
"@recoveryKeyLost": {},
"enableAutoBackups": "Увімкнути автоматичне резервне копіювання",
"@enableAutoBackups": {},
"users": "Користувачі",
"@users": {},
"stories": "Історії",
@ -2346,6 +2412,8 @@
"@dehydrate": {},
"dehydrateWarning": "Цю дію не можна скасувати. Переконайтеся, що ви безпечно зберігаєте файл резервної копії.",
"@dehydrateWarning": {},
"dehydrateShare": "Це ваш приватний експорт FluffyChat. Переконайтеся, що ви не втратите його та зберігайте його приватно.",
"@dehydrateShare": {},
"dehydrateTor": "Користувачі TOR: експорт сеансу",
"@dehydrateTor": {},
"dehydrateTorLong": "Для користувачів TOR рекомендується експортувати сеанс перед закриттям вікна.",
@ -2464,6 +2532,8 @@
"@sorryThatsNotPossible": {},
"deviceKeys": "Ключі пристрою:",
"@deviceKeys": {},
"noSearchResult": "Немає відповідних результатів пошуку.",
"@noSearchResult": {},
"letsStart": "Розпочнімо",
"@letsStart": {},
"enterInviteLinkOrMatrixId": "Введіть запрошувальне посилання або Matrix ID...",
@ -2490,25 +2560,5 @@
"jump": "Перейти",
"@jump": {},
"openLinkInBrowser": "Відкрити посилання у браузері",
"@openLinkInBrowser": {},
"allRooms": "Усі групові бесіди",
"@allRooms": {
"type": "text",
"placeholders": {}
},
"reportErrorDescription": "О, ні. Щось пішло не так. Повторіть спробу пізніше. Якщо хочете, можете повідомити про помилку розробникам.",
"@reportErrorDescription": {},
"report": "повідомити",
"@report": {},
"discover": "Огляд",
"@discover": {
"type": "text",
"placeholders": {}
},
"pleaseTryAgainLaterOrChooseDifferentServer": "Спробуйте пізніше або виберіть інший сервер.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
"signInWithPassword": "Увійти за допомогою пароля",
"@signInWithPassword": {},
"continueWith": "Продовжити за допомогою:",
"@continueWith": {}
"@openLinkInBrowser": {}
}

View File

@ -61,6 +61,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "Phòng hội thảo đã lưu trữ",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "Khách vãng lai có được tham gia không",
"@areGuestsAllowedToJoin": {
"type": "text",

View File

@ -77,6 +77,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "已存档的聊天室",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "是否允许游客加入",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -517,6 +522,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "交叉签名已启用",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "目前活跃",
"@currentlyActive": {
"type": "text",
@ -602,6 +612,11 @@
"type": "text",
"placeholders": {}
},
"discover": "探索",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "显示名称已被改变",
"@displaynameHasBeenChanged": {
"type": "text",
@ -759,6 +774,11 @@
"type": "text",
"placeholders": {}
},
"friday": "星期五",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "自加入起",
"@fromJoining": {
"type": "text",
@ -949,6 +969,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "密钥已被缓存",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "👞 {username} 踢了 {targetName}",
"@kicked": {
"type": "text",
@ -1073,6 +1098,11 @@
"type": "text",
"placeholders": {}
},
"monday": "星期一",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "将该聊天静音",
"@muteChat": {
"type": "text",
@ -1448,6 +1478,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "星期六",
"@saturday": {
"type": "text",
"placeholders": {}
},
"saveFile": "保存文件",
"@saveFile": {
"type": "text",
@ -1671,6 +1706,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "星期日",
"@sunday": {
"type": "text",
"placeholders": {}
},
"synchronizingPleaseWait": "同步中…请等待。",
"@synchronizingPleaseWait": {
"type": "text",
@ -1691,6 +1731,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "该聊天室已被归档。",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "星期四",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1727,6 +1777,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "星期二",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "不可用",
"@unavailable": {
"type": "text",
@ -1897,6 +1952,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "星期三",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "我们向您发送了一封电子邮件",
"@weSentYouAnEmail": {
"type": "text",
@ -2072,8 +2132,12 @@
"@pleaseEnterValidEmail": {},
"repeatPassword": "再次输入密码",
"@repeatPassword": {},
"yourUserId": "您的 ID",
"@yourUserId": {},
"shareYourInviteLink": "分享您的邀请链接",
"@shareYourInviteLink": {},
"typeInInviteLinkManually": "手动输入邀请链接…",
"@typeInInviteLinkManually": {},
"addAccount": "添加账户",
"@addAccount": {},
"editBundlesForAccount": "编辑该账户的集合",
@ -2324,6 +2388,8 @@
"@storeInAndroidKeystore": {},
"storeSecurlyOnThisDevice": "安全地存储在此设备上",
"@storeSecurlyOnThisDevice": {},
"enableAutoBackups": "启用自动备份",
"@enableAutoBackups": {},
"users": "用户",
"@users": {},
"stories": "故事",
@ -2334,6 +2400,8 @@
"count": {}
}
},
"dehydrateShare": "这是私人的 FluffyChat 导出。 确保你不会丢失它并将其保密。",
"@dehydrateShare": {},
"dehydrateTor": "TOR 用户:导出会话",
"@dehydrateTor": {},
"dehydrateTorLong": "建议 TOR 用户在关闭窗口之前导出会话。",
@ -2463,12 +2531,5 @@
"sorryThatsNotPossible": "非常抱歉……这是做不到的",
"@sorryThatsNotPossible": {},
"deviceKeys": "设备密钥:",
"@deviceKeys": {},
"report": "举报",
"@report": {},
"discover": "探索",
"@discover": {
"type": "text",
"placeholders": {}
}
"@deviceKeys": {}
}

View File

@ -76,6 +76,11 @@
"type": "text",
"placeholders": {}
},
"archivedRoom": "已封存的對話",
"@archivedRoom": {
"type": "text",
"placeholders": {}
},
"areGuestsAllowedToJoin": "是否允許訪客加入",
"@areGuestsAllowedToJoin": {
"type": "text",
@ -422,6 +427,11 @@
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "第三方登入已啟用",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"currentlyActive": "目前活躍",
"@currentlyActive": {
"type": "text",
@ -507,6 +517,11 @@
"type": "text",
"placeholders": {}
},
"discover": "探索",
"@discover": {
"type": "text",
"placeholders": {}
},
"displaynameHasBeenChanged": "顯示名稱已被變更",
"@displaynameHasBeenChanged": {
"type": "text",
@ -659,6 +674,11 @@
"type": "text",
"placeholders": {}
},
"friday": "星期五",
"@friday": {
"type": "text",
"placeholders": {}
},
"fromJoining": "自加入起",
"@fromJoining": {
"type": "text",
@ -844,6 +864,11 @@
"type": "text",
"placeholders": {}
},
"keysCached": "金鑰已被快取",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"kicked": "{username}踢了{targetName}",
"@kicked": {
"type": "text",
@ -961,6 +986,11 @@
"type": "text",
"placeholders": {}
},
"monday": "星期一",
"@monday": {
"type": "text",
"placeholders": {}
},
"muteChat": "將該聊天室靜音",
"@muteChat": {
"type": "text",
@ -1296,6 +1326,11 @@
"type": "text",
"placeholders": {}
},
"saturday": "星期六",
"@saturday": {
"type": "text",
"placeholders": {}
},
"search": "搜尋",
"@search": {
"type": "text",
@ -1495,6 +1530,11 @@
"type": "text",
"placeholders": {}
},
"sunday": "星期日",
"@sunday": {
"type": "text",
"placeholders": {}
},
"systemTheme": "自動",
"@systemTheme": {
"type": "text",
@ -1510,6 +1550,16 @@
"type": "text",
"placeholders": {}
},
"thisRoomHasBeenArchived": "這個聊天室已被封存。",
"@thisRoomHasBeenArchived": {
"type": "text",
"placeholders": {}
},
"thursday": "星期四",
"@thursday": {
"type": "text",
"placeholders": {}
},
"title": "FluffyChat",
"@title": {
"description": "Title for the application",
@ -1546,6 +1596,11 @@
"type": "text",
"placeholders": {}
},
"tuesday": "星期二",
"@tuesday": {
"type": "text",
"placeholders": {}
},
"unavailable": "無法取得",
"@unavailable": {
"type": "text",
@ -1716,6 +1771,11 @@
"type": "text",
"placeholders": {}
},
"wednesday": "星期三",
"@wednesday": {
"type": "text",
"placeholders": {}
},
"weSentYouAnEmail": "我們向您傳送了一封電子郵件",
"@weSentYouAnEmail": {
"type": "text",
@ -1912,6 +1972,8 @@
"type": "text",
"placeholders": {}
},
"yourUserId": "您的ID",
"@yourUserId": {},
"chatHasBeenAddedToThisSpace": "聊天室已添加到此空間",
"@chatHasBeenAddedToThisSpace": {},
"clearArchive": "清除存檔",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 212 KiB

BIN
assets/start_chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -55,7 +55,7 @@
<div class="flex mb-8 justify-center content-center">
<a rel="me"
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
rel="me" href="https://mastodon.art/@krille">
rel="me" 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;">
@ -116,4 +116,4 @@
</body>
</html>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -291,7 +291,6 @@
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (

View File

@ -33,11 +33,6 @@ abstract class AppConfig {
static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat';
static const String supportUrl =
'https://gitlab.com/famedly/fluffychat/issues';
static final Uri newIssueUrl = Uri(
scheme: 'https',
host: 'gitlab.com',
path: '/famedly/fluffychat/-/issues/new',
);
static const bool enableSentry = true;
static const String sentryDns =
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';

View File

@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/pages/device_settings/device_settings.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
@ -26,6 +27,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
import 'package:fluffychat/pages/settings_security/settings_security.dart';
import 'package:fluffychat/pages/settings_stories/settings_stories.dart';
import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pages/sign_up/signup.dart';
import 'package:fluffychat/pages/story/story_page.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/loading_view.dart';
@ -264,6 +266,23 @@ class AppRoutes {
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'connect',
widget: const ConnectPage(),
buildTransition: _fadeTransition,
stackedRoutes: [
VWidget(
path: 'login',
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'signup',
widget: const SignupPage(),
buildTransition: _fadeTransition,
),
],
),
VWidget(
path: 'logs',
widget: const LogViewer(),
@ -339,6 +358,23 @@ class AppRoutes {
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'connect',
widget: const ConnectPage(),
buildTransition: _fadeTransition,
stackedRoutes: [
VWidget(
path: 'login',
widget: const Login(),
buildTransition: _fadeTransition,
),
VWidget(
path: 'signup',
widget: const SignupPage(),
buildTransition: _fadeTransition,
),
],
),
],
),
VWidget(

View File

@ -41,22 +41,6 @@ abstract class FluffyThemes {
titleSmall: fallbackTextStyle,
);
static LinearGradient backgroundGradient(
BuildContext context,
int alpha,
) {
final colorScheme = Theme.of(context).colorScheme;
return LinearGradient(
begin: Alignment.topCenter,
colors: [
colorScheme.primaryContainer.withAlpha(alpha),
colorScheme.secondaryContainer.withAlpha(alpha),
colorScheme.tertiaryContainer.withAlpha(alpha),
colorScheme.primaryContainer.withAlpha(alpha),
],
);
}
static const Duration animationDuration = Duration(milliseconds: 250);
static const Curve animationCurve = Curves.easeInOut;

View File

@ -23,7 +23,6 @@ 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/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/ios_badge_client_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@ -77,7 +76,7 @@ class ChatPageWithRoom extends StatefulWidget {
}
class ChatController extends State<ChatPageWithRoom> {
Room get room => sendingClient.getRoomById(roomId) ?? widget.room;
Room get room => widget.room;
late Client sendingClient;
@ -152,10 +151,7 @@ class ChatController extends State<ChatPageWithRoom> {
Event? editEvent;
bool _scrolledUp = false;
bool get showScrollDownButton =>
_scrolledUp || timeline?.allowNewEvent == false;
bool showScrollDownButton = false;
bool get selectMode => selectedEvents.isNotEmpty;
@ -204,7 +200,6 @@ class ChatController extends State<ChatPageWithRoom> {
void requestHistory() async {
if (!timeline!.canRequestHistory) return;
Logs().v('Requesting history...');
try {
await timeline!.requestHistory(historyCount: _loadHistoryCount);
} catch (err) {
@ -223,7 +218,6 @@ class ChatController extends State<ChatPageWithRoom> {
final timeline = this.timeline;
if (timeline == null) return;
if (!timeline.canRequestFuture) return;
Logs().v('Requesting future...');
try {
final mostRecentEventId = timeline.events.first.eventId;
await timeline.requestFuture(historyCount: _loadHistoryCount);
@ -246,11 +240,18 @@ class ChatController extends State<ChatPageWithRoom> {
}
setReadMarker();
if (!scrollController.hasClients) return;
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
requestHistory();
} else if (scrollController.position.pixels == 0) {
requestFuture();
}
if (timeline?.allowNewEvent == false ||
scrollController.position.pixels > 0 && _scrolledUp == false) {
setState(() => _scrolledUp = true);
} else if (scrollController.position.pixels == 0 && _scrolledUp == true) {
setState(() => _scrolledUp = false);
scrollController.position.pixels > 0 && showScrollDownButton == false) {
setState(() => showScrollDownButton = true);
} else if (scrollController.position.pixels == 0 &&
showScrollDownButton == true) {
setState(() => showScrollDownButton = false);
}
}
@ -271,10 +272,7 @@ class ChatController extends State<ChatPageWithRoom> {
super.initState();
sendingClient = Matrix.of(context).client;
readMarkerEventId = room.fullyRead;
loadTimelineFuture =
_getTimeline(eventContextId: readMarkerEventId).onError(
ErrorReporter(context, 'Unable to load timeline').onErrorCallback,
);
loadTimelineFuture = _getTimeline();
}
void updateView() {
@ -286,12 +284,12 @@ class ChatController extends State<ChatPageWithRoom> {
Future<void> _getTimeline({
String? eventContextId,
Duration timeout = const Duration(seconds: 7),
Duration timeout = const Duration(seconds: 5),
}) async {
await Matrix.of(context).client.roomsLoading;
await Matrix.of(context).client.accountDataLoading;
if (eventContextId != null &&
(!eventContextId.isValidMatrixId || eventContextId.sigil != '\$')) {
eventContextId ??= room.fullyRead;
if (!eventContextId.isValidMatrixId || eventContextId.sigil != '\$') {
eventContextId = null;
}
try {
@ -301,22 +299,19 @@ class ChatController extends State<ChatPageWithRoom> {
eventContextId: eventContextId,
)
.timeout(timeout);
} catch (e, s) {
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
} on TimeoutException catch (_) {
if (!mounted) return;
timeline = await room.getTimeline(onUpdate: updateView);
if (!mounted) return;
if (e is TimeoutException || e is IOException) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.jumpToLastReadMessage),
action: SnackBarAction(
label: L10n.of(context)!.jump,
onPressed: () => scrollToEventId(eventContextId!),
),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.jumpToLastReadMessage),
action: SnackBarAction(
label: L10n.of(context)!.jump,
onPressed: () => scrollToEventId(eventContextId!),
),
);
}
),
);
}
timeline!.requestKeys(onlineKeyBackupOnly: false);
if (timeline!.events.isNotEmpty) {
@ -356,7 +351,7 @@ class ChatController extends State<ChatPageWithRoom> {
eventId ??= timeline.events.first.eventId;
Logs().v('Set read marker...', eventId);
// ignore: unawaited_futures
_setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) {
_setReadMarkerFuture = timeline.setReadMarker(eventId).then((_) {
_setReadMarkerFuture = null;
});
room.client.updateIosBadge();
@ -373,7 +368,7 @@ class ChatController extends State<ChatPageWithRoom> {
TextEditingController sendController = TextEditingController();
void setSendingClient(Client c) {
// first cancel typing with the old sending client
// first cancle typing with the old sending client
if (currentlyTyping) {
// no need to have the setting typing to false be blocking
typingCoolDown?.cancel();
@ -381,15 +376,6 @@ class ChatController extends State<ChatPageWithRoom> {
room.setTyping(false);
currentlyTyping = false;
}
// then cancel the old timeline
// fixes bug with read reciepts and quick switching
loadTimelineFuture = _getTimeline(eventContextId: room.fullyRead).onError(
ErrorReporter(
context,
'Unable to load timeline after changing sending Client',
).onErrorCallback,
);
// then set the new sending client
setState(() => sendingClient = c);
}
@ -407,7 +393,7 @@ class ChatController extends State<ChatPageWithRoom> {
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
if (commandMatch != null &&
!sendingClient.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
!room.client.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
final l10n = L10n.of(context)!;
final dialogResult = await showOkCancelAlertDialog(
context: context,
@ -815,13 +801,9 @@ class ChatController extends State<ChatPageWithRoom> {
if (eventIndex == -1) {
setState(() {
timeline = null;
_scrolledUp = false;
loadTimelineFuture = _getTimeline(
eventContextId: eventId,
timeout: const Duration(seconds: 30),
).onError(
ErrorReporter(context, 'Unable to load timeline after scroll to ID')
.onErrorCallback,
);
});
await loadTimelineFuture;
@ -841,11 +823,7 @@ class ChatController extends State<ChatPageWithRoom> {
if (!timeline!.allowNewEvent) {
setState(() {
timeline = null;
_scrolledUp = false;
loadTimelineFuture = _getTimeline().onError(
ErrorReporter(context, 'Unable to load timeline after scroll down')
.onErrorCallback,
);
loadTimelineFuture = _getTimeline();
});
await loadTimelineFuture;
setReadMarker(eventId: timeline!.events.first.eventId);
@ -869,11 +847,8 @@ class ChatController extends State<ChatPageWithRoom> {
setState(() => showEmojiPicker = false);
if (emoji == null) return;
// make sure we don't send the same emoji twice
if (_allReactionEvents.any(
(e) => e.content.tryGetMap('m.relates_to')?['key'] == emoji.emoji,
)) {
return;
}
if (_allReactionEvents
.any((e) => e.content['m.relates_to']['key'] == emoji.emoji)) return;
return sendEmojiAction(emoji.emoji);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
@ -53,18 +54,14 @@ class ChatEventList extends StatelessWidget {
);
}
if (controller.timeline!.canRequestFuture) {
return Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => controller.requestFuture(),
);
return Center(
child: IconButton(
onPressed: controller.requestFuture,
icon: const Icon(Icons.refresh_outlined),
),
);
},
Center(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
),
onPressed: controller.requestFuture,
child: Text(L10n.of(context)!.loadMore),
),
);
}
return Column(
@ -84,18 +81,14 @@ class ChatEventList extends StatelessWidget {
);
}
if (controller.timeline!.canRequestHistory) {
return Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => controller.requestHistory(),
);
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
);
},
Center(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
),
onPressed: controller.requestHistory,
child: Text(L10n.of(context)!.loadMore),
),
);
}
return const SizedBox.shrink();

View File

@ -146,6 +146,7 @@ class ChatView extends StatelessWidget {
);
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
final colorScheme = Theme.of(context).colorScheme;
return VWidgetGuard(
onSystemPop: (redirector) async {
@ -196,7 +197,6 @@ class ChatView extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 56.0),
child: FloatingActionButton(
onPressed: controller.scrollDown,
heroTag: null,
mini: true,
child: const Icon(Icons.arrow_downward_outlined),
),
@ -219,9 +219,14 @@ class ChatView extends StatelessWidget {
else
Container(
decoration: BoxDecoration(
gradient: FluffyThemes.backgroundGradient(
context,
64,
gradient: LinearGradient(
begin: Alignment.topCenter,
colors: [
colorScheme.primaryContainer.withAlpha(64),
colorScheme.secondaryContainer.withAlpha(64),
colorScheme.tertiaryContainer.withAlpha(64),
colorScheme.primaryContainer.withAlpha(64),
],
),
),
),

View File

@ -20,9 +20,7 @@ class EncryptionButton extends StatelessWidget {
.where((s) => s.deviceLists != null),
builder: (context, snapshot) {
return FutureBuilder<EncryptionHealthState>(
future: room.encrypted
? room.calcEncryptionHealthState()
: Future.value(EncryptionHealthState.allVerified),
future: room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: room.encrypted
? L10n.of(context)!.encrypted

View File

@ -4,12 +4,12 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
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/error_reporter.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import '../../../utils/matrix_sdk_extensions/event_extension.dart';
@ -132,10 +132,14 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
} else {
await audioPlayer.setAudioSource(MatrixFileAudioSource(matrixFile!));
}
audioPlayer.play().onError(
ErrorReporter(context, 'Unable to play audio message')
.onErrorCallback,
);
audioPlayer.play().catchError((e, s) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.oopsSomethingWentWrong),
),
);
Logs().w('Error while playing audio', e, s);
});
}
static const double buttonSize = 36;

View File

@ -36,16 +36,19 @@ class _CuteContentState extends State<CuteContent> {
return GestureDetector(
onTap: addOverlay,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.event.text,
style: const TextStyle(fontSize: 150),
),
if (label != null) Text(label)
],
child: SizedBox.square(
dimension: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.event.text,
style: const TextStyle(fontSize: 150),
),
if (label != null) Text(label)
],
),
),
);
},
@ -141,26 +144,24 @@ class _CuteEventOverlayState extends State<CuteEventOverlay>
return SizedBox(
height: constraints.maxHeight,
width: constraints.maxWidth,
child: OverflowBox(
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,
),
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(),
),
),
)
.toList(),
),
);
},

View File

@ -1,29 +1,31 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html_table/flutter_html_table.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:linkify/linkify.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_matrix_html/flutter_html.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart';
import '../../../config/setting_keys.dart';
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../utils/url_launcher.dart';
class HtmlMessage extends StatelessWidget {
final String html;
final int? maxLines;
final Room room;
final Color textColor;
final TextStyle? defaultTextStyle;
final TextStyle? linkStyle;
final double? emoteSize;
const HtmlMessage({
Key? key,
required this.html,
this.maxLines,
required this.room,
this.textColor = Colors.black,
this.defaultTextStyle,
this.linkStyle,
this.emoteSize,
}) : super(key: key);
@override
@ -44,461 +46,101 @@ class HtmlMessage extends StatelessWidget {
'',
);
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
final linkifiedRenderHtml = linkify(
renderHtml,
options: const LinkifyOptions(humanize: false),
)
.map(
(element) {
if (element is! UrlElement ||
element.text.contains('<') ||
element.text.contains('>') ||
element.text.contains('"')) {
return element.text;
}
return '<a href="${element.url}">${element.text}</a>';
},
)
.join('')
.replaceAll('\n', '');
final linkColor = textColor.withAlpha(150);
// there is no need to pre-validate the html, as we validate it while rendering
final matrix = Matrix.of(context);
final themeData = Theme.of(context);
return Html(
data: linkifiedRenderHtml,
style: {
'*': Style(
color: textColor,
margin: Margins.all(0),
fontSize: FontSize(fontSize),
),
'a': Style(color: linkColor, textDecorationColor: linkColor),
'h1': Style(
fontSize: FontSize(fontSize * 2),
lineHeight: LineHeight.number(1.5),
fontWeight: FontWeight.w600,
),
'h2': Style(
fontSize: FontSize(fontSize * 1.75),
lineHeight: LineHeight.number(1.5),
fontWeight: FontWeight.w500,
),
'h3': Style(
fontSize: FontSize(fontSize * 1.5),
lineHeight: LineHeight.number(1.5),
),
'h4': Style(
fontSize: FontSize(fontSize * 1.25),
lineHeight: LineHeight.number(1.5),
),
'h5': Style(
fontSize: FontSize(fontSize * 1.25),
lineHeight: LineHeight.number(1.5),
),
'h6': Style(
fontSize: FontSize(fontSize),
lineHeight: LineHeight.number(1.5),
),
'blockquote': Style(
border: Border(
left: BorderSide(
width: 3,
color: textColor,
),
data: renderHtml,
defaultTextStyle: defaultTextStyle,
emoteSize: emoteSize,
linkStyle: linkStyle ??
themeData.textTheme.bodyMedium!.copyWith(
color: themeData.colorScheme.secondary,
decoration: TextDecoration.underline,
decorationColor: themeData.colorScheme.secondary,
),
padding: HtmlPaddings.only(left: 6, bottom: 0),
),
'hr': Style(
border: Border.all(color: textColor, width: 0.5),
),
'table': Style(
border: Border.all(color: textColor, width: 0.5),
),
'tr': Style(
border: Border.all(color: textColor, width: 0.5),
),
'td': Style(
border: Border.all(color: textColor, width: 0.5),
padding: HtmlPaddings.all(2),
),
'th': Style(
border: Border.all(color: textColor, width: 0.5),
),
shrinkToFit: true,
maxLines: maxLines,
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
getMxcUrl: (
String mxc,
double? width,
double? height, {
bool? animated = false,
}) {
final ratio = MediaQuery.of(context).devicePixelRatio;
return Uri.parse(mxc)
.getThumbnail(
matrix.client,
width: (width ?? 800) * ratio,
height: (height ?? 800) * ratio,
method: ThumbnailMethod.scale,
animated: AppConfig.autoplayImages ? animated : false,
)
.toString();
},
extensions: [
RoomPillExtension(context, room),
CodeExtension(fontSize: fontSize),
MatrixMathExtension(
style: TextStyle(fontSize: fontSize, color: textColor),
),
const TableHtmlExtension(),
SpoilerExtension(textColor: textColor),
const ImageExtension(),
FontColorExtension(),
],
onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(),
onlyRenderTheseTags: const {
...allowedHtmlTags,
// Needed to make it work properly
'body',
'html',
onImageTap: (url) => UrlLauncher(context, url).launchUrl(),
setCodeLanguage: (String key, String value) async {
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
},
getCodeLanguage: (String key) async {
return await matrix.store.getItem('${SettingKeys.codeLanguage}.$key');
},
getPillInfo: (String url) async {
final identityParts = url.parseIdentifierIntoParts();
final identifier = identityParts?.primaryIdentifier;
if (identifier == null) {
return {};
}
if (identifier.sigil == '@') {
// we have a user pill
final user = room.getState('m.room.member', identifier);
if (user != null) {
return user.content;
}
// there might still be a profile...
final profile = await room.client.getProfileFromUserId(identifier);
return {
'displayname': profile.displayName,
'avatar_url': profile.avatarUrl.toString(),
};
}
if (identifier.sigil == '#') {
// we have an alias pill
for (final r in room.client.rooms) {
final state = r.getState('m.room.canonical_alias');
if (state != null &&
((state.content['alias'] is String &&
state.content['alias'] == identifier) ||
(state.content['alt_aliases'] is List &&
state.content['alt_aliases'].contains(identifier)))) {
// we have a room!
return {
'displayname':
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
'avatar_url': r.getState('m.room.avatar')?.content['url'],
};
}
}
return {};
}
if (identifier.sigil == '!') {
// we have a room ID pill
final r = room.client.getRoomById(identifier);
if (r == null) {
return {};
}
return {
'displayname':
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
'avatar_url': r.getState('m.room.avatar')?.content['url'],
};
}
return {};
},
shrinkWrap: true,
);
}
/// Keep in sync with: https://spec.matrix.org/v1.6/client-server-api/#mroommessage-msgtypes
static const Set<String> allowedHtmlTags = {
'font',
'del',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'p',
'a',
'ul',
'ol',
'sup',
'sub',
'li',
'b',
'i',
'u',
'strong',
'em',
'strike',
'code',
'hr',
'br',
'div',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'caption',
'pre',
'span',
'img',
'details',
'summary',
// Not in the allowlist of the matrix spec yet but should be harmless:
'ruby',
'rp',
'rt',
};
}
class FontColorExtension extends HtmlExtension {
static const String colorAttribute = 'color';
static const String mxColorAttribute = 'data-mx-color';
static const String bgColorAttribute = 'data-mx-bg-color';
@override
Set<String> get supportedTags => {'font', 'span'};
@override
bool matches(ExtensionContext context) {
if (!supportedTags.contains(context.elementName)) return false;
return context.element?.attributes.keys.any(
{
colorAttribute,
mxColorAttribute,
bgColorAttribute,
}.contains,
) ??
false;
}
Color? hexToColor(String? hexCode) {
if (hexCode == null) return null;
if (hexCode.startsWith('#')) hexCode = hexCode.substring(1);
if (hexCode.length == 6) hexCode = 'FF$hexCode';
final colorValue = int.tryParse(hexCode, radix: 16);
return colorValue == null ? null : Color(colorValue);
}
@override
InlineSpan build(
ExtensionContext context,
) {
final colorText = context.element?.attributes[colorAttribute] ??
context.element?.attributes[mxColorAttribute];
final bgColor = context.element?.attributes[bgColorAttribute];
return TextSpan(
style: TextStyle(
color: hexToColor(colorText),
backgroundColor: hexToColor(bgColor),
),
text: context.innerHtml,
);
}
}
class ImageExtension extends HtmlExtension {
final double defaultDimension;
const ImageExtension({this.defaultDimension = 64});
@override
Set<String> get supportedTags => {'img'};
@override
InlineSpan build(ExtensionContext context) {
final mxcUrl = Uri.tryParse(context.attributes['src'] ?? '');
if (mxcUrl == null || mxcUrl.scheme != 'mxc') {
return TextSpan(text: context.attributes['alt']);
}
final width = double.tryParse(context.attributes['width'] ?? '');
final height = double.tryParse(context.attributes['height'] ?? '');
return WidgetSpan(
child: SizedBox(
width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension,
child: MxcImage(
uri: mxcUrl,
width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension,
cacheKey: mxcUrl.toString(),
),
),
);
}
}
class SpoilerExtension extends HtmlExtension {
final Color textColor;
const SpoilerExtension({required this.textColor});
@override
Set<String> get supportedTags => {'span'};
static const String customDataAttribute = 'data-mx-spoiler';
@override
bool matches(ExtensionContext context) {
if (context.elementName != 'span') return false;
return context.element?.attributes.containsKey(customDataAttribute) ??
false;
}
@override
InlineSpan build(ExtensionContext context) {
var obscure = true;
final children = context.inlineSpanChildren;
return WidgetSpan(
child: StatefulBuilder(
builder: (context, setState) {
return InkWell(
onTap: () => setState(() {
obscure = !obscure;
}),
child: RichText(
text: TextSpan(
style: obscure ? TextStyle(backgroundColor: textColor) : null,
children: children,
),
),
);
},
),
);
}
}
class MatrixMathExtension extends HtmlExtension {
final TextStyle? style;
MatrixMathExtension({this.style});
@override
Set<String> get supportedTags => {'div'};
@override
bool matches(ExtensionContext context) {
if (context.elementName != 'div') return false;
final mathData = context.element?.attributes['data-mx-maths'];
return mathData != null;
}
@override
InlineSpan build(ExtensionContext context) {
final data = context.element?.attributes['data-mx-maths'] ?? '';
return WidgetSpan(
child: Math.tex(
data,
textStyle: style,
onErrorFallback: (e) {
Logs().d('Flutter math parse error', e);
return Text(
data,
style: style,
);
},
),
);
}
}
class CodeExtension extends HtmlExtension {
final double fontSize;
CodeExtension({required this.fontSize});
@override
Set<String> get supportedTags => {'code'};
@override
InlineSpan build(ExtensionContext context) => WidgetSpan(
child: Material(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: HighlightView(
context.element?.text ?? '',
language: context.element?.className
.split(' ')
.singleWhereOrNull(
(className) => className.startsWith('language-'),
)
?.split('language-')
.last ??
'md',
theme: shadesOfPurpleTheme,
padding: EdgeInsets.symmetric(
horizontal: 6,
vertical: context.element?.parent?.localName == 'pre' ? 6 : 0,
),
textStyle: TextStyle(fontSize: fontSize),
),
),
),
);
}
class RoomPillExtension extends HtmlExtension {
final Room room;
final BuildContext context;
RoomPillExtension(this.context, this.room);
@override
Set<String> get supportedTags => {'a'};
@override
bool matches(ExtensionContext context) {
if (context.elementName != 'a') return false;
final userId = context.element?.attributes['href']
?.parseIdentifierIntoParts()
?.primaryIdentifier;
return userId != null;
}
static final _cachedUsers = <String, User?>{};
Future<User?> _fetchUser(String matrixId) async =>
_cachedUsers[room.id + matrixId] ??= await room.requestUser(matrixId);
@override
InlineSpan build(ExtensionContext context) {
final href = context.element?.attributes['href'];
final matrixId = href?.parseIdentifierIntoParts()?.primaryIdentifier;
if (href == null || matrixId == null) {
return TextSpan(text: context.innerHtml);
}
if (matrixId.sigil == '@') {
return WidgetSpan(
child: FutureBuilder<User?>(
future: _fetchUser(matrixId),
builder: (context, snapshot) => MatrixPill(
key: Key('user_pill_$matrixId'),
name: _cachedUsers[room.id + matrixId]?.calcDisplayname() ??
matrixId.localpart ??
matrixId,
avatar: _cachedUsers[room.id + matrixId]?.avatarUrl,
uri: href,
outerContext: this.context,
),
),
);
}
if (matrixId.sigil == '#' || matrixId.sigil == '!') {
final room = matrixId.sigil == '!'
? this.room.client.getRoomById(matrixId)
: this.room.client.getRoomByAlias(matrixId);
if (room != null) {
return WidgetSpan(
child: MatrixPill(
name: room.getLocalizedDisplayname(),
avatar: room.avatar,
uri: href,
outerContext: this.context,
),
);
}
}
return TextSpan(text: context.innerHtml);
}
}
class MatrixPill extends StatelessWidget {
final String name;
final BuildContext outerContext;
final Uri? avatar;
final String uri;
const MatrixPill({
super.key,
required this.name,
required this.outerContext,
this.avatar,
required this.uri,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: UrlLauncher(outerContext, uri).launchUrl,
child: Material(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
side: BorderSide(
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
width: 0.5,
),
),
color: Theme.of(outerContext).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
mxContent: avatar,
name: name,
size: 16,
),
const SizedBox(width: 6),
Text(
name,
style: TextStyle(
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
),
),
],
),
),
),
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
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';
@ -150,10 +150,23 @@ class MessageContent extends StatelessWidget {
if (event.messageType == MessageTypes.Emote) {
html = '* $html';
}
final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 10;
return HtmlMessage(
html: html,
textColor: textColor,
defaultTextStyle: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
decorationColor: textColor.withAlpha(150),
),
room: event.room,
emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5,
);
}
// else we fall through to the normal message rendering
@ -229,26 +242,25 @@ class MessageContent extends StatelessWidget {
hideReply: true,
),
builder: (context, snapshot) {
return Linkify(
return LinkText(
text: snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
),
style: TextStyle(
textStyle: TextStyle(
color: textColor,
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
decorationColor: textColor.withAlpha(150),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
);
},
);

View File

@ -37,17 +37,14 @@ class MessageDownloadContent extends StatelessWidget {
color: textColor,
),
const SizedBox(width: 16),
Flexible(
child: Text(
filename,
maxLines: 1,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
Text(
filename,
maxLines: 1,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
)
),
],
),
),

View File

@ -61,7 +61,7 @@ class MessageReactions extends StatelessWidget {
final evt = allReactionEvents.firstWhereOrNull(
(e) =>
e.senderId == e.room.client.userID &&
e.content.tryGetMap('m.relates_to')?['key'] == r.key,
e.content['m.relates_to']['key'] == r.key,
);
if (evt != null) {
showFutureLoadingDialog(

View File

@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import '../../../config/app_config.dart';
import 'html_message.dart';
class ReplyContent extends StatelessWidget {
final Event replyEvent;
@ -25,23 +26,47 @@ class ReplyContent extends StatelessWidget {
final displayEvent =
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
replyBody = Text(
displayEvent.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false,
hideReply: true,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground,
fontSize: fontSize,
),
);
if (AppConfig.renderHtml &&
[EventTypes.Message, EventTypes.Encrypted]
.contains(displayEvent.type) &&
[MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote]
.contains(displayEvent.messageType) &&
!displayEvent.redacted &&
displayEvent.content['format'] == 'org.matrix.custom.html' &&
displayEvent.content['formatted_body'] is String) {
String? html = displayEvent.content['formatted_body'];
if (displayEvent.messageType == MessageTypes.Emote) {
html = '* $html';
}
replyBody = HtmlMessage(
html: html!,
defaultTextStyle: TextStyle(
color: ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground,
fontSize: fontSize,
),
maxLines: 1,
room: displayEvent.room,
emoteSize: fontSize * 1.5,
);
} else {
replyBody = Text(
displayEvent.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false,
hideReply: true,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: ownMessage
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground,
fontSize: fontSize,
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[

View File

@ -14,7 +14,6 @@ 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/event_extension.dart';
import '../../../utils/error_reporter.dart';
class EventVideoPlayer extends StatefulWidget {
final Event event;
@ -52,8 +51,7 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
final networkUri = _networkUri;
if (kIsWeb && networkUri != null && _chewieManager == null) {
_chewieManager ??= ChewieController(
videoPlayerController:
VideoPlayerController.networkUrl(Uri.parse(networkUri)),
videoPlayerController: VideoPlayerController.network(networkUri),
autoPlay: true,
autoInitialize: true,
);
@ -71,7 +69,12 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
),
);
} catch (e, s) {
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toLocalizedString(context)),
),
);
Logs().w('Error while playing video', e, s);
} finally {
// Workaround for Chewie needs time to get the aspectRatio
await Future.delayed(const Duration(milliseconds: 100));

View File

@ -183,13 +183,12 @@ class InputBar extends StatelessWidget {
final state = r.getState(EventTypes.RoomCanonicalAlias);
if ((state != null &&
((state.content['alias'] is String &&
state.content
.tryGet<String>('alias')!
state.content['alias']
.split(':')[0]
.toLowerCase()
.contains(roomSearch)) ||
(state.content['alt_aliases'] is List &&
(state.content['alt_aliases'] as List).any(
state.content['alt_aliases'].any(
(l) =>
l is String &&
l
@ -264,8 +263,6 @@ class InputBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
MxcImage(
// ensure proper ordering ...
key: ValueKey(suggestion['name']),
uri: suggestion['mxc'] is String
? Uri.parse(suggestion['mxc'] ?? '')
: null,

View File

@ -4,8 +4,8 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
@ -101,16 +101,15 @@ class PinnedEvents extends StatelessWidget {
hideReply: true,
),
builder: (context, snapshot) {
return Linkify(
return LinkText(
text: snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true,
hideReply: true,
),
options: const LinkifyOptions(humanize: false),
maxLines: 2,
style: TextStyle(
textStyle: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
overflow: TextOverflow.ellipsis,
@ -127,8 +126,8 @@ class PinnedEvents extends StatelessWidget {
decorationColor:
Theme.of(context).colorScheme.onSurfaceVariant,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
);
},
),

View File

@ -52,7 +52,7 @@ class ReactionsPicker extends StatelessWidget {
for (final event in allReactionEvents) {
try {
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
emojis.remove(event.content['m.relates_to']['key']);
} catch (_) {}
}
return Row(

View File

@ -83,9 +83,7 @@ class ChatDetailsController extends State<ChatDetails> {
RequestType.GET,
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
)
.then(
(response) => List<String>.from(response['aliases'] as Iterable),
),
.then((response) => List<String>.from(response['aliases'])),
);
// Switch to the stable api once it is implemented.

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
@ -125,14 +125,13 @@ class ChatDetailsView extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Linkify(
child: LinkText(
text: room.topic.isEmpty
? L10n.of(context)!.addGroupDescription
: room.topic,
options: const LinkifyOptions(humanize: false),
linkStyle:
const TextStyle(color: Colors.blueAccent),
style: TextStyle(
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
@ -143,8 +142,8 @@ class ChatDetailsView extends StatelessWidget {
.bodyMedium!
.color,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
),
const SizedBox(height: 8),

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -30,7 +29,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
onPressed: () =>
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
),
title: Text(L10n.of(context)!.encryption),
title: Text(L10n.of(context)!.endToEndEncryption),
actions: [
TextButton(
onPressed: () => launchUrlString(AppConfig.encryptionTutorial),
@ -51,12 +50,13 @@ class ChatEncryptionSettingsView extends StatelessWidget {
value: room.encrypted,
onChanged: controller.enableEncryption,
),
Icon(
CupertinoIcons.lock_shield,
size: 128,
color: Theme.of(context).colorScheme.onInverseSurface,
Center(
child: Image.asset(
'assets/encryption.png',
width: 212,
),
),
const Divider(),
const Divider(height: 1),
if (room.isDirectChat)
Padding(
padding: const EdgeInsets.all(16.0),

View File

@ -253,7 +253,7 @@ class ChatListController extends State<ChatList>
BoxConstraints? snappingSheetContainerSize;
final ScrollController scrollController = ScrollController();
final ValueNotifier<bool> scrolledToTop = ValueNotifier(true);
bool scrolledToTop = true;
final StreamController<Client> _clientStream = StreamController.broadcast();
@ -263,8 +263,10 @@ class ChatListController extends State<ChatList>
void _onScroll() {
final newScrolledToTop = scrollController.position.pixels <= 0;
if (newScrolledToTop != scrolledToTop.value) {
scrolledToTop.value = newScrolledToTop;
if (newScrolledToTop != scrolledToTop) {
setState(() {
scrolledToTop = newScrolledToTop;
});
}
}

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
@ -9,6 +8,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/chat_list/start_chat_fab.dart';
import 'package:fluffychat/pages/chat_list/stories_header.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
@ -32,11 +32,6 @@ class ChatListViewBody extends StatelessWidget {
final roomSearchResult = controller.roomSearchResult;
final userSearchResult = controller.userSearchResult;
final client = Matrix.of(context).client;
const dummyChatCount = 4;
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return PageTransitionSwitcher(
transitionBuilder: (
@ -70,203 +65,157 @@ class ChatListViewBody extends StatelessWidget {
key: Key(controller.activeSpaceId ?? 'Spaces'),
);
}
final rooms = controller.filteredRooms;
final displayStoriesHeader = {
ActiveFilter.allChats,
ActiveFilter.messages,
}.contains(controller.activeFilter) &&
client.storiesRooms.isNotEmpty;
return SafeArea(
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
ChatListHeader(controller: controller),
SliverList(
delegate: SliverChildListDelegate(
[
if (controller.isSearchMode) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: roomSearchResult == null ||
roomSearchResult.chunk.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: roomSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: roomSearchResult.chunk.length,
itemBuilder: (context, i) => _SearchItem(
title: roomSearchResult.chunk[i].name ??
roomSearchResult.chunk[i].canonicalAlias
?.localpart ??
L10n.of(context)!.group,
avatar: roomSearchResult.chunk[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias: roomSearchResult
.chunk[i].canonicalAlias ??
roomSearchResult.chunk[i].roomId,
outerContext: context,
chunk: roomSearchResult.chunk[i],
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult
.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar:
userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId:
userSearchResult.results[i].userId,
outerContext: context,
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
],
if (displayStoriesHeader)
StoriesHeader(
key: const Key('stories_header'),
filter: controller.searchController.text,
),
const ConnectionStatusHeader(),
AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: controller.dehydrate,
),
),
),
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
),
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[
Padding(
padding: const EdgeInsets.all(32.0),
child: Icon(
CupertinoIcons.chat_bubble_2,
size: 128,
color:
Theme.of(context).colorScheme.onInverseSurface,
),
),
],
],
),
),
if (client.prevBatch == null)
if (controller.waitForFirstSync && client.prevBatch != null) {
final rooms = controller.filteredRooms;
final displayStoriesHeader = {
ActiveFilter.allChats,
ActiveFilter.messages,
}.contains(controller.activeFilter) &&
client.storiesRooms.isNotEmpty;
return SafeArea(
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
ChatListHeader(controller: controller),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color:
Theme.of(context).textTheme.bodyLarge!.color,
),
delegate: SliverChildListDelegate(
[
if (controller.isSearchMode) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: roomSearchResult == null ||
roomSearchResult.chunk.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: roomSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: roomSearchResult.chunk.length,
itemBuilder: (context, i) => _SearchItem(
title: roomSearchResult.chunk[i].name ??
roomSearchResult.chunk[i]
.canonicalAlias?.localpart ??
L10n.of(context)!.group,
avatar:
roomSearchResult.chunk[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias: roomSearchResult
.chunk[i].canonicalAlias ??
roomSearchResult.chunk[i].roomId,
outerContext: context,
chunk: roomSearchResult.chunk[i],
),
),
),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult
.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar:
userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: userSearchResult
.results[i].userId,
outerContext: context,
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
],
if (displayStoriesHeader)
StoriesHeader(
key: const Key('stories_header'),
filter: controller.searchController.text,
),
const ConnectionStatusHeader(),
AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle:
Text(L10n.of(context)!.dehydrateTorLong),
trailing:
const Icon(Icons.chevron_right_outlined),
onTap: controller.dehydrate,
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
childCount: dummyChatCount,
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
),
if (rooms.isEmpty && !controller.isSearchMode) ...[
Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/start_chat.png',
height: 256,
),
const Divider(height: 1),
],
),
),
Center(
child: StartChatFloatingActionButton(
activeFilter: controller.activeFilter,
roomsIsEmpty: true,
scrolledToTop: controller.scrolledToTop,
),
),
],
],
),
),
if (client.prevBatch != null)
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
@ -296,7 +245,68 @@ class ChatListViewBody extends StatelessWidget {
childCount: rooms.length,
),
),
],
],
),
);
}
const dummyChatCount = 5;
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return ListView.builder(
key: const Key('dummychats'),
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
);
},

View File

@ -192,6 +192,7 @@ class ChatListView extends StatelessWidget {
VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child: selectMode == SelectMode.normal &&
controller.filteredRooms.isNotEmpty &&
!controller.isSearchMode
? StartChatFloatingActionButton(
activeFilter: controller.activeFilter,

View File

@ -8,7 +8,7 @@ import 'chat_list.dart';
class StartChatFloatingActionButton extends StatelessWidget {
final ActiveFilter activeFilter;
final ValueNotifier<bool> scrolledToTop;
final bool scrolledToTop;
final bool roomsIsEmpty;
const StartChatFloatingActionButton({
@ -61,26 +61,27 @@ class StartChatFloatingActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: scrolledToTop,
builder: (context, scrolledToTop, _) => AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.none,
child: scrolledToTop
? FloatingActionButton.extended(
onPressed: () => _onPressed(context),
icon: Icon(icon),
label: Text(
getLabel(context),
overflow: TextOverflow.fade,
),
)
: FloatingActionButton(
onPressed: () => _onPressed(context),
child: Icon(icon),
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: roomsIsEmpty
? null
: scrolledToTop
? 144
: 56,
child: scrolledToTop
? FloatingActionButton.extended(
onPressed: () => _onPressed(context),
icon: Icon(icon),
label: Text(
getLabel(context),
overflow: TextOverflow.fade,
),
),
)
: FloatingActionButton(
onPressed: () => _onPressed(context),
child: Icon(icon),
),
);
}
}

View File

@ -73,10 +73,8 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
void updateRoomAction(Capabilities capabilities) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final roomVersion = room
.getState(EventTypes.RoomCreate)!
.content['room_version'] as String? ??
'1';
final String roomVersion =
room.getState(EventTypes.RoomCreate)!.content['room_version'] ?? '1';
final newVersion = await showConfirmationDialog<String>(
context: context,
title: L10n.of(context)!.replaceRoomWithNewerVersion,

View File

@ -127,9 +127,9 @@ class ChatPermissionsSettingsView extends StatelessWidget {
),
);
}
final roomVersion = room
final String roomVersion = room
.getState(EventTypes.RoomCreate)!
.content['room_version'] as String? ??
.content['room_version'] ??
'1';
return ListTile(

View File

@ -0,0 +1,197 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/connect/connect_page_view.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ConnectPage extends StatefulWidget {
const ConnectPage({Key? key}) : super(key: key);
@override
State<ConnectPage> createState() => ConnectPageController();
}
class ConnectPageController extends State<ConnectPage> {
final TextEditingController usernameController = TextEditingController();
String? signupError;
bool loading = false;
void pickAvatar() async {
final source = !PlatformInfos.isMobile
? ImageSource.gallery
: await showModalActionSheet<ImageSource>(
context: context,
title: L10n.of(context)!.changeYourAvatar,
actions: [
SheetAction(
key: ImageSource.camera,
label: L10n.of(context)!.openCamera,
isDefaultAction: true,
icon: Icons.camera_alt_outlined,
),
SheetAction(
key: ImageSource.gallery,
label: L10n.of(context)!.openGallery,
icon: Icons.photo_outlined,
),
],
);
if (source == null) return;
final picked = await ImagePicker().pickImage(
source: source,
imageQuality: 50,
maxWidth: 512,
maxHeight: 512,
);
setState(() {
Matrix.of(context).loginAvatar = picked;
});
}
void signUp() async {
usernameController.text = usernameController.text.trim();
final localpart =
usernameController.text.toLowerCase().replaceAll(' ', '_');
if (localpart.isEmpty) {
setState(() {
signupError = L10n.of(context)!.pleaseChooseAUsername;
});
return;
}
setState(() {
signupError = null;
loading = true;
});
try {
try {
await Matrix.of(context).getLoginClient().register(username: localpart);
} on MatrixException catch (e) {
if (!e.requireAdditionalAuthentication) rethrow;
}
setState(() {
loading = false;
});
Matrix.of(context).loginUsername = usernameController.text;
VRouter.of(context).to('signup');
} catch (e, s) {
Logs().d('Sign up failed', e, s);
setState(() {
signupError = e.toLocalizedString(context);
loading = false;
});
}
}
bool _supportsFlow(String flowType) =>
Matrix.of(context)
.loginHomeserverSummary
?.loginFlows
.any((flow) => flow.type == flowType) ??
false;
bool get supportsSso => _supportsFlow('m.login.sso');
bool isDefaultPlatform =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
bool get supportsLogin => _supportsFlow('m.login.password');
void login() => VRouter.of(context).to('login');
Map<String, dynamic>? _rawLoginTypes;
List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null;
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
(flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'];
final list = (rawProviders as List)
.map((json) => IdentityProvider.fromJson(json))
.toList();
if (PlatformInfos.isCupertinoStyle) {
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
}
return list;
}
void ssoLoginAction(String id) async {
final redirectUrl = kIsWeb
? '${html.window.origin!}/web/auth.html'
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url =
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
final result = await FlutterWebAuth2.authenticate(
url: url,
callbackUrlScheme: urlScheme,
);
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).getLoginClient().login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
}
@override
void initState() {
super.initState();
if (supportsSso) {
Matrix.of(context)
.getLoginClient()
.request(
RequestType.GET,
'/client/r0/login',
)
.then(
(loginTypes) => setState(() {
_rawLoginTypes = loginTypes;
}),
);
}
}
@override
Widget build(BuildContext context) => ConnectPageView(this);
}
class IdentityProvider {
final String? id;
final String? name;
final String? icon;
final String? brand;
IdentityProvider({this.id, this.name, this.icon, this.brand});
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
IdentityProvider(
id: json['id'],
name: json['name'],
icon: json['icon'],
brand: json['brand'],
);
}

View File

@ -0,0 +1,226 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'sso_button.dart';
class ConnectPageView extends StatelessWidget {
final ConnectPageController controller;
const ConnectPageView(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final avatar = Matrix.of(context).loginAvatar;
final identityProviders = controller.identityProviders;
return LoginScaffold(
appBar: AppBar(
leading: controller.loading ? null : const BackButton(),
automaticallyImplyLeading: !controller.loading,
centerTitle: true,
title: Text(
Matrix.of(context).getLoginClient().homeserver?.host ?? '',
),
),
body: ListView(
key: const Key('ConnectPageListView'),
children: [
if (Matrix.of(context).loginRegistrationSupported ?? false) ...[
Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: Stack(
children: [
Material(
borderRadius: BorderRadius.circular(64),
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
10,
color: Colors.transparent,
shadowColor: Theme.of(context)
.colorScheme
.onBackground
.withAlpha(64),
clipBehavior: Clip.hardEdge,
child: CircleAvatar(
radius: 64,
backgroundColor: Colors.white,
child: avatar == null
? const Icon(
Icons.person,
color: Colors.black,
size: 64,
)
: FutureBuilder<Uint8List>(
future: avatar.readAsBytes(),
builder: (context, snapshot) {
final bytes = snapshot.data;
if (bytes == null) {
return const CircularProgressIndicator
.adaptive();
}
return Image.memory(
bytes,
fit: BoxFit.cover,
width: 128,
height: 128,
);
},
),
),
),
Positioned(
bottom: 0,
right: 0,
child: FloatingActionButton(
mini: true,
onPressed: controller.pickAvatar,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
child: const Icon(Icons.camera_alt_outlined),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: controller.usernameController,
onSubmitted: (_) => controller.signUp(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_box_outlined),
hintText: L10n.of(context)!.chooseAUsername,
errorText: controller.signupError,
errorStyle: const TextStyle(color: Colors.orange),
),
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: 'loginButton',
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading ? () {} : controller.signUp,
icon: const Icon(Icons.person_add_outlined),
label: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.signUp),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
],
if (controller.supportsSso)
identityProviders == null
? const SizedBox(
height: 74,
child: Center(child: CircularProgressIndicator.adaptive()),
)
: Center(
child: identityProviders.length == 1
? Container(
width: double.infinity,
padding: const EdgeInsets.all(12.0),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
icon: identityProviders.single.icon == null
? const Icon(
Icons.web_outlined,
size: 16,
)
: Image.network(
Uri.parse(identityProviders.single.icon!)
.getDownloadLink(
Matrix.of(context).getLoginClient(),
)
.toString(),
width: 32,
height: 32,
),
onPressed: () => controller
.ssoLoginAction(identityProviders.single.id!),
label: Text(
identityProviders.single.name ??
identityProviders.single.brand ??
L10n.of(context)!.loginWithOneClick,
),
),
)
: Wrap(
children: [
for (final identityProvider in identityProviders)
SsoButton(
onPressed: () => controller
.ssoLoginAction(identityProvider.id!),
identityProvider: identityProvider,
),
].toList(),
),
),
if (controller.supportsLogin)
Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: 'signinButton',
child: ElevatedButton.icon(
icon: const Icon(Icons.login_outlined),
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
),
onPressed: controller.loading ? () {} : controller.login,
label: Text(L10n.of(context)!.login),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/connect/connect_page.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SsoButton extends StatelessWidget {
final IdentityProvider identityProvider;
final void Function()? onPressed;
const SsoButton({
Key? key,
required this.identityProvider,
this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(7),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: identityProvider.icon == null
? const Icon(Icons.web_outlined)
: Image.network(
Uri.parse(identityProvider.icon!)
.getDownloadLink(
Matrix.of(context).getLoginClient(),
)
.toString(),
width: 32,
height: 32,
),
),
),
const SizedBox(height: 8),
Text(
identityProvider.name ??
identityProvider.brand ??
L10n.of(context)!.singlesignon,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:fluffychat/config/app_config.dart';
import 'homeserver_bottom_sheet.dart';
import 'homeserver_picker.dart';
class HomeserverAppBar extends StatelessWidget {
@ -16,57 +13,25 @@ class HomeserverAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TypeAheadField<HomeserverBenchmarkResult>(
suggestionsBoxDecoration: SuggestionsBoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
constraints: const BoxConstraints(maxHeight: 256),
),
itemBuilder: (context, homeserver) => ListTile(
title: Text(homeserver.homeserver.baseUrl.toString()),
subtitle: Text(homeserver.homeserver.description ?? ''),
trailing: IconButton(
icon: const Icon(Icons.info_outlined),
onPressed: () => showModalBottomSheet(
context: context,
builder: (_) => HomeserverBottomSheet(
homeserver: homeserver,
),
),
),
),
suggestionsCallback: (pattern) async {
final homeserverList =
await const JoinmatrixOrgParser().fetchHomeservers();
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
homeserverList,
timeout: const Duration(seconds: 3),
);
return benchmark;
},
onSuggestionSelected: (suggestion) {
controller.homeserverController.text =
suggestion.homeserver.baseUrl.host;
controller.checkHomeserverAction();
},
textFieldConfiguration: TextFieldConfiguration(
controller: controller.homeserverController,
decoration: InputDecoration(
prefixIcon: Navigator.of(context).canPop()
? IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.arrow_back),
)
: null,
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
),
textInputAction: TextInputAction.search,
onSubmitted: controller.checkHomeserverAction,
autocorrect: false,
return TextField(
focusNode: controller.homeserverFocusNode,
controller: controller.homeserverController,
onChanged: controller.onChanged,
decoration: InputDecoration(
prefixIcon: Navigator.of(context).canPop()
? IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.arrow_back),
)
: null,
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
errorText: controller.error,
),
readOnly: !AppConfig.allowOtherHomeservers,
onSubmitted: (_) => controller.checkHomeserverAction(),
autocorrect: false,
);
}
}

View File

@ -7,16 +7,16 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html;
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_bottom_sheet.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/localized_exception_extension.dart';
@ -35,8 +35,14 @@ class HomeserverPickerController extends State<HomeserverPicker> {
final TextEditingController homeserverController = TextEditingController(
text: AppConfig.defaultHomeserver,
);
final FocusNode homeserverFocusNode = FocusNode();
String? error;
List<HomeserverBenchmarkResult>? benchmarkResults;
bool displayServerList = false;
bool get loadingHomeservers =>
AppConfig.allowOtherHomeservers && benchmarkResults == null;
String searchTerm = '';
bool isTorBrowser = false;
@ -59,34 +65,98 @@ class HomeserverPickerController extends State<HomeserverPicker> {
isTorBrowser = isTor;
}
String? _lastCheckedUrl;
void _updateFocus() {
if (benchmarkResults == null) _loadHomeserverList();
if (homeserverFocusNode.hasFocus) {
setState(() {
displayServerList = true;
});
}
}
void showServerInfo(HomeserverBenchmarkResult server) =>
showAdaptiveBottomSheet(
context: context,
builder: (_) => HomeserverBottomSheet(
homeserver: server,
),
);
void onChanged(String text) => setState(() {
searchTerm = text;
});
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
.where(
(element) =>
element.homeserver.baseUrl.host.contains(searchTerm) ||
(element.homeserver.description?.contains(searchTerm) ?? false),
)
.toList();
void _loadHomeserverList() async {
try {
final homeserverList =
await const JoinmatrixOrgParser().fetchHomeservers();
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
homeserverList,
timeout: const Duration(seconds: 10),
);
if (!mounted) return;
setState(() {
benchmarkResults = benchmark;
});
} catch (e, s) {
Logs().e('Homeserver benchmark failed', e, s);
benchmarkResults = [];
}
}
void setServer(String server) => setState(() {
homeserverController.text = server;
searchTerm = '';
homeserverFocusNode.unfocus();
displayServerList = false;
});
/// Starts an analysis of the given homeserver. It uses the current domain and
/// makes sure that it is prefixed with https. Then it searches for the
/// well-known information and forwards to the login page depending on the
/// login type.
Future<void> checkHomeserverAction([_]) async {
homeserverController.text =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
if (homeserverController.text == _lastCheckedUrl) return;
_lastCheckedUrl = homeserverController.text;
Future<void> checkHomeserverAction() async {
setState(() {
error = _rawLoginTypes = loginHomeserverSummary = null;
homeserverFocusNode.unfocus();
error = null;
isLoading = true;
searchTerm = '';
displayServerList = false;
});
try {
homeserverController.text =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
var homeserver = Uri.parse(homeserverController.text);
if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeserverController.text, '');
}
final client = Matrix.of(context).getLoginClient();
loginHomeserverSummary = await client.checkHomeserver(homeserver);
if (supportsSso) {
_rawLoginTypes = await client.request(
RequestType.GET,
'/client/r0/login',
);
final matrix = Matrix.of(context);
matrix.loginHomeserverSummary =
await matrix.getLoginClient().checkHomeserver(homeserver);
final ssoSupported = matrix.loginHomeserverSummary!.loginFlows
.any((flow) => flow.type == 'm.login.sso');
try {
await Matrix.of(context).getLoginClient().register();
matrix.loginRegistrationSupported = true;
} on MatrixException catch (e) {
matrix.loginRegistrationSupported = e.requireAdditionalAuthentication;
}
if (!ssoSupported && matrix.loginRegistrationSupported == false) {
// Server does not support SSO or registration. We can skip to login page:
VRouter.of(context).to('login');
} else {
VRouter.of(context).to('connect');
}
} catch (e) {
setState(() => error = (e).toLocalizedString(context));
@ -97,71 +167,17 @@ class HomeserverPickerController extends State<HomeserverPicker> {
}
}
HomeserverSummary? loginHomeserverSummary;
bool _supportsFlow(String flowType) =>
loginHomeserverSummary?.loginFlows.any((flow) => flow.type == flowType) ??
false;
bool get supportsSso => _supportsFlow('m.login.sso');
bool isDefaultPlatform =
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
bool get supportsPasswordLogin => _supportsFlow('m.login.password');
Map<String, dynamic>? _rawLoginTypes;
void ssoLoginAction(String id) async {
final redirectUrl = kIsWeb
? '${html.window.origin!}/web/auth.html'
: isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login';
final url =
'${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}';
final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme
: "http://localhost:3001";
final result = await FlutterWebAuth2.authenticate(
url: url,
callbackUrlScheme: urlScheme,
);
final token = Uri.parse(result).queryParameters['loginToken'];
if (token?.isEmpty ?? false) return;
await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).getLoginClient().login(
LoginType.mLoginToken,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
@override
void dispose() {
homeserverFocusNode.removeListener(_updateFocus);
super.dispose();
}
List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null;
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
(flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'];
final list = (rawProviders as List)
.map((json) => IdentityProvider.fromJson(json))
.toList();
if (PlatformInfos.isCupertinoStyle) {
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
}
return list;
}
void login() => VRouter.of(context).to('login');
@override
void initState() {
homeserverFocusNode.addListener(_updateFocus);
_checkTorBrowser();
super.initState();
WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction);
}
@override
@ -188,20 +204,3 @@ class HomeserverPickerController extends State<HomeserverPicker> {
);
}
}
class IdentityProvider {
final String? id;
final String? name;
final String? icon;
final String? brand;
IdentityProvider({this.id, this.name, this.icon, this.brand});
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
IdentityProvider(
id: json['id'],
name: json['name'],
icon: json['icon'],
brand: json['brand'],
);
}

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import '../../config/themes.dart';
import '../../widgets/mxc_image.dart';
import 'homeserver_app_bar.dart';
import 'homeserver_picker.dart';
@ -16,19 +16,15 @@ class HomeserverPickerView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final identityProviders = controller.identityProviders;
final errorText = controller.error;
final benchmarkResults = controller.benchmarkResults;
return LoginScaffold(
appBar: AppBar(
titleSpacing: 12,
title: Padding(
padding: const EdgeInsets.all(0.0),
child: HomeserverAppBar(controller: controller),
),
),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: HomeserverAppBar(controller: controller),
),
// display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are
// usually forced to logout as TOR browser is non-persistent
@ -53,153 +49,112 @@ class HomeserverPickerView extends StatelessWidget {
),
),
Expanded(
child: controller.isLoading
? const Center(child: CircularProgressIndicator.adaptive())
: ListView(
child: controller.displayServerList
? ListView(
children: [
Image.asset(
'assets/info-logo.png',
height: 96,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 1,
height: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.continueWith,
style: const TextStyle(fontSize: 12),
),
),
Expanded(
child: Divider(
thickness: 1,
height: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
if (errorText != null) ...[
const Center(
child: Icon(
Icons.error_outline,
size: 48,
color: Colors.orange,
),
),
const SizedBox(height: 12),
Center(
child: Text(
errorText,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 18,
),
),
),
Center(
child: Text(
L10n.of(context)!
.pleaseTryAgainLaterOrChooseDifferentServer,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
const SizedBox(height: 12),
],
if (identityProviders != null) ...[
...identityProviders.map(
(provider) => _LoginButton(
icon: provider.icon == null
? const Icon(Icons.open_in_new_outlined)
: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: MxcImage(
placeholder: (_) =>
const Icon(Icons.web_outlined),
uri: Uri.parse(provider.icon!),
width: 24,
height: 24,
if (controller.displayServerList)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
color: Theme.of(context)
.colorScheme
.onInverseSurface,
clipBehavior: Clip.hardEdge,
child: benchmarkResults == null
? const Center(
child: Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator
.adaptive(),
),
)
: Column(
children: controller.filteredHomeservers
.map(
(server) => ListTile(
trailing: IconButton(
icon: const Icon(
Icons.info_outlined,
color: Colors.black,
),
onPressed: () => controller
.showServerInfo(server),
),
onTap: () => controller.setServer(
server.homeserver.baseUrl.host,
),
title: Text(
server.homeserver.baseUrl.host,
style: const TextStyle(
color: Colors.black,
),
),
subtitle: Text(
server.homeserver.description ??
'',
style: TextStyle(
color: Colors.grey.shade700,
),
),
),
)
.toList(),
),
label: provider.name ??
provider.brand ??
L10n.of(context)!.singlesignon,
onPressed: () =>
controller.ssoLoginAction(provider.id!),
),
),
],
if (controller.supportsPasswordLogin)
_LoginButton(
onPressed: controller.login,
icon: const Icon(Icons.login_outlined),
label: L10n.of(context)!.signInWithPassword,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
),
),
],
)
: Container(
alignment: Alignment.topCenter,
child: Image.asset(
'assets/banner_transparent.png',
filterQuality: FilterQuality.medium,
),
),
),
SafeArea(
child: Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextButton(
onPressed: () => launchUrlString(AppConfig.privacyUrl),
child: Text(L10n.of(context)!.privacy),
),
TextButton(
onPressed: controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
),
Hero(
tag: 'loginButton',
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primary,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.isLoading
? null
: controller.checkHomeserverAction,
icon: const Icon(Icons.start_outlined),
label: controller.isLoading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.letsStart),
),
),
],
),
),
),
],
),
),
);
}
}
class _LoginButton extends StatelessWidget {
final Widget icon;
final String label;
final void Function() onPressed;
const _LoginButton({
required this.icon,
required this.label,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
margin: const EdgeInsets.only(bottom: 16),
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: icon,
label: Text(label),
),
);
}
}

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